Files
thrilltrack-explorer/src/components/admin/database-stats/RecentAdditionsTable.tsx
gpt-engineer-app[bot] f036776dce Implement admin database stats dashboard
Add admin-only database statistics dashboard
- Introduces types for database statistics and recent additions
- Implements hooks to fetch statistics and recent additions via RPCs
- Adds UI components for stats cards and a recent additions table
- Integrates new AdminDatabaseStats page and routing under /admin/database-stats
- Updates admin sidebar and app routes to expose the new dashboard
- Enables real-time updates and export capabilities for recent additions
2025-11-11 16:54:02 +00:00

222 lines
8.1 KiB
TypeScript

import { useState, useMemo } from 'react';
import { Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import {
Building2,
Bike,
Factory,
Box,
MapPin,
Calendar,
Image,
Download,
Search
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { RecentAddition } from '@/types/database-stats';
interface RecentAdditionsTableProps {
additions: RecentAddition[];
isLoading: boolean;
}
const entityTypeConfig = {
park: { icon: Building2, label: 'Park', color: 'bg-blue-500' },
ride: { icon: Bike, label: 'Ride', color: 'bg-purple-500' },
company: { icon: Factory, label: 'Company', color: 'bg-orange-500' },
ride_model: { icon: Box, label: 'Model', color: 'bg-green-500' },
location: { icon: MapPin, label: 'Location', color: 'bg-yellow-500' },
timeline_event: { icon: Calendar, label: 'Event', color: 'bg-pink-500' },
photo: { icon: Image, label: 'Photo', color: 'bg-teal-500' },
};
export function RecentAdditionsTable({ additions, isLoading }: RecentAdditionsTableProps) {
const [entityTypeFilter, setEntityTypeFilter] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState('');
const filteredAdditions = useMemo(() => {
let filtered = additions;
if (entityTypeFilter !== 'all') {
filtered = filtered.filter(item => item.entity_type === entityTypeFilter);
}
if (searchQuery) {
const query = searchQuery.toLowerCase();
filtered = filtered.filter(item =>
item.entity_name.toLowerCase().includes(query) ||
item.created_by_username?.toLowerCase().includes(query)
);
}
return filtered;
}, [additions, entityTypeFilter, searchQuery]);
const exportToCSV = () => {
const headers = ['Type', 'Name', 'Added By', 'Added At'];
const rows = filteredAdditions.map(item => [
entityTypeConfig[item.entity_type].label,
item.entity_name,
item.created_by_username || 'System',
new Date(item.created_at).toISOString(),
]);
const csv = [headers, ...rows].map(row => row.join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `recent-additions-${new Date().toISOString()}.csv`;
a.click();
URL.revokeObjectURL(url);
};
const getEntityLink = (item: RecentAddition) => {
if (item.entity_type === 'park' && item.entity_slug) {
return `/parks/${item.entity_slug}`;
}
if (item.entity_type === 'ride' && item.park_slug && item.entity_slug) {
return `/parks/${item.park_slug}/rides/${item.entity_slug}`;
}
if (item.entity_type === 'company' && item.entity_slug) {
return `/manufacturers/${item.entity_slug}`;
}
if (item.entity_type === 'ride_model' && item.entity_slug) {
return `/models/${item.entity_slug}`;
}
return null;
};
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle>Latest Additions</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Latest Additions (Newest First)</CardTitle>
<Button onClick={exportToCSV} variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export CSV
</Button>
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col sm:flex-row gap-4 mb-6">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by name or creator..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
/>
</div>
<Select value={entityTypeFilter} onValueChange={setEntityTypeFilter}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Types</SelectItem>
<SelectItem value="park">Parks</SelectItem>
<SelectItem value="ride">Rides</SelectItem>
<SelectItem value="company">Companies</SelectItem>
<SelectItem value="ride_model">Ride Models</SelectItem>
<SelectItem value="location">Locations</SelectItem>
<SelectItem value="timeline_event">Timeline Events</SelectItem>
<SelectItem value="photo">Photos</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
{filteredAdditions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No additions found matching your filters.
</div>
) : (
filteredAdditions.map((item) => {
const config = entityTypeConfig[item.entity_type];
const Icon = config.icon;
const link = getEntityLink(item);
return (
<div
key={`${item.entity_type}-${item.entity_id}`}
className="flex items-center gap-4 p-4 rounded-lg border bg-card hover:bg-accent/50 transition-colors"
>
<div className={`p-2 rounded-lg ${config.color} bg-opacity-10`}>
<Icon className="h-5 w-5" />
</div>
{item.image_url && (
<img
src={item.image_url}
alt={item.entity_name}
className="h-12 w-12 rounded object-cover"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<Badge variant="outline" className="text-xs">
{config.label}
</Badge>
{link ? (
<Link
to={link}
className="font-medium text-sm hover:underline truncate"
>
{item.entity_name}
</Link>
) : (
<span className="font-medium text-sm truncate">
{item.entity_name}
</span>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{item.created_by_username ? (
<>
<Avatar className="h-4 w-4">
<AvatarImage src={item.created_by_avatar || undefined} />
<AvatarFallback className="text-[8px]">
{item.created_by_username[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<span>@{item.created_by_username}</span>
</>
) : (
<span>System</span>
)}
<span></span>
<span>{formatDistanceToNow(new Date(item.created_at), { addSuffix: true })}</span>
</div>
</div>
</div>
);
})
)}
</div>
</CardContent>
</Card>
);
}