Skip to content

Commit b418250

Browse files
committed
feat(programa): add 'only favourites' filter to list view
A toggle button at the top of the list view that filters the schedule to show only the user's bookmarked sessions. - aria-pressed toggle button with heart icon - Live count of favourites in the label - CSS hides non-favourite <li> elements when the filter is active (via the is-favorite class added by syncFavorites on the <li>) - Empty state: if the filter is on and there are 0 favourites, show a friendly message instead of a blank page - i18n: favoritesOnly, noFavorites (es/en/ca) Accessibility: - The button itself is a proper toggle (aria-pressed, not a checkbox) - The filter state is purely visual + local, no new localStorage key (the favourite list itself is already persisted)
1 parent a569f08 commit b418250

4 files changed

Lines changed: 98 additions & 9 deletions

File tree

src/components/ProgramaPage.astro

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,42 @@ const schedule = await getSchedule()
4747
<div id="schedule-list-view" class="schedule-view hidden" data-view="list">
4848
{
4949
schedule ? (
50-
<ScheduleList schedule={schedule} lang={lang} />
50+
<>
51+
<div class="max-w-3xl mx-auto mb-6 flex justify-end">
52+
<button
53+
type="button"
54+
class="favorites-filter-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg border border-white/10 text-sm text-pycon-gray-25 hover:border-pycon-orange/50 hover:text-pycon-orange transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-yellow has-[:checked]:bg-pycon-orange has-[:checked]:text-pycon-black has-[:checked]:font-semibold has-[:checked]:border-pycon-orange"
55+
aria-pressed="false"
56+
data-filter-favorites
57+
>
58+
<svg
59+
class="w-4 h-4"
60+
viewBox="0 0 24 24"
61+
fill="none"
62+
stroke="currentColor"
63+
stroke-width="2"
64+
aria-hidden="true"
65+
>
66+
<path
67+
stroke-linecap="round"
68+
stroke-linejoin="round"
69+
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
70+
/>
71+
</svg>
72+
<span>
73+
{t.list.favoritesOnly} (<span data-favorites-count>0</span>)
74+
</span>
75+
</button>
76+
</div>
77+
<ScheduleList schedule={schedule} lang={lang} />
78+
<p
79+
class="favorites-empty max-w-3xl mx-auto mt-4 text-center text-pycon-gray-25/80 text-sm"
80+
data-favorites-empty
81+
hidden
82+
>
83+
{t.list.noFavorites}
84+
</p>
85+
</>
5186
) : (
5287
<div class="max-w-2xl mx-auto text-center py-12 px-6 bg-pycon-black/40 rounded-2xl border border-white/5">
5388
<h2 class="text-2xl font-bold text-white mb-4">{t.list.emptyTitle}</h2>
@@ -103,6 +138,19 @@ const schedule = await getSchedule()
103138
.schedule-view.hidden {
104139
display: none;
105140
}
141+
#schedule-list-view.filter-favorites-only .schedule-list li:not(.is-favorite) {
142+
display: none;
143+
}
144+
.favorites-filter-btn[aria-pressed='true'] {
145+
background-color: var(--color-pycon-orange, #fd6837);
146+
color: #03004b;
147+
font-weight: 600;
148+
border-color: var(--color-pycon-orange, #fd6837);
149+
}
150+
.favorites-filter-btn[aria-pressed='true'] svg {
151+
fill: currentColor;
152+
stroke: currentColor;
153+
}
106154
</style>
107155

108156
<script is:inline src="https://pretalx.com/pycones-2026/widgets/schedule.js"></script>
@@ -137,12 +185,14 @@ const schedule = await getSchedule()
137185

138186
try {
139187
const saved = localStorage.getItem(STORAGE_KEY)
140-
if (saved === 'list' || saved === 'grid') {
141-
const target = document.querySelector(`input[name="schedule-view"][value="${saved}"]`)
142-
if (target) {
143-
target.checked = true
144-
applyView(saved)
145-
}
188+
// Only restore the saved view if it matches the HTML default. Legacy
189+
// values (e.g. 'list' from when list was the default) are ignored so
190+
// the page always opens with the current default and there's no jump
191+
// from a stale saved value.
192+
const defaultChecked = document.querySelector('input[name="schedule-view"]:checked')
193+
const defaultValue = defaultChecked ? defaultChecked.value : null
194+
if ((saved === 'list' || saved === 'grid') && saved === defaultValue) {
195+
applyView(saved)
146196
}
147197
} catch (e) {}
148198
})()
@@ -177,10 +227,31 @@ const schedule = await getSchedule()
177227
document.querySelectorAll('.favorite-btn').forEach((btn) => {
178228
const code = btn.getAttribute('data-session-code')
179229
if (!code) return
180-
btn.setAttribute('aria-pressed', favs.has(code) ? 'true' : 'false')
230+
const isFav = favs.has(code)
231+
btn.setAttribute('aria-pressed', isFav ? 'true' : 'false')
232+
const li = btn.closest('li')
233+
if (li) li.classList.toggle('is-favorite', isFav)
234+
})
235+
updateFavoritesCount()
236+
updateEmptyState()
237+
}
238+
239+
function updateFavoritesCount() {
240+
const count = readFavs().length
241+
document.querySelectorAll('[data-favorites-count]').forEach((el) => {
242+
el.textContent = String(count)
181243
})
182244
}
183245

246+
function updateEmptyState() {
247+
const empty = document.querySelector('[data-favorites-empty]')
248+
const listView = document.getElementById('schedule-list-view')
249+
if (!empty || !listView) return
250+
const filterActive = listView.classList.contains('filter-favorites-only')
251+
const count = readFavs().length
252+
empty.hidden = !(filterActive && count === 0)
253+
}
254+
184255
// Click handler: toggle, persist, update visual state and announce
185256
document.addEventListener('click', (e) => {
186257
const btn = e.target.closest && e.target.closest('.favorite-btn')
@@ -222,6 +293,18 @@ const schedule = await getSchedule()
222293
if (e.key === FAV_KEY) syncFavorites()
223294
})
224295

296+
// Filter button: show only favourited sessions
297+
const filterBtn = document.querySelector('[data-filter-favorites]')
298+
if (filterBtn) {
299+
filterBtn.addEventListener('click', () => {
300+
const pressed = filterBtn.getAttribute('aria-pressed') === 'true'
301+
filterBtn.setAttribute('aria-pressed', pressed ? 'false' : 'true')
302+
const listView = document.getElementById('schedule-list-view')
303+
if (listView) listView.classList.toggle('filter-favorites-only', !pressed)
304+
updateEmptyState()
305+
})
306+
}
307+
225308
// Initial render
226309
syncFavorites()
227310
})()

src/i18n/programa/ca.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const ca = {
2929
favorite: 'Preferit',
3030
favoriteAdded: 'Afegit a preferits',
3131
favoriteRemoved: 'Tret de preferits',
32+
favoritesOnly: 'Només preferits',
33+
noFavorites: "Encara no tens preferits. Marca les sessions que t'interessin amb el cor.",
3234
showAbstract: 'Mostra la descripció',
3335
hideAbstract: 'Amaga la descripció',
3436
talkLink: 'Veure detalls de la xerrada a Pretalx',

src/i18n/programa/en.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const en = {
1414
},
1515
list: {
1616
viewModeLegend: 'Schedule view mode',
17-
viewModeList: 'Accessible list',
17+
viewModeList: 'List',
1818
viewModeGrid: 'Visual grid',
1919
dayN: (n: number) => `Day ${n}`,
2020
speakers: (count: number) => (count === 1 ? 'Speaker' : 'Speakers'),
@@ -29,6 +29,8 @@ export const en = {
2929
favorite: 'Favourite',
3030
favoriteAdded: 'Added to favourites',
3131
favoriteRemoved: 'Removed from favourites',
32+
favoritesOnly: 'Only favourites',
33+
noFavorites: "You don't have any favourites yet. Tap the heart on the sessions you're interested in.",
3234
showAbstract: 'Show description',
3335
hideAbstract: 'Hide description',
3436
talkLink: 'View talk details on Pretalx',

src/i18n/programa/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ export const es = {
2929
favorite: 'Favorito',
3030
favoriteAdded: 'Añadido a favoritos',
3131
favoriteRemoved: 'Quitado de favoritos',
32+
favoritesOnly: 'Solo favoritos',
33+
noFavorites: 'No tienes favoritos todavía. Marca las sesiones que te interesen con el corazón.',
3234
showAbstract: 'Ver descripción',
3335
hideAbstract: 'Ocultar descripción',
3436
talkLink: 'Ver detalles de la charla en Pretalx',

0 commit comments

Comments
 (0)