Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/avatar/gladys-agudelo-patino.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/avatar/gustavo-diaz-jaimes.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/assets/data/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,7 @@ export const scheduleEvents: ScheduleEvent[] = [
date: "2026-07-26",
title: "Sponsor Space: Aimpoint",
displayTitle: "Sponsor Space: Aimpoint",
speaker: "Aimpoint",
speaker: "Felipe Sánchez",
label: "Sponsor Space",
},
{
Expand Down
86 changes: 61 additions & 25 deletions src/assets/data/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,40 @@ export const teamMembers: TeamMember[] = [

export const volunteerMembers: VolunteerMember[] = [
{
slug: "valerie-arismendy",
name: "Valerie Arismendy",
role: "Co-host de auditorio",
image: "/images/avatar/valerie-arismendy.webp",
linkedin: "https://www.linkedin.com/in/valerie-arismendy/",
slug: "alejandro-alvarez",
name: "Alejandro Alvarez",
role: "Redes Sociales",
image: "/images/avatar/alejandro-alvarez.jpg",
},
{
slug: "blanca-arcilia-florez-urrutia",
name: "Blanca Arcilia Flórez Urrutia",
role: "Logistics",
image: "/images/avatar/blanca-arcilia-florez-urrutia.jpeg",
},
{
slug: "carolina-monsalve-delgado",
name: "Carolina Monsalve Delgado",
role: "Logistics",
image: "/images/avatar/carolina-monsalve-delgado.jpeg",
},
{
slug: "gladys-agudelo-patino",
name: "Gladys Agudelo Patiño",
role: "Logistics",
image: "/images/avatar/gladys-agudelo-patino.jpeg",
},
{
slug: "gustavo-diaz-jaimes",
name: "Gustavo Diaz Jaimes",
role: "Auditorium",
image: "/images/avatar/gustavo-diaz-jaimes.jpg",
},
{
slug: "jenny-acuna",
name: "Jenny Acuña",
role: "Logística",
image: "/images/avatar/jenny-acuna.jpeg",
},
{
slug: "jesus-mena",
Expand All @@ -118,18 +147,10 @@ export const volunteerMembers: VolunteerMember[] = [
linkedin: "https://www.linkedin.com/in/yisuslinkon",
},
{
slug: "yurley-sanchez-florez",
name: "Yurley Katterine Sanchez Florez",
role: "Co-host de auditorio",
image: "/images/avatar/yurley-sanchez.jpg",
linkedin: "https://www.linkedin.com/in/yursksf1/",
},
{
slug: "veruzka-borges",
name: "Veruzka Borges",
role: "Co-host de auditorio",
image: "/images/avatar/veruzka-borges.png",
linkedin: "https://www.linkedin.com/in/veruzkab/",
slug: "jose-angel-botero-cuervo",
name: "José Ángel Botero Cuervo",
role: "Logistics",
image: "/images/avatar/jose-angel-botero-cuervo.jpeg",
},
{
slug: "juan-camilo-velasquez",
Expand All @@ -147,15 +168,30 @@ export const volunteerMembers: VolunteerMember[] = [
linkedin: "https://www.linkedin.com/in/mateousmevalencia/",
},
{
slug: "alejandro-alvarez",
name: "Alejandro Alvarez",
role: "Redes Sociales",
image: "/images/avatar/alejandro-alvarez.jpg",
slug: "simon-joel-torres-castaneda",
name: "Simón Joel Torres Castañeda",
role: "Logistics",
image: "/images/avatar/simon-joel-torres-castaneda.jpeg",
},
{
slug: "jenny-acuna",
name: "Jenny Acuña",
role: "Logística",
image: "/images/avatar/jenny-acuna.jpeg",
slug: "valerie-arismendy",
name: "Valerie Arismendy",
role: "Co-host de auditorio",
image: "/images/avatar/valerie-arismendy.webp",
linkedin: "https://www.linkedin.com/in/valerie-arismendy/",
},
{
slug: "veruzka-borges",
name: "Veruzka Borges",
role: "Co-host de auditorio",
image: "/images/avatar/veruzka-borges.png",
linkedin: "https://www.linkedin.com/in/veruzkab/",
},
{
slug: "yurley-sanchez-florez",
name: "Yurley Katterine Sanchez Florez",
role: "Co-host de auditorio",
image: "/images/avatar/yurley-sanchez.jpg",
linkedin: "https://www.linkedin.com/in/yursksf1/",
},
];
2 changes: 1 addition & 1 deletion src/assets/data/workshop-speakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ const workshopSpeakerListBase: SpeakerInput[] = [
{
slug: "felipe-sanchez",
talkKey: "fastmcp-pokedex",
name: "Felipe Sanchez",
name: "Felipe Sánchez",
linkedin: "https://www.linkedin.com/in/felipe-sánchez-7765b8206",
tracks: ["artificial-intelligence", "core-python"] as SpeakerTrack[],
country: "Colombia",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";

import {
type DaySchedule,
Expand Down Expand Up @@ -54,6 +54,11 @@ import {
isSponsorSpaceEvent,
resolveSponsorForScheduleEvent,
} from "@/lib/schedule-sponsors";
import {
getScheduleDateTime,
isScheduleEventNow,
parseScheduleNowOverride,
} from "@/lib/schedule-time";
import { resolveSpeakerImageUrl } from "@/lib/speaker-image-url";
import type { SponsorWithTier } from "@/lib/sponsors";
import { getTalkHref } from "@/lib/talks";
Expand Down Expand Up @@ -133,6 +138,7 @@ type ScheduleEventCardProps = {
event: ScheduleEvent;
t: (key: string) => string;
locale: "en" | "es";
isNow: boolean;
};

type ScheduleSpeakerRowProps = {
Expand Down Expand Up @@ -223,7 +229,12 @@ function ScheduleSponsorRow({ sponsor }: { sponsor: SponsorWithTier }) {
);
}

function ScheduleEventCard({ event, t, locale }: ScheduleEventCardProps) {
function ScheduleEventCard({
event,
t,
locale,
isNow,
}: ScheduleEventCardProps) {
const category = getScheduleEventCategory(event);
const sponsor = resolveSponsorForScheduleEvent(event);
const keynote = event.keynoteSlug
Expand Down Expand Up @@ -274,7 +285,18 @@ function ScheduleEventCard({ event, t, locale }: ScheduleEventCardProps) {
const eventTitle = getLocalizedScheduleEventTitle(event, locale);

return (
<Card className="h-full transition-all">
<Card
className={cn(
"relative h-full overflow-hidden transition-all",
isNow &&
"border-primary/70 bg-primary/10 ring-primary/20 shadow-md ring-2 dark:bg-primary/15",
)}
>
{isNow ? (
<div className="bg-primary text-primary-foreground absolute top-4 -right-8 z-20 w-28 rotate-45 py-1 text-center text-xs font-semibold tracking-wide uppercase shadow-sm">
{t("blocks.scheduleUi.now")}
</div>
) : null}
<CardContent className="space-y-2.5">
<div className="mb-1 flex items-start justify-between gap-2.5 max-sm:flex-col">
<div>
Expand Down Expand Up @@ -353,12 +375,51 @@ function ScheduleEventCard({ event, t, locale }: ScheduleEventCardProps) {
const ScheduleCard = ({ scheduleData }: ScheduleCardProps) => {
const { locale } = useLanguage();
const { t } = useTranslations();
const [scheduleNow, setScheduleNow] = useState<Date>();
const [selectedDate, setSelectedDate] = useState<ScheduleDayDate>(
scheduleDays[0].date,
);
const [activeTab, setActiveTab] = useState<ScheduleTab>("all");
const [searchQuery, setSearchQuery] = useState("");
const [sortBy, setSortBy] = useState<"time" | "name" | "default">("default");
const currentScheduleDate = scheduleNow
? getScheduleDateTime(scheduleNow).date
: undefined;

useEffect(() => {
const queryOverride = new URLSearchParams(window.location.search).get(
"scheduleNow",
);
const override = parseScheduleNowOverride(
queryOverride ?? process.env.NEXT_PUBLIC_SCHEDULE_NOW,
);

if (override) {
setScheduleNow(override);
return;
}

const updateNow = () => setScheduleNow(new Date());
updateNow();

const interval = window.setInterval(updateNow, 30_000);

return () => window.clearInterval(interval);
}, []);

useEffect(() => {
if (!currentScheduleDate) {
return;
}

const matchingDay = scheduleDays.find(
(day) => day.date === currentScheduleDate,
);

if (matchingDay) {
setSelectedDate(matchingDay.date);
}
}, [currentScheduleDate]);

const selectedDay = scheduleDays.find((day) => day.date === selectedDate);

Expand Down Expand Up @@ -579,8 +640,9 @@ const ScheduleCard = ({ scheduleData }: ScheduleCardProps) => {
key={group.map((event) => event.id).join("-")}
className={cn(
"gap-4",
group.length > 1 &&
"grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
group.length === 1
? "grid sm:grid-cols-2"
: "grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3",
)}
>
{group.map((event) => (
Expand All @@ -589,6 +651,11 @@ const ScheduleCard = ({ scheduleData }: ScheduleCardProps) => {
event={event}
t={t}
locale={locale}
isNow={
scheduleNow
? isScheduleEventNow(event, scheduleNow)
: false
}
/>
))}
</div>
Expand Down
7 changes: 7 additions & 0 deletions src/lib/i18n/blocks-en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ export const blocksEn = {
"mateo-usme-valencia": "Keynotes Godfather",
"alejandro-alvarez": "Social Networks",
"jenny-acuna": "Logistics",
"blanca-arcilia-florez-urrutia": "Logistics",
"carolina-monsalve-delgado": "Logistics",
"gladys-agudelo-patino": "Logistics",
"gustavo-diaz-jaimes": "Auditorium",
"jose-angel-botero-cuervo": "Logistics",
"simon-joel-torres-castaneda": "Logistics",
},
},
},
Expand Down Expand Up @@ -389,6 +395,7 @@ export const blocksEn = {
keynote: "Keynote",
workshop: "Workshop",
other: "Event",
now: "Now",
viewSpeaker: "View speaker",
viewWorkshop: "View workshop",
viewKeynote: "View keynote",
Expand Down
7 changes: 7 additions & 0 deletions src/lib/i18n/blocks-es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ export const blocksEs: BlocksMessages = {
"mateo-usme-valencia": "Padrino Keynotes",
"alejandro-alvarez": "Redes Sociales",
"jenny-acuna": "Logística",
"blanca-arcilia-florez-urrutia": "Logística",
"carolina-monsalve-delgado": "Logística",
"gladys-agudelo-patino": "Logística",
"gustavo-diaz-jaimes": "Auditorio",
"jose-angel-botero-cuervo": "Logística",
"simon-joel-torres-castaneda": "Logística",
},
},
},
Expand Down Expand Up @@ -394,6 +400,7 @@ export const blocksEs: BlocksMessages = {
keynote: "Keynote",
workshop: "Taller",
other: "Evento",
now: "Ahora",
viewSpeaker: "Ver ponente",
viewWorkshop: "Ver taller",
viewKeynote: "Ver keynote",
Expand Down
79 changes: 79 additions & 0 deletions src/lib/schedule-time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { ScheduleEvent } from "@/assets/data/schedule";

const SCHEDULE_TIME_ZONE = "America/Bogota";

type ScheduleDateTime = {
date: string;
minutes: number;
};

function getPart(
parts: Intl.DateTimeFormatPart[],
type: Intl.DateTimeFormatPartTypes,
) {
return parts.find((part) => part.type === type)?.value ?? "";
}

export function getScheduleDateTime(now: Date): ScheduleDateTime {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: SCHEDULE_TIME_ZONE,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
}).formatToParts(now);
const hour = Number.parseInt(getPart(parts, "hour"), 10);
const minute = Number.parseInt(getPart(parts, "minute"), 10);

return {
date: `${getPart(parts, "year")}-${getPart(parts, "month")}-${getPart(parts, "day")}`,
minutes: hour * 60 + minute,
};
}

function parseHourRange(hour: string) {
const match = hour.match(/^\s*(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})\s*$/);

if (!match) {
return undefined;
}

return {
start: Number.parseInt(match[1], 10) * 60 + Number.parseInt(match[2], 10),
end: Number.parseInt(match[3], 10) * 60 + Number.parseInt(match[4], 10),
};
}

export function isScheduleEventNow(event: ScheduleEvent, now: Date) {
const range = parseHourRange(event.hour);

if (!range) {
return false;
}

const scheduleNow = getScheduleDateTime(now);

return (
event.date === scheduleNow.date &&
scheduleNow.minutes >= range.start &&
scheduleNow.minutes < range.end
);
}

export function parseScheduleNowOverride(value?: string | null) {
const trimmed = value?.trim();

if (!trimmed) {
return undefined;
}

const date = new Date(
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?$/.test(trimmed)
? `${trimmed}-05:00`
: trimmed,
);

return Number.isNaN(date.getTime()) ? undefined : date;
}
Loading