Refactor review UX to use name
- Align all review-related components to use profiles.name instead of profiles.full_name - Update AdminReviews, TestimonialsSection, ProductReviews to fetch and display name via name field - Adjust related admin member and consultant views to reference name - Update MemberProfile editing flow placeholders to reflect name field - Ensure public reviews still render with approved reviews only and inline summaries where applicable X-Lovable-Edit-ID: edt-81d7dcc8-ea28-4072-9da0-5a7d623fb1ed
This commit is contained in:
@@ -9,7 +9,7 @@ interface Review {
|
|||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
profiles: { full_name: string | null } | null;
|
profiles: { name: string | null } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProductReviewsProps {
|
interface ProductReviewsProps {
|
||||||
@@ -29,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, profiles:user_id (full_name)')
|
.select('id, rating, title, body, created_at, profiles:user_id (name)')
|
||||||
.eq('is_approved', true);
|
.eq('is_approved', true);
|
||||||
|
|
||||||
if (productId) {
|
if (productId) {
|
||||||
@@ -74,7 +74,7 @@ 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?.full_name || 'Anonymous'}
|
authorName={review.profiles?.name || 'Anonymous'}
|
||||||
date={review.created_at}
|
date={review.created_at}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface Review {
|
|||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
profiles: { full_name: string | null } | null;
|
profiles: { name: string | null } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TestimonialsSection() {
|
export function TestimonialsSection() {
|
||||||
@@ -22,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, profiles:user_id (full_name)')
|
.select('id, rating, title, body, created_at, profiles:user_id (name)')
|
||||||
.eq('is_approved', true)
|
.eq('is_approved', true)
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(6);
|
.limit(6);
|
||||||
@@ -45,7 +45,7 @@ export function TestimonialsSection() {
|
|||||||
rating={review.rating}
|
rating={review.rating}
|
||||||
title={review.title}
|
title={review.title}
|
||||||
body={review.body}
|
body={review.body}
|
||||||
authorName={review.profiles?.full_name || 'Anonymous'}
|
authorName={review.profiles?.name || 'Anonymous'}
|
||||||
date={review.created_at}
|
date={review.created_at}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ interface ConsultingSlot {
|
|||||||
meet_link: string | null;
|
meet_link: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
profiles?: {
|
profiles?: {
|
||||||
full_name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export default function AdminConsulting() {
|
|||||||
.from('consulting_slots')
|
.from('consulting_slots')
|
||||||
.select(`
|
.select(`
|
||||||
*,
|
*,
|
||||||
profiles:user_id (full_name, email)
|
profiles:user_id (name, email)
|
||||||
`)
|
`)
|
||||||
.order('date', { ascending: false })
|
.order('date', { ascending: false })
|
||||||
.order('start_time', { ascending: true });
|
.order('start_time', { ascending: true });
|
||||||
@@ -125,7 +125,7 @@ export default function AdminConsulting() {
|
|||||||
body: {
|
body: {
|
||||||
template_key: 'consulting_scheduled',
|
template_key: 'consulting_scheduled',
|
||||||
recipient_email: selectedSlot.profiles.email,
|
recipient_email: selectedSlot.profiles.email,
|
||||||
recipient_name: selectedSlot.profiles.full_name,
|
recipient_name: selectedSlot.profiles.name,
|
||||||
variables: {
|
variables: {
|
||||||
consultation_date: format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id }),
|
consultation_date: format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id }),
|
||||||
consultation_time: `${selectedSlot.start_time.substring(0, 5)} - ${selectedSlot.end_time.substring(0, 5)}`,
|
consultation_time: `${selectedSlot.start_time.substring(0, 5)} - ${selectedSlot.end_time.substring(0, 5)}`,
|
||||||
@@ -168,7 +168,7 @@ export default function AdminConsulting() {
|
|||||||
start_time: selectedSlot.start_time,
|
start_time: selectedSlot.start_time,
|
||||||
end_time: selectedSlot.end_time,
|
end_time: selectedSlot.end_time,
|
||||||
topic: selectedSlot.topic_category,
|
topic: selectedSlot.topic_category,
|
||||||
client_name: selectedSlot.profiles?.full_name || 'Client',
|
client_name: selectedSlot.profiles?.name || 'Client',
|
||||||
client_email: selectedSlot.profiles?.email,
|
client_email: selectedSlot.profiles?.email,
|
||||||
calendar_id: settings.integration_google_calendar_id,
|
calendar_id: settings.integration_google_calendar_id,
|
||||||
}),
|
}),
|
||||||
@@ -251,7 +251,7 @@ export default function AdminConsulting() {
|
|||||||
{todaySlots.map(slot => (
|
{todaySlots.map(slot => (
|
||||||
<div key={slot.id} className="flex items-center justify-between text-sm">
|
<div key={slot.id} className="flex items-center justify-between text-sm">
|
||||||
<span>
|
<span>
|
||||||
{slot.start_time.substring(0, 5)} - {slot.profiles?.full_name || 'N/A'} ({slot.topic_category})
|
{slot.start_time.substring(0, 5)} - {slot.profiles?.name || 'N/A'} ({slot.topic_category})
|
||||||
</span>
|
</span>
|
||||||
{slot.meet_link ? (
|
{slot.meet_link ? (
|
||||||
<a href={slot.meet_link} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1">
|
<a href={slot.meet_link} target="_blank" rel="noopener noreferrer" className="text-primary underline flex items-center gap-1">
|
||||||
@@ -334,7 +334,7 @@ export default function AdminConsulting() {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{slot.profiles?.full_name || '-'}</p>
|
<p className="font-medium">{slot.profiles?.name || '-'}</p>
|
||||||
<p className="text-sm text-muted-foreground">{slot.profiles?.email}</p>
|
<p className="text-sm text-muted-foreground">{slot.profiles?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -425,7 +425,7 @@ export default function AdminConsulting() {
|
|||||||
<TableRow key={slot.id}>
|
<TableRow key={slot.id}>
|
||||||
<TableCell>{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}</TableCell>
|
<TableCell>{format(parseISO(slot.date), 'd MMM yyyy', { locale: id })}</TableCell>
|
||||||
<TableCell>{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}</TableCell>
|
<TableCell>{slot.start_time.substring(0, 5)} - {slot.end_time.substring(0, 5)}</TableCell>
|
||||||
<TableCell>{slot.profiles?.full_name || '-'}</TableCell>
|
<TableCell>{slot.profiles?.name || '-'}</TableCell>
|
||||||
<TableCell><Badge variant="outline">{slot.topic_category}</Badge></TableCell>
|
<TableCell><Badge variant="outline">{slot.topic_category}</Badge></TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
<Badge variant={statusLabels[slot.status]?.variant || 'secondary'}>
|
||||||
@@ -462,7 +462,7 @@ export default function AdminConsulting() {
|
|||||||
<div className="p-3 bg-muted rounded-lg text-sm space-y-1">
|
<div className="p-3 bg-muted rounded-lg text-sm space-y-1">
|
||||||
<p><strong>Tanggal:</strong> {format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id })}</p>
|
<p><strong>Tanggal:</strong> {format(parseISO(selectedSlot.date), 'd MMMM yyyy', { locale: id })}</p>
|
||||||
<p><strong>Waktu:</strong> {selectedSlot.start_time.substring(0, 5)} - {selectedSlot.end_time.substring(0, 5)}</p>
|
<p><strong>Waktu:</strong> {selectedSlot.start_time.substring(0, 5)} - {selectedSlot.end_time.substring(0, 5)}</p>
|
||||||
<p><strong>Klien:</strong> {selectedSlot.profiles?.full_name}</p>
|
<p><strong>Klien:</strong> {selectedSlot.profiles?.name}</p>
|
||||||
<p><strong>Topik:</strong> {selectedSlot.topic_category}</p>
|
<p><strong>Topik:</strong> {selectedSlot.topic_category}</p>
|
||||||
{selectedSlot.notes && <p><strong>Catatan:</strong> {selectedSlot.notes}</p>}
|
{selectedSlot.notes && <p><strong>Catatan:</strong> {selectedSlot.notes}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { toast } from "@/hooks/use-toast";
|
|||||||
interface Member {
|
interface Member {
|
||||||
id: string;
|
id: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
full_name: string | null;
|
name: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ export default function AdminMembers() {
|
|||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell>{member.email || "-"}</TableCell>
|
<TableCell>{member.email || "-"}</TableCell>
|
||||||
<TableCell>{member.full_name || "-"}</TableCell>
|
<TableCell>{member.name || "-"}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{adminIds.has(member.id) ? (
|
{adminIds.has(member.id) ? (
|
||||||
<Badge className="bg-primary">Admin</Badge>
|
<Badge className="bg-primary">Admin</Badge>
|
||||||
@@ -166,7 +166,7 @@ export default function AdminMembers() {
|
|||||||
<span className="text-muted-foreground">Email:</span> {selectedMember.email}
|
<span className="text-muted-foreground">Email:</span> {selectedMember.email}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">Nama:</span> {selectedMember.full_name || "-"}
|
<span className="text-muted-foreground">Nama:</span> {selectedMember.name || "-"}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="text-muted-foreground">ID:</span> {selectedMember.id}
|
<span className="text-muted-foreground">ID:</span> {selectedMember.id}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface Review {
|
|||||||
body: string;
|
body: string;
|
||||||
is_approved: boolean;
|
is_approved: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
profiles: { full_name: string | null; email: string | null } | null;
|
profiles: { name: string | null; email: string | null } | null;
|
||||||
products: { title: string } | null;
|
products: { title: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export default function AdminReviews() {
|
|||||||
.from("reviews")
|
.from("reviews")
|
||||||
.select(`
|
.select(`
|
||||||
*,
|
*,
|
||||||
profiles:user_id (full_name, email),
|
profiles:user_id (name, email),
|
||||||
products:product_id (title)
|
products:product_id (title)
|
||||||
`)
|
`)
|
||||||
.order("created_at", { ascending: false });
|
.order("created_at", { ascending: false });
|
||||||
@@ -224,7 +224,7 @@ export default function AdminReviews() {
|
|||||||
<h3 className="font-bold">{review.title}</h3>
|
<h3 className="font-bold">{review.title}</h3>
|
||||||
{review.body && <p className="text-muted-foreground text-sm">{review.body}</p>}
|
{review.body && <p className="text-muted-foreground text-sm">{review.body}</p>}
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
<span>{review.profiles?.full_name || review.profiles?.email || "Unknown"}</span>
|
<span>{review.profiles?.name || review.profiles?.email || "Unknown"}</span>
|
||||||
{review.products && <span> • {review.products.title}</span>}
|
{review.products && <span> • {review.products.title}</span>}
|
||||||
<span> • {new Date(review.created_at).toLocaleDateString("id-ID")}</span>
|
<span> • {new Date(review.created_at).toLocaleDateString("id-ID")}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,7 +300,7 @@ export default function AdminReviews() {
|
|||||||
<h3 className="font-bold">{review.title}</h3>
|
<h3 className="font-bold">{review.title}</h3>
|
||||||
{review.body && <p className="text-muted-foreground text-sm">{review.body}</p>}
|
{review.body && <p className="text-muted-foreground text-sm">{review.body}</p>}
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{review.profiles?.full_name || review.profiles?.email}
|
{review.profiles?.name || review.profiles?.email}
|
||||||
{review.products && <span> • {review.products.title}</span>}
|
{review.products && <span> • {review.products.title}</span>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { User, LogOut, Phone } from 'lucide-react';
|
|||||||
interface Profile {
|
interface Profile {
|
||||||
id: string;
|
id: string;
|
||||||
email: string | null;
|
email: string | null;
|
||||||
full_name: string | null;
|
name: string | null;
|
||||||
avatar_url: string | null;
|
avatar_url: string | null;
|
||||||
whatsapp_number: string | null;
|
whatsapp_number: string | null;
|
||||||
whatsapp_opt_in: boolean;
|
whatsapp_opt_in: boolean;
|
||||||
@@ -28,7 +28,7 @@ export default function MemberProfile() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [form, setForm] = useState({
|
const [form, setForm] = useState({
|
||||||
full_name: '',
|
name: '',
|
||||||
avatar_url: '',
|
avatar_url: '',
|
||||||
whatsapp_number: '',
|
whatsapp_number: '',
|
||||||
whatsapp_opt_in: false,
|
whatsapp_opt_in: false,
|
||||||
@@ -48,7 +48,7 @@ export default function MemberProfile() {
|
|||||||
if (data) {
|
if (data) {
|
||||||
setProfile(data);
|
setProfile(data);
|
||||||
setForm({
|
setForm({
|
||||||
full_name: data.full_name || '',
|
name: data.name || '',
|
||||||
avatar_url: data.avatar_url || '',
|
avatar_url: data.avatar_url || '',
|
||||||
whatsapp_number: data.whatsapp_number || '',
|
whatsapp_number: data.whatsapp_number || '',
|
||||||
whatsapp_opt_in: data.whatsapp_opt_in || false,
|
whatsapp_opt_in: data.whatsapp_opt_in || false,
|
||||||
@@ -78,7 +78,7 @@ export default function MemberProfile() {
|
|||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.update({
|
.update({
|
||||||
full_name: form.full_name,
|
name: form.name,
|
||||||
avatar_url: form.avatar_url || null,
|
avatar_url: form.avatar_url || null,
|
||||||
whatsapp_number: normalizedWA || null,
|
whatsapp_number: normalizedWA || null,
|
||||||
whatsapp_opt_in: form.whatsapp_opt_in,
|
whatsapp_opt_in: form.whatsapp_opt_in,
|
||||||
@@ -132,8 +132,8 @@ export default function MemberProfile() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Nama Lengkap</Label>
|
<Label>Nama Lengkap</Label>
|
||||||
<Input
|
<Input
|
||||||
value={form.full_name}
|
value={form.name}
|
||||||
onChange={(e) => setForm({ ...form, full_name: e.target.value })}
|
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||||
className="border-2"
|
className="border-2"
|
||||||
placeholder="Masukkan nama lengkap"
|
placeholder="Masukkan nama lengkap"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user