Enhance Events page with column sorting and filtering

This commit is contained in:
CycroftX
2026-02-03 20:47:08 +05:30
parent dd57db8869
commit 1e0845c4be

View File

@@ -1,6 +1,8 @@
import { Calendar, Ticket, Flag, Search, Filter, Plus, ArrowUpDown } from 'lucide-react';
import { useState, useMemo } from 'react';
import { Calendar, Ticket, Flag, Search, Filter, Plus, ArrowUpDown, ArrowUp, ArrowDown, Check } from 'lucide-react';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
@@ -21,7 +23,71 @@ const statusStyles = {
flagged: 'bg-error/10 text-error',
};
type SortKey = 'title' | 'partnerName' | 'date' | 'status' | 'ticketsSold' | 'revenue';
export default function Events() {
const [searchQuery, setSearchQuery] = useState('');
const [sortConfig, setSortConfig] = useState<{ key: SortKey; direction: 'asc' | 'desc' } | null>(null);
const [statusFilters, setStatusFilters] = useState<string[]>([]);
const allStatuses = ['draft', 'published', 'live', 'completed', 'cancelled', 'flagged'];
const filteredAndSortedEvents = useMemo(() => {
let result = [...mockEvents];
// Filter by Search
if (searchQuery) {
const lowerQuery = searchQuery.toLowerCase();
result = result.filter(event =>
event.title.toLowerCase().includes(lowerQuery) ||
event.partnerName.toLowerCase().includes(lowerQuery)
);
}
// Filter by Status
if (statusFilters.length > 0) {
result = result.filter(event => statusFilters.includes(event.status));
}
// Sort
if (sortConfig) {
result.sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return result;
}, [mockEvents, searchQuery, statusFilters, sortConfig]);
const handleSort = (key: SortKey) => {
setSortConfig(current => {
if (current?.key === key) {
return { key, direction: current.direction === 'asc' ? 'desc' : 'asc' };
}
return { key, direction: 'asc' };
});
};
const toggleStatusFilter = (status: string) => {
setStatusFilters(current =>
current.includes(status)
? current.filter(s => s !== status)
: [...current, status]
);
};
const SortIcon = ({ column }: { column: SortKey }) => {
if (sortConfig?.key !== column) return <ArrowUpDown className="ml-2 h-4 w-4 opacity-50" />;
return sortConfig.direction === 'asc'
? <ArrowUp className="ml-2 h-4 w-4 text-accent" />
: <ArrowDown className="ml-2 h-4 w-4 text-accent" />;
};
return (
<AppLayout
title="Events"
@@ -35,7 +101,9 @@ export default function Events() {
<Calendar className="h-6 w-6 text-accent" />
</div>
<div>
<p className="text-2xl font-bold text-foreground">43</p>
<p className="text-2xl font-bold text-foreground">
{mockEvents.filter(e => e.status === 'live').length}
</p>
<p className="text-sm text-muted-foreground">Live Events</p>
</div>
</div>
@@ -57,7 +125,9 @@ export default function Events() {
<Flag className="h-6 w-6 text-error" />
</div>
<div>
<p className="text-2xl font-bold text-foreground">3</p>
<p className="text-2xl font-bold text-foreground">
{mockEvents.filter(e => e.status === 'flagged').length}
</p>
<p className="text-sm text-muted-foreground">Flagged Events</p>
</div>
</div>
@@ -74,32 +144,51 @@ export default function Events() {
<input
type="text"
placeholder="Search events..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-10 w-64 pl-10 pr-4 rounded-xl text-sm bg-secondary shadow-neu-inset focus:outline-none focus:ring-2 focus:ring-accent/50"
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-10 px-4 rounded-xl neu-button flex items-center gap-2">
<ArrowUpDown className="h-4 w-4" />
<span className="text-sm font-medium">Sort</span>
<button className={cn(
"h-10 px-4 rounded-xl neu-button flex items-center gap-2",
statusFilters.length > 0 && "ring-2 ring-accent/50 text-accent"
)}>
<Filter className="h-4 w-4" />
<span className="text-sm font-medium">
Filter {statusFilters.length > 0 && `(${statusFilters.length})`}
</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuLabel>Sort by</DropdownMenuLabel>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Newest First</DropdownMenuItem>
<DropdownMenuItem>Oldest First</DropdownMenuItem>
<DropdownMenuItem>Name (A-Z)</DropdownMenuItem>
<DropdownMenuItem>Upcoming</DropdownMenuItem>
<DropdownMenuItem>Status</DropdownMenuItem>
{allStatuses.map(status => (
<DropdownMenuCheckboxItem
key={status}
checked={statusFilters.includes(status)}
onCheckedChange={() => toggleStatusFilter(status)}
className="capitalize"
>
{status}
</DropdownMenuCheckboxItem>
))}
{statusFilters.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="justify-center text-error font-medium"
onClick={() => setStatusFilters([])}
>
Clear Filters
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<button className="h-10 px-4 rounded-xl neu-button flex items-center gap-2">
<Filter className="h-4 w-4" />
<span className="text-sm font-medium">Filter</span>
</button>
<CreateEventSheet>
<button className="h-10 px-4 rounded-xl bg-primary text-primary-foreground flex items-center gap-2 shadow-neu-sm hover:shadow-neu transition-shadow">
<Plus className="h-4 w-4" />
@@ -113,17 +202,47 @@ export default function Events() {
<table className="w-full">
<thead>
<tr className="border-b border-border/50">
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Event</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Partner</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Date</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Status</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Tickets</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Revenue</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('title')}>
<div className="flex items-center">
Event <SortIcon column="title" />
</div>
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('partnerName')}>
<div className="flex items-center">
Partner <SortIcon column="partnerName" />
</div>
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('date')}>
<div className="flex items-center">
Date <SortIcon column="date" />
</div>
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('status')}>
<div className="flex items-center">
Status <SortIcon column="status" />
</div>
</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('ticketsSold')}>
<div className="flex items-center justify-end">
Tickets <SortIcon column="ticketsSold" />
</div>
</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
onClick={() => handleSort('revenue')}>
<div className="flex items-center justify-end">
Revenue <SortIcon column="revenue" />
</div>
</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Actions</th>
</tr>
</thead>
<tbody>
{mockEvents.map((event) => (
{filteredAndSortedEvents.map((event) => (
<tr key={event.id} className="border-b border-border/30 hover:bg-secondary/30 transition-colors">
<td className="py-4 px-4">
<p className="font-medium text-foreground">{event.title}</p>
@@ -157,6 +276,13 @@ export default function Events() {
</td>
</tr>
))}
{filteredAndSortedEvents.length === 0 && (
<tr>
<td colSpan={7} className="text-center py-8 text-muted-foreground">
No events found matching your criteria.
</td>
</tr>
)}
</tbody>
</table>
</div>