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:
@@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<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>
|
<span>{authorName}</span>
|
||||||
|
</div>
|
||||||
<span>{new Date(date).toLocaleDateString('id-ID')}</span>
|
<span>{new Date(date).toLocaleDateString('id-ID')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user