feat: add a filter toggle

This commit is contained in:
Levente Orban
2025-09-01 15:10:13 +02:00
parent 6020a78302
commit 7d1991eb94
5 changed files with 168 additions and 112 deletions

View File

@@ -1,12 +0,0 @@
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
};
export const formatTime = (timeString: string): string => {
const [hours, minutes] = timeString.split(':');
return `${hours}:${minutes}`;
};

40
src/lib/dateHelpers.ts Normal file
View File

@@ -0,0 +1,40 @@
import type { Event } from './types';
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}/${month}/${day}`;
};
export const formatTime = (timeString: string): string => {
const [hours, minutes] = timeString.split(':');
return `${hours}:${minutes}`;
};
// Helper function to check if an event is within a time range
export const isEventInTimeRange = (event: Event, timeFilter: string): boolean => {
if (timeFilter === 'any') return true;
const eventDate = new Date(`${event.date}T${event.time}`);
const now = new Date();
// Handle temporal status filters
if (timeFilter === 'upcoming') {
return eventDate >= now;
}
if (timeFilter === 'past') {
return eventDate < now;
}
// Handle time range filters
const ranges: Record<string, Date> = {
'next-week': new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000),
'next-month': new Date(new Date(now).setMonth(now.getMonth() + 1))
};
const endDate = ranges[timeFilter];
return endDate ? eventDate >= now && eventDate <= endDate : true;
};

View File

@@ -2,7 +2,7 @@
import type { Event, EventType } from '$lib/types'; import type { Event, EventType } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { PageData } from '../$types'; import type { PageData } from '../$types';
import { formatTime, formatDate } from '$lib/dateFormatter'; import { formatTime, formatDate, isEventInTimeRange } from '$lib/dateHelpers';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
let publicEvents: Event[] = []; let publicEvents: Event[] = [];
@@ -10,7 +10,9 @@
let searchQuery = ''; let searchQuery = '';
let selectedEventType: EventType | 'all' = 'all'; let selectedEventType: EventType | 'all' = 'all';
let selectedTimeFilter: 'any' | 'next-week' | 'next-month' = 'any'; let selectedTimeFilter: 'any' | 'next-week' | 'next-month' = 'any';
let selectedTemporalStatus: 'all' | 'upcoming' | 'past' = 'all';
let selectedSortOrder: 'asc' | 'desc' = 'asc'; let selectedSortOrder: 'asc' | 'desc' = 'asc';
let showFilters = false;
let fuse: Fuse<Event>; let fuse: Fuse<Event>;
export let data: PageData; export let data: PageData;
@@ -28,29 +30,7 @@
includeMatches: true includeMatches: true
}); });
// Helper function to check if an event is within a time range // Filter events based on search query, event type, time filter, and temporal status
function isEventInTimeRange(event: Event, timeFilter: string): boolean {
if (timeFilter === 'any') return true;
const eventDate = new Date(`${event.date}T${event.time}`);
const now = new Date();
if (timeFilter === 'next-week') {
const nextWeek = new Date(now);
nextWeek.setDate(now.getDate() + 7);
return eventDate >= now && eventDate <= nextWeek;
}
if (timeFilter === 'next-month') {
const nextMonth = new Date(now);
nextMonth.setMonth(now.getMonth() + 1);
return eventDate >= now && eventDate <= nextMonth;
}
return true;
}
// Filter events based on search query, event type, and time filter using Fuse.js
$: filteredEvents = (() => { $: filteredEvents = (() => {
let events = publicEvents; let events = publicEvents;
@@ -59,6 +39,11 @@
events = events.filter((event) => event.type === selectedEventType); events = events.filter((event) => event.type === selectedEventType);
} }
// Then filter by temporal status (past/upcoming/all)
if (selectedTemporalStatus !== 'all') {
events = events.filter((event) => isEventInTimeRange(event, selectedTemporalStatus));
}
// Then filter by time range // Then filter by time range
if (selectedTimeFilter !== 'any') { if (selectedTimeFilter !== 'any') {
events = events.filter((event) => isEventInTimeRange(event, selectedTimeFilter)); events = events.filter((event) => isEventInTimeRange(event, selectedTimeFilter));
@@ -67,10 +52,13 @@
// Then apply search query // Then apply search query
if (searchQuery.trim() !== '') { if (searchQuery.trim() !== '') {
events = fuse.search(searchQuery).map((result) => result.item); events = fuse.search(searchQuery).map((result) => result.item);
// Re-apply type and time filters after search // Re-apply all filters after search
if (selectedEventType !== 'all') { if (selectedEventType !== 'all') {
events = events.filter((event) => event.type === selectedEventType); events = events.filter((event) => event.type === selectedEventType);
} }
if (selectedTemporalStatus !== 'all') {
events = events.filter((event) => isEventInTimeRange(event, selectedTemporalStatus));
}
if (selectedTimeFilter !== 'any') { if (selectedTimeFilter !== 'any') {
events = events.filter((event) => isEventInTimeRange(event, selectedTimeFilter)); events = events.filter((event) => isEventInTimeRange(event, selectedTimeFilter));
} }
@@ -134,8 +122,9 @@
<!-- Search and Filter Section --> <!-- Search and Filter Section -->
<div class="mb-8 max-h-screen"> <div class="mb-8 max-h-screen">
<!-- Search Bar --> <!-- Search Bar and Filter Toggle -->
<div class="relative mx-auto w-full md:w-2/3"> <div class="mx-auto flex w-full items-center gap-3 md:w-2/3">
<div class="relative flex-1">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg <svg
class="h-5 w-5 text-slate-400" class="h-5 w-5 text-slate-400"
@@ -155,7 +144,7 @@
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
placeholder="Search events by name, location..." placeholder="Search events by name, location..."
class="w-full rounded-lg border border-slate-600 bg-slate-800 px-4 py-3 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none" class="w-full rounded-sm border border-slate-600 bg-slate-800 pl-10 text-white placeholder-slate-400 focus:border-violet-500 focus:ring-violet-500/20"
/> />
{#if searchQuery} {#if searchQuery}
<button <button
@@ -175,29 +164,67 @@
{/if} {/if}
</div> </div>
<!-- Filter Toggle Button -->
<button
on:click={() => (showFilters = !showFilters)}
class="flex items-center rounded-sm border p-3 font-semibold {showFilters
? 'border-violet-500 bg-violet-400/20'
: 'border-slate-600 bg-slate-800'}"
aria-label="Toggle filters"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.207A1 1 0 013 6.5V4z"
></path>
</svg>
</button>
</div>
<!-- Time Filter and Sort Controls --> <!-- Time Filter and Sort Controls -->
<div class="mx-auto mt-4 flex flex-col items-center gap-4 sm:flex-row sm:justify-center"> {#if showFilters}
<div
class="mx-auto mt-4 flex flex-col items-center gap-4 sm:flex-row sm:justify-center"
>
<!-- Event Type Filter --> <!-- Event Type Filter -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="event-type-filter" class="text-sm font-medium text-slate-400">Type:</label <label for="event-type-filter" class="text-sm font-medium text-slate-400"
>Type:</label
> >
<select <select
id="event-type-filter" id="event-type-filter"
bind:value={selectedEventType} bind:value={selectedEventType}
class="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none" class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
> >
<option value="all">All</option> <option value="all">All</option>
<option value="limited">Limited</option> <option value="limited">Limited</option>
<option value="unlimited">Unlimited</option> <option value="unlimited">Unlimited</option>
</select> </select>
</div> </div>
<!-- Temporal Status Filter -->
<div class="flex items-center gap-2">
<label for="temporal-status-filter" class="text-sm font-medium text-slate-400"
>Status:</label
>
<select
id="temporal-status-filter"
bind:value={selectedTemporalStatus}
class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
>
<option value="all">All events</option>
<option value="upcoming">Upcoming events</option>
<option value="past">Past events</option>
</select>
</div>
<!-- Time Filter Dropdown --> <!-- Time Filter Dropdown -->
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<label for="time-filter" class="text-sm font-medium text-slate-400">Time:</label> <label for="time-filter" class="text-sm font-medium text-slate-400">Time:</label>
<select <select
id="time-filter" id="time-filter"
bind:value={selectedTimeFilter} bind:value={selectedTimeFilter}
class="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none" class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
> >
<option value="any">Any time</option> <option value="any">Any time</option>
<option value="next-week">Next week</option> <option value="next-week">Next week</option>
@@ -211,13 +238,14 @@
<select <select
id="sort-order" id="sort-order"
bind:value={selectedSortOrder} bind:value={selectedSortOrder}
class="rounded-lg border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20 focus:outline-none" class="rounded-sm border border-slate-600 bg-slate-800 px-3 py-2 text-sm text-white focus:border-violet-500 focus:ring-2 focus:ring-violet-500/20"
> >
<option value="asc">Earliest first</option> <option value="asc">Earliest first</option>
<option value="desc">Latest first</option> <option value="desc">Latest first</option>
</select> </select>
</div> </div>
</div> </div>
{/if}
</div> </div>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Event } from '$lib/types'; import type { Event } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { formatTime, formatDate } from '$lib/dateFormatter'; import { formatTime, formatDate } from '$lib/dateHelpers';
export let data: { events: Event[] }; export let data: { events: Event[] };

View File

@@ -3,7 +3,7 @@
import type { Event, RSVP } from '$lib/types'; import type { Event, RSVP } from '$lib/types';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { formatTime, formatDate } from '$lib/dateFormatter'; import { formatTime, formatDate } from '$lib/dateHelpers.js';
export let data: { event: Event; rsvps: RSVP[]; userId: string }; export let data: { event: Event; rsvps: RSVP[]; userId: string };
export let form; export let form;