Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
391 changes: 368 additions & 23 deletions src/components/ProgramaPage.astro

Large diffs are not rendered by default.

156 changes: 156 additions & 0 deletions src/components/ScheduleList.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
import type { Schedule } from '@/types/schedule'
import { formatDayLabel, formatTime } from '@/lib/schedule'
import { menuTexts } from '@/i18n/menu'
import { programaTexts } from '@/i18n/programa'

interface Props {
schedule: Schedule
lang: string
}

const { schedule, lang } = Astro.props
const t = programaTexts[lang as keyof typeof programaTexts]
const menuT = menuTexts[lang as keyof typeof menuTexts]
---

<div class="schedule-list">
{
schedule.days.map((day) => (
<section class="mb-12" aria-labelledby={`day-${day.index}-heading`}>
<h2
id={`day-${day.index}-heading`}
class="text-2xl md:text-3xl font-black text-white mb-6 flex items-baseline gap-3"
>
<span class="text-pycon-orange">{t.list.dayN(day.index)}</span>
<span class="text-base md:text-lg font-normal text-pycon-gray-25 capitalize">
{formatDayLabel(day.date, lang, schedule.timezone)}
</span>
</h2>
<ol class="flex flex-col gap-4 list-none m-0 p-0">
{day.sessions.map((s) => (
<li class="bg-pycon-black/40 rounded-2xl border border-white/5 p-5 md:p-6 transition-colors hover:border-pycon-orange/40 focus-within:border-pycon-orange/60">
<article>
<header class="flex flex-wrap items-baseline gap-x-4 gap-y-1 mb-3">
<p class="text-sm font-mono text-pycon-yellow shrink-0">
<span class="sr-only">{t.list.startTime} </span>
<time datetime={s.date}>{formatTime(s.date, schedule.timezone)}</time>
<span aria-hidden="true">–</span>
<span class="sr-only"> {t.list.endTime} </span>
<time datetime={s.end}>{formatTime(s.end, schedule.timezone)}</time>
</p>
<span class="text-xs text-pycon-gray-25/70 shrink-0">
<span class="sr-only">{t.list.duration}: </span>
{s.duration}
</span>
{s.track && (
<span class="text-xs px-2 py-0.5 rounded-full bg-pycon-orange/15 text-pycon-orange border border-pycon-orange/30 shrink-0">
{s.track}
</span>
)}
{s.type && <span class="text-xs text-pycon-gray-25/70 shrink-0">{s.type}</span>}
</header>

<h3 class="text-lg md:text-xl font-bold text-white mb-2 leading-snug">{s.title}</h3>

{s.subtitle && <p class="text-sm text-pycon-gray-25 mb-3 italic">{s.subtitle}</p>}

{s.persons.length > 0 && (
<p class="text-sm text-pycon-gray-25 mb-3">
<span class="font-semibold text-white">{t.list.by}: </span>
{s.persons.map((p, i) => {
const sep =
i === 0
? ''
: i === s.persons.length - 1
? ' & '
: s.persons.length === 2
? ' & '
: ', '
return (
<>
{sep}
<span class="text-white">{p.public_name || p.name}</span>
</>
)
})}
</p>
)}

<dl class="flex flex-wrap gap-x-6 gap-y-1 text-xs text-pycon-gray-25/80 mb-4">
<div class="flex items-baseline gap-1.5">
<dt class="font-semibold text-white">{t.list.room}:</dt>
<dd>{s.room}</dd>
</div>
{s.language && (
<div class="flex items-baseline gap-1.5">
<dt class="font-semibold text-white">{t.list.language}:</dt>
<dd>
<span lang={s.language}>{s.language.toUpperCase()}</span>
</dd>
</div>
)}
</dl>

{s.abstract && (
<details class="group mb-3">
<summary class="cursor-pointer text-sm text-pycon-yellow hover:text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-yellow rounded inline-flex items-center gap-1.5 select-none list-none [&::-webkit-details-marker]:hidden">
<svg
class="w-3.5 h-3.5 transition-transform group-open:rotate-90"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 5l7 7-7 7"
/>
</svg>
<span class="group-open:hidden">{t.list.showAbstract}</span>
<span class="hidden group-open:inline">{t.list.hideAbstract}</span>
</summary>
<p
lang={s.language === 'en' ? 'en' : s.language === 'ca' ? 'ca' : 'es'}
class="mt-3 text-sm text-pycon-gray-25 leading-relaxed whitespace-pre-line"
>
{s.abstract}
</p>
</details>
)}

{s.url && (
<a
href={s.url}
target="_blank"
rel="noopener noreferrer"
aria-label={`${s.title} — ${t.list.talkLink} ${menuT.new_tab}`}
class="inline-flex items-center gap-1.5 text-sm text-pycon-yellow hover:text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-yellow rounded"
>
<span>{t.list.talkLink}</span>
<svg
class="w-3.5 h-3.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
)}
</article>
</li>
))}
</ol>
</section>
))
}
</div>
1 change: 1 addition & 0 deletions src/i18n/menu/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ export const ca = {
href: '/past-editions',
},
],
skipToContent: 'Salta al contingut principal',
new_tab: "(s'obre en una pestanya nova)",
} as const
1 change: 1 addition & 0 deletions src/i18n/menu/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ export const en = {
href: '/past-editions',
},
],
skipToContent: 'Skip to main content',
new_tab: '(opens in new tab)',
} as const
1 change: 1 addition & 0 deletions src/i18n/menu/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ export const es = {
href: '/past-editions',
},
],
skipToContent: 'Saltar al contenido principal',
new_tab: '(se abre en nueva pestaña)',
} as const
28 changes: 28 additions & 0 deletions src/i18n/programa/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,32 @@ export const ca = {
locale: 'ca',
noscript: 'JavaScript està desactivat al teu navegador. Per veure el programa sense JavaScript,',
noscriptLink: 'fes clic aquí',
a11y: {
statusTemplate: (count: number) => `Mostrant ${count} ${count === 1 ? 'sessió' : 'sessions'}`,
statusEmpty: 'No hi ha sessions que coincideixin amb els filtres',
searchLabel: 'Cerca sessions',
timezoneLabel: 'Zona horària',
closeFilters: 'Tanca els filtres',
},
list: {
viewModeLegend: 'Mode de visualització del programa',
viewModeList: 'Llista accessible',
viewModeGrid: 'Graella visual',
dayN: (n: number) => `Dia ${n}`,
speakers: (count: number) => (count === 1 ? 'Ponent' : 'Ponents'),
by: 'Per',
room: 'Sala',
track: 'Track',
type: 'Tipus',
language: 'Idioma',
duration: 'Durada',
startTime: 'De',
endTime: 'a',
showAbstract: 'Mostra la descripció',
hideAbstract: 'Amaga la descripció',
talkLink: 'Veure detalls de la xerrada a Pretalx',
emptyTitle: 'El horari encara no està disponible',
emptyBody: 'Estem acabant de preparar el cronograma. Torna a visitar-nos els propers dies.',
emptyCta: 'Consulta el horari a Pretalx',
},
} as const
28 changes: 28 additions & 0 deletions src/i18n/programa/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,32 @@ export const en = {
locale: 'en',
noscript: 'JavaScript is disabled in your browser. To view the schedule without JavaScript,',
noscriptLink: 'click here',
a11y: {
statusTemplate: (count: number) => `Showing ${count} ${count === 1 ? 'session' : 'sessions'}`,
statusEmpty: 'No sessions match the filters',
searchLabel: 'Search sessions',
timezoneLabel: 'Timezone',
closeFilters: 'Close filters',
},
list: {
viewModeLegend: 'Schedule view mode',
viewModeList: 'Accessible list',
viewModeGrid: 'Visual grid',
dayN: (n: number) => `Day ${n}`,
speakers: (count: number) => (count === 1 ? 'Speaker' : 'Speakers'),
by: 'By',
room: 'Room',
track: 'Track',
type: 'Type',
language: 'Language',
duration: 'Duration',
startTime: 'From',
endTime: 'to',
showAbstract: 'Show description',
hideAbstract: 'Hide description',
talkLink: 'View talk details on Pretalx',
emptyTitle: 'The schedule is not available yet',
emptyBody: "We're still putting the programme together. Check back in a few days.",
emptyCta: 'See the schedule on Pretalx',
},
} as const
28 changes: 28 additions & 0 deletions src/i18n/programa/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,32 @@ export const es = {
locale: 'es',
noscript: 'JavaScript está deshabilitado en tu navegador. Para ver el programa sin JavaScript,',
noscriptLink: 'haz click aquí',
a11y: {
statusTemplate: (count: number) => `Mostrando ${count} ${count === 1 ? 'sesión' : 'sesiones'}`,
statusEmpty: 'No hay sesiones que coincidan con los filtros',
searchLabel: 'Buscar sesiones',
timezoneLabel: 'Zona horaria',
closeFilters: 'Cerrar filtros',
},
list: {
viewModeLegend: 'Modo de visualización del programa',
viewModeList: 'Lista accesible',
viewModeGrid: 'Grilla visual',
dayN: (n: number) => `Día ${n}`,
speakers: (count: number) => (count === 1 ? 'Ponente' : 'Ponentes'),
by: 'Por',
room: 'Sala',
track: 'Track',
type: 'Tipo',
language: 'Idioma',
duration: 'Duración',
startTime: 'De',
endTime: 'a',
showAbstract: 'Ver descripción',
hideAbstract: 'Ocultar descripción',
talkLink: 'Ver detalles de la charla en Pretalx',
emptyTitle: 'El horario todavía no está disponible',
emptyBody: 'Estamos terminando de armar el cronograma. Vuelve a visitarnos en los próximos días.',
emptyCta: 'Consultar el cronograma en Pretalx',
},
} as const
39 changes: 39 additions & 0 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import '@/style/global.css'
import '@fontsource-variable/jetbrains-mono'
import '@fontsource-variable/outfit'
import { ClientRouter } from 'astro:transitions'
import { menuTexts } from '@/i18n/menu'

interface Props {
title: string
Expand All @@ -13,6 +14,7 @@ interface Props {
}
const { lang = 'es' } = Astro.params
const { title, description = 'PyconES 2026', fullWidth = false } = Astro.props
const skipT = menuTexts[lang as keyof typeof menuTexts].skipToContent

const siteUrl = Astro.site ? Astro.site.origin : 'https://2026.es.pycon.org'
const canonicalURL = new URL(Astro.url.pathname, siteUrl)
Expand Down Expand Up @@ -115,6 +117,7 @@ const alternates = [
<body
class="bg-pycon-black font-outfit text-pycon-white antialiased min-h-screen relative flex flex-col transition-colors duration-300"
>
<a href="#main-content" class="skip-link">{skipT}</a>
<div
class="fixed inset-0 bg-no-repeat bg-size-[80vh] bg-position-[140%_120%] opacity-5 pointer-events-none blur transition-all duration-300"
style="background-image: var(--bg-symbol);"
Expand All @@ -123,6 +126,7 @@ const alternates = [
<Header lang={lang as string} />

<main
id="main-content"
class:list={[
'relative grow z-10 flex flex-col justify-center',
fullWidth ? 'px-4 md:px-8' : 'container mx-auto p-4 md:p-8',
Expand Down Expand Up @@ -162,4 +166,39 @@ const alternates = [
::-webkit-scrollbar-thumb:hover {
background: #8e8e8d;
}

/* Skip link (visible on focus) */
.skip-link {
position: absolute;
top: -100px;
left: 1rem;
z-index: 1000;
padding: 0.75rem 1rem;
background: #fff;
color: #03004b;
font-weight: 600;
text-decoration: none;
border-radius: 0.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
transition: top 0.2s ease;
}
.skip-link:focus,
.skip-link:focus-visible {
top: 1rem;
outline: 2px solid #03004b;
outline-offset: 2px;
}

/* Screen reader only utility */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
Loading
Loading