Enhance Events page with column sorting and filtering
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user