Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
22 changes: 11 additions & 11 deletions src/app/bookings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from 'react'
import { DashboardLayout } from '@/components/layout/DashboardLayout'
import { useEvents, useEventBookings, useBookingById } from '@/hooks/useApi'
import { useEvents, useEventBookings, useBookingDetails } from '@/hooks/useApi'
import { BookingStatus } from '@/lib/types'
import { Eye, Search, Filter, ChevronLeft, ChevronRight, Loader2 } from 'lucide-react'

Expand All @@ -16,7 +16,7 @@ export default function BookingsPage() {
const pageSize = 20

const { data: bookings, isLoading } = useEventBookings(selectedEventId, statusFilter, searchQuery, page, pageSize)
const { data: bookingDetails } = useBookingById(viewingBookingId || '')
const { data: bookingDetails } = useBookingDetails(viewingBookingId || '')

const statuses = ['PENDING', 'CONFIRMED', 'CANCELLED', 'EXPIRED']

Expand Down Expand Up @@ -111,10 +111,10 @@ export default function BookingsPage() {
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{bookings.data.map((booking: any) => (
<tr key={booking.id} className="hover:bg-gray-50 dark:hover:bg-[#1A1A1A] transition-colors">
<td className="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{booking.user.name}</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">{booking.user.email}</td>
<td className="px-6 py-4 text-sm text-center text-gray-900 dark:text-white font-semibold">{booking.seats.length}</td>
<td className="px-6 py-4 text-sm text-right font-semibold text-gray-900 dark:text-white">₹{booking.totalPrice.toLocaleString()}</td>
<td className="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{booking.customerName}</td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">{booking.customerEmail}</td>
<td className="px-6 py-4 text-sm text-center text-gray-900 dark:text-white font-semibold">{booking.seatsCount}</td>
<td className="px-6 py-4 text-sm text-right font-semibold text-gray-900 dark:text-white">₹{booking.totalAmount?.toLocaleString()}</td>
<td className="px-6 py-4 text-center"><span className={`px-3 py-1 rounded-full text-xs font-semibold ${getStatusColor(booking.status)}`}>{booking.status}</span></td>
<td className="px-6 py-4 text-sm text-gray-600 dark:text-gray-400">{new Date(booking.createdAt).toLocaleDateString()}</td>
<td className="px-6 py-4 text-center">
Expand Down Expand Up @@ -158,12 +158,12 @@ export default function BookingsPage() {
<div className="bg-white dark:bg-[#1A1A1A] rounded-lg p-6 max-w-md w-full">
<h2 className="text-xl font-bold mb-4 text-gray-900 dark:text-white">Booking Details</h2>
<div className="space-y-3 mb-6 text-sm">
<div><p className="text-gray-500">Customer</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.user.name}</p></div>
<div><p className="text-gray-500">Email</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.user.email}</p></div>
<div><p className="text-gray-500">Customer</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.customerName}</p></div>
<div><p className="text-gray-500">Email</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.customerEmail}</p></div>
<div><p className="text-gray-500">Status</p><span className={`inline-block px-3 py-1 rounded-full text-xs font-semibold ${getStatusColor(bookingDetails.status)}`}>{bookingDetails.status}</span></div>
<div><p className="text-gray-500">Total Seats</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.seats.length}</p></div>
<div><p className="text-gray-500">Total Price</p><p className="font-semibold text-lg text-blue-600 dark:text-blue-400">₹{bookingDetails.totalPrice.toLocaleString()}</p></div>
<div><p className="text-gray-500">Seats</p><p className="font-mono text-xs">{bookingDetails.seats.join(', ')}</p></div>
<div><p className="text-gray-500">Total Seats</p><p className="font-semibold text-gray-900 dark:text-white">{bookingDetails.seats?.length || 0}</p></div>
<div><p className="text-gray-500">Total Price</p><p className="font-semibold text-lg text-blue-600 dark:text-blue-400">₹{bookingDetails.totalAmount?.toLocaleString()}</p></div>
<div><p className="text-gray-500">Seats</p><p className="font-mono text-xs">{bookingDetails.seats?.map((s: any) => `${s.rowLabel}${s.seatNumber}`).join(', ')}</p></div>
</div>
<button onClick={() => setViewingBookingId(null)} className="w-full btn btn-ghost">Close</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/events/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function EventsPage() {
const { data: events = [], isLoading } = useEvents()
const { data: venues = [] } = useVenues()
const createEvent = useCreateEvent()
const updateEvent = useUpdateEvent('')
const updateEvent = useUpdateEvent()
const publishEvent = usePublishEvent()
const cancelEvent = useCancelEvent()

Expand Down
175 changes: 175 additions & 0 deletions src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { Mail, Lock, ArrowRight, Loader2, Eye, EyeOff, Ticket } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'

export default function LoginPage() {
const router = useRouter()
const { login, isLoading, error, clearError, isAuthenticated, hydrate } = useAuthStore()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [showPassword, setShowPassword] = useState(false)

useEffect(() => {
hydrate()
}, [])

useEffect(() => {
if (isAuthenticated) {
router.push('/')
}
}, [isAuthenticated, router])

useEffect(() => {
clearError()
}, [email, password])

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
await login(email.trim().toLowerCase(), password)
router.push('/')
} catch {
// error is set in store
}
}

return (
<div className="flex min-h-screen">
{/* Left Panel - Hero */}
<div className="hidden lg:flex lg:w-1/2 relative overflow-hidden bg-gradient-to-br from-[#FF6B35] via-[#E8522A] to-[#004E89]">
<div className="absolute inset-0">
{/* Animated circles */}
<div className="absolute top-20 left-20 w-72 h-72 bg-white/10 rounded-full blur-3xl animate-pulse" />
<div className="absolute bottom-20 right-20 w-96 h-96 bg-white/5 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }} />
<div className="absolute top-1/2 left-1/3 w-48 h-48 bg-white/10 rounded-full blur-2xl animate-pulse" style={{ animationDelay: '2s' }} />

{/* Grid pattern */}
<div className="absolute inset-0 opacity-10" style={{
backgroundImage: 'radial-gradient(circle at 1px 1px, white 1px, transparent 0)',
backgroundSize: '40px 40px',
}} />
</div>

<div className="relative z-10 flex flex-col justify-center px-16 text-white">
<div className="flex items-center gap-3 mb-8">
<div className="w-14 h-14 bg-white/20 backdrop-blur-sm rounded-2xl flex items-center justify-center">
<Ticket className="w-8 h-8 text-white" />
</div>
<h1 className="text-4xl font-black tracking-tight">RushTix</h1>
</div>
<h2 className="text-3xl font-bold mb-4 leading-tight">
Manage Your Events<br />Like a Pro
</h2>
<p className="text-lg text-white/80 max-w-md leading-relaxed">
The all-in-one platform for event organizers. Create events, manage venues, track bookings, and optimize ticket pricing with AI-powered tools.
</p>

<div className="mt-12 grid grid-cols-3 gap-6">
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-4">
<p className="text-2xl font-bold">10K+</p>
<p className="text-sm text-white/70">Events Created</p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-4">
<p className="text-2xl font-bold">500K+</p>
<p className="text-sm text-white/70">Tickets Sold</p>
</div>
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-4">
<p className="text-2xl font-bold">99.9%</p>
<p className="text-sm text-white/70">Uptime</p>
</div>
</div>
</div>
</div>

{/* Right Panel - Form */}
<div className="flex-1 flex items-center justify-center px-6 py-12 bg-white dark:bg-[#0F0F0F]">
<div className="w-full max-w-md">
{/* Mobile Logo */}
<div className="lg:hidden flex items-center gap-3 mb-8">
<div className="w-12 h-12 bg-[#FF6B35] rounded-2xl flex items-center justify-center">
<Ticket className="w-6 h-6 text-white" />
</div>
<h1 className="text-3xl font-black text-[#FF6B35]">RushTix</h1>
</div>

<div className="mb-8">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">Welcome back</h2>
<p className="text-gray-500 dark:text-gray-400 mt-2">Sign in to your organizer account</p>
</div>

{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-700 dark:text-red-300 text-sm">
{error}
</div>
)}

<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Email Address</label>
<div className="relative">
<Mail className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
className="w-full pl-12 pr-4 py-3 rounded-xl border border-gray-300 dark:border-[#2A2A2A] bg-gray-50 dark:bg-[#1A1A1A] text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FF6B35] focus:border-transparent transition-all"
/>
</div>
</div>

<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Password</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
minLength={6}
className="w-full pl-12 pr-12 py-3 rounded-xl border border-gray-300 dark:border-[#2A2A2A] bg-gray-50 dark:bg-[#1A1A1A] text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-[#FF6B35] focus:border-transparent transition-all"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
</div>

<button
type="submit"
disabled={isLoading}
className="w-full flex items-center justify-center gap-2 py-3 px-6 rounded-xl bg-[#FF6B35] hover:bg-[#E8522A] text-white font-semibold transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-[#FF6B35]/25 hover:shadow-[#FF6B35]/40"
>
{isLoading ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<>
Sign In
<ArrowRight className="w-5 h-5" />
</>
)}
</button>
</form>

<p className="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
Don't have an account?{' '}
<Link href="/signup" className="text-[#FF6B35] hover:text-[#E8522A] font-semibold transition-colors">
Create one now
</Link>
</p>
</div>
</div>
</div>
)
}
23 changes: 10 additions & 13 deletions src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useState } from 'react'
import { DashboardLayout } from '@/components/layout/DashboardLayout'
import { useEvents, useCategories, useCreateCategory, useUpdateCategoryPrice, useDeleteCategory } from '@/hooks/useApi'
import { useEvents, useTicketCategories, useCreateCategory, useUpdateCategoryPrice, useDeleteCategory } from '@/hooks/useApi'
import { TicketCategoryRequest, PricingAlgorithm } from '@/lib/types'
import { Plus, Edit2, Trash2, DollarSign, Loader2 } from 'lucide-react'

Expand All @@ -13,13 +13,12 @@ export default function PricingPage() {
const [editingId, setEditingId] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)

const { data: categories = [], isLoading } = useCategories(selectedEventId)
const { data: categories = [], isLoading } = useTicketCategories(selectedEventId)
const createCategory = useCreateCategory()
const updatePrice = useUpdateCategoryPrice()
const deleteCategory = useDeleteCategory()

const [formData, setFormData] = useState<TicketCategoryRequest>({
eventId: selectedEventId,
name: '',
basePrice: 0,
minPrice: 0,
Expand All @@ -39,13 +38,12 @@ export default function PricingPage() {
{ value: PricingAlgorithm.TIME_DECAY, label: 'Time-Based Decay' },
]

const selectedEvent = events.find(e => e.id === selectedEventId)
const selectedEvent = events.find((e: any) => e.id === selectedEventId)

const handleOpenForm = (category?: any) => {
if (category) {
setEditingId(category.id)
setFormData({
eventId: selectedEventId,
name: category.name,
basePrice: category.basePrice,
minPrice: category.minPrice,
Expand All @@ -58,7 +56,6 @@ export default function PricingPage() {
} else {
setEditingId(null)
setFormData({
eventId: selectedEventId,
name: '',
basePrice: 0,
minPrice: 0,
Expand All @@ -76,7 +73,7 @@ export default function PricingPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
try {
const totalCapacity = categories.reduce((sum, c) => sum + (c.id === editingId ? 0 : c.capacity), 0) + formData.capacity
const totalCapacity = categories.reduce((sum: number, c: any) => sum + (c.id === editingId ? 0 : c.capacity), 0) + formData.capacity
if (totalCapacity > (selectedEvent?.totalSeats || 0)) {
setError(`Total capacity (${totalCapacity}) exceeds event seats (${selectedEvent?.totalSeats})`)
return
Expand All @@ -86,7 +83,7 @@ export default function PricingPage() {
// Update existing category - not implemented in API currently
alert('Category update not implemented yet')
} else {
await createCategory.mutateAsync(formData)
await createCategory.mutateAsync({ eventId: selectedEventId, request: formData })
}
setShowForm(false)
setEditingId(null)
Expand All @@ -107,7 +104,7 @@ export default function PricingPage() {
const handleDelete = async (categoryId: string) => {
if (confirm('Delete this category? This action cannot be undone.')) {
try {
await deleteCategory.mutateAsync(categoryId)
await deleteCategory.mutateAsync({ eventId: selectedEventId, categoryId })
} catch (error: any) {
setError(error?.response?.data?.message || 'Failed to delete')
}
Expand All @@ -133,7 +130,7 @@ export default function PricingPage() {
className="input w-full md:w-96"
>
<option value="">Choose an event...</option>
{events.map(e => (
{events.map((e: any) => (
<option key={e.id} value={e.id}>
{e.title} ({e.status}) - Seats: {e.totalSeats}
</option>
Expand All @@ -146,8 +143,8 @@ export default function PricingPage() {
<div className="flex items-center justify-between">
<div className="text-sm text-gray-600 dark:text-gray-400">
Event Capacity: <span className="font-bold text-gray-900 dark:text-white">{selectedEvent?.totalSeats}</span> seats |
Used: <span className="font-bold text-blue-600">{categories.reduce((sum, c) => sum + c.capacity, 0)}</span> |
Available: <span className="font-bold text-green-600">{(selectedEvent?.totalSeats || 0) - categories.reduce((sum, c) => sum + c.capacity, 0)}</span>
Used: <span className="font-bold text-blue-600">{categories.reduce((sum: number, c: any) => sum + c.capacity, 0)}</span> |
Available: <span className="font-bold text-green-600">{(selectedEvent?.totalSeats || 0) - categories.reduce((sum: number, c: any) => sum + c.capacity, 0)}</span>
</div>
<button onClick={() => handleOpenForm()} className="btn btn-primary gap-2">
<Plus className="w-5 h-5" />
Expand All @@ -169,7 +166,7 @@ export default function PricingPage() {
<input type="number" placeholder="Display Order" className="input" value={formData.displayOrder} onChange={(e) => setFormData({ ...formData, displayOrder: Number(e.target.value) })} />
<select className="input" value={formData.pricingAlgorithm} onChange={(e) => setFormData({ ...formData, pricingAlgorithm: e.target.value as any })}>
<option value="">Select Algorithm</option>
{pricingAlgorithms.map(a => <option key={a.value} value={a.value}>{a.label}</option>)}
{pricingAlgorithms.map((a: any) => <option key={a.value} value={a.value}>{a.label}</option>)}
</select>
<div className="flex items-center gap-3">
<input type="checkbox" id="dynamic" checked={formData.dynamicPricingEnabled} onChange={(e) => setFormData({ ...formData, dynamicPricingEnabled: e.target.checked })} />
Expand Down
Loading
Loading