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
2 changes: 0 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
# Default owners for everything in the repo.
# These maintainers are requested for review on every pull request.
* @eluciano11 @rnegron
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Commands

Node version is pinned via `.nvmrc` (currently `22.22.2`, lts/jod). Use `nvm use` before running anything.
Node version is pinned via `.nvmrc`. Use `nvm use` before running anything.

- `npm run develop` — start the Gatsby dev server (`gatsby develop`); also aliased as `npm start`
- `npm run build` — production build into `public/`
Expand Down
2 changes: 1 addition & 1 deletion gatsby-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module.exports = {
background_color: `#0282f8`,
theme_color: `#0282f8`,
display: `standalone`,
icon: `src/images/fsn.png` // This path is relative to the root of the site.
icon: `src/images/fsn.png`
}
},
`gatsby-plugin-postcss`,
Expand Down
12 changes: 1 addition & 11 deletions netlify/functions/submission-created.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
// Netlify calls this automatically on every verified form submission
// (spam/honeypot hits are filtered out before this runs). It forwards the
// submission to a private Discord channel via an incoming webhook.
//
// This is an event-triggered function (named after the "submission-created"
// event), so Netlify invokes it internally — it is not exposed as a public
// HTTP endpoint. Set DISCORD_WEBHOOK_URL in Netlify → Environment variables.

// null-prototype map so a form-controlled name can't resolve to an inherited
// Object.prototype member (e.g. "toString", "constructor").
// Null prototype: form names are user-controlled.
const TITLES = Object.assign(Object.create(null), {
"speaker-submission": "🎤 New speaker / topic submission",
feedback: "💬 New feedback"
});

// Netlify metadata we don't want to echo into Discord.
const HIDDEN_FIELDS = new Set(["bot-field", "form-name"]);

exports.handler = async (event) => {
Expand Down
7 changes: 0 additions & 7 deletions src/components/layout.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
/**
* Layout component that queries for data
* with Gatsby's useStaticQuery component
*
* See: https://www.gatsbyjs.org/docs/use-static-query/
*/

import React from "react";
import PropTypes from "prop-types";

Expand Down
3 changes: 1 addition & 2 deletions src/components/netlify-honeypot.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React from "react";
import { useTranslation } from "react-i18next";

// Shared Netlify form plumbing: the hidden form-name (must match the <form>'s
// name) plus the honeypot field that traps bots. Used by every Netlify form.
// formName must match the <form>'s name attribute.
function NetlifyHoneypot({ formName }) {
const { t } = useTranslation();

Expand Down
1 change: 0 additions & 1 deletion src/components/sponsors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";

import PageSection from "./page-section";

// Hidden unless an active event defines `event.sponsors`.
function Sponsors({ sponsors = [] }) {
const { t } = useTranslation();

Expand Down
4 changes: 0 additions & 4 deletions src/components/submission-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import NetlifyHoneypot from "./netlify-honeypot";

const FORM_NAME = "speaker-submission";

// Maps the ?type= query param (set by each page's CTA) to a pre-selected intent.
const INTENT_BY_TYPE = {
talk: "Present a talk",
table: "Host a topic table",
Expand Down Expand Up @@ -36,15 +35,12 @@ function SubmissionForm() {
const [status, setStatus] = useState("idle");
const successRef = useRef(null);

// Move focus to the confirmation so screen-reader users are told the outcome.
useEffect(() => {
if (status === "success" && successRef.current) {
successRef.current.focus();
}
}, [status]);

// Pre-select the intent based on which CTA the visitor arrived from. Done in
// an effect (not initial state) so it doesn't fight server-side hydration.
useEffect(() => {
const type = new URLSearchParams(window.location.search).get("type");
if (INTENT_BY_TYPE[type]) setIntent(INTENT_BY_TYPE[type]);
Expand Down
56 changes: 2 additions & 54 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,58 +1,6 @@
// 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: 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: {
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/"
}
]
}
activeEvent: false,
event: {}
};

export default CONFIG;
5 changes: 0 additions & 5 deletions src/date.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@ 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);

Expand Down
3 changes: 0 additions & 3 deletions src/language.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// 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";
}
3 changes: 0 additions & 3 deletions src/pages/schedule.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ function buildTimeline(event, t, language) {
header: t("schedule.introduction-and-kick-off"),
subheader: t("schedule.grab-your-seat")
},
// Slots stay unnamed so the schedule doesn't commit to a speaking order.
...event.participants.map((_, index) => ({
dateTime: at(firstTalkMinutes + index * talkMinutes),
header: t("schedule.presentation", { number: index + 1 })
Expand Down Expand Up @@ -76,8 +75,6 @@ export default function Schedule() {
const { t, i18n } = useTranslation();
const { activeEvent, event } = CONFIG;

// The nav hides /schedule/ off-season, but the route stays reachable by
// bookmark — point those visitors at the calendar, not an empty timeline.
if (!activeEvent) {
return (
<Layout>
Expand Down
2 changes: 1 addition & 1 deletion src/past-speakers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const PAST_SPEAKERS = [
{ name: "Axel Rivera", link: "", img: "/past-speakers/axel-rivera.jpeg" },
{
name: "Jonathan González", // Not found
name: "Jonathan González",
link: "",
img: "/past-speakers/jonathan-gonzalez.jpeg"
},
Expand Down
2 changes: 0 additions & 2 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ svg {
transition: width 0.3s;
}

/* Default form styles */
/* Feedback form overrides */
.feedback__input {
@apply border border-white-hover px-2 py-2 mb-3 w-full rounded resize-none;
border-width: 1px;
Expand Down
Loading