diff --git a/src/App.tsx b/src/App.tsx index 6b5b84d..bd6f2f9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import AdminMembers from "./pages/admin/AdminMembers"; import AdminEvents from "./pages/admin/AdminEvents"; import AdminSettings from "./pages/admin/AdminSettings"; import AdminConsulting from "./pages/admin/AdminConsulting"; +import AdminReviews from "./pages/admin/AdminReviews"; const queryClient = new QueryClient(); @@ -76,6 +77,7 @@ const App = () => ( } /> } /> } /> + } /> } /> diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index b353eeb..1e5f4bc 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -23,6 +23,7 @@ import { MoreHorizontal, X, Video, + Star, } from 'lucide-react'; interface NavItem { @@ -46,6 +47,7 @@ const adminNavItems: NavItem[] = [ { label: 'Konsultasi', href: '/admin/consulting', icon: Video }, { label: 'Order', href: '/admin/orders', icon: Receipt }, { label: 'Member', href: '/admin/members', icon: Users }, + { label: 'Ulasan', href: '/admin/reviews', icon: Star }, { label: 'Kalender', href: '/admin/events', icon: Calendar }, { label: 'Pengaturan', href: '/admin/settings', icon: Settings }, ]; diff --git a/src/components/WhatsAppBanner.tsx b/src/components/WhatsAppBanner.tsx new file mode 100644 index 0000000..5eeb972 --- /dev/null +++ b/src/components/WhatsAppBanner.tsx @@ -0,0 +1,17 @@ +import { Link } from 'react-router-dom'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Phone } from 'lucide-react'; + +export function WhatsAppBanner() { + return ( + + + + Lengkapi nomor WhatsApp Anda untuk pengingat konsultasi & bootcamp. + + Atur Sekarang + + + + ); +} diff --git a/src/components/reviews/ProductReviews.tsx b/src/components/reviews/ProductReviews.tsx new file mode 100644 index 0000000..5cad30a --- /dev/null +++ b/src/components/reviews/ProductReviews.tsx @@ -0,0 +1,84 @@ +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; + profiles: { full_name: string | null } | null; +} + +interface ProductReviewsProps { + productId: string; + type?: string; +} + +export function ProductReviews({ productId, type }: ProductReviewsProps) { + const [reviews, setReviews] = useState([]); + 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, profiles (full_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); + + 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 ( +
+
+
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+ {stats.avg} + ({stats.count} ulasan) +
+
+ {reviews.map((review) => ( + + ))} +
+
+ ); +} diff --git a/src/components/reviews/ReviewCard.tsx b/src/components/reviews/ReviewCard.tsx new file mode 100644 index 0000000..104f110 --- /dev/null +++ b/src/components/reviews/ReviewCard.tsx @@ -0,0 +1,32 @@ +import { Star } from 'lucide-react'; + +interface ReviewCardProps { + rating: number; + title: string; + body: string; + authorName: string; + date: string; +} + +export function ReviewCard({ rating, title, body, authorName, date }: ReviewCardProps) { + return ( +
+
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+

{title}

+ {body &&

{body}

} +
+ {authorName} + {new Date(date).toLocaleDateString('id-ID')} +
+
+ ); +} diff --git a/src/components/reviews/ReviewForm.tsx b/src/components/reviews/ReviewForm.tsx new file mode 100644 index 0000000..0501286 --- /dev/null +++ b/src/components/reviews/ReviewForm.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import { supabase } from '@/integrations/supabase/client'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { toast } from '@/hooks/use-toast'; +import { Star } from 'lucide-react'; + +interface ReviewFormProps { + userId: string; + productId?: string; + type: 'consulting' | 'bootcamp' | 'webinar' | 'general'; + onSuccess?: () => void; +} + +export function ReviewForm({ userId, productId, type, onSuccess }: ReviewFormProps) { + const [rating, setRating] = useState(0); + const [hoverRating, setHoverRating] = useState(0); + const [title, setTitle] = useState(''); + const [body, setBody] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async () => { + if (rating === 0) { + toast({ title: 'Error', description: 'Pilih rating terlebih dahulu', variant: 'destructive' }); + return; + } + if (!title.trim()) { + toast({ title: 'Error', description: 'Judul tidak boleh kosong', variant: 'destructive' }); + return; + } + + setSubmitting(true); + const { error } = await supabase.from('reviews').insert({ + user_id: userId, + product_id: productId || null, + type, + rating, + title: title.trim(), + body: body.trim(), + is_approved: false, + }); + + if (error) { + toast({ title: 'Error', description: 'Gagal mengirim ulasan', variant: 'destructive' }); + } else { + toast({ title: 'Berhasil', description: 'Ulasan Anda akan ditinjau oleh admin' }); + setRating(0); + setTitle(''); + setBody(''); + onSuccess?.(); + } + setSubmitting(false); + }; + + return ( + + + Beri Ulasan + + +
+ +
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+
+
+ + setTitle(e.target.value)} + placeholder="Ringkasan pengalaman Anda" + className="border-2" + maxLength={100} + /> +
+
+ +