From e512956444f8222a59283a13a55c07b235660a08 Mon Sep 17 00:00:00 2001 From: dwindown Date: Fri, 26 Dec 2025 01:11:11 +0700 Subject: [PATCH] Add celebratory review UI to Bootcamp page & fix access page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bootcamp page changes: - Add UserReview interface to store full review data - Fetch review data with is_approved status - Add celebratory UI when review is approved: - Gradient background with brand accent - "Ulasan Anda Terbit!" heading with "Disetujui" badge - Display user's review with stars, title, body - Publication date - Show pending state with clock icon while waiting approval - Update onSuccess callback to refresh review data MemberAccess page changes: - Change "Lanjutkan Bootcamp" to "Mulai Bootcamp" (clearer) - Fix webinar action buttons: - Check if event_start has passed - Only show "Gabung Webinar" if webinar hasn't ended - Show "Tonton Rekaman" button if recording_url exists - Show "Rekaman segera tersedia" badge for passed webinars without recording 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/pages/Bootcamp.tsx | 98 +++++++++++++++++++++++++++---- src/pages/member/MemberAccess.tsx | 51 ++++++++++------ 2 files changed, 118 insertions(+), 31 deletions(-) diff --git a/src/pages/Bootcamp.tsx b/src/pages/Bootcamp.tsx index d8d1d34..03e3b83 100644 --- a/src/pages/Bootcamp.tsx +++ b/src/pages/Bootcamp.tsx @@ -40,6 +40,15 @@ interface Progress { completed_at: string; } +interface UserReview { + id: string; + rating: number; + title: string; + body: string; + is_approved: boolean; + created_at: string; +} + export default function Bootcamp() { const { slug } = useParams<{ slug: string }>(); const navigate = useNavigate(); @@ -52,7 +61,7 @@ export default function Bootcamp() { const [loading, setLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [hasReviewed, setHasReviewed] = useState(false); + const [userReview, setUserReview] = useState(null); const [reviewModalOpen, setReviewModalOpen] = useState(false); useEffect(() => { @@ -135,12 +144,17 @@ export default function Bootcamp() { // Check if user has already reviewed this bootcamp const { data: reviewData } = await supabase .from('reviews') - .select('id') + .select('id, rating, title, body, is_approved, created_at') .eq('user_id', user!.id) .eq('product_id', productData.id) + .order('created_at', { ascending: false }) .limit(1); - - setHasReviewed(!!(reviewData && reviewData.length > 0)); + + if (reviewData && reviewData.length > 0) { + setUserReview(reviewData[0] as UserReview); + } else { + setUserReview(null); + } setLoading(false); }; @@ -435,14 +449,59 @@ export default function Bootcamp() { {/* Bootcamp completion review prompt */} {isBootcampCompleted && ( - - - {hasReviewed ? ( -
- - Terima kasih atas ulasan Anda (menunggu moderasi) -
+ + + {userReview ? ( + userReview.is_approved ? ( + // Approved review - celebratory display +
+
+
+ +
+
+
+

Ulasan Anda Terbit!

+ Disetujui +
+

Terima kasih telah berbagi pengalaman Anda. Ulasan Anda membantu peserta lain!

+
+
+ + {/* User's review display */} +
+
+ {[1, 2, 3, 4, 5].map((i) => ( + + ))} +
+

{userReview.title}

+ {userReview.body && ( +

{userReview.body}

+ )} +
+ +
+ Diterbitkan pada {new Date(userReview.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })} +
+
+ ) : ( + // Pending review +
+
+ +
+
+

Ulasan Anda sedang ditinjau

+

Terima kasih! Ulasan akan muncul setelah disetujui admin.

+
+
+ ) ) : ( + // No review yet - prompt to review

🎉 Selamat menyelesaikan bootcamp!

@@ -482,7 +541,22 @@ export default function Bootcamp() { productId={product.id} type="bootcamp" contextLabel={product.title} - onSuccess={() => setHasReviewed(true)} + onSuccess={() => { + // Refresh review data + const refreshReview = async () => { + const { data } = await supabase + .from('reviews') + .select('id, rating, title, body, is_approved, created_at') + .eq('user_id', user.id) + .eq('product_id', product.id) + .order('created_at', { ascending: false }) + .limit(1); + if (data && data.length > 0) { + setUserReview(data[0] as UserReview); + } + }; + refreshReview(); + }} /> )}
diff --git a/src/pages/member/MemberAccess.tsx b/src/pages/member/MemberAccess.tsx index e9b26a1..38aede5 100644 --- a/src/pages/member/MemberAccess.tsx +++ b/src/pages/member/MemberAccess.tsx @@ -93,30 +93,43 @@ export default function MemberAccess() { ); case 'webinar': - return ( -
- {item.product.meeting_link && ( - - )} - {item.product.recording_url && ( - + ); + } + + // Only show join link if webinar hasn't ended + if (!webinarEnded && item.product.meeting_link) { + return ( + - )} -
- ); + Gabung Webinar + + + ); + } + + // Webinar ended but no recording yet + if (webinarEnded) { + return Rekaman segera tersedia; + } + + return null; case 'bootcamp': return ( );