Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
7c28bbd
W-01: el proxy deja pasar rutas admin con cookie de sesión (SPEC v3 §8)
Saul-Gomez-J Sep 6, 2026
ca8e308
W-02: guardia SSR de staff y layout base de /events/admin (SPEC v3 §8)
Saul-Gomez-J Sep 6, 2026
337dfe7
W-03: lista de eventos y editor (crear, editar, clonar, archivar) en …
Saul-Gomez-J Sep 6, 2026
2bacdc2
W-04: control de estado con previsualización de avisos (dry_run) en l…
Saul-Gomez-J Sep 6, 2026
d332cfa
W-05: participantes (tabla, alta, edición, baja, promoción, import y …
Saul-Gomez-J Sep 6, 2026
2d5704d
W-06: equipos (tablero, mover, crear, renombrar, disolver, generar) e…
Saul-Gomez-J Sep 6, 2026
61620e3
W-07: entregas (edición, retirada, checks, premio) en /events/admin/{…
Saul-Gomez-J Sep 6, 2026
e0850c2
W-10: /events lee los eventos de la API y ofrece añadirlos al calendario
Saul-Gomez-J Sep 6, 2026
e20a414
W-11: estado published en el panel y en la ficha pública
Saul-Gomez-J Sep 6, 2026
0181a2b
W-08: votos y ranking en el panel
Saul-Gomez-J Sep 6, 2026
11013ef
W-09: auditoría y backups en el panel
Saul-Gomez-J Sep 6, 2026
5d14171
fix(events): la ficha pública centrada y con aire
Saul-Gomez-J Sep 7, 2026
dd295ba
fix(events): participantes no revienta si specialties/levels llegan null
Saul-Gomez-J Sep 7, 2026
568b042
fix(admin): la importación CSV refresca los contadores y no repite el…
Saul-Gomez-J Sep 7, 2026
53fa997
fix(admin): la fase no dice "abierta" cuando la ventana está cerrada
Saul-Gomez-J Sep 7, 2026
704c9a8
fix(events): las especialidades que no están en el diccionario salen …
Saul-Gomez-J Sep 7, 2026
3290d1d
fix(admin): el aviso de URL no válida dice qué campo falla
Saul-Gomez-J Sep 7, 2026
b25e0ac
fix(admin): se puede abrir la votación desde la pantalla de Votos
Saul-Gomez-J Sep 7, 2026
96cc0ee
fix(events): la convocatoria de reasignación no se ofrece contra uno …
Saul-Gomez-J Sep 7, 2026
939b234
fix(events): la copia pública no nombra fechas que el evento no tiene
Saul-Gomez-J Sep 7, 2026
eed8127
fix(events): tipar como nullable lo que el backend serializa null
Saul-Gomez-J Sep 8, 2026
9e02b01
test(events): guarda de tipos en vez de barrido de texto
Saul-Gomez-J Sep 8, 2026
558b9f7
fix(admin): el panel etiqueta especialidades y niveles como la web pú…
Saul-Gomez-J Sep 8, 2026
f759bea
fix(events): hasSessionCookie mira el nombre de la cookie, no la subc…
Saul-Gomez-J Sep 9, 2026
5165a93
fix(admin): AdminEventView y EventLike tipan las listas como string[]…
Saul-Gomez-J Sep 9, 2026
ebf62f3
refactor(events): apiBase, ssrHeaders, safeSegments y los tipos compa…
Saul-Gomez-J Sep 9, 2026
04ae0b4
refactor(admin): badForm, beginForm, SAFE_ID y las etiquetas de los c…
Saul-Gomez-J Sep 9, 2026
281214a
refactor(admin): AdminScreenProps, eventSession y ownerKind en las pa…
Saul-Gomez-J Sep 9, 2026
95407f6
fix(admin): la píldora de ventanas se marca cuando la ventana está ce…
Saul-Gomez-J Sep 9, 2026
66adf3c
test(admin): resolveAdminEventRoute, reloadAdminEventView y los campo…
Saul-Gomez-J Sep 9, 2026
87d2edb
docs(web): los comentarios de los ficheros anteriores al PR pasan al …
Saul-Gomez-J Sep 9, 2026
61f2686
revert: restore the English comments translated in 87d2edb
Saul-Gomez-J Sep 11, 2026
7374007
fix(events): the proxy forwards an empty CSV or iCalendar body as is
Saul-Gomez-J Sep 11, 2026
1a3fb55
test(events): pin cookieHeaderHasSession against the decoy cookie names
Saul-Gomez-J Sep 11, 2026
0a8b960
feat(events): show the event cover on the public page and as og:image…
Saul-Gomez-J Sep 16, 2026
9c095cb
fix(events): guard the cover scheme on the page and harden the layout…
Saul-Gomez-J Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
"voting": "Voting",
"demoDay": "Demo Day"
},
"where": {
"label": "Where",
"link": "Link",
"calendar": "Add to my calendar",
"calendarTitle": "Add this event to Google Calendar",
"ics": "or download the .ics"
},
"login": {
"prompt": "Sign in with your NaN account",
"description": "Enter your NaN account email and we'll send you a link to sign in. It's the same access as the platform.",
Expand All @@ -75,15 +82,18 @@
"notOpenTitle": "Not open yet",
"notOpen": "Registration for this event isn't open yet. Check back soon to sign up.",
"submissionNotOpen": "Submissions open on {date}. Come back then to submit your project.",
"submissionNotOpenNoDate": "Submissions aren't open yet. Come back soon to submit your project.",
"closedTitle": "Registration closed",
"closed": "The registration window has closed. We hope to see you at the next NaN event!",
"inProgressTitle": "The event is underway",
"inProgress": "Registration is closed, but you're already in. Check your status and your team.",
"buildingTitle": "Submissions are open",
"building": "Submit your project before {date}. You can edit it as many times as you want until then.",
"buildingNoDate": "Submit your project. You can edit it as many times as you want while the window is open.",
"buildingCta": "Submit my project",
"submittedTitle": "Submission received ✓",
"submitted": "Your project is in. You can edit it until {date}.",
"submittedNoDate": "Your project is in. You can edit it while the window is open.",
"submittedCta": "Edit my submission",
"submissionClosedTitle": "Submissions closed",
"submissionClosed": "The submission window has closed. Voting opens soon.",
Expand All @@ -96,7 +106,10 @@
"results": "The event has ended. Check out the final leaderboard.",
"resultsCta": "View leaderboard",
"closedEventTitle": "Event closed",
"closedEvent": "This event has ended. Thanks to everyone who took part!"
"closedEvent": "This event has ended. Thanks to everyone who took part!",
"publishedTitle": "Open to everyone",
"published": "No registration needed. Add it to your calendar and join through the link on the day.",
"publishedCta": "Join the event"
},
"alreadyRegistered": {
"title": "You're already registered",
Expand Down Expand Up @@ -165,11 +178,12 @@
"reassignCta": "Request reassignment",
"reassignPending": "Request registered. Another teammate must confirm it ({have}/{need}).",
"reassignFilled": "Reassignment complete: a member from the reserve pool has joined.",
"reassignNoPool": "Reassignment complete: the pool was empty, your team continues with the minimum size.",
"reassignNoPool": "Reassignment complete: the pool was empty, your team continues with the remaining members.",
"reassignAlready": "You've already requested this reassignment. Another teammate still needs to confirm it.",
"reassignSelectAbsent": "Select absent member",
"reassignErrorSubmit": "Couldn't register.",
"reassignSending": "Sending…",
"reassignRefresh": "See the updated team",
"reassignInfo": {
"label": "Reassign absent member",
"toggleAria": "More information about reassignment",
Expand All @@ -179,7 +193,7 @@
"It takes TWO team members: the request isn't executed until a second teammate confirms the same reassignment.",
"Once that agreement is reached, that person is marked absent and removed from your team and from the hackathon. The action is immediate and irreversible.",
"The system automatically picks the first candidate from the reserve pool (by registration order) and adds them to your team in their place.",
"If the pool is empty, your team continues with 3 members (the allowed minimum) and the organizers are notified.",
"If the pool is empty, your team continues with the remaining members and the organizers are notified.",
"It's only available during the building phase."
]
}
Expand Down Expand Up @@ -224,8 +238,10 @@
"errorEligible": "Your NaN account is not eligible for this event.",
"notOpenTitle": "Submissions not open yet",
"notOpen": "Submissions open on {date}.",
"notOpenNoDate": "Submissions aren't open yet.",
"closedTitle": "Submission window closed",
"closedNote": "Submissions closed on {date}. They can no longer be created or edited.",
"closedNoteNoDate": "The submission window is closed. Submissions can no longer be created or edited.",
"notSubmitted": "Nothing was submitted before the deadline closed.",
"checks": "Automatic checks",
"notPrizeEligible": "Prize requirements not met (see the event rules)."
Expand Down Expand Up @@ -578,8 +594,18 @@
"types": {
"workshop": "workshop",
"event": "event",
"hackathon": "hackathon"
"hackathon": "hackathon",
"challenge": "challenge",
"other": "event"
},
"calendarTitle": "Take the agenda to your calendar.",
"calendarBody": "Add the NaN calendar once and the published events, plus the ones we announce later, show up on their own.",
"calendarGoogle": "Add to Google Calendar",
"calendarIcs": "Apple, Outlook or other (.ics)",
"calendarFeedLabel": "Calendar URL",
"calendarAdd": "+ calendar",
"calendarAddTitle": "Add this event to Google Calendar",
"calendarTz": "Times in Europe/Madrid.",
"workshopsLabel": "workshops",
"workshopsTitle": "Workshops.",
"workshopsSub": "Hands-on sessions on agents, tooling and shipping with open models. Recorded for members.",
Expand Down
34 changes: 30 additions & 4 deletions i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@
"voting": "Votación",
"demoDay": "Demo Day"
},
"where": {
"label": "Dónde",
"link": "Enlace",
"calendar": "Añadir a mi calendario",
"calendarTitle": "Añadir este evento a Google Calendar",
"ics": "o descarga el .ics"
},
"login": {
"prompt": "Entra con tu cuenta NaN",
"description": "Introduce el email de tu cuenta NaN y te enviaremos un enlace para iniciar sesión. Es el mismo acceso que la plataforma.",
Expand All @@ -75,15 +82,18 @@
"notOpenTitle": "Todavía no está abierto",
"notOpen": "La inscripción a este evento aún no está abierta. Vuelve pronto para apuntarte.",
"submissionNotOpen": "Las entregas abren el {date}. Vuelve entonces para entregar tu proyecto.",
"submissionNotOpenNoDate": "Las entregas todavía no están abiertas. Vuelve pronto para entregar tu proyecto.",
"closedTitle": "Inscripción cerrada",
"closed": "El plazo de inscripción ha terminado. ¡Esperamos verte en el próximo evento de NaN!",
"inProgressTitle": "El evento está en marcha",
"inProgress": "La inscripción está cerrada, pero tú ya estás dentro. Consulta tu estado y tu equipo.",
"buildingTitle": "Entregas abiertas",
"building": "Entrega tu proyecto antes del {date}. Puedes editarlo tantas veces como quieras hasta entonces.",
"buildingNoDate": "Entrega tu proyecto. Puedes editarlo tantas veces como quieras mientras el plazo siga abierto.",
"buildingCta": "Entregar mi proyecto",
"submittedTitle": "Entrega recibida ✓",
"submitted": "Tu proyecto está dentro. Puedes editarlo hasta el {date}.",
"submittedNoDate": "Tu proyecto está dentro. Puedes editarlo mientras el plazo siga abierto.",
"submittedCta": "Editar mi entrega",
"submissionClosedTitle": "Entregas cerradas",
"submissionClosed": "El plazo de entrega ha terminado. La votación abre pronto.",
Expand All @@ -96,7 +106,10 @@
"results": "El evento ha terminado. Consulta la clasificación final.",
"resultsCta": "Ver clasificación",
"closedEventTitle": "Evento cerrado",
"closedEvent": "Este evento ha terminado. ¡Gracias a todos los que habéis participado!"
"closedEvent": "Este evento ha terminado. ¡Gracias a todos los que habéis participado!",
"publishedTitle": "Abierto a todos",
"published": "Este evento no necesita inscripción. Apúntalo en tu calendario y entra por el enlace el día del evento.",
"publishedCta": "Ir al evento"
},
"alreadyRegistered": {
"title": "Ya estás inscrito",
Expand Down Expand Up @@ -165,11 +178,12 @@
"reassignCta": "Convocar reasignación",
"reassignPending": "Convocatoria registrada. Hace falta que otro compañero la confirme ({have}/{need}).",
"reassignFilled": "Reasignación completada: se ha incorporado un miembro de la pool de reservas.",
"reassignNoPool": "Reasignación completada: no quedaba nadie en la pool, el equipo continúa con 3 miembros.",
"reassignNoPool": "Reasignación completada: no quedaba nadie en la pool, el equipo sigue con los miembros que quedan.",
"reassignAlready": "Ya has convocado esta reasignación. Falta que otro compañero la confirme.",
"reassignSelectAbsent": "Selecciona ausente",
"reassignErrorSubmit": "No se pudo registrar.",
"reassignSending": "Enviando…",
"reassignRefresh": "Ver el equipo actualizado",
"reassignInfo": {
"label": "Reasignar miembro ausente",
"toggleAria": "Más información sobre la reasignación",
Expand All @@ -179,7 +193,7 @@
"Hacen falta DOS miembros del equipo: la convocatoria no se ejecuta hasta que un segundo compañero confirma la misma reasignación.",
"Cuando se alcanza ese acuerdo, esa persona se marca como ausente y sale de tu equipo y del hackatón. La acción es inmediata e irreversible.",
"El sistema busca automáticamente al primer candidato de la pool de reservas (por orden de inscripción) y lo añade a tu equipo en su lugar.",
"Si no queda nadie en la pool, tu equipo continúa con 3 miembros (el mínimo permitido) y se avisa a la organización.",
"Si no queda nadie en la pool, tu equipo sigue con los miembros que quedan y se avisa a la organización.",
"Solo está disponible durante la fase de construcción."
]
}
Expand Down Expand Up @@ -224,8 +238,10 @@
"errorEligible": "Tu cuenta de NaN no puede participar en este evento.",
"notOpenTitle": "Entregas aún no abiertas",
"notOpen": "Las entregas abren el {date}.",
"notOpenNoDate": "Las entregas todavía no están abiertas.",
"closedTitle": "Plazo de entrega cerrado",
"closedNote": "Las entregas cerraron el {date}. Ya no se pueden crear ni editar.",
"closedNoteNoDate": "El plazo de entrega está cerrado. Ya no se pueden crear ni editar.",
"notSubmitted": "No se entregó nada antes de que cerrara el plazo.",
"checks": "Comprobaciones automáticas",
"notPrizeEligible": "No cumple los requisitos del premio (ver reglas del evento)."
Expand Down Expand Up @@ -578,8 +594,18 @@
"types": {
"workshop": "workshop",
"event": "evento",
"hackathon": "hackathon"
"hackathon": "hackathon",
"challenge": "reto",
"other": "evento"
},
"calendarTitle": "Llévate la agenda a tu calendario.",
"calendarBody": "Añade el calendario de NaN una vez y te aparecerán los eventos publicados y los que vayamos anunciando, sin volver aquí.",
"calendarGoogle": "Añadir a Google Calendar",
"calendarIcs": "Apple, Outlook u otro (.ics)",
"calendarFeedLabel": "URL del calendario",
"calendarAdd": "+ calendario",
"calendarAddTitle": "Añadir este evento a Google Calendar",
"calendarTz": "Horas en Europe/Madrid.",
"workshopsLabel": "workshops",
"workshopsTitle": "Workshops.",
"workshopsSub": "Sesiones prácticas sobre agentes, tooling y lanzar con modelos abiertos. Grabadas para miembros.",
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@astrojs/check": "^0.9.8",
"@cloudflare/workers-types": "^4.20260317.1",
"playwright": "^1.59.1",
"preact-render-to-string": "^6.6.7",
"typescript": "^5.9.3",
"vitest": "^4.1.0",
"wrangler": "^4.76.0"
Expand Down
Binary file added public/img/events/gentleman-programming.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 25 additions & 10 deletions src/components/events/EventHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
/**
* Cabecera común de las pantallas de un evento: enlace atrás, etiqueta del
* tipo de evento, nombre y navegación entre pantallas (solo las que aplican
* al formato/fase).
* al formato/fase). El titular usa la Archivo expandida del sistema, pero con
* un tamaño propio: estas pantallas montan el shell con scale={false} porque
* la escala de 96px parte las palabras en columnas estrechas.
*/
import { t, withLang } from '../../lib/i18n';
import type { EventInfo } from '../../lib/events';
Expand All @@ -28,19 +30,32 @@ if (event.windows.leaderboard.visible) nav.push({ key: 'leaderboard', href: `${b
---

<a href={withLang(current === 'index' ? '/events' : base, locale)}
class="font-mono text-xs text-neutral-500 transition-colors hover:text-violet-400">
class="inline-block font-mono text-sm text-neutral-500 transition-colors hover:text-violet-400">
{current === 'index' ? t('events.backToEvents', locale) : t('events.backToEvent', locale)}
</a>
<header class="mt-8">
<p class="font-mono text-xs uppercase tracking-widest text-violet-400">// {kindLabel}</p>
<h1 class="mt-3 font-mono text-2xl md:text-3xl text-white tracking-tight leading-snug">
{event.name}
</h1>
<header class="mt-10">
<p class="font-mono text-sm uppercase tracking-[0.2em] text-violet-400">// {kindLabel}</p>
<h1 class="ev-title mt-4 text-white">{event.name}</h1>
{nav.length > 0 && (
<nav class="mt-5 flex flex-wrap gap-4 font-mono text-xs" aria-label={t('events.nav.aria', locale)}>
<nav class="mt-8 flex flex-wrap gap-x-6 gap-y-2 border-t border-neutral-800 pt-4 font-mono text-sm" aria-label={t('events.nav.aria', locale)}>
{nav.map((n) => n.key === current
? <span class="text-white">{n.label}</span>
: <a href={withLang(n.href, locale)} class="text-neutral-400 hover:text-violet-400">{n.label}</a>)}
? <span class="text-white" aria-current="page">{n.label}</span>
: <a href={withLang(n.href, locale)} class="text-neutral-400 transition-colors hover:text-violet-400">{n.label}</a>)}
</nav>
)}
</header>

<style>
/* Titular de la casa (Archivo expandida, mayúsculas) a una escala que cabe
en la columna de la ficha; sin !important para no pelear con .nan-scale. */
.ev-title {
font-family: var(--font-serif);
font-variation-settings: 'wdth' 118, 'wght' 850;
font-size: clamp(2rem, 5.5vw, 3.5rem);
line-height: 1.02;
letter-spacing: -0.02em;
text-transform: uppercase;
overflow-wrap: break-word;
text-wrap: balance;
}
</style>
21 changes: 10 additions & 11 deletions src/components/events/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,34 +66,33 @@ export default function LoginForm({ t }: { t: LoginStrings }) {

if (status.kind === 'success') {
return (
<div role="status" class="rounded-xl border border-violet-500/30 bg-violet-950/20 p-6 text-center">
<p class="text-sm text-white leading-relaxed">{t.success}</p>
<div role="status" class="border border-violet-500/40 bg-violet-500/10 p-8 text-center md:p-10">
<p class="text-lg leading-relaxed text-white">{t.success}</p>
</div>
);
}

const submitting = status.kind === 'submitting';
const err = status.kind === 'error' ? status.message : null;
const inputCls =
'w-full bg-neutral-950 border border-neutral-800 rounded-lg px-4 py-3 text-sm text-white font-mono placeholder-neutral-600 focus:outline-none focus:border-violet-500/60 focus:ring-1 focus:ring-violet-500/40 transition-colors';
// .field y .btn son los controles de la casa (styles/nan-system.css).
const inputCls = 'field w-full';

return (
<form onSubmit={onSubmit} noValidate class="rounded-xl border border-neutral-800 bg-neutral-900/30 p-6 md:p-8 space-y-4">
<form onSubmit={onSubmit} noValidate class="grid gap-6 border border-neutral-800 bg-neutral-900/40 p-8 md:p-10">
<div>
<p class="font-mono text-sm text-white">{t.prompt}</p>
<p class="mt-2 text-sm text-neutral-400 leading-relaxed">{t.description}</p>
<p class="font-mono text-sm uppercase tracking-[0.2em] text-violet-300">{t.prompt}</p>
<p class="mt-3 max-w-prose text-base leading-relaxed text-neutral-300">{t.description}</p>
</div>
<div>
<label class="block font-mono text-[10px] uppercase tracking-widest text-violet-400 mb-2" for="hk-email">
<label class="mb-2 block font-mono text-xs uppercase tracking-[0.2em] text-violet-400" for="hk-email">
{t.emailLabel}
</label>
<input id="hk-email" type="email" required disabled={submitting} value={email}
placeholder={t.emailPlaceholder}
onInput={(e) => setEmail((e.currentTarget as HTMLInputElement).value)} class={inputCls} />
</div>
{err && <p role="alert" class="text-sm text-red-400 font-mono">{err}</p>}
<button type="submit" disabled={submitting}
class="w-full font-mono text-sm px-8 py-3 rounded-lg bg-violet-600 text-white hover:bg-violet-500 hover:shadow-[0_0_24px_rgba(139,92,246,0.4)] transition-all disabled:opacity-50">
{err && <p role="alert" class="font-mono text-sm text-red-400">{err}</p>}
<button type="submit" disabled={submitting} class="btn btn-primary w-full disabled:opacity-50">
{submitting ? t.submitting : t.submit}
</button>
</form>
Expand Down
21 changes: 18 additions & 3 deletions src/components/events/ReassignForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface ReassignLabels {
selectAbsent: string;
errorSubmit: string;
sending: string;
refresh: string;
}

export default function ReassignForm({
Expand All @@ -39,6 +40,10 @@ export default function ReassignForm({
const [ghostId, setGhostId] = useState('');
const [state, setState] = useState<'idle' | 'busy' | 'done' | 'error'>('idle');
const [result, setResult] = useState('');
// La convocatoria que se ejecuta cambia el equipo (sale el ausente, entra la
// reserva), y la lista de compañeros viene del servidor: sin recargar seguiría
// enseñando el equipo de antes justo debajo del aviso de que ha cambiado.
const [resolved, setResolved] = useState(false);
const [showInfo, setShowInfo] = useState(false);

async function submit() {
Expand All @@ -62,8 +67,8 @@ export default function ReassignForm({
return;
}
const r = body?.data;
if (r?.status === 'filled') setResult(labels.filled);
else if (r?.status === 'no_pool') setResult(labels.noPool);
if (r?.status === 'filled') { setResult(labels.filled); setResolved(true); }
else if (r?.status === 'no_pool') { setResult(labels.noPool); setResolved(true); }
else {
const have = String(r?.requested_by?.length ?? 1);
const need = String(r?.quorum ?? 2);
Expand All @@ -76,7 +81,17 @@ export default function ReassignForm({
}

if (state === 'done') {
return <p class="font-mono text-sm text-violet-400">{result}</p>;
return (
<div class="space-y-3">
<p class="font-mono text-sm text-violet-400">{result}</p>
{resolved && (
<button type="button" onClick={() => location.reload()}
class="font-mono text-xs px-5 py-2 rounded-lg border border-neutral-700 text-neutral-300 hover:border-violet-400 hover:text-violet-300">
{labels.refresh}
</button>
)}
</div>
);
}

return (
Expand Down
Loading
Loading