feat: Add recent changes and recently opened tabs

This commit is contained in:
gpt-engineer-app[bot]
2025-10-10 15:58:54 +00:00
parent 4d2d39fb5a
commit c8443e05a3
2 changed files with 232 additions and 1 deletions

View File

@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ParkCard } from '@/components/parks/ParkCard'; import { ParkCard } from '@/components/parks/ParkCard';
import { RideCard } from '@/components/rides/RideCard'; import { RideCard } from '@/components/rides/RideCard';
import { RecentChangeCard } from './RecentChangeCard';
import { Badge } from '@/components/ui/badge';
import { Park, Ride } from '@/types/database'; import { Park, Ride } from '@/types/database';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
@@ -10,6 +12,8 @@ export function ContentTabs() {
const [trendingRides, setTrendingRides] = useState<Ride[]>([]); const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
const [recentParks, setRecentParks] = useState<Park[]>([]); const [recentParks, setRecentParks] = useState<Park[]>([]);
const [recentRides, setRecentRides] = useState<Ride[]>([]); const [recentRides, setRecentRides] = useState<Ride[]>([]);
const [recentChanges, setRecentChanges] = useState<any[]>([]);
const [recentlyOpened, setRecentlyOpened] = useState<Array<Park | Ride>>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -46,10 +50,69 @@ export function ContentTabs() {
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.limit(12); .limit(12);
// Fetch recent entity changes
const { data: changesData } = await supabase
.from('entity_versions')
.select(`
id,
entity_type,
entity_id,
version_number,
version_data,
changed_at,
change_type,
change_reason,
changer_profile:profiles!entity_versions_changed_by_fkey(username, avatar_url)
`)
.order('changed_at', { ascending: false })
.limit(24);
// Process changes to extract entity info from version_data
const processedChanges = changesData?.map(change => {
const versionData = change.version_data as any;
return {
...change,
entity_name: versionData?.name || 'Unknown',
entity_slug: versionData?.slug || '',
entity_image_url: versionData?.card_image_url || versionData?.banner_image_url,
};
}) || [];
// Fetch recently opened parks and rides
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
const dateThreshold = oneYearAgo.toISOString().split('T')[0];
const { data: openedParks } = await supabase
.from('parks')
.select(`*, location:locations(*), operator:companies!parks_operator_id_fkey(*)`)
.not('opening_date', 'is', null)
.gte('opening_date', dateThreshold)
.order('opening_date', { ascending: false })
.limit(20);
const { data: openedRides } = await supabase
.from('rides')
.select(`*, park:parks!inner(name, slug, location:locations(*))`)
.not('opening_date', 'is', null)
.gte('opening_date', dateThreshold)
.order('opening_date', { ascending: false })
.limit(20);
// Combine and sort by opening date
const combinedOpened = [
...(openedParks || []).map(p => ({ ...p, entityType: 'park' as const })),
...(openedRides || []).map(r => ({ ...r, entityType: 'ride' as const }))
]
.sort((a, b) => new Date(b.opening_date).getTime() - new Date(a.opening_date).getTime())
.slice(0, 24);
setTrendingParks(trending || []); setTrendingParks(trending || []);
setRecentParks(recent || []); setRecentParks(recent || []);
setTrendingRides(trendingRidesData || []); setTrendingRides(trendingRidesData || []);
setRecentRides(recentRidesData || []); setRecentRides(recentRidesData || []);
setRecentChanges(processedChanges);
setRecentlyOpened(combinedOpened);
} catch (error) { } catch (error) {
console.error('Error fetching content:', error); console.error('Error fetching content:', error);
} finally { } finally {
@@ -80,7 +143,7 @@ export function ContentTabs() {
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<Tabs defaultValue="trending-parks" className="w-full"> <Tabs defaultValue="trending-parks" className="w-full">
<div className="text-center mb-8"> <div className="text-center mb-8">
<TabsList className="grid w-full max-w-3xl mx-auto grid-cols-2 md:grid-cols-4 h-auto p-2 bg-gradient-to-r from-primary/10 via-secondary/10 to-accent/10 dark:from-primary/20 dark:via-secondary/20 dark:to-accent/20 dark:bg-card/50 border border-primary/20 dark:border-primary/30 rounded-xl shadow-lg backdrop-blur-sm"> <TabsList className="grid w-full max-w-5xl mx-auto grid-cols-3 md:grid-cols-6 h-auto p-2 bg-gradient-to-r from-primary/10 via-secondary/10 to-accent/10 dark:from-primary/20 dark:via-secondary/20 dark:to-accent/20 dark:bg-card/50 border border-primary/20 dark:border-primary/30 rounded-xl shadow-lg backdrop-blur-sm">
<TabsTrigger value="trending-parks" className="text-xs md:text-sm px-2 py-3"> <TabsTrigger value="trending-parks" className="text-xs md:text-sm px-2 py-3">
Trending Parks Trending Parks
</TabsTrigger> </TabsTrigger>
@@ -93,6 +156,12 @@ export function ContentTabs() {
<TabsTrigger value="recent-rides" className="text-xs md:text-sm px-2 py-3"> <TabsTrigger value="recent-rides" className="text-xs md:text-sm px-2 py-3">
New Rides New Rides
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="recent-changes" className="text-xs md:text-sm px-2 py-3">
Recent Changes
</TabsTrigger>
<TabsTrigger value="recently-opened" className="text-xs md:text-sm px-2 py-3">
Recently Opened
</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -143,6 +212,56 @@ export function ContentTabs() {
))} ))}
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="recent-changes" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold mb-2">Recent Changes</h2>
<p className="text-muted-foreground">Latest updates across all entities</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
{recentChanges.map((change) => (
<RecentChangeCard
key={change.id}
entityType={change.entity_type}
entityId={change.entity_id}
entityName={change.entity_name}
entitySlug={change.entity_slug}
imageUrl={change.entity_image_url}
changeType={change.change_type}
changedAt={change.changed_at}
changedByUsername={change.changer_profile?.username}
changedByAvatar={change.changer_profile?.avatar_url}
changeReason={change.change_reason}
/>
))}
</div>
</TabsContent>
<TabsContent value="recently-opened" className="mt-8">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold mb-2">Recently Opened</h2>
<p className="text-muted-foreground">Parks and rides that opened in the last year</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-7 3xl:grid-cols-8 gap-4 lg:gap-5 xl:gap-4 2xl:gap-5">
{recentlyOpened.map((entity: any) => (
entity.entityType === 'park' ? (
<div key={entity.id} className="relative">
<ParkCard park={entity} />
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
{new Date(entity.opening_date).getFullYear()}
</Badge>
</div>
) : (
<div key={entity.id} className="relative">
<RideCard ride={entity} />
<Badge className="absolute top-2 right-2 bg-green-500/90 text-white backdrop-blur-sm">
{new Date(entity.opening_date).getFullYear()}
</Badge>
</div>
)
))}
</div>
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,112 @@
import { Link } from 'react-router-dom';
import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Clock, User } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
interface RecentChangeCardProps {
entityType: 'park' | 'ride' | 'company';
entityId: string;
entityName: string;
entitySlug: string;
imageUrl?: string;
changeType: string;
changedAt: string;
changedByUsername?: string;
changedByAvatar?: string;
changeReason?: string;
}
const changeTypeColors = {
created: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/20',
updated: 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/20',
deleted: 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/20',
restored: 'bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/20',
archived: 'bg-muted text-muted-foreground border-muted',
};
const entityTypeColors = {
park: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/20',
ride: 'bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-500/20',
company: 'bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 border-indigo-500/20',
};
export function RecentChangeCard({
entityType,
entityId,
entityName,
entitySlug,
imageUrl,
changeType,
changedAt,
changedByUsername,
changedByAvatar,
changeReason,
}: RecentChangeCardProps) {
const getEntityPath = () => {
if (entityType === 'park') return `/parks/${entitySlug}`;
if (entityType === 'ride') {
// For rides, we need the park slug too - for now, just link to rides page
return `/rides`;
}
// Company paths - link to the appropriate company page
return '/';
};
return (
<Link to={getEntityPath()}>
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full">
{imageUrl && (
<div className="aspect-video w-full overflow-hidden bg-muted">
<img
src={imageUrl}
alt={entityName}
className="w-full h-full object-cover"
loading="lazy"
/>
</div>
)}
<div className="p-4 space-y-3">
<div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className={entityTypeColors[entityType]}>
{entityType}
</Badge>
<Badge variant="outline" className={changeTypeColors[changeType as keyof typeof changeTypeColors] || changeTypeColors.archived}>
{changeType}
</Badge>
</div>
<h3 className="font-semibold line-clamp-2 text-sm">{entityName}</h3>
</div>
{changeReason && (
<p className="text-xs text-muted-foreground line-clamp-2 italic">
{changeReason}
</p>
)}
<div className="flex items-center justify-between text-xs text-muted-foreground gap-2">
<div className="flex items-center gap-1">
<Clock className="h-3 w-3" />
<span className="line-clamp-1">{formatDistanceToNow(new Date(changedAt), { addSuffix: true })}</span>
</div>
{changedByUsername && (
<div className="flex items-center gap-1">
<Avatar className="h-4 w-4">
<AvatarImage src={changedByAvatar} />
<AvatarFallback>
<User className="h-2 w-2" />
</AvatarFallback>
</Avatar>
<span className="line-clamp-1">{changedByUsername}</span>
</div>
)}
</div>
</div>
</Card>
</Link>
);
}