mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 23:51:11 -05:00
feat: Implement complete plan for "coming soon" placeholders
This commit is contained in:
345
src/components/profile/RideCreditCard.tsx
Normal file
345
src/components/profile/RideCreditCard.tsx
Normal file
@@ -0,0 +1,345 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Calendar, MapPin, Edit, Trash2, Plus, Minus, Check, X } from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { format } from 'date-fns';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { UserRideCredit } from '@/types/database';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface RideCreditCardProps {
|
||||
credit: UserRideCredit;
|
||||
viewMode: 'grid' | 'list';
|
||||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCreditCardProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editCount, setEditCount] = useState(credit.ride_count);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const rideName = credit.rides?.name || 'Unknown Ride';
|
||||
const parkName = credit.rides?.parks?.name || 'Unknown Park';
|
||||
const parkSlug = credit.rides?.parks?.slug || '';
|
||||
const rideSlug = credit.rides?.slug || '';
|
||||
const imageUrl = credit.rides?.card_image_url;
|
||||
const category = credit.rides?.category || '';
|
||||
|
||||
const handleUpdateCount = async () => {
|
||||
try {
|
||||
setUpdating(true);
|
||||
const { error } = await supabase
|
||||
.from('user_ride_credits')
|
||||
.update({ ride_count: editCount })
|
||||
.eq('id', credit.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Ride count updated');
|
||||
setIsEditing(false);
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error updating count:', error);
|
||||
toast.error(getErrorMessage(error));
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickIncrement = async () => {
|
||||
try {
|
||||
const newCount = credit.ride_count + 1;
|
||||
const { error } = await supabase
|
||||
.from('user_ride_credits')
|
||||
.update({ ride_count: newCount })
|
||||
.eq('id', credit.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast.success('Ride count increased');
|
||||
onUpdate();
|
||||
} catch (error) {
|
||||
console.error('Error incrementing count:', error);
|
||||
toast.error(getErrorMessage(error));
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryBadge = (category: string) => {
|
||||
const categoryMap: Record<string, { label: string; variant: 'default' | 'secondary' | 'outline' }> = {
|
||||
roller_coaster: { label: 'Coaster', variant: 'default' },
|
||||
flat_ride: { label: 'Flat Ride', variant: 'secondary' },
|
||||
water_ride: { label: 'Water Ride', variant: 'outline' },
|
||||
dark_ride: { label: 'Dark Ride', variant: 'outline' },
|
||||
};
|
||||
const info = categoryMap[category] || { label: category, variant: 'outline' as const };
|
||||
return <Badge variant={info.variant}>{info.label}</Badge>;
|
||||
};
|
||||
|
||||
const ridePath = parkSlug && rideSlug
|
||||
? `/parks/${parkSlug}/rides/${rideSlug}`
|
||||
: '#';
|
||||
|
||||
if (viewMode === 'list') {
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{imageUrl && (
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={rideName}
|
||||
className="w-20 h-20 object-cover rounded"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Link
|
||||
to={ridePath}
|
||||
className="font-semibold hover:underline truncate"
|
||||
>
|
||||
{rideName}
|
||||
</Link>
|
||||
{getCategoryBadge(category)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/parks/${parkSlug}`}
|
||||
className="text-sm text-muted-foreground hover:underline block truncate"
|
||||
>
|
||||
<MapPin className="w-3 h-3 inline mr-1" />
|
||||
{parkName}
|
||||
</Link>
|
||||
|
||||
{credit.first_ride_date && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
<Calendar className="w-3 h-3 inline mr-1" />
|
||||
First ride: {format(new Date(credit.first_ride_date), 'MMM d, yyyy')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditing ? (
|
||||
<>
|
||||
<Input
|
||||
type="number"
|
||||
value={editCount}
|
||||
onChange={(e) => setEditCount(parseInt(e.target.value) || 1)}
|
||||
className="w-20"
|
||||
min="1"
|
||||
/>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleUpdateCount}
|
||||
disabled={updating}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditCount(credit.ride_count);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold">{credit.ride_count}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{credit.ride_count === 1 ? 'ride' : 'rides'}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={handleQuickIncrement}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Ride Credit?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove {rideName} from your ride credits. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onDelete}>Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Grid view
|
||||
return (
|
||||
<Card className="overflow-hidden">
|
||||
{imageUrl && (
|
||||
<div className="aspect-video relative">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={rideName}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="p-4">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2 mb-1">
|
||||
<Link
|
||||
to={ridePath}
|
||||
className="font-semibold hover:underline line-clamp-2"
|
||||
>
|
||||
{rideName}
|
||||
</Link>
|
||||
{getCategoryBadge(category)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={`/parks/${parkSlug}`}
|
||||
className="text-sm text-muted-foreground hover:underline block truncate"
|
||||
>
|
||||
<MapPin className="w-3 h-3 inline mr-1" />
|
||||
{parkName}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{credit.first_ride_date && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<Calendar className="w-3 h-3 inline mr-1" />
|
||||
{format(new Date(credit.first_ride_date), 'MMM d, yyyy')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Input
|
||||
type="number"
|
||||
value={editCount}
|
||||
onChange={(e) => setEditCount(parseInt(e.target.value) || 1)}
|
||||
className="w-20"
|
||||
min="1"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleUpdateCount}
|
||||
disabled={updating}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
setEditCount(credit.ride_count);
|
||||
}}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-sm">
|
||||
<span className="font-bold text-lg">{credit.ride_count}</span>
|
||||
<span className="text-muted-foreground ml-1">
|
||||
{credit.ride_count === 1 ? 'ride' : 'rides'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleQuickIncrement}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setShowDeleteDialog(true)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Ride Credit?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove {rideName} from your ride credits. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onDelete}>Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user