Changes:
- ReviewModal now fetches name from profiles, auth metadata, or email
- Added console logging to debug review data
- Falls back to email username if no name found
- This ensures reviewer_name is always populated
For existing reviews without names, run:
UPDATE reviews r
SET reviewer_name = (
SELECT COALESCE(
raw_user_meta_data->>'name',
SPLIT_PART(email, '@', 1)
)
FROM auth.users WHERE id = r.user_id
)
WHERE reviewer_name IS NULL;
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
88 lines
2.5 KiB
TypeScript
88 lines
2.5 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { ReviewCard } from './ReviewCard';
|
|
import { Star } from 'lucide-react';
|
|
|
|
interface Review {
|
|
id: string;
|
|
rating: number;
|
|
title: string;
|
|
body: string;
|
|
created_at: string;
|
|
reviewer_name: string | null;
|
|
profiles: { name: string | null } | null;
|
|
}
|
|
|
|
interface ProductReviewsProps {
|
|
productId: string;
|
|
type?: string;
|
|
}
|
|
|
|
export function ProductReviews({ productId, type }: ProductReviewsProps) {
|
|
const [reviews, setReviews] = useState<Review[]>([]);
|
|
const [stats, setStats] = useState({ avg: 0, count: 0 });
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
fetchReviews();
|
|
}, [productId, type]);
|
|
|
|
const fetchReviews = async () => {
|
|
let query = supabase
|
|
.from('reviews')
|
|
.select('id, rating, title, body, created_at, reviewer_name, profiles!user_id (name)')
|
|
.eq('is_approved', true);
|
|
|
|
if (productId) {
|
|
query = query.eq('product_id', productId);
|
|
} else if (type) {
|
|
query = query.eq('type', type);
|
|
}
|
|
|
|
const { data } = await query.order('created_at', { ascending: false }).limit(3);
|
|
|
|
console.log('Raw reviews data:', data);
|
|
|
|
if (data && data.length > 0) {
|
|
const typedData = data as unknown as Review[];
|
|
setReviews(typedData);
|
|
const avg = typedData.reduce((sum, r) => sum + r.rating, 0) / typedData.length;
|
|
setStats({ avg: Math.round(avg * 10) / 10, count: typedData.length });
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
if (loading || reviews.length === 0) return null;
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex gap-0.5">
|
|
{[1, 2, 3, 4, 5].map((i) => (
|
|
<Star
|
|
key={i}
|
|
className={`w-5 h-5 ${
|
|
i <= Math.round(stats.avg) ? 'fill-primary text-primary' : 'text-muted-foreground'
|
|
}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
<span className="font-bold">{stats.avg}</span>
|
|
<span className="text-muted-foreground">({stats.count} ulasan)</span>
|
|
</div>
|
|
<div className="grid gap-4">
|
|
{reviews.map((review) => (
|
|
<ReviewCard
|
|
key={review.id}
|
|
rating={review.rating}
|
|
title={review.title}
|
|
body={review.body}
|
|
authorName={review.reviewer_name || review.profiles?.name || 'Anonymous'}
|
|
date={review.created_at}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|