From 4ab43c2f7713fbca5f16e2b47696848edbc2afdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20G=C3=B3mez=20Jim=C3=A9nez?= Date: Thu, 1 Oct 2026 13:02:48 +0200 Subject: [PATCH] feat(events): the published panel says who the event is for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Abierto a todos" was shown on every published informative event, which is false for the workshops held as Discord events of the private NaN server. Read `ev.access` (B-28, helmcode/nan-events-backend#3) and pick the copy: "members" → "Solo para la comunidad de NaN", "open" → "Abierto a todos", empty → neutral "Evento anunciado". The CTA is unchanged. Guards: the page branches on the field with the neutral fallback, both locales carry the three variants and only the open one says "everyone". Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014ToscGMqPkLEBZ6x8MhcP7 --- i18n/en.json | 8 ++++++-- i18n/es.json | 8 ++++++-- src/lib/events.ts | 2 ++ src/lib/eventsCalendar.ts | 2 +- src/pages/events/[slug]/_index.astro | 15 ++++++++++++--- src/tests/lib/eventsCalendar.test.ts | 2 +- src/tests/pages/eventsUiGuards.test.ts | 25 +++++++++++++++++++++++++ 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 55e01b61..c4db52be 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -107,8 +107,12 @@ "resultsCta": "View leaderboard", "closedEventTitle": "Event closed", "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.", + "publishedTitle": "Event announced", + "published": "Add it to your calendar and join through the link on the day.", + "publishedOpenTitle": "Open to everyone", + "publishedOpen": "No NaN registration needed. Add it to your calendar and join through the link on the day.", + "publishedMembersTitle": "NaN members only", + "publishedMembers": "This event is for NaN members only. If you are one, open the link, mark yourself as interested and Discord will remind you on the day.", "publishedCta": "Join the event" }, "alreadyRegistered": { diff --git a/i18n/es.json b/i18n/es.json index 1d8a8fd5..6fdcb3e1 100644 --- a/i18n/es.json +++ b/i18n/es.json @@ -107,8 +107,12 @@ "resultsCta": "Ver clasificación", "closedEventTitle": "Evento cerrado", "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.", + "publishedTitle": "Evento anunciado", + "published": "Apúntalo en tu calendario y entra por el enlace el día del evento.", + "publishedOpenTitle": "Abierto a todos", + "publishedOpen": "Este evento no necesita inscripción en NaN. Apúntalo en tu calendario y entra por el enlace el día del evento.", + "publishedMembersTitle": "Solo para la comunidad de NaN", + "publishedMembers": "Este evento es solo para miembros de NaN. Si ya eres miembro, entra por el enlace, márcate como interesado y Discord te avisará el día del evento.", "publishedCta": "Ir al evento" }, "alreadyRegistered": { diff --git a/src/lib/events.ts b/src/lib/events.ts index fa96b990..3e403932 100644 --- a/src/lib/events.ts +++ b/src/lib/events.ts @@ -154,6 +154,8 @@ export interface EventInfo { /** Dónde se celebra (texto libre) y enlace de acceso; opcionales (SPEC v3 §3.1, B-25). */ location: string; url: string; + /** Who the event is for (B-28): copy only, the page never enforces it. */ + access: 'open' | 'members' | ''; /** Cover image (http/https, optional; B-27): shown on the public page and used as og:image. */ image_url: string; format: EventFormat; diff --git a/src/lib/eventsCalendar.ts b/src/lib/eventsCalendar.ts index 680bacd4..ce106d94 100644 --- a/src/lib/eventsCalendar.ts +++ b/src/lib/eventsCalendar.ts @@ -16,7 +16,7 @@ import { apiBase, jsonData, ssrHeaders, type EventInfo } from './events'; /** Resumen de `GET /api/events`: los campos de la ficha que usa la página. */ export type PublicEventSummary = Pick< EventInfo, - 'slug' | 'kind' | 'name' | 'description' | 'location' | 'url' | 'image_url' | 'status' | 'dates' + 'slug' | 'kind' | 'name' | 'description' | 'location' | 'url' | 'access' | 'image_url' | 'status' | 'dates' >; /** Ruta del feed dentro del sitio: pasa por el proxy same-origin. */ diff --git a/src/pages/events/[slug]/_index.astro b/src/pages/events/[slug]/_index.astro index 4fb3f8ea..7e916d45 100644 --- a/src/pages/events/[slug]/_index.astro +++ b/src/pages/events/[slug]/_index.astro @@ -83,9 +83,18 @@ const submittedBody = closeAt ? fill(phase.submitted, { date: closeAt }) : phase switch (ev.phase) { case 'published': // Informativo anunciado (B-26): no hay inscripción; el CTA es el enlace del evento si lo tiene. - panel = ev.url - ? { title: phase.publishedTitle, body: phase.published, href: ev.url, cta: phase.publishedCta, tone: 'accent' } - : { title: phase.publishedTitle, body: phase.published, tone: 'accent' }; + // El título y el texto dependen de `access` (B-28): "members" es solo para la comunidad, + // "open" es abierto a todos y vacío es neutro, para no anunciar como abierto lo que no lo es. + { + const copy = ev.access === 'members' + ? { title: phase.publishedMembersTitle, body: phase.publishedMembers } + : ev.access === 'open' + ? { title: phase.publishedOpenTitle, body: phase.publishedOpen } + : { title: phase.publishedTitle, body: phase.published }; + panel = ev.url + ? { ...copy, href: ev.url, cta: phase.publishedCta, tone: 'accent' } + : { ...copy, tone: 'accent' }; + } break; case 'draft': case 'registration': diff --git a/src/tests/lib/eventsCalendar.test.ts b/src/tests/lib/eventsCalendar.test.ts index 3409ec94..5f4c791a 100644 --- a/src/tests/lib/eventsCalendar.test.ts +++ b/src/tests/lib/eventsCalendar.test.ts @@ -21,7 +21,7 @@ import { const ev = (over: Partial = {}): PublicEventSummary => ({ slug: 'taller-agentes', kind: 'workshop', name: 'Taller de agentes', description: 'Qué es un agente.', - location: 'Discord de NaN', url: '', image_url: '', status: 'closed', + location: 'Discord de NaN', url: '', access: '', image_url: '', status: 'closed', dates: { demo_day: '2026-09-16T17:00:00Z', demo_day_end: null }, ...over, }); diff --git a/src/tests/pages/eventsUiGuards.test.ts b/src/tests/pages/eventsUiGuards.test.ts index 2a63cc9f..e03f4911 100644 --- a/src/tests/pages/eventsUiGuards.test.ts +++ b/src/tests/pages/eventsUiGuards.test.ts @@ -156,3 +156,28 @@ describe('event cover (B-27)', () => { expect(base).toContain('const ogImageGeneric = !image;'); }); }); + +describe('event access (B-28)', () => { + const page = readFileSync(resolve(src, 'pages/events/[slug]/_index.astro'), 'utf-8'); + const es = JSON.parse(readFileSync(resolve(src, '../i18n/es.json'), 'utf-8')); + const en = JSON.parse(readFileSync(resolve(src, '../i18n/en.json'), 'utf-8')); + + test('the published panel picks its copy from ev.access, with a neutral fallback', () => { + expect(page).toMatch(/ev\.access === 'members'\s*\? \{ title: phase\.publishedMembersTitle, body: phase\.publishedMembers \}/); + expect(page).toMatch(/ev\.access === 'open'\s*\? \{ title: phase\.publishedOpenTitle, body: phase\.publishedOpen \}/); + expect(page).toMatch(/: \{ title: phase\.publishedTitle, body: phase\.published \}/); + }); + + test('both locales carry the three variants and only the open one says "everyone"', () => { + for (const dict of [es, en]) { + const phase = dict.events.phase; + for (const key of ['publishedTitle', 'published', 'publishedOpenTitle', 'publishedOpen', 'publishedMembersTitle', 'publishedMembers']) { + expect(phase[key], key).toBeTruthy(); + } + expect(phase.publishedTitle).not.toMatch(/todos|everyone/i); + expect(phase.publishedMembersTitle).not.toMatch(/todos|everyone/i); + } + expect(es.events.phase.publishedOpenTitle).toBe('Abierto a todos'); + expect(en.events.phase.publishedOpenTitle).toBe('Open to everyone'); + }); +});