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 { import {
DropdownMenu, DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
@@ -21,7 +23,71 @@ const statusStyles = {
flagged: 'bg-error/10 text-error', flagged: 'bg-error/10 text-error',
}; };
type SortKey = 'title' | 'partnerName' | 'date' | 'status' | 'ticketsSold' | 'revenue';
export default function Events() { 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 ( return (
<AppLayout <AppLayout
title="Events" title="Events"
@@ -35,7 +101,9 @@ export default function Events() {
<Calendar className="h-6 w-6 text-accent" /> <Calendar className="h-6 w-6 text-accent" />
</div> </div>
<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> <p className="text-sm text-muted-foreground">Live Events</p>
</div> </div>
</div> </div>
@@ -57,7 +125,9 @@ export default function Events() {
<Flag className="h-6 w-6 text-error" /> <Flag className="h-6 w-6 text-error" />
</div> </div>
<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> <p className="text-sm text-muted-foreground">Flagged Events</p>
</div> </div>
</div> </div>
@@ -74,32 +144,51 @@ export default function Events() {
<input <input
type="text" type="text"
placeholder="Search events..." 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" 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> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<button className="h-10 px-4 rounded-xl neu-button flex items-center gap-2"> <button className={cn(
<ArrowUpDown className="h-4 w-4" /> "h-10 px-4 rounded-xl neu-button flex items-center gap-2",
<span className="text-sm font-medium">Sort</span> 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> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Sort by</DropdownMenuLabel> <DropdownMenuLabel>Filter by Status</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>Newest First</DropdownMenuItem> {allStatuses.map(status => (
<DropdownMenuItem>Oldest First</DropdownMenuItem> <DropdownMenuCheckboxItem
<DropdownMenuItem>Name (A-Z)</DropdownMenuItem> key={status}
<DropdownMenuItem>Upcoming</DropdownMenuItem> checked={statusFilters.includes(status)}
<DropdownMenuItem>Status</DropdownMenuItem> 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> </DropdownMenuContent>
</DropdownMenu> </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> <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"> <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" /> <Plus className="h-4 w-4" />
@@ -113,17 +202,47 @@ export default function Events() {
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-border/50"> <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 cursor-pointer hover:text-foreground transition-colors"
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Partner</th> onClick={() => handleSort('title')}>
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Date</th> <div className="flex items-center">
<th className="text-left py-3 px-4 text-sm font-semibold text-muted-foreground">Status</th> Event <SortIcon column="title" />
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Tickets</th> </div>
<th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Revenue</th> </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> <th className="text-right py-3 px-4 text-sm font-semibold text-muted-foreground">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{mockEvents.map((event) => ( {filteredAndSortedEvents.map((event) => (
<tr key={event.id} className="border-b border-border/30 hover:bg-secondary/30 transition-colors"> <tr key={event.id} className="border-b border-border/30 hover:bg-secondary/30 transition-colors">
<td className="py-4 px-4"> <td className="py-4 px-4">
<p className="font-medium text-foreground">{event.title}</p> <p className="font-medium text-foreground">{event.title}</p>
@@ -157,6 +276,13 @@ export default function Events() {
</td> </td>
</tr> </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> </tbody>
</table> </table>
</div> </div>