feat: Implement complete plan for "coming soon" placeholders

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 14:47:18 +00:00
parent 8d26ac0749
commit 198742e165
7 changed files with 1123 additions and 69 deletions

View File

@@ -0,0 +1,249 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Plus, LayoutGrid, List } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { getErrorMessage } from '@/lib/errorHandler';
import { AddRideCreditDialog } from './AddRideCreditDialog';
import { RideCreditCard } from './RideCreditCard';
import { UserRideCredit } from '@/types/database';
interface RideCreditsManagerProps {
userId: string;
}
export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
const [credits, setCredits] = useState<UserRideCredit[]>([]);
const [loading, setLoading] = useState(true);
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [sortBy, setSortBy] = useState<'date' | 'count' | 'name'>('date');
const [filterCategory, setFilterCategory] = useState<string>('all');
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
useEffect(() => {
fetchCredits();
}, [userId, sortBy, filterCategory]);
const fetchCredits = async () => {
try {
setLoading(true);
let query = supabase
.from('user_ride_credits')
.select(`
*,
rides:ride_id (
id,
name,
slug,
category,
card_image_url,
parks:park_id (
id,
name,
slug
)
)
`)
.eq('user_id', userId);
if (filterCategory !== 'all') {
query = query.eq('rides.category', filterCategory);
}
if (sortBy === 'date') {
query = query.order('first_ride_date', { ascending: false, nullsFirst: false });
} else if (sortBy === 'count') {
query = query.order('ride_count', { ascending: false });
} else if (sortBy === 'name') {
query = query.order('rides(name)', { ascending: true });
}
const { data, error } = await query;
if (error) throw error;
// Type assertion since the query returns properly structured data
setCredits((data || []) as UserRideCredit[]);
} catch (error) {
console.error('Error fetching ride credits:', error);
toast.error(getErrorMessage(error));
} finally {
setLoading(false);
}
};
const handleCreditAdded = () => {
fetchCredits();
setIsAddDialogOpen(false);
};
const handleCreditUpdated = () => {
fetchCredits();
};
const handleCreditDeleted = async (creditId: string) => {
try {
const { error } = await supabase
.from('user_ride_credits')
.delete()
.eq('id', creditId)
.eq('user_id', userId);
if (error) throw error;
toast.success('Ride credit removed');
fetchCredits();
} catch (error) {
console.error('Error deleting credit:', error);
toast.error(getErrorMessage(error));
}
};
const stats = {
totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0),
uniqueCredits: credits.length,
parksVisited: new Set(credits.map(c => c.rides?.parks?.id).filter(Boolean)).size,
coasters: credits.filter(c => c.rides?.category === 'roller_coaster').length
};
if (loading) {
return (
<div className="space-y-4">
{Array.from({ length: 6 }, (_, i) => (
<Card key={i} className="animate-pulse">
<CardContent className="p-6">
<div className="h-32 bg-muted rounded"></div>
</CardContent>
</Card>
))}
</div>
);
}
return (
<div className="space-y-6">
{/* Statistics */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{stats.totalRides}</div>
<div className="text-sm text-muted-foreground">Total Rides</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{stats.uniqueCredits}</div>
<div className="text-sm text-muted-foreground">Unique Credits</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{stats.parksVisited}</div>
<div className="text-sm text-muted-foreground">Parks Visited</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="text-2xl font-bold">{stats.coasters}</div>
<div className="text-sm text-muted-foreground">Coasters</div>
</CardContent>
</Card>
</div>
{/* Controls */}
<div className="flex flex-wrap gap-4 items-center justify-between">
<div className="flex gap-2">
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Credit
</Button>
</div>
<div className="flex gap-2">
<Select value={filterCategory} onValueChange={setFilterCategory}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Rides</SelectItem>
<SelectItem value="roller_coaster">Roller Coasters</SelectItem>
<SelectItem value="flat_ride">Flat Rides</SelectItem>
<SelectItem value="water_ride">Water Rides</SelectItem>
<SelectItem value="dark_ride">Dark Rides</SelectItem>
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={(value) => setSortBy(value as typeof sortBy)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="date">Most Recent</SelectItem>
<SelectItem value="count">Most Ridden</SelectItem>
<SelectItem value="name">Ride Name</SelectItem>
</SelectContent>
</Select>
<div className="flex border rounded-md">
<Button
variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('grid')}
>
<LayoutGrid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('list')}
>
<List className="w-4 h-4" />
</Button>
</div>
</div>
</div>
{/* Credits Display */}
{credits.length === 0 ? (
<Card>
<CardContent className="py-12">
<div className="text-center">
<h3 className="text-xl font-semibold mb-2">No Ride Credits Yet</h3>
<p className="text-muted-foreground mb-4">
Start tracking your ride experiences
</p>
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Your First Credit
</Button>
</div>
</CardContent>
</Card>
) : (
<div className={viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-4'
}>
{credits.map((credit) => (
<RideCreditCard
key={credit.id}
credit={credit}
viewMode={viewMode}
onUpdate={handleCreditUpdated}
onDelete={() => handleCreditDeleted(credit.id)}
/>
))}
</div>
)}
{/* Add Credit Dialog */}
<AddRideCreditDialog
userId={userId}
open={isAddDialogOpen}
onOpenChange={setIsAddDialogOpen}
onSuccess={handleCreditAdded}
/>
</div>
);
}