initial version of the app

This commit is contained in:
mikicvi
2025-09-21 17:11:02 +01:00
commit c8062cf96b
101 changed files with 23061 additions and 0 deletions
+191
View File
@@ -0,0 +1,191 @@
'use client';
import { useState } from 'react';
import { Calendar } from '@/components/ui/calendar';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { CalendarIcon, Clock, MapPin } from 'lucide-react';
const timeSlots = [
'09:00',
'10:00',
'11:00',
'12:00',
'13:00',
'14:00',
'15:00',
'16:00',
'17:00',
'18:00',
'19:00',
'20:00',
];
const courts = [
{ id: 'court-1', name: 'Court 1', isActive: true },
{ id: 'court-2', name: 'Court 2', isActive: true },
];
export function BookingCalendar() {
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [selectedSlot, setSelectedSlot] = useState<string | null>(null);
const [selectedCourt, setSelectedCourt] = useState<string | null>(null);
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const handleBooking = async () => {
if (!selectedDate || !selectedSlot || !selectedCourt) {
return;
}
try {
const response = await fetch('/api/bookings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
courtId: selectedCourt,
date: selectedDate.toISOString().split('T')[0],
timeSlot: selectedSlot,
}),
});
const result = await response.json();
if (response.ok) {
// Reset selections and show success
setSelectedSlot(null);
setSelectedCourt(null);
// Show success message
alert('Booking created successfully!');
} else {
alert(result.error || 'Booking failed');
}
} catch (error) {
console.error('Booking error:', error);
alert('An error occurred while creating the booking');
}
};
return (
<div className='grid gap-6 lg:grid-cols-2'>
{/* Calendar */}
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<CalendarIcon className='h-5 w-5' />
Select Date
</CardTitle>
<CardDescription>Choose the date for your table tennis session</CardDescription>
</CardHeader>
<CardContent>
<Calendar
mode='single'
selected={selectedDate}
onSelect={(date) => date && setSelectedDate(date)}
disabled={(date) => date < new Date() || date.getDay() === 0} // Disable past dates and Sundays
className='rounded-md border'
/>
</CardContent>
</Card>
{/* Time Slots and Courts */}
<Card>
<CardHeader>
<CardTitle className='flex items-center gap-2'>
<Clock className='h-5 w-5' />
Available Slots
</CardTitle>
<CardDescription>{selectedDate ? formatDate(selectedDate) : 'Select a date first'}</CardDescription>
</CardHeader>
<CardContent className='space-y-6'>
{/* Court Selection */}
<div>
<h4 className='font-medium mb-3 flex items-center gap-2'>
<MapPin className='h-4 w-4' />
Select Court
</h4>
<div className='grid grid-cols-2 gap-2'>
{courts.map((court) => (
<Button
key={court.id}
variant={selectedCourt === court.id ? 'default' : 'outline'}
size='sm'
onClick={() => setSelectedCourt(court.id)}
disabled={!court.isActive}
>
{court.name}
</Button>
))}
</div>
</div>
{/* Time Slot Selection */}
<div>
<h4 className='font-medium mb-3 flex items-center gap-2'>
<Clock className='h-4 w-4' />
Select Time
</h4>
<div className='grid grid-cols-3 gap-2'>
{timeSlots.map((time) => {
const isBooked = Math.random() > 0.7; // Simulate some bookings
return (
<Button
key={time}
variant={selectedSlot === time ? 'default' : 'outline'}
size='sm'
onClick={() => !isBooked && setSelectedSlot(time)}
disabled={isBooked}
className='relative'
>
{time}
{isBooked && (
<Badge
variant='destructive'
className='absolute -top-1 -right-1 h-2 w-2 p-0'
/>
)}
</Button>
);
})}
</div>
</div>
{/* Booking Summary */}
{selectedDate && selectedSlot && selectedCourt && (
<div className='bg-blue-50 border border-blue-200 rounded-lg p-4 space-y-2'>
<h4 className='font-medium text-blue-900'>Booking Summary</h4>
<div className='text-sm text-blue-700 space-y-1'>
<div className='flex items-center gap-2'>
<CalendarIcon className='h-3 w-3' />
{formatDate(selectedDate)}
</div>
<div className='flex items-center gap-2'>
<Clock className='h-3 w-3' />
{selectedSlot} - {String(parseInt(selectedSlot.split(':')[0]) + 1).padStart(2, '0')}
:00
</div>
<div className='flex items-center gap-2'>
<MapPin className='h-3 w-3' />
{courts.find((c) => c.id === selectedCourt)?.name}
</div>
</div>
<Button onClick={handleBooking} className='w-full mt-3'>
Confirm Booking
</Button>
</div>
)}
</CardContent>
</Card>
</div>
);
}
+90
View File
@@ -0,0 +1,90 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Bell, LogOut, Settings, User, Calendar } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
interface DashboardHeaderProps {
user: {
userId: string;
email: string;
role: 'user' | 'admin';
};
}
export function DashboardHeader({ user }: DashboardHeaderProps) {
const router = useRouter();
const { toast } = useToast();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const handleLogout = async () => {
setIsLoggingOut(true);
try {
await fetch('/api/auth/logout', {
method: 'POST',
});
toast({
title: 'Logged out successfully',
description: 'See you next time!',
});
router.push('/login');
router.refresh();
} catch (error) {
toast({
title: 'Logout failed',
description: 'Please try again',
variant: 'destructive',
});
} finally {
setIsLoggingOut(false);
}
};
return (
<header className='bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50'>
<div className='container mx-auto px-4'>
<div className='flex items-center justify-between h-16'>
<div className='flex items-center space-x-4'>
<div className='flex items-center space-x-2'>
<Calendar className='h-6 w-6 text-blue-600' />
<h1 className='text-xl font-bold text-gray-900'>TT Booking</h1>
</div>
{user.role === 'admin' && (
<Badge variant='secondary' className='bg-purple-100 text-purple-800'>
Admin
</Badge>
)}
</div>
<div className='flex items-center space-x-4'>
<Button variant='ghost' size='sm'>
<Bell className='h-4 w-4' />
</Button>
{user.role === 'admin' && (
<Button variant='ghost' size='sm' onClick={() => router.push('/admin')}>
<Settings className='h-4 w-4 mr-2' />
Admin Panel
</Button>
)}
<div className='flex items-center space-x-2'>
<User className='h-4 w-4 text-gray-600' />
<span className='text-sm text-gray-700'>{user.email.split('@')[0]}</span>
</div>
<Button variant='outline' size='sm' onClick={handleLogout} disabled={isLoggingOut}>
<LogOut className='h-4 w-4 mr-2' />
{isLoggingOut ? 'Logging out...' : 'Logout'}
</Button>
</div>
</div>
</div>
</header>
);
}
+135
View File
@@ -0,0 +1,135 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar, Clock, Users, MapPin, TrendingUp, Activity } from 'lucide-react';
interface DashboardStats {
totalUsers: number;
todayBookings: number;
activeCourts: number;
userBookings: number;
upcomingBookings: number;
}
export function QuickStats() {
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
todayBookings: 0,
activeCourts: 0,
userBookings: 0,
upcomingBookings: 0,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, []);
const fetchStats = async () => {
try {
setLoading(true);
const response = await fetch('/api/dashboard/stats');
if (response.ok) {
const data = await response.json();
setStats(data.stats);
}
} catch (error) {
console.error('Error fetching dashboard stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className='space-y-4'>
<Card>
<CardContent className='p-6'>
<div className='animate-pulse'>
<div className='h-4 bg-gray-200 rounded w-3/4 mb-4'></div>
<div className='space-y-3'>
{[1, 2, 3, 4].map((i) => (
<div key={i} className='flex justify-between'>
<div className='h-3 bg-gray-200 rounded w-1/2'></div>
<div className='h-3 bg-gray-200 rounded w-1/4'></div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className='space-y-4'>
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>Quick Stats</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-blue-600' />
<span className='text-sm'>Your Bookings</span>
</div>
<Badge variant='secondary'>{stats.userBookings} active</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Clock className='h-4 w-4 text-green-600' />
<span className='text-sm'>Upcoming</span>
</div>
<Badge variant='secondary'>{stats.upcomingBookings} bookings</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<MapPin className='h-4 w-4 text-purple-600' />
<span className='text-sm'>Active Courts</span>
</div>
<Badge variant='secondary'>{stats.activeCourts} available</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Activity className='h-4 w-4 text-orange-600' />
<span className='text-sm'>Today\'s Bookings</span>
</div>
<Badge variant='secondary'>{stats.todayBookings} total</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>System Info</CardTitle>
</CardHeader>
<CardContent className='space-y-3'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Users className='h-4 w-4 text-gray-600' />
<span className='text-sm'>Total Users</span>
</div>
<Badge variant='outline'>{stats.totalUsers}</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<TrendingUp className='h-4 w-4 text-green-600' />
<span className='text-sm'>System Status</span>
</div>
<div className='flex items-center gap-2'>
<div className='h-2 w-2 bg-green-500 rounded-full' />
<span className='text-xs text-green-600'>Online</span>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
+135
View File
@@ -0,0 +1,135 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar, Clock, Users, MapPin, TrendingUp, Activity } from 'lucide-react';
interface DashboardStats {
totalUsers: number;
todayBookings: number;
activeCourts: number;
userBookings: number;
upcomingBookings: number;
}
export function QuickStats() {
const [stats, setStats] = useState<DashboardStats>({
totalUsers: 0,
todayBookings: 0,
activeCourts: 0,
userBookings: 0,
upcomingBookings: 0,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, []);
const fetchStats = async () => {
try {
setLoading(true);
const response = await fetch('/api/dashboard/stats');
if (response.ok) {
const data = await response.json();
setStats(data.stats);
}
} catch (error) {
console.error('Error fetching dashboard stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className='space-y-4'>
<Card>
<CardContent className='p-6'>
<div className='animate-pulse'>
<div className='h-4 bg-gray-200 rounded w-3/4 mb-4'></div>
<div className='space-y-3'>
{[1, 2, 3, 4].map((i) => (
<div key={i} className='flex justify-between'>
<div className='h-3 bg-gray-200 rounded w-1/2'></div>
<div className='h-3 bg-gray-200 rounded w-1/4'></div>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className='space-y-4'>
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>Quick Stats</CardTitle>
</CardHeader>
<CardContent className='space-y-4'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Calendar className='h-4 w-4 text-blue-600' />
<span className='text-sm'>Your Bookings</span>
</div>
<Badge variant='secondary'>{stats.userBookings} active</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Clock className='h-4 w-4 text-green-600' />
<span className='text-sm'>Upcoming</span>
</div>
<Badge variant='secondary'>{stats.upcomingBookings} bookings</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<MapPin className='h-4 w-4 text-purple-600' />
<span className='text-sm'>Active Courts</span>
</div>
<Badge variant='secondary'>{stats.activeCourts} available</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Activity className='h-4 w-4 text-orange-600' />
<span className='text-sm'>Today\'s Bookings</span>
</div>
<Badge variant='secondary'>{stats.todayBookings} total</Badge>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>System Info</CardTitle>
</CardHeader>
<CardContent className='space-y-3'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<Users className='h-4 w-4 text-gray-600' />
<span className='text-sm'>Total Users</span>
</div>
<Badge variant='outline'>{stats.totalUsers}</Badge>
</div>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<TrendingUp className='h-4 w-4 text-green-600' />
<span className='text-sm'>System Status</span>
</div>
<div className='flex items-center gap-2'>
<div className='h-2 w-2 bg-green-500 rounded-full' />
<span className='text-xs text-green-600'>Online</span>
</div>
</div>
</CardContent>
</Card>
</div>
);
}
+126
View File
@@ -0,0 +1,126 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Calendar, Clock, MapPin } from 'lucide-react';
import { format } from 'date-fns';
interface Booking {
id: string;
date: string;
startTime: string;
endTime: string;
status: string;
notes?: string;
createdAt: Date;
court: {
id: string;
name: string;
};
user: {
id: string;
name: string;
surname: string;
email: string;
};
}
export function RecentBookings() {
const [bookings, setBookings] = useState<Booking[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchRecentBookings();
}, []);
const fetchRecentBookings = async () => {
try {
setLoading(true);
const response = await fetch('/api/dashboard/recent-bookings');
if (response.ok) {
const data = await response.json();
setBookings(data.bookings || []);
}
} catch (error) {
console.error('Error fetching recent bookings:', error);
} finally {
setLoading(false);
}
};
const formatDate = (dateStr: string) => {
try {
const date = new Date(dateStr);
return format(date, 'MMM dd');
} catch {
return dateStr;
}
};
if (loading) {
return (
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>Recent Bookings</CardTitle>
</CardHeader>
<CardContent>
<div className='space-y-3'>
{[1, 2, 3].map((i) => (
<div key={i} className='animate-pulse'>
<div className='h-4 bg-gray-200 rounded w-3/4 mb-2'></div>
<div className='h-3 bg-gray-200 rounded w-1/2'></div>
</div>
))}
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader className='pb-3'>
<CardTitle className='text-base'>Recent Bookings</CardTitle>
</CardHeader>
<CardContent>
{bookings.length === 0 ? (
<div className='text-sm text-gray-500 text-center py-6'>
No recent bookings yet. Make your first booking!
</div>
) : (
<div className='space-y-3'>
{bookings.map((booking) => (
<div key={booking.id} className='border rounded-lg p-3 space-y-2'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-2'>
<MapPin className='h-4 w-4 text-blue-600' />
<span className='font-medium text-sm'>{booking.court.name}</span>
</div>
<Badge variant={booking.status === 'active' ? 'default' : 'secondary'}>
{booking.status}
</Badge>
</div>
<div className='flex items-center gap-4 text-xs text-gray-500'>
<div className='flex items-center gap-1'>
<Calendar className='h-3 w-3' />
<span>{formatDate(booking.date)}</span>
</div>
<div className='flex items-center gap-1'>
<Clock className='h-3 w-3' />
<span>
{booking.startTime} - {booking.endTime}
</span>
</div>
</div>
{booking.notes && <p className='text-xs text-gray-600 italic'>{booking.notes}</p>}
</div>
))}
</div>
)}
</CardContent>
</Card>
);
}
+134
View File
@@ -0,0 +1,134 @@
'use client';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Bell, LogOut, Settings, User, Calendar } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
import { UserProfile } from '@/components/user/user-profile';
interface DashboardHeaderProps {
user: {
userId: string;
email: string;
role: 'user' | 'admin';
};
}
export function DashboardHeader({ user }: DashboardHeaderProps) {
const router = useRouter();
const { toast } = useToast();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const [showAnnouncements, setShowAnnouncements] = useState(false);
const [showUserProfile, setShowUserProfile] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
// Fetch unread announcements count on component mount
useEffect(() => {
fetchUnreadCount();
}, []);
const fetchUnreadCount = async () => {
try {
const response = await fetch('/api/announcements');
if (response.ok) {
const data = await response.json();
setUnreadCount(data.unreadCount || 0);
}
} catch (error) {
console.error('Error fetching unread count:', error);
}
};
const handleLogout = async () => {
setIsLoggingOut(true);
try {
await fetch('/api/auth/logout', {
method: 'POST',
});
toast({
title: 'Logged out successfully',
description: 'See you next time!',
});
router.push('/login');
router.refresh();
} catch (error) {
toast({
title: 'Logout failed',
description: 'Please try again',
variant: 'destructive',
});
} finally {
setIsLoggingOut(false);
}
};
return (
<header className='bg-white/80 backdrop-blur-md border-b border-gray-200 sticky top-0 z-50'>
<div className='container mx-auto px-4'>
<div className='flex items-center justify-between h-16'>
<div className='flex items-center space-x-4'>
<div className='flex items-center space-x-2'>
<Calendar className='h-6 w-6 text-blue-600' />
<h1 className='text-xl font-bold text-gray-900'>TT Booking</h1>
</div>
{user.role === 'admin' && (
<Badge variant='secondary' className='bg-purple-100 text-purple-800'>
Admin
</Badge>
)}
</div>
<div className='flex items-center space-x-4'>
<NotificationBell unreadCount={unreadCount} onClick={() => setShowAnnouncements(true)} />
{user.role === 'admin' && (
<Button variant='ghost' size='sm' onClick={() => router.push('/admin')}>
<Settings className='h-4 w-4 mr-2' />
Admin Panel
</Button>
)}
<Button
variant='ghost'
size='sm'
onClick={() => setShowUserProfile(true)}
className='flex items-center space-x-2'
>
<User className='h-4 w-4 text-gray-600' />
<span className='text-sm text-gray-700'>{user.email.split('@')[0]}</span>
</Button>
<Button variant='outline' size='sm' onClick={handleLogout} disabled={isLoggingOut}>
<LogOut className='h-4 w-4 mr-2' />
{isLoggingOut ? 'Logging out...' : 'Logout'}
</Button>
</div>
</div>
</div>
{/* Announcements Modal */}
<AnnouncementsModal
isOpen={showAnnouncements}
onClose={() => setShowAnnouncements(false)}
unreadCount={unreadCount}
onCountUpdate={setUnreadCount}
/>
{/* User Profile Modal */}
<Dialog open={showUserProfile} onOpenChange={setShowUserProfile}>
<DialogContent className='sm:max-w-4xl max-h-[90vh] overflow-y-auto'>
<DialogHeader>
<DialogTitle>User Profile</DialogTitle>
</DialogHeader>
<UserProfile />
</DialogContent>
</Dialog>
</header>
);
}