diff --git a/.github/ISSUE_TEMPLATE/cfs.yml b/.github/ISSUE_TEMPLATE/cfs.yml
new file mode 100644
index 0000000..7605fe9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/cfs.yml
@@ -0,0 +1,64 @@
+name: Agregar Call for Speakers
+description: Proponer una nueva convocatoria de ponentes (CFS) para la comunidad tech en Perú
+title: "[CFS] "
+labels: []
+body:
+ - type: markdown
+ attributes:
+ value: |
+ ¡Gracias por querer compartir un Call for Speakers en peruanos.dev!
+ Por favor completa la siguiente información sobre la convocatoria.
+
+ - type: input
+ id: title
+ attributes:
+ label: Título de la convocatoria / evento
+ description: Nombre o título de la convocatoria para speakers
+ placeholder: 'ej. DevOpsDays Lima 2025 - Call for Speakers'
+ validations:
+ required: true
+
+ - type: input
+ id: community
+ attributes:
+ label: Comunidad o evento organizador
+ description: Nombre de la comunidad u organización que convoca
+ placeholder: 'ej. React Lima'
+ validations:
+ required: true
+
+ - type: input
+ id: cfs_url
+ attributes:
+ label: Link para postular (CFS URL)
+ description: URL del formulario o plataforma para enviar propuestas (ej. Sessionize, Google Forms)
+ placeholder: 'ej. https://sessionize.com/... o https://forms.gle/...'
+ validations:
+ required: true
+
+ - type: input
+ id: deadline
+ attributes:
+ label: Fecha límite de postulación (Deadline)
+ description: Fecha límite para enviar propuestas (YYYY-MM-DD) o indicar si es continua/abierta todo el año
+ placeholder: 'ej. 2025-06-30 o Abierto todo el año'
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Descripción de la convocatoria
+ description: Resumen de los temas buscados, formato de charlas, etc.
+ placeholder: 'Buscamos charlas de 30 minutos sobre...'
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Confirmación
+ description: Por favor confirma lo siguiente
+ options:
+ - label: He verificado que la convocatoria no esté duplicada
+ required: true
+ - label: La información proporcionada es correcta y está actualizada
+ required: true
+ - label: Tengo permiso o represento a la comunidad convocante
+ required: true
diff --git a/app/api/cfs/route.ts b/app/api/cfs/route.ts
new file mode 100644
index 0000000..539b377
--- /dev/null
+++ b/app/api/cfs/route.ts
@@ -0,0 +1,56 @@
+import { NextResponse } from 'next/server';
+import { CFS_LIST } from '@/app/data/cfs';
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(request: Request) {
+ try {
+ const { searchParams } = new URL(request.url);
+ const topic = searchParams.get('topic')?.toLowerCase();
+ const type = searchParams.get('type')?.toLowerCase();
+ const status = searchParams.get('status')?.toLowerCase();
+
+ let filtered = CFS_LIST;
+
+ if (topic) {
+ filtered = filtered.filter((item) =>
+ item.topics.some((t) => t.toLowerCase() === topic)
+ );
+ }
+
+ if (type) {
+ filtered = filtered.filter(
+ (item) => item.type?.toLowerCase() === type
+ );
+ }
+
+ if (status) {
+ filtered = filtered.filter(
+ (item) => item.status?.toLowerCase() === status
+ );
+ }
+
+ filtered = [...filtered].sort((a, b) => {
+ const aClosed = a.status === 'closed';
+ const bClosed = b.status === 'closed';
+ if (!aClosed && bClosed) return -1;
+ if (aClosed && !bClosed) return 1;
+
+ if (a.deadline && b.deadline) {
+ return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
+ }
+ if (a.deadline && !b.deadline) return -1;
+ if (!a.deadline && b.deadline) return 1;
+
+ return 0;
+ });
+
+ return NextResponse.json(filtered, { status: 200 });
+ } catch (error) {
+ console.error("Error al obtener los Call for Speakers:", error);
+ return NextResponse.json(
+ { error: "Error al obtener los Call for Speakers" },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/apis/ApiDocsViewer.tsx b/app/apis/ApiDocsViewer.tsx
index 9b3707c..71cc5d3 100644
--- a/app/apis/ApiDocsViewer.tsx
+++ b/app/apis/ApiDocsViewer.tsx
@@ -60,6 +60,27 @@ const ENDPOINTS = [
"stars": 128
},
// ...
+]`
+ },
+ {
+ id: "cfs",
+ method: "GET",
+ path: "/api/cfs",
+ title: "/api/cfs",
+ description: "Obtiene el directorio de convocatorias para ponentes (Call for Speakers) abiertas y continuas en Perú.",
+ filters: ["topic=devops", "type=presencial", "status=open"],
+ fetchCode: "fetch('https://peruanos.dev/api/cfs?topic=devops&status=open')\n .then(res => res.json())\n .then(data => console.log(data))",
+ jsonSnippet: `data = [
+ {
+ "id": "cfs-devopsdays-lima-2025",
+ "title": "DevOpsDays Lima 2025 - Call for Speakers",
+ "community": "DevOpsDays Lima",
+ "deadline": "2025-06-15",
+ "cfs_url": "https://sessionize.com/devopsdays-lima-2025",
+ "type": "Presencial",
+ "topics": ["DevOps", "SRE", "Cloud Native"]
+ },
+ // ...
]`
}
];
diff --git a/app/cfs/page.tsx b/app/cfs/page.tsx
new file mode 100644
index 0000000..dc374d4
--- /dev/null
+++ b/app/cfs/page.tsx
@@ -0,0 +1,58 @@
+import Link from 'next/link';
+import type { Metadata } from 'next';
+import CfsClient from '../components/cfs/CfsClient';
+import { addUTMParams } from '../lib/utm';
+
+export const metadata: Metadata = {
+ title: 'Call for Speakers | Convocatorias para Speakers Tech en Perú',
+ description: 'Directorio centralizado de Call for Speakers (CFS) y convocatorias de ponentes para eventos, conferencias y meetups de la comunidad tech en Perú.',
+ keywords: ['call for speakers', 'cfs', 'speakers peru', 'conferencias tech', 'meetups peru', 'charlas tecnologia', 'comunidad tech', 'desarrolladores peru'],
+ authors: [{ name: 'Luis Eduardo', url: 'https://lperezp.dev' }],
+ openGraph: {
+ title: 'Call for Speakers | Convocatorias Tech en Perú',
+ description: 'Encuentra oportunidades para dar charlas y workshops en eventos y comunidades tecnológicas de Perú.',
+ url: 'https://peruanos.dev/cfs',
+ images: [
+ {
+ url: 'https://peruanos.dev/images/og-image.png',
+ }
+ ],
+ siteName: 'Peruanos.dev',
+ locale: 'es_PE',
+ type: 'website',
+ },
+ twitter: {
+ card: 'summary_large_image',
+ title: 'Call for Speakers | Convocatorias Tech en Perú',
+ description: 'Encuentra oportunidades para dar charlas y workshops en eventos y comunidades tecnológicas de Perú.',
+ images: 'https://peruanos.dev/images/og-image.png',
+ creator: '@lperezp_pe',
+ },
+};
+
+export default function CfsPage() {
+ return (
+
+
+
+ Call for Speakers
+
+
+ Directorio centralizado de convocatorias para speakers en el Perú. Si deseas compartir tus conocimientos, experiencias o proyectos con la comunidad tech, explora las convocatorias abiertas y postula a tu próximo evento o meetup.
+
+
+
+ Publicar un Call for Speakers
+
+
+
+
+
+
+ );
+}
diff --git a/app/components/cfs/CfsCard.tsx b/app/components/cfs/CfsCard.tsx
new file mode 100644
index 0000000..7c35223
--- /dev/null
+++ b/app/components/cfs/CfsCard.tsx
@@ -0,0 +1,213 @@
+'use client';
+
+import {
+ AlarmClock,
+ Calendar,
+ Clock,
+ ExternalLink,
+ Globe,
+ MapPin,
+ Users,
+ Video,
+} from 'lucide-react';
+import { ICFS } from '../../models/cfs.model';
+import Badge from '../ui/Badge';
+import ShareButton from '../ui/ShareButton';
+import TrackedLink from '../ui/TrackedLink';
+import { addUTMParams } from '../../lib/utm';
+
+interface Props {
+ cfs: ICFS;
+}
+
+export default function CfsCard({ cfs }: Props) {
+ // Format event date (e.g. "Sáb, 14 Nov 2026")
+ let formattedEventDate = '';
+ if (cfs.event_date) {
+ const eDate = new Date(cfs.event_date);
+ if (!isNaN(eDate.getTime())) {
+ const weekday = eDate.toLocaleDateString('es-PE', { weekday: 'short', timeZone: 'UTC' });
+ const day = eDate.getUTCDate();
+ const month = eDate.toLocaleDateString('es-PE', { month: 'short', timeZone: 'UTC' });
+ const year = eDate.getUTCFullYear();
+ const capWeekday = weekday.charAt(0).toUpperCase() + weekday.slice(1).replace('.', '');
+ const capMonth = month.charAt(0).toUpperCase() + month.slice(1).replace('.', '');
+ formattedEventDate = `${capWeekday}, ${day} ${capMonth} ${year}`;
+ }
+ }
+
+ // Format deadline text
+ let deadlineText = 'Convocatoria continua';
+ if (cfs.deadline) {
+ const deadlineDate = new Date(cfs.deadline);
+ if (!isNaN(deadlineDate.getTime())) {
+ const day = deadlineDate.getUTCDate();
+ const month = deadlineDate.toLocaleString('es-PE', { month: 'long', timeZone: 'UTC' });
+ const year = deadlineDate.getUTCFullYear();
+ deadlineText = `${day} de ${month}, ${year}`;
+ } else {
+ deadlineText = cfs.deadline;
+ }
+ }
+
+ // Format clean website URL and display hostname
+ const rawWebsite = cfs.website_url || cfs.cfs_url;
+ let displayWebsite = '';
+ let cleanWebsiteUrl = rawWebsite;
+ if (rawWebsite) {
+ try {
+ const parsed = new URL(rawWebsite);
+ displayWebsite =
+ parsed.hostname.replace(/^www\./, '') +
+ (parsed.pathname !== '/' && !cfs.website_url ? parsed.pathname : '');
+ cleanWebsiteUrl = rawWebsite;
+ } catch {
+ displayWebsite = rawWebsite.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
+ }
+ }
+
+ const isClosed = cfs.status === 'closed';
+
+ return (
+
+
+ {/* Top Badges Row (Sessionize layout styled strictly with app tokens) */}
+