Use live profile data for reviews instead of frozen reviewer_name

Changes:
- Revert to using profiles!user_id (name, avatar_url) JOIN for reviews
- Remove reviewer_name storage from ReviewModal (no longer needed)
- Add avatar display to ReviewCard component
- Reviews now sync automatically with profile changes
- Public queries safely expose only name + avatar via RLS

This ensures:
- Name/avatar changes update across all reviews automatically
- No frozen/outdated reviewer data
- Only public profile fields exposed (secure)
- Reviews serve as live, credible social proof

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
dwindown
2025-12-26 15:39:58 +07:00
parent 74b7dd09ea
commit a824e101ed
4 changed files with 20 additions and 41 deletions

View File

@@ -9,8 +9,7 @@ interface Review {
title: string; title: string;
body: string; body: string;
created_at: string; created_at: string;
reviewer_name: string | null; profiles: { name: string | null; avatar_url: string | null } | null;
profiles: { name: string | null } | null;
} }
interface ProductReviewsProps { interface ProductReviewsProps {
@@ -30,7 +29,7 @@ export function ProductReviews({ productId, type }: ProductReviewsProps) {
const fetchReviews = async () => { const fetchReviews = async () => {
let query = supabase let query = supabase
.from('reviews') .from('reviews')
.select('id, rating, title, body, created_at, reviewer_name, profiles!user_id (name)') .select('id, rating, title, body, created_at, profiles!user_id (name, avatar_url)')
.eq('is_approved', true); .eq('is_approved', true);
if (productId) { if (productId) {
@@ -75,7 +74,8 @@ export function ProductReviews({ productId, type }: ProductReviewsProps) {
rating={review.rating} rating={review.rating}
title={review.title} title={review.title}
body={review.body} body={review.body}
authorName={review.profiles?.name || review.reviewer_name || 'Anonymous'} authorName={review.profiles?.name || 'Anonymous'}
authorAvatar={review.profiles?.avatar_url}
date={review.created_at} date={review.created_at}
/> />
))} ))}

View File

@@ -5,10 +5,11 @@ interface ReviewCardProps {
title: string; title: string;
body: string; body: string;
authorName: string; authorName: string;
authorAvatar?: string | null;
date: string; date: string;
} }
export function ReviewCard({ rating, title, body, authorName, date }: ReviewCardProps) { export function ReviewCard({ rating, title, body, authorName, authorAvatar, date }: ReviewCardProps) {
return ( return (
<div className="border-2 border-border p-6 space-y-3"> <div className="border-2 border-border p-6 space-y-3">
<div className="flex gap-0.5"> <div className="flex gap-0.5">
@@ -24,7 +25,16 @@ export function ReviewCard({ rating, title, body, authorName, date }: ReviewCard
<h4 className="font-bold">{title}</h4> <h4 className="font-bold">{title}</h4>
{body && <p className="text-muted-foreground text-sm">{body}</p>} {body && <p className="text-muted-foreground text-sm">{body}</p>}
<div className="flex items-center justify-between text-xs text-muted-foreground"> <div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{authorName}</span> <div className="flex items-center gap-2">
{authorAvatar && (
<img
src={authorAvatar}
alt={authorName}
className="w-6 h-6 rounded-full object-cover"
/>
)}
<span>{authorName}</span>
</div>
<span>{new Date(date).toLocaleDateString('id-ID')}</span> <span>{new Date(date).toLocaleDateString('id-ID')}</span>
</div> </div>
</div> </div>

View File

@@ -33,36 +33,6 @@ export function ReviewModal({
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [body, setBody] = useState(''); const [body, setBody] = useState('');
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [userName, setUserName] = useState<string | null>(null);
useEffect(() => {
const fetchUserName = async () => {
if (!userId) return;
// Try to get name from profiles table first
const { data: profileData } = await supabase
.from('profiles')
.select('name')
.eq('id', userId)
.maybeSingle();
if (profileData?.name) {
setUserName(profileData.name);
return;
}
// Fallback: get from auth metadata
const { data: { user } } = await supabase.auth.getUser();
if (user?.user_metadata?.name) {
setUserName(user.user_metadata.name);
} else if (user?.email) {
// Use email username as last resort
const emailUsername = user.email.split('@')[0];
setUserName(emailUsername);
}
};
fetchUserName();
}, [userId]);
const handleSubmit = async () => { const handleSubmit = async () => {
if (rating === 0) { if (rating === 0) {
@@ -84,7 +54,6 @@ export function ReviewModal({
title: title.trim(), title: title.trim(),
body: body.trim() || null, body: body.trim() || null,
is_approved: false, is_approved: false,
reviewer_name: userName || null,
}); });
if (error) { if (error) {

View File

@@ -8,8 +8,7 @@ interface Review {
title: string; title: string;
body: string; body: string;
created_at: string; created_at: string;
reviewer_name: string | null; profiles: { name: string | null; avatar_url: string | null } | null;
profiles: { name: string | null } | null;
} }
export function TestimonialsSection() { export function TestimonialsSection() {
@@ -23,7 +22,7 @@ export function TestimonialsSection() {
const fetchReviews = async () => { const fetchReviews = async () => {
const { data } = await supabase const { data } = await supabase
.from('reviews') .from('reviews')
.select('id, rating, title, body, created_at, reviewer_name, profiles!user_id (name)') .select('id, rating, title, body, created_at, profiles!user_id (name, avatar_url)')
.eq('is_approved', true) .eq('is_approved', true)
.order('created_at', { ascending: false }) .order('created_at', { ascending: false })
.limit(6); .limit(6);
@@ -46,7 +45,8 @@ export function TestimonialsSection() {
rating={review.rating} rating={review.rating}
title={review.title} title={review.title}
body={review.body} body={review.body}
authorName={review.profiles?.name || review.reviewer_name || 'Anonymous'} authorName={review.profiles?.name || 'Anonymous'}
authorAvatar={review.profiles?.avatar_url}
date={review.created_at} date={review.created_at}
/> />
))} ))}