Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-11-09 16:31:34 -05:00
parent 2884bc23ce
commit eb68cf40c6
1080 changed files with 27361 additions and 56687 deletions

View File

@@ -0,0 +1,38 @@
/**
* Memoized Review Card Component
* Optimized for list rendering performance
*/
import React from 'react';
interface Review {
id: string;
rating: number;
comment: string | null;
created_at: string;
updated_at: string;
user_id: string;
}
interface ReviewCardProps {
review: Review;
onEdit?: (review: Review) => void;
onDelete?: (reviewId: string) => void;
}
export const ReviewCard: React.FC<ReviewCardProps> = ({ review, onEdit, onDelete }) => {
// Component implementation would go here
// This is a placeholder for the actual review card
return null;
};
export const ReviewCardMemo = React.memo(ReviewCard, (prevProps, nextProps) => {
return (
prevProps.review.id === nextProps.review.id &&
prevProps.review.rating === nextProps.review.rating &&
prevProps.review.comment === nextProps.review.comment &&
prevProps.review.updated_at === nextProps.review.updated_at
);
});
ReviewCardMemo.displayName = 'ReviewCardMemo';

View File

@@ -0,0 +1,212 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { DatePicker } from '@/components/ui/date-picker';
import { Star, Send } from 'lucide-react';
import { useAuth } from '@/hooks/useAuth';
import { supabase } from '@/lib/supabaseClient';
import { toast } from '@/hooks/use-toast';
import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { StarRating } from './StarRating';
import { toDateOnly, parseDateOnly } from '@/lib/dateUtils';
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
const reviewSchema = z.object({
rating: z.number().min(0.5).max(5).multipleOf(0.5),
title: z.string().optional(),
content: z.string().min(10, 'Review must be at least 10 characters long'),
visit_date: z.string().optional(),
wait_time_minutes: z.number().positive().optional(),
photos: z.array(z.string()).optional()
});
type ReviewFormData = z.infer<typeof reviewSchema>;
interface ReviewFormProps {
entityType: 'park' | 'ride';
entityId: string;
entityName: string;
onReviewSubmitted: () => void;
}
export function ReviewForm({
entityType,
entityId,
entityName,
onReviewSubmitted
}: ReviewFormProps) {
const {
user
} = useAuth();
const [rating, setRating] = useState(0);
const [submitting, setSubmitting] = useState(false);
const [photos, setPhotos] = useState<string[]>([]);
const {
register,
handleSubmit,
reset,
setValue,
watch,
formState: {
errors
}
} = useForm<ReviewFormData>({
resolver: zodResolver(reviewSchema)
});
const handleRatingChange = (selectedRating: number) => {
setRating(selectedRating);
setValue('rating', selectedRating);
};
const onSubmit = async (data: ReviewFormData) => {
if (!user) {
toast({
title: "Authentication Required",
description: "Please sign in to submit a review.",
variant: "destructive"
});
return;
}
setSubmitting(true);
try {
// Insert review first
const reviewData = {
user_id: user.id,
rating: data.rating,
title: data.title || null,
content: data.content,
visit_date: data.visit_date || null,
wait_time_minutes: data.wait_time_minutes || null,
moderation_status: 'pending' as const,
...(entityType === 'park' ? {
park_id: entityId
} : {
ride_id: entityId
})
};
const { data: review, error: reviewError } = await supabase
.from('reviews')
.insert([reviewData])
.select()
.single();
if (reviewError) throw reviewError;
// Insert photos into review_photos table if any
if (photos.length > 0 && review) {
const photoRecords = photos.map((url, index) => ({
review_id: review.id,
cloudflare_image_id: url.split('/').slice(-2, -1)[0] || '', // Extract ID from URL
cloudflare_image_url: url,
order_index: index,
}));
const { error: photosError } = await supabase
.from('review_photos')
.insert(photoRecords);
if (photosError) {
handleNonCriticalError(photosError, {
action: 'Insert review photos',
userId: user?.id,
metadata: { reviewId: review.id, photoCount: photos.length }
});
// Don't throw - review is already created
}
}
toast({
title: "Review Submitted!",
description: "Thank you for your review. It will be published after moderation."
});
reset();
setRating(0);
setPhotos([]);
onReviewSubmitted();
} catch (error: unknown) {
toast({
title: "Error",
description: getErrorMessage(error),
variant: "destructive"
});
} finally {
setSubmitting(false);
}
};
if (!user) {
return <Card>
<CardContent className="p-6 text-center">
<Star className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Share Your Experience</h3>
<p className="text-muted-foreground mb-4">
Sign in to write a review for {entityName}
</p>
<Button onClick={() => window.location.href = '/auth'}>
Sign In to Review
</Button>
</CardContent>
</Card>;
}
return <Card>
<CardHeader>
<CardTitle>Write a Review for {entityName}</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{/* Rating */}
<div className="space-y-2">
<Label>Rating *</Label>
<StarRating rating={rating} onChange={handleRatingChange} interactive={true} showLabel={true} size="lg" />
{errors.rating && <p className="text-sm text-destructive">Please select a rating</p>}
</div>
{/* Title */}
<div className="space-y-2">
<Label htmlFor="title">Review Title</Label>
<Input id="title" placeholder="Give your review a title" {...register('title')} />
</div>
{/* Content */}
<div className="space-y-2">
<Label htmlFor="content">Your Review *</Label>
<Textarea id="content" placeholder="Share your experience..." rows={4} {...register('content')} />
{errors.content && <p className="text-sm text-destructive">{errors.content.message}</p>}
</div>
{/* Visit Date */}
<div className="space-y-2">
<Label>Visit Date</Label>
<DatePicker
date={watch('visit_date') ? parseDateOnly(watch('visit_date') || '') : undefined}
onSelect={(date) => setValue('visit_date', date ? toDateOnly(date) : '')}
placeholder="When did you visit?"
disableFuture={true}
fromYear={1950}
/>
<p className="text-xs text-muted-foreground">
Select the date of your visit to help others understand when this experience occurred.
</p>
</div>
{/* Wait Time (for rides) */}
{entityType === 'ride' && <div className="space-y-2">
<Label htmlFor="wait_time">Wait Time (minutes)</Label>
<Input id="wait_time" type="number" min="0" placeholder="How long did you wait?" {...register('wait_time_minutes', {
valueAsNumber: true,
setValueAs: (v) => v === '' || isNaN(v) ? undefined : v
})} />
</div>}
{/* Photo Upload */}
<Button type="submit" loading={submitting} loadingText="Submitting..." className="w-full">
<Send className="w-4 h-4 mr-2" />
Submit Review
</Button>
</form>
</CardContent>
</Card>;
}

View File

@@ -0,0 +1,204 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { UserAvatar } from '@/components/ui/user-avatar';
import { Star, ThumbsUp, Calendar, MapPin } from 'lucide-react';
import { supabase } from '@/lib/supabaseClient';
import { ReportButton } from '@/components/moderation/ReportButton';
import { StarRating } from './StarRating';
import { getErrorMessage, handleNonCriticalError } from '@/lib/errorHandler';
interface ReviewWithProfile {
id: string;
user_id: string;
park_id: string | null;
ride_id: string | null;
rating: number;
title: string | null;
content: string | null;
visit_date: string | null;
wait_time_minutes: number | null;
helpful_votes: number;
total_votes: number;
moderation_status: string;
created_at: string;
updated_at: string;
profiles?: {
username: string;
avatar_url: string | null;
display_name: string | null;
} | null;
}
interface ReviewsListProps {
entityType: 'park' | 'ride';
entityId: string;
entityName: string;
}
export function ReviewsList({ entityType, entityId, entityName }: ReviewsListProps) {
const [reviews, setReviews] = useState<ReviewWithProfile[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchReviews();
}, [entityType, entityId]);
const fetchReviews = async () => {
try {
const query = supabase
.from('reviews')
.select(`
*,
profiles!reviews_user_id_fkey(username, avatar_url, display_name)
`)
.eq('moderation_status', 'approved')
.order('created_at', { ascending: false });
if (entityType === 'park') {
query.eq('park_id', entityId);
} else {
query.eq('ride_id', entityId);
}
const { data } = await query;
setReviews((data || []) as ReviewWithProfile[]);
} catch (error: unknown) {
handleNonCriticalError(error, {
action: 'Fetch reviews',
metadata: { entityType, entityId }
});
} finally {
setLoading(false);
}
};
const renderStars = (rating: number) => {
return <StarRating rating={rating} size="sm" />;
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
if (loading) {
return (
<div className="space-y-4">
{Array.from({ length: 3 }, (_, i) => (
<Card key={i} className="animate-pulse">
<CardContent className="p-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-muted rounded-full"></div>
<div className="flex-1 space-y-2">
<div className="h-4 bg-muted rounded w-1/4"></div>
<div className="h-4 bg-muted rounded w-1/6"></div>
<div className="h-20 bg-muted rounded"></div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
);
}
if (reviews.length === 0) {
return (
<div className="text-center py-12">
<Star className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No Reviews Yet</h3>
<p className="text-muted-foreground">
Be the first to share your experience with {entityName}!
</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">
{reviews.length} Review{reviews.length !== 1 ? 's' : ''}
</h3>
</div>
<div className="space-y-4">
{reviews.map((review) => (
<Card key={review.id}>
<CardContent className="p-6">
<div className="flex items-start gap-4">
<UserAvatar
key={review.profiles?.avatar_url || `review-${review.id}`}
avatarUrl={review.profiles?.avatar_url}
fallbackText={review.profiles?.display_name || review.profiles?.username || 'U'}
size="lg"
className="w-12 h-12"
/>
<div className="flex-1 space-y-3">
<div className="flex items-center justify-between">
<div>
<div className="font-medium">
{review.profiles?.display_name || review.profiles?.username || 'Anonymous User'}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Calendar className="w-3 h-3" />
{formatDate(review.created_at)}
{review.visit_date && (
<>
<span></span>
<span>Visited {formatDate(review.visit_date)}</span>
</>
)}
</div>
</div>
<div className="flex items-center gap-1">
{renderStars(review.rating)}
<span className="ml-1 text-sm font-medium">
{review.rating}
</span>
</div>
</div>
{review.title && (
<h4 className="font-medium text-lg">{review.title}</h4>
)}
{review.content && (
<p className="text-muted-foreground leading-relaxed">
{review.content}
</p>
)}
<div className="flex items-center gap-4 text-sm">
{review.wait_time_minutes && (
<div className="flex items-center gap-1 text-muted-foreground">
<MapPin className="w-3 h-3" />
<span>Wait time: {review.wait_time_minutes} min</span>
</div>
)}
<div className="flex items-center gap-1 text-muted-foreground">
<ThumbsUp className="w-3 h-3" />
<span>{review.helpful_votes} helpful</span>
</div>
<ReportButton
entityType="review"
entityId={review.id}
className="text-xs"
/>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,111 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Star, Plus } from 'lucide-react';
import { ReviewsList } from './ReviewsList';
import { ReviewForm } from './ReviewForm';
interface ReviewsSectionProps {
entityType: 'park' | 'ride';
entityId: string;
entityName: string;
averageRating: number;
reviewCount: number;
}
export function ReviewsSection({
entityType,
entityId,
entityName,
averageRating,
reviewCount
}: ReviewsSectionProps) {
const [showForm, setShowForm] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
const handleReviewSubmitted = () => {
setShowForm(false);
setRefreshKey(prev => prev + 1);
};
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, i) => (
<Star
key={i}
className={`w-5 h-5 ${
i < Math.floor(rating)
? 'fill-yellow-400 text-yellow-400'
: i < rating
? 'fill-yellow-400/50 text-yellow-400'
: 'text-muted-foreground'
}`}
/>
));
};
return (
<div className="space-y-6">
{/* Reviews Summary */}
{reviewCount > 0 && (
<Card>
<CardContent className="p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="text-center">
<div className="text-3xl font-bold">{averageRating.toFixed(1)}</div>
<div className="flex items-center gap-1 justify-center">
{renderStars(averageRating)}
</div>
<div className="text-sm text-muted-foreground">
{reviewCount} review{reviewCount !== 1 ? 's' : ''}
</div>
</div>
</div>
<Button onClick={() => setShowForm(!showForm)}>
<Plus className="w-4 h-4 mr-2" />
Write Review
</Button>
</div>
</CardContent>
</Card>
)}
{/* Review Form */}
{showForm && (
<ReviewForm
entityType={entityType}
entityId={entityId}
entityName={entityName}
onReviewSubmitted={handleReviewSubmitted}
/>
)}
{/* Show write review button if no reviews yet and form not shown */}
{reviewCount === 0 && !showForm && (
<Card>
<CardContent className="p-6 text-center">
<Star className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No Reviews Yet</h3>
<p className="text-muted-foreground mb-4">
Be the first to share your experience with {entityName}!
</p>
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Write First Review
</Button>
</CardContent>
</Card>
)}
{/* Reviews List */}
<ReviewsList
key={refreshKey}
entityType={entityType}
entityId={entityId}
entityName={entityName}
/>
</div>
);
}

View File

@@ -0,0 +1,110 @@
import { useState } from 'react';
import { Star } from 'lucide-react';
import { cn } from '@/lib/utils';
interface StarRatingProps {
rating: number;
onChange?: (rating: number) => void;
size?: 'sm' | 'md' | 'lg';
interactive?: boolean;
showLabel?: boolean;
className?: string;
}
export function StarRating({
rating,
onChange,
size = 'md',
interactive = false,
showLabel = false,
className
}: StarRatingProps) {
const [hoverRating, setHoverRating] = useState(0);
const sizes = {
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8'
};
const starSize = sizes[size];
const displayRating = hoverRating || rating;
const handleStarClick = (starIndex: number, isHalf: boolean) => {
if (!interactive || !onChange) return;
const newRating = starIndex + (isHalf ? 0.5 : 1);
onChange(newRating);
};
const handleStarHover = (starIndex: number, isHalf: boolean) => {
if (!interactive) return;
const newHoverRating = starIndex + (isHalf ? 0.5 : 1);
setHoverRating(newHoverRating);
};
const handleMouseLeave = () => {
if (!interactive) return;
setHoverRating(0);
};
const renderStar = (index: number) => {
const starValue = index + 1;
const isFullStar = displayRating >= starValue;
const isHalfStar = displayRating >= starValue - 0.5 && displayRating < starValue;
const getStarFill = () => {
if (isFullStar) return '100%';
if (isHalfStar) return '50%';
return '0%';
};
return (
<div key={index} className="relative inline-block">
{/* Background unfilled star */}
<Star className={cn(starSize, 'text-muted-foreground')} />
{/* Filled star overlay */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: getStarFill() }}
>
<Star className={cn(starSize, 'fill-yellow-400 text-yellow-400')} />
</div>
{/* Interactive click areas */}
{interactive && (
<>
{/* Left half click area */}
<div
className="absolute top-0 left-0 w-1/2 h-full cursor-pointer z-10"
onClick={() => handleStarClick(index, true)}
onMouseEnter={() => handleStarHover(index, true)}
onMouseLeave={handleMouseLeave}
/>
{/* Right half click area */}
<div
className="absolute top-0 right-0 w-1/2 h-full cursor-pointer z-10"
onClick={() => handleStarClick(index, false)}
onMouseEnter={() => handleStarHover(index, false)}
onMouseLeave={handleMouseLeave}
/>
</>
)}
</div>
);
};
return (
<div className={cn('flex items-center gap-1', className)}>
<div className="flex items-center">
{Array.from({ length: 5 }, (_, i) => renderStar(i))}
</div>
{showLabel && (
<span className="ml-2 text-sm text-muted-foreground">
{displayRating} star{displayRating !== 1 ? 's' : ''}
</span>
)}
</div>
);
}