From 21cd10fa0790495cfc90470c781ee4937bdf63a3 Mon Sep 17 00:00:00 2001 From: im-rk Date: Thu, 25 Jun 2026 15:07:11 +0530 Subject: [PATCH] feat: impl user module --- src/app/booking/checkout/[id]/page.tsx | 204 +++++++ src/app/{ => dashboard}/bookings/page.tsx | 0 .../events/[id]/categories/page.tsx | 0 src/app/dashboard/events/[id]/page.tsx | 474 +++++++++++++++ .../events/[id]/seatmap/page.tsx | 0 src/app/{ => dashboard}/events/page.tsx | 0 src/app/dashboard/page.tsx | 192 ++++++ src/app/{ => dashboard}/seat-mapping/page.tsx | 0 src/app/{ => dashboard}/venues/[id]/page.tsx | 0 src/app/{ => dashboard}/venues/page.tsx | 0 src/app/events/[id]/page.tsx | 547 +++++------------- src/app/page.tsx | 317 +++++----- src/components/layout/PublicLayout.tsx | 179 ++++++ src/components/layout/Sidebar.tsx | 10 +- src/components/public/SeatSelectionDrawer.tsx | 216 +++++++ src/hooks/useApi.ts | 78 +++ 16 files changed, 1624 insertions(+), 593 deletions(-) create mode 100644 src/app/booking/checkout/[id]/page.tsx rename src/app/{ => dashboard}/bookings/page.tsx (100%) rename src/app/{ => dashboard}/events/[id]/categories/page.tsx (100%) create mode 100644 src/app/dashboard/events/[id]/page.tsx rename src/app/{ => dashboard}/events/[id]/seatmap/page.tsx (100%) rename src/app/{ => dashboard}/events/page.tsx (100%) create mode 100644 src/app/dashboard/page.tsx rename src/app/{ => dashboard}/seat-mapping/page.tsx (100%) rename src/app/{ => dashboard}/venues/[id]/page.tsx (100%) rename src/app/{ => dashboard}/venues/page.tsx (100%) create mode 100644 src/components/layout/PublicLayout.tsx create mode 100644 src/components/public/SeatSelectionDrawer.tsx diff --git a/src/app/booking/checkout/[id]/page.tsx b/src/app/booking/checkout/[id]/page.tsx new file mode 100644 index 0000000..925e432 --- /dev/null +++ b/src/app/booking/checkout/[id]/page.tsx @@ -0,0 +1,204 @@ +'use client' + +import React, { useState, useEffect } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { PublicLayout } from '@/components/layout/PublicLayout' +import { useBookingDetails, useSinglePayBooking } from '@/hooks/useApi' +import { Timer, CreditCard, Users, ShieldCheck, ChevronRight, AlertCircle, ArrowLeft } from 'lucide-react' + +export default function CheckoutCockpit() { + const params = useParams() + const router = useRouter() + const bookingId = params.id as string + + const { data: booking, isLoading } = useBookingDetails(bookingId) + const singlePay = useSinglePayBooking() + + // 10 minute countdown timer (600 seconds) + const [timeLeft, setTimeLeft] = useState(600) + + useEffect(() => { + if (timeLeft <= 0) return + const timer = setInterval(() => { + setTimeLeft(prev => prev - 1) + }, 1000) + return () => clearInterval(timer) + }, [timeLeft]) + + const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60) + const s = seconds % 60 + return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` + } + + const handleSinglePay = async () => { + try { + const idempotencyKey = crypto.randomUUID() + const result = await singlePay.mutateAsync({ bookingId, idempotencyKey }) + + // If Stripe returns clientSecret, redirect to Stripe checkout or embed elements + // For now, if we get a URL, we redirect (assuming hosted checkout for simplicity, or we can use elements) + if (result.clientSecret) { + // Just as a placeholder for the actual stripe element integration + alert("Stripe initialized! Redirecting to payment...") + } + } catch (err) { + console.error("Payment failed", err) + alert("Failed to initialize payment gateway.") + } + } + + const handleSplitPay = () => { + alert("Split Pay group initialization is coming soon!") + } + + if (isLoading) { + return ( + +
+
+
+ + ) + } + + if (!booking) { + return ( + +
+

Booking Session Expired

+ +
+
+ ) + } + + return ( + +
+
+ + {/* Header Row */} +
+
+ +

Checkout

+

Complete your purchase to secure your seats.

+
+ + {/* The Countdown Timer */} +
+
+ +
+
+

Time Remaining

+

{formatTime(timeLeft)}

+
+
+
+ +
+ + {/* Left Section - Booking Summary */} +
+
+

Order Summary

+ +
+ {/* Mock Event Info for now since booking details might not have full event entity populated deeply yet depending on backend */} +
+

Event

+

{(booking as any).event?.name || 'RushTix Event'}

+
+ +
+

Selected Seats

+
+ {(booking as any).seats?.map((seat: any) => ( + + {seat.rowLabel}{seat.columnNumber} + + ))} +
+
+ +
+
+

Subtotal ({(booking as any).seats?.length || 0} seats)

+

₹{booking.totalAmount}

+
+
+

Taxes & Fees

+

₹0

+
+
+

Total Amount

+

₹{booking.totalAmount}

+
+
+
+
+ +
+ +

+ Your connection is secure. We use 256-bit encryption to protect your payment details. +

+
+
+ + {/* Right Section - The Two Payment Pillars */} +
+

How would you like to pay?

+ +
+ + {/* Card A (Single Pay) */} +
+
+ +
+

Pay Full Amount Now

+

+ Pay for all tickets instantly using credit card, debit card, or UPI. Secure your seats immediately for the entire group. +

+ +
+ + {/* Card B (Split Pay) */} +
+
+ +
+

Split Bill With Friends 👥

+

+ Lock these seats right now for free. We will generate split payment links for your group chat so everyone pays their own share. +

+ +
+ +
+
+ +
+
+
+
+ ) +} diff --git a/src/app/bookings/page.tsx b/src/app/dashboard/bookings/page.tsx similarity index 100% rename from src/app/bookings/page.tsx rename to src/app/dashboard/bookings/page.tsx diff --git a/src/app/events/[id]/categories/page.tsx b/src/app/dashboard/events/[id]/categories/page.tsx similarity index 100% rename from src/app/events/[id]/categories/page.tsx rename to src/app/dashboard/events/[id]/categories/page.tsx diff --git a/src/app/dashboard/events/[id]/page.tsx b/src/app/dashboard/events/[id]/page.tsx new file mode 100644 index 0000000..5cd3b27 --- /dev/null +++ b/src/app/dashboard/events/[id]/page.tsx @@ -0,0 +1,474 @@ +'use client' + +import { useState } from 'react' +import { useParams, useRouter } from 'next/navigation' +import { DashboardLayout } from '@/components/layout/DashboardLayout' +import { useEventById, useUpdateEvent, usePublishEvent, useCancelEvent, useDeleteEvent, useVenues } from '@/hooks/useApi' +import { EventRequest } from '@/lib/types' +import { + ArrowLeft, + Edit2, + Trash2, + Calendar, + MapPin, + Loader2, + Clock, + Users, + CheckCircle, + AlertCircle, + XCircle, + Tag, + AlignLeft, + LayoutGrid +} from 'lucide-react' + +// Helper to format ISO datetime string from backend (UTC) to YYYY-MM-DDTHH:mm for datetime-local input +const formatToLocalDatetime = (isoString?: string) => { + if (!isoString) return '' + const date = new Date(isoString) + return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16) +} + +// Helper to convert local datetime string back to UTC ISO string for backend +const convertToIsoString = (localString: string) => { + if (!localString) return undefined + return new Date(localString).toISOString() +} + +export default function EventDetailPage() { + const params = useParams() + const router = useRouter() + const eventId = params.id as string + + const { data: event, isLoading, error: fetchError } = useEventById(eventId) + const { data: venues = [] } = useVenues() + + const updateEvent = useUpdateEvent() + const publishEvent = usePublishEvent() + const cancelEvent = useCancelEvent() + const deleteEvent = useDeleteEvent() + + const [isEditing, setIsEditing] = useState(false) + const [formError, setFormError] = useState(null) + const [formData, setFormData] = useState({ + title: '', + description: '', + category: '', + imageUrl: '', + venueId: '', + eventDate: '', + eventEndDate: '', + bookingOpensAt: '', + bookingClosesAt: '', + totalSeats: 0, + }) + + const getStatusColor = (status: string) => { + const colors: { [key: string]: string } = { + DRAFT: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300', + PUBLISHED: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300', + ON_SALE: 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300', + SOLD_OUT: 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300', + CANCELLED: 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300', + } + return colors[status] || colors.DRAFT + } + + const handleStartEdit = () => { + if (!event) return + setFormData({ + title: event.title, + description: event.description || '', + category: event.category, + imageUrl: event.imageUrl || '', + venueId: event.venue.id, + eventDate: formatToLocalDatetime(event.eventDate), + eventEndDate: formatToLocalDatetime(event.eventEndDate), + bookingOpensAt: formatToLocalDatetime(event.bookingOpensAt), + bookingClosesAt: formatToLocalDatetime(event.bookingClosesAt), + totalSeats: event.totalSeats, + }) + setIsEditing(true) + setFormError(null) + } + + const handleCancelEdit = () => { + setIsEditing(false) + setFormError(null) + } + + const handleSaveEdit = async (e: React.FormEvent) => { + e.preventDefault() + setFormError(null) + + if (formData.title.trim().length < 5 || formData.title.trim().length > 200) { + setFormError('Title must be between 5 and 200 characters.') + return + } + if (formData.category.trim().length < 2 || formData.category.trim().length > 50) { + setFormError('Category must be between 2 and 50 characters.') + return + } + if (formData.totalSeats < 1 || formData.totalSeats > 100000) { + setFormError('Total capacity must be between 1 and 100,000.') + return + } + + const selectedVenue = venues.find((v: any) => v.id === formData.venueId) + if (selectedVenue && formData.totalSeats > selectedVenue.totalCapacity) { + setFormError(`Total seats (${formData.totalSeats}) cannot exceed the venue's total capacity (${selectedVenue.totalCapacity}).`) + return + } + + try { + const payload = { + ...formData, + title: formData.title.trim(), + description: formData.description?.trim() || undefined, + category: formData.category.trim(), + imageUrl: formData.imageUrl?.trim() || undefined, + eventDate: convertToIsoString(formData.eventDate)!, + eventEndDate: formData.eventEndDate ? convertToIsoString(formData.eventEndDate) : undefined, + bookingOpensAt: convertToIsoString(formData.bookingOpensAt)!, + bookingClosesAt: convertToIsoString(formData.bookingClosesAt)!, + } + + await updateEvent.mutateAsync({ id: eventId, data: payload }) + setIsEditing(false) + } catch (err: any) { + setFormError(err?.response?.data?.message || err?.message || 'Failed to update event') + } + } + + const handlePublish = async () => { + if (confirm('Publish this event? This will open ticket bookings according to the configuration.')) { + try { + await publishEvent.mutateAsync(eventId) + } catch (err: any) { + alert(err?.response?.data?.message || 'Failed to publish') + } + } + } + + const handleCancel = async () => { + const reason = prompt('Please enter the reason for cancellation (optional):') + if (reason !== null) { + try { + await cancelEvent.mutateAsync({ id: eventId, reason: reason || undefined }) + } catch (err: any) { + alert(err?.response?.data?.message || 'Failed to cancel event') + } + } + } + + const handleDelete = async () => { + if (confirm('Are you sure you want to delete this event? This cannot be undone.')) { + try { + await deleteEvent.mutateAsync(eventId) + router.push('/events') + } catch (err: any) { + alert(err?.response?.data?.message || 'Failed to delete event.') + } + } + } + + if (isLoading) { + return ( + +
+ +
+
+ ) + } + + if (fetchError || !event) { + return ( + +
+ +

Event not found

+

+ The event you're looking for doesn't exist or you don't have access. +

+ +
+
+ ) + } + + return ( + +
+ {/* Header */} +
+
+ +
+
+

{event.title}

+ {event.status} +
+

+ {event.category} +

+
+
+
+ {event.status === 'DRAFT' && ( + + )} + {event.status !== 'CANCELLED' && event.status !== 'COMPLETED' && ( + + )} + + +
+
+ + {/* Edit Form */} + {isEditing && ( +
+

Edit Event

+ {formError && ( +
+ {formError} +
+ )} +
+
+
+ + setFormData({ ...formData, title: e.target.value })} required /> +
+ +
+ +