mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:11:12 -05:00
feat: Add recent changes and recently opened tabs
This commit is contained in:
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ParkCard } from '@/components/parks/ParkCard';
|
||||
import { RideCard } from '@/components/rides/RideCard';
|
||||
import { RecentChangeCard } from './RecentChangeCard';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Park, Ride } from '@/types/database';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
@@ -10,6 +12,8 @@ export function ContentTabs() {
|
||||
const [trendingRides, setTrendingRides] = useState<Ride[]>([]);
|
||||
const [recentParks, setRecentParks] = useState<Park[]>([]);
|
||||
const [recentRides, setRecentRides] = useState<Ride[]>([]);
|
||||
const [recentChanges, setRecentChanges] = useState<any[]>([]);
|
||||
const [recentlyOpened, setRecentlyOpened] = useState<Array<Park | Ride>>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -46,10 +50,69 @@ export function ContentTabs() {
|
||||
.order('created_at', { ascending: false })
|
||||
.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 || []);
|
||||
setRecentParks(recent || []);
|
||||
setTrendingRides(trendingRidesData || []);
|
||||
setRecentRides(recentRidesData || []);
|
||||
setRecentChanges(processedChanges);
|
||||
setRecentlyOpened(combinedOpened);
|
||||
} catch (error) {
|
||||
console.error('Error fetching content:', error);
|
||||
} finally {
|
||||
@@ -80,7 +143,7 @@ export function ContentTabs() {
|
||||
<div className="container mx-auto px-4">
|
||||
<Tabs defaultValue="trending-parks" className="w-full">
|
||||
<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">
|
||||
Trending Parks
|
||||
</TabsTrigger>
|
||||
@@ -93,6 +156,12 @@ export function ContentTabs() {
|
||||
<TabsTrigger value="recent-rides" className="text-xs md:text-sm px-2 py-3">
|
||||
New Rides
|
||||
</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>
|
||||
</div>
|
||||
|
||||
@@ -143,6 +212,56 @@ export function ContentTabs() {
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
112
src/components/homepage/RecentChangeCard.tsx
Normal file
112
src/components/homepage/RecentChangeCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user