Compare commits
49
Commits
a636eca3bd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7191591648 | ||
|
|
b2c45e821c | ||
|
|
1e497d5884 | ||
|
|
3761ae2f2f | ||
|
|
3871bb6cd2 | ||
|
|
f8711dd464 | ||
|
|
1b63d808f5 | ||
|
|
fd1c49d469 | ||
|
|
39bb53a9c0 | ||
|
|
a3f8096288 | ||
|
|
ed6d140a35 | ||
|
|
1de9431883 | ||
|
|
2876197ed9 | ||
|
|
97fe393e15 | ||
|
|
e407e5c626 | ||
|
|
3222393019 | ||
|
|
abc1e2028a | ||
|
|
911c7dd7e8 | ||
|
|
dac9449075 | ||
|
|
9835fe190f | ||
|
|
4fec80c301 | ||
|
|
e6d43d0eb5 | ||
|
|
584e1c3c72 | ||
|
|
0132492941 | ||
|
|
1a9c9e54ac | ||
|
|
28f167e074 | ||
|
|
a25c7c256e | ||
|
|
6c3a572cdd | ||
|
|
632dc9c139 | ||
|
|
f5f16ac55b | ||
|
|
caecb951d8 | ||
|
|
9f90c75f9c | ||
|
|
1b82ae24a7 | ||
|
|
da8cc01fad | ||
|
|
f0682d7de8 | ||
|
|
fe1d0e6808 | ||
|
|
5ef0578889 | ||
|
|
691a3ccb22 | ||
|
|
bc87da15b1 | ||
|
|
c8afac0f6d | ||
|
|
e8b1f765c8 | ||
|
|
7b4c8a4d27 | ||
|
|
1b93a2e2aa | ||
|
|
7e1ae9841b | ||
|
|
89bb7b4d34 | ||
|
|
4be49b0c91 | ||
|
|
5d53ec049b | ||
|
|
c8189269e6 | ||
|
|
de95532a89 |
+6
-5
@@ -1,6 +1,7 @@
|
|||||||
chan.html
|
/chan.html
|
||||||
feed.xml
|
/feed.xml
|
||||||
thumb_*.jpg
|
/thumb_*.jpg
|
||||||
avatar.jpg
|
/avatar.jpg
|
||||||
banner.jpg
|
/banner.jpg
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
public/index.html.tmp
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ FROM nginx:1.27-alpine
|
|||||||
RUN apk add --no-cache python3 py3-pillow
|
RUN apk add --no-cache python3 py3-pillow
|
||||||
COPY public/ /usr/share/nginx/html/
|
COPY public/ /usr/share/nginx/html/
|
||||||
COPY build.py /app/build.py
|
COPY build.py /app/build.py
|
||||||
|
COPY server.py /app/server.py
|
||||||
|
COPY guest_form.json /app/guest_form.json
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
"""Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor)."""
|
"""Genera index.html a partir del feed RSS del canal (se ejecuta cada 15 min en el contenedor)."""
|
||||||
import html
|
import html
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
|
from collections import Counter
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from PIL import Image, ImageStat
|
from PIL import Image
|
||||||
|
|
||||||
OUT = os.environ.get('SITE_DIR', 'public')
|
OUT = os.environ.get('SITE_DIR', 'public')
|
||||||
FEED = os.environ.get('FEED_URL', 'https://www.youtube.com/feeds/videos.xml?channel_id=UC8gVtzB3Lqm7nnfMhMvXGxQ')
|
CHANNEL = 'UC8gVtzB3Lqm7nnfMhMvXGxQ'
|
||||||
MAX_VIDEOS = 15
|
# YouTube separa cada tipo en su propia lista: UULF = solo vídeos, UUSH = solo Shorts
|
||||||
NS = {'a': 'http://www.w3.org/2005/Atom', 'y': 'http://www.youtube.com/xml/schemas/2015'}
|
FEED_VIDEOS = os.environ.get('FEED_VIDEOS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UULF{CHANNEL[2:]}')
|
||||||
|
FEED_SHORTS = os.environ.get('FEED_SHORTS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UUSH{CHANNEL[2:]}')
|
||||||
|
FEED_POP_VIDEOS = os.environ.get('FEED_POP_VIDEOS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UULP{CHANNEL[2:]}')
|
||||||
|
FEED_POP_SHORTS = os.environ.get('FEED_POP_SHORTS_URL', f'https://www.youtube.com/feeds/videos.xml?playlist_id=UUPS{CHANNEL[2:]}')
|
||||||
|
POP_VIDEOS_SHOWN = 4
|
||||||
|
POP_SHORTS_SHOWN = 6
|
||||||
|
VIDEOS_SHOWN = 8 # además del destacado
|
||||||
|
SHORTS_SHOWN = 12
|
||||||
|
NS = {'a': 'http://www.w3.org/2005/Atom', 'y': 'http://www.youtube.com/xml/schemas/2015',
|
||||||
|
'm': 'http://search.yahoo.com/mrss/'}
|
||||||
MESES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
MESES = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
||||||
YT = 'https://www.youtube.com/@andaquenosabesna'
|
YT = 'https://www.youtube.com/@andaquenosabesna'
|
||||||
TT = 'https://www.tiktok.com/@andaquenosabesna'
|
TT = 'https://www.tiktok.com/@andaquenosabesna'
|
||||||
IG = 'https://www.instagram.com/andaquenosabesna'
|
IG = 'https://www.instagram.com/andaquenosabesna'
|
||||||
MAIL = 'andaquenosabesnada@gmail.com'
|
MAIL = 'contacto@andaquenosabesna.com'
|
||||||
|
|
||||||
|
|
||||||
def fetch(url, timeout=20):
|
def fetch(url, timeout=20):
|
||||||
@@ -24,71 +37,180 @@ def fetch(url, timeout=20):
|
|||||||
return urllib.request.urlopen(req, timeout=timeout).read()
|
return urllib.request.urlopen(req, timeout=timeout).read()
|
||||||
|
|
||||||
|
|
||||||
try:
|
def read_feed(url):
|
||||||
root = ET.fromstring(fetch(FEED))
|
root = ET.fromstring(fetch(url))
|
||||||
except Exception as exc: # sin red o feed caído: se conserva la web anterior
|
out = []
|
||||||
print('feed no disponible, se mantiene index.html:', exc, file=sys.stderr)
|
for e in root.findall('a:entry', NS)[:15]:
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
videos = []
|
|
||||||
for e in root.findall('a:entry', NS)[:MAX_VIDEOS]:
|
|
||||||
d = e.find('a:published', NS).text[:10].split('-')
|
d = e.find('a:published', NS).text[:10].split('-')
|
||||||
videos.append({
|
out.append({
|
||||||
'id': e.find('y:videoId', NS).text,
|
'id': e.find('y:videoId', NS).text,
|
||||||
'title': e.find('a:title', NS).text,
|
'title': e.find('a:title', NS).text,
|
||||||
'date': f'{int(d[2])} {MESES[int(d[1]) - 1]} {d[0]}',
|
'date': f'{int(d[2])} {MESES[int(d[1]) - 1]} {d[0]}',
|
||||||
|
'desc': e.find('m:group/m:description', NS).text or '',
|
||||||
|
'views': int((e.find('m:group/m:community/m:statistics', NS).attrib.get('views') or 0)
|
||||||
|
if e.find('m:group/m:community/m:statistics', NS) is not None else 0),
|
||||||
})
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
def sync_thumbs(vids):
|
|
||||||
|
try:
|
||||||
|
videos, shorts = read_feed(FEED_VIDEOS), read_feed(FEED_SHORTS)
|
||||||
|
pop_videos, pop_shorts = read_feed(FEED_POP_VIDEOS), read_feed(FEED_POP_SHORTS)
|
||||||
|
except Exception as exc: # sin red o feed caído: se conserva la web anterior
|
||||||
|
print('feed no disponible, se mantiene index.html:', exc, file=sys.stderr)
|
||||||
|
sys.exit(0)
|
||||||
|
videos, shorts = videos[:VIDEOS_SHOWN + 1], shorts[:SHORTS_SHOWN]
|
||||||
|
# la lista de populares de YouTube no viene ordenada por vistas: se ordena aquí
|
||||||
|
pop_videos = sorted(pop_videos, key=lambda v: -v['views'])[:POP_VIDEOS_SHOWN]
|
||||||
|
pop_shorts = sorted(pop_shorts, key=lambda v: -v['views'])[:POP_SHORTS_SHOWN]
|
||||||
|
|
||||||
|
|
||||||
|
KEEP = {'avatar.jpg', 'banner.jpg', 'fondo.jpg'}
|
||||||
|
# (nombre en YouTube, ancho máx. al guardar) por orden de preferencia
|
||||||
|
SOURCES = {
|
||||||
|
'video': [('maxresdefault', 1280), ('sddefault', 960), ('hqdefault', 960)],
|
||||||
|
'short': [('oar2', 540), ('hqdefault', 480)], # oar2 = miniatura vertical 1080x1920
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def grab(vid, kind):
|
||||||
|
"""Devuelve (bytes JPEG recomprimido, es_alta_calidad) con la mejor miniatura disponible."""
|
||||||
|
for i, (name, width) in enumerate(SOURCES[kind]):
|
||||||
|
try:
|
||||||
|
im = Image.open(io.BytesIO(fetch(f'https://i.ytimg.com/vi/{vid}/{name}.jpg'))).convert('RGB')
|
||||||
|
except Exception:
|
||||||
|
continue # 404 u otro fallo: se prueba la siguiente
|
||||||
|
if im.width < 200: # YouTube devuelve un gif de 120x90 cuando no existe
|
||||||
|
continue
|
||||||
|
if im.width > width:
|
||||||
|
im = im.resize((width, round(im.height * width / im.width)), Image.LANCZOS)
|
||||||
|
buf = io.BytesIO()
|
||||||
|
im.save(buf, 'JPEG', quality=82, optimize=True, progressive=True)
|
||||||
|
return buf.getvalue(), i == 0
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
|
||||||
|
def sync_thumbs(vids, kind):
|
||||||
|
"""Descarga la miniatura HD de cada vídeo (…-hd.jpg). Si aún no existe la HD, usa una
|
||||||
|
de respaldo (…-lo.jpg) y en la siguiente ejecución vuelve a intentar la HD."""
|
||||||
img_dir = os.path.join(OUT, 'img')
|
img_dir = os.path.join(OUT, 'img')
|
||||||
os.makedirs(img_dir, exist_ok=True)
|
os.makedirs(img_dir, exist_ok=True)
|
||||||
keep = {'avatar.jpg', 'banner.jpg'}
|
|
||||||
ok = []
|
ok = []
|
||||||
for v in vids:
|
for v in vids:
|
||||||
path = os.path.join(img_dir, v['id'] + '.jpg')
|
hd, lo = f"{v['id']}-hd.jpg", f"{v['id']}-lo.jpg"
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(os.path.join(img_dir, hd)):
|
||||||
try:
|
data, is_hd = grab(v['id'], kind)
|
||||||
open(path, 'wb').write(fetch(f"https://i.ytimg.com/vi/{v['id']}/hqdefault.jpg"))
|
if data is None:
|
||||||
except Exception as exc:
|
print('miniatura no descargada', v['id'], file=sys.stderr)
|
||||||
print('miniatura no descargada', v['id'], exc, file=sys.stderr)
|
|
||||||
continue
|
continue
|
||||||
keep.add(v['id'] + '.jpg')
|
open(os.path.join(img_dir, hd if is_hd else lo), 'wb').write(data)
|
||||||
|
v['img'] = hd if os.path.exists(os.path.join(img_dir, hd)) else lo
|
||||||
|
KEEP.add(v['img'])
|
||||||
ok.append(v)
|
ok.append(v)
|
||||||
for f in os.listdir(img_dir):
|
|
||||||
if f not in keep:
|
|
||||||
os.remove(os.path.join(img_dir, f))
|
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
videos = sync_thumbs(videos)
|
def prune_thumbs():
|
||||||
|
img_dir = os.path.join(OUT, 'img')
|
||||||
|
for f in os.listdir(img_dir):
|
||||||
|
if f not in KEEP:
|
||||||
|
os.remove(os.path.join(img_dir, f))
|
||||||
|
|
||||||
|
|
||||||
|
ok = sync_thumbs(videos + pop_videos, 'video') + sync_thumbs(shorts + pop_shorts, 'short')
|
||||||
|
prune_thumbs()
|
||||||
|
okids = {v['id'] for v in ok}
|
||||||
|
videos = [v for v in videos if v['id'] in okids]
|
||||||
|
shorts = [v for v in shorts if v['id'] in okids]
|
||||||
|
pop_videos = [v for v in pop_videos if v['id'] in okids]
|
||||||
|
pop_shorts = [v for v in pop_shorts if v['id'] in okids]
|
||||||
if not videos:
|
if not videos:
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
def is_short(vid):
|
# --- Temas: hashtags de las descripciones de YouTube -------------------------------------------
|
||||||
"""Los Shorts llevan barras laterales desenfocadas: poca textura en los laterales."""
|
STOP = {'podcast', 'podcastclips', 'andaquenosabesna', 'andaquenosabesna', 'aqnsn', 'shorts', 'short', 'fyp',
|
||||||
im = Image.open(os.path.join(OUT, 'img', vid + '.jpg')).convert('L')
|
'viral', 'parati', 'galaxoide', 'youtube', 'video', 'clips'}
|
||||||
w, h = im.size
|
LABELS = { # clave normalizada -> texto a mostrar (sinónimos incluidos)
|
||||||
return ImageStat.Stat(im.crop((0, 0, int(w * .18), h))).stddev[0] < 25
|
'egipto': 'Egipto', 'egiptomania': 'Egipto', 'educacion': 'Educación', 'arabe': 'Lengua árabe',
|
||||||
|
'lenguas': 'Lengua árabe', 'religion': 'Religiones', 'religiones': 'Religiones', 'magia': 'Magia',
|
||||||
|
'burocracia': 'Burocracia', 'poligamia': 'Poligamia', 'economia': 'Economía',
|
||||||
|
'gastronomia': 'Gastronomía', 'comidaegipcia': 'Comida egipcia', 'robotica': 'Robótica',
|
||||||
|
'tecnologia': 'Tecnología', 'agricultura': 'Agricultura', 'curiosidades': 'Curiosidades',
|
||||||
|
'antiguoegipto': 'Egipto', 'egiptoantiguo': 'Egipto', 'cairo': 'El Cairo', 'elcairo': 'El Cairo',
|
||||||
|
'arqueologia': 'Arqueología', 'historia': 'Historia',
|
||||||
|
}
|
||||||
|
PINNED = ['Cultura granaína', 'Entrevistas'] # siempre presentes
|
||||||
|
MAX_TOPICS = 12
|
||||||
|
|
||||||
|
|
||||||
|
def norm(tag):
|
||||||
|
t = unicodedata.normalize('NFKD', tag.lower())
|
||||||
|
return ''.join(c for c in t if not unicodedata.combining(c))
|
||||||
|
|
||||||
|
|
||||||
|
def topics(vids):
|
||||||
|
count = Counter()
|
||||||
|
for v in vids:
|
||||||
|
seen = set()
|
||||||
|
for tag in re.findall(r'#(\w+)', v['desc']):
|
||||||
|
k = norm(tag)
|
||||||
|
if k in STOP:
|
||||||
|
continue
|
||||||
|
label = LABELS.get(k) or (tag[0].upper() + tag[1:])
|
||||||
|
if label in seen: # sinónimos del mismo vídeo cuentan una sola vez
|
||||||
|
continue
|
||||||
|
seen.add(label)
|
||||||
|
count[label] += 1
|
||||||
|
ranked = [t for t, _ in sorted(count.items(), key=lambda kv: (-kv[1], kv[0]))]
|
||||||
|
out = [t for t in ranked if t not in PINNED][:max(MAX_TOPICS - len(PINNED), 0)]
|
||||||
|
return out + PINNED
|
||||||
|
|
||||||
|
|
||||||
|
chips = ''.join(f'<li>{html.escape(t)}</li>' for t in topics(videos + shorts))
|
||||||
|
|
||||||
esc = html.escape
|
esc = html.escape
|
||||||
featured, rest = videos[0], videos[1:] # el feed viene del más nuevo al más antiguo
|
featured, rest = videos[0], videos[1:] # el destacado es el último vídeo (no Short)
|
||||||
|
|
||||||
|
|
||||||
def url(v):
|
def url(v):
|
||||||
return f"https://www.youtube.com/watch?v={v['id']}"
|
return f"https://www.youtube.com/watch?v={v['id']}"
|
||||||
|
|
||||||
|
|
||||||
def card(v, n):
|
EYE = ('<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg>')
|
||||||
|
|
||||||
|
|
||||||
|
def badge(v):
|
||||||
|
if not v['views']:
|
||||||
|
return ''
|
||||||
|
n = f"{v['views']:,}".replace(',', '.')
|
||||||
|
return f'<span class="views" title="{n} vistas">{EYE}<b>{n}</b></span>'
|
||||||
|
|
||||||
|
|
||||||
|
def vistas(v):
|
||||||
|
return f"{v['views']:,}".replace(',', '.') + ' vistas' if v['views'] else ''
|
||||||
|
|
||||||
|
|
||||||
|
def card(v, n, show_views=False):
|
||||||
return f'''
|
return f'''
|
||||||
<a class="card reveal" style="--d:{(n % 3) * 70}ms" href="{url(v)}" target="_blank" rel="noopener">
|
<a class="card reveal" style="--d:{(n % 3) * 70}ms" href="{url(v)}" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/{v['id']}.jpg" alt="" loading="lazy" width="480" height="360"{' class="short"' if is_short(v['id']) else ''}><span class="play" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/{v['img']}" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span>{badge(v)}</span>
|
||||||
<span class="meta"><time>{esc(v['date'])}</time><strong>{esc(v['title'])}</strong></span>
|
<span class="meta"><time>{esc(v['date'])}</time><strong>{esc(v['title'])}</strong></span>
|
||||||
</a>'''
|
</a>'''
|
||||||
|
|
||||||
|
|
||||||
|
def short_card(v, n, show_views=False):
|
||||||
|
return f'''
|
||||||
|
<a class="scard reveal" style="--d:{(n % 6) * 50}ms" href="https://www.youtube.com/shorts/{v['id']}" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/{v['img']}" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span>{badge(v)}</span>
|
||||||
|
<span class="meta"><strong>{esc(v['title'])}</strong></span>
|
||||||
|
</a>'''
|
||||||
|
|
||||||
|
|
||||||
cards = ''.join(card(v, i) for i, v in enumerate(rest))
|
cards = ''.join(card(v, i) for i, v in enumerate(rest))
|
||||||
|
scards = ''.join(short_card(v, i) for i, v in enumerate(shorts))
|
||||||
|
pcards = ''.join(card(v, i, True) for i, v in enumerate(pop_videos))
|
||||||
|
pscards = ''.join(short_card(v, i, True) for i, v in enumerate(pop_shorts))
|
||||||
|
|
||||||
page = f'''<!doctype html>
|
page = f'''<!doctype html>
|
||||||
<html lang="es">
|
<html lang="es">
|
||||||
@@ -100,7 +222,7 @@ page = f'''<!doctype html>
|
|||||||
<meta name="theme-color" content="#06080d">
|
<meta name="theme-color" content="#06080d">
|
||||||
<meta property="og:title" content="Anda Que No Sabes Ná">
|
<meta property="og:title" content="Anda Que No Sabes Ná">
|
||||||
<meta property="og:description" content="El programa nocturno 100% granaíno.">
|
<meta property="og:description" content="El programa nocturno 100% granaíno.">
|
||||||
<meta property="og:image" content="https://andaquenosabesna.thax.es/img/banner.jpg">
|
<meta property="og:image" content="https://andaquenosabesna.com/img/banner.jpg">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<link rel="icon" href="img/avatar.jpg">
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
@@ -111,9 +233,11 @@ page = f'''<!doctype html>
|
|||||||
<header class="top">
|
<header class="top">
|
||||||
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
<nav aria-label="Principal">
|
<nav aria-label="Principal">
|
||||||
<a href="#programas">Programas</a>
|
<a href="#programas">Vídeos</a>
|
||||||
<a href="#programa">El programa</a>
|
<a href="#programa">El programa</a>
|
||||||
|
<a href="#quienes">Quiénes somos</a>
|
||||||
<a href="#seguir">Seguir</a>
|
<a href="#seguir">Seguir</a>
|
||||||
|
<a class="navcta" href="contacto.html">Contacto</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -122,12 +246,12 @@ page = f'''<!doctype html>
|
|||||||
<div class="hero-bg" aria-hidden="true"></div>
|
<div class="hero-bg" aria-hidden="true"></div>
|
||||||
<div class="hero-inner">
|
<div class="hero-inner">
|
||||||
<img class="neon" src="img/avatar.jpg" alt="Anda Que No Sabes Ná" width="220" height="220">
|
<img class="neon" src="img/avatar.jpg" alt="Anda Que No Sabes Ná" width="220" height="220">
|
||||||
<p class="kicker">Directo desde Granada, de noche</p>
|
<p class="kicker">Con invitados que están aquí y otros que están pa allá</p>
|
||||||
<h1>El programa nocturno<br><em>100% granaíno</em></h1>
|
<h1>El programa nocturno<br><em>100% granaíno</em></h1>
|
||||||
<p class="lead">Charlas sin guion sobre lo que sabemos, lo que no, y lo que creemos saber.</p>
|
<p class="lead">Charlas sobre lo que sabemos, lo que no, y lo que creemos saber.</p>
|
||||||
<div class="cta">
|
<div class="cta">
|
||||||
<a class="btn primary" href="{YT}" target="_blank" rel="noopener">Ver en YouTube</a>
|
<a class="btn primary" href="{YT}" target="_blank" rel="noopener">Ver en YouTube</a>
|
||||||
<a class="btn" href="#programas">Últimos programas</a>
|
<a class="btn" href="#programas">Últimos vídeos</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
|
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
|
||||||
@@ -135,7 +259,7 @@ page = f'''<!doctype html>
|
|||||||
|
|
||||||
<section id="destacado" class="featured wrap reveal">
|
<section id="destacado" class="featured wrap reveal">
|
||||||
<a class="feat" href="{url(featured)}" target="_blank" rel="noopener">
|
<a class="feat" href="{url(featured)}" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/{featured['id']}.jpg" alt="" width="480" height="360"><span class="play big" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/{featured['img']}" alt="" width="1280" height="720"><span class="play big" aria-hidden="true"></span>{badge(featured)}</span>
|
||||||
<span class="feat-text">
|
<span class="feat-text">
|
||||||
<span class="tag">Último vídeo</span>
|
<span class="tag">Último vídeo</span>
|
||||||
<strong>{esc(featured['title'])}</strong>
|
<strong>{esc(featured['title'])}</strong>
|
||||||
@@ -154,12 +278,48 @@ page = f'''<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="shorts" class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<h2>Últimos <em>Shorts</em></h2>
|
||||||
|
<a class="more" href="{YT}/shorts" target="_blank" rel="noopener">Todos los Shorts →</a>
|
||||||
|
</div>
|
||||||
|
<div class="shorts">{scards}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="populares" class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<h2>Lo más <em>visto</em></h2>
|
||||||
|
<a class="more" href="{YT}/videos?view=0&sort=p" target="_blank" rel="noopener">Ver populares en YouTube →</a>
|
||||||
|
</div>
|
||||||
|
<h3 class="sub reveal">Vídeos</h3>
|
||||||
|
<div class="grid">{pcards}
|
||||||
|
</div>
|
||||||
|
<h3 class="sub reveal">Shorts</h3>
|
||||||
|
<div class="shorts">{pscards}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="programa" class="about wrap reveal">
|
<section id="programa" class="about wrap reveal">
|
||||||
<h2>De qué va</h2>
|
<h2>De qué va</h2>
|
||||||
<p>Un programa de noche, con tertulia y sin prisa. Cada semana entra gente distinta, con su oficio y su manía, y se habla de lo que sale: de cómo se educa a un adolescente, de la vida en las calles de El Cairo, de si el árabe es tan antiguo como dicen, de qué tienen en común las religiones o de cómo se hace un truco de cartas.</p>
|
<p>Un programa de noche, con tertulia y sin prisa. Cada semana entra gente distinta, con su oficio y su manía, y se habla de lo que sale: de cómo se educa a un adolescente, de la vida en las calles de El Cairo, de si el árabe es tan antiguo como dicen, de qué tienen en común las religiones o de cómo se hace un truco de cartas.</p>
|
||||||
<ul class="chips" aria-label="Temas">
|
<ul class="chips" aria-label="Temas">{chips}</ul>
|
||||||
<li>Educación</li><li>Egipto</li><li>Lengua árabe</li><li>Religiones</li><li>Magia</li><li>Cultura granaína</li><li>Entrevistas</li>
|
</section>
|
||||||
</ul>
|
|
||||||
|
<section id="quienes" class="who wrap reveal">
|
||||||
|
<h2>Quiénes somos</h2>
|
||||||
|
<p>Somos Álex y Juan, las dos personas detrás de Anda Que No Sabes Ná: un programa hecho en Granada, de noche, con ganas de aprender de quien se sienta a charlar.</p>
|
||||||
|
<div class="team">
|
||||||
|
<div class="person"><b>Juan</b><em>Captura la realidad</em><span>Consultor en metodología BIM y escáner láser 3D. Juan lleva años escaneando entornos y edificios, capturando cada detalle. Pero hay algo que se le da casi tan bien como escanear un lugar: «escanear» a las personas. Observador, curioso y con buen ojo para leer gestos, actitudes y personalidades.</span></div>
|
||||||
|
<div class="person"><b>Álex</b><em>Da forma a las ideas</em><span>Arquitecto de formación y líder de equipos de ingeniería. Es quien desarrolla y automatiza todo para que el podcast funcione: pone el orden y el guion para que todo salga rodado. Y anda metido en cómo usar la IA en el día a día.</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wrap reveal" aria-labelledby="venir">
|
||||||
|
<div class="invite">
|
||||||
|
<div><h2 id="venir">¿Quieres venir al programa?</h2><p>Cuéntanos quién eres y de qué te gustaría hablar.</p></div>
|
||||||
|
<div class="cta"><a class="btn primary" href="contacto.html">Escríbenos</a><a class="btn" href="patrocinadores.html">Quiero anunciarme</a></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="seguir" class="follow wrap reveal">
|
<section id="seguir" class="follow wrap reveal">
|
||||||
@@ -175,7 +335,7 @@ page = f'''<!doctype html>
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="wrap foot">
|
<div class="wrap foot">
|
||||||
<p>© 2026 Anda Que No Sabes Ná · Granada</p>
|
<p>© 2026 Anda Que No Sabes Ná · Granada</p>
|
||||||
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="mailto:{MAIL}">Contacto</a></p>
|
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="contacto.html">Contacto</a><a href="patrocinadores.html">Patrocinadores</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Primer build al arrancar y después cada 15 minutos; si el feed falla se conserva la web anterior.
|
# Primer build al arrancar y después cada 15 minutos; si el feed falla se conserva la web anterior.
|
||||||
python3 /app/build.py || true
|
python3 /app/build.py || true
|
||||||
|
python3 /app/server.py &
|
||||||
( while true; do sleep 900; python3 /app/build.py || true; done ) &
|
( while true; do sleep 900; python3 /app/build.py || true; done ) &
|
||||||
exec nginx -g 'daemon off;'
|
exec nginx -g 'daemon off;'
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
{
|
||||||
|
"title": "Anda Que No Sabes Ná Podcast",
|
||||||
|
"description": "Queremos conocerte un poco antes de darle al REC. No hace falta que prepares respuestas perfectas: esto nos sirve para encontrar buenas historias, preguntas interesantes y, sobre todo, temas sobre los que merezca la pena hablar.",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": 11,
|
||||||
|
"title": "",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "👤 Tú",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"title": "Nombre y cómo prefieres que te presentemos",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿A qué te dedicas actualmente?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Cuál es tu proyecto, empresa o actividad principal?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"title": "LinkedIn, web o cualquier enlace que nos ayude a conocerte mejor",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Tus redes sociales que quieras que mencionemos o utilicemos para promocionar el episodio",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🎙️ Para conocerte un poco mejor",
|
||||||
|
"desc": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Si tuvieras que contarnos tu historia en dos minutos, ¿por dónde empezarías?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué tema podrías estar hablando durante horas sin aburrirte?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna experiencia, proyecto, fracaso, éxito o anécdota que te parezca especialmente buena para contar en el podcast?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué idea, mito o tópico de tu sector te gustaría desmontar?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🔥 Vamos a meternos en harina",
|
||||||
|
"desc": "Contestar en esta sección es opcional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna opinión tuya sobre tu sector que pueda generar debate?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Qué pregunta te hacen siempre y estás cansado/a de responder?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algo que normalmente no se cuenta cuando se habla de tu profesión, sector o trayectoria y que crees que deberíamos contar?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Si pudiéramos conseguir que después de escucharte alguien cambiara de opinión sobre una cosa, ¿qué te gustaría que fuera?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "🚧 Territorio delicado",
|
||||||
|
"desc": "No hace falta justificarlo. Queremos saber dónde están tus límites antes de empezar."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algún tema que prefieras que no toquemos?",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna persona, empresa, experiencia o asunto sobre el que prefieras que no hagamos preguntas?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "Y... ¿Hay algún otro tema de tu vida que quieras contarnos?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "📣 Y para terminar...",
|
||||||
|
"desc": "Contestar en esta sección es opcional"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay algún proyecto, libro, producto, evento o iniciativa que tengas ahora mismo y que te gustaría dar a conocer durante la entrevista?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna pregunta que te encantaría que te hiciéramos y que normalmente nadie te hace?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"title": "¿Hay alguna historia que te gustaría contar sí o sí?",
|
||||||
|
"desc": "",
|
||||||
|
"required": false,
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 8,
|
||||||
|
"title": "Antes de enviar",
|
||||||
|
"desc": "Gracias por tu tiempo y tu disposición. Este podcast se realiza de forma altruista y colaborativa. Ni la preparación, ni la grabación, ni la publicación contemplan remuneración económica. La participación es voluntaria y con buena fe, con la intención de compartir conocimiento, experiencias e historias. Al aceptar participar, entiendes y estás de acuerdo con estas condiciones."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 4,
|
||||||
|
"title": "Aceptación de condiciones",
|
||||||
|
"desc": "",
|
||||||
|
"required": true,
|
||||||
|
"options": [
|
||||||
|
"He leído y acepto las condiciones de participación descritas arriba."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Genera public/invitados.html a partir de guest_form.json (copia de las preguntas del formulario de Google)."""
|
||||||
|
import html, json
|
||||||
|
|
||||||
|
esc = html.escape
|
||||||
|
F = json.load(open('guest_form.json', encoding='utf-8'))
|
||||||
|
|
||||||
|
secciones, actual, nq = [], None, 0
|
||||||
|
consent = None
|
||||||
|
for it in F['items']:
|
||||||
|
t = it['type']
|
||||||
|
if t == 8:
|
||||||
|
actual = {'titulo': it['title'], 'nota': it['desc'], 'preguntas': []}
|
||||||
|
secciones.append(actual)
|
||||||
|
elif t in (0, 1):
|
||||||
|
nq += 1
|
||||||
|
actual['preguntas'].append({'id': f'q{nq:02d}', 'titulo': it['title'], 'desc': it['desc'],
|
||||||
|
'req': it['required'], 'largo': t == 1})
|
||||||
|
elif t == 4:
|
||||||
|
consent = {'titulo': it['title'], 'opcion': it['options'][0], 'req': it['required']}
|
||||||
|
consent_text = secciones[-1]['nota']
|
||||||
|
secciones.pop() # la última sección solo contiene las condiciones
|
||||||
|
|
||||||
|
def pregunta(q):
|
||||||
|
mark = ' <span class="req" title="Obligatoria">*</span>' if q['req'] else ''
|
||||||
|
campo = (f'<textarea name="{q["id"]}" rows="4" maxlength="3000"{" required" if q["req"] else ""}></textarea>'
|
||||||
|
if q['largo'] else
|
||||||
|
f'<input name="{q["id"]}" type="text" maxlength="300"{" required" if q["req"] else ""}>')
|
||||||
|
d = f'<small>{esc(q["desc"])}</small>' if q['desc'] else ''
|
||||||
|
return f' <label><span>{esc(q["titulo"])}{mark}</span>{d}\n {campo}\n </label>'
|
||||||
|
|
||||||
|
EMAIL = ''' <label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small>
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200" required>
|
||||||
|
</label>
|
||||||
|
<label><span>Tu teléfono móvil <span class="req" title="Obligatoria">*</span></span><small>Solo para contactarte el día de la grabación si surge algún imprevisto.</small>
|
||||||
|
<input name="tel" type="tel" autocomplete="tel" inputmode="tel" maxlength="20" required>
|
||||||
|
</label>'''
|
||||||
|
bloques = []
|
||||||
|
for s in secciones:
|
||||||
|
nota = f'\n <p class="note">{esc(s["nota"])}</p>' if s['nota'] else ''
|
||||||
|
bloques.append(f' <fieldset class="sec">\n <legend>{esc(s["titulo"])}</legend>{nota}\n' +
|
||||||
|
'\n'.join(pregunta(q) for q in s['preguntas']) + '\n </fieldset>')
|
||||||
|
bloques[0] = bloques[0].replace('</fieldset>', EMAIL + '\n </fieldset>')
|
||||||
|
|
||||||
|
def nav(i, total):
|
||||||
|
ant = '<button type="button" class="btn ghost prev">← Anterior</button>' if i else ''
|
||||||
|
sig = ('<button class="btn primary" type="submit">Enviar mi ficha</button>' if i == total - 1
|
||||||
|
else '<button type="button" class="btn primary next">Siguiente →</button>')
|
||||||
|
return f' <div class="nav">{ant}{sig}</div>\n </fieldset>'
|
||||||
|
|
||||||
|
FINAL = ''' <fieldset class="sec">
|
||||||
|
<legend>Antes de enviar</legend>
|
||||||
|
<p class="note">Este podcast se realiza de forma altruista y colaborativa.</p>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído y acepto las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a> descritas. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input name="privacy" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen estos datos para preparar el episodio. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
</fieldset>'''
|
||||||
|
pasos = bloques + [FINAL]
|
||||||
|
pasos = [p[:p.rindex(' </fieldset>')] + nav(i, len(pasos)) for i, p in enumerate(pasos)]
|
||||||
|
CUERPO = chr(10).join(pasos)
|
||||||
|
|
||||||
|
ELIGE_DIA = '''
|
||||||
|
<div id="elige-dia" hidden>
|
||||||
|
<fieldset id="dias-fs" class="sec">
|
||||||
|
<legend>¿Qué día te viene mejor?</legend>
|
||||||
|
</fieldset>
|
||||||
|
<p id="elige-err" class="status err" hidden>Selecciona un día para continuar.</p>
|
||||||
|
<div class="nav"><button id="elige-ok" type="button" class="btn primary">Continuar →</button></div>
|
||||||
|
</div>
|
||||||
|
'''
|
||||||
|
|
||||||
|
page = f'''<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<title>Ficha del invitado · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact guest">
|
||||||
|
<h1>Ficha del <em>invitado</em></h1>
|
||||||
|
<p id="hola" class="lead-c" hidden></p>
|
||||||
|
<p id="malo" class="status err" hidden>Este enlace no es válido o ha caducado. Cuéntanos qué ha pasado desde el <a href="contacto.html">formulario de contacto</a> y te mandamos uno nuevo.</p>
|
||||||
|
<p id="cita" class="cita" hidden></p>
|
||||||
|
{ELIGE_DIA}
|
||||||
|
<p id="intro" class="lead-c" hidden>{esc(F['description'])}</p>
|
||||||
|
|
||||||
|
<form id="g" class="form" novalidate hidden data-ok="¡Gracias! Hemos recibido tu ficha y te hemos enviado un correo con los datos de la grabación. Si no lo ves en unos minutos, mira en la carpeta de spam y añade contacto@andaquenosabesna.com a tus contactos.">
|
||||||
|
{CUERPO}
|
||||||
|
<p id="paso" class="paso" aria-live="polite"></p>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
<p class="note">Tus respuestas se guardan en este navegador mientras escribes, por si cierras la página. Se borran al enviarlas.</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="invitados.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
'''
|
||||||
|
open('public/invitados.html', 'w', encoding='utf-8').write(page)
|
||||||
|
print('invitados.html:', len(page), 'bytes;', nq, 'preguntas;', len(secciones), 'secciones')
|
||||||
+16
@@ -1,6 +1,7 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
if ($host = andaquenosabesna.thax.es) { return 301 https://andaquenosabesna.com$request_uri; }
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Content-Type-Options nosniff;
|
||||||
@@ -9,5 +10,20 @@ server {
|
|||||||
location = / { add_header Cache-Control "no-cache"; }
|
location = / { add_header Cache-Control "no-cache"; }
|
||||||
location /img/ { expires 7d; add_header Cache-Control "public"; }
|
location /img/ { expires 7d; add_header Cache-Control "public"; }
|
||||||
location ~* \.(css)$ { add_header Cache-Control "no-cache"; }
|
location ~* \.(css)$ { add_header Cache-Control "no-cache"; }
|
||||||
|
location = /invitados.html {
|
||||||
|
add_header X-Robots-Tag "noindex, nofollow, noarchive" always;
|
||||||
|
add_header Cache-Control "no-store";
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
}
|
||||||
|
location = /api/invitados {
|
||||||
|
client_max_body_size 128k;
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||||
|
}
|
||||||
|
location /api/ {
|
||||||
|
client_max_body_size 16k;
|
||||||
|
proxy_pass http://127.0.0.1:8081;
|
||||||
|
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||||
|
}
|
||||||
location / { try_files $uri $uri/ =404; }
|
location / { try_files $uri $uri/ =404; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Contacto · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="description" content="¿Quieres venir al programa, proponer un tema o decirnos algo? Escríbenos.">
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
<nav aria-label="Principal"><a href="/">Inicio</a><a href="/#quienes">Quiénes somos</a><a href="patrocinadores.html">Patrocinadores</a></nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact">
|
||||||
|
<h1>¿Quieres venir al <em>programa</em>?</h1>
|
||||||
|
<p class="lead-c">Cuéntanos quién eres y de qué te gustaría hablar. También puedes proponer un tema o simplemente escribirnos. Te respondemos por correo.</p>
|
||||||
|
|
||||||
|
<form id="f" class="form" novalidate data-endpoint="/api/contacto" data-mail="contacto@andaquenosabesna.com" data-ok="¡Gracias! Hemos recibido tu mensaje y te responderemos pronto.">
|
||||||
|
<label>Nombre
|
||||||
|
<input name="nombre" type="text" autocomplete="name" maxlength="100" required>
|
||||||
|
</label>
|
||||||
|
<label>Correo electrónico
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200" required>
|
||||||
|
</label>
|
||||||
|
<label>Motivo
|
||||||
|
<select name="motivo">
|
||||||
|
<option value="venir">Quiero venir al programa</option>
|
||||||
|
<option value="tema">Propongo un tema</option>
|
||||||
|
<option value="otro">Otro asunto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Mensaje
|
||||||
|
<textarea name="mensaje" rows="6" maxlength="3000" required placeholder="Cuéntanos a qué te dedicas y qué te gustaría contar…"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen mis datos para responderme.</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn primary" type="submit">Enviar mensaje</button>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
</form>
|
||||||
|
<noscript><p>Necesitas JavaScript para usar el formulario. Mientras tanto, escríbenos a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p></noscript>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="form.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
(function () {
|
||||||
|
var f = document.querySelector('form[data-endpoint]');
|
||||||
|
if (!f) return;
|
||||||
|
var st = document.getElementById('st'), t0 = Date.now(), busy = false, mail = f.getAttribute('data-mail');
|
||||||
|
function say(msg, ok) { st.textContent = msg; st.className = 'status ' + (ok ? 'ok' : 'err'); }
|
||||||
|
f.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (busy) return;
|
||||||
|
var body = { consent: f.consent.checked, t: Date.now() - t0 };
|
||||||
|
Array.prototype.forEach.call(f.elements, function (el) {
|
||||||
|
if (el.name && el.name !== 'consent') body[el.name] = (el.value || '').trim();
|
||||||
|
});
|
||||||
|
if (!body.nombre || !/^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(body.email || '') || (body.mensaje || '').length < 10 ||
|
||||||
|
!body.consent || (f.empresa && !body.empresa)) {
|
||||||
|
say('Revisa los campos obligatorios, el correo y que el mensaje tenga al menos 10 caracteres, y marca la casilla de privacidad.', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (body.sitio && !/^https?:\/\//.test(body.sitio)) { say('La web debe empezar por http:// o https://', false); return; }
|
||||||
|
busy = true; say('Enviando…', true);
|
||||||
|
fetch(f.getAttribute('data-endpoint'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
||||||
|
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
|
||||||
|
.then(function (x) {
|
||||||
|
if (x.ok) { f.reset(); say(f.getAttribute('data-ok'), true); }
|
||||||
|
else { say((x.j && x.j.error) || 'No se pudo enviar. Escríbenos a ' + mail + '.', false); }
|
||||||
|
})
|
||||||
|
.catch(function () { say('No se pudo enviar. Escríbenos a ' + mail + '.', false); })
|
||||||
|
.then(function () { busy = false; });
|
||||||
|
});
|
||||||
|
})();
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
+163
-61
@@ -8,7 +8,7 @@
|
|||||||
<meta name="theme-color" content="#06080d">
|
<meta name="theme-color" content="#06080d">
|
||||||
<meta property="og:title" content="Anda Que No Sabes Ná">
|
<meta property="og:title" content="Anda Que No Sabes Ná">
|
||||||
<meta property="og:description" content="El programa nocturno 100% granaíno.">
|
<meta property="og:description" content="El programa nocturno 100% granaíno.">
|
||||||
<meta property="og:image" content="https://andaquenosabesna.thax.es/img/banner.jpg">
|
<meta property="og:image" content="https://andaquenosabesna.com/img/banner.jpg">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<link rel="icon" href="img/avatar.jpg">
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
@@ -19,9 +19,11 @@
|
|||||||
<header class="top">
|
<header class="top">
|
||||||
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
<nav aria-label="Principal">
|
<nav aria-label="Principal">
|
||||||
<a href="#programas">Programas</a>
|
<a href="#programas">Vídeos</a>
|
||||||
<a href="#programa">El programa</a>
|
<a href="#programa">El programa</a>
|
||||||
|
<a href="#quienes">Quiénes somos</a>
|
||||||
<a href="#seguir">Seguir</a>
|
<a href="#seguir">Seguir</a>
|
||||||
|
<a class="navcta" href="contacto.html">Contacto</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -30,24 +32,24 @@
|
|||||||
<div class="hero-bg" aria-hidden="true"></div>
|
<div class="hero-bg" aria-hidden="true"></div>
|
||||||
<div class="hero-inner">
|
<div class="hero-inner">
|
||||||
<img class="neon" src="img/avatar.jpg" alt="Anda Que No Sabes Ná" width="220" height="220">
|
<img class="neon" src="img/avatar.jpg" alt="Anda Que No Sabes Ná" width="220" height="220">
|
||||||
<p class="kicker">Directo desde Granada, de noche</p>
|
<p class="kicker">Con invitados que están aquí y otros que están pa allá</p>
|
||||||
<h1>El programa nocturno<br><em>100% granaíno</em></h1>
|
<h1>El programa nocturno<br><em>100% granaíno</em></h1>
|
||||||
<p class="lead">Charlas sin guion sobre lo que sabemos, lo que no, y lo que creemos saber.</p>
|
<p class="lead">Charlas sobre lo que sabemos, lo que no, y lo que creemos saber.</p>
|
||||||
<div class="cta">
|
<div class="cta">
|
||||||
<a class="btn primary" href="https://www.youtube.com/@andaquenosabesna" target="_blank" rel="noopener">Ver en YouTube</a>
|
<a class="btn primary" href="https://www.youtube.com/@andaquenosabesna" target="_blank" rel="noopener">Ver en YouTube</a>
|
||||||
<a class="btn" href="#programas">Últimos programas</a>
|
<a class="btn" href="#programas">Últimos vídeos</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
|
<a class="scroll" href="#destacado" aria-label="Bajar"><span></span></a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="destacado" class="featured wrap reveal">
|
<section id="destacado" class="featured wrap reveal">
|
||||||
<a class="feat" href="https://www.youtube.com/watch?v=PxQHPd5YDLs" target="_blank" rel="noopener">
|
<a class="feat" href="https://www.youtube.com/watch?v=4WtlNKB3xwI" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/PxQHPd5YDLs.jpg" alt="" width="480" height="360"><span class="play big" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/4WtlNKB3xwI-hd.jpg" alt="" width="1280" height="720"><span class="play big" aria-hidden="true"></span><span class="views" title="8 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>8</b></span></span>
|
||||||
<span class="feat-text">
|
<span class="feat-text">
|
||||||
<span class="tag">Último vídeo</span>
|
<span class="tag">Último vídeo</span>
|
||||||
<strong>La Tediosa Burocracia en la Educación</strong>
|
<strong>¿Están perdiendo AUTORIDAD los Profesores en las aulas?</strong>
|
||||||
<time>19 oct 2026</time>
|
<time>20 oct 2026</time>
|
||||||
<span class="go">Ver el programa <i aria-hidden="true">→</i></span>
|
<span class="go">Ver el programa <i aria-hidden="true">→</i></span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -59,71 +61,171 @@
|
|||||||
<a class="more" href="https://www.youtube.com/@andaquenosabesna/videos" target="_blank" rel="noopener">Todos los vídeos →</a>
|
<a class="more" href="https://www.youtube.com/@andaquenosabesna/videos" target="_blank" rel="noopener">Todos los vídeos →</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=rLFSEqe77OY" target="_blank" rel="noopener">
|
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/rLFSEqe77OY.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/_-VaTy2fazY-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="421 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>421</b></span></span>
|
||||||
<span class="meta"><time>19 oct 2026</time><strong>¿Cómo dejas de comerte las uñas? El trauma útil de Palín</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/_-VaTy2fazY.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>18 oct 2026</time><strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong></span>
|
<span class="meta"><time>18 oct 2026</time><strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=jmV_Ao85RvI" target="_blank" rel="noopener">
|
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=7zY2ey_ERlQ" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/jmV_Ao85RvI.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/7zY2ey_ERlQ-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="27 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>27</b></span></span>
|
||||||
<span class="meta"><time>14 oct 2026</time><strong>¿Cómo de antiguo es el Árabe?</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=DLcn8i9lV6Q" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/DLcn8i9lV6Q.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>13 oct 2026</time><strong>Todas las religiones son similares...</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=E_eULZSlWJs" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/E_eULZSlWJs.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>12 oct 2026</time><strong>La Vida de las Mujeres Egipcias en las zonas costeras</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=600_XwAHkv0" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/600_XwAHkv0.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>12 oct 2026</time><strong>Clases religiosas separadas, la política de las escuelas egipcias</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=XKSVhuk_lQI" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/XKSVhuk_lQI.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>11 oct 2026</time><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=voK4g7lIv4E" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/voK4g7lIv4E.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>11 oct 2026</time><strong>El Barrio Pijo de Egipto, Masr El Gedida, y sus tiendas de Oro</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=7zY2ey_ERlQ" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/7zY2ey_ERlQ.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>10 oct 2026</time><strong>Un Simple Teletransporte de Cartas...</strong></span>
|
<span class="meta"><time>10 oct 2026</time><strong>Un Simple Teletransporte de Cartas...</strong></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=yzTMyAKQnDQ" target="_blank" rel="noopener">
|
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=Q7qYQbvjMxQ" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/yzTMyAKQnDQ.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/Q7qYQbvjMxQ-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="29 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>29</b></span></span>
|
||||||
<span class="meta"><time>10 oct 2026</time><strong>Las Franquicias se extienden en EGIPTO...</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=Q7qYQbvjMxQ" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/Q7qYQbvjMxQ.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>9 oct 2026</time><strong>El Mahshi y otras RECETAS EXÓTICAS de Egipto que PODRÍAN PERDERSE</strong></span>
|
<span class="meta"><time>9 oct 2026</time><strong>El Mahshi y otras RECETAS EXÓTICAS de Egipto que PODRÍAN PERDERSE</strong></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=DgTa9qX4xH0" target="_blank" rel="noopener">
|
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener">
|
||||||
<span class="thumb"><img src="img/DgTa9qX4xH0.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
<span class="thumb"><img src="img/mxP0dvn2HD4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="196 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>196</b></span></span>
|
||||||
<span class="meta"><time>9 oct 2026</time><strong>El barrio cristiano del Cairo que RECICLA BASURA</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=oAoEz2Kwmk8" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/oAoEz2Kwmk8.jpg" alt="" loading="lazy" width="480" height="360" class="short"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>8 oct 2026</time><strong>Tiendas QUE NO CIERRAN en Egipto 🌇</strong></span>
|
|
||||||
</a>
|
|
||||||
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener">
|
|
||||||
<span class="thumb"><img src="img/mxP0dvn2HD4.jpg" alt="" loading="lazy" width="480" height="360"><span class="play" aria-hidden="true"></span></span>
|
|
||||||
<span class="meta"><time>6 oct 2026</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
|
<span class="meta"><time>6 oct 2026</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=f6UO6jtUgrU" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/f6UO6jtUgrU-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="65 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>65</b></span></span>
|
||||||
|
<span class="meta"><time>5 oct 2026</time><strong>"Así robaron el busto de Nefertiti..." - El problema del EXPOLIO DE TESOROS de Egipto</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/A2v_T2CCupo-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="252 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>252</b></span></span>
|
||||||
|
<span class="meta"><time>4 oct 2026</time><strong>¿Cómo es la vida nocturna y callejera en Egipto?</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=H_yHEe6skW4" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/H_yHEe6skW4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="139 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>139</b></span></span>
|
||||||
|
<span class="meta"><time>3 oct 2026</time><strong>EGIPTO NO ES COMO TE LO HAN CONTADO | AQNSN Programa 2 con Sara Yasser</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=Ei_VnfM-uLY" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/Ei_VnfM-uLY-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="124 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>124</b></span></span>
|
||||||
|
<span class="meta"><time>30 sep 2026</time><strong>TANATOPRACTOR DE DÍA Y ANIMADOR TUPPER SEX DE NOCHE | AQNSN Programa 1 con Javi (Parte 2)</strong></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="shorts" class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<h2>Últimos <em>Shorts</em></h2>
|
||||||
|
<a class="more" href="https://www.youtube.com/@andaquenosabesna/shorts" target="_blank" rel="noopener">Todos los Shorts →</a>
|
||||||
|
</div>
|
||||||
|
<div class="shorts">
|
||||||
|
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/PxQHPd5YDLs" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/PxQHPd5YDLs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="198 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>198</b></span></span>
|
||||||
|
<span class="meta"><strong>La Tediosa Burocracia en la Educación</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/rLFSEqe77OY" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/rLFSEqe77OY-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="584 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>584</b></span></span>
|
||||||
|
<span class="meta"><strong>¿Cómo dejas de comerte las uñas? El trauma útil de Palín</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/jmV_Ao85RvI" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/jmV_Ao85RvI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="16 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>16</b></span></span>
|
||||||
|
<span class="meta"><strong>¿Cómo de antiguo es el Árabe?</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DLcn8i9lV6Q" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/DLcn8i9lV6Q-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="546 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>546</b></span></span>
|
||||||
|
<span class="meta"><strong>Todas las religiones son similares...</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/E_eULZSlWJs" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/E_eULZSlWJs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="467 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>467</b></span></span>
|
||||||
|
<span class="meta"><strong>La Vida de las Mujeres Egipcias en las zonas costeras</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/600_XwAHkv0" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/600_XwAHkv0-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="847 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>847</b></span></span>
|
||||||
|
<span class="meta"><strong>Clases religiosas separadas, la política de las escuelas egipcias</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/XKSVhuk_lQI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.207 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.207</b></span></span>
|
||||||
|
<span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/voK4g7lIv4E" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/voK4g7lIv4E-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="497 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>497</b></span></span>
|
||||||
|
<span class="meta"><strong>El Barrio Pijo de Egipto, Masr El Gedida, y sus tiendas de Oro</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/yzTMyAKQnDQ" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/yzTMyAKQnDQ-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="221 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>221</b></span></span>
|
||||||
|
<span class="meta"><strong>Las Franquicias se extienden en EGIPTO...</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/DgTa9qX4xH0" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/DgTa9qX4xH0-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="466 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>466</b></span></span>
|
||||||
|
<span class="meta"><strong>El barrio cristiano del Cairo que RECICLA BASURA</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/oAoEz2Kwmk8" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/oAoEz2Kwmk8-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="308 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>308</b></span></span>
|
||||||
|
<span class="meta"><strong>Tiendas QUE NO CIERRAN en Egipto 🌇</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/05m1CMyGyUA" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/05m1CMyGyUA-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="172 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>172</b></span></span>
|
||||||
|
<span class="meta"><strong>¿Se parece la comida EGIPCIA a la ESPAÑOLA?</strong></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="populares" class="wrap">
|
||||||
|
<div class="sec-head reveal">
|
||||||
|
<h2>Lo más <em>visto</em></h2>
|
||||||
|
<a class="more" href="https://www.youtube.com/@andaquenosabesna/videos?view=0&sort=p" target="_blank" rel="noopener">Ver populares en YouTube →</a>
|
||||||
|
</div>
|
||||||
|
<h3 class="sub reveal">Vídeos</h3>
|
||||||
|
<div class="grid">
|
||||||
|
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=_-VaTy2fazY" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/_-VaTy2fazY-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="421 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>421</b></span></span>
|
||||||
|
<span class="meta"><time>18 oct 2026</time><strong>DOMESTICAR ADOLESCENTES, PLANTAR TOMATES Y CONSTRUIR MINIATURAS | AQNSN Programa 3 - Galaxoide</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:70ms" href="https://www.youtube.com/watch?v=A2v_T2CCupo" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/A2v_T2CCupo-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="252 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>252</b></span></span>
|
||||||
|
<span class="meta"><time>4 oct 2026</time><strong>¿Cómo es la vida nocturna y callejera en Egipto?</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:140ms" href="https://www.youtube.com/watch?v=rp9j_Ph9XvI" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/rp9j_Ph9XvI-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="219 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>219</b></span></span>
|
||||||
|
<span class="meta"><time>16 sep 2026</time><strong>ISA BECERRIL NOS PRESENTA SU NOVELA "HIALURÓNICO" | AQNSN Episodio Piloto 0</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="card reveal" style="--d:0ms" href="https://www.youtube.com/watch?v=mxP0dvn2HD4" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/mxP0dvn2HD4-hd.jpg" alt="" loading="lazy" width="640" height="360"><span class="play" aria-hidden="true"></span><span class="views" title="196 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>196</b></span></span>
|
||||||
|
<span class="meta"><time>6 oct 2026</time><strong>Árabe formal VS Árabe de la calle, ¿qué diferencias hay?</strong></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h3 class="sub reveal">Shorts</h3>
|
||||||
|
<div class="shorts">
|
||||||
|
<a class="scard reveal" style="--d:0ms" href="https://www.youtube.com/shorts/zJ_x4iA8OSs" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/zJ_x4iA8OSs-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.547 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.547</b></span></span>
|
||||||
|
<span class="meta"><strong>El árabe que se estudia y el que se habla en las calles son diferentes</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:50ms" href="https://www.youtube.com/shorts/nvF7P_S081o" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/nvF7P_S081o-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.545 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.545</b></span></span>
|
||||||
|
<span class="meta"><strong>😌 "Solo vi a una persona morir sonriendo..."</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:100ms" href="https://www.youtube.com/shorts/Wfq5vjJ6sQU" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/Wfq5vjJ6sQU-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.257 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.257</b></span></span>
|
||||||
|
<span class="meta"><strong>"Recoger a un niño ahogado de dos años, eso..."</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:150ms" href="https://www.youtube.com/shorts/XKSVhuk_lQI" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/XKSVhuk_lQI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.207 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.207</b></span></span>
|
||||||
|
<span class="meta"><strong>Poligamia en Egipto, de la antigüedad a la actualidad</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:200ms" href="https://www.youtube.com/shorts/p5trUANcq_c" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/p5trUANcq_c-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="1.043 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>1.043</b></span></span>
|
||||||
|
<span class="meta"><strong>El número 13 y su relación con la muerte #podcast #granada #misterio #latenight #andaquenosabesna</strong></span>
|
||||||
|
</a>
|
||||||
|
<a class="scard reveal" style="--d:250ms" href="https://www.youtube.com/shorts/7PRSukQTCwI" target="_blank" rel="noopener">
|
||||||
|
<span class="thumb"><img src="img/7PRSukQTCwI-hd.jpg" alt="" loading="lazy" width="540" height="960"><span class="play" aria-hidden="true"></span><span class="views" title="886 vistas"><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 5C6.5 5 2.3 8.6 1 12c1.3 3.4 5.5 7 11 7s9.7-3.6 11-7c-1.3-3.4-5.5-7-11-7zm0 11.5A4.5 4.5 0 1 1 12 7.5a4.5 4.5 0 0 1 0 9zm0-7a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/></svg><b>886</b></span></span>
|
||||||
|
<span class="meta"><strong>¿Cómo puede meterte miedo un Tanatopractor? 😖</strong></span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="programa" class="about wrap reveal">
|
<section id="programa" class="about wrap reveal">
|
||||||
<h2>De qué va</h2>
|
<h2>De qué va</h2>
|
||||||
<p>Un programa de noche, con tertulia y sin prisa. Cada semana entra gente distinta, con su oficio y su manía, y se habla de lo que sale: de cómo se educa a un adolescente, de la vida en las calles de El Cairo, de si el árabe es tan antiguo como dicen, de qué tienen en común las religiones o de cómo se hace un truco de cartas.</p>
|
<p>Un programa de noche, con tertulia y sin prisa. Cada semana entra gente distinta, con su oficio y su manía, y se habla de lo que sale: de cómo se educa a un adolescente, de la vida en las calles de El Cairo, de si el árabe es tan antiguo como dicen, de qué tienen en común las religiones o de cómo se hace un truco de cartas.</p>
|
||||||
<ul class="chips" aria-label="Temas">
|
<ul class="chips" aria-label="Temas"><li>Egipto</li><li>Curiosidades</li><li>Arqueología</li><li>Educación</li><li>Gastronomía</li><li>Lengua árabe</li><li>Agricultura</li><li>Burocracia</li><li>Comida egipcia</li><li>CulturaEgipcia</li><li>Cultura granaína</li><li>Entrevistas</li></ul>
|
||||||
<li>Educación</li><li>Egipto</li><li>Lengua árabe</li><li>Religiones</li><li>Magia</li><li>Cultura granaína</li><li>Entrevistas</li>
|
</section>
|
||||||
</ul>
|
|
||||||
|
<section id="quienes" class="who wrap reveal">
|
||||||
|
<h2>Quiénes somos</h2>
|
||||||
|
<p>Somos Álex y Juan, las dos personas detrás de Anda Que No Sabes Ná: un programa hecho en Granada, de noche, con ganas de aprender de quien se sienta a charlar.</p>
|
||||||
|
<div class="team">
|
||||||
|
<div class="person"><b>Álex</b><em>Da forma a las ideas</em><span>Arquitecto de formación y líder de equipos de ingeniería. Es quien desarrolla y automatiza todo para que el podcast funcione: pone el orden y el guion para que todo salga rodado. Y anda metido en cómo usar la IA en el día a día.</span></div>
|
||||||
|
<div class="person"><b>Juan</b><em>Captura la realidad</em><span>Consultor en metodología BIM y escáner láser 3D. Juan lleva años escaneando entornos y edificios, capturando cada detalle. Pero hay algo que se le da casi tan bien como escanear un lugar: «escanear» a las personas. Observador, curioso y con buen ojo para leer gestos, actitudes y personalidades.</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wrap reveal" aria-labelledby="venir">
|
||||||
|
<div class="invite">
|
||||||
|
<div><h2 id="venir">¿Quieres venir al programa?</h2><p>Cuéntanos quién eres y de qué te gustaría hablar.</p></div>
|
||||||
|
<div class="cta"><a class="btn primary" href="contacto.html">Escríbenos</a><a class="btn" href="patrocinadores.html">Quiero anunciarme</a></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="seguir" class="follow wrap reveal">
|
<section id="seguir" class="follow wrap reveal">
|
||||||
@@ -139,7 +241,7 @@
|
|||||||
<footer>
|
<footer>
|
||||||
<div class="wrap foot">
|
<div class="wrap foot">
|
||||||
<p>© 2026 Anda Que No Sabes Ná · Granada</p>
|
<p>© 2026 Anda Que No Sabes Ná · Granada</p>
|
||||||
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="mailto:andaquenosabesnada@gmail.com">Contacto</a></p>
|
<p class="links"><a href="privacidad.html">Política de privacidad</a><a href="terminos.html">Condiciones del servicio</a><a href="contacto.html">Contacto</a><a href="patrocinadores.html">Patrocinadores</a></p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<title>Ficha del invitado · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact guest">
|
||||||
|
<h1>Ficha del <em>invitado</em></h1>
|
||||||
|
<p id="hola" class="lead-c" hidden></p>
|
||||||
|
<p id="malo" class="status err" hidden>Este enlace no es válido o ha caducado. Cuéntanos qué ha pasado desde el <a href="contacto.html">formulario de contacto</a> y te mandamos uno nuevo.</p>
|
||||||
|
<p id="cita" class="cita" hidden></p>
|
||||||
|
|
||||||
|
<div id="elige-dia" hidden>
|
||||||
|
<fieldset id="dias-fs" class="sec">
|
||||||
|
<legend>¿Qué día te viene mejor?</legend>
|
||||||
|
</fieldset>
|
||||||
|
<p id="elige-err" class="status err" hidden>Selecciona un día para continuar.</p>
|
||||||
|
<div class="nav"><button id="elige-ok" type="button" class="btn primary">Continuar →</button></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="intro" class="lead-c" hidden>Queremos conocerte un poco antes de darle al REC. No hace falta que prepares respuestas perfectas: esto nos sirve para encontrar buenas historias, preguntas interesantes y, sobre todo, temas sobre los que merezca la pena hablar.</p>
|
||||||
|
|
||||||
|
<form id="g" class="form" novalidate hidden data-ok="¡Gracias! Hemos recibido tu ficha y te hemos enviado un correo con los datos de la grabación. Si no lo ves en unos minutos, mira en la carpeta de spam y añade contacto@andaquenosabesna.com a tus contactos.">
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>👤 Tú</legend>
|
||||||
|
<label><span>Nombre y cómo prefieres que te presentemos <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<input name="q01" type="text" maxlength="300" required>
|
||||||
|
</label>
|
||||||
|
<label><span>¿A qué te dedicas actualmente? <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<textarea name="q02" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Cuál es tu proyecto, empresa o actividad principal?</span>
|
||||||
|
<textarea name="q03" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>LinkedIn, web o cualquier enlace que nos ayude a conocerte mejor</span>
|
||||||
|
<input name="q04" type="text" maxlength="300">
|
||||||
|
</label>
|
||||||
|
<label><span>Tus redes sociales que quieras que mencionemos o utilicemos para promocionar el episodio</span>
|
||||||
|
<textarea name="q05" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>Tu correo electrónico <span class="req" title="Obligatoria">*</span></span><small>Te enviaremos un recordatorio de tu cita y así podemos responderte.</small>
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200" required>
|
||||||
|
</label>
|
||||||
|
<label><span>Tu teléfono móvil <span class="req" title="Obligatoria">*</span></span><small>Solo para contactarte el día de la grabación si surge algún imprevisto.</small>
|
||||||
|
<input name="tel" type="tel" autocomplete="tel" inputmode="tel" maxlength="20" required>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn primary next">Siguiente →</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🎙️ Para conocerte un poco mejor</legend>
|
||||||
|
<label><span>Si tuvieras que contarnos tu historia en dos minutos, ¿por dónde empezarías? <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<textarea name="q06" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Qué tema podrías estar hablando durante horas sin aburrirte? <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<textarea name="q07" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Hay alguna experiencia, proyecto, fracaso, éxito o anécdota que te parezca especialmente buena para contar en el podcast? <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<textarea name="q08" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Qué idea, mito o tópico de tu sector te gustaría desmontar?</span>
|
||||||
|
<textarea name="q09" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn ghost prev">← Anterior</button><button type="button" class="btn primary next">Siguiente →</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🔥 Vamos a meternos en harina</legend>
|
||||||
|
<p class="note">Contestar en esta sección es opcional</p>
|
||||||
|
<label><span>¿Hay alguna opinión tuya sobre tu sector que pueda generar debate?</span>
|
||||||
|
<textarea name="q10" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Qué pregunta te hacen siempre y estás cansado/a de responder?</span>
|
||||||
|
<textarea name="q11" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Hay algo que normalmente no se cuenta cuando se habla de tu profesión, sector o trayectoria y que crees que deberíamos contar?</span>
|
||||||
|
<textarea name="q12" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>Si pudiéramos conseguir que después de escucharte alguien cambiara de opinión sobre una cosa, ¿qué te gustaría que fuera?</span>
|
||||||
|
<textarea name="q13" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn ghost prev">← Anterior</button><button type="button" class="btn primary next">Siguiente →</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>🚧 Territorio delicado</legend>
|
||||||
|
<p class="note">No hace falta justificarlo. Queremos saber dónde están tus límites antes de empezar.</p>
|
||||||
|
<label><span>¿Hay algún tema que prefieras que no toquemos? <span class="req" title="Obligatoria">*</span></span>
|
||||||
|
<textarea name="q14" rows="4" maxlength="3000" required></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Hay alguna persona, empresa, experiencia o asunto sobre el que prefieras que no hagamos preguntas?</span>
|
||||||
|
<textarea name="q15" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>Y... ¿Hay algún otro tema de tu vida que quieras contarnos?</span>
|
||||||
|
<textarea name="q16" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn ghost prev">← Anterior</button><button type="button" class="btn primary next">Siguiente →</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>📣 Y para terminar...</legend>
|
||||||
|
<p class="note">Contestar en esta sección es opcional</p>
|
||||||
|
<label><span>¿Hay algún proyecto, libro, producto, evento o iniciativa que tengas ahora mismo y que te gustaría dar a conocer durante la entrevista?</span>
|
||||||
|
<textarea name="q17" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Hay alguna pregunta que te encantaría que te hiciéramos y que normalmente nadie te hace?</span>
|
||||||
|
<textarea name="q18" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<label><span>¿Hay alguna historia que te gustaría contar sí o sí?</span>
|
||||||
|
<textarea name="q19" rows="4" maxlength="3000"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn ghost prev">← Anterior</button><button type="button" class="btn primary next">Siguiente →</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset class="sec">
|
||||||
|
<legend>Antes de enviar</legend>
|
||||||
|
<p class="note">Este podcast se realiza de forma altruista y colaborativa.</p>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído y acepto las <a href="terminos.html#participacion" target="_blank" rel="noopener">condiciones de participación</a> descritas. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input name="privacy" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen estos datos para preparar el episodio. <span class="req">*</span></span>
|
||||||
|
</label>
|
||||||
|
<div class="nav"><button type="button" class="btn ghost prev">← Anterior</button><button class="btn primary" type="submit">Enviar mi ficha</button></div>
|
||||||
|
</fieldset>
|
||||||
|
<p id="paso" class="paso" aria-live="polite"></p>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
<p class="note">Tus respuestas se guardan en este navegador mientras escribes, por si cierras la página. Se borran al enviarlas.</p>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="invitados.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
(function () {
|
||||||
|
var $ = function (id) { return document.getElementById(id); };
|
||||||
|
var P = new URLSearchParams(location.search);
|
||||||
|
var n = (P.get('n') || '').trim();
|
||||||
|
var fe = (P.get('f') || '').trim();
|
||||||
|
var de = (P.get('d') || '').trim();
|
||||||
|
var k = n && (fe || de) ? n + (fe || de) : '';
|
||||||
|
var diaElegido = null;
|
||||||
|
var f = $('g'), st = $('st'), hola = $('hola'), malo = $('malo'), intro = $('intro'), cita = $('cita'), paso = $('paso');
|
||||||
|
var eligeDia = $('elige-dia'), diasFs = $('dias-fs'), eligeErr = $('elige-err'), eligeOk = $('elige-ok');
|
||||||
|
var steps = Array.prototype.slice.call(f.querySelectorAll('fieldset.sec'));
|
||||||
|
var cur = 0, t0 = Date.now(), busy = false, KEY = 'aqnsn-ficha-' + k.slice(0, 40);
|
||||||
|
function say(m, ok) { st.textContent = m; st.className = 'status ' + (ok ? 'ok' : 'err'); }
|
||||||
|
function store(op, v) { try { return op === 'get' ? localStorage.getItem(KEY) : op === 'set' ? localStorage.setItem(KEY, v) : localStorage.removeItem(KEY); } catch (e) { return null; } }
|
||||||
|
var okMail = function (v) { return /^[^@\s]+@[^@\s]+\.[^@\s]{2,}$/.test(v); };
|
||||||
|
if (!k) { malo.hidden = false; return; }
|
||||||
|
|
||||||
|
function show(i) {
|
||||||
|
cur = i;
|
||||||
|
steps.forEach(function (s, j) { s.hidden = j !== i; });
|
||||||
|
paso.textContent = 'Paso ' + (i + 1) + ' de ' + steps.length;
|
||||||
|
say('', true);
|
||||||
|
var top = hola.getBoundingClientRect().top + window.scrollY - 12;
|
||||||
|
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
function validar(i) {
|
||||||
|
var s = steps[i], els = s.querySelectorAll('input[required],textarea[required]');
|
||||||
|
for (var j = 0; j < els.length; j++) {
|
||||||
|
var el = els[j], bad = el.type === 'checkbox' ? !el.checked : !el.value.trim();
|
||||||
|
if (bad) { el.focus(); say(el.type === 'checkbox' ? 'Marca las dos casillas finales para poder enviar la ficha.' : 'Faltan respuestas obligatorias (las marcadas con *).', false); return false; }
|
||||||
|
}
|
||||||
|
var t = s.querySelector('input[type=tel]');
|
||||||
|
if (t && !/^\+?[\d ().-]{7,20}$/.test(t.value.trim())) { t.focus(); say('El teléfono no parece válido.', false); return false; }
|
||||||
|
var m = s.querySelector('input[type=email]');
|
||||||
|
if (m && !okMail(m.value.trim())) { m.focus(); say('El correo no parece válido.', false); return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
f.addEventListener('click', function (e) {
|
||||||
|
var b = e.target.closest && e.target.closest('button.next,button.prev');
|
||||||
|
if (!b) return;
|
||||||
|
if (b.classList.contains('prev')) show(cur - 1);
|
||||||
|
else if (validar(cur)) show(cur + 1);
|
||||||
|
});
|
||||||
|
f.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter' && e.target.tagName === 'INPUT' && e.target.type !== 'checkbox' && cur < steps.length - 1) { e.preventDefault(); if (validar(cur)) show(cur + 1); }
|
||||||
|
});
|
||||||
|
|
||||||
|
function mostrarFormulario() {
|
||||||
|
intro.hidden = false; f.hidden = false;
|
||||||
|
try { var s = JSON.parse(store('get') || '{}'); Object.keys(s).forEach(function (nm) { if (f.elements[nm] && f.elements[nm].type !== 'checkbox') f.elements[nm].value = s[nm]; }); } catch (e) {}
|
||||||
|
show(0);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkUrl = '/api/invitados/check?n=' + encodeURIComponent(n) + (fe ? '&f=' + encodeURIComponent(fe) : '&d=' + encodeURIComponent(de));
|
||||||
|
fetch(checkUrl, { cache: 'no-store' })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (j) {
|
||||||
|
if (!j.ok) { malo.hidden = false; return; }
|
||||||
|
hola.textContent = 'Hola, ' + j.nombre + '. ¡Gracias por querer venir al programa!';
|
||||||
|
hola.hidden = false;
|
||||||
|
if (j.cita) {
|
||||||
|
var d = new Date(j.cita * 1000), o = { timeZone: 'Europe/Madrid' };
|
||||||
|
var dia = d.toLocaleDateString('es-ES', Object.assign({ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }, o)).replace(',', '');
|
||||||
|
var hora = d.toLocaleTimeString('es-ES', Object.assign({ hour: '2-digit', minute: '2-digit' }, o));
|
||||||
|
cita.textContent = '📅 Recuerda que grabamos el ' + dia + ' a las ' + hora + ', y te enviaremos por correo electrónico la dirección del estudio.';
|
||||||
|
cita.hidden = false;
|
||||||
|
mostrarFormulario();
|
||||||
|
} else if (j.dias && j.dias.length) {
|
||||||
|
j.dias.forEach(function (op) {
|
||||||
|
var lbl = document.createElement('label');
|
||||||
|
lbl.className = 'check';
|
||||||
|
var inp = document.createElement('input');
|
||||||
|
inp.type = 'radio'; inp.name = 'dia-opcion'; inp.value = op.ts;
|
||||||
|
var span = document.createElement('span');
|
||||||
|
span.textContent = op.txt;
|
||||||
|
lbl.appendChild(inp); lbl.appendChild(span);
|
||||||
|
diasFs.appendChild(lbl);
|
||||||
|
});
|
||||||
|
eligeDia.hidden = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { malo.hidden = false; });
|
||||||
|
|
||||||
|
eligeOk.addEventListener('click', function () {
|
||||||
|
var r = diasFs.querySelector('input[name="dia-opcion"]:checked');
|
||||||
|
if (!r) { eligeErr.hidden = false; return; }
|
||||||
|
eligeErr.hidden = true;
|
||||||
|
diaElegido = r.value;
|
||||||
|
var lbl = r.closest('label');
|
||||||
|
cita.textContent = '📅 Has elegido grabar el ' + (lbl ? lbl.querySelector('span').textContent : '') + '. Te enviaremos por correo la dirección del estudio.';
|
||||||
|
cita.hidden = false;
|
||||||
|
eligeDia.hidden = true;
|
||||||
|
mostrarFormulario();
|
||||||
|
});
|
||||||
|
|
||||||
|
var timer;
|
||||||
|
f.addEventListener('input', function () {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(function () {
|
||||||
|
var d = {};
|
||||||
|
Array.prototype.forEach.call(f.elements, function (el) { if (el.name && el.type !== 'checkbox' && el.name !== 'web' && el.value) d[el.name] = el.value; });
|
||||||
|
store('set', JSON.stringify(d));
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
|
||||||
|
f.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (busy) return;
|
||||||
|
for (var i = 0; i < steps.length; i++) { if (!validar(i)) { if (i !== cur) show(i); return; } }
|
||||||
|
var resp = {};
|
||||||
|
Array.prototype.forEach.call(f.elements, function (el) { if (/^q\d+$/.test(el.name)) resp[el.name] = el.value.trim(); });
|
||||||
|
busy = true; say('Enviando…', true);
|
||||||
|
fetch('/api/invitados', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ n: n, f: fe, d: de, dia: diaElegido || '', email: f.email.value.trim(), tel: f.tel.value.trim(), resp: resp, consent: true, web: f.web.value || '', t: Date.now() - t0 }) })
|
||||||
|
.then(function (r) { return r.json().catch(function () { return {}; }).then(function (j) { return { ok: r.ok && j.ok, j: j }; }); })
|
||||||
|
.then(function (x) {
|
||||||
|
if (x.ok) { store('del'); f.reset(); steps.forEach(function (s) { s.hidden = true; }); paso.hidden = true; f.querySelector('.note:last-of-type').hidden = true; say(f.getAttribute('data-ok'), true); }
|
||||||
|
else say((x.j && x.j.error) || 'No se pudo enviar. Cuéntanos qué ha pasado en andaquenosabesna.com/contacto.html', false);
|
||||||
|
})
|
||||||
|
.catch(function () { say('No se pudo enviar. Cuéntanos qué ha pasado en andaquenosabesna.com/contacto.html', false); })
|
||||||
|
.then(function () { busy = false; });
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
|
<title>Nuevo enlace de invitado · AQNSN</title>
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
<style>
|
||||||
|
.gen-form { display: grid; gap: 20px; max-width: 560px; }
|
||||||
|
.gen-form label { display: grid; gap: 6px; font-weight: 600; font-size: .95rem; }
|
||||||
|
.gen-form input[type=text], .gen-form input[type=password], .gen-form input[type=datetime-local] {
|
||||||
|
width: 100%; padding: 13px 15px; border-radius: 12px; border: 1px solid var(--line);
|
||||||
|
background: var(--card); color: var(--ink); font: inherit; font-weight: 400; transition: .2s;
|
||||||
|
}
|
||||||
|
.gen-form input:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 3px rgba(46,230,255,.18); }
|
||||||
|
.dias-list { display: grid; gap: 10px; }
|
||||||
|
.dia-row { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.dia-row input { flex: 1; }
|
||||||
|
.dia-row .del-dia { background: transparent; border: 1px solid var(--line); color: var(--muted);
|
||||||
|
padding: 8px 13px; border-radius: 8px; font: inherit; cursor: pointer; transition: .2s; flex: none; }
|
||||||
|
.dia-row .del-dia:hover { border-color: #ff8a8a; color: #ff8a8a; }
|
||||||
|
.add-dia { background: transparent; border: 1px dashed var(--line); color: var(--muted);
|
||||||
|
padding: 11px 18px; border-radius: 12px; font: inherit; cursor: pointer; width: 100%; transition: .2s; }
|
||||||
|
.add-dia:hover { border-color: var(--neon); color: var(--neon); }
|
||||||
|
.result-box { display: none; margin-top: 8px; padding: 16px 18px; border: 1px solid var(--line);
|
||||||
|
border-left: 3px solid var(--neon); border-radius: 12px; background: var(--card); }
|
||||||
|
.result-box .url { word-break: break-all; font-size: .92rem; color: var(--neon); margin: 0 0 12px; }
|
||||||
|
.copy-btn { padding: 9px 20px; border-radius: 999px; border: 1px solid var(--neon);
|
||||||
|
background: transparent; color: var(--neon); font: inherit; font-weight: 700; cursor: pointer; transition: .2s; }
|
||||||
|
.copy-btn:hover { background: var(--neon); color: #001a20; }
|
||||||
|
.lock { max-width: 340px; }
|
||||||
|
small { color: var(--muted); font-weight: 400; font-size: .88rem; }
|
||||||
|
#login-err { color: #ff8a8a; font-size: .92rem; min-height: 1.4em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact">
|
||||||
|
<h1>Nuevo <em>enlace</em> de invitado</h1>
|
||||||
|
|
||||||
|
<div id="login-panel" hidden>
|
||||||
|
<p class="lead-c">Solo para el equipo del programa.</p>
|
||||||
|
<form id="login-form" class="gen-form lock" novalidate>
|
||||||
|
<label>Contraseña
|
||||||
|
<input id="pwd" type="password" autocomplete="current-password" required>
|
||||||
|
</label>
|
||||||
|
<p id="login-err"></p>
|
||||||
|
<button class="btn primary" type="submit">Entrar</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="gen-panel" hidden>
|
||||||
|
<p class="lead-c">Rellena el nombre del invitado y los días propuestos. Se generará un enlace que podrás enviarle.</p>
|
||||||
|
<form id="gen-form" class="gen-form" novalidate autocomplete="off">
|
||||||
|
<label>Nombre del invitado <span class="req">*</span>
|
||||||
|
<input id="nombre" type="text" maxlength="60" placeholder="Ej: Ana García" required>
|
||||||
|
<small>Tal como quieres presentarle en el programa.</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;font-size:.95rem;margin-bottom:8px">Días propuestos <span class="req">*</span></div>
|
||||||
|
<small style="display:block;margin-bottom:12px">Añade entre 1 y 5 opciones. La invitada elegirá la que mejor le venga.</small>
|
||||||
|
<div id="dias-list" class="dias-list"></div>
|
||||||
|
<button type="button" id="add-dia" class="add-dia">+ Añadir otro día</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div style="font-weight:600;font-size:.95rem;margin-bottom:8px">Modo de enlace</div>
|
||||||
|
<label class="check" style="margin-bottom:8px">
|
||||||
|
<input type="radio" name="modo" value="propuestos" checked>
|
||||||
|
<span>Días propuestos — la invitada elige</span>
|
||||||
|
</label>
|
||||||
|
<label class="check">
|
||||||
|
<input type="radio" name="modo" value="fijo">
|
||||||
|
<span>Fecha fija — ya está confirmada</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="gen-err" class="status err" hidden></p>
|
||||||
|
<button class="btn primary" type="submit">Generar enlace</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="result" class="result-box">
|
||||||
|
<p class="url" id="result-url"></p>
|
||||||
|
<button class="copy-btn" id="copy-btn">Copiar enlace</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading" style="color:var(--muted);font-size:.95rem">Cargando…</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var SESSION_KEY = 'aqnsn-gen-token';
|
||||||
|
var loginPanel = document.getElementById('login-panel');
|
||||||
|
var genPanel = document.getElementById('gen-panel');
|
||||||
|
var loginForm = document.getElementById('login-form');
|
||||||
|
var loginErr = document.getElementById('login-err');
|
||||||
|
var pwd = document.getElementById('pwd');
|
||||||
|
var loading = document.getElementById('loading');
|
||||||
|
|
||||||
|
function showLogin() { loading.hidden = true; loginPanel.hidden = false; }
|
||||||
|
function showGen() { loading.hidden = true; loginPanel.hidden = true; genPanel.hidden = false; }
|
||||||
|
|
||||||
|
var stored = '';
|
||||||
|
try { stored = sessionStorage.getItem(SESSION_KEY) || ''; } catch (e) {}
|
||||||
|
|
||||||
|
if (stored) {
|
||||||
|
fetch('/api/gen-verify?t=' + encodeURIComponent(stored), { cache: 'no-store' })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (j) { j.ok ? showGen() : showLogin(); })
|
||||||
|
.catch(function () { showLogin(); });
|
||||||
|
} else {
|
||||||
|
showLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
loginForm.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
loginErr.textContent = 'Verificando…';
|
||||||
|
fetch('/api/gen-auth', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ pass: pwd.value })
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (j) {
|
||||||
|
if (j.ok) {
|
||||||
|
try { sessionStorage.setItem(SESSION_KEY, j.token); } catch (e) {}
|
||||||
|
showGen();
|
||||||
|
} else {
|
||||||
|
loginErr.textContent = j.error || 'Contraseña incorrecta.';
|
||||||
|
pwd.value = ''; pwd.focus();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () { loginErr.textContent = 'Error de conexión.'; });
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Generador ---
|
||||||
|
var diasList = document.getElementById('dias-list');
|
||||||
|
var addDiaBtn = document.getElementById('add-dia');
|
||||||
|
var genForm = document.getElementById('gen-form');
|
||||||
|
var genErr = document.getElementById('gen-err');
|
||||||
|
var result = document.getElementById('result');
|
||||||
|
var resultUrl = document.getElementById('result-url');
|
||||||
|
var copyBtn = document.getElementById('copy-btn');
|
||||||
|
|
||||||
|
function addDia() {
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'dia-row';
|
||||||
|
var inp = document.createElement('input');
|
||||||
|
inp.type = 'datetime-local'; inp.className = 'dia-inp';
|
||||||
|
var del = document.createElement('button');
|
||||||
|
del.type = 'button'; del.className = 'del-dia'; del.textContent = '✕';
|
||||||
|
del.addEventListener('click', function () { row.remove(); actualizarAddBtn(); });
|
||||||
|
row.appendChild(inp); row.appendChild(del);
|
||||||
|
diasList.appendChild(row);
|
||||||
|
actualizarAddBtn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function actualizarAddBtn() {
|
||||||
|
addDiaBtn.style.display = diasList.children.length >= 5 ? 'none' : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
addDia();
|
||||||
|
addDiaBtn.addEventListener('click', function () { addDia(); });
|
||||||
|
|
||||||
|
genForm.addEventListener('submit', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
genErr.hidden = true;
|
||||||
|
var nombre = document.getElementById('nombre').value.trim();
|
||||||
|
if (!nombre) { genErr.textContent = 'Escribe el nombre del invitado.'; genErr.hidden = false; return; }
|
||||||
|
|
||||||
|
var modo = genForm.querySelector('input[name="modo"]:checked').value;
|
||||||
|
var inps = Array.prototype.slice.call(diasList.querySelectorAll('input.dia-inp'));
|
||||||
|
var timestamps = [];
|
||||||
|
for (var i = 0; i < inps.length; i++) {
|
||||||
|
var v = inps[i].value;
|
||||||
|
if (!v) continue;
|
||||||
|
var ts = Math.round(new Date(v).getTime() / 1000);
|
||||||
|
if (!isNaN(ts)) timestamps.push(ts);
|
||||||
|
}
|
||||||
|
if (timestamps.length === 0) { genErr.textContent = 'Añade al menos un día.'; genErr.hidden = false; return; }
|
||||||
|
|
||||||
|
var base = location.origin + '/invitados.html';
|
||||||
|
var url;
|
||||||
|
if (modo === 'fijo') {
|
||||||
|
if (timestamps.length > 1) { genErr.textContent = 'En modo fecha fija solo puedes indicar un día.'; genErr.hidden = false; return; }
|
||||||
|
url = base + '?n=' + encodeURIComponent(nombre) + '&f=' + timestamps[0];
|
||||||
|
} else {
|
||||||
|
url = base + '?n=' + encodeURIComponent(nombre) + '&d=' + timestamps.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
resultUrl.textContent = url;
|
||||||
|
result.style.display = 'block';
|
||||||
|
result.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
|
});
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', function () {
|
||||||
|
var url = resultUrl.textContent;
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(url).then(function () {
|
||||||
|
copyBtn.textContent = '¡Copiado!';
|
||||||
|
setTimeout(function () { copyBtn.textContent = 'Copiar enlace'; }, 2000);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var ta = document.createElement('textarea');
|
||||||
|
ta.value = url; document.body.appendChild(ta); ta.select(); document.execCommand('copy');
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
copyBtn.textContent = '¡Copiado!';
|
||||||
|
setTimeout(function () { copyBtn.textContent = 'Copiar enlace'; }, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Patrocinadores · Anda Que No Sabes Ná</title>
|
||||||
|
<meta name="description" content="¿Quieres que tu marca suene en Anda Que No Sabes Ná? Cuéntanos qué tienes en mente.">
|
||||||
|
<meta name="theme-color" content="#06080d">
|
||||||
|
<link rel="icon" href="img/avatar.jpg">
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<a class="brand" href="/" aria-label="Anda Que No Sabes Ná, inicio"><img src="img/avatar.jpg" alt="" width="40" height="40"><span>Anda que no sabes ná</span></a>
|
||||||
|
<nav aria-label="Principal"><a href="/">Inicio</a><a href="contacto.html">Contacto</a></nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="legal contact">
|
||||||
|
<h1>¿Quieres <em>anunciarte</em> en el programa?</h1>
|
||||||
|
<p class="lead-c">Si tu marca quiere sonar en Anda Que No Sabes Ná, escríbenos. Cuéntanos quién eres, qué tienes en mente y qué te interesa: una mención, patrocinar episodios, una colaboración… Lo hablamos con calma.</p>
|
||||||
|
|
||||||
|
<form id="f" class="form" novalidate data-endpoint="/api/patrocinio" data-mail="contacto@andaquenosabesna.com" data-ok="¡Gracias! Hemos recibido tu propuesta y te escribiremos pronto.">
|
||||||
|
<label>Empresa o marca
|
||||||
|
<input name="empresa" type="text" autocomplete="organization" maxlength="120" required>
|
||||||
|
</label>
|
||||||
|
<label>Tu nombre
|
||||||
|
<input name="nombre" type="text" autocomplete="name" maxlength="100" required>
|
||||||
|
</label>
|
||||||
|
<label>Correo electrónico
|
||||||
|
<input name="email" type="email" autocomplete="email" maxlength="200" required>
|
||||||
|
</label>
|
||||||
|
<label>Web (opcional)
|
||||||
|
<input name="sitio" type="text" inputmode="url" maxlength="200" placeholder="https://" autocomplete="url">
|
||||||
|
</label>
|
||||||
|
<label>¿Qué te interesa?
|
||||||
|
<select name="tipo">
|
||||||
|
<option value="mencion">Anunciarme / que me mencionéis en el programa</option>
|
||||||
|
<option value="patrocinio">Patrocinar episodios o una temporada</option>
|
||||||
|
<option value="colab">Colaboración o canje</option>
|
||||||
|
<option value="otro">Otra propuesta</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Cuéntanos tu propuesta
|
||||||
|
<textarea name="mensaje" rows="6" maxlength="3000" required placeholder="Qué hacéis, a quién queréis llegar, qué tenéis en mente…"></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="trap" aria-hidden="true"><label>No rellenes este campo <input name="web" type="text" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<label class="check">
|
||||||
|
<input name="consent" type="checkbox" required>
|
||||||
|
<span>He leído la <a href="privacidad.html" target="_blank" rel="noopener">política de privacidad</a> y acepto que usen mis datos para responderme.</span>
|
||||||
|
</label>
|
||||||
|
<button class="btn primary" type="submit">Enviar propuesta</button>
|
||||||
|
<p id="st" class="status" role="status" aria-live="polite"></p>
|
||||||
|
</form>
|
||||||
|
<noscript><p>Necesitas JavaScript para usar el formulario. Mientras tanto, escríbenos a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p></noscript>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="form.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+15
-9
@@ -20,15 +20,21 @@
|
|||||||
<p class="upd">Última actualización: 20 de octubre de 2026</p>
|
<p class="upd">Última actualización: 20 de octubre de 2026</p>
|
||||||
|
|
||||||
<h2>1. Quién somos</h2>
|
<h2>1. Quién somos</h2>
|
||||||
<p><strong>Anda Que No Sabes Ná</strong> es un programa de pódcast y vídeo producido en Granada (España). Esta política cubre la web <a href="https://andaquenosabesna.thax.es">andaquenosabesna.thax.es</a> y la aplicación privada que usamos para publicar nuestros propios vídeos en YouTube, TikTok e Instagram.</p>
|
<p><strong>Anda Que No Sabes Ná</strong> es un programa de pódcast y vídeo producido en Granada (España). Esta política cubre la web <a href="https://andaquenosabesna.com">andaquenosabesna.com</a> y la aplicación privada que usamos para publicar nuestros propios vídeos en YouTube, TikTok e Instagram.</p>
|
||||||
<p>Contacto para cualquier asunto de privacidad: <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p>
|
<p>Contacto para cualquier asunto de privacidad: <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p>
|
||||||
|
|
||||||
<h2>2. Esta web</h2>
|
<h2>2. Esta web</h2>
|
||||||
<p>La web es estática. <strong>No usa cookies, no tiene analítica, no tiene formularios ni cuentas de usuario</strong> y no carga scripts, fuentes ni imágenes de terceros: las miniaturas están alojadas en este mismo sitio.</p>
|
<p>La web es estática. <strong>No usa cookies, no tiene analítica, no tiene formularios ni cuentas de usuario</strong> y no carga scripts, fuentes ni imágenes de terceros: las miniaturas están alojadas en este mismo sitio.</p>
|
||||||
<p>Como cualquier servidor web, nuestro servidor puede registrar de forma temporal la dirección IP, la fecha y la página solicitada, únicamente por seguridad y para detectar abusos. No usamos esos registros para identificar a nadie ni los compartimos.</p>
|
<p>Como cualquier servidor web, nuestro servidor puede registrar de forma temporal la dirección IP, la fecha y la página solicitada, únicamente por seguridad y para detectar abusos. No usamos esos registros para identificar a nadie ni los compartimos.</p>
|
||||||
<p>Los enlaces a YouTube, TikTok e Instagram llevan a servicios de terceros, que tienen sus propias políticas de privacidad.</p>
|
<p>Los enlaces a YouTube, TikTok e Instagram llevan a servicios de terceros, que tienen sus propias políticas de privacidad.</p>
|
||||||
|
|
||||||
<h2>3. Nuestra aplicación de publicación</h2>
|
<h2>3. Formularios de contacto, patrocinio y ficha de invitados</h2>
|
||||||
|
<p>Si nos escribes desde la página de <a href="contacto.html">contacto</a> o de <a href="patrocinadores.html">patrocinadores</a>, recogemos los datos que tú mismo introduces: nombre, correo electrónico, mensaje y motivo o tipo de colaboración y, en el formulario de patrocinio, también el nombre de la empresa y su web si la indicas. Los usamos <strong>únicamente para responderte</strong>. La base legal es tu consentimiento, que das al marcar la casilla del formulario.</p>
|
||||||
|
<p>La web no guarda los mensajes: el formulario los reenvía por correo electrónico, a través de nuestro propio servidor de correo, a la cuenta del programa. Los conservamos solo el tiempo necesario para atender tu consulta y no los cedemos a terceros ni los usamos para otra cosa. Los formularios incluyen medidas antispam que no identifican a nadie. Puedes pedirnos que borremos tus datos escribiendo al correo de contacto.</p>
|
||||||
|
|
||||||
|
<p><strong>Ficha del invitado.</strong> Si participas como invitado y rellenas la ficha (a la que se accede solo mediante un enlace personal), recogemos tu correo electrónico (para enviarte el recordatorio de la cita y la dirección del estudio), tu teléfono móvil (solo para contactarte el día de la grabación si surge algún imprevisto) y las respuestas que escribes, que pueden incluir datos sobre tu actividad profesional, tu trayectoria y, si tú decides contarlo, aspectos personales. Las usamos únicamente para preparar la entrevista y el episodio, y se envían por correo electrónico al equipo del programa; la web no las guarda. No las cedemos a terceros. La base legal es tu consentimiento, que das al aceptar las <a href="terminos.html#participacion">condiciones de participación</a> y esta política, y puedes pedirnos que las borremos en cualquier momento.</p>
|
||||||
|
|
||||||
|
<h2>4. Nuestra aplicación de publicación</h2>
|
||||||
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
|
<p>Usamos una aplicación de gestión de redes sociales de código abierto (Postiz), instalada en un servidor propio, para programar y publicar el contenido del programa. La aplicación está pensada para uso interno: <strong>solo la utiliza el propietario de las cuentas</strong> y no está abierta al público.</p>
|
||||||
<p>Para funcionar, la aplicación solicita autorización a las cuentas del propio programa y accede a:</p>
|
<p>Para funcionar, la aplicación solicita autorización a las cuentas del propio programa y accede a:</p>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -38,21 +44,21 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<p>Los tokens de acceso se guardan en una base de datos de nuestro servidor. <strong>No vendemos, alquilamos ni compartimos estos datos con nadie, no los usamos para publicidad ni los transferimos a terceros.</strong> Usamos los datos solo para publicar y programar el contenido del programa en sus propias cuentas.</p>
|
<p>Los tokens de acceso se guardan en una base de datos de nuestro servidor. <strong>No vendemos, alquilamos ni compartimos estos datos con nadie, no los usamos para publicidad ni los transferimos a terceros.</strong> Usamos los datos solo para publicar y programar el contenido del programa en sus propias cuentas.</p>
|
||||||
|
|
||||||
<h2>4. Servicios de API de YouTube</h2>
|
<h2>5. Servicios de API de YouTube</h2>
|
||||||
<p>La aplicación usa los Servicios de API de YouTube. Al utilizarla, aceptas quedar sujeto a los <a href="https://www.youtube.com/t/terms" target="_blank" rel="noopener">Términos del Servicio de YouTube</a>. La política de privacidad de Google está disponible en <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">policies.google.com/privacy</a>.</p>
|
<p>La aplicación usa los Servicios de API de YouTube. Al utilizarla, aceptas quedar sujeto a los <a href="https://www.youtube.com/t/terms" target="_blank" rel="noopener">Términos del Servicio de YouTube</a>. La política de privacidad de Google está disponible en <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">policies.google.com/privacy</a>.</p>
|
||||||
<p>El uso que hace la aplicación de la información recibida de las API de Google se ajusta a la <a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener">Política de datos de usuario de los Servicios de API de Google</a>, incluidos los requisitos de uso limitado.</p>
|
<p>El uso que hace la aplicación de la información recibida de las API de Google se ajusta a la <a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noopener">Política de datos de usuario de los Servicios de API de Google</a>, incluidos los requisitos de uso limitado.</p>
|
||||||
<p>Puedes revocar en cualquier momento el acceso de la aplicación a tu cuenta de Google desde <a href="https://myaccount.google.com/connections" target="_blank" rel="noopener">myaccount.google.com/connections</a>.</p>
|
<p>Puedes revocar en cualquier momento el acceso de la aplicación a tu cuenta de Google desde <a href="https://myaccount.google.com/connections" target="_blank" rel="noopener">myaccount.google.com/connections</a>.</p>
|
||||||
|
|
||||||
<h2>5. TikTok e Instagram</h2>
|
<h2>6. TikTok e Instagram</h2>
|
||||||
<p>El acceso a TikTok se rige también por los <a href="https://www.tiktok.com/legal/page/row/terms-of-service/es" target="_blank" rel="noopener">Términos del Servicio de TikTok</a> y su política de privacidad. El acceso a Instagram, por las condiciones y la política de datos de Meta. Puedes revocar el acceso de la aplicación desde la configuración de seguridad de cada plataforma.</p>
|
<p>El acceso a TikTok se rige también por los <a href="https://www.tiktok.com/legal/page/row/terms-of-service/es" target="_blank" rel="noopener">Términos del Servicio de TikTok</a> y su política de privacidad. El acceso a Instagram, por las condiciones y la política de datos de Meta. Puedes revocar el acceso de la aplicación desde la configuración de seguridad de cada plataforma.</p>
|
||||||
|
|
||||||
<h2>6. Conservación y eliminación</h2>
|
<h2>7. Conservación y eliminación</h2>
|
||||||
<p>Conservamos los tokens solo mientras la cuenta esté conectada. Si desconectamos una cuenta o revocas el acceso, el token deja de ser válido y se elimina de nuestra base de datos. Si quieres que borremos cualquier dato asociado a ti, escríbenos al correo de contacto.</p>
|
<p>Conservamos los tokens solo mientras la cuenta esté conectada. Si desconectamos una cuenta o revocas el acceso, el token deja de ser válido y se elimina de nuestra base de datos. Si quieres que borremos cualquier dato asociado a ti, escríbenos al correo de contacto.</p>
|
||||||
|
|
||||||
<h2>7. Tus derechos</h2>
|
<h2>8. Tus derechos</h2>
|
||||||
<p>Conforme al Reglamento General de Protección de Datos (RGPD) y a la normativa española, puedes ejercer tus derechos de acceso, rectificación, supresión, oposición, limitación y portabilidad escribiendo a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>. También puedes reclamar ante la Agencia Española de Protección de Datos (<a href="https://www.aepd.es" target="_blank" rel="noopener">aepd.es</a>).</p>
|
<p>Conforme al Reglamento General de Protección de Datos (RGPD) y a la normativa española, puedes ejercer tus derechos de acceso, rectificación, supresión, oposición, limitación y portabilidad escribiendo a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>. También puedes reclamar ante la Agencia Española de Protección de Datos (<a href="https://www.aepd.es" target="_blank" rel="noopener">aepd.es</a>).</p>
|
||||||
|
|
||||||
<h2>8. Cambios</h2>
|
<h2>9. Cambios</h2>
|
||||||
<p>Si cambiamos esta política, publicaremos la nueva versión en esta misma página con su fecha de actualización.</p>
|
<p>Si cambiamos esta política, publicaremos la nueva versión en esta misma página con su fecha de actualización.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+93
-11
@@ -36,9 +36,10 @@ a { color: inherit; text-decoration: none; }
|
|||||||
backdrop-filter: blur(6px);
|
backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: .02em; }
|
.brand { display: flex; align-items: center; gap: 10px; font-weight: 600; letter-spacing: .02em; }
|
||||||
.brand img { border-radius: 50%; box-shadow: 0 0 14px rgba(46, 230, 255, .5); }
|
.brand img { aspect-ratio: 1 / 1; object-fit: cover; border-radius: 50%; box-shadow: 0 0 14px rgba(46, 230, 255, .5); }
|
||||||
.top nav { display: flex; gap: 22px; font-size: .95rem; color: var(--muted); }
|
.top nav { display: flex; gap: 22px; font-size: .95rem; color: var(--muted); }
|
||||||
.top nav a:hover { color: var(--neon); }
|
.top nav a:hover { color: var(--neon); }
|
||||||
|
.top nav .navcta { color: var(--neon); border: 1px solid rgba(46, 230, 255, .4); padding: 2px 14px; border-radius: 999px; }
|
||||||
@media (max-width: 640px) { .brand span { display: none; } .top nav { gap: 16px; } }
|
@media (max-width: 640px) { .brand span { display: none; } .top nav { gap: 16px; } }
|
||||||
|
|
||||||
/* hero */
|
/* hero */
|
||||||
@@ -49,17 +50,20 @@ a { color: inherit; text-decoration: none; }
|
|||||||
.hero-bg {
|
.hero-bg {
|
||||||
position: absolute; inset: 0;
|
position: absolute; inset: 0;
|
||||||
background:
|
background:
|
||||||
radial-gradient(60% 50% at 50% 38%, rgba(46, 230, 255, .16), transparent 70%),
|
radial-gradient(60% 50% at 50% 38%, rgba(46, 230, 255, .18), transparent 70%),
|
||||||
linear-gradient(180deg, rgba(6, 8, 13, .78) 0%, rgba(6, 8, 13, .92) 65%, var(--bg) 100%),
|
linear-gradient(180deg, rgba(6, 8, 13, .60) 0%, var(--bg) 100%);
|
||||||
url("img/banner.jpg") center / cover no-repeat;
|
|
||||||
filter: blur(2px) saturate(.9);
|
|
||||||
transform: scale(1.04);
|
|
||||||
animation: drift 18s ease-in-out infinite alternate;
|
|
||||||
}
|
}
|
||||||
@keyframes drift { to { transform: scale(1.1) translateY(-1.5%); } }
|
@media (max-width: 760px) {
|
||||||
|
.hero-bg {
|
||||||
|
background:
|
||||||
|
radial-gradient(70% 38% at 50% 40%, rgba(46, 230, 255, .16), transparent 70%),
|
||||||
|
linear-gradient(180deg, rgba(6, 8, 13, .60) 0%, var(--bg) 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes drift { to { transform: scale(1.08) translateY(-1%); } }
|
||||||
.hero-inner { position: relative; max-width: 760px; }
|
.hero-inner { position: relative; max-width: 760px; }
|
||||||
.neon {
|
.neon {
|
||||||
width: clamp(130px, 22vw, 190px); margin: 0 auto 18px; border-radius: 50%;
|
width: clamp(130px, 22vw, 190px); height: auto; aspect-ratio: 1 / 1; object-fit: cover; margin: 0 auto 18px; border-radius: 50%;
|
||||||
border: 1px solid rgba(46, 230, 255, .35);
|
border: 1px solid rgba(46, 230, 255, .35);
|
||||||
animation: flicker 6s infinite;
|
animation: flicker 6s infinite;
|
||||||
}
|
}
|
||||||
@@ -135,8 +139,8 @@ h2 em { font-style: normal; color: var(--neon); text-shadow: 0 0 18px rgba(46, 2
|
|||||||
.meta strong { font-size: 1rem; line-height: 1.3; font-weight: 600; }
|
.meta strong { font-size: 1rem; line-height: 1.3; font-weight: 600; }
|
||||||
|
|
||||||
/* about */
|
/* about */
|
||||||
.about { max-width: 780px; }
|
.about { max-width: none; }
|
||||||
.about p { color: #c3d3e3; font-size: 1.1rem; }
|
.about p { color: #c3d3e3; font-size: 1.1rem; max-width: 780px; }
|
||||||
.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 10px; padding: 0; margin: 26px 0 0; }
|
.chips { list-style: none; display: flex; flex-wrap: wrap; gap: 10px; padding: 0; margin: 26px 0 0; }
|
||||||
.chips li { padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--muted); font-size: .92rem; transition: .25s; }
|
.chips li { padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line); background: var(--card); color: var(--muted); font-size: .92rem; transition: .25s; }
|
||||||
.chips li:hover { border-color: var(--neon); color: var(--neon); box-shadow: 0 0 16px rgba(46, 230, 255, .25); }
|
.chips li:hover { border-color: var(--neon); color: var(--neon); box-shadow: 0 0 16px rgba(46, 230, 255, .25); }
|
||||||
@@ -174,3 +178,81 @@ footer { margin-top: 40px; border-top: 1px solid var(--line); background: var(--
|
|||||||
.legal p, .legal li { color: #c3d3e3; }
|
.legal p, .legal li { color: #c3d3e3; }
|
||||||
.legal a { color: var(--neon); text-decoration: underline; text-underline-offset: 3px; }
|
.legal a { color: var(--neon); text-decoration: underline; text-underline-offset: 3px; }
|
||||||
.legal .upd { color: var(--muted); font-size: .9rem; }
|
.legal .upd { color: var(--muted); font-size: .9rem; }
|
||||||
|
|
||||||
|
/* contacto */
|
||||||
|
.contact h1 em { font-style: normal; color: var(--neon); text-shadow: var(--glow); }
|
||||||
|
.lead-c { color: #c3d3e3; font-size: 1.08rem; margin-bottom: 28px; }
|
||||||
|
.form { display: grid; gap: 18px; }
|
||||||
|
.form label { display: grid; gap: 6px; font-weight: 600; font-size: .95rem; }
|
||||||
|
.form input[type=text], .form input[type=email], .form input[type=tel], .form select, .form textarea {
|
||||||
|
width: 100%; padding: 13px 15px; border-radius: 12px; border: 1px solid var(--line);
|
||||||
|
background: var(--card); color: var(--ink); font: inherit; font-weight: 400; transition: .2s;
|
||||||
|
}
|
||||||
|
.form textarea { resize: vertical; min-height: 140px; }
|
||||||
|
.form input:focus, .form select:focus, .form textarea:focus { outline: none; border-color: var(--neon); box-shadow: 0 0 0 3px rgba(46, 230, 255, .18); }
|
||||||
|
.form .check, .guest .check { display: flex; align-items: flex-start; gap: 10px; font-weight: 400; color: var(--muted); font-size: .92rem; }
|
||||||
|
.form .check input, .guest .check input { margin-top: 4px; accent-color: var(--neon); width: 18px; height: 18px; flex: none; }
|
||||||
|
.form .check a, .guest .check a { color: var(--neon); text-decoration: underline; text-underline-offset: 3px; }
|
||||||
|
.form .btn { justify-self: start; cursor: pointer; font: inherit; font-weight: 700; }
|
||||||
|
.trap { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
||||||
|
.status { min-height: 1.4em; margin: 0; font-size: .95rem; }
|
||||||
|
.status.ok { color: #6ee7a8; }
|
||||||
|
.status.err { color: #ff8a8a; }
|
||||||
|
|
||||||
|
/* quiénes somos + invitación */
|
||||||
|
.who { max-width: none; }
|
||||||
|
.who > p { color: #c3d3e3; font-size: 1.1rem; max-width: 720px; }
|
||||||
|
.team { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; margin-top: 26px; }
|
||||||
|
.person { padding: 22px 24px; border-radius: var(--radius); border: 1px solid var(--line); background: linear-gradient(135deg, var(--card), var(--bg2)); transition: .3s; }
|
||||||
|
.person:hover { border-color: var(--neon); box-shadow: 0 0 24px rgba(46, 230, 255, .2); }
|
||||||
|
.person b { display: block; font-size: 1.3rem; color: var(--neon); text-shadow: 0 0 14px rgba(46, 230, 255, .4); }
|
||||||
|
.person em { display: block; margin: 2px 0 10px; font-style: normal; font-size: .82rem; letter-spacing: .14em; text-transform: uppercase; color: var(--amber); }
|
||||||
|
.person span { display: block; color: #b4c4d4; font-size: .98rem; line-height: 1.55; }
|
||||||
|
.invite {
|
||||||
|
display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
|
||||||
|
padding: clamp(22px, 4vw, 38px); border-radius: 22px; border: 1px solid var(--line);
|
||||||
|
background: radial-gradient(80% 140% at 0% 0%, rgba(46, 230, 255, .12), transparent 60%), linear-gradient(135deg, var(--card), var(--bg2));
|
||||||
|
}
|
||||||
|
.invite h2 { margin: 0 0 4px; }
|
||||||
|
.invite p { margin: 0; color: #c3d3e3; }
|
||||||
|
|
||||||
|
/* shorts */
|
||||||
|
.shorts { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; }
|
||||||
|
.scard { display: grid; gap: 8px; align-content: start; transition: transform .3s; }
|
||||||
|
.scard:hover { transform: translateY(-4px); }
|
||||||
|
.scard .thumb { aspect-ratio: 9 / 16; border-radius: 14px; }
|
||||||
|
.scard .meta strong { font-size: .88rem; line-height: 1.3; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||||
|
.scard:hover img { transform: scale(1.06); }
|
||||||
|
.scard:hover .play { opacity: 1; }
|
||||||
|
.scard .play::after { width: 44px; height: 44px; }
|
||||||
|
|
||||||
|
/* lo más visto */
|
||||||
|
.sub { margin: 26px 0 14px; font-size: .8rem; letter-spacing: .22em; text-transform: uppercase; color: var(--amber); font-weight: 700; }
|
||||||
|
.scard .meta time { color: var(--muted); font-size: .8rem; }
|
||||||
|
|
||||||
|
/* contador de vistas: misma etiqueta que "Último vídeo" (fondo naranja, texto oscuro) */
|
||||||
|
.views {
|
||||||
|
position: absolute; left: 10px; bottom: 10px; z-index: 2;
|
||||||
|
display: inline-flex; align-items: center; gap: 6px; padding: 3px 11px 3px 9px;
|
||||||
|
border-radius: 999px; background: var(--amber); color: #001a20;
|
||||||
|
font-size: .78rem; font-weight: 700; letter-spacing: .04em; line-height: 1.5;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, .45); font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.views svg { flex: none; }
|
||||||
|
.views b { font-weight: 700; }
|
||||||
|
.scard .views { left: 6px; bottom: 6px; padding: 2px 9px 2px 7px; font-size: .72rem; }
|
||||||
|
.feat .views { left: 14px; bottom: 14px; font-size: .9rem; padding: 4px 13px 4px 11px; }
|
||||||
|
|
||||||
|
/* ficha del invitado */
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
.guest .sec { display: grid; gap: 18px; margin: 0 0 22px; padding: 20px 22px 24px; border: 1px solid var(--line); border-radius: 16px; background: linear-gradient(135deg, var(--card), var(--bg2)); }
|
||||||
|
.guest .sec legend { padding: 0 10px; font-weight: 700; font-size: 1.1rem; color: var(--neon); }
|
||||||
|
.guest .form small { display: block; margin-top: 2px; color: var(--muted); font-weight: 400; }
|
||||||
|
.guest .note, .note { margin: 0; color: var(--muted); font-size: .92rem; }
|
||||||
|
.guest .note a { color: var(--neon); text-decoration: underline; text-underline-offset: 3px; }
|
||||||
|
.req { color: var(--amber); font-weight: 700; }
|
||||||
|
.cita { margin: 0 0 14px; padding: 12px 16px; border: 1px solid var(--line); border-left: 3px solid var(--amber); border-radius: 12px; background: var(--card); font-weight: 600; }
|
||||||
|
.paso { margin: 0; color: var(--muted); font-size: .9rem; }
|
||||||
|
.guest .nav { display: flex; justify-content: space-between; gap: 12px; margin-top: 4px; }
|
||||||
|
.guest .nav .next, .guest .nav button[type=submit] { margin-left: auto; }
|
||||||
|
.btn.ghost { background: transparent; color: var(--muted); }
|
||||||
|
|||||||
+19
-7
@@ -20,7 +20,7 @@
|
|||||||
<p class="upd">Última actualización: 20 de octubre de 2026</p>
|
<p class="upd">Última actualización: 20 de octubre de 2026</p>
|
||||||
|
|
||||||
<h2>1. Objeto</h2>
|
<h2>1. Objeto</h2>
|
||||||
<p>Estas condiciones regulan el uso de la web <a href="https://andaquenosabesna.thax.es">andaquenosabesna.thax.es</a>, del programa <strong>Anda Que No Sabes Ná</strong> y de la aplicación privada con la que publicamos nuestro contenido en YouTube, TikTok e Instagram. Al usar la web aceptas estas condiciones.</p>
|
<p>Estas condiciones regulan el uso de la web <a href="https://andaquenosabesna.com">andaquenosabesna.com</a>, del programa <strong>Anda Que No Sabes Ná</strong> y de la aplicación privada con la que publicamos nuestro contenido en YouTube, TikTok e Instagram. Al usar la web aceptas estas condiciones.</p>
|
||||||
|
|
||||||
<h2>2. La web</h2>
|
<h2>2. La web</h2>
|
||||||
<p>La web es informativa: presenta el programa y enlaza a nuestros vídeos y redes sociales. No requiere registro y no ofrece servicios de pago. Intentamos que la información sea correcta, pero puede cambiar o contener errores.</p>
|
<p>La web es informativa: presenta el programa y enlaza a nuestros vídeos y redes sociales. No requiere registro y no ofrece servicios de pago. Intentamos que la información sea correcta, pero puede cambiar o contener errores.</p>
|
||||||
@@ -36,20 +36,32 @@
|
|||||||
<li>Condiciones de uso de Instagram (Meta)</li>
|
<li>Condiciones de uso de Instagram (Meta)</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>5. Propiedad intelectual</h2>
|
<h2 id="participacion">5. Condiciones de participación de invitados</h2>
|
||||||
|
<p>Estas condiciones se aplican a las personas que, tras ser invitadas, participan en el programa <strong>Anda Que No Sabes Ná</strong> y rellenan la <em>ficha del invitado</em>. Se aceptan marcando la casilla correspondiente al final de dicha ficha.</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Proyecto altruista y colaborativo.</strong> El programa se realiza de forma altruista y colaborativa, con la intención de compartir conocimiento, experiencias e historias.</li>
|
||||||
|
<li><strong>Sin remuneración económica.</strong> Ni la preparación, ni la grabación, ni la publicación del episodio contemplan remuneración económica.</li>
|
||||||
|
<li><strong>Participación voluntaria y de buena fe.</strong> La participación es voluntaria y se realiza de buena fe.</li>
|
||||||
|
<li><strong>Ficha previa.</strong> La ficha sirve para conocer al invitado antes de la grabación y encontrar historias, preguntas y temas sobre los que merezca la pena hablar. No es necesario preparar respuestas perfectas, y las secciones indicadas como opcionales pueden dejarse en blanco.</li>
|
||||||
|
<li><strong>Límites temáticos.</strong> En la ficha, el invitado puede indicar los temas, personas, empresas o asuntos sobre los que prefiere que no se hable, sin necesidad de justificarlo. El equipo del programa tendrá en cuenta estas indicaciones al preparar la entrevista.</li>
|
||||||
|
<li><strong>Aceptación.</strong> Al marcar la casilla de aceptación y enviar la ficha, el invitado declara haber leído y entendido estas condiciones y estar de acuerdo con ellas.</li>
|
||||||
|
</ul>
|
||||||
|
<p>El tratamiento de los datos que se faciliten en la ficha se describe en la <a href="privacidad.html">política de privacidad</a>. Para cualquier duda sobre estas condiciones, escríbenos a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p>
|
||||||
|
|
||||||
|
<h2>6. Propiedad intelectual</h2>
|
||||||
<p>Los vídeos, textos, imágenes, el nombre y el logotipo de Anda Que No Sabes Ná pertenecen a sus autores y a las personas que participan en el programa. No está permitido reproducirlos, distribuirlos ni usarlos con fines comerciales sin autorización previa por escrito. Puedes compartir los enlaces a nuestros vídeos libremente.</p>
|
<p>Los vídeos, textos, imágenes, el nombre y el logotipo de Anda Que No Sabes Ná pertenecen a sus autores y a las personas que participan en el programa. No está permitido reproducirlos, distribuirlos ni usarlos con fines comerciales sin autorización previa por escrito. Puedes compartir los enlaces a nuestros vídeos libremente.</p>
|
||||||
|
|
||||||
<h2>6. Enlaces externos</h2>
|
<h2>7. Enlaces externos</h2>
|
||||||
<p>La web contiene enlaces a sitios de terceros. No controlamos su contenido ni somos responsables de sus políticas o prácticas.</p>
|
<p>La web contiene enlaces a sitios de terceros. No controlamos su contenido ni somos responsables de sus políticas o prácticas.</p>
|
||||||
|
|
||||||
<h2>7. Limitación de responsabilidad</h2>
|
<h2>8. Limitación de responsabilidad</h2>
|
||||||
<p>La web se ofrece «tal cual», sin garantías de disponibilidad continua. En la medida permitida por la ley, no seremos responsables de los daños derivados de su uso o de la imposibilidad de acceder a ella. Las opiniones expresadas en el programa son de quienes las dicen.</p>
|
<p>La web se ofrece «tal cual», sin garantías de disponibilidad continua. En la medida permitida por la ley, no seremos responsables de los daños derivados de su uso o de la imposibilidad de acceder a ella. Las opiniones expresadas en el programa son de quienes las dicen.</p>
|
||||||
|
|
||||||
<h2>8. Cambios</h2>
|
<h2>9. Cambios</h2>
|
||||||
<p>Podemos actualizar estas condiciones. La versión vigente es la publicada en esta página, con su fecha de actualización.</p>
|
<p>Podemos actualizar estas condiciones. La versión vigente es la publicada en esta página, con su fecha de actualización.</p>
|
||||||
|
|
||||||
<h2>9. Ley aplicable y contacto</h2>
|
<h2>10. Ley aplicable y contacto</h2>
|
||||||
<p>Estas condiciones se rigen por la legislación española. Para cualquier duda, escríbenos a <a href="mailto:andaquenosabesnada@gmail.com">andaquenosabesnada@gmail.com</a>.</p>
|
<p>Estas condiciones se rigen por la legislación española. Para cualquier duda, escríbenos a <a href="mailto:contacto@andaquenosabesna.com">contacto@andaquenosabesna.com</a>.</p>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
tiktok-developers-site-verification=t8ZeNlFqbY0yUukJkPfgjKdxPkFxoOg6
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
"""Endpoint del formulario de contacto: POST /api/contacto -> correo por SMTP.
|
||||||
|
|
||||||
|
Configuración solo por variables de entorno (nunca en el repo):
|
||||||
|
SMTP_HOST, SMTP_PORT (587), SMTP_USER, SMTP_PASS, SMTP_TLS_NAME (nombre del certificado, por defecto SMTP_HOST),
|
||||||
|
MAIL_FROM, MAIL_TO
|
||||||
|
No guarda los mensajes en disco: solo los reenvía por correo.
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
import hmac as _hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse, parse_qs, quote
|
||||||
|
import re
|
||||||
|
import smtplib
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict, deque
|
||||||
|
from email.message import EmailMessage
|
||||||
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
|
MOTIVOS = {
|
||||||
|
'venir': 'Quiero venir al programa',
|
||||||
|
'tema': 'Propongo un tema',
|
||||||
|
'otro': 'Otro asunto',
|
||||||
|
}
|
||||||
|
TIPOS = {
|
||||||
|
'mencion': 'Anunciarme / que me mencionéis en el programa',
|
||||||
|
'patrocinio': 'Patrocinar episodios o una temporada',
|
||||||
|
'colab': 'Colaboración o canje',
|
||||||
|
'otro': 'Otra propuesta',
|
||||||
|
}
|
||||||
|
EMAIL_RE = re.compile(r'^[^@\s]{1,64}@[^@\s]{1,190}\.[^@\s]{2,}$')
|
||||||
|
MAX_BODY = 16 * 1024
|
||||||
|
RATE_MAX, RATE_WINDOW = 5, 3600 # 5 mensajes por IP y hora
|
||||||
|
hits = defaultdict(deque)
|
||||||
|
|
||||||
|
|
||||||
|
def clean(s, n):
|
||||||
|
return re.sub(r'[\r\n\t]+', ' ', str(s or '')).strip()[:n]
|
||||||
|
|
||||||
|
|
||||||
|
def rate_ok(ip):
|
||||||
|
now = time.time()
|
||||||
|
q = hits[ip]
|
||||||
|
while q and now - q[0] > RATE_WINDOW:
|
||||||
|
q.popleft()
|
||||||
|
if len(q) >= RATE_MAX:
|
||||||
|
return False
|
||||||
|
q.append(now)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def send(subject, reply_to, body, to=None):
|
||||||
|
host, user, pwd = os.environ.get('SMTP_HOST'), os.environ.get('SMTP_USER'), os.environ.get('SMTP_PASS')
|
||||||
|
if not (host and user and pwd and os.environ.get('MAIL_TO')):
|
||||||
|
raise RuntimeError('SMTP no configurado')
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg['Subject'] = subject
|
||||||
|
msg['From'] = os.environ.get('MAIL_FROM', user)
|
||||||
|
msg['To'] = to or os.environ['MAIL_TO']
|
||||||
|
if reply_to:
|
||||||
|
msg['Reply-To'] = reply_to
|
||||||
|
msg.set_content(body)
|
||||||
|
s = smtplib.SMTP(host, int(os.environ.get('SMTP_PORT', '587')), timeout=20)
|
||||||
|
try:
|
||||||
|
s.ehlo()
|
||||||
|
s._host = os.environ.get('SMTP_TLS_NAME', host) # el certificado se valida contra este nombre
|
||||||
|
s.starttls(context=ssl.create_default_context())
|
||||||
|
s.ehlo()
|
||||||
|
s.login(user, pwd)
|
||||||
|
s.send_message(msg)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
s.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_guest():
|
||||||
|
"""Preguntas de la ficha (copia del formulario de Google) -> [{id, sec, t, req}]."""
|
||||||
|
p = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'guest_form.json')
|
||||||
|
try:
|
||||||
|
form = json.load(open(p, encoding='utf-8'))
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
out, sec, n = [], '', 0
|
||||||
|
for it in form['items']:
|
||||||
|
if it['type'] == 8:
|
||||||
|
sec = it['title']
|
||||||
|
elif it['type'] in (0, 1):
|
||||||
|
n += 1
|
||||||
|
out.append({'id': f'q{n:02d}', 'sec': sec, 't': it['title'], 'req': it['required']})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
GUEST_Q = load_guest()
|
||||||
|
check_hits = defaultdict(deque)
|
||||||
|
guest_hits = defaultdict(deque)
|
||||||
|
|
||||||
|
|
||||||
|
def limited(bucket, ip, maxn, window=3600):
|
||||||
|
now = time.time()
|
||||||
|
q = bucket[ip]
|
||||||
|
while q and now - q[0] > window:
|
||||||
|
q.popleft()
|
||||||
|
if len(q) >= maxn:
|
||||||
|
return True
|
||||||
|
q.append(now)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
confirmados = set()
|
||||||
|
DIAS = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo']
|
||||||
|
MESES = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre']
|
||||||
|
|
||||||
|
|
||||||
|
def cita_txt(ts):
|
||||||
|
d = datetime.datetime.fromtimestamp(ts, ZoneInfo('Europe/Madrid'))
|
||||||
|
return f'{DIAS[d.weekday()]} {d.day} de {MESES[d.month - 1]} de {d.year}, a las {d:%H:%M}'
|
||||||
|
|
||||||
|
|
||||||
|
def cita_corta(ts):
|
||||||
|
d = datetime.datetime.fromtimestamp(ts, ZoneInfo('Europe/Madrid'))
|
||||||
|
return f'el {DIAS[d.weekday()]} {d.day} de {MESES[d.month - 1]} a las {d:%H:%M}'
|
||||||
|
|
||||||
|
|
||||||
|
NAME_RE = re.compile(r"^[^\W\d_][\w .'\-]{1,59}$", re.UNICODE)
|
||||||
|
|
||||||
|
|
||||||
|
def invitado(n, f):
|
||||||
|
"""Enlace = nombre + fecha de grabación como marca de tiempo Unix (segundos). Válido de 14 días antes a 10 días después."""
|
||||||
|
n = ' '.join(str(n or '').split())
|
||||||
|
if not NAME_RE.match(n):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
ts = int(str(f).strip())
|
||||||
|
dia = datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).date()
|
||||||
|
except (ValueError, OverflowError, OSError):
|
||||||
|
return None
|
||||||
|
delta = (dia - datetime.date.today()).days
|
||||||
|
return (n, ts) if -10 <= delta <= 14 else None
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_token(offset=0):
|
||||||
|
"""Token HMAC diario para la página generadora. Válido 48h (slot actual + anterior)."""
|
||||||
|
key = (os.environ.get('LINK_PASS') or '').encode()
|
||||||
|
if not key:
|
||||||
|
return None
|
||||||
|
slot = str(int(time.time()) // 86400 - offset).encode()
|
||||||
|
return _hmac.new(key, slot, 'sha256').hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_dias(d_str):
|
||||||
|
"""Valida una lista de timestamps separados por comas. Devuelve lista de ints o None."""
|
||||||
|
if not d_str:
|
||||||
|
return None
|
||||||
|
parts = str(d_str).replace(' ', '').split(',')
|
||||||
|
if not 1 <= len(parts) <= 10:
|
||||||
|
return None
|
||||||
|
today = datetime.date.today()
|
||||||
|
result = []
|
||||||
|
for p in parts:
|
||||||
|
try:
|
||||||
|
ts = int(p)
|
||||||
|
day = datetime.datetime.fromtimestamp(ts, datetime.timezone.utc).date()
|
||||||
|
if not (-1 <= (day - today).days <= 60):
|
||||||
|
return None
|
||||||
|
result.append(ts)
|
||||||
|
except (ValueError, OverflowError, OSError):
|
||||||
|
return None
|
||||||
|
return result if result else None
|
||||||
|
|
||||||
|
|
||||||
|
class H(BaseHTTPRequestHandler):
|
||||||
|
server_version = 'aqnsn'
|
||||||
|
|
||||||
|
def log_message(self, fmt, *a): # sin IPs ni contenido en los logs
|
||||||
|
pass
|
||||||
|
|
||||||
|
def reply(self, code, obj):
|
||||||
|
body = json.dumps(obj).encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.send_header('Content-Length', str(len(body)))
|
||||||
|
self.send_header('Cache-Control', 'no-store')
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
|
def client_ip(self):
|
||||||
|
return (self.headers.get('X-Forwarded-For') or self.client_address[0]).split(',')[0].strip()
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
path = urlparse(self.path).path
|
||||||
|
if path == '/api/gen-verify':
|
||||||
|
tok = parse_qs(urlparse(self.path).query).get('t', [''])[0]
|
||||||
|
valid = tok and tok in filter(None, [_gen_token(0), _gen_token(1)])
|
||||||
|
return self.reply(200, {'ok': bool(valid)})
|
||||||
|
if path != '/api/invitados/check':
|
||||||
|
return self.reply(404, {'ok': False})
|
||||||
|
if limited(check_hits, self.client_ip(), 40):
|
||||||
|
return self.reply(429, {'ok': False})
|
||||||
|
q = parse_qs(urlparse(self.path).query)
|
||||||
|
n_raw = q.get('n', [''])[0]
|
||||||
|
f_raw = q.get('f', [''])[0]
|
||||||
|
d_raw = q.get('d', [''])[0]
|
||||||
|
if f_raw:
|
||||||
|
inv = invitado(n_raw, f_raw)
|
||||||
|
return self.reply(200, {'ok': True, 'nombre': inv[0], 'cita': inv[1]} if inv else {'ok': False})
|
||||||
|
if d_raw:
|
||||||
|
n = ' '.join(str(n_raw or '').split())
|
||||||
|
if not n or not NAME_RE.match(n):
|
||||||
|
return self.reply(200, {'ok': False})
|
||||||
|
dias = parse_dias(d_raw)
|
||||||
|
if not dias:
|
||||||
|
return self.reply(200, {'ok': False})
|
||||||
|
return self.reply(200, {'ok': True, 'nombre': n,
|
||||||
|
'dias': [{'ts': ts, 'txt': cita_txt(ts)} for ts in dias]})
|
||||||
|
return self.reply(200, {'ok': False})
|
||||||
|
|
||||||
|
def invitados(self):
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get('Content-Length', '0'))
|
||||||
|
if n <= 0 or n > 128 * 1024:
|
||||||
|
return self.reply(413, {'ok': False, 'error': 'La ficha es demasiado larga.'})
|
||||||
|
d = json.loads(self.rfile.read(n))
|
||||||
|
except Exception:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Petición no válida.'})
|
||||||
|
if d.get('web') or (isinstance(d.get('t'), (int, float)) and d['t'] < 20000):
|
||||||
|
return self.reply(200, {'ok': True}) # trampa o demasiado rápido: no se envía
|
||||||
|
if limited(guest_hits, self.client_ip(), 8):
|
||||||
|
return self.reply(429, {'ok': False, 'error': 'Demasiados envíos. Inténtalo más tarde.'})
|
||||||
|
f_raw = str(d.get('f') or '').strip()
|
||||||
|
d_raw = str(d.get('d') or '').strip()
|
||||||
|
dia_raw = str(d.get('dia') or '').strip()
|
||||||
|
if f_raw:
|
||||||
|
inv = invitado(d.get('n'), f_raw)
|
||||||
|
nombre, ts = inv if inv else (None, None)
|
||||||
|
elif d_raw and dia_raw:
|
||||||
|
n_val = ' '.join(str(d.get('n') or '').split())
|
||||||
|
if not n_val or not NAME_RE.match(n_val):
|
||||||
|
nombre, ts = None, None
|
||||||
|
else:
|
||||||
|
dias = parse_dias(d_raw)
|
||||||
|
try:
|
||||||
|
dia_ts = int(dia_raw)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
dia_ts = None
|
||||||
|
if dias and dia_ts is not None and dia_ts in dias:
|
||||||
|
nombre, ts = n_val, dia_ts
|
||||||
|
else:
|
||||||
|
nombre, ts = None, None
|
||||||
|
else:
|
||||||
|
nombre, ts = None, None
|
||||||
|
if not nombre:
|
||||||
|
return self.reply(403, {'ok': False, 'error': 'Enlace no válido o caducado.'})
|
||||||
|
if d.get('consent') is not True:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Debes aceptar las condiciones de participación.'})
|
||||||
|
resp = d.get('resp') if isinstance(d.get('resp'), dict) else {}
|
||||||
|
email = clean(d.get('email'), 200)
|
||||||
|
if not email or not EMAIL_RE.match(email):
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'El correo no parece válido.'})
|
||||||
|
tel = ' '.join(str(d.get('tel') or '').split())
|
||||||
|
if not re.match(r'^\+?[\d ().-]{7,20}$', tel):
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'El teléfono no parece válido.'})
|
||||||
|
lineas, seccion = [], None
|
||||||
|
for q in GUEST_Q:
|
||||||
|
ans = str(resp.get(q['id']) or '').replace('\r', '').strip()[:3000]
|
||||||
|
if q['req'] and not ans:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Faltan respuestas obligatorias.'})
|
||||||
|
if q['sec'] != seccion:
|
||||||
|
seccion = q['sec']
|
||||||
|
lineas += ['', f'== {seccion} ==']
|
||||||
|
lineas += ['', f"• {q['t']}", ' ' + (ans.replace('\n', '\n ') if ans else '(sin respuesta)')]
|
||||||
|
cabecera = [f'Ficha del invitado: {nombre}', 'Recibida: ' + time.strftime('%d/%m/%Y %H:%M', time.localtime()),
|
||||||
|
f'Cita: {cita_txt(ts)}', f'Correo del invitado: {email}', f'Teléfono móvil: {tel}',
|
||||||
|
'Aceptó las condiciones de participación y la política de privacidad: sí']
|
||||||
|
try:
|
||||||
|
send(f'[Invitado] {clean(nombre, 80)} · ficha del podcast', email, '\n'.join(cabecera + lineas) + '\n')
|
||||||
|
except RuntimeError:
|
||||||
|
return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'})
|
||||||
|
except Exception as exc:
|
||||||
|
print('error enviando ficha:', type(exc).__name__, file=sys.stderr, flush=True)
|
||||||
|
return self.reply(502, {'ok': False, 'error': 'No se pudo enviar. Inténtalo más tarde.'})
|
||||||
|
key = (nombre, ts)
|
||||||
|
lugar = os.environ.get('STUDIO_ADDRESS', '').strip()
|
||||||
|
mapa = os.environ.get('STUDIO_MAP', '').strip() or 'https://www.google.com/maps/search/?api=1&query=' + quote(lugar) # solo en el servidor, nunca en el código
|
||||||
|
if key not in confirmados: # un solo recordatorio por invitado y cita
|
||||||
|
confirmados.add(key)
|
||||||
|
try:
|
||||||
|
send(f'¡Grabamos {cita_corta(ts)}!', None,
|
||||||
|
f'Hola, {nombre}:\n\n¡Ya tenemos tu ficha, gracias! Ahora toca lo bueno: grabamos {cita_corta(ts)}.\n\n'
|
||||||
|
+ (f'Nos vemos en {lugar}.\nCómo llegar: {mapa}\n\n' if lugar else 'En breve te mandamos por correo electrónico la dirección del estudio.\n\n') +
|
||||||
|
'Ven con ganas de contar historias, que de lo demás nos encargamos nosotros. '
|
||||||
|
'Si te surge cualquier imprevisto, responde a este correo y lo arreglamos.\n\n'
|
||||||
|
'Un abrazo,\nÁlex y Juan\nAnda Que No Sabes Ná\nhttps://andaquenosabesna.com\n', to=email)
|
||||||
|
except Exception as exc:
|
||||||
|
confirmados.discard(key)
|
||||||
|
print('error enviando recordatorio:', type(exc).__name__, file=sys.stderr, flush=True)
|
||||||
|
self.reply(200, {'ok': True})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
p = self.path.split('?')[0]
|
||||||
|
if p == '/api/gen-auth':
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get('Content-Length', '0'))
|
||||||
|
d = json.loads(self.rfile.read(n)) if 0 < n <= 1024 else {}
|
||||||
|
except Exception:
|
||||||
|
return self.reply(400, {'ok': False})
|
||||||
|
link_pass = os.environ.get('LINK_PASS', '')
|
||||||
|
if not link_pass:
|
||||||
|
return self.reply(503, {'ok': False, 'error': 'No configurado.'})
|
||||||
|
if _hmac.compare_digest(str(d.get('pass', '')), link_pass):
|
||||||
|
return self.reply(200, {'ok': True, 'token': _gen_token(0)})
|
||||||
|
return self.reply(401, {'ok': False, 'error': 'Contraseña incorrecta.'})
|
||||||
|
if p == '/api/invitados':
|
||||||
|
return self.invitados()
|
||||||
|
if self.path not in ('/api/contacto', '/api/patrocinio'):
|
||||||
|
return self.reply(404, {'ok': False})
|
||||||
|
patro = self.path == '/api/patrocinio'
|
||||||
|
try:
|
||||||
|
n = int(self.headers.get('Content-Length', '0'))
|
||||||
|
if n <= 0 or n > MAX_BODY:
|
||||||
|
return self.reply(413, {'ok': False, 'error': 'Mensaje demasiado largo.'})
|
||||||
|
d = json.loads(self.rfile.read(n))
|
||||||
|
except Exception:
|
||||||
|
return self.reply(400, {'ok': False, 'error': 'Petición no válida.'})
|
||||||
|
ip = (self.headers.get('X-Forwarded-For') or self.client_address[0]).split(',')[0].strip()
|
||||||
|
# antispam: campo trampa relleno o envío en menos de 3 s => se responde "ok" sin enviar
|
||||||
|
if d.get('web') or (isinstance(d.get('t'), (int, float)) and d['t'] < 3000):
|
||||||
|
return self.reply(200, {'ok': True})
|
||||||
|
if not rate_ok(ip):
|
||||||
|
return self.reply(429, {'ok': False, 'error': 'Demasiados mensajes. Inténtalo más tarde.'})
|
||||||
|
nombre, email = clean(d.get('nombre'), 100), clean(d.get('email'), 200)
|
||||||
|
mensaje = str(d.get('mensaje') or '').replace('\r', '').strip()[:3000]
|
||||||
|
bad = {'ok': False, 'error': 'Revisa los campos del formulario.'}
|
||||||
|
if not nombre or not EMAIL_RE.match(email) or len(mensaje) < 10 or d.get('consent') is not True:
|
||||||
|
return self.reply(400, bad)
|
||||||
|
if patro:
|
||||||
|
empresa, sitio = clean(d.get('empresa'), 120), clean(d.get('sitio'), 200)
|
||||||
|
tipo = d.get('tipo') if d.get('tipo') in TIPOS else 'otro'
|
||||||
|
if not empresa or (sitio and not re.match(r'^https?://', sitio)):
|
||||||
|
return self.reply(400, bad)
|
||||||
|
subject = f'[Web AQNSN · Patrocinio] {empresa} · {nombre}'
|
||||||
|
body = (f'Empresa: {empresa}\nContacto: {nombre}\nEmail: {email}\nWeb: {sitio or "—"}\n'
|
||||||
|
f'Interés: {TIPOS[tipo]}\n\n{mensaje}\n')
|
||||||
|
else:
|
||||||
|
motivo = d.get('motivo') if d.get('motivo') in MOTIVOS else 'otro'
|
||||||
|
subject = f'[Web AQNSN] {MOTIVOS[motivo]} · {nombre}'
|
||||||
|
body = f'Motivo: {MOTIVOS[motivo]}\nNombre: {nombre}\nEmail: {email}\n\n{mensaje}\n'
|
||||||
|
try:
|
||||||
|
send(subject, email, body)
|
||||||
|
except RuntimeError:
|
||||||
|
return self.reply(503, {'ok': False, 'error': 'El formulario no está disponible ahora mismo.'})
|
||||||
|
except Exception as exc:
|
||||||
|
print('error enviando correo:', type(exc).__name__, file=sys.stderr, flush=True)
|
||||||
|
return self.reply(502, {'ok': False, 'error': 'No se pudo enviar. Inténtalo más tarde.'})
|
||||||
|
self.reply(200, {'ok': True})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
ThreadingHTTPServer(('127.0.0.1', 8081), H).serve_forever()
|
||||||
Reference in New Issue
Block a user