Sistema de Eventos - Documentación Técnica Completa
📋 Tabla de Contenidos
- Visión General
- Arquitectura del Sistema
- Backend - APIs y Base de Datos
- Frontend - Componentes
- Flujo End-to-End
- Actualización Automática
- Especificaciones Técnicas
Visión General
El sistema de eventos de Makers of Murcia es un sistema completamente autónomo que obtiene, almacena y muestra eventos desde Meetup sin intervención humana. El sistema combina múltiples fuentes de datos y se actualiza automáticamente.
Características Principales
- ✅ Obtención automática de eventos futuros desde GraphQL API de Meetup
- ✅ Obtención automática de eventos pasados desde RSS Feed de Meetup
- ✅ Almacenamiento en base de datos PostgreSQL con Prisma ORM
- ✅ Actualización mensual automática mediante cron job
- ✅ Fallback a JSON estático cuando la base de datos está vacía
- ✅ Mismo comportamiento en local y producción
- ✅ Sin datos mockup - solo eventos reales
Arquitectura del Sistema
Diagrama de Flujo
┌─────────────────────────────────────────────────────────────┐
│ FUENTES DE DATOS │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ GraphQL API │ │ RSS Feed │ │
│ │ (Futuros) │ │ (Pasados) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ /api/populate-events │ │
│ │ (Guarda en BD) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ PostgreSQL (Prisma)│ │
│ │ Tabla: cached_events│ │
│ └──────────┬───────────┘ │
│ │ │
└────────────────────┼────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ API UNIFICADA │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ │
│ │ /api/events │ │
│ │ │ │
│ │ 1. GraphQL (futuros) │ │
│ │ 2. BD Cache (pasados) │ │
│ │ 3. JSON Fallback │ │
│ │ │ │
│ │ → Combina y normaliza │ │
│ │ → Elimina duplicados │ │
│ │ → Ordena cronológicamente │
│ └──────────┬───────────┘ │
│ │ │
└────────────────────┼────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ COMPONENTES FRONTEND │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Event.tsx │ │EventsArchive │ │
│ │ (Página │ │.tsx │ │
│ │ raíz) │ │(/events) │ │
│ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Backend - APIs y Base de Datos
1. API Unificada: /api/events
Archivo: app/api/events/route.ts
Función: Endpoint principal que combina todas las fuentes de eventos y las normaliza.
Parámetros de Query:
limit(opcional): Número máximo de eventos. Por defecto: todos los disponiblesstatus(opcional): Filtrar por estado ("all"|"past"|"upcoming"). Por defecto:"all"group(opcional): URL del grupo de Meetup. Por defecto:"makers-of-murcia"
Flujo de Ejecución:
- Obtiene eventos futuros desde GraphQL API (en paralelo)
- Obtiene eventos pasados desde base de datos (en paralelo)
- Obtiene eventos pasados desde JSON estático como fallback (en paralelo)
- Combina todos los eventos
- Normaliza fechas y formatos
- Elimina duplicados (priorizando GraphQL sobre JSON)
- Ordena cronológicamente (más reciente primero)
- Aplica límite si se especifica
- Devuelve respuesta JSON con todos los eventos
Respuesta:
{
success: true,
events: ProcessedEvent[],
count: number,
source: 'graphql+json' | 'graphql+cache',
stats: {
future: number,
past: number,
total: number
}
}Código Clave:
// app/api/events/route.ts
// Obtener eventos futuros desde GraphQL
const futureEventsPromise = fetchEventsFromGraphQL(groupUrlname, limit)
// Obtener eventos pasados desde la base de datos (actualizada automáticamente por cron)
const pastEventsFromCachePromise = getPastEventsFromCache(limit)
const pastEventsFromJSONPromise = loadPastEventsFromJSON()
// Ejecutar todas las promesas en paralelo
const [futureEvents, pastEventsFromCache, pastEventsFromJSON] = await Promise.all([
futureEventsPromise,
pastEventsFromCachePromise,
pastEventsFromJSONPromise
])
// Usar eventos de caché si hay, sino usar JSON como fallback
const pastEvents = pastEventsFromCache.length > 0 ? pastEventsFromCache : pastEventsFromJSON2. Biblioteca Compartida: lib/meetup-api.ts
Archivo: lib/meetup-api.ts
Función: Funciones reutilizables para obtener eventos desde Meetup API.
fetchEventsFromGraphQL()
Obtiene eventos futuros desde la GraphQL API oficial de Meetup.
Endpoint: https://api.meetup.com/gql-ext
Autenticación: Opcional (mejora rate limits con MEETUP_API_TOKEN)
Limitación: GraphQL solo devuelve eventos futuros, NO eventos pasados
Características:
- Solo devuelve eventos futuros (no pasados)
- No requiere autenticación (opcional con
MEETUP_API_TOKEN) - Timeout de 5 segundos
- Normaliza fechas a formato ISO
- Determina automáticamente el status (
upcoming|past|cancelled)
Query GraphQL:
query GetGroupEvents($urlname: String!, $first: Int!) {
groupByUrlname(urlname: $urlname) {
id
name
urlname
events(first: $first) {
edges {
node {
id
title
description
localTime
localDate
eventUrl
photoUrl
rsvpCount
status
venue {
name
address
city
state
country
}
timezone
duration
group {
name
}
}
}
}
}
}fetchEventsFromRSS()
Obtiene eventos pasados desde el RSS Feed de Meetup.
URLs intentadas (en orden):
https://www.meetup.com/{group}/events/rss/https://www.meetup.com/es-ES/{group}/events/rss/https://meetup.com/{group}/events/rss/
Proceso:
- Intenta cada URL hasta encontrar una que funcione
- Parsea XML RSS usando regex
- Extrae: título, link, descripción, fecha
- Genera ID del evento desde la URL (
/events/{id}/) - Elimina duplicados por ID
- Devuelve array de eventos
Limitaciones:
- RSS no incluye número de asistentes (se establece en 0)
- RSS no incluye imágenes (usa placeholder de Unsplash)
- Fechas se formatean a formato legible español
Interfaz ProcessedEvent:
interface ProcessedEvent {
id: string
title: string
organizer: {
name: string
avatar: string
subtitle: string
}
image: string
date: string // ISO format: "2022-11-05T10:00:00+01:00"
description: string
link: string
attendees: number
status: string // "past" | "upcoming" | "cancelled"
venue?: {
name?: string
address?: string
}
time?: string
duration?: string
}3. API de Población: /api/populate-events
Archivo: app/api/populate-events/route.ts
Función: Guarda eventos en la base de datos PostgreSQL.
Parámetros de Query:
group(opcional): URL del grupo. Por defecto:"makers-of-murcia"limit(opcional): Número máximo de eventos a guardar. Por defecto:20
Flujo de Ejecución:
- Obtiene eventos futuros desde GraphQL
- Obtiene eventos pasados desde RSS Feed
- Combina ambos tipos de eventos
- Elimina duplicados por ID
- Borra todos los eventos existentes en la base de datos (transacción)
- Inserta nuevos eventos en la base de datos
- Devuelve confirmación con el número de eventos guardados
Código Clave:
// app/api/populate-events/route.ts
// Obtener eventos futuros desde GraphQL
const futureEvents = await fetchEventsFromGraphQL(groupUrlname, limit)
// Obtener eventos pasados desde RSS (GraphQL solo devuelve futuros)
const pastEventsFromRSS = await fetchEventsFromRSS(groupUrlname)
// Combinar eventos futuros + pasados
let events = [...futureEvents, ...pastEventsFromRSS]
// Guardar en base de datos
await prisma.$transaction(async (tx: any) => {
// Borrar todos los eventos existentes
const deleted = await tx.cachedEvent.deleteMany({})
// Insertar nuevos eventos
for (const event of eventsToSave) {
await tx.cachedEvent.create({
data: {
id: event.id,
title: event.title,
organizer: JSON.stringify(event.organizer),
image: event.image,
date: event.date,
description: event.description,
link: event.link,
attendees: event.attendees,
status: event.status,
venue: event.venue ? JSON.stringify(event.venue) : null,
time: event.time || undefined,
duration: event.duration || undefined,
}
})
}
})4. Cron Job: /api/cron/update-events-cache
Archivo: app/api/cron/update-events-cache/route.ts
Función: Actualiza automáticamente la caché de eventos periódicamente.
Configuración en Vercel (vercel.json):
{
"crons": [
{
"path": "/api/cron/update-events-cache",
"schedule": "0 0 1 * *"
}
]
}Frecuencia: El día 1 de cada mes a las 00:00 UTC
Seguridad:
- Requiere header
x-vercel-cron(automático en Vercel) - O
Authorization: Bearer {CRON_SECRET}para llamadas manuales
Flujo de Ejecución:
- Verifica autorización (header de Vercel o secret)
- Llama a
/api/eventspara obtener todos los eventos - Llama a
/api/populate-eventspara guardarlos en la base de datos - Devuelve confirmación con estadísticas
Código Clave:
// app/api/cron/update-events-cache/route.ts
// Obtener eventos desde la API unificada
const response = await fetch(
`${baseUrl}/api/events?group=makers-of-murcia&limit=200&status=all`
)
// Guardar eventos en base de datos
const populateResponse = await fetch(
`${baseUrl}/api/populate-events?group=makers-of-murcia&limit=200`,
{
headers: {
'Authorization': request.headers.get('authorization') || ''
}
}
)5. Base de Datos: Prisma Schema
Archivo: prisma/schema.prisma
Modelo: CachedEvent
model CachedEvent {
id String @id // ID del evento de Meetup
title String
organizer String // JSON string: {name, avatar, subtitle}
image String
date String
description String
link String
attendees Int
status String
venue String? // JSON string opcional: {name?, address?}
time String?
duration String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([updatedAt])
@@map("cached_events")
}Características:
- PostgreSQL como base de datos
- Campos JSON serializados como strings (
organizer,venue) - Índice en
updatedAtpara consultas ordenadas ides el ID del evento de Meetup (clave primaria)
6. Fallback: JSON Estático
Archivo: data/scraped-events.json
Función: Archivo JSON con 77 eventos pasados scrapeados previamente.
Uso: Se utiliza como fallback cuando la base de datos está vacía.
Estructura: Array de objetos ProcessedEvent
Carga: Se lee desde el sistema de archivos en tiempo de ejecución.
Frontend - Componentes
1. Componente Event (Página Raíz)
Archivo: components/event.tsx
Ubicación: Sección “Eventos anteriores” en la página raíz (/)
Función: Muestra un carrusel horizontal de eventos pasados.
Características:
- Carrusel con scroll horizontal
- Navegación con flechas (visibles desde 640px en adelante)
- Una card a la vez en mobile, múltiples en desktop
- Cards con efecto 3D al hacer hover
- Card “Ver todos” que redirige a
/events
API que consume: /api/events?limit=200&status=all
Timeout: 10 segundos en desarrollo, 5 segundos en producción
Reintento automático: Si hay timeout, reintenta con 15 segundos
Código Clave:
// components/event.tsx
const loadEvents = async () => {
try {
const fetchTimeout = isDevelopment ? 10000 : 5000
const response = await fetch('/api/events?limit=200&status=all', {
signal: AbortSignal.timeout(fetchTimeout)
})
const data = await response.json()
if (data.success && data.events && data.events.length > 0) {
setEvents(data.events)
setError(null)
}
} catch (err) {
// Reintento automático si es timeout
if (errorMessage.includes('timeout')) {
// Reintentar con timeout más largo
}
}
}Formato de Fecha: Usa formatEventDate() para mostrar fechas en formato legible (“5 nov 2022”)
2. Componente EventsArchive (Página /events)
Archivo: components/events-archive.tsx
Ubicación: Página /events
Función: Muestra todos los eventos en un grid responsive.
Características:
- Grid de 1 columna en mobile, 2 columnas en desktop
- Cards con efecto 3D al hacer hover
- Muestra contador de eventos encontrados
- Estados de carga (skeleton) y error
API que consume: /api/events?limit=200&status=all
Timeout: 5 segundos
Código Clave:
// components/events-archive.tsx
const response = await fetch('/api/events?limit=200&status=all', {
signal: AbortSignal.timeout(5000)
})
const data = await response.json()
if (data.success && data.events && data.events.length > 0) {
setEvents(data.events)
setError(null)
}Formato de Fecha: Usa formatEventDate() para mostrar fechas en formato legible
Flujo End-to-End
Flujo Completo: Desde Meetup hasta la UI
1. MEETUP.COM
│
├─ GraphQL API (eventos futuros)
│ └─→ fetchEventsFromGraphQL()
│
└─ RSS Feed (eventos pasados)
└─→ fetchEventsFromRSS()
2. API /api/populate-events
│
└─→ Guarda en PostgreSQL (Prisma)
└─→ Tabla: cached_events
3. API /api/events (unificada)
│
├─→ Lee desde GraphQL (futuros)
├─→ Lee desde BD (pasados)
└─→ Fallback a JSON (si BD vacía)
│
└─→ Combina, normaliza, ordena
└─→ Devuelve JSON unificado
4. COMPONENTES FRONTEND
│
├─→ Event.tsx (página raíz)
└─→ EventsArchive.tsx (/events)
│
└─→ Renderiza cards con eventos realesFlujo de Actualización Automática
CRON JOB (Mensual - día 1, 00:00 UTC)
│
├─→ /api/cron/update-events-cache
│ │
│ ├─→ Llama a /api/events
│ │ └─→ Obtiene eventos desde GraphQL + RSS
│ │
│ └─→ Llama a /api/populate-events
│ └─→ Guarda en base de datos
│
└─→ Base de datos actualizada
└─→ Próximas consultas usan BD en lugar de JSONActualización Automática
Cron Job Mensual
Configuración: vercel.json
{
"crons": [
{
"path": "/api/cron/update-events-cache",
"schedule": "0 0 1 * *"
}
]
}Frecuencia: El día 1 de cada mes a las 00:00 UTC
Qué hace:
- Obtiene eventos futuros desde GraphQL
- Obtiene eventos pasados desde RSS
- Guarda todo en la base de datos
- Reemplaza eventos antiguos con nuevos
Resultado: La base de datos se actualiza automáticamente cada mes con los eventos más recientes.
Crecimiento Automático
- Eventos futuros: Se agregan automáticamente cuando se crean en Meetup (GraphQL en tiempo real)
- Eventos pasados: Se agregan automáticamente cuando el cron job se ejecuta (mensualmente)
- Sin intervención humana: El sistema es completamente autónomo
Especificaciones Técnicas
Stack Tecnológico
- Backend: Next.js 14 API Routes
- Base de Datos: PostgreSQL con Prisma ORM
- APIs Externas:
- Meetup GraphQL API (
https://api.meetup.com/gql-ext) - Meetup RSS Feed (
https://www.meetup.com/{group}/events/rss/)
- Meetup GraphQL API (
- Frontend: React 18, TypeScript, Tailwind CSS
- Deployment: Vercel con Cron Jobs
Variables de Entorno
Nota: Estas variables deben configurarse en el entorno de producción (Vercel) y nunca deben exponerse públicamente.
# Opcional - mejora rate limits de Meetup API
MEETUP_API_TOKEN=<token_de_meetup>
# Requerido para cron jobs manuales (si se ejecutan fuera de Vercel)
CRON_SECRET=<secret_aleatorio_seguro>
# Base de datos PostgreSQL (URL de conexión)
DATABASE_URL=<url_de_conexion_postgresql>
# NextAuth (solo warnings, no afecta eventos)
NEXTAUTH_URL=<url_de_la_aplicacion>
NEXTAUTH_SECRET=<secret_aleatorio>
GITHUB_ID=<github_oauth_id>
GITHUB_SECRET=<github_oauth_secret>Importante:
- Estas variables se configuran en Vercel Dashboard → Settings → Environment Variables
- Nunca deben aparecer en el código fuente ni en documentación pública con valores reales
- Los valores mostrados aquí son solo ejemplos de qué variables se necesitan
Formatos de Datos
Fecha: ISO 8601 ("2022-11-05T10:00:00+01:00")
Fecha mostrada: Formato legible español ("5 nov 2022")
Status: "past" | "upcoming" | "cancelled"
Límites y Timeouts
- GraphQL: Timeout de 5 segundos
- RSS: Sin timeout específico (usa fetch default)
- Frontend fetch: 10 segundos en desarrollo, 5 en producción
- Reintento: 15 segundos si hay timeout
- Límite de eventos: Por defecto todos (sin límite), máximo 200 por fuente
Orden de Prioridad
- Eventos futuros: GraphQL (siempre la fuente más actualizada)
- Eventos pasados: Base de datos (actualizada automáticamente)
- Fallback: JSON estático (solo si BD vacía)
Eliminación de Duplicados
- Se eliminan eventos con el mismo
id - Se prioriza GraphQL sobre JSON/BD cuando hay duplicados
- Se mantiene el primero encontrado, se descarta el segundo
Páginas y Componentes que Usan Eventos
1. Página Raíz (/)
Archivo: app/page.tsx
Componente usado: <Event /> desde components/event.tsx
Sección: “Eventos anteriores”
Datos: Eventos pasados en formato carrusel horizontal
API: /api/events?limit=200&status=all
Características:
- Carrusel con scroll horizontal
- Una card a la vez en mobile, múltiples en desktop
- Navegación con flechas (visibles desde 640px)
- Card “Ver todos” que redirige a
/events
Código de integración:
// app/page.tsx
import Event from "@/components/event"
export default function Home() {
return (
<>
<Header currentPath="/" />
<Hero />
<main>
<Event /> {/* Sección "Eventos anteriores" */}
<Testimonials />
<ContactForm />
</main>
</>
)
}2. Página /events
Archivo: app/events/page.tsx
Componente usado: <EventsArchive /> desde components/events-archive.tsx
Datos: Todos los eventos en grid responsive
API: /api/events?limit=200&status=all
Características:
- Grid de 1 columna en mobile, 2 columnas en desktop
- Cards con efecto 3D al hacer hover
- Contador de eventos encontrados
- Estados de carga y error
Código de integración:
// app/events/page.tsx
import EventsArchive from "@/components/events-archive"
export default function EventsPage() {
return (
<Layout currentPath="/events">
<section>
<EventsArchive />
</section>
</Layout>
)
}Componentes Frontend - Detalles Técnicos
EventCard (Página Raíz)
Estructura de la Card:
- Altura total: 226px (186px imagen + 40px barra de título)
- Imagen: 186px de alto, full width
- Overlay: Gradiente negro semitransparente con fecha y asistentes
- Barra inferior: 40px de alto con título del evento
- Efecto 3D: Aplicado con
use3DCardEffecthook
Datos mostrados:
- Fecha formateada:
formatEventDate(event.date)→ “5 nov 2022” - Asistentes:
👥 {event.attendees} - Título: Truncado con ellipsis si es muy largo
Responsive:
- Mobile:
w-full(una card a la vez, full width) - Desktop:
md:w-[292px](múltiples cards visibles)
EventCardArchive (Página /events)
Estructura de la Card:
- Altura total: 208px (168px imagen + 40px barra de título)
- Grid: 1 columna en mobile, 2 columnas en desktop
- Click: Abre el link del evento en nueva pestaña
Datos mostrados:
- Mismo formato que EventCard
- Sin efecto carrusel, solo grid estático
Funciones de Utilidad
formatEventDate()
Archivo: lib/date-utils.ts
Función: Convierte fecha ISO a formato legible en español.
Input: "2022-11-05T10:00:00+01:00"
Output: "5 nov 2022"
Código:
// lib/date-utils.ts
export function formatEventDate(dateISO: string): string {
const date = new Date(dateISO)
return date.toLocaleDateString('es-ES', {
day: 'numeric',
month: 'short',
year: 'numeric'
})
}Flujos de Datos Detallados
Flujo 1: Carga Inicial de Eventos (Frontend)
Usuario visita página
│
▼
Componente monta (useEffect)
│
▼
fetch('/api/events?limit=200&status=all')
│
├─ Timeout: 10s (dev) / 5s (prod)
│
▼
API /api/events ejecuta:
│
├─→ fetchEventsFromGraphQL() [paralelo]
├─→ getPastEventsFromCache() [paralelo]
└─→ loadPastEventsFromJSON() [paralelo]
│
▼
Combina y normaliza eventos
│
▼
Devuelve JSON con eventos
│
▼
Componente recibe eventos
│
▼
setEvents(data.events)
│
▼
Renderiza cards con eventos realesFlujo 2: Actualización Automática (Cron)
Vercel Cron (día 1, 00:00 UTC)
│
▼
GET /api/cron/update-events-cache
│
├─ Verifica autorización (x-vercel-cron header)
│
▼
Llama a /api/events
│
├─→ Obtiene eventos futuros (GraphQL)
└─→ Obtiene eventos pasados (RSS)
│
▼
Llama a /api/populate-events
│
├─→ Borra eventos antiguos de BD
└─→ Inserta nuevos eventos en BD
│
▼
Base de datos actualizada
│
▼
Próximas consultas usan BD en lugar de JSONOrden de Prioridad de Fuentes
Cuando /api/events se ejecuta, sigue este orden:
- Eventos futuros: Siempre desde GraphQL (más actualizado)
- Eventos pasados:
- Primero intenta base de datos (si tiene eventos)
- Si BD vacía, usa JSON estático como fallback
- Combinación: Une ambos tipos, elimina duplicados, ordena
Ejemplo:
GraphQL devuelve: 2 eventos futuros
BD devuelve: 50 eventos pasados
JSON tiene: 77 eventos pasados
Resultado: 2 futuros + 50 pasados (de BD) = 52 eventos totalesNormalización de Datos
Normalización de Fechas
Todas las fechas se convierten a formato ISO:
// Input: "5 nov 2022" o "2022-11-05"
// Output: "2022-11-05T10:00:00+01:00"
function normalizeEvent(event: ProcessedEvent): ProcessedEvent {
let dateISO = event.date
if (!event.date.includes('T') && !event.date.includes('Z')) {
const parsedDate = new Date(event.date)
if (!isNaN(parsedDate.getTime())) {
dateISO = parsedDate.toISOString()
}
}
return { ...event, date: dateISO }
}Determinación de Status
El status se determina automáticamente si no está definido:
let status = event.status
if (!status) {
const eventDate = new Date(dateISO)
status = eventDate < new Date() ? 'past' : 'upcoming'
}Manejo de Errores
En la API
- GraphQL falla: Devuelve array vacío, continúa con otras fuentes
- BD falla: Usa JSON como fallback automáticamente
- JSON falla: Devuelve array vacío, pero no rompe la API
- Error general: Devuelve
{ success: false, events: [] }con status 500
En el Frontend
- Timeout: Reintenta automáticamente con timeout más largo (15s)
- Error HTTP: Muestra mensaje de error, no datos mockup
- Sin eventos: Muestra “No hay eventos disponibles”
- Carga: Muestra skeleton mientras carga
Resumen Ejecutivo
El sistema de eventos es completamente autónomo y funciona de la siguiente manera:
- Obtención: GraphQL (futuros) + RSS (pasados)
- Almacenamiento: PostgreSQL con Prisma
- Actualización: Cron job mensual automático
- Visualización: Componentes React que consumen API unificada
- Fallback: JSON estático si la BD está vacía
No requiere intervención humana - los eventos se actualizan automáticamente cada mes y los futuros se obtienen en tiempo real.
Puntos Clave
- ✅ Sistema autónomo: Sin intervención humana necesaria
- ✅ Mismo comportamiento en local y producción: Usa las mismas APIs y fuentes
- ✅ Solo eventos reales: No hay datos mockup en producción
- ✅ Actualización automática: Cron job mensual actualiza la BD
- ✅ Crecimiento automático: Los eventos se agregan automáticamente
- ✅ Fallback robusto: Si una fuente falla, usa otra automáticamente