Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 8 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ Static marketing site for Fullstack Nights (a Puerto Rico tech/design community)

`CONFIG.activeEvent` is the single switch that drives event-related UI across the site:

- When `false`: the `/schedule/` route is filtered out of the nav (`getSections` in `src/constants.js`) and the homepage hides `<UpcomingEvent>`.
- When `true`: set `CONFIG.event` to `{ type, date, venue, participants }`. `type` is `"topic-tables"` or anything else (treated as Speakers). The homepage layout shifts (different spacing classes are applied).
- When `false`: the `/schedule/` route is filtered out of the nav (`getSections` in `src/constants.js`), the homepage hides `<UpcomingEvent>`, and `/schedule/` falls back to the Luma calendar for anyone arriving by bookmark.
- When `true`: set `CONFIG.event` to `{ type, date, registrationUrl, venue, participants }`, plus optional `endDate`, `agenda`, and `sponsors`. The homepage layout shifts (different spacing classes are applied).

`date` and `endDate` are offset-less ISO strings (`"2026-09-10T18:00:00"`). JavaScript parses that form as _local_ time, so every visitor sees the venue's wall-clock time instead of their own timezone — do not append a `-04:00` offset or the times will shift for out-of-state visitors.

`/schedule/` builds its timeline from `participants`, allotting one equal-length slot per speaker. The slots are numbered ("Presentation 1") rather than named, so the schedule doesn't commit to a speaking order — only the count of `participants` reaches the timeline. `agenda` (`{ kickOffMinutes, firstTalkMinutes, talkMinutes }`) tunes those offsets; omit it to use `DEFAULT_AGENDA` in `src/pages/schedule.js`.

`type` only selects the "Topic Tables" vs "Speakers" label on the homepage card. The generated timeline is speakers-shaped either way — a topic-tables event needs a matching branch in `buildTimeline` plus the agenda copy in both locale files, neither of which exists today.

When adding a new event, edit only `src/config.js` — no other files should hardcode event data.

Expand Down
2 changes: 2 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import LumaEvents from "./luma-events";
import PageHighlight from "./page-highlight";
import PageSection from "./page-section";
import ProfileCard from "./profile-card";
import RegistrationButton from "./registration-button";
import Question from "./question";
import Sponsors from "./sponsors";
import SEO from "./seo";
Expand All @@ -32,6 +33,7 @@ export {
PageHighlight,
PageSection,
ProfileCard,
RegistrationButton,
Question,
Sponsors,
SEO,
Expand Down
4 changes: 3 additions & 1 deletion src/components/luma-events.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React from "react";
import { useTranslation } from "react-i18next";

import { resolveLanguage } from "../language";

const LUMA_CALENDAR_ID = "cal-xorZLhCJO1uKH5s";

function LumaEvents() {
const { t, i18n } = useTranslation();
const lang = i18n.language?.startsWith("es") ? "es" : "en";
const lang = resolveLanguage(i18n.language);
const src = `https://luma.com/embed/calendar/${LUMA_CALENDAR_ID}/events?lang=${lang}`;

return (
Expand Down
19 changes: 19 additions & 0 deletions src/components/registration-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import { useTranslation } from "react-i18next";

function RegistrationButton({ url }) {
const { t } = useTranslation();

return (
<a
className="btn btn--primary w-1/2 lg:w-2/12"
href={url}
target="_blank"
rel="noopener noreferrer"
>
{t("get-tickets")}
</a>
);
}

export default RegistrationButton;
2 changes: 2 additions & 0 deletions src/components/sponsors.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ function Sponsors({ sponsors = [] }) {
className="max-h-20 m-6"
src={sponsor.logo}
alt={sponsor.name}
width="160"
height="28"
/>
);

Expand Down
50 changes: 15 additions & 35 deletions src/components/upcoming-event.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React from "react";
import { useTranslation } from "react-i18next";

import { Link } from "gatsby";
import { format } from "date-fns";

import Card from "./card";
import ProfileCard from "./profile-card";
import RegistrationButton from "./registration-button";
import { formatFullDate, formatTime } from "../date";

const EVENTBRITE_LINK = "https://fullstacknights.eventbrite.com";

function UpcomingEvent({ participants, type, date, venue }) {
const { t } = useTranslation();
function UpcomingEvent({
participants,
type,
date,
endDate,
venue,
registrationUrl
}) {
const { t, i18n } = useTranslation();

return (
<section className="mb-20">
Expand All @@ -23,8 +27,9 @@ function UpcomingEvent({ participants, type, date, venue }) {
{type === "topic-tables" ? "Topic Tables" : "Speakers"}
</h4>
<p className="text-h4">
{format(new Date(date), "EEEE, MMMM d, y")},{" "}
{t("upcoming-event.at")} 7pm
{formatFullDate(date, i18n.language)}, {t("upcoming-event.at")}{" "}
{formatTime(date)}
{endDate ? ` – ${formatTime(endDate)}` : ""}
</p>
<p className="text-h4 flex items-center justify-center">
{t("upcoming-event.venue")}:
Expand All @@ -39,14 +44,7 @@ function UpcomingEvent({ participants, type, date, venue }) {
</p>
</div>
<div className="mb-10">
<a
className="btn btn--primary w-1/2 lg:w-2/12"
href={EVENTBRITE_LINK}
target="_blank"
rel="noopener noreferrer"
>
{t("upcoming-event.get-tickets")}
</a>
<RegistrationButton url={registrationUrl} />
</div>
<h4 className="text-h4 font-extrabold">
{t("upcoming-event.line-up")}
Expand All @@ -64,24 +62,6 @@ function UpcomingEvent({ participants, type, date, venue }) {
/>
))}
</div>
<p className="text-rg inline-block">
{t("upcoming-event.while-you-wait")}{" "}
<div className="inline-block">
<Link
className="flex flex-col self-center link"
to="/code-of-conduct"
>
{t("upcoming-event.code-of-conduct")}
</Link>
</div>
,{" "}
<div className="inline-block">
<Link className="flex flex-col link" to="/schedule">
{t("upcoming-event.schedule")}
</Link>
</div>{" "}
{t("upcoming-event.or-request-an-open-mic")}
</p>
</Card>
</section>
);
Expand Down
57 changes: 53 additions & 4 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,58 @@
// Luma keeps the street address guests-only, so the venue links to the event
// page rather than to a map.
const LUMA_EVENT_URL = "https://luma.com/s9jx3jri";

const CONFIG = {
activeEvent: false,
// When activeEvent is true, populate event with:
// { type, date, venue, participants, sponsors }
activeEvent: true,
// When activeEvent is true, `endDate`, `agenda`, and `sponsors` are optional;
// everything else is required.
// date/endDate are local wall-clock ISO strings (no offset) so every visitor
// sees the venue's time, not their own.
// agenda drives the /schedule/ timeline; omit it to use the defaults below.
// sponsors: [{ name, logo, url }] renders a Sponsors section on the home page.
event: {}
event: {
type: "speakers",
date: "2026-09-10T18:00:00",
endDate: "2026-09-10T21:00:00",
registrationUrl: LUMA_EVENT_URL,
agenda: {
kickOffMinutes: 20,
firstTalkMinutes: 30,
// 15 min talk + 5 min Q&A.
talkMinutes: 20
},
venue: {
name: "Bayamón, Puerto Rico",
location: LUMA_EVENT_URL
},
participants: [
{
name: "Giovanni Collazo",
topic: "jj es más fácil que git",
img: "/founders/giovanni-collazo.jpeg",
links: [{ network: "github", url: "https://github.com/gcollazo" }]
},
{
name: "Christian Rodríguez",
topic: "Tus agents necesitan acceso (seguro) a producción",
img: "/organizers/christian-rodriguez.jpeg",
links: [{ network: "github", url: "https://github.com/chrisrodz" }]
},
{
name: "Raúl Negrón-Otero",
topic: "Un vistazo a Django Template Partials",
img: "/organizers/raul-negron.jpeg",
links: [{ network: "github", url: "https://github.com/rnegron" }]
}
],
sponsors: [
{
name: "PostHog",
logo: "/sponsors/posthog.svg",
url: "https://posthog.com/"
}
]
}
};

export default CONFIG;
29 changes: 29 additions & 0 deletions src/date.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { format } from "date-fns";
import { enUS, es } from "date-fns/locale";

import { resolveLanguage } from "./language";

function isSpanish(language) {
return resolveLanguage(language) === "es";
}

// "Thursday, September 10th, 2026" / "jueves, 10 de septiembre de 2026"
export function formatFullDate(date, language) {
return format(date, "PPPP", { locale: isSpanish(language) ? es : enUS });
}

// "6:00pm" — Puerto Rico uses 12-hour time in both languages, and date-fns
// spells the day period the same way in each, so this needs no locale.
export function formatTime(date) {
return format(date, "h:mmaaa");
}

// "Sep 10" / "10 sep". date-fns has no localized month-and-day-only token, so
// the field order is spelled out per language.
export function formatShortDay(date, language) {
const spanish = isSpanish(language);

return format(date, spanish ? "d MMM" : "MMM d", {
locale: spanish ? es : enUS
});
}
3 changes: 2 additions & 1 deletion src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import i18next from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";

import { resolveLanguage } from "./language";
import englishLocale from "./locales/en.json";
import spanishLocale from "./locales/es.json";

export function getLanguageSwitcher({ i18n, classNames }) {
let langDisplay;
let langSwitch;

if (i18n.language === "en-US") {
if (resolveLanguage(i18n.language) === "en") {
langDisplay = "español";
langSwitch = "es-PR";
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/language.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// i18next hands out full tags ("en-US", "es-PR", and whatever the browser
// detector reports), but the site ships exactly two locales. Every "which
// language is this?" decision routes through here so the answer can't drift.
export function resolveLanguage(language) {
return language?.startsWith("es") ? "es" : "en";
}
18 changes: 5 additions & 13 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"find-us": "Find us on",
"join-us": "Join us",
"learn-more": "Learn more",
"get-tickets": "Get tickets",
"submit": "Submit",
"subject": "Subject",
"your-thoughts": "Your thoughts",
Expand Down Expand Up @@ -65,7 +66,6 @@
"ready-to-become-a-speaker": "Ready to become a speaker?",
"join-the-community": "Join the community in Discord",
"ask-questions-get-help": "Ask questions, get help from the community and stay up to date with the latest events.",

"we-will-help-polish-your-talk": "How it works",
"everyone-has-valuable-knowledge-to-share": "Everyone has valuable knowledge to share, and we keep the process simple. Submit your topic and we'll take it from there.",
"topic-scoping": "Topic scoping",
Expand Down Expand Up @@ -117,16 +117,13 @@
"schedule": {
"schedule": "Schedule",
"we-try-to-be-punctual": "We try to be as organized and punctual as possible. Check out our schedule below.",
"get-tickets": "Get tickets",
"make-your-way-to-our-venue": "Make your way to our venue",
"doors-open": "Doors open",
"check-in": "Check-in and reset your open-mic spot",
"check-in": "Check-in",
"introduction-and-kick-off": "Introduction and kick-off",
"grab-your-seat": "Grab your seat",
"open-mic-6-x-3": "Open Mic (6 x 3 min)",
"enjoy-the-event": "Enjoy the event!",
"presentation": "Presentation {{number}}",
"wrap-up-networking": "Wrap-up / Networking",
"networking-break": "Networking / Break",
"the-end": "The end"
},
"topic-tables": {
Expand Down Expand Up @@ -154,19 +151,14 @@
"upcoming-events": "Upcoming events",
"at": "at",
"venue": "Venue",
"get-tickets": "Get tickets",
"line-up": "Line up for the next event",
"while-you-wait": "While you wait for the event check out our",
"code-of-conduct": "code of conduct",
"schedule": "schedule",
"or-request-an-open-mic": "or request an open mic spot."
"line-up": "Line up for the next event"
},
"footer": {
"code-of-conduct": "Code of Conduct"
},
"sponsors": {
"title": "Sponsors",
"thanks-to-our-sponsors": "Thanks to the sponsors making this event possible."
"thanks-to-our-sponsors": "We thank our sponsors for making this edition possible."
},
"submission": {
"title": "Submit your topic",
Expand Down
18 changes: 5 additions & 13 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"find-us": "Encuéntranos en",
"join-us": "Únete",
"learn-more": "Conoce más",
"get-tickets": "Obtén tu boleto",
"submit": "Enviar",
"subject": "Título",
"your-thoughts": "Sus ideas",
Expand Down Expand Up @@ -65,7 +66,6 @@
"ready-to-become-a-speaker": "¡Se parte de esta lista!",
"join-the-community": "Únete a la comunidad en Discord",
"ask-questions-get-help": "Haz preguntas, obtén ayuda y mantente al tanto de los eventos más recientes.",

"we-will-help-polish-your-talk": "Cómo funciona",
"everyone-has-valuable-knowledge-to-share": "Toda persona tiene conocimiento valioso que aportar a la comunidad, y mantenemos el proceso sencillo. Envíanos tu tema y nosotros nos encargamos del resto.",
"topic-scoping": "Alcance del tema",
Expand Down Expand Up @@ -117,16 +117,13 @@
"schedule": {
"schedule": "Horario",
"we-try-to-be-punctual": "Tratamos de que el evento sea lo más organizado y puntual posible. Abajo puedes confirmar el horario.",
"get-tickets": "Obtén tu boleto",
"make-your-way-to-our-venue": "Llega hasta el local del evento",
"doors-open": "Puertas abren",
"check-in": "Registro y confirmación de micrófono abierto",
"check-in": "Registro",
"introduction-and-kick-off": "Introducción y comienzo",
"grab-your-seat": "Tome asiento",
"open-mic-6-x-3": "Open Mic (6 x 3 min)",
"enjoy-the-event": "¡Disfrute el evento!",
"presentation": "Presentación {{number}}",
"wrap-up-networking": "Conclusión / Networking",
"networking-break": "Networking / Descanso",
"the-end": "Fin"
},
"topic-tables": {
Expand Down Expand Up @@ -154,19 +151,14 @@
"upcoming-events": "Próximos eventos",
"at": "a las",
"venue": "Sitio",
"get-tickets": "Obtén tu boleto",
"line-up": "Apúntate para el próximo evento",
"while-you-wait": "A lo que llega la hora del próximo evento, revisa nuestro",
"code-of-conduct": "código de conducta",
"or-request-an-open-mic": "o solicita un espacio de microfono abierto.",
"schedule": "horario"
"line-up": "Apúntate para el próximo evento"
},
"footer": {
"code-of-conduct": "Código de Conducta"
},
"sponsors": {
"title": "Patrocinadores",
"thanks-to-our-sponsors": "Gracias a los patrocinadores que hacen este evento posible."
"thanks-to-our-sponsors": "Agradecemos a nuestros patrocinadores por hacer posible esta edición."
},
"submission": {
"title": "Envía tu tema",
Expand Down
2 changes: 2 additions & 0 deletions src/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ function IndexPage() {
<UpcomingEvent
type={event.type}
date={event.date}
endDate={event.endDate}
venue={event.venue}
registrationUrl={event.registrationUrl}
participants={event.participants}
/>
) : (
Expand Down
Loading
Loading