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 {
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user