feat(events): calendar popup respects the selected category filter #115

Merged
padreug merged 2 commits from feat/calendar-respect-categories into dev 2026-06-18 12:41:18 +00:00
6 changed files with 72 additions and 7 deletions

View file

@ -71,6 +71,8 @@ const messages: LocaleMessages = {
past: 'Past', past: 'Past',
filters: 'Filters', filters: 'Filters',
clearAll: 'Clear all', clearAll: 'Clear all',
filteringBy: 'Filtering by:',
removeCategory: 'Remove {category} filter',
}, },
categories: { categories: {
concert: 'Concert', concert: 'Concert',

View file

@ -71,6 +71,8 @@ const messages: LocaleMessages = {
past: 'Pasado', past: 'Pasado',
filters: 'Filtros', filters: 'Filtros',
clearAll: 'Limpiar todo', clearAll: 'Limpiar todo',
filteringBy: 'Filtrando por:',
removeCategory: 'Quitar el filtro {category}',
}, },
categories: { categories: {
concert: 'Concierto', concert: 'Concierto',

View file

@ -71,6 +71,8 @@ const messages: LocaleMessages = {
past: 'Passé', past: 'Passé',
filters: 'Filtres', filters: 'Filtres',
clearAll: 'Tout effacer', clearAll: 'Tout effacer',
filteringBy: 'Filtré par :',
removeCategory: 'Retirer le filtre {category}',
}, },
categories: { categories: {
concert: 'Concert', concert: 'Concert',

View file

@ -72,6 +72,8 @@ export interface LocaleMessages {
past: string past: string
filters: string filters: string
clearAll: string clearAll: string
filteringBy: string
removeCategory: string
} }
categories: Record<string, string> categories: Record<string, string>
detail: { detail: {

View file

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { import {
DialogRoot, DialogRoot,
DialogPortal, DialogPortal,
@ -10,8 +11,10 @@ import {
DialogClose, DialogClose,
} from 'reka-ui' } from 'reka-ui'
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import { Badge } from '@/components/ui/badge'
import EventCalendarView from './EventCalendarView.vue' import EventCalendarView from './EventCalendarView.vue'
import type { Event } from '../types/event' import type { Event } from '../types/event'
import type { EventCategory } from '../types/category'
// A date-picker popup: the month grid (with per-day event dots) in a // A date-picker popup: the month grid (with per-day event dots) in a
// dialog. Picking a day emits selectDate and closes. Reused by the feed // dialog. Picking a day emits selectDate and closes. Reused by the feed
@ -21,23 +24,40 @@ import type { Event } from '../types/event'
// DialogContent) so it can use a light, blurred overlay instead of the // DialogContent) so it can use a light, blurred overlay instead of the
// usual opaque dark dim the feed stays visible, softly blurred, behind // usual opaque dark dim the feed stays visible, softly blurred, behind
// the frosted-glass panel. // the frosted-glass panel.
const props = defineProps<{ const props = withDefaults(
defineProps<{
open: boolean open: boolean
events: Event[] events: Event[]
title: string title: string
description: string description: string
}>() // Active category filter mirrored from the feed. Rendered as
// deselectable chips so the user can see and loosen what's
// narrowing the calendar without closing it. Defaults to none for
// callers that don't filter by category (e.g. My Tickets).
selectedCategories?: EventCategory[]
}>(),
{
selectedCategories: () => [],
},
)
const emit = defineEmits<{ const emit = defineEmits<{
'update:open': [value: boolean] 'update:open': [value: boolean]
selectDate: [date: Date] selectDate: [date: Date]
'toggle-category': [category: EventCategory]
}>() }>()
const { t } = useI18n()
const isOpen = computed({ const isOpen = computed({
get: () => props.open, get: () => props.open,
set: (v) => emit('update:open', v), set: (v) => emit('update:open', v),
}) })
function categoryLabel(cat: EventCategory): string {
return t(`events.categories.${cat}`, cat)
}
function onSelectDate(date: Date) { function onSelectDate(date: Date) {
emit('selectDate', date) emit('selectDate', date)
isOpen.value = false isOpen.value = false
@ -62,6 +82,29 @@ function onSelectDate(date: Date) {
{{ description }} {{ description }}
</DialogDescription> </DialogDescription>
</div> </div>
<!-- Active category filter only the selected categories, each
removable. Clicking deselects via the parent's toggle, which
reactively re-narrows the calendar dots without closing. -->
<div
v-if="selectedCategories.length"
class="flex flex-wrap items-center gap-1.5"
>
<span class="text-xs text-muted-foreground">
{{ t('events.filters.filteringBy') }}
</span>
<Badge
v-for="cat in selectedCategories"
:key="cat"
variant="secondary"
class="cursor-pointer gap-1 text-xs select-none hover:opacity-80 transition-opacity"
:aria-label="t('events.filters.removeCategory', { category: categoryLabel(cat) })"
@click="emit('toggle-category', cat)"
>
{{ categoryLabel(cat) }}
<X class="w-3 h-3" />
</Badge>
</div>
<EventCalendarView :events="events" picker-mode @select-date="onSelectDate" /> <EventCalendarView :events="events" picker-mode @select-date="onSelectDate" />
<DialogClose <DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none" class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none"

View file

@ -61,6 +61,18 @@ const {
const filtersOpen = ref(false) const filtersOpen = ref(false)
const calendarOpen = ref(false) const calendarOpen = ref(false)
// Events feeding the calendar popup's per-day dots. Respects the active
// category filter (so the calendar reflects what the user is browsing),
// but not the temporal/day filters the calendar is for picking any
// date. No categories selected all events.
const calendarEvents = computed(() =>
selectedCategories.value.length
? allEvents.value.filter(
(e) => e.category && selectedCategories.value.includes(e.category),
)
: allEvents.value,
)
// Human label for the active day filter, shown as a removable chip. // Human label for the active day filter, shown as a removable chip.
const selectedDateLabel = computed(() => const selectedDateLabel = computed(() =>
selectedDate.value selectedDate.value
@ -255,10 +267,12 @@ onBeforeRouteLeave(() => {
day filters the feed to it and closes. --> day filters the feed to it and closes. -->
<EventCalendarPopup <EventCalendarPopup
v-model:open="calendarOpen" v-model:open="calendarOpen"
:events="allEvents" :events="calendarEvents"
:selected-categories="selectedCategories"
:title="t('events.nav.calendar', 'Calendar')" :title="t('events.nav.calendar', 'Calendar')"
:description="t('events.calendar.pickDay', 'Pick a day to see its events')" :description="t('events.calendar.pickDay', 'Pick a day to see its events')"
@select-date="onSelectDate" @select-date="onSelectDate"
@toggle-category="toggleCategory"
/> />
</div> </div>
</template> </template>