mirror of
https://github.com/polaroi8d/cactoide.git
synced 2026-03-22 06:05:28 +00:00
feat: add a filter toggle
This commit is contained in:
@@ -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
40
src/lib/dateHelpers.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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,90 +122,130 @@
|
|||||||
|
|
||||||
<!-- 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="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
<div class="relative flex-1">
|
||||||
<svg
|
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||||
class="h-5 w-5 text-slate-400"
|
<svg
|
||||||
fill="none"
|
class="h-5 w-5 text-slate-400"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
stroke="currentColor"
|
||||||
>
|
viewBox="0 0 24 24"
|
||||||
<path
|
>
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
bind:value={searchQuery}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
{#if searchQuery}
|
|
||||||
<button
|
|
||||||
on:click={() => (searchQuery = '')}
|
|
||||||
class="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-300"
|
|
||||||
aria-label="Search input"
|
|
||||||
>
|
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M6 18L18 6M6 6l12 12"
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</div>
|
||||||
{/if}
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
placeholder="Search events by name, location..."
|
||||||
|
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}
|
||||||
|
<button
|
||||||
|
on:click={() => (searchQuery = '')}
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center pr-3 text-slate-400 hover:text-slate-300"
|
||||||
|
aria-label="Search input"
|
||||||
|
>
|
||||||
|
<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="M6 18L18 6M6 6l12 12"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</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>
|
</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}
|
||||||
<!-- Event Type Filter -->
|
<div
|
||||||
<div class="flex items-center gap-2">
|
class="mx-auto mt-4 flex flex-col items-center gap-4 sm:flex-row sm:justify-center"
|
||||||
<label for="event-type-filter" class="text-sm font-medium text-slate-400">Type:</label
|
>
|
||||||
>
|
<!-- Event Type Filter -->
|
||||||
<select
|
<div class="flex items-center gap-2">
|
||||||
id="event-type-filter"
|
<label for="event-type-filter" class="text-sm font-medium text-slate-400"
|
||||||
bind:value={selectedEventType}
|
>Type:</label
|
||||||
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"
|
>
|
||||||
>
|
<select
|
||||||
<option value="all">All</option>
|
id="event-type-filter"
|
||||||
<option value="limited">Limited</option>
|
bind:value={selectedEventType}
|
||||||
<option value="unlimited">Unlimited</option>
|
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"
|
||||||
</select>
|
>
|
||||||
</div>
|
<option value="all">All</option>
|
||||||
<!-- Time Filter Dropdown -->
|
<option value="limited">Limited</option>
|
||||||
<div class="flex items-center gap-2">
|
<option value="unlimited">Unlimited</option>
|
||||||
<label for="time-filter" class="text-sm font-medium text-slate-400">Time:</label>
|
</select>
|
||||||
<select
|
</div>
|
||||||
id="time-filter"
|
<!-- Temporal Status Filter -->
|
||||||
bind:value={selectedTimeFilter}
|
<div class="flex items-center gap-2">
|
||||||
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"
|
<label for="temporal-status-filter" class="text-sm font-medium text-slate-400"
|
||||||
>
|
>Status:</label
|
||||||
<option value="any">Any time</option>
|
>
|
||||||
<option value="next-week">Next week</option>
|
<select
|
||||||
<option value="next-month">Next month</option>
|
id="temporal-status-filter"
|
||||||
</select>
|
bind:value={selectedTemporalStatus}
|
||||||
</div>
|
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 -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<label for="time-filter" class="text-sm font-medium text-slate-400">Time:</label>
|
||||||
|
<select
|
||||||
|
id="time-filter"
|
||||||
|
bind:value={selectedTimeFilter}
|
||||||
|
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="next-week">Next week</option>
|
||||||
|
<option value="next-month">Next month</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Sort Order Dropdown -->
|
<!-- Sort Order Dropdown -->
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="sort-order" class="text-sm font-medium text-slate-400">Sort:</label>
|
<label for="sort-order" class="text-sm font-medium text-slate-400">Sort:</label>
|
||||||
<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">
|
||||||
|
|||||||
@@ -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[] };
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user