diff --git a/SEO_REPORT.md b/SEO_REPORT.md
new file mode 100644
index 0000000..fe73944
--- /dev/null
+++ b/SEO_REPORT.md
@@ -0,0 +1,91 @@
+# SEO Audit Report — JavaScript Chile
+Generated: 2026-06-20
+
+> **Nota:** Este es un proyecto Next.js App Router. Las correcciones se aplican en `src/app/layout.tsx`, `public/robots.txt` y `public/sitemap.xml` directamente (no existe carpeta `seo-optimized/` porque no son HTMLs estáticos).
+
+---
+
+## Summary
+- Páginas analizadas: 1 (SPA — single page)
+- Issues críticos: 4
+- Warnings: 3
+- Checks pasados: 7
+
+---
+
+## Resultados
+
+### `src/app/layout.tsx` (metadata global)
+
+#### ✅ Pasando
+- `` presente y correcto
+- `
` bien formado: "JavaScript Chile | Comunidad de Desarrolladores" (52 chars)
+- `` presente y relevante (~200 chars — ver warning)
+- `og:title` presente
+- `og:description` presente
+- `og:url` presente (`https://jschile.org/`)
+- `og:siteName` presente
+- `og:locale` presente (`es_CL`)
+- `og:type` presente (`website`)
+
+#### ⚠️ Warnings
+- **Description demasiado larga**: 218 chars (recomendado: 150–160). Google la trunca en SERP.
+- **`metadataBase` es string**: debe ser `new URL('https://jschile.org')` para que Next.js resuelva URLs relativas correctamente.
+- **Sin `twitter:card`**: falta por completo. Afecta previews en X/Twitter.
+
+#### ❌ Issues Críticos
+- **`og:image` apunta a `/icon.png` que NO EXISTE** (fue eliminado al convertir imágenes a webp). Las previews sociales (LinkedIn, WhatsApp, Twitter) están rotas.
+- **Sin ``**: sin esto Google puede indexar duplicados (www vs non-www, http vs https).
+- **Sin `robots.txt`**: Google no tiene instrucciones de crawl.
+- **Sin `sitemap.xml`**: Google no conoce la estructura del sitio.
+
+---
+
+### Imágenes
+
+#### ✅ Pasando
+- `group-crowd.webp` → alt: "Asistentes en evento JavaScript Chile" ✅
+- `group-selfie.webp` → alt: "JSConf Chile — foto grupal" ✅
+- `group-stage.webp` → alt: "Escenario JSConf Chile" ✅
+- Logo en Navbar → alt: "JavaScript Chile" ✅
+- Logo en Footer → alt: "JavaScript Chile" ✅
+- Mapa Chile → alt: "Mapa de Chile" ✅
+- Fotos de galería → alt descriptivo ✅
+
+#### ⚠️ Warning
+- **Imágenes de staff sin alt en StaffCard**: usan `background-image` CSS en lugar de `
`. Google no indexa imágenes de fondo CSS. Las fotos de staff son invisibles para buscadores.
+
+---
+
+## Issues Globales
+- `robots.txt` ausente
+- `sitemap.xml` ausente
+- OG image rota (`/icon.png` eliminado)
+- Twitter Card inexistente
+- `metadataBase` es string, no URL
+- `canonical` ausente
+
+---
+
+## Quick Wins (fix estos primero)
+1. ✅ **APLICADO** — Crear `robots.txt` en `/public/`
+2. ✅ **APLICADO** — Crear `sitemap.xml` en `/public/`
+3. ✅ **APLICADO** — Corregir `metadataBase` a `new URL(...)`
+4. ✅ **APLICADO** — Añadir Twitter Card tags
+5. ✅ **APLICADO** — Añadir canonical
+6. ✅ **APLICADO** — Acortar description a 155 chars
+7. ⚠️ **PENDIENTE MANUAL** — Crear imagen OG 1200×630px y reemplazar `/icon.png` en metadata (ver abajo)
+8. ⚠️ **PENDIENTE MANUAL** — StaffCard: cambiar background-image CSS por `
` con alt text
+
+---
+
+## Imagen OG — Acción requerida
+
+Se necesita una imagen de **1200×630 px** en formato JPG o PNG para las previews sociales.
+
+**Opciones:**
+- Usar `hero-bg.webp` redimensionada a 1200×630 (conversión manual o con sharp)
+- Diseñar una imagen de marca con el logo y claim
+- Usar Next.js [opengraph-image.tsx](https://nextjs.org/docs/app/api-reference/file-conventions/opengraph-image) para generarla dinámicamente
+
+Mientras tanto se ha configurado la URL a `/assets/og-image.jpg` — **debes crear ese archivo**.
diff --git a/package-lock.json b/package-lock.json
index a214c97..5c232e6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@mui/material-nextjs": "^7.3.8",
+ "@phosphor-icons/react": "^2.1.10",
"framer-motion": "^12.34.3",
"next": "16.1.6",
"react": "19.2.3",
@@ -29,6 +30,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
+ "node-ical": "^0.26.1",
"typescript": "^5"
},
"engines": {
@@ -1189,6 +1191,19 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@js-temporal/polyfill": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@js-temporal/polyfill/-/polyfill-0.5.1.tgz",
+ "integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "jsbi": "^4.3.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/@mui/core-downloads-tracker": {
"version": "7.3.9",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.9.tgz",
@@ -1662,6 +1677,19 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@phosphor-icons/react": {
+ "version": "2.1.10",
+ "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.10.tgz",
+ "integrity": "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8",
+ "react-dom": ">= 16.8"
+ }
+ },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
@@ -4739,6 +4767,13 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsbi": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.2.tgz",
+ "integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5093,6 +5128,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/node-ical": {
+ "version": "0.26.1",
+ "resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.26.1.tgz",
+ "integrity": "sha512-KoYLpsz7Ga9lPDpt9vy0iKcgcb/9Ix7ICRZd0csLXMl2lZOSONGj7HrcktJFR7Jid1l44Zu1H4k/1nB04rWPgQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "rrule-temporal": "^1.5.3",
+ "temporal-polyfill": "^0.3.2"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/node-releases": {
"version": "2.0.36",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz",
@@ -5650,6 +5699,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rrule-temporal": {
+ "version": "1.5.3",
+ "resolved": "https://registry.npmjs.org/rrule-temporal/-/rrule-temporal-1.5.3.tgz",
+ "integrity": "sha512-qALnXyu4MKNUeykkkO0r6Xxl5or3rM8Cf6ibKIe/29sgmq3tGm1oNq4G1Ddp8Ku3mnKmvC3+3yFAJ3OgOu6OJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@js-temporal/polyfill": "^0.5.1"
+ }
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6200,6 +6259,23 @@
"uuid": "^10.0.0"
}
},
+ "node_modules/temporal-polyfill": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.2.tgz",
+ "integrity": "sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "temporal-spec": "0.3.1"
+ }
+ },
+ "node_modules/temporal-spec": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.1.tgz",
+ "integrity": "sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
diff --git a/package.json b/package.json
index 670c14f..9e65f07 100644
--- a/package.json
+++ b/package.json
@@ -3,12 +3,12 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
- "build": "next build",
+ "fetch-events": "node scripts/fetch-events.mjs",
+ "dev": "npm run fetch-events && next dev",
+ "build": "npm run fetch-events && next build",
"start": "next start",
- "generate": "next build",
+ "generate": "npm run build",
"prestart": "npm run generate",
- "predev": "npm run generate",
"typecheck": "tsc",
"lint": "npx eslint --cache src",
"lint:fix": "npx eslint --cache src --fix",
@@ -26,6 +26,7 @@
"@mui/icons-material": "^7.3.8",
"@mui/material": "^7.3.8",
"@mui/material-nextjs": "^7.3.8",
+ "@phosphor-icons/react": "^2.1.10",
"framer-motion": "^12.34.3",
"next": "16.1.6",
"react": "19.2.3",
@@ -40,6 +41,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.1.6",
+ "node-ical": "^0.26.1",
"typescript": "^5"
}
}
diff --git a/public/assets/Bg-footer.webp b/public/assets/Bg-footer.webp
new file mode 100644
index 0000000..408a6e5
Binary files /dev/null and b/public/assets/Bg-footer.webp differ
diff --git a/public/assets/OG.png b/public/assets/OG.png
new file mode 100644
index 0000000..f160b75
Binary files /dev/null and b/public/assets/OG.png differ
diff --git a/public/assets/banner-inivitation-whatsapp.webp b/public/assets/banner-inivitation-whatsapp.webp
new file mode 100644
index 0000000..d23e596
Binary files /dev/null and b/public/assets/banner-inivitation-whatsapp.webp differ
diff --git a/public/assets/bg-line-time.webp b/public/assets/bg-line-time.webp
new file mode 100644
index 0000000..4dc87a5
Binary files /dev/null and b/public/assets/bg-line-time.webp differ
diff --git a/public/assets/default.webp b/public/assets/default.webp
new file mode 100644
index 0000000..a1b541b
Binary files /dev/null and b/public/assets/default.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.jpeg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.jpeg
deleted file mode 100644
index 8af861c..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.jpeg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.webp
new file mode 100644
index 0000000..80f5d92
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.jpeg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.jpeg
deleted file mode 100644
index 3ac10e6..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.jpeg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.webp
new file mode 100644
index 0000000..de932c4
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.jpeg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.jpeg
deleted file mode 100644
index cb3eb05..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.jpeg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.webp
new file mode 100644
index 0000000..df973d6
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.jpeg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.jpeg
deleted file mode 100644
index 14901c3..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.jpeg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.webp
new file mode 100644
index 0000000..203f2ed
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.jpeg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.jpeg
deleted file mode 100644
index 3273a9e..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.jpeg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.webp
new file mode 100644
index 0000000..bf68a3c
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.webp differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.jpg b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.jpg
deleted file mode 100644
index 8156deb..0000000
Binary files a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.jpg and /dev/null differ
diff --git a/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.webp b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.webp
new file mode 100644
index 0000000..342571e
Binary files /dev/null and b/public/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.webp differ
diff --git a/public/assets/events/group-crowd.webp b/public/assets/events/group-crowd.webp
new file mode 100644
index 0000000..419dbd5
Binary files /dev/null and b/public/assets/events/group-crowd.webp differ
diff --git a/public/assets/events/group-selfie.webp b/public/assets/events/group-selfie.webp
new file mode 100644
index 0000000..91e7830
Binary files /dev/null and b/public/assets/events/group-selfie.webp differ
diff --git a/public/assets/events/group-stage.webp b/public/assets/events/group-stage.webp
new file mode 100644
index 0000000..d55d6ef
Binary files /dev/null and b/public/assets/events/group-stage.webp differ
diff --git a/public/assets/hero-bg.webp b/public/assets/hero-bg.webp
new file mode 100644
index 0000000..9d03936
Binary files /dev/null and b/public/assets/hero-bg.webp differ
diff --git a/public/assets/javascript-chile-logo.png b/public/assets/javascript-chile-logo.png
deleted file mode 100644
index a6a5f2f..0000000
Binary files a/public/assets/javascript-chile-logo.png and /dev/null differ
diff --git a/public/assets/js-chile-mapa.svg b/public/assets/js-chile-mapa.svg
new file mode 100644
index 0000000..79256ae
--- /dev/null
+++ b/public/assets/js-chile-mapa.svg
@@ -0,0 +1,15 @@
+
diff --git a/public/assets/logo-cljs.svg b/public/assets/logo-cljs.svg
new file mode 100644
index 0000000..703d72a
--- /dev/null
+++ b/public/assets/logo-cljs.svg
@@ -0,0 +1,14 @@
+
diff --git a/public/assets/square-logo-cljs.svg b/public/assets/square-logo-cljs.svg
new file mode 100644
index 0000000..b812cf9
--- /dev/null
+++ b/public/assets/square-logo-cljs.svg
@@ -0,0 +1,81 @@
+
diff --git a/public/assets/staff/staff-armando-rivas-dev.jpg b/public/assets/staff/staff-armando-rivas-dev.jpg
deleted file mode 100644
index e396314..0000000
Binary files a/public/assets/staff/staff-armando-rivas-dev.jpg and /dev/null differ
diff --git a/public/assets/staff/staff-armando-rivas-dev.webp b/public/assets/staff/staff-armando-rivas-dev.webp
new file mode 100644
index 0000000..11a2bab
Binary files /dev/null and b/public/assets/staff/staff-armando-rivas-dev.webp differ
diff --git a/public/assets/staff/staff-carlos-romero.jpeg b/public/assets/staff/staff-carlos-romero.jpeg
deleted file mode 100644
index 80c0513..0000000
Binary files a/public/assets/staff/staff-carlos-romero.jpeg and /dev/null differ
diff --git a/public/assets/staff/staff-carlos-romero.webp b/public/assets/staff/staff-carlos-romero.webp
new file mode 100644
index 0000000..7875fcf
Binary files /dev/null and b/public/assets/staff/staff-carlos-romero.webp differ
diff --git a/public/assets/staff/staff-cecilia-geraldo.jpeg b/public/assets/staff/staff-cecilia-geraldo.jpeg
deleted file mode 100644
index 7c958f2..0000000
Binary files a/public/assets/staff/staff-cecilia-geraldo.jpeg and /dev/null differ
diff --git a/public/assets/staff/staff-cecilia-geraldo.webp b/public/assets/staff/staff-cecilia-geraldo.webp
new file mode 100644
index 0000000..c44090c
Binary files /dev/null and b/public/assets/staff/staff-cecilia-geraldo.webp differ
diff --git a/public/assets/staff/staff-erika-pinedo.webp b/public/assets/staff/staff-erika-pinedo.webp
new file mode 100644
index 0000000..a440a01
Binary files /dev/null and b/public/assets/staff/staff-erika-pinedo.webp differ
diff --git a/public/assets/staff/staff-javier-vargas.png b/public/assets/staff/staff-javier-vargas.png
deleted file mode 100644
index f77ca2d..0000000
Binary files a/public/assets/staff/staff-javier-vargas.png and /dev/null differ
diff --git a/public/assets/staff/staff-javier-vargas.webp b/public/assets/staff/staff-javier-vargas.webp
new file mode 100644
index 0000000..115e594
Binary files /dev/null and b/public/assets/staff/staff-javier-vargas.webp differ
diff --git a/public/assets/staff/staff-mapachito.jpeg b/public/assets/staff/staff-mapachito.jpeg
deleted file mode 100644
index 5194f78..0000000
Binary files a/public/assets/staff/staff-mapachito.jpeg and /dev/null differ
diff --git a/public/assets/staff/staff-mapachito.webp b/public/assets/staff/staff-mapachito.webp
new file mode 100644
index 0000000..882a9ac
Binary files /dev/null and b/public/assets/staff/staff-mapachito.webp differ
diff --git a/public/assets/staff/staff-samuel-angulo.jpeg b/public/assets/staff/staff-samuel-angulo.jpeg
deleted file mode 100644
index 477f61d..0000000
Binary files a/public/assets/staff/staff-samuel-angulo.jpeg and /dev/null differ
diff --git a/public/assets/staff/staff-samuel-angulo.webp b/public/assets/staff/staff-samuel-angulo.webp
new file mode 100644
index 0000000..0e36828
Binary files /dev/null and b/public/assets/staff/staff-samuel-angulo.webp differ
diff --git a/public/events.json b/public/events.json
new file mode 100644
index 0000000..856818c
--- /dev/null
+++ b/public/events.json
@@ -0,0 +1,236 @@
+{
+ "generated_at": "2026-07-08T01:04:43.838Z",
+ "upcoming": [],
+ "past": [
+ {
+ "uid": "evt-i6BhFdLiCz08KXx@events.lu.ma",
+ "title": "Meetup Invierno JSCHILE + Frontend USACH",
+ "start": "2026-07-04T13:00:00.000Z",
+ "end": "2026-07-04T17:00:00.000Z",
+ "location": null,
+ "url": "https://luma.com/kf94jzi8",
+ "cover_url": "https://images.lumacdn.com/uploads/g5/54714ab0-b3d0-4e6b-801d-76913a16b575.png",
+ "status": "past"
+ },
+ {
+ "uid": "evt-Nk9yR6T7rdnx5a1@events.lu.ma",
+ "title": "3era Meetup JS Chile @ Buk",
+ "start": "2026-05-27T22:30:00.000Z",
+ "end": "2026-05-28T01:00:00.000Z",
+ "location": null,
+ "url": "https://luma.com/d5fh7j0c",
+ "cover_url": "https://images.lumacdn.com/uploads/1e/fcaefb4b-b5f2-4ca8-88d2-d494f5aa93de.jpg",
+ "status": "past"
+ },
+ {
+ "uid": "evt-QODSqbmej6hUZxZ@events.lu.ma",
+ "title": "2da Meetup 2026",
+ "start": "2026-04-30T22:30:00.000Z",
+ "end": "2026-05-01T01:30:00.000Z",
+ "location": "Los Jesuitas 881, 7500000 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/bd3njvma",
+ "cover_url": "https://images.lumacdn.com/event-covers/4q/93e8c36d-a069-4856-981c-6e0c6f566165.jpg",
+ "status": "past"
+ },
+ {
+ "uid": "evt-4frzlb8wNTXH4Cf@events.lu.ma",
+ "title": "1er Meetup 2026",
+ "start": "2026-03-11T21:30:00.000Z",
+ "end": "2026-03-12T00:30:00.000Z",
+ "location": null,
+ "url": "https://luma.com/rxjqdqdn",
+ "cover_url": "https://images.lumacdn.com/event-covers/cc/e3a68441-b8f1-4d24-be27-ed392ab23e2e.jpg",
+ "status": "past"
+ },
+ {
+ "uid": "evt-NTvDUhExvxoN4Vi@events.lu.ma",
+ "title": "5to Meetup 2025 JSChile + Seelie",
+ "start": "2025-07-15T23:00:00.000Z",
+ "end": "2025-07-16T02:00:00.000Z",
+ "location": "Seelie Cafeteria, Padre Mariano 205, 7500015 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/v6rhzit1",
+ "cover_url": "https://images.lumacdn.com/event-covers/7m/68297e23-c3b5-470f-8197-649660538c97.jpg",
+ "status": "past"
+ },
+ {
+ "uid": "evt-ZEMRCvM6FpCFSP0@events.lu.ma",
+ "title": "3er Meetup JSChile x Fintual",
+ "start": "2025-03-21T22:00:00.000Z",
+ "end": "2025-03-22T00:30:00.000Z",
+ "location": "Av. Providencia 229, 7500768 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/i01aiz8v",
+ "cover_url": "https://images.lumacdn.com/event-covers/my/66a52de4-84a2-4476-b8f7-9573f7c2f897.png",
+ "status": "past"
+ },
+ {
+ "uid": "evt-pCGUrxcBIigvZKO@events.lu.ma",
+ "title": "2do Meetup 2025 JSChile + Meli",
+ "start": "2025-02-20T21:30:00.000Z",
+ "end": "2025-02-21T00:00:00.000Z",
+ "location": "Av. Apoquindo 4820, 7560969 Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/a346a3gd",
+ "cover_url": "https://images.lumacdn.com/event-covers/j0/2cddaa02-d529-441a-b2a6-5c53075af8a7.png",
+ "status": "past"
+ },
+ {
+ "uid": "evt-Bm94xUNx8bGTKJp@events.lu.ma",
+ "title": "1er Meetup 2025 JSChile + Mach",
+ "start": "2025-01-15T21:00:00.000Z",
+ "end": "2025-01-15T23:00:00.000Z",
+ "location": "Av. Apoquindo 3885, 7550177 Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/enlyl72s",
+ "cover_url": "https://images.lumacdn.com/event-covers/li/6a472ee3-2b97-4513-841a-7dfea05dc407",
+ "status": "past"
+ },
+ {
+ "uid": "evt-KAvBz5ZwA0Y5t4X@events.lu.ma",
+ "title": "Watch Party de LA PREVIA JSCONF CL 2024",
+ "start": "2024-10-12T13:00:00.000Z",
+ "end": "2024-10-12T21:00:00.000Z",
+ "location": "Hub Providencia, Los Jesuitas 881, 7501300 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/6tstu8yu",
+ "cover_url": "https://images.lumacdn.com/event-covers/3g/55d1b297-fd3e-4ca3-be73-f66158285481",
+ "status": "past"
+ },
+ {
+ "uid": "evt-eQXgFVhg6wH9Q53@events.lu.ma",
+ "title": "Hablemos de liderazgo",
+ "start": "2024-05-07T22:30:00.000Z",
+ "end": "2024-05-08T02:00:00.000Z",
+ "location": "Av. Apoquindo 4800, Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/hablemos-de-liderazgo",
+ "cover_url": "https://images.lumacdn.com/event-covers/d0/14e43869-a1a1-479e-a63a-cd697e615eac",
+ "status": "past"
+ },
+ {
+ "uid": "evt-fSQuqP1FHNq5oDA@events.lu.ma",
+ "title": "Meetup Marzo",
+ "start": "2024-03-26T21:30:00.000Z",
+ "end": "2024-03-27T01:00:00.000Z",
+ "location": "El Canelo 2715, Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/marzo-jschile",
+ "cover_url": "https://images.lumacdn.com/event-covers/jz/c4cab306-efe4-423b-9c91-d830f93b52e4",
+ "status": "past"
+ },
+ {
+ "uid": "evt-nQnHAtoSarF7cum@events.lu.ma",
+ "title": "1er Meetup Concepción",
+ "start": "2024-01-19T22:00:00.000Z",
+ "end": "2024-01-20T00:00:00.000Z",
+ "location": "DUOC UC, Paicaví 3280, Concepción, Bío Bío, Chile",
+ "url": "https://luma.com/conce-jschile",
+ "cover_url": "https://images.lumacdn.com/event-covers/79/61cdeffe-dbef-4bd0-966a-c4380b21f109",
+ "status": "past"
+ },
+ {
+ "uid": "evt-PTPvFP4jilTBmnI@events.lu.ma",
+ "title": "TechSchool V2.0.0 2023",
+ "start": "2023-12-16T13:00:00.000Z",
+ "end": "2023-12-16T19:30:00.000Z",
+ "location": "Hub Providencia, Los Jesuitas 881, 7501300 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/techschool-v2",
+ "cover_url": "https://images.lumacdn.com/event-covers/in/4c170257-feda-4648-89de-c5b26e7753a3",
+ "status": "past"
+ },
+ {
+ "uid": "evt-XLBVcuBakwBMN9d@events.lu.ma",
+ "title": "Meetup Noviembre",
+ "start": "2023-11-21T22:00:00.000Z",
+ "end": "2023-11-22T01:00:00.000Z",
+ "location": "Mercado Urbano Tobalaba, Av. Apoquindo 2730, 7550000 Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/meetup-noviembre",
+ "cover_url": "https://images.lumacdn.com/event-covers/ee/203edf20-e27b-4cc0-b362-5ffca0ee8428",
+ "status": "past"
+ },
+ {
+ "uid": "evt-hS7S16zgCMIgtxB@events.lu.ma",
+ "title": "Sesión de Inglés: Lectura de Noticias",
+ "start": "2023-11-10T23:00:00.000Z",
+ "end": "2023-11-11T01:00:00.000Z",
+ "location": null,
+ "url": "https://luma.com/noticias-en-ingles",
+ "cover_url": "https://images.lumacdn.com/event-covers/ra/3007a70d-994d-48e8-ab8c-50cad635e7d7",
+ "status": "past"
+ },
+ {
+ "uid": "evt-qJByauqbyaV4nwn@events.lu.ma",
+ "title": "TechSchool V1.0.0 2023",
+ "start": "2023-10-28T13:00:00.000Z",
+ "end": "2023-10-28T19:30:00.000Z",
+ "location": "Hub Providencia, Los Jesuitas 881, 7501300 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/TechSchoolv1.0.0-2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/8m/f9704bcf-03a7-430a-bf90-9b1ce7f7fa88",
+ "status": "past"
+ },
+ {
+ "uid": "evt-S7pzB8Mo2GKumSb@events.lu.ma",
+ "title": "Oktober Meetup",
+ "start": "2023-10-25T22:00:00.000Z",
+ "end": "2023-10-26T01:30:00.000Z",
+ "location": "Av. Apoquindo 4800, Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/jfjvsheu",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/2d/c2dfe8f1-d70b-41fe-bd8a-942ecb98304e",
+ "status": "past"
+ },
+ {
+ "uid": "evt-trHhNbkVxzgoQW1@events.lu.ma",
+ "title": "TikiTikiTi!!! La Fondita Tech",
+ "start": "2023-09-07T22:30:00.000Z",
+ "end": "2023-09-08T01:30:00.000Z",
+ "location": "Mercado Urbano Tobalaba, Av. Apoquindo 2730, 7550000 Las Condes, Región Metropolitana, Chile",
+ "url": "https://luma.com/fondita-tech",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/2d/4d39ad5e-1de5-49ba-80b3-1820ffc4f3b9",
+ "status": "past"
+ },
+ {
+ "uid": "evt-Fnhdu6W0HXYVvND@events.lu.ma",
+ "title": "NodeSchool Santiago: Agosto 2023",
+ "start": "2023-08-12T14:00:00.000Z",
+ "end": "2023-08-12T22:00:00.000Z",
+ "location": "El Canelo 2715, Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/nodeschool-santiago-agosto-2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/80/c04aa548-1f0b-40ba-951e-7541eb194546",
+ "status": "past"
+ },
+ {
+ "uid": "evt-okboIKx8c42NlJO@events.lu.ma",
+ "title": "Meetup M8 + Javascript Chile",
+ "start": "2023-07-19T23:00:00.000Z",
+ "end": "2023-07-20T02:00:00.000Z",
+ "location": "El Canelo 2715, Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/M8-JavaScriptChile-Julio2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/qb/1388d1f1-1339-430f-a5f4-bef811f8373e",
+ "status": "past"
+ },
+ {
+ "uid": "evt-Nh36XmbP1r3BpNn@events.lu.ma",
+ "title": "Status 418: I'm a Teapot",
+ "start": "2023-07-19T00:30:00.000Z",
+ "end": "2023-07-19T02:30:00.000Z",
+ "location": null,
+ "url": "https://luma.com/status-418-julio-2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/3b/d9b492d5-9a5f-42e8-a5ea-c4572921e7bd",
+ "status": "past"
+ },
+ {
+ "uid": "evt-UZpob2DeZj28pQc@events.lu.ma",
+ "title": "MeetUp Javascript Chile",
+ "start": "2023-05-25T23:00:00.000Z",
+ "end": "2023-05-26T02:00:00.000Z",
+ "location": "Hub Providencia - Infante 1415, José Manuel Infante 1415, 7501300 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/javascript-chile-mayo-2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/r7/04a11821-dc73-499a-a933-ea519db6d4aa",
+ "status": "past"
+ },
+ {
+ "uid": "evt-0iJTh3sbU3PWy49@events.lu.ma",
+ "title": "NodeSchool Santiago: Abril 2023",
+ "start": "2023-04-22T14:00:00.000Z",
+ "end": "2023-04-22T21:30:00.000Z",
+ "location": "Hub Providencia - Infante 1415, José Manuel Infante 1415, 7501300 Providencia, Región Metropolitana, Chile",
+ "url": "https://luma.com/nodeschool-santiago-abril-2023",
+ "cover_url": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/tn/64f1f053-6eda-4d46-8a78-12f36dfedf42",
+ "status": "past"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/public/file.svg b/public/file.svg
deleted file mode 100644
index 004145c..0000000
--- a/public/file.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/globe.svg b/public/globe.svg
deleted file mode 100644
index 567f17b..0000000
--- a/public/globe.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/next.svg b/public/next.svg
deleted file mode 100644
index 5174b28..0000000
--- a/public/next.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..946b376
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,4 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://jschile.org/sitemap.xml
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..bbe8e9a
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,9 @@
+
+
+
+ https://jschile.org/
+ 2026-07-08
+ monthly
+ 1.0
+
+
diff --git a/public/vercel.svg b/public/vercel.svg
deleted file mode 100644
index 7705396..0000000
--- a/public/vercel.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/public/window.svg b/public/window.svg
deleted file mode 100644
index b2b2a44..0000000
--- a/public/window.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/scripts/fetch-events.mjs b/scripts/fetch-events.mjs
new file mode 100644
index 0000000..5a7071d
--- /dev/null
+++ b/scripts/fetch-events.mjs
@@ -0,0 +1,212 @@
+/**
+ * Build script: fetches the Luma iCal feed, scrapes og:image covers,
+ * and writes public/events.json ready for the Next.js static export.
+ *
+ * Run: node scripts/fetch-events.mjs
+ * Env: none required — uses the public iCal feed only.
+ */
+
+import { readFile, writeFile } from 'node:fs/promises';
+import { existsSync } from 'node:fs';
+import { createHash } from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import ical from 'node-ical';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const ROOT = join(__dirname, '..');
+const CACHE_FILE = join(__dirname, 'og-cache.json');
+const OUTPUT_FILE = join(ROOT, 'public', 'events.json');
+const ICAL_URL = 'https://api.lu.ma/ics/get?entity=calendar&id=cal-k2TNsuCIp2uuGla';
+
+// ──────────────────────────────────────────────
+// Cache (persists og:image lookups across runs)
+// ──────────────────────────────────────────────
+
+async function loadCache() {
+ if (existsSync(CACHE_FILE)) {
+ return JSON.parse(await readFile(CACHE_FILE, 'utf-8'));
+ }
+ return {};
+}
+
+async function saveCache(cache) {
+ await writeFile(CACHE_FILE, JSON.stringify(cache, null, 2));
+}
+
+// ──────────────────────────────────────────────
+// og:image scraping
+// ──────────────────────────────────────────────
+
+async function fetchOgImage(url, cache) {
+ if (url in cache) return cache[url];
+
+ try {
+ const res = await fetch(url, {
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; JSChile-EventBot/1.0)' },
+ signal: AbortSignal.timeout(12_000),
+ });
+ const html = await res.text();
+
+ // og:image may appear as `content="..."` or `content='...'`
+ const match = html.match(/property=["']og:image["'][^>]+content=["']([^"']+)["']/);
+ if (!match) {
+ cache[url] = null;
+ return null;
+ }
+
+ const raw = match[1].replace(/&/g, '&');
+
+ // Luma CDN OG image: extract the real event cover from the `img=` param
+ if (raw.includes('og.luma.com')) {
+ try {
+ const parsed = new URL(raw);
+ const imgParam = parsed.searchParams.get('img');
+ if (imgParam) {
+ cache[url] = imgParam;
+ return imgParam;
+ }
+ } catch { /* fall through to raw */ }
+ }
+
+ // Generic default social image → no cover
+ if (raw.includes('/social-images/')) {
+ cache[url] = null;
+ return null;
+ }
+
+ cache[url] = raw;
+ return raw;
+ } catch (err) {
+ console.warn(` ⚠ og:image fetch failed for ${url}: ${err.message}`);
+ cache[url] = null;
+ return null;
+ }
+}
+
+// ──────────────────────────────────────────────
+// Helpers
+// ──────────────────────────────────────────────
+
+function extractUrlFromDescription(description) {
+ if (!description) return null;
+ // description may have literal '\n' or real newlines depending on node-ical version
+ const clean = description.replace(/\\n/g, '\n');
+ const match = clean.match(/https:\/\/luma\.com\/([^\s\n\\]+)/);
+ if (!match) return null;
+ return `https://luma.com/${match[1]}`;
+}
+
+function classifyEvent(start, end, now) {
+ if (end < now) return 'past';
+ if (start <= now && now <= end) return 'ongoing';
+ return 'upcoming';
+}
+
+function isCancelled(summary) {
+ return /\[CANCELADO\]/i.test(summary ?? '');
+}
+
+// ──────────────────────────────────────────────
+// Main
+// ──────────────────────────────────────────────
+
+async function main() {
+ console.log('📅 Fetching Luma iCal feed…');
+ const res = await fetch(ICAL_URL, { signal: AbortSignal.timeout(20_000) });
+ const text = await res.text();
+
+ const parsed = ical.parseICS(text);
+ const cache = await loadCache();
+ const now = new Date();
+
+ const events = [];
+ const vevents = Object.values(parsed).filter(e => e.type === 'VEVENT');
+
+ console.log(` Found ${vevents.length} VEVENTs. Scraping og:image covers…`);
+
+ for (const ev of vevents) {
+ const summary = (ev.summary ?? '').trim();
+
+ if (isCancelled(summary)) continue;
+
+ const start = ev.start instanceof Date ? ev.start : new Date(ev.start);
+ const end = ev.end instanceof Date ? ev.end : new Date(ev.end);
+ const status = classifyEvent(start, end, now);
+
+ const url = extractUrlFromDescription(ev.description);
+
+ // Skip events with no real URL (shouldn't happen in practice)
+ const eventUrl = url ?? `https://lu.ma/jschile`;
+
+ let cover_url = null;
+ if (url) {
+ process.stdout.write(` → ${summary.slice(0, 50)}`);
+ cover_url = await fetchOgImage(url, cache);
+ console.log(cover_url ? ' ✓' : ' (no cover)');
+ }
+
+ // Don't treat location as address when Luma fills it with their own URL
+ const location =
+ ev.location && !ev.location.startsWith('https://luma.com')
+ ? ev.location
+ : null;
+
+ events.push({
+ uid: ev.uid ?? `${summary}-${start.toISOString()}`,
+ title: summary,
+ start: start.toISOString(),
+ end: end.toISOString(),
+ location,
+ url: eventUrl,
+ cover_url,
+ status,
+ });
+ }
+
+ await saveCache(cache);
+
+ const upcoming = events
+ .filter(e => e.status === 'upcoming' || e.status === 'ongoing')
+ .sort((a, b) => new Date(a.start) - new Date(b.start));
+
+ const past = events
+ .filter(e => e.status === 'past')
+ .sort((a, b) => new Date(b.start) - new Date(a.start));
+
+ // Only write if events actually changed (ignore generated_at drift)
+ const newHash = createHash('sha256')
+ .update(JSON.stringify({ upcoming, past }))
+ .digest('hex');
+
+ let existingHash = '';
+ if (existsSync(OUTPUT_FILE)) {
+ try {
+ const existing = JSON.parse(await readFile(OUTPUT_FILE, 'utf-8'));
+ existingHash = createHash('sha256')
+ .update(JSON.stringify({ upcoming: existing.upcoming, past: existing.past }))
+ .digest('hex');
+ } catch { /* corrupt file — will overwrite */ }
+ }
+
+ if (newHash === existingHash) {
+ console.log(`\n✅ No changes detected — events.json unchanged.`);
+ console.log(` ${upcoming.length} próximos / ${past.length} pasados`);
+ return;
+ }
+
+ await writeFile(OUTPUT_FILE, JSON.stringify({
+ generated_at: now.toISOString(),
+ upcoming,
+ past,
+ }, null, 2));
+
+ console.log(`\n✅ events.json updated:`);
+ console.log(` ${upcoming.length} próximos / ${past.length} pasados`);
+ console.log(` → ${OUTPUT_FILE}`);
+}
+
+main().catch(err => {
+ console.error('❌ fetch-events failed:', err);
+ process.exit(1);
+});
diff --git a/scripts/og-cache.json b/scripts/og-cache.json
new file mode 100644
index 0000000..717b943
--- /dev/null
+++ b/scripts/og-cache.json
@@ -0,0 +1,25 @@
+{
+ "https://luma.com/nodeschool-santiago-abril-2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/tn/64f1f053-6eda-4d46-8a78-12f36dfedf42",
+ "https://luma.com/javascript-chile-mayo-2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/r7/04a11821-dc73-499a-a933-ea519db6d4aa",
+ "https://luma.com/status-418-julio-2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/3b/d9b492d5-9a5f-42e8-a5ea-c4572921e7bd",
+ "https://luma.com/M8-JavaScriptChile-Julio2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/qb/1388d1f1-1339-430f-a5f4-bef811f8373e",
+ "https://luma.com/nodeschool-santiago-agosto-2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/80/c04aa548-1f0b-40ba-951e-7541eb194546",
+ "https://luma.com/fondita-tech": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/2d/4d39ad5e-1de5-49ba-80b3-1820ffc4f3b9",
+ "https://luma.com/jfjvsheu": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/2d/c2dfe8f1-d70b-41fe-bd8a-942ecb98304e",
+ "https://luma.com/TechSchoolv1.0.0-2023": "https://images.lumacdn.com/cdn-cgi/image/format=auto,fit=cover,dpr=1,anim=false,background=white,quality=75,width=1000,height=500/event-covers/8m/f9704bcf-03a7-430a-bf90-9b1ce7f7fa88",
+ "https://luma.com/noticias-en-ingles": "https://images.lumacdn.com/event-covers/ra/3007a70d-994d-48e8-ab8c-50cad635e7d7",
+ "https://luma.com/meetup-noviembre": "https://images.lumacdn.com/event-covers/ee/203edf20-e27b-4cc0-b362-5ffca0ee8428",
+ "https://luma.com/techschool-v2": "https://images.lumacdn.com/event-covers/in/4c170257-feda-4648-89de-c5b26e7753a3",
+ "https://luma.com/conce-jschile": "https://images.lumacdn.com/event-covers/79/61cdeffe-dbef-4bd0-966a-c4380b21f109",
+ "https://luma.com/marzo-jschile": "https://images.lumacdn.com/event-covers/jz/c4cab306-efe4-423b-9c91-d830f93b52e4",
+ "https://luma.com/hablemos-de-liderazgo": "https://images.lumacdn.com/event-covers/d0/14e43869-a1a1-479e-a63a-cd697e615eac",
+ "https://luma.com/6tstu8yu": "https://images.lumacdn.com/event-covers/3g/55d1b297-fd3e-4ca3-be73-f66158285481",
+ "https://luma.com/enlyl72s": "https://images.lumacdn.com/event-covers/li/6a472ee3-2b97-4513-841a-7dfea05dc407",
+ "https://luma.com/a346a3gd": "https://images.lumacdn.com/event-covers/j0/2cddaa02-d529-441a-b2a6-5c53075af8a7.png",
+ "https://luma.com/i01aiz8v": "https://images.lumacdn.com/event-covers/my/66a52de4-84a2-4476-b8f7-9573f7c2f897.png",
+ "https://luma.com/v6rhzit1": "https://images.lumacdn.com/event-covers/7m/68297e23-c3b5-470f-8197-649660538c97.jpg",
+ "https://luma.com/rxjqdqdn": "https://images.lumacdn.com/event-covers/cc/e3a68441-b8f1-4d24-be27-ed392ab23e2e.jpg",
+ "https://luma.com/bd3njvma": "https://images.lumacdn.com/event-covers/4q/93e8c36d-a069-4856-981c-6e0c6f566165.jpg",
+ "https://luma.com/d5fh7j0c": "https://images.lumacdn.com/uploads/1e/fcaefb4b-b5f2-4ca8-88d2-d494f5aa93de.jpg",
+ "https://luma.com/kf94jzi8": "https://images.lumacdn.com/uploads/g5/54714ab0-b3d0-4e6b-801d-76913a16b575.png"
+}
\ No newline at end of file
diff --git a/src/app/android-chrome-192x192.png b/src/app/android-chrome-192x192.png
new file mode 100644
index 0000000..1086a8a
Binary files /dev/null and b/src/app/android-chrome-192x192.png differ
diff --git a/src/app/android-chrome-512x512.png b/src/app/android-chrome-512x512.png
new file mode 100644
index 0000000..0fd0ddb
Binary files /dev/null and b/src/app/android-chrome-512x512.png differ
diff --git a/src/app/apple-touch-icon.png b/src/app/apple-touch-icon.png
new file mode 100644
index 0000000..9d72953
Binary files /dev/null and b/src/app/apple-touch-icon.png differ
diff --git a/src/app/favicon-16x16.png b/src/app/favicon-16x16.png
new file mode 100644
index 0000000..7580858
Binary files /dev/null and b/src/app/favicon-16x16.png differ
diff --git a/src/app/favicon-32x32.png b/src/app/favicon-32x32.png
new file mode 100644
index 0000000..a37d453
Binary files /dev/null and b/src/app/favicon-32x32.png differ
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
new file mode 100644
index 0000000..2c1eabe
Binary files /dev/null and b/src/app/favicon.ico differ
diff --git a/src/app/icon.png b/src/app/icon.png
deleted file mode 100644
index a6a5f2f..0000000
Binary files a/src/app/icon.png and /dev/null differ
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index e07ef0e..2b2f5d0 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -5,30 +5,73 @@ import MuiRootProvider from '@/providers/MuiRootProvider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
- title: 'JavaScript Chile | Comunidad de Desarrolladores',
+ title: 'JSChile | Comunidad de Desarrolladores JavaScript en Chile',
description:
- 'Somos una comunidad de desarrolladores apasionados por la tecnología. Organizamos meetups, workshops e iniciativas para compartir conocimiento, conectar profesionales y construir el futuro del desarrollo en Chile.',
- metadataBase: 'https://jschile.org/',
+ 'JSChile: comunidad de devs en Chile desde 2013. Meetups, workshops y conferencias para conectar, aprender y crecer. +120 eventos, +4.800 asistentes, +200 speakers. ¡Únete gratis!',
+ applicationName: 'JSChile',
+ keywords: [
+ 'javascript chile',
+ 'comunidad desarrolladores chile',
+ 'meetup javascript santiago',
+ 'jsconf chile',
+ 'programación chile',
+ 'frontend chile',
+ 'nodejs chile',
+ 'react chile',
+ 'developers community',
+ ],
+ metadataBase: new URL('https://jschile.org'),
+ alternates: {
+ canonical: 'https://jschile.org/',
+ },
icons: {
- icon: '/icon.png',
+ icon: '/favicon.ico',
},
openGraph: {
- title: 'JavaScript Chile | Comunidad de Desarrolladores',
+ title: 'JSChile | Comunidad de Desarrolladores JavaScript en Chile',
description:
- 'Somos una comunidad de desarrolladores apasionados por la tecnología. Organizamos meetups, workshops e iniciativas para compartir conocimiento, conectar profesionales y construir el futuro del desarrollo en Chile.',
+ 'La comunidad de devs JavaScript más grande de Chile. Desde 2013: +120 eventos, +4.800 asistentes, +200 speakers. ¡Únete gratis!',
url: 'https://jschile.org/',
- siteName: 'JavaScript Chile',
+ siteName: 'JSChile',
locale: 'es_CL',
type: 'website',
images: [
{
- url: '/icon.png',
+ url: '/assets/OG.png',
width: 1200,
height: 630,
- alt: 'JavaScript Chile',
+ alt: 'JSChile — Comunidad de desarrolladores JavaScript desde 2013',
},
],
},
+ twitter: {
+ card: 'summary_large_image',
+ title: 'JSChile | Comunidad de Desarrolladores JavaScript en Chile',
+ description:
+ 'La comunidad de devs JavaScript más grande de Chile. Desde 2013: +120 eventos, +4.800 asistentes, +200 speakers. ¡Únete gratis!',
+ images: ['/assets/OG.png'],
+ },
+};
+
+const jsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'Organization',
+ name: 'JSChile',
+ url: 'https://jschile.org',
+ logo: 'https://jschile.org/assets/logo-cljs.svg',
+ foundingDate: '2013',
+ description:
+ 'Comunidad de desarrolladores JavaScript en Chile. Organizamos meetups, workshops y conferencias desde 2013.',
+ sameAs: [
+ 'https://www.instagram.com/javascriptchile/',
+ 'https://www.linkedin.com/company/jschile/',
+ 'https://github.com/jschile',
+ ],
+ location: {
+ '@type': 'Place',
+ name: 'Chile',
+ address: { '@type': 'PostalAddress', addressCountry: 'CL' },
+ },
};
export default function RootLayout({
@@ -38,7 +81,23 @@ export default function RootLayout({
}>) {
return (
+
+
+
+
+
+ {/* WCAG AA: skip link — visible solo con foco de teclado via CSS */}
+
+
+ Saltar al contenido principal
+
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 04f80fd..7a09f9d 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,13 +1,50 @@
+import { COLORS } from '@/lib/tokens';
import StaffContainer from '@/sections/staff/StaffContainer';
+import { JoinCTA } from '@/sections/JoinCTA';
import { HeroContainer } from '@/sections/hero/HeroContainer';
-import { CardsContainer } from '@/sections/cards/CardsContainer';
+import { StatsSection } from '@/sections/StatsSection';
+import { WhatsAppCTA } from '@/sections/WhatsAppCTA';
+import { NextEvent } from '@/sections/NextEvent';
+import { PastEventsCarousel } from '@/sections/PastEventsCarousel';
+import { HistoriaTimeline } from '@/sections/HistoriaTimeline';
+import { NuestrosValores } from '@/sections/NuestrosValores';
+import { BrandBanner } from '@/sections/BrandBanner';
+import { SpeakersSectionWrapper } from '@/sections/SpeakersSectionWrapper';
+import Box from '@mui/material/Box';
export default function Home() {
return (
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {/* Sections below scroll over the sticky BrandBanner */}
+
);
}
diff --git a/src/components/AnimatedButton.tsx b/src/components/AnimatedButton.tsx
index f8a6c66..0b87476 100644
--- a/src/components/AnimatedButton.tsx
+++ b/src/components/AnimatedButton.tsx
@@ -1,4 +1,5 @@
'use client';
+import { COLORS } from '@/lib/tokens';
import * as React from 'react';
import Button, { ButtonProps } from '@mui/material/Button';
@@ -6,72 +7,80 @@ import { motion } from 'framer-motion';
type AnimatedButtonProps = ButtonProps & {
hoverColor?: string;
+ hoverTextColor?: string;
target?: string;
rel?: string;
};
export const AnimatedButton = ({
children,
- hoverColor = '#F0DB4F',
+ hoverColor = COLORS.yellow,
+ hoverTextColor,
sx,
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
disabled,
+ fullWidth,
...props
}: AnimatedButtonProps) => {
const [isActive, setIsActive] = React.useState(false);
return (
-
+ onBlur={(event) => {
+ setIsActive(false);
+ onBlur?.(event);
+ }}
+ sx={[
+ {
+ position: 'relative',
+ overflow: 'hidden',
+ isolation: 'isolate',
+ transition: 'color 0.25s ease',
+ ...(hoverTextColor && {
+ '&:hover': { color: hoverTextColor },
+ }),
+ },
+ ...(Array.isArray(sx) ? sx : [sx]),
+ ]}
+ >
+
+ {children}
+
+
);
};
diff --git a/src/components/CommunityJoinModal.tsx b/src/components/CommunityJoinModal.tsx
index 11eb39d..84315eb 100644
--- a/src/components/CommunityJoinModal.tsx
+++ b/src/components/CommunityJoinModal.tsx
@@ -1,15 +1,16 @@
'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
import React, { useState } from 'react';
import Modal from '@mui/material/Modal';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
-import CloseIcon from '@mui/icons-material/Close';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import { AnimatedButton } from './AnimatedButton';
-import WhatsAppIcon from '@mui/icons-material/WhatsApp';
+import { WhatsappLogo, X } from '@phosphor-icons/react';
import { Stack } from '@mui/material';
interface CommunityJoinModalProps {
@@ -17,27 +18,7 @@ interface CommunityJoinModalProps {
onClose: () => void;
}
-const modalStyle = {
- position: 'absolute' as const,
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: { xs: '95%', sm: 500, md: 650 },
- maxHeight: '90vh',
- overflowY: 'auto',
- bgcolor: 'background.paper',
- borderRadius: 4,
- boxShadow: 24,
- p: { xs: 4, sm: 6 },
- display: 'flex',
- flexDirection: 'column',
- outline: 'none',
-};
-
-export const CommunityJoinModal: React.FC = ({
- open,
- onClose,
-}) => {
+export const CommunityJoinModal: React.FC = ({ open, onClose }) => {
const [accepted, setAccepted] = useState(false);
return (
@@ -48,97 +29,104 @@ export const CommunityJoinModal: React.FC = ({
aria-describedby='community-join-modal-description'
disableScrollLock
>
-
+
-
+
-
-
-
- Únete a nuestra Comunidad
+
+
+
- Bienvenido al grupo de WhatsApp de JavaScript Chile
+ Bienvenido al grupo de WhatsApp de JSChile
+ {/* Resumen código de conducta */}
-
- Reporta Problemas
-
- Si eres víctima o testigo de acoso, contacta inmediatamente a los
- moderadores.
-
+
+ {[
+ { title: 'Comunidad Inclusiva', body: 'Todos son bienvenidos sin importar género, raza, orientación sexual, capacidades o creencias.' },
+ { title: 'Cero Tolerancia al Acoso', body: 'No toleramos abuso físico/verbal, intimidación, acecho, ni contenido inapropiado.' },
+ { title: 'Respeto y Profesionalismo', body: 'Mantén un tono respetuoso. Cualquier violación resultará en expulsión del grupo y futuros eventos.' },
+ { title: 'Reporta Problemas', body: 'Si eres víctima o testigo de acoso, contacta inmediatamente a los moderadores.' },
+ ].map(({ title, body }) => (
+
+ {title}
+
{body}
+
+ ))}
-
- Somos una comunidad inclusiva que valora la diversidad. Si no
- estás de acuerdo con estos valores, probablemente no sea el lugar
- para ti
+
+ Somos una comunidad inclusiva que valora la diversidad. Si no estás de acuerdo con estos valores, probablemente no sea el lugar para ti.
@@ -148,12 +136,15 @@ export const CommunityJoinModal: React.FC = ({
setAccepted(e.target.checked)}
- color='primary'
+ sx={{
+ color: 'rgba(255,255,255,0.4)',
+ '&.Mui-checked': { color: COLORS.yellow },
+ }}
/>
}
label={
-
- Acepto el Código de Conducta de JavaScript Chile
+
+ Acepto el Código de Conducta de JSChile
}
sx={{ mb: 3, ml: 0 }}
@@ -161,22 +152,24 @@ export const CommunityJoinModal: React.FC = ({
-
-
- Ir al Grupo de WhatsApp
+
+
+ Ir al Grupo de WhatsApp
diff --git a/src/components/ScrollReveal.tsx b/src/components/ScrollReveal.tsx
new file mode 100644
index 0000000..54f8eae
--- /dev/null
+++ b/src/components/ScrollReveal.tsx
@@ -0,0 +1,138 @@
+'use client';
+
+import { motion, Variants } from 'framer-motion';
+import { ReactNode } from 'react';
+import { EASE } from '@/lib/tokens';
+
+const EASE_OUT_EXPO = EASE;
+
+/* ── Fade + slide hacia arriba ── */
+export const FadeUp = ({
+ children,
+ delay = 0,
+ duration = 0.7,
+}: {
+ children: ReactNode;
+ delay?: number;
+ duration?: number;
+}) => (
+
+ {children}
+
+);
+
+/* ── Slide desde la izquierda ── */
+export const SlideLeft = ({
+ children,
+ delay = 0,
+}: {
+ children: ReactNode;
+ delay?: number;
+}) => (
+
+ {children}
+
+);
+
+/* ── Slide desde la derecha ── */
+export const SlideRight = ({
+ children,
+ delay = 0,
+}: {
+ children: ReactNode;
+ delay?: number;
+}) => (
+
+ {children}
+
+);
+
+/* ── Pop con spring (escala desde 0) ── */
+export const PopIn = ({
+ children,
+ delay = 0,
+}: {
+ children: ReactNode;
+ delay?: number;
+}) => (
+
+ {children}
+
+);
+
+/* ── Cae desde arriba con spring + rotación inicial ── */
+export const DropIn = ({
+ children,
+ delay = 0,
+ fromRotate = 0,
+}: {
+ children: ReactNode;
+ delay?: number;
+ fromRotate?: number;
+}) => (
+
+ {children}
+
+);
+
+/* ── Contenedor con stagger para hijos ── */
+const staggerContainer: Variants = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.12 } },
+};
+const staggerChild: Variants = {
+ hidden: { opacity: 0, y: 36 },
+ show: { opacity: 1, y: 0, transition: { duration: 0.6, ease: EASE_OUT_EXPO } },
+};
+
+export const StaggerContainer = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
+
+export const StaggerItem = ({ children }: { children: ReactNode }) => (
+ {children}
+);
+
+/* ── Ícono que oscila (wobble) al entrar ── */
+export const WobbleOnEnter = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+);
diff --git a/src/components/WhatsappFab.tsx b/src/components/WhatsappFab.tsx
index e65428b..de75f2b 100644
--- a/src/components/WhatsappFab.tsx
+++ b/src/components/WhatsappFab.tsx
@@ -1,6 +1,7 @@
+import { COLORS } from '@/lib/tokens';
import { CommunityJoinModal } from './CommunityJoinModal';
import Fab from '@mui/material/Fab';
-import WhatsAppIcon from '@mui/icons-material/WhatsApp';
+import { WhatsappLogo } from '@phosphor-icons/react';
import { useState } from 'react';
export const WhatsappFab = () => {
@@ -20,16 +21,16 @@ export const WhatsappFab = () => {
right: { xs: 16, md: 32 },
zIndex: 2000,
- bgcolor: '#F0DB4F',
+ bgcolor: COLORS.yellow,
color: 'black',
'&:hover': {
- bgcolor: '#FFE970',
+ bgcolor: COLORS.yellowHover,
transform: 'scale(1.1)',
},
transition: 'all 0.3s ease-in-out',
}}
>
-
+
>
diff --git a/src/data/data_staff.ts b/src/data/data_staff.ts
index 6726031..cd8fe6b 100644
--- a/src/data/data_staff.ts
+++ b/src/data/data_staff.ts
@@ -69,7 +69,7 @@ export const DATA_STAFF: StaffMember[] = [
linkedin: 'https://www.linkedin.com/in/ceciliageraldo/',
date: '2023 - 2025',
role: 'Staff',
- imageUrl: '/assets/staff/staff-cecilia-geraldo.jpeg',
+ imageUrl: '/assets/staff/staff-cecilia-geraldo.webp',
isExStaff: true,
},
// Actuales Staff
@@ -77,11 +77,11 @@ export const DATA_STAFF: StaffMember[] = [
id: '7',
fullName: 'Ricardo Castillo',
linkedin: '#',
- date: '2023 - Presente',
+ date: '2023 al 2025',
role: 'Staff',
imageUrl:
'https://images.ctfassets.net/1kfhsqlc8ewi/19Z63BSGRvkB44joLGM0Ph/a6ab11ef6f5f8552b97a90db01ac389e/bio.JPG?fm=webp',
- isExStaff: false,
+ isExStaff: true,
},
{
id: '8',
@@ -104,7 +104,7 @@ export const DATA_STAFF: StaffMember[] = [
email: 'armandor@jschile.org',
date: '2026 - Presente',
role: 'Staff',
- imageUrl: '/assets/staff/staff-armando-rivas-dev.jpg',
+ imageUrl: '/assets/staff/staff-armando-rivas-dev.webp',
isExStaff: false,
},
{
@@ -114,7 +114,7 @@ export const DATA_STAFF: StaffMember[] = [
email: 'samuel@jschile.org',
date: '2026 - Presente',
role: 'Staff',
- imageUrl: '/assets/staff/staff-samuel-angulo.jpeg',
+ imageUrl: '/assets/staff/staff-samuel-angulo.webp',
isExStaff: false,
},
{
@@ -123,25 +123,29 @@ export const DATA_STAFF: StaffMember[] = [
linkedin: 'https://cl.linkedin.com/in/konstanzabosch',
date: '2026 - Presente',
role: 'Staff',
- imageUrl: '/assets/staff/staff-mapachito.jpeg',
+ imageUrl: '/assets/staff/staff-mapachito.webp',
isExStaff: false,
},
{
id: '12',
fullName: 'Carlos Romero',
- linkedin: 'https://www.instagram.com/cxrlosarmando/',
+ linkedin: 'https://www.linkedin.com/in/carlos-romero-025364238/',
+ instagram: 'https://www.instagram.com/cxrlosarmando/',
date: '2026 - Presente',
role: 'Staff',
- imageUrl: '/assets/staff/staff-carlos-romero.jpeg',
+ imageUrl: '/assets/staff/staff-carlos-romero.webp',
isExStaff: false,
},
{
id: '13',
- fullName: 'Javier Vargas',
- linkedin: 'https://www.linkedin.com/in/javierdvt/',
+ fullName: 'Erika Pinedo',
+ linkedin: 'https://www.linkedin.com/in/erika-pinedo-camacho/?locale=es-ES',
+ instagram: 'https://www.instagram.com/otroanguloweb/',
+ website: 'https://otroanguloweb.cl/',
+ email: 'pinedoerika.dg@gmail.com',
date: '2026 - Presente',
role: 'Staff',
- imageUrl: '/assets/staff/staff-javier-vargas.png',
+ imageUrl: '/assets/staff/staff-erika-pinedo.webp',
isExStaff: false,
},
];
diff --git a/src/layout/Footer.tsx b/src/layout/Footer.tsx
index 8e2424a..faa5ceb 100644
--- a/src/layout/Footer.tsx
+++ b/src/layout/Footer.tsx
@@ -1,122 +1,206 @@
'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
-import * as React from 'react';
import Box from '@mui/material/Box';
+import { useCommunityModal } from '@/providers/CommunityModalProvider';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
-import LinkedInIcon from '@mui/icons-material/LinkedIn';
-import TwitterIcon from '@mui/icons-material/Twitter';
-import GitHubIcon from '@mui/icons-material/GitHub';
-import MailIcon from '@mui/icons-material/Mail';
-import Link from '@mui/material/Link';
+import Container from '@mui/material/Container';
+import Grid from '@mui/material/Grid';
+import { GithubLogo, InstagramLogo, WhatsappLogo, LinkedinLogo, XLogo } from '@phosphor-icons/react';
+import Link from 'next/link';
+
+
+const NAV_LINKS = [
+ { label: 'Eventos', href: '#eventos' },
+ { label: 'Nuestra historia', href: '#historia' },
+ { label: 'Nuestros valores', href: '#valores' },
+ { label: 'Speakers', href: '#speakers' },
+ { label: 'Staff', href: '#staff' },
+];
+
+const ACCIONES_LINKS = [
+ { label: 'Enviar mi propuesta', href: 'mailto:contacto@jschile.org' },
+];
+
+const SOCIAL = [
+ { Icon: GithubLogo, href: 'https://github.com/JSConfCL/org-landing', label: 'GitHub' },
+ { Icon: InstagramLogo, href: 'https://www.instagram.com/javascriptchile/', label: 'Instagram' },
+ { Icon: WhatsappLogo, href: null, label: 'WhatsApp' },
+ { Icon: LinkedinLogo, href: 'https://www.linkedin.com/company/javascriptchile/posts/?feedView=all', label: 'LinkedIn' },
+ { Icon: XLogo, href: 'https://x.com/javascriptchile', label: 'X' },
+];
+
+const linkSx = {
+ fontFamily: 'Inter',
+ fontWeight: 600,
+ fontSize: '0.95rem',
+ color: COLORS.textPrimary,
+ textDecoration: 'none',
+ display: 'block',
+ mb: 1.5,
+ '&:hover': { textDecoration: 'underline' },
+};
+
+const colTitle = (text: string) => (
+
+ {text}
+
+);
+
+const buttonLinkSx = {
+ ...linkSx,
+ background: 'none',
+ border: 'none',
+ padding: 0,
+ cursor: 'pointer',
+ textAlign: 'left' as const,
+};
const Footer = () => {
+ const { openModal } = useCommunityModal();
+
return (
-
-
- JavaScript
-
-
- Chile
-
-
+ {/* ── Contenido principal ── */}
+
+
+
+ {/* Logo */}
+
+ window.scrollTo({ top: 0, behavior: 'smooth' })}>
+
+
+
+
+ {/* Navegación */}
+
+ {colTitle('Navegación')}
+ {/* WCAG AA: nav landmark para los enlaces del footer */}
+
+ {NAV_LINKS.map((l) => (
+
+ {l.label}
+
+ ))}
+
+
+
+ {/* Acciones */}
+
+ {colTitle('Acciones')}
+
+ Unirme al Whatsapp
+
+ {ACCIONES_LINKS.map((l) => (
+
+ {l.label}
+
+ ))}
+
+
+ {/* Contáctanos */}
+
+ {colTitle('Contáctanos')}
+
+ contacto@jschile.org
+
+
+
+ {/* Síguenos */}
+
+ {colTitle('Síguenos')}
+ {/* WCAG AA: aria-label descriptivo incluye "(abre en nueva pestaña)" */}
+
+ {SOCIAL.map(({ Icon, href, label }) => (
+
+
+
+ ))}
+
+
-
+
+
+ {/* ── Barra inferior ── */}
+
- Comunidad de JavaScript • Desde 2013
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Comunidad JSChile • Desde 2013
+
+
+ © 2026 JSChile — Hecho con 🖤 por{' '}
+
+ Erika Pinedo
+
+
-
-
- © 2026 JavaScript Chile • Desarrollado por{' '}
-
- RivasCode
-
-
);
};
diff --git a/src/layout/Navbar.tsx b/src/layout/Navbar.tsx
index f2a48fc..3af16fe 100644
--- a/src/layout/Navbar.tsx
+++ b/src/layout/Navbar.tsx
@@ -1,6 +1,6 @@
'use client';
+import { COLORS } from '@/lib/tokens';
-import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
@@ -9,88 +9,135 @@ import Container from '@mui/material/Container';
import Link from 'next/link';
import { AnimatedButton } from '@/components/AnimatedButton';
-import { CommunityJoinModal } from '@/components/CommunityJoinModal';
+import { useCommunityModal } from '@/providers/CommunityModalProvider';
import { Stack } from '@mui/material';
-import WhatsAppIcon from '@mui/icons-material/WhatsApp';
+import { WhatsappLogo } from '@phosphor-icons/react';
+import { useState, useEffect } from 'react';
const Navbar = () => {
- const [isCommunityModalOpen, setIsCommunityModalOpen] = React.useState(false);
+ const { openModal } = useCommunityModal();
+ const [pastHero, setPastHero] = useState(false);
+
+ useEffect(() => {
+ const onScroll = () => setPastHero(window.scrollY >= window.innerHeight * 0.8);
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => window.removeEventListener('scroll', onScroll);
+ }, []);
return (
<>
+ {/* WCAG AA: landmark de navegación principal */}
-
-
+
+
window.scrollTo({ top: 0, behavior: 'smooth' })}
style={{
display: 'flex',
alignItems: 'center',
textDecoration: 'none',
}}
>
-
-
- JavaScript
-
-
+
+
+ {/* Nav links — hidden on mobile, visible after hero */}
+
+ {[
+ { label: 'Eventos', href: '#eventos' },
+ { label: 'Nuestra historia', href: '#historia' },
+ { label: 'Nuestros valores', href: '#valores' },
+ { label: 'Speakers', href: '#speakers' },
+ { label: 'Staff', href: '#staff' },
+ ].map(({ label, href }) => (
+
+
- Chile
-
+ {label}
+
-
+ ))}
+
- {/* Action Buttons & Mobile Menu Toggle */}
+ {/* Action button */}
setIsCommunityModalOpen(true)}
+ onClick={openModal}
variant='contained'
color='primary'
- hoverColor='#FFE970'
+ hoverColor={COLORS.yellowHover}
disableElevation
sx={{
display: 'flex',
- fontWeight: 950,
- px: { xs: 1, sm: 4 },
- py: { xs: 1, sm: 1.5 },
+ fontWeight: 700,
+ px: { xs: 2, sm: 2.5 },
+ py: { xs: 0.9, sm: 0.8 },
borderRadius: 100,
- fontSize: { xs: '0.7rem', sm: '1rem' },
- bgcolor: '#F0DB4F',
- color: 'black',
+ fontSize: { xs: '0.85rem', sm: '0.85rem' },
+ bgcolor: COLORS.yellow,
+ color: COLORS.black,
}}
>
-
- Unirse a la Comunidad
+
+ Unirme al WhatsApp
@@ -98,10 +145,6 @@ const Navbar = () => {
- setIsCommunityModalOpen(false)}
- />
>
);
};
diff --git a/src/lib/fetchSpeakers.ts b/src/lib/fetchSpeakers.ts
new file mode 100644
index 0000000..689e83b
--- /dev/null
+++ b/src/lib/fetchSpeakers.ts
@@ -0,0 +1,68 @@
+export interface Speaker {
+ id: string;
+ name: string;
+ avatar?: string;
+ showInSection: boolean;
+ linkedin?: string;
+ instagram?: string;
+ github?: string;
+ website?: string;
+}
+
+const SHEET_URL =
+ 'https://docs.google.com/spreadsheets/d/1cqTsGP4EvlwEo4ByIRwnu_PAmRx6uZHYL_skx02Osps/export?format=csv&gid=0';
+
+function parseCSVRow(line: string): string[] {
+ const result: string[] = [];
+ let current = '';
+ let inQuotes = false;
+
+ for (const ch of line) {
+ if (ch === '"') {
+ inQuotes = !inQuotes;
+ } else if (ch === ',' && !inQuotes) {
+ result.push(current);
+ current = '';
+ } else {
+ current += ch;
+ }
+ }
+ result.push(current);
+ return result;
+}
+
+function clean(s: string): string {
+ // strip zero-width spaces that Google Sheets sometimes injects
+ return s.replace(/[]/g, '').trim();
+}
+
+export async function fetchSpeakers(): Promise {
+ try {
+ const res = await fetch(SHEET_URL, { next: { revalidate: 300 } });
+ if (!res.ok) return [];
+
+ const text = await res.text();
+ const rows = text.trim().split('\n').slice(1); // skip header row
+
+ const speakers: Speaker[] = [];
+ rows.forEach((line, i) => {
+ const cols = parseCSVRow(line);
+ const name = clean(cols[1] ?? '');
+ if (!name) return;
+
+ speakers.push({
+ id: String(i + 1),
+ name,
+ avatar: clean(cols[0] ?? '') || undefined,
+ showInSection: clean(cols[2] ?? '').toUpperCase() === 'TRUE',
+ linkedin: clean(cols[3] ?? '') || undefined,
+ instagram: clean(cols[4] ?? '') || undefined,
+ github: clean(cols[5] ?? '') || undefined,
+ website: clean(cols[6] ?? '') || undefined,
+ });
+ });
+ return speakers;
+ } catch {
+ return [];
+ }
+}
diff --git a/src/lib/fonts.ts b/src/lib/fonts.ts
new file mode 100644
index 0000000..34efd65
--- /dev/null
+++ b/src/lib/fonts.ts
@@ -0,0 +1,3 @@
+import { Jersey_10 } from 'next/font/google';
+
+export const jersey10 = Jersey_10({ weight: '400', subsets: ['latin'], display: 'swap' });
diff --git a/src/lib/tokens.ts b/src/lib/tokens.ts
new file mode 100644
index 0000000..c361230
--- /dev/null
+++ b/src/lib/tokens.ts
@@ -0,0 +1,20 @@
+export const COLORS = {
+ yellow: '#EBCE17',
+ yellowHover: '#FAE672',
+ yellowMid: '#F5C40F',
+ yellowDark: '#C8A108',
+ black: '#1D1D1D',
+ charcoal: '#2B2B2B',
+ gray: '#636363',
+ graySubtle: 'rgba(99,99,99,0.08)',
+ olive: '#70650C',
+ oliveDark: '#444028',
+ white: '#FFFFFF',
+ // aliases para compatibilidad con código existente
+ darkBg: '#1D1D1D',
+ darkOlive: '#2B2B2B',
+ darkOliveCard:'#444028',
+ textPrimary: '#1D1D1D',
+} as const;
+
+export const EASE = [0.16, 1, 0.3, 1] as const;
diff --git a/src/providers/CommunityModalProvider.tsx b/src/providers/CommunityModalProvider.tsx
new file mode 100644
index 0000000..165b89f
--- /dev/null
+++ b/src/providers/CommunityModalProvider.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import React, { createContext, useContext, useState } from 'react';
+import { CommunityJoinModal } from '@/components/CommunityJoinModal';
+
+const CommunityModalContext = createContext<{ openModal: () => void }>({
+ openModal: () => {},
+});
+
+export const useCommunityModal = () => useContext(CommunityModalContext);
+
+export const CommunityModalProvider = ({ children }: { children: React.ReactNode }) => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ setIsOpen(true) }}>
+ {children}
+ setIsOpen(false)} />
+
+ );
+};
diff --git a/src/providers/MuiRootProvider.tsx b/src/providers/MuiRootProvider.tsx
index 984af75..e3dfd1d 100644
--- a/src/providers/MuiRootProvider.tsx
+++ b/src/providers/MuiRootProvider.tsx
@@ -1,15 +1,16 @@
'use client';
+import { COLORS } from '@/lib/tokens';
import { AppRouterCacheProvider } from '@mui/material-nextjs/v16-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from '@/theme/theme';
-import Navbar from '@/layout/Navbar';
import Footer from '@/layout/Footer';
import Box from '@mui/material/Box';
-import { StarsBackground } from '@/components/StarsBackground';
import { WhatsappFab } from '@/components/WhatsappFab';
+import { CommunityModalProvider } from '@/providers/CommunityModalProvider';
+import Navbar from '@/layout/Navbar';
export default function MuiRootProvider({
children,
@@ -20,28 +21,27 @@ export default function MuiRootProvider({
-
-
-
+
+
+
+ {children}
+
-
- {children}
-
-
-
+
-
-
+
+
+
);
diff --git a/src/sections/BrandBanner.tsx b/src/sections/BrandBanner.tsx
new file mode 100644
index 0000000..fb9b75b
--- /dev/null
+++ b/src/sections/BrandBanner.tsx
@@ -0,0 +1,82 @@
+'use client';
+import { COLORS } from '@/lib/tokens';
+import { jersey10 } from '@/lib/fonts';
+
+import { useEffect, useRef, useState } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { useInView } from 'framer-motion';
+
+
+const WHITE = 'Comunidad ';
+const YELLOW = 'JSCHILE';
+const FULL = WHITE + YELLOW;
+const CHAR_DELAY = 90;
+
+export const BrandBanner = () => {
+ const ref = useRef(null);
+ const inView = useInView(ref, { once: true, margin: '-10%' });
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ if (!inView || count >= FULL.length) return;
+ const t = setTimeout(() => setCount((c) => c + 1), CHAR_DELAY);
+ return () => clearTimeout(t);
+ }, [inView, count]);
+
+ const whiteText = FULL.slice(0, Math.min(count, WHITE.length));
+ const yellowText = count > WHITE.length ? FULL.slice(WHITE.length, count) : '';
+ const done = count >= FULL.length;
+
+ return (
+
+
+ {whiteText}
+ {yellowText}
+ {!done && (
+
+ _
+
+ )}
+
+
+ );
+};
diff --git a/src/sections/HistoriaTimeline.tsx b/src/sections/HistoriaTimeline.tsx
new file mode 100644
index 0000000..c82f97b
--- /dev/null
+++ b/src/sections/HistoriaTimeline.tsx
@@ -0,0 +1,268 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+
+import { useRef } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Container from '@mui/material/Container';
+import Grid from '@mui/material/Grid';
+import { motion, useScroll, useTransform, MotionValue } from 'framer-motion';
+import { Star, RocketLaunch } from '@phosphor-icons/react';
+
+
+const MILESTONES = [
+ {
+ year: '2013',
+ title: 'El Inicio',
+ description:
+ 'Nace la comunidad de JSChile como un pequeño grupo de entusiastas buscando compartir conocimientos y experiencias con esta tecnología web emergente.',
+ isLast: false,
+ // Scroll threshold at which this node activates (0 → 1)
+ t: 0,
+ },
+ {
+ year: '2016',
+ title: 'Los Primeros Meetups',
+ description:
+ 'Se realizan los primeros meetups regulares en Santiago, conectando a la comunidad developer con charlas técnicas de alto nivel y networking.',
+ isLast: false,
+ t: 0.2,
+ },
+ {
+ year: '2018',
+ title: 'La Expansión',
+ description:
+ 'Speakers internacionales se suman a los eventos y la comunidad crece exponencialmente, llegando a cientos de asistentes por encuentro.',
+ isLast: false,
+ t: 0.38,
+ },
+ {
+ year: '2023',
+ title: 'La Consolidación',
+ description:
+ 'JSChile se consolida como la comunidad tech más activa del país, con eventos de clase mundial y miles de miembros activos.',
+ isLast: false,
+ t: 0.58,
+ },
+ {
+ year: '2026',
+ title: 'El Presente',
+ description:
+ 'Seguimos creciendo, aprendiendo y conectando a la comunidad developer de Chile. El mejor capítulo aún está por escribirse.',
+ isLast: true,
+ t: 0.76,
+ },
+];
+
+const NODE_SIZE = 32;
+const NODE_HALF = NODE_SIZE / 2;
+
+function ChileMap() {
+ return (
+
+ );
+}
+
+function MilestoneItem({
+ milestone,
+ scrollProgress,
+}: {
+ milestone: (typeof MILESTONES)[0];
+ scrollProgress: MotionValue;
+}) {
+ const { t } = milestone;
+ const appear = [Math.max(0, t - 0.08), Math.min(1, t + 0.07)];
+
+ const nodeScale = useTransform(scrollProgress, [Math.max(0, t - 0.07), t], [0.55, 1]);
+ const nodeBg = useTransform(scrollProgress, (v) => v >= t ? COLORS.yellow : 'rgba(240,219,79,0.15)');
+ const iconColor = useTransform(scrollProgress, (v) => v >= t ? COLORS.textPrimary : COLORS.yellow);
+
+ const contentOpacity = useTransform(scrollProgress, appear, [0.25, 1]);
+ const contentX = useTransform(scrollProgress, appear, [20, 0]);
+
+ return (
+
+ {/* Node (layered on top of the continuous line) */}
+
+
+
+
+ {milestone.isLast ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Content */}
+
+
+ {milestone.year}
+
+
+ {milestone.title}
+
+
+ {milestone.description}
+
+
+
+ );
+}
+
+export const HistoriaTimeline = () => {
+ const sectionRef = useRef(null);
+
+ const { scrollYProgress } = useScroll({
+ target: sectionRef,
+ offset: ['start 0.3', 'end 0.4'],
+ });
+
+ // Map scroll progress to the portion of line covered (0 → last milestone threshold)
+ const lineScaleY = useTransform(scrollYProgress, [0, 1], [0, 1]);
+
+ return (
+
+
+ {/* Header */}
+
+
+
+ Pero... ¿cómo empezó todo?
+
+
+ Conoce cómo evolucionó la comunidad JSChile{' '}
+
+ más activa de Chile a través de los años
+
+
+
+
+
+ {/* Map + Timeline */}
+
+
+ {/* Chile map */}
+
+
+
+
+
+
+ {/* Timeline */}
+
+
+ {/* ── Track (always visible, fades to transparent at bottom) ── */}
+
+
+ {/* ── Animated yellow fill ── */}
+
+
+ {/* ── Milestones ── */}
+ {MILESTONES.map((milestone) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/src/sections/HomeHallOfFame.tsx b/src/sections/HomeHallOfFame.tsx
index 402aa83..8ccc0f9 100644
--- a/src/sections/HomeHallOfFame.tsx
+++ b/src/sections/HomeHallOfFame.tsx
@@ -1,4 +1,5 @@
'use client';
+import { COLORS } from '@/lib/tokens';
import Box from '@mui/material/Box';
import Container from '@mui/material/Container';
@@ -126,7 +127,7 @@ const MarqueeRow = ({
px: 3,
py: 1.5,
mx: 1, // Use mx for consistent spacing instead of gap
- bgcolor: member.relevant ? '#F0DB4F' : '#F5F5F5',
+ bgcolor: member.relevant ? COLORS.yellow : COLORS.graySubtle,
borderRadius: 100,
fontWeight: 800,
fontSize: '0.9rem',
@@ -173,7 +174,7 @@ const HomeHallOfFame = () => {
{
sx={{
textAlign: 'center',
mt: 8,
- color: '#AAA',
+ color: COLORS.gray,
fontWeight: 700,
fontSize: '0.9rem',
display: 'flex',
diff --git a/src/sections/JoinCTA.tsx b/src/sections/JoinCTA.tsx
new file mode 100644
index 0000000..a8592ce
--- /dev/null
+++ b/src/sections/JoinCTA.tsx
@@ -0,0 +1,119 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Stack from '@mui/material/Stack';
+import { motion } from 'framer-motion';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import { useCommunityModal } from '@/providers/CommunityModalProvider';
+import { PaperPlaneTilt, WhatsappLogo } from '@phosphor-icons/react';
+
+
+export const JoinCTA = () => {
+ const { openModal } = useCommunityModal();
+
+ return (
+
+
+
+ ¡Sé parte de JSChile HOY!
+
+
+
+ Ya sea que tengas una idea para compartir o simplemente quieras estar al tanto de lo que viene,{' '}
+
+ hay un lugar para ti en esta comunidad.
+
+
+
+
+
+
+
+ Enviar mi propuesta
+
+
+
+
+
+
+ Unirme al WhatsApp
+
+
+
+
+
+ );
+};
diff --git a/src/sections/NextEvent.tsx b/src/sections/NextEvent.tsx
new file mode 100644
index 0000000..f03dd58
--- /dev/null
+++ b/src/sections/NextEvent.tsx
@@ -0,0 +1,72 @@
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { COLORS } from '@/lib/tokens';
+import Box from '@mui/material/Box';
+import Container from '@mui/material/Container';
+import type { CalEvent, EventsJson } from '@/types/events';
+import { NextEventClient } from './NextEventClient';
+
+async function loadUpcomingEvents(): Promise {
+ try {
+ const raw = await readFile(join(process.cwd(), 'public', 'events.json'), 'utf-8');
+ const data: EventsJson = JSON.parse(raw);
+ return data.upcoming; // already sorted ascending by start date
+ } catch {
+ return [];
+ }
+}
+
+function formatDate(iso: string): string {
+ return new Date(iso).toLocaleDateString('es-CL', {
+ timeZone: 'America/Santiago',
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+}
+
+function formatTime(iso: string): string {
+ return new Date(iso).toLocaleTimeString('es-CL', {
+ timeZone: 'America/Santiago',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+}
+
+export const NextEvent = async () => {
+ const events = await loadUpcomingEvents();
+ if (events.length === 0) return null;
+
+ const [first, ...rest] = events;
+
+ const dateLabel = formatDate(first.start);
+ const timeLabel = `${formatTime(first.start)} hrs`;
+ const shortAddress = first.location ? first.location.split(',')[0] : null;
+
+ const otherEvents = rest.map(e => ({
+ uid: e.uid,
+ title: e.title,
+ url: e.url,
+ dateLabel: formatDate(e.start),
+ timeLabel: `${formatTime(e.start)} hrs`,
+ }));
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/sections/NextEventClient.tsx b/src/sections/NextEventClient.tsx
new file mode 100644
index 0000000..4075d4c
--- /dev/null
+++ b/src/sections/NextEventClient.tsx
@@ -0,0 +1,251 @@
+'use client';
+import { motion } from 'framer-motion';
+import Box from '@mui/material/Box';
+import Grid from '@mui/material/Grid';
+import Typography from '@mui/material/Typography';
+import { Stack } from '@mui/material';
+import { CalendarDots, MapPin, ArrowRight } from '@phosphor-icons/react';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import type { CalEvent } from '@/types/events';
+
+interface OtherEvent {
+ uid: string;
+ title: string;
+ url: string;
+ dateLabel: string;
+ timeLabel: string;
+}
+
+interface Props {
+ event: CalEvent;
+ dateLabel: string;
+ timeLabel: string;
+ shortAddress: string | null;
+ otherEvents: OtherEvent[];
+}
+
+const stagger = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.14 } },
+};
+
+const slideLeft = {
+ hidden: { opacity: 0, x: -60 },
+ show: { opacity: 1, x: 0, transition: { duration: 0.7, ease: EASE } },
+};
+
+export function NextEventClient({ event, dateLabel, timeLabel, shortAddress, otherEvents }: Props) {
+ return (
+
+ {/* Left column */}
+
+
+
+
+ Próximo evento
+
+
+
+
+
+ {event.title}
+
+
+
+
+
+
+
+ {dateLabel} · {timeLabel}
+
+
+
+
+ {shortAddress && (
+
+
+
+
+ {shortAddress}
+
+
+
+ )}
+
+
+
+
+
+ Inscribirme GRATIS
+
+
+
+
+ {/* Upcoming events after the main one */}
+ {otherEvents.length > 0 && (
+
+
+
+ También próximamente
+
+
+
+ {otherEvents.map(e => (
+
+
+
+ {e.title}
+
+
+ {e.dateLabel} · {e.timeLabel}
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* Right column — cover image */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/sections/NuestrosValores.tsx b/src/sections/NuestrosValores.tsx
new file mode 100644
index 0000000..95ff7f2
--- /dev/null
+++ b/src/sections/NuestrosValores.tsx
@@ -0,0 +1,154 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+
+import { useState } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Container from '@mui/material/Container';
+import { motion } from 'framer-motion';
+import { Gavel } from '@phosphor-icons/react';
+import { CodeOfConductModal } from './cards/code-of-conduct/CodeOfConductModal';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import { Stack } from '@mui/material';
+
+
+const stagger = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.12 } },
+};
+const item = {
+ hidden: { opacity: 0, y: 24 },
+ show: { opacity: 1, y: 0, transition: { duration: 0.65, ease: EASE } },
+};
+
+export const NuestrosValores = () => {
+ const [modalOpen, setModalOpen] = useState(false);
+
+ return (
+ <>
+
+
+ {/* Row 1 — title + button */}
+
+
+
+
+ Nuestros valores
+
+
+ setModalOpen(true)}
+ hoverColor={COLORS.yellowHover}
+ sx={{
+ bgcolor: COLORS.yellow,
+ color: COLORS.black,
+ px: 4,
+ py: 1.6,
+ fontSize: '1rem',
+ fontWeight: 700,
+ whiteSpace: 'nowrap',
+ }}
+ >
+
+
+ Ver código de conducta completo
+
+
+
+
+
+
+ {/* Row 2 — paragraphs */}
+
+
+
+ Queremos que cada evento sea un espacio seguro y libre de acoso{' '}
+
+ para todos
+
+ , sin importar género, orientación sexual, raza, capacidades, apariencia física o
+ creencias.
+
+
+
+
+
+
+ No toleramos ningún tipo de acoso, directo o indirecto.
+ {' '}
+ Quien no respete esto será excluido del evento y de futuras instancias.
+
+
+
+
+
+ ¿Viste o viviste algo así? Acércate a cualquier persona del staff (las
+ identificamos al inicio de cada evento). Te vamos a ayudar.
+
+
+
+
+
+
+ setModalOpen(false)} />
+ >
+ );
+};
diff --git a/src/sections/PastEventsCarousel.tsx b/src/sections/PastEventsCarousel.tsx
new file mode 100644
index 0000000..2713af5
--- /dev/null
+++ b/src/sections/PastEventsCarousel.tsx
@@ -0,0 +1,62 @@
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { PastEventsCarouselClient } from './PastEventsCarouselClient';
+import { FadeUp } from '@/components/ScrollReveal';
+import type { CalEvent, EventsJson } from '@/types/events';
+
+async function loadPastEvents(): Promise {
+ try {
+ const raw = await readFile(join(process.cwd(), 'public', 'events.json'), 'utf-8');
+ const data: EventsJson = JSON.parse(raw);
+ return data.past;
+ } catch {
+ return [];
+ }
+}
+
+export const PastEventsCarousel = async () => {
+ const events = await loadPastEvents();
+
+ return (
+
+
+
+
+ Lo que nos perdimos juntos
+
+
+ {events.length} eventos desde que empezamos
+
+
+
+
+
+
+ );
+};
diff --git a/src/sections/PastEventsCarouselClient.tsx b/src/sections/PastEventsCarouselClient.tsx
new file mode 100644
index 0000000..5c994b0
--- /dev/null
+++ b/src/sections/PastEventsCarouselClient.tsx
@@ -0,0 +1,209 @@
+'use client';
+import { COLORS } from '@/lib/tokens';
+import { useRef } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { motion, useMotionValue, useAnimationFrame } from 'framer-motion';
+import { Images } from '@phosphor-icons/react';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import { Stack } from '@mui/material';
+import type { CalEvent } from '@/types/events';
+
+const CARD_WIDTH = 320;
+const IMAGE_SIZE = 116;
+const GAP = 14;
+const ITEM_WIDTH = CARD_WIDTH + GAP;
+
+function EventCard({ event }: { event: CalEvent }) {
+ const date = new Date(event.start).toLocaleDateString('es-CL', {
+ timeZone: 'America/Santiago',
+ weekday: 'long',
+ day: 'numeric',
+ month: 'long',
+ });
+
+ const card = (
+
+
+
+
+ {event.title}
+
+
+
+ {date}
+
+
+ {event.location && (
+
+ {event.location.split(',')[0]}
+
+ )}
+
+
+ );
+
+ return (
+
+ {card}
+
+ );
+}
+
+function InfiniteRow({ events, reversed }: { events: CalEvent[]; reversed: boolean }) {
+ const totalWidth = ITEM_WIDTH * events.length;
+ // reversed row moves right (x: -totalWidth → 0), normal moves left (x: 0 → -totalWidth)
+ const x = useMotionValue(reversed ? -totalWidth : 0);
+ const paused = useRef(false);
+ // px per ms to complete one full loop in `events.length * 14` seconds
+ const speed = totalWidth / (events.length * 14 * 1000);
+
+ useAnimationFrame((_, delta) => {
+ if (paused.current) return;
+ let current = x.get();
+ if (reversed) {
+ current += speed * delta;
+ if (current >= 0) current -= totalWidth;
+ } else {
+ current -= speed * delta;
+ if (current <= -totalWidth) current += totalWidth;
+ }
+ x.set(current);
+ });
+
+ const tripled = [...events, ...events, ...events];
+
+ return (
+
+ { paused.current = true; }}
+ onMouseLeave={() => { paused.current = false; }}
+ >
+ {tripled.map((event, i) => (
+
+ ))}
+
+
+ );
+}
+
+export function PastEventsCarouselClient({ events }: { events: CalEvent[] }) {
+ const half = Math.ceil(events.length / 2);
+ const row1 = events.slice(0, half);
+ const row2 = events.slice(half);
+
+ return (
+ <>
+
+ = 3 ? row1 : events} reversed={false} />
+ = 3 ? row2 : events} reversed={true} />
+
+
+
+
+
+
+
+
+
+ Revive los momentos
+
+
+
+ >
+ );
+}
diff --git a/src/sections/SpeakersSection.tsx b/src/sections/SpeakersSection.tsx
new file mode 100644
index 0000000..6f38b26
--- /dev/null
+++ b/src/sections/SpeakersSection.tsx
@@ -0,0 +1,273 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+import { Speaker } from '@/lib/fetchSpeakers';
+
+import { useState } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Container from '@mui/material/Container';
+import Grid from '@mui/material/Grid';
+import IconButton from '@mui/material/IconButton';
+import Link from '@mui/material/Link';
+import Stack from '@mui/material/Stack';
+import { motion } from 'framer-motion';
+import { PaperPlaneTilt, UsersThree, LinkedinLogo, GithubLogo, InstagramLogo, Globe } from '@phosphor-icons/react';
+import { SpeakersModal } from './speakers/SpeakersModal';
+import { AnimatedButton } from '@/components/AnimatedButton';
+
+interface SpeakersSectionProps {
+ sectionSpeakers: Speaker[];
+ allSpeakers: Speaker[];
+}
+
+const stagger = {
+ hidden: {},
+ show: { transition: { staggerChildren: 0.07 } },
+};
+const cardAnim = {
+ hidden: { opacity: 0, y: 20 },
+ show: { opacity: 1, y: 0, transition: { duration: 0.5, ease: EASE } },
+};
+
+const ICON_SX = {
+ color: COLORS.yellow,
+ bgcolor: 'rgba(255,255,255,0.08)',
+ borderRadius: '10px',
+ '&:hover': { bgcolor: 'rgba(255,255,255,0.15)' },
+} as const;
+
+function SocialLinks({ speaker, size = 20 }: { speaker: Speaker; size?: number }) {
+ const links = [
+ { href: speaker.linkedin, Icon: LinkedinLogo, label: 'LinkedIn' },
+ { href: speaker.github, Icon: GithubLogo, label: 'GitHub' },
+ { href: speaker.instagram, Icon: InstagramLogo, label: 'Instagram' },
+ { href: speaker.website, Icon: Globe, label: 'Sitio web' },
+ ].filter((l) => l.href);
+
+ if (!links.length) return null;
+
+ return (
+
+ {links.map(({ href, Icon, label }) => (
+
+
+
+
+
+ ))}
+
+ );
+}
+
+function SpeakerCard({ speaker }: { speaker: Speaker }) {
+ return (
+
+
+
+
+
+ {speaker.name}
+
+
+
+
+
+ );
+}
+
+export const SpeakersSection = ({ sectionSpeakers, allSpeakers }: SpeakersSectionProps) => {
+ const [modalOpen, setModalOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ Speakers
+
+
+ setModalOpen(true)}
+ hoverColor='rgba(0, 0, 0, 0.2)'
+ sx={{
+ bgcolor: 'transparent',
+ color: COLORS.yellow,
+ border: `1px solid ${COLORS.yellow}`,
+ px: 4,
+ py: 1.6,
+ fontSize: '1rem',
+ fontWeight: 700,
+ whiteSpace: 'nowrap',
+ '&:hover': { border: `1px solid ${COLORS.yellow}` },
+ }}
+ >
+
+
+ Ver todos los speakers
+
+
+
+
+
+ Devs, diseñadores, CTOs y builders que subieron al escenario para que tú no tengas que aprenderlo solo.
+
+
+
+
+
+ {sectionSpeakers.map((speaker) => (
+
+
+
+ ))}
+
+
+
+
+
+
+ ¿Tú también tienes algo que enseñar?
+
+
+
+
+
+ Enviar mi propuesta
+
+
+
+
+
+
+
+ setModalOpen(false)}
+ speakers={allSpeakers}
+ />
+ >
+ );
+};
diff --git a/src/sections/SpeakersSectionWrapper.tsx b/src/sections/SpeakersSectionWrapper.tsx
new file mode 100644
index 0000000..fe115b2
--- /dev/null
+++ b/src/sections/SpeakersSectionWrapper.tsx
@@ -0,0 +1,8 @@
+import { fetchSpeakers } from '@/lib/fetchSpeakers';
+import { SpeakersSection } from './SpeakersSection';
+
+export async function SpeakersSectionWrapper() {
+ const speakers = await fetchSpeakers();
+ const sectionSpeakers = speakers.filter((s) => s.showInSection);
+ return ;
+}
diff --git a/src/sections/StatsSection.tsx b/src/sections/StatsSection.tsx
new file mode 100644
index 0000000..60f901b
--- /dev/null
+++ b/src/sections/StatsSection.tsx
@@ -0,0 +1,194 @@
+'use client';
+import { EASE, COLORS } from '@/lib/tokens';
+import { jersey10 } from '@/lib/fonts';
+
+import { useEffect, useRef, useState } from 'react';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { motion, useInView } from 'framer-motion';
+
+
+const BORDER_SUBTLE = '8px solid rgba(220, 215, 200, 0.85)';
+const BORDER_WHITE = '8px solid rgba(255, 255, 255, 1)';
+
+/* ── Count-up animado ── */
+function CountUp({ target, suffix = '' }: { target: number; suffix?: string }) {
+ const [count, setCount] = useState(0);
+ const ref = useRef(null);
+ const inView = useInView(ref, { once: true });
+
+ useEffect(() => {
+ if (!inView) return;
+ let frame = 0;
+ const total = 60;
+ const timer = setInterval(() => {
+ frame++;
+ const progress = frame / total;
+ // ease-out cubic
+ const eased = 1 - Math.pow(1 - progress, 3);
+ setCount(Math.round(eased * target));
+ if (frame >= total) clearInterval(timer);
+ }, 20);
+ return () => clearInterval(timer);
+ }, [inView, target]);
+
+ return {count.toLocaleString('es-CL')}{suffix};
+}
+
+const STATS = [
+ { prefix: '+', target: 120, label: 'Eventos', sub: 'desde 2013' },
+ { prefix: '+', target: 4800, label: 'Asistentes', sub: 'desde 2013' },
+ { prefix: '+', target: 200, label: 'Speakers', sub: 'desde 2013' },
+ { prefix: '+', target: 1500, suffix: ' en', label: 'WhatsApp', sub: 'desde 2013' },
+];
+
+const PHOTOS = [
+ {
+ src: '/assets/events/group-crowd.webp',
+ alt: 'Asistentes en evento JSChile',
+ border: BORDER_SUBTLE,
+ containerSx: {
+ transform: 'rotate(-7.27deg)',
+ zIndex: 1,
+ width: { xs: 180, sm: 280, md: 370 },
+ marginRight: { xs: '-50px', md: '-100px' },
+ },
+ fromRotate: -20,
+ delay: 0.1,
+ },
+ {
+ src: '/assets/events/group-selfie.webp',
+ alt: 'JSConf Chile — foto grupal',
+ border: BORDER_WHITE,
+ containerSx: {
+ transform: { xs: 'rotate(-0.42deg) translateY(-30px)', md: 'rotate(-0.42deg) translateY(-70px)' },
+ zIndex: 3,
+ width: { xs: 220, sm: 340, md: 480 },
+ },
+ fromRotate: 8,
+ delay: 0.5,
+ },
+ {
+ src: '/assets/events/group-stage.webp',
+ alt: 'Escenario JSConf Chile',
+ border: BORDER_SUBTLE,
+ containerSx: {
+ transform: 'rotate(7.2deg)',
+ zIndex: 1,
+ width: { xs: 180, sm: 280, md: 370 },
+ marginLeft: { xs: '-50px', md: '-100px' },
+ },
+ fromRotate: 20,
+ delay: 0.3,
+ },
+];
+
+export const StatsSection = () => {
+ return (
+
+
+ {/* ── Fotos rotadas ── */}
+
+ {PHOTOS.map((photo) => (
+
+
+
+
+
+ ))}
+
+
+ {/* ── Stats con count-up ── */}
+
+ {STATS.map((stat, i) => (
+
+
+
+ {stat.prefix}
+
+
+
+ {stat.label}
+
+
+ {stat.sub}
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/sections/WhatsAppCTA.tsx b/src/sections/WhatsAppCTA.tsx
new file mode 100644
index 0000000..994b374
--- /dev/null
+++ b/src/sections/WhatsAppCTA.tsx
@@ -0,0 +1,102 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import { motion } from 'framer-motion';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import { useCommunityModal } from '@/providers/CommunityModalProvider';
+import { WhatsappLogo } from '@phosphor-icons/react';
+import { Stack } from '@mui/material';
+
+
+export const WhatsAppCTA = () => {
+ const { openModal } = useCommunityModal();
+
+ return (
+ <>
+
+ {/* Icono oscilante */}
+
+
+
+
+
+
+ {/* Título */}
+
+
+ Únete a donde pasan las cosas
+
+
+
+ {/* Botón con spring */}
+
+
+
+
+ Unirme al WhatsApp
+
+
+
+
+
+ >
+ );
+};
diff --git a/src/sections/cards/CardsContainer.tsx b/src/sections/cards/CardsContainer.tsx
index c0f4fb4..071265f 100644
--- a/src/sections/cards/CardsContainer.tsx
+++ b/src/sections/cards/CardsContainer.tsx
@@ -9,7 +9,7 @@ import { Gallery } from './gallery/Gallery';
export const CardsContainer = () => {
return (
-
+
diff --git a/src/sections/cards/code-of-conduct/CodeOfConductModal.tsx b/src/sections/cards/code-of-conduct/CodeOfConductModal.tsx
index 5b7aeb0..6d8dd5a 100644
--- a/src/sections/cards/code-of-conduct/CodeOfConductModal.tsx
+++ b/src/sections/cards/code-of-conduct/CodeOfConductModal.tsx
@@ -1,10 +1,13 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
+
import React from 'react';
import Modal from '@mui/material/Modal';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
-import CloseIcon from '@mui/icons-material/Close';
-import GavelIcon from '@mui/icons-material/Gavel';
+import { X, Gavel } from '@phosphor-icons/react';
import { Stack } from '@mui/material';
interface CodeOfConductModalProps {
@@ -12,27 +15,26 @@ interface CodeOfConductModalProps {
onClose: () => void;
}
-const modalStyle = {
- position: 'absolute' as const,
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: { xs: '95%', sm: 600, md: 800 },
- maxHeight: '90vh',
- overflowY: 'auto',
- bgcolor: 'background.paper',
- borderRadius: 4,
- boxShadow: 24,
- p: { xs: 4, sm: 6 },
- display: 'flex',
- flexDirection: 'column',
- outline: 'none',
-};
+const SECTIONS = [
+ {
+ title: 'La versión más corta de todas',
+ body: '¿Has visto como luego de algunos eventos, todos terminan hablando de "aquella persona" que causó un mal rato a uno o más asistentes? Que no sea de ti de quien hablen.',
+ },
+ {
+ title: 'La versión no tan corta',
+ body: 'En los eventos desarrollados por JSChile (JSConf, TechSchool, Conferencias, etc), así como en cualquiera de las plataformas de redes sociales buscamos crear un espacio libre de acoso. Todos son bienvenidos, sin importar género, raza, orientación sexual, capacidades, apariencia física y/o creencias. No toleramos el acoso bajo ningún tipo, forma ni contexto. Cualquier participante que viole estas reglas será sancionado y expulsado de este y futuros eventos.',
+ },
+ {
+ title: '¿Qué se entiende por acoso?',
+ body: 'Abuso físico y/o verbal relacionados al género, orientación sexual, capacidades, apariencia física, nivel de conocimiento, raza y/o creencias religiosas. Creación y/o reproducción de imágenes de contenido sexual en espacios públicos, intimidación, acecho, interrupciones groseras de charlas y/o contacto físico inapropiado.',
+ },
+ {
+ title: '¿Qué pasa si alguien viola el código?',
+ body: 'La organización se reserva el derecho de solicitar que cualquier comportamiento relacionado a lo anterior sea detenido inmediatamente. Quienes atenten contra este código podrán ser expulsados de este y futuros eventos. Si eres víctima o testigo de acoso, por favor contacta a un miembro de la organización de forma inmediata.',
+ },
+];
-export const CodeOfConductModal: React.FC = ({
- open,
- onClose,
-}) => {
+export const CodeOfConductModal: React.FC = ({ open, onClose }) => {
return (
= ({
aria-describedby='code-of-conduct-modal-description'
disableScrollLock
>
-
+
-
+
-
-
-
-
- Código de Conducta
-
-
-
-
-
- La versión más corta de todas
-
+
+
- ¿Has visto como luego de algunos eventos, todos terminan hablando de
- “aquella persona” que causó un mal rato a uno o más asistentes?. Que
- no sea de ti de quien hablen
+ Código de Conducta
+
-
- La versión no tan corta
-
-
- En los eventos desarrollados por Javascript Chile (JSConf,
- TechSchool, Conferencias, etc), así como en cualquiera de las
- plataformas de redes sociales (Facebook, Slack, Discord, Twitter,
- etc) buscamos crear un espacio, y una experiencia, libre de acoso.
- Nuestro lema es que todos son bienvenidos, sin importar género,
- raza, orientación sexual, capacidades, apariencia física y/o
- creencias. No toleramos el acoso bajo ningún tipo, forma ni
- contexto, directa o indirectamente. Cualquier participante que viole
- estas reglas será sancionado y expulsado de este y futuros eventos
-
+
+ Nuestro compromiso con una comunidad segura e inclusiva para todos.
+
-
- La versión más larga ¿Qué se entiende por acoso?
-
-
-
- Abuso físico y/o verbal relacionados al género de una persona,
- orientación sexual, capacidades, género, apariencia física, nivel
- de conocimiento, raza y/o creencias religiosas. Adicionalmente,
- creación y/o reproducción de imágenes de contenido sexual en
- espacios públicos, intimidación física y/o verbal, acecho,
- interrupciones groseras de charlas y/o contacto físico inapropiado
-
-
- La organización se reserva el derecho de solicitar que cualquier
- comportamiento relacionado a lo anterior que sea visto durante un
- evento, sea detenido inmediatamente por quien(es) lo realizan.
- Adicionalmente, quienes atenten contra este código de conducta,
- podrán ser expulsados de este y futuros eventos
-
-
- Si eres víctima o testigo de algún tipo de acoso, de cualquiera
- según lo detallado anteriormente, por favor contacta a un miembro
- de la organización de forma inmediata. Los encargados del evento
- se identificarán oportunamente al comienzo de cada instancia. Si
- es necesario, la organización ayudará a contactar autoridades
- policiales y/o proveerá cualquier tipo de asistencia necesaria
- para remediar la situación
-
-
- Somos una comunidad inclusiva que valora la diversidad. Si no
- estás de acuerdo con esto, probablemente no debas estar en
- nuestros eventos
-
-
+
+ {SECTIONS.map(({ title, body }) => (
+
+
+ {title}
+
+
+ {body}
+
+
+ ))}
diff --git a/src/sections/cards/gallery/Gallery.tsx b/src/sections/cards/gallery/Gallery.tsx
index a4914f4..dc21f9e 100644
--- a/src/sections/cards/gallery/Gallery.tsx
+++ b/src/sections/cards/gallery/Gallery.tsx
@@ -1,30 +1,24 @@
'use client';
-import React, { useState } from 'react';
import { CardActions } from '@mui/material';
-import { GalleryModal } from './GalleryModal';
import { AnimatedButton } from '@/components/AnimatedButton';
import { BaseCard } from '@/components/BaseCard';
export const Gallery = () => {
- const [isModalOpen, setIsModalOpen] = useState(false);
-
return (
- <>
-
-
- setIsModalOpen(true)}
- >
- Revive los Momentos
-
-
-
-
- setIsModalOpen(false)} />
- >
+
+
+
+ Revive los Momentos
+
+
+
);
};
diff --git a/src/sections/cards/gallery/GalleryModal.tsx b/src/sections/cards/gallery/GalleryModal.tsx
index e5770d9..aa261d1 100644
--- a/src/sections/cards/gallery/GalleryModal.tsx
+++ b/src/sections/cards/gallery/GalleryModal.tsx
@@ -1,15 +1,16 @@
'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
import React from 'react';
import Modal from '@mui/material/Modal';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
-import CloseIcon from '@mui/icons-material/Close';
import ImageList from '@mui/material/ImageList';
import ImageListItem from '@mui/material/ImageListItem';
import { AnimatedButton } from '@/components/AnimatedButton';
-import PhotoCameraIcon from '@mui/icons-material/PhotoCamera';
+import { X, Camera } from '@phosphor-icons/react';
import { Stack } from '@mui/material';
interface GalleryModalProps {
@@ -17,47 +18,13 @@ interface GalleryModalProps {
onClose: () => void;
}
-const modalStyle = {
- position: 'absolute' as const,
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: { xs: '90%', sm: '80%', md: '70%' },
- maxWidth: '900px',
- maxHeight: '90vh',
- bgcolor: 'background.paper',
- borderRadius: 2,
- boxShadow: 24,
- p: 4,
- overflow: 'auto',
- outline: 'none',
-};
-
const itemData = [
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.jpeg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.jpg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.jpeg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.jpeg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.jpeg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
- {
- img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.jpeg',
- title: '1ra Junta Casual JS Chile Febrero 2026',
- },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.01.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-07-08.34.59.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.12.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.15.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.18.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
+ { img: '/assets/events/2026/1ra-junta/junta-casual-2026-02-06-18.35.16.webp', title: '1ra Junta Casual JS Chile Febrero 2026' },
];
const pattern = [
@@ -72,68 +39,91 @@ const pattern = [
function srcset(image: string, size: number, rows = 1, cols = 1) {
return {
src: `${image}?w=${size * cols}&h=${size * rows}&fit=crop&auto=format`,
- srcSet: `${image}?w=${size * cols}&h=${
- size * rows
- }&fit=crop&auto=format&dpr=2 2x`,
+ srcSet: `${image}?w=${size * cols}&h=${size * rows}&fit=crop&auto=format&dpr=2 2x`,
};
}
-export const GalleryModal: React.FC = ({
- open,
- onClose,
-}) => {
+export const GalleryModal: React.FC = ({ open, onClose }) => {
return (
-
+
-
+
-
-
-
+
+
+
Galería de Eventos
-
+
Revive los mejores momentos de nuestros meetups y conferencias.
- Inspiración y aprendizaje en cada encuentro.
-
+
{itemData.map((item, index) => {
const layout = pattern[index % pattern.length];
return (
-
+
);
@@ -143,11 +133,17 @@ export const GalleryModal: React.FC = ({
Ver Galería Completa →
diff --git a/src/sections/cards/our-story/OurStoryModal.tsx b/src/sections/cards/our-story/OurStoryModal.tsx
index 36f0e33..363cd23 100644
--- a/src/sections/cards/our-story/OurStoryModal.tsx
+++ b/src/sections/cards/our-story/OurStoryModal.tsx
@@ -1,71 +1,49 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
+
import React from 'react';
import Modal from '@mui/material/Modal';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
-import CloseIcon from '@mui/icons-material/Close';
import { Stack } from '@mui/material';
-import HistoryIcon from '@mui/icons-material/History';
+import { X, ClockCounterClockwise } from '@phosphor-icons/react';
interface OurStoryModalProps {
open: boolean;
onClose: () => void;
}
-const modalStyle = {
- position: 'absolute' as const,
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: { xs: '95%', sm: 600, md: 800 },
- maxHeight: '90vh',
- overflowY: 'auto',
- bgcolor: 'background.paper',
- borderRadius: 4,
- boxShadow: 24,
- p: { xs: 4, sm: 6 },
- display: 'flex',
- flexDirection: 'column',
- outline: 'none',
-};
-
const timelineData = [
{
year: '2013',
- title: 'Los Inicios ',
- description:
- 'Nace la comunidad de JavaScript Chile como un pequeño grupo de entusiastas buscando compartir conocimientos y experiencias con esta tecnología web emergente.',
+ title: 'Los Inicios',
+ description: 'Nace la comunidad de JSChile como un pequeño grupo de entusiastas buscando compartir conocimientos y experiencias con esta tecnología web emergente.',
},
{
year: '2016',
- title: 'Crecimiento y Consolidación ',
- description:
- 'Los meetups mensuales comienzan a ser un éxito, atrayendo a centenares de desarrolladores. JS Chile se convierte en la principal agrupación de la tecnología en el país.',
+ title: 'Crecimiento y Consolidación',
+ description: 'Los meetups mensuales comienzan a ser un éxito, atrayendo a centenares de desarrolladores. JS Chile se convierte en la principal agrupación de la tecnología en el país.',
},
{
year: '2018',
- title: 'Hito Histórico: JSConf Chile ',
- description:
- 'Se organiza la primera edición de la prestigiosa conferencia internacional JSConf en nuestro país, posicionando a Chile en el mapa global del desarrollo frontend.',
+ title: 'Hito Histórico: JSConf Chile',
+ description: 'Se organiza la primera edición de la prestigiosa conferencia internacional JSConf en nuestro país, posicionando a Chile en el mapa global del desarrollo frontend.',
},
{
year: '2023',
- title: 'Renacimiento Pospandemia ',
- description:
- 'Tras unos años desafiantes, un nuevo grupo de organizadores toma la batuta para reactivar la magia de los meetups presenciales, llenando nuevamente los auditorios.',
+ title: 'Renacimiento Pospandemia',
+ description: 'Tras unos años desafiantes, un nuevo grupo de organizadores toma la batuta para reactivar la magia de los meetups presenciales, llenando nuevamente los auditorios.',
},
{
year: '2026',
- title: 'El Futuro es Ahora ',
- description:
- 'Lanzamiento de la nueva identidad visual y plataforma web oficial. La comunidad más grande de desarrollo en Chile sigue construyendo un puente educativo accesible para todos.',
+ title: 'El Futuro es Ahora',
+ description: 'Lanzamiento de la nueva identidad visual y plataforma web oficial. La comunidad más grande de desarrollo en Chile sigue construyendo un puente educativo accesible para todos.',
},
];
-export const OurStoryModal: React.FC = ({
- open,
- onClose,
-}) => {
+export const OurStoryModal: React.FC = ({ open, onClose }) => {
return (
= ({
aria-describedby='our-story-modal-description'
disableScrollLock
>
-
+
-
+
-
-
-
-
+
+
+
Nuestra Historia
- Conoce cómo evolucionó la comunidad de JavaScript más activa de Chile
- a través de los años
+ Cómo evolucionó la comunidad JSChile más activa de Chile a través de los años.
- {/* Timeline Minimalista Custom */}
-
- {/* Línea vertical principal */}
+ {/* Timeline */}
+
+ {/* Línea vertical */}
@@ -126,48 +134,43 @@ export const OurStoryModal: React.FC = ({
sx={{
position: 'relative',
pl: 5,
- pb: index !== timelineData.length - 1 ? 5 : 2,
+ pb: index !== timelineData.length - 1 ? 4 : 0,
}}
>
- {/* Punto indicador */}
+ {/* Punto */}
-
-
- {item.year}
-
-
- {item.title}
-
-
- {item.description}
-
-
+
+ {item.year}
+
+
+ {item.title}
+
+
+ {item.description}
+
))}
diff --git a/src/sections/hero/HeroContainer.tsx b/src/sections/hero/HeroContainer.tsx
index 8b21c2c..5e593d1 100644
--- a/src/sections/hero/HeroContainer.tsx
+++ b/src/sections/hero/HeroContainer.tsx
@@ -1,19 +1,29 @@
-import Container from '@mui/material/Container';
-import Grid from '@mui/material/Grid';
+import Box from '@mui/material/Box';
import { HeroInfo } from './HeroInfo';
-import { HeroNextEvent } from './HeroNextEvent';
export const HeroContainer = () => {
return (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
);
};
diff --git a/src/sections/hero/HeroInfo.tsx b/src/sections/hero/HeroInfo.tsx
index cdb8d80..290b76c 100644
--- a/src/sections/hero/HeroInfo.tsx
+++ b/src/sections/hero/HeroInfo.tsx
@@ -1,82 +1,136 @@
-import Card from '@mui/material/Card';
-import Grid from '@mui/material/Grid';
-import Typography from '@mui/material/Typography';
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { EASE, COLORS } from '@/lib/tokens';
+
+import { useState } from 'react';
import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import Stack from '@mui/material/Stack';
+import { motion } from 'framer-motion';
+import { AnimatedButton } from '@/components/AnimatedButton';
+import { useCommunityModal } from '@/providers/CommunityModalProvider';
+import { WhatsappLogo, CaretDown } from '@phosphor-icons/react';
+
+
+const FINAL_TEXT = 'Más que eventos.\nUna red de devs que se apoyan';
+
+const containerVariants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.09, delayChildren: 0.1 } },
+};
+
+const wordVariants = {
+ hidden: { opacity: 0, filter: 'blur(20px)', y: 8 },
+ visible: {
+ opacity: 1,
+ filter: 'blur(0px)',
+ y: 0,
+ transition: { duration: 0.7, ease: EASE },
+ },
+};
export const HeroInfo = () => {
+ const { openModal } = useCommunityModal();
+ const [showContent, setShowContent] = useState(false);
+
+ const lines = FINAL_TEXT.split('\n');
+
return (
-
-
-
-
- JavaScript Chile
-
-
- Somos una comunidad de desarrolladores apasionados por la
- tecnología. Organizamos meetups, workshops e iniciativas para
- compartir conocimiento, conectar profesionales y construir el futuro
- del desarrollo en Chile.
-
-
-
+
+ {/* ── Title blur-reveal word by word ── */}
+ setShowContent(true)}
+ >
+
+ {lines.map((line, li) => (
+
+ {line.split(' ').map((word, wi) => (
+
+ {word}
+
+ ))}
+
+ ))}
+
+
+
+ {/* ── Subtitle + buttons fade in after title ── */}
+
+
- {/* Logo from Assets - Bigger size as requested */}
-
-
-
-
+ Somos una comunidad de desarrolladores apasionados por la tecnología.
+ Organizamos meetups, workshops e iniciativas para compartir conocimiento,
+ conectar profesionales y construir el futuro del desarrollo en Chile.
+
+
+
+
+
+
+ Conocer más
+
+
+
+
+
+
+ Unirme al WhatsApp
+
+
+
+
+
+
);
};
diff --git a/src/sections/hero/HeroNextEvent.tsx b/src/sections/hero/HeroNextEvent.tsx
index 5b493c9..b3dbd6b 100644
--- a/src/sections/hero/HeroNextEvent.tsx
+++ b/src/sections/hero/HeroNextEvent.tsx
@@ -1,13 +1,12 @@
+import { COLORS } from '@/lib/tokens';
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
-import BoltIcon from '@mui/icons-material/Bolt';
import { AnimatedButton } from '@/components/AnimatedButton';
-import CalendarMonthIcon from '@mui/icons-material/CalendarMonth';
import { Stack } from '@mui/material';
-import FavoriteIcon from '@mui/icons-material/Favorite';
+import { Lightning, CalendarDots, Heart } from '@phosphor-icons/react';
export const HeroNextEvent = () => {
return (
@@ -24,13 +23,13 @@ export const HeroNextEvent = () => {
variant='h5'
sx={{
fontWeight: 800,
- color: '#000',
+ color: COLORS.black,
gap: 1,
letterSpacing: '0.1em',
}}
>
-
+
PRÓXIMOS EVENTOS
@@ -47,9 +46,7 @@ export const HeroNextEvent = () => {
¿Listo para aprender, conectar y crecer? Descubre nuestros próximos
eventos y súmate a la comunidad
-
+
@@ -62,7 +59,7 @@ export const HeroNextEvent = () => {
target='_blank'
>
-
+
Ver próximos eventos
diff --git a/src/sections/speakers/SpeakersModal.tsx b/src/sections/speakers/SpeakersModal.tsx
new file mode 100644
index 0000000..8afa9be
--- /dev/null
+++ b/src/sections/speakers/SpeakersModal.tsx
@@ -0,0 +1,178 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
+import { Speaker } from '@/lib/fetchSpeakers';
+
+import React from 'react';
+import Modal from '@mui/material/Modal';
+import Box from '@mui/material/Box';
+import Typography from '@mui/material/Typography';
+import IconButton from '@mui/material/IconButton';
+import Link from '@mui/material/Link';
+import Grid from '@mui/material/Grid';
+import Stack from '@mui/material/Stack';
+import { X, Microphone, LinkedinLogo, GithubLogo, InstagramLogo, Globe } from '@phosphor-icons/react';
+
+interface SpeakersModalProps {
+ open: boolean;
+ onClose: () => void;
+ speakers: Speaker[];
+}
+
+const ICON_SX = {
+ color: COLORS.yellow,
+ bgcolor: 'rgba(255,255,255,0.08)',
+ borderRadius: '10px',
+ '&:hover': { bgcolor: 'rgba(255,255,255,0.15)' },
+} as const;
+
+function ModalSpeakerCard({ speaker }: { speaker: Speaker }) {
+ const links = [
+ { href: speaker.linkedin, Icon: LinkedinLogo, label: 'LinkedIn' },
+ { href: speaker.github, Icon: GithubLogo, label: 'GitHub' },
+ { href: speaker.instagram, Icon: InstagramLogo, label: 'Instagram' },
+ { href: speaker.website, Icon: Globe, label: 'Sitio web' },
+ ].filter((l) => l.href);
+
+ return (
+
+
+
+
+ {speaker.name}
+
+ {links.length > 0 && (
+
+ {links.map(({ href, Icon, label }) => (
+
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export const SpeakersModal: React.FC = ({ open, onClose, speakers }) => {
+ return (
+
+
+
+
+
+
+
+
+
+ Todos los Speakers
+
+
+
+
+ {speakers.length} speakers que han subido al escenario de JSChile.
+
+
+
+ {speakers.map((speaker) => (
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/src/sections/staff/StaffCard.tsx b/src/sections/staff/StaffCard.tsx
index a1e0a10..3769a2c 100644
--- a/src/sections/staff/StaffCard.tsx
+++ b/src/sections/staff/StaffCard.tsx
@@ -1,4 +1,5 @@
'use client';
+import { COLORS } from '@/lib/tokens';
import React, { useState } from 'react';
import { motion } from 'framer-motion';
@@ -26,11 +27,16 @@ export const StaffCard: React.FC = ({
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
+ onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(); } }}
sx={{
cursor: 'pointer',
+ '&:focus-visible': { outline: `3px solid ${COLORS.yellow}`, borderRadius: '12px' },
}}
>
= ({
height: imageSize,
borderRadius: '12px',
border: '3px solid',
- borderColor: isHovered ? '#F0DB4F' : 'primary.main',
+ borderColor: isHovered ? COLORS.yellow : COLORS.black,
transition: 'all 0.3s ease',
position: 'relative',
zIndex: 1,
overflow: 'hidden',
- backgroundImage: `url(${staff.imageUrl})`,
- backgroundSize: 'cover',
- backgroundPosition: 'center',
- filter:
- isExStaff && !isHovered ? 'grayscale(100%)' : 'grayscale(0%)',
}}
- />
+ >
+
+
= ({
isModalOpen = false,
reverseDirection = false,
}) => {
- const [isPaused, setIsPaused] = useState(false);
- const [animationKey, setAnimationKey] = useState(0);
- const [isHovering, setIsHovering] = useState(false);
+ const cardWidth = 120;
+ const gap = 16;
+ const totalWidth = (cardWidth + gap) * staff.length;
+ // non-reversed: x goes from -totalWidth → 0 (moves right)
+ // reversed: x goes from 0 → -totalWidth (moves left)
+ const x = useMotionValue(reverseDirection ? 0 : -totalWidth);
+ const speed = totalWidth / (staff.length * 5 * 1000); // px per ms
- // Pausar si el modal está abierto
- React.useEffect(() => {
- if (isModalOpen) {
- setIsPaused(true);
- } else if (!isHovering) {
- setIsPaused(false);
+ // Two independent reasons to pause — use a ref so changes don't re-render
+ const hovering = useRef(false);
+ const modalOpen = useRef(isModalOpen);
+
+ useEffect(() => {
+ modalOpen.current = isModalOpen;
+ }, [isModalOpen]);
+
+ useAnimationFrame((_, delta) => {
+ if (hovering.current || modalOpen.current) return;
+ let current = x.get();
+ if (reverseDirection) {
+ current -= speed * delta;
+ if (current <= -totalWidth) current += totalWidth;
+ } else {
+ current += speed * delta;
+ if (current >= 0) current -= totalWidth;
}
- }, [isModalOpen, isHovering]);
+ x.set(current);
+ });
- // Duplicar el array para crear efecto infinito
const triplicatedStaff = [...staff, ...staff, ...staff];
- const duplicatedStaff = reverseDirection
- ? triplicatedStaff.reverse()
- : triplicatedStaff;
-
- const cardWidth = 120;
- const gap = 16;
- const totalWidth = (cardWidth + gap) * staff.length;
- const animationValues = reverseDirection
- ? [0, -totalWidth]
- : [-totalWidth, 0];
return (
-
+
setIsHovering(true)}
- onMouseLeave={() => setIsHovering(false)}
+ style={{ display: 'flex', gap: `${gap}px`, width: 'fit-content', x }}
+ onMouseEnter={() => { hovering.current = true; }}
+ onMouseLeave={() => { hovering.current = false; }}
>
- {duplicatedStaff.map((member, index) => (
-
+ {triplicatedStaff.map((member, index) => (
+
onStaffClick(member)}
@@ -94,29 +74,18 @@ export const StaffCarousel: React.FC = ({
))}
- {/* Gradientes en los bordes para efecto fade */}
diff --git a/src/sections/staff/StaffContainer.tsx b/src/sections/staff/StaffContainer.tsx
index ec21e2b..b208a84 100644
--- a/src/sections/staff/StaffContainer.tsx
+++ b/src/sections/staff/StaffContainer.tsx
@@ -1,14 +1,16 @@
'use client';
+import { COLORS } from '@/lib/tokens';
+import { jersey10 } from '@/lib/fonts';
import React, { useState } from 'react';
import { Box, Container, Typography, Grid, Stack } from '@mui/material';
import { DATA_STAFF, StaffMember } from '@/data/data_staff';
import StaffModal from './StaffModal';
-import StarIcon from '@mui/icons-material/Star';
-import CalendarViewMonthIcon from '@mui/icons-material/CalendarViewMonth';
+import { Star, SquaresFour } from '@phosphor-icons/react';
import { StaffCarousel } from './StaffCarousel';
+
const StaffContainer: React.FC = () => {
const [selectedStaff, setSelectedStaff] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -27,12 +29,12 @@ const StaffContainer: React.FC = () => {
const exStaff = DATA_STAFF.filter((staff) => staff.isExStaff);
return (
-
+
{
spacing={1}
sx={{ mb: 1 }}
>
-
+
Staff 2026
@@ -75,16 +75,9 @@ const StaffContainer: React.FC = () => {
spacing={1}
sx={{ mb: 1 }}
>
-
-
2023 al 2025
diff --git a/src/sections/staff/StaffModal.tsx b/src/sections/staff/StaffModal.tsx
index d0a85b6..31a1246 100644
--- a/src/sections/staff/StaffModal.tsx
+++ b/src/sections/staff/StaffModal.tsx
@@ -1,15 +1,14 @@
+'use client';
+import { jersey10 } from '@/lib/fonts';
+import { COLORS } from '@/lib/tokens';
+
import React from 'react';
import Modal from '@mui/material/Modal';
import Box from '@mui/material/Box';
-import Avatar from '@mui/material/Avatar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import Link from '@mui/material/Link';
-import CloseIcon from '@mui/icons-material/Close';
-import LinkedInIcon from '@mui/icons-material/LinkedIn';
-import InstagramIcon from '@mui/icons-material/Instagram';
-import LanguageIcon from '@mui/icons-material/Language';
-import MailIcon from '@mui/icons-material/Mail';
+import { X, LinkedinLogo, InstagramLogo, Globe, Envelope } from '@phosphor-icons/react';
import { StaffMember } from '@/data/data_staff';
interface StaffModalProps {
@@ -18,22 +17,6 @@ interface StaffModalProps {
staff: StaffMember | null;
}
-const modalStyle = {
- position: 'absolute' as const,
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: { xs: '90%', sm: 400 },
- bgcolor: 'background.paper',
- borderRadius: 2,
- boxShadow: 24,
- p: 4,
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- outline: 'none',
-};
-
const StaffModal: React.FC = ({ open, onClose, staff }) => {
if (!staff) return null;
@@ -45,50 +28,115 @@ const StaffModal: React.FC = ({ open, onClose, staff }) => {
aria-describedby='staff-modal-description'
disableScrollLock
>
-
+
-
+
-
{staff.fullName}
-
+
{staff.role}
-
+
{staff.date}
-
+
{staff.linkedin && (
-
-
+
+
)}
@@ -97,10 +145,21 @@ const StaffModal: React.FC = ({ open, onClose, staff }) => {
href={staff.instagram}
target='_blank'
rel='noopener noreferrer'
- sx={{ display: 'flex', alignItems: 'center' }}
+ aria-label={`Instagram de ${staff.fullName} (abre en nueva pestaña)`}
+ role='listitem'
>
-
-
+
+
)}
@@ -109,20 +168,42 @@ const StaffModal: React.FC = ({ open, onClose, staff }) => {
href={staff.website}
target='_blank'
rel='noopener noreferrer'
- sx={{ display: 'flex', alignItems: 'center' }}
+ aria-label={`Sitio web de ${staff.fullName} (abre en nueva pestaña)`}
+ role='listitem'
>
-
-
+
+
)}
{staff.email && (
-
-
+
+
)}
diff --git a/src/theme/theme.ts b/src/theme/theme.ts
index de02515..69b320a 100644
--- a/src/theme/theme.ts
+++ b/src/theme/theme.ts
@@ -1,4 +1,4 @@
-'use client';
+import { COLORS } from '@/lib/tokens';
import { createTheme } from '@mui/material/styles';
import { Inter } from 'next/font/google';
@@ -11,40 +11,42 @@ const inter = Inter({
// Brand Palette based on JSChile Logo
const brand = {
- 50: '#FEFCE8',
- 100: '#FEF9C3',
- 200: '#FEF08A',
- 300: '#FDE047',
- 400: '#FACC15',
- 500: '#ecd51a', // Official JS Yellow (Matches Logo)
- 600: '#EAB308',
- 700: '#CA8A04',
- 800: '#A16207',
- 900: '#713F12',
+ 50: COLORS.yellowHover,
+ 100: COLORS.yellowHover,
+ 200: COLORS.yellowHover,
+ 300: COLORS.yellow,
+ 400: COLORS.yellow,
+ 500: COLORS.yellow,
+ 600: COLORS.yellowMid,
+ 700: COLORS.yellowDark,
+ 800: COLORS.olive,
+ 900: COLORS.oliveDark,
};
const gray = {
- 50: '#FAFAFA',
+ 50: '#FAFAFA',
100: '#F5F5F5',
200: '#EEEEEE',
300: '#E0E0E0',
400: '#BDBDBD',
500: '#9E9E9E',
- 600: '#757575',
- 700: '#616161',
- 800: '#424242',
- 900: '#212121',
+ 600: COLORS.gray,
+ 700: COLORS.gray,
+ 800: COLORS.charcoal,
+ 900: COLORS.black,
};
const theme = createTheme({
palette: {
mode: 'light',
primary: {
- main: brand[500], // Yellow
- contrastText: '#000000',
+ main: brand[500],
+ dark: brand[500], // keeps bg unchanged on hover — AnimatedButton's span handles it
+ contrastText: COLORS.black,
},
secondary: {
- main: '#000000',
+ main: COLORS.black,
+ dark: COLORS.black, // same reason
contrastText: '#FFFFFF',
},
background: {
@@ -52,7 +54,7 @@ const theme = createTheme({
paper: '#FFFFFF',
},
text: {
- primary: '#000000',
+ primary: COLORS.black,
secondary: gray[600],
},
},
@@ -96,6 +98,15 @@ const theme = createTheme({
WebkitFontSmoothing: 'antialiased',
MozOsxFontSmoothing: 'grayscale',
},
+ /* WCAG AA: detiene animaciones CSS para usuarios con prefers-reduced-motion */
+ '@media (prefers-reduced-motion: reduce)': {
+ '*, *::before, *::after': {
+ animationDuration: '0.01ms !important',
+ animationIterationCount: '1 !important',
+ transitionDuration: '0.01ms !important',
+ scrollBehavior: 'auto !important',
+ },
+ },
a: {
color: 'inherit',
textDecoration: 'none',
@@ -141,11 +152,8 @@ const theme = createTheme({
},
},
containedSecondary: {
- backgroundColor: '#000000',
+ backgroundColor: COLORS.black,
color: '#FFFFFF',
- '&:hover': {
- backgroundColor: '#333333',
- },
},
},
},
diff --git a/src/types/events.ts b/src/types/events.ts
new file mode 100644
index 0000000..4f10026
--- /dev/null
+++ b/src/types/events.ts
@@ -0,0 +1,16 @@
+export interface CalEvent {
+ uid: string;
+ title: string;
+ start: string;
+ end: string;
+ location: string | null;
+ url: string;
+ cover_url: string | null;
+ status: 'upcoming' | 'ongoing' | 'past';
+}
+
+export interface EventsJson {
+ generated_at: string;
+ upcoming: CalEvent[];
+ past: CalEvent[];
+}