diff --git a/app/api/admin/announcements/[id]/route.ts b/app/api/admin/announcements/[id]/route.ts
index a3ef906..7525170 100644
--- a/app/api/admin/announcements/[id]/route.ts
+++ b/app/api/admin/announcements/[id]/route.ts
@@ -4,7 +4,7 @@ import { announcements } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session';
-export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
@@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
const { title, content, priority, expiresAt, isActive } = await request.json();
- const announcementId = params.id;
+ const { id } = await context.params;
+ const announcementId = id;
if (!title || !content) {
return NextResponse.json({ error: 'Title and content are required' }, { status: 400 });
@@ -49,14 +50,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
}
-export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
- const announcementId = params.id;
+ const { id } = await context.params;
+ const announcementId = id;
// Check if announcement exists
const existing = await db.select().from(announcements).where(eq(announcements.id, announcementId)).limit(1);
diff --git a/app/api/admin/courts/[id]/route.ts b/app/api/admin/courts/[id]/route.ts
index 19b972d..e93b69f 100644
--- a/app/api/admin/courts/[id]/route.ts
+++ b/app/api/admin/courts/[id]/route.ts
@@ -4,7 +4,7 @@ import { courts } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session';
-export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
@@ -12,7 +12,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
const { name, isActive } = await request.json();
- const courtId = params.id;
+ const { id } = await context.params;
+ const courtId = id;
if (!name) {
return NextResponse.json({ error: 'Court name is required' }, { status: 400 });
@@ -46,14 +47,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
}
-export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
- const courtId = params.id;
+ const { id } = await context.params;
+ const courtId = id;
// Check if court exists
const existing = await db.select().from(courts).where(eq(courts.id, courtId)).limit(1);
diff --git a/app/api/admin/settings/route.ts b/app/api/admin/settings/route.ts
index afd084b..bf96585 100644
--- a/app/api/admin/settings/route.ts
+++ b/app/api/admin/settings/route.ts
@@ -3,6 +3,7 @@ import { db } from '@/lib/db';
import { settings } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session';
+import { invalidateAppConfigCache } from '@/lib/app-config';
export async function GET(request: NextRequest) {
try {
@@ -70,6 +71,9 @@ export async function PUT(request: NextRequest) {
await Promise.all(updatePromises);
+ // Invalidate app config cache since settings changed
+ invalidateAppConfigCache();
+
return NextResponse.json({ message: 'Settings updated successfully' });
} catch (error) {
console.error('Error updating settings:', error);
diff --git a/app/api/admin/users/[id]/route.ts b/app/api/admin/users/[id]/route.ts
index 671435b..6662076 100644
--- a/app/api/admin/users/[id]/route.ts
+++ b/app/api/admin/users/[id]/route.ts
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm';
import { getSession } from '@/lib/session';
import bcrypt from 'bcryptjs';
-export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
+export async function PUT(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
@@ -13,7 +13,8 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
const { name, surname, email, role, password } = await request.json();
- const userId = params.id;
+ const { id } = await context.params;
+ const userId = id;
if (!name || !surname || !email) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
@@ -58,14 +59,15 @@ export async function PUT(request: NextRequest, { params }: { params: { id: stri
}
}
-export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session || session.role !== 'admin') {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
- const userId = params.id;
+ const { id } = await context.params;
+ const userId = id;
// Prevent admin from deleting themselves
if (session.userId === userId) {
diff --git a/app/api/bookings/[id]/route.ts b/app/api/bookings/[id]/route.ts
index 8774655..df16617 100644
--- a/app/api/bookings/[id]/route.ts
+++ b/app/api/bookings/[id]/route.ts
@@ -5,7 +5,7 @@ import { bookings, settings } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
-export async function PATCH(request: NextRequest, { params }: { params: { id: string } }) {
+export async function PATCH(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session) {
@@ -13,7 +13,8 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
}
const { notes } = await request.json();
- const bookingId = params.id;
+ const { id } = await context.params;
+ const bookingId = id;
// Check if booking exists and belongs to user
const existingBooking = await db
@@ -98,14 +99,15 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
}
}
-export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
+export async function DELETE(request: NextRequest, context: { params: Promise<{ id: string }> }) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
- const bookingId = params.id;
+ const { id } = await context.params;
+ const bookingId = id;
// Check if booking exists and belongs to user
const existingBooking = await db
diff --git a/app/api/bookings/route.ts b/app/api/bookings/route.ts
index bd30e75..9dc0406 100644
--- a/app/api/bookings/route.ts
+++ b/app/api/bookings/route.ts
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { bookings, courts, timeSlots, settings, metrics } from '@/lib/db/schema';
-import { eq, and } from 'drizzle-orm';
+import { eq, and, gte, asc } from 'drizzle-orm';
import { getSession } from '@/lib/session';
import { logActivity, ACTIONS, ENTITY_TYPES } from '@/lib/activity-logger';
@@ -12,6 +12,9 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
+ // Get today's date to filter out past bookings
+ const today = new Date().toISOString().split('T')[0];
+
const userBookings = await db
.select({
id: bookings.id,
@@ -29,7 +32,8 @@ export async function GET(request: NextRequest) {
})
.from(bookings)
.innerJoin(courts, eq(bookings.courtId, courts.id))
- .where(eq(bookings.userId, session.userId));
+ .where(and(eq(bookings.userId, session.userId), eq(bookings.status, 'active'), gte(bookings.date, today)))
+ .orderBy(asc(bookings.date), asc(bookings.startTime));
return NextResponse.json({ bookings: userBookings });
} catch (error) {
diff --git a/app/api/config/route.ts b/app/api/config/route.ts
new file mode 100644
index 0000000..daffa0a
--- /dev/null
+++ b/app/api/config/route.ts
@@ -0,0 +1,12 @@
+import { NextResponse } from 'next/server';
+import { getAppConfig } from '@/lib/app-config';
+
+export async function GET() {
+ try {
+ const config = await getAppConfig();
+ return NextResponse.json(config);
+ } catch (error) {
+ console.error('Error fetching app config:', error);
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
+ }
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index 293e7f4..9ed13c6 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -6,8 +6,10 @@ import { eq } from 'drizzle-orm';
import { DashboardHeader } from '@/components/dashboard/dashboard-header';
import { EnhancedBookingCalendar } from '@/components/booking/enhanced-booking-calendar';
import { UserBookingManagement } from '@/components/booking/user-booking-management';
+import { getAppConfig } from '@/lib/app-config';
export default async function DashboardPage() {
+ const config = await getAppConfig();
const session = await getSession();
if (!session) {
@@ -38,7 +40,7 @@ export default async function DashboardPage() {
};
return (
-
+
@@ -51,7 +53,9 @@ export default async function DashboardPage() {
{user.name && user.surname ? `${user.name} ${user.surname}` : user.email.split('@')[0]}!
🏓
- Book your table tennis court and enjoy your game
+
+ Book your {config.sportName.toLowerCase()} court and enjoy your game
+
diff --git a/app/globals.css b/app/globals.css
index d22c59d..f428892 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,65 +4,99 @@
@layer base {
:root {
- --background: 0 0% 100%;
- --foreground: 222.2 84% 4.9%;
-
- --card: 0 0% 100%;
- --card-foreground: 222.2 84% 4.9%;
-
- --popover: 0 0% 100%;
- --popover-foreground: 222.2 84% 4.9%;
-
- --primary: 222.2 47.4% 11.2%;
- --primary-foreground: 210 40% 98%;
-
- --secondary: 210 40% 96%;
- --secondary-foreground: 222.2 84% 4.9%;
-
- --muted: 210 40% 96%;
- --muted-foreground: 215.4 16.3% 46.9%;
-
- --accent: 210 40% 96%;
- --accent-foreground: 222.2 84% 4.9%;
-
- --destructive: 0 84.2% 60.2%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 214.3 31.8% 91.4%;
- --input: 214.3 31.8% 91.4%;
- --ring: 222.2 84% 4.9%;
-
+ --background: 0 0% 98.8235%;
+ --foreground: 0 0% 9.0196%;
+ --card: 0 0% 98.8235%;
+ --card-foreground: 0 0% 9.0196%;
+ --popover: 0 0% 98.8235%;
+ --popover-foreground: 0 0% 32.1569%;
+ --primary: 151.3274 66.8639% 66.8627%;
+ --primary-foreground: 153.3333 13.0435% 13.5294%;
+ --secondary: 0 0% 99.2157%;
+ --secondary-foreground: 0 0% 9.0196%;
+ --muted: 0 0% 92.9412%;
+ --muted-foreground: 0 0% 12.549%;
+ --accent: 0 0% 92.9412%;
+ --accent-foreground: 0 0% 12.549%;
+ --destructive: 9.8901 81.982% 43.5294%;
+ --destructive-foreground: 0 100% 99.4118%;
+ --border: 0 0% 87.451%;
+ --input: 0 0% 96.4706%;
+ --ring: 151.3274 66.8639% 66.8627%;
+ --chart-1: 151.3274 66.8639% 66.8627%;
+ --chart-2: 217.2193 91.2195% 59.8039%;
+ --chart-3: 258.3117 89.5349% 66.2745%;
+ --chart-4: 37.6923 92.126% 50.1961%;
+ --chart-5: 160.1183 84.0796% 39.4118%;
+ --sidebar: 0 0% 98.8235%;
+ --sidebar-foreground: 0 0% 43.9216%;
+ --sidebar-primary: 151.3274 66.8639% 66.8627%;
+ --sidebar-primary-foreground: 153.3333 13.0435% 13.5294%;
+ --sidebar-accent: 0 0% 92.9412%;
+ --sidebar-accent-foreground: 0 0% 12.549%;
+ --sidebar-border: 0 0% 87.451%;
+ --sidebar-ring: 151.3274 66.8639% 66.8627%;
+ --font-sans: Outfit, sans-serif;
+ --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
+ --font-mono: monospace;
--radius: 0.5rem;
+ --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
+ --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
+ --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
+ --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
+ --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
+ --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
+ --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
+ --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
+ --tracking-normal: 0.025em;
+ --spacing: 0.25rem;
}
.dark {
- --background: 222.2 84% 4.9%;
- --foreground: 210 40% 98%;
-
- --card: 222.2 84% 4.9%;
- --card-foreground: 210 40% 98%;
-
- --popover: 222.2 84% 4.9%;
- --popover-foreground: 210 40% 98%;
-
- --primary: 210 40% 98%;
- --primary-foreground: 222.2 47.4% 11.2%;
-
- --secondary: 217.2 32.6% 17.5%;
- --secondary-foreground: 210 40% 98%;
-
- --muted: 217.2 32.6% 17.5%;
- --muted-foreground: 215 20.2% 65.1%;
-
- --accent: 217.2 32.6% 17.5%;
- --accent-foreground: 210 40% 98%;
-
- --destructive: 0 62.8% 30.6%;
- --destructive-foreground: 210 40% 98%;
-
- --border: 217.2 32.6% 17.5%;
- --input: 217.2 32.6% 17.5%;
- --ring: 212.7 26.8% 83.9%;
+ --background: 0 0% 7.0588%;
+ --foreground: 214.2857 31.8182% 91.3725%;
+ --card: 0 0% 9.0196%;
+ --card-foreground: 214.2857 31.8182% 91.3725%;
+ --popover: 0 0% 14.1176%;
+ --popover-foreground: 0 0% 66.2745%;
+ --primary: 154.898 100% 19.2157%;
+ --primary-foreground: 152.7273 19.2982% 88.8235%;
+ --secondary: 0 0% 14.1176%;
+ --secondary-foreground: 0 0% 98.0392%;
+ --muted: 0 0% 12.1569%;
+ --muted-foreground: 0 0% 63.5294%;
+ --accent: 0 0% 19.2157%;
+ --accent-foreground: 0 0% 98.0392%;
+ --destructive: 6.6667 60% 20.5882%;
+ --destructive-foreground: 12 12.1951% 91.9608%;
+ --border: 0 0% 16.0784%;
+ --input: 0 0% 14.1176%;
+ --ring: 141.8919 69.1589% 58.0392%;
+ --chart-1: 141.8919 69.1589% 58.0392%;
+ --chart-2: 213.1169 93.9024% 67.8431%;
+ --chart-3: 255.1351 91.7355% 76.2745%;
+ --chart-4: 43.2558 96.4126% 56.2745%;
+ --chart-5: 172.4551 66.0079% 50.3922%;
+ --sidebar: 0 0% 7.0588%;
+ --sidebar-foreground: 0 0% 53.7255%;
+ --sidebar-primary: 154.898 100% 19.2157%;
+ --sidebar-primary-foreground: 152.7273 19.2982% 88.8235%;
+ --sidebar-accent: 0 0% 19.2157%;
+ --sidebar-accent-foreground: 0 0% 98.0392%;
+ --sidebar-border: 0 0% 16.0784%;
+ --sidebar-ring: 141.8919 69.1589% 58.0392%;
+ --font-sans: Outfit, sans-serif;
+ --font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
+ --font-mono: monospace;
+ --radius: 0.5rem;
+ --shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
+ --shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
+ --shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
+ --shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
+ --shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
+ --shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
+ --shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
+ --shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
}
}
@@ -72,5 +106,6 @@
}
body {
@apply bg-background text-foreground;
+ letter-spacing: var(--tracking-normal);
}
}
diff --git a/app/layout.tsx b/app/layout.tsx
index 95e8333..9c1e461 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -3,13 +3,18 @@ import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/toaster';
+import { getAppConfig } from '@/lib/app-config';
const inter = Inter({ subsets: ['latin'] });
-export const metadata: Metadata = {
- title: 'Table Tennis Booking System',
- description: 'Book your table tennis court slots with ease',
-};
+export async function generateMetadata(): Promise
{
+ const config = await getAppConfig();
+
+ return {
+ title: config.appTitle,
+ description: config.appDescription,
+ };
+}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
diff --git a/app/login/page.tsx b/app/login/page.tsx
index eb76114..083f43f 100644
--- a/app/login/page.tsx
+++ b/app/login/page.tsx
@@ -1,13 +1,16 @@
import Link from 'next/link';
import { LoginForm } from '@/components/auth/LoginForm';
+import { getAppConfig } from '@/lib/app-config';
+
+export default async function LoginPage() {
+ const config = await getAppConfig();
-export default function LoginPage() {
return (
-
🏓 TT Booking
-
Professional table tennis court booking system
+
🏓 {config.clubName}
+
{config.appDescription}
diff --git a/app/register/page.tsx b/app/register/page.tsx
index 8be7746..57c6e6d 100644
--- a/app/register/page.tsx
+++ b/app/register/page.tsx
@@ -1,13 +1,16 @@
import Link from 'next/link';
import { RegisterForm } from '@/components/auth/RegisterForm';
+import { getAppConfig } from '@/lib/app-config';
+
+export default async function RegisterPage() {
+ const config = await getAppConfig();
-export default function RegisterPage() {
return (
-
🏓 TT Booking
-
Join our table tennis community
+
🏓 {config.clubName}
+
Join our {config.sportName.toLowerCase()} community
diff --git a/components/admin/AdminAnnouncementManagement.tsx b/components/admin/AdminAnnouncementManagement.tsx
index 1c7d321..253cd9f 100644
--- a/components/admin/AdminAnnouncementManagement.tsx
+++ b/components/admin/AdminAnnouncementManagement.tsx
@@ -8,6 +8,17 @@ import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Textarea } from '@/components/ui/textarea';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useToast } from '@/hooks/use-toast';
@@ -37,7 +48,9 @@ export function AdminAnnouncementManagement() {
const [loading, setLoading] = useState(true);
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingAnnouncement, setEditingAnnouncement] = useState
(null);
+ const [announcementToDelete, setAnnouncementToDelete] = useState(null);
const [formData, setFormData] = useState({
title: '',
content: '',
@@ -176,12 +189,21 @@ export function AdminAnnouncementManagement() {
}
};
+ const openDeleteDialog = (announcement: Announcement) => {
+ setAnnouncementToDelete(announcement);
+ setIsDeleteDialogOpen(true);
+ };
+
+ const confirmDeleteAnnouncement = async () => {
+ if (announcementToDelete) {
+ await handleDeleteAnnouncement(announcementToDelete.id);
+ setIsDeleteDialogOpen(false);
+ setAnnouncementToDelete(null);
+ }
+ };
+
const handleDeleteAnnouncement = async (announcementId: string) => {
try {
- if (!confirm('Are you sure you want to delete this announcement?')) {
- return;
- }
-
const response = await fetch(`/api/admin/announcements/${announcementId}`, {
method: 'DELETE',
});
@@ -444,7 +466,7 @@ export function AdminAnnouncementManagement() {
handleDeleteAnnouncement(announcement.id)}
+ onClick={() => openDeleteDialog(announcement)}
className='text-destructive hover:text-destructive/90'
>
@@ -539,6 +561,29 @@ export function AdminAnnouncementManagement() {
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Are you sure?
+
+ Are you sure you want to delete{' '}
+ {announcementToDelete ? `"${announcementToDelete.title}"` : 'this announcement'}? This
+ action cannot be undone.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
);
}
diff --git a/components/admin/AdminCourtManagement.tsx b/components/admin/AdminCourtManagement.tsx
index 9a459db..0de8a2f 100644
--- a/components/admin/AdminCourtManagement.tsx
+++ b/components/admin/AdminCourtManagement.tsx
@@ -6,6 +6,17 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
@@ -32,7 +43,9 @@ export function AdminCourtManagement() {
const [editing, setEditing] = useState
(null);
const [deleting, setDeleting] = useState(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingCourt, setEditingCourt] = useState(null);
+ const [courtToDelete, setCourtToDelete] = useState(null);
const [formData, setFormData] = useState({
name: '',
isActive: true,
@@ -150,11 +163,20 @@ export function AdminCourtManagement() {
setIsDialogOpen(true);
};
- const handleDelete = async (courtId: string) => {
- if (!confirm('Are you sure you want to delete this court? This action cannot be undone.')) {
- return;
- }
+ const openDeleteDialog = (court: Court) => {
+ setCourtToDelete(court);
+ setIsDeleteDialogOpen(true);
+ };
+ const confirmDeleteCourt = async () => {
+ if (courtToDelete) {
+ await handleDelete(courtToDelete.id);
+ setIsDeleteDialogOpen(false);
+ setCourtToDelete(null);
+ }
+ };
+
+ const handleDelete = async (courtId: string) => {
try {
setDeleting(courtId);
const response = await fetch(`/api/admin/courts/${courtId}`, {
@@ -319,7 +341,7 @@ export function AdminCourtManagement() {
handleDelete(court.id)}
+ onClick={() => openDeleteDialog(court)}
disabled={deleting === court.id}
className='text-red-600 hover:text-red-700'
>
@@ -337,6 +359,28 @@ export function AdminCourtManagement() {
)}
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Are you sure?
+
+ Are you sure you want to delete {courtToDelete ? `"${courtToDelete.name}"` : 'this court'}?
+ This action cannot be undone.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
);
}
diff --git a/components/admin/AdminSettingsManagement.tsx b/components/admin/AdminSettingsManagement.tsx
index e1da9ce..b02a87f 100644
--- a/components/admin/AdminSettingsManagement.tsx
+++ b/components/admin/AdminSettingsManagement.tsx
@@ -17,6 +17,12 @@ interface Setting {
}
interface SettingsData {
+ // Club/Brand Settings
+ club_name: string;
+ sport_name: string;
+ app_title: string;
+ app_description: string;
+ // Booking Settings
booking_window_days: string;
max_booking_duration_hours: string;
min_booking_duration_minutes: string;
@@ -30,6 +36,12 @@ interface SettingsData {
export function AdminSettingsManagement() {
const [settings, setSettings] = useState
({
+ // Club/Brand Settings
+ club_name: 'TT Club',
+ sport_name: 'Table Tennis',
+ app_title: 'Table Tennis Booking System',
+ app_description: 'Book your table tennis court slots with ease',
+ // Booking Settings
booking_window_days: '7',
max_booking_duration_hours: '2',
min_booking_duration_minutes: '30',
@@ -38,7 +50,7 @@ export function AdminSettingsManagement() {
allow_weekend_bookings: 'true',
max_bookings_per_user_per_hour_per_day: '1',
allow_booking_modifications: 'true',
- booking_modification_hours_before: '2',
+ booking_modification_hours_before: '1',
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
@@ -54,6 +66,12 @@ export function AdminSettingsManagement() {
if (response.ok) {
const data = await response.json();
const settingsMap: SettingsData = {
+ // Club/Brand Settings
+ club_name: 'TT Club',
+ sport_name: 'Table Tennis',
+ app_title: 'Table Tennis Booking System',
+ app_description: 'Book your table tennis court slots with ease',
+ // Booking Settings
booking_window_days: '7',
max_booking_duration_hours: '2',
min_booking_duration_minutes: '30',
@@ -62,10 +80,8 @@ export function AdminSettingsManagement() {
allow_weekend_bookings: 'true',
max_bookings_per_user_per_hour_per_day: '1',
allow_booking_modifications: 'true',
- booking_modification_hours_before: '2',
- };
-
- // Map the settings array to our object
+ booking_modification_hours_before: '1',
+ }; // Map the settings array to our object
data.settings?.forEach((setting: Setting) => {
if (setting.key in settingsMap) {
settingsMap[setting.key as keyof SettingsData] = setting.value;
@@ -189,139 +205,205 @@ export function AdminSettingsManagement() {
-
- {/* Booking Window */}
-
-
Booking Window (days)
-
updateSetting('booking_window_days', e.target.value)}
- />
-
How many days in advance users can book
-
+ {/* Club/Brand Configuration Section */}
+
+
Club & Branding
+
+ {/* Club Name */}
+
+
Club Name
+
updateSetting('club_name', e.target.value)}
+ />
+
The name of your club or organization
+
- {/* Max Duration */}
-
-
Max Booking Duration (hours)
-
updateSetting('max_booking_duration_hours', e.target.value)}
- />
-
Maximum hours per booking session
-
+ {/* Sport Name */}
+
+
Sport Name
+
updateSetting('sport_name', e.target.value)}
+ />
+
The sport played at your facility
+
- {/* Min Duration */}
-
-
Min Booking Duration (minutes)
-
updateSetting('min_booking_duration_minutes', e.target.value)}
- />
-
Minimum minutes per booking session
-
+ {/* App Title */}
+
+
Application Title
+
updateSetting('app_title', e.target.value)}
+ />
+
Main title shown in browser and app header
+
- {/* Start Time */}
-
-
Daily Start Time
-
updateSetting('booking_start_time', e.target.value)}
- />
-
When courts open for booking each day
+ {/* App Description */}
+
+
Application Description
+
updateSetting('app_description', e.target.value)}
+ />
+
Short description for login/register pages
+
+
- {/* End Time */}
-
-
Daily End Time
-
updateSetting('booking_end_time', e.target.value)}
- />
-
When courts close for booking each day
-
+ {/* Booking Configuration Section */}
+
+
Booking Configuration
+
+ {/* Booking Window */}
+
+
Booking Window (days)
+
updateSetting('booking_window_days', e.target.value)}
+ />
+
How many days in advance users can book
+
- {/* Booking Restrictions */}
-
-
Max Bookings per User per Hour
-
updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)}
- />
-
Maximum bookings per user per hour on the same day
-
+ {/* Max Duration */}
+
+
Max Booking Duration (hours)
+
updateSetting('max_booking_duration_hours', e.target.value)}
+ />
+
Maximum hours per booking session
+
- {/* Booking Modification Settings */}
-
-
-
- updateSetting('allow_booking_modifications', checked.toString())
+ {/* Min Duration */}
+
+
Min Booking Duration (minutes)
+
updateSetting('min_booking_duration_minutes', e.target.value)}
+ />
+
Minimum minutes per booking session
+
+
+ {/* Start Time */}
+
+
Daily Start Time
+
updateSetting('booking_start_time', e.target.value)}
+ />
+
When courts open for booking each day
+
+
+ {/* End Time */}
+
+
Daily End Time
+
updateSetting('booking_end_time', e.target.value)}
+ />
+
When courts close for booking each day
+
+
+ {/* Booking Restrictions */}
+
+
+ Max Bookings per User per Hour
+
+
+ updateSetting('max_bookings_per_user_per_hour_per_day', e.target.value)
}
/>
-
Allow Booking Modifications
+
Maximum bookings per user per hour on the same day
- Whether users can edit or cancel their bookings
-
- {/* Modification Time Restriction */}
-
-
- Modification Time Limit (hours before session)
-
-
updateSetting('booking_modification_hours_before', e.target.value)}
- disabled={settings.allow_booking_modifications !== 'true'}
- />
-
- {settings.allow_booking_modifications === 'true'
- ? 'How many hours before a session users can still modify bookings'
- : 'Enable booking modifications to configure this setting'}
-
-
+ {/* Booking Modification Settings */}
+
+
+
+ updateSetting('allow_booking_modifications', checked.toString())
+ }
+ />
+ Allow Booking Modifications
+
+
Whether users can edit or cancel their bookings
+
- {/* Weekend Bookings */}
-
-
-
- updateSetting('allow_weekend_bookings', checked.toString())
- }
+ {/* Modification Time Restriction */}
+
+
+ Modification Time Limit (hours before session)
+
+
updateSetting('booking_modification_hours_before', e.target.value)}
+ disabled={settings.allow_booking_modifications !== 'true'}
/>
-
Allow Weekend Bookings
+
+ {settings.allow_booking_modifications === 'true'
+ ? 'How many hours before a session users can still modify bookings'
+ : 'Enable booking modifications to configure this setting'}
+
+
+
+ {/* Weekend Bookings */}
+
+
+
+ updateSetting('allow_weekend_bookings', checked.toString())
+ }
+ />
+ Allow Weekend Bookings
+
+
Whether users can book courts on weekends
- Whether users can book courts on weekends
diff --git a/components/admin/AdminTimeSlotManagement.tsx b/components/admin/AdminTimeSlotManagement.tsx
index 0ffb44e..ec606bf 100644
--- a/components/admin/AdminTimeSlotManagement.tsx
+++ b/components/admin/AdminTimeSlotManagement.tsx
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
import { Switch } from '@/components/ui/switch';
import { Plus, Edit, Trash2, Clock } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
@@ -30,7 +41,11 @@ export function AdminTimeSlotManagement() {
const [timeSlots, setTimeSlots] = useState
([]);
const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
+ const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+ const [showWipeDayDialog, setShowWipeDayDialog] = useState(false);
const [editingSlot, setEditingSlot] = useState(null);
+ const [slotToDelete, setSlotToDelete] = useState(null);
+ const [dayToWipe, setDayToWipe] = useState(null);
const [formData, setFormData] = useState({
dayOfWeek: 1, // Default to Monday (Irish standard)
startTime: '',
@@ -115,11 +130,20 @@ export function AdminTimeSlotManagement() {
}
};
- const handleDelete = async (id: string) => {
- if (!confirm('Are you sure you want to delete this time slot?')) {
- return;
- }
+ const openDeleteDialog = (slot: TimeSlot) => {
+ setSlotToDelete(slot);
+ setShowDeleteDialog(true);
+ };
+ const confirmDeleteSlot = async () => {
+ if (slotToDelete) {
+ await handleDelete(slotToDelete.id);
+ setShowDeleteDialog(false);
+ setSlotToDelete(null);
+ }
+ };
+
+ const handleDelete = async (id: string) => {
try {
setLoading(true);
const response = await fetch(`/api/admin/time-slots/${id}`, {
@@ -152,14 +176,23 @@ export function AdminTimeSlotManagement() {
}
};
- const handleWipeDay = async (dayOfWeek: number) => {
- const dayName = DAYS[dayOfWeek];
- if (!confirm(`Are you sure you want to delete ALL time slots for ${dayName}? This action cannot be undone.`)) {
- return;
- }
+ const openWipeDayDialog = (dayOfWeek: number) => {
+ setDayToWipe(dayOfWeek);
+ setShowWipeDayDialog(true);
+ };
+ const confirmWipeDay = async () => {
+ if (dayToWipe !== null) {
+ await handleWipeDay(dayToWipe);
+ setShowWipeDayDialog(false);
+ setDayToWipe(null);
+ }
+ };
+
+ const handleWipeDay = async (dayOfWeek: number) => {
try {
setLoading(true);
+ const dayName = DAYS[dayOfWeek];
const slotsToDelete = timeSlots.filter((slot) => slot.dayOfWeek === dayOfWeek);
// Delete all slots for this day
@@ -322,7 +355,7 @@ export function AdminTimeSlotManagement() {
handleWipeDay(jsDayOfWeek)}
+ onClick={() => openWipeDayDialog(jsDayOfWeek)}
className='text-destructive hover:text-destructive/80 hover:bg-destructive/10'
disabled={loading}
>
@@ -369,7 +402,7 @@ export function AdminTimeSlotManagement() {
handleDelete(slot.id)}
+ onClick={() => openDeleteDialog(slot)}
className='text-destructive hover:text-destructive/80'
>
@@ -389,6 +422,46 @@ export function AdminTimeSlotManagement() {
)}
+
+ {/* Delete Time Slot Dialog */}
+
+
+
+ Are you sure?
+
+ Are you sure you want to delete this time slot? This action cannot be undone.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
+
+ {/* Wipe Day Dialog */}
+
+
+
+ Are you sure?
+
+ Are you sure you want to delete ALL time slots for{' '}
+ {dayToWipe !== null ? DAYS[dayToWipe] : 'this day'}? This action cannot be undone.
+
+
+
+ Cancel
+
+ Delete All
+
+
+
+
);
}
diff --git a/components/admin/AdminUserManagement.tsx b/components/admin/AdminUserManagement.tsx
index c62de37..35fd833 100644
--- a/components/admin/AdminUserManagement.tsx
+++ b/components/admin/AdminUserManagement.tsx
@@ -7,6 +7,17 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from '@/components/ui/alert-dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useToast } from '@/hooks/use-toast';
@@ -35,7 +46,9 @@ export function AdminUserManagement() {
const [searchTerm, setSearchTerm] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [editingUser, setEditingUser] = useState
(null);
+ const [userToDelete, setUserToDelete] = useState(null);
const [formData, setFormData] = useState({
name: '',
surname: '',
@@ -184,12 +197,21 @@ export function AdminUserManagement() {
}
};
+ const openDeleteDialog = (user: User) => {
+ setUserToDelete(user);
+ setIsDeleteDialogOpen(true);
+ };
+
+ const confirmDeleteUser = async () => {
+ if (userToDelete) {
+ await handleDeleteUser(userToDelete.id);
+ setIsDeleteDialogOpen(false);
+ setUserToDelete(null);
+ }
+ };
+
const handleDeleteUser = async (userId: string) => {
try {
- if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
- return;
- }
-
const response = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE',
});
@@ -406,7 +428,7 @@ export function AdminUserManagement() {
handleDeleteUser(user.id)}
+ onClick={() => openDeleteDialog(user)}
className='text-red-600 hover:text-red-700'
>
@@ -498,6 +520,29 @@ export function AdminUserManagement() {
+
+ {/* Delete Confirmation Dialog */}
+
+
+
+ Are you sure?
+
+ Are you sure you want to delete{' '}
+ {userToDelete ? `${userToDelete.name} ${userToDelete.surname}` : 'this user'}? This action
+ cannot be undone.
+
+
+
+ Cancel
+
+ Delete
+
+
+
+
);
}
diff --git a/components/booking/enhanced-booking-calendar.tsx b/components/booking/enhanced-booking-calendar.tsx
index ed8d9cb..04bd233 100644
--- a/components/booking/enhanced-booking-calendar.tsx
+++ b/components/booking/enhanced-booking-calendar.tsx
@@ -473,11 +473,11 @@ export function EnhancedBookingCalendar() {
: ''
} ${
isTodayDate && !isSelectedDate
- ? 'ring-2 ring-blue-400 ring-opacity-50 bg-blue-50 dark:bg-blue-950 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900 text-foreground'
+ ? 'ring-2 ring-primary/20 bg-accent border-primary/20 hover:bg-accent/80 text-foreground'
: ''
} ${
isSelectedDate && isTodayDate
- ? 'bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 dark:from-blue-700 dark:to-blue-800 dark:hover:from-blue-800 dark:hover:to-blue-900 text-white'
+ ? 'bg-gradient-to-r from-primary to-primary/80 hover:from-primary/90 hover:to-primary/70 text-primary-foreground'
: ''
}`}
>
@@ -501,8 +501,8 @@ export function EnhancedBookingCalendar() {
)}
diff --git a/components/booking/user-booking-management.tsx b/components/booking/user-booking-management.tsx
index e3edf1a..4f2ea2b 100644
--- a/components/booking/user-booking-management.tsx
+++ b/components/booking/user-booking-management.tsx
@@ -61,16 +61,8 @@ export function UserBookingManagement() {
const response = await fetch('/api/bookings');
if (response.ok) {
const data = await response.json();
- // Filter to show only future and today's bookings
- const now = new Date();
- const today = now.toISOString().split('T')[0];
-
- const relevantBookings = data.bookings.filter((booking: Booking) => {
- if (booking.status !== 'active') return false;
- return booking.date >= today;
- });
-
- setBookings(relevantBookings);
+ // API already filters to show only active future bookings (today onwards)
+ setBookings(data.bookings || []);
}
} catch (error) {
console.error('Error fetching bookings:', error);
@@ -282,7 +274,7 @@ export function UserBookingManagement() {
- Your Bookings
+ Your Upcoming Bookings
@@ -390,16 +382,16 @@ export function UserBookingManagement() {
{selectedBooking && (
-
-
+
+
{formatDate(selectedBooking.date)}
-
+
{selectedBooking.startTime} - {selectedBooking.endTime}
-
+
{selectedBooking.court.name}
diff --git a/components/dashboard/BookingCalendar.tsx b/components/dashboard/BookingCalendar.tsx
index b273470..f06390e 100644
--- a/components/dashboard/BookingCalendar.tsx
+++ b/components/dashboard/BookingCalendar.tsx
@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { CalendarIcon, Clock, MapPin } from 'lucide-react';
+import { useToast } from '@/hooks/use-toast';
const timeSlots = [
'09:00',
@@ -31,6 +32,7 @@ export function BookingCalendar() {
const [selectedDate, setSelectedDate] = useState
(new Date());
const [selectedSlot, setSelectedSlot] = useState(null);
const [selectedCourt, setSelectedCourt] = useState(null);
+ const { toast } = useToast();
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-IE', {
@@ -66,13 +68,24 @@ export function BookingCalendar() {
setSelectedSlot(null);
setSelectedCourt(null);
// Show success message
- alert('Booking created successfully!');
+ toast({
+ title: 'Success',
+ description: 'Booking created successfully!',
+ });
} else {
- alert(result.error || 'Booking failed');
+ toast({
+ title: 'Error',
+ description: result.error || 'Booking failed',
+ variant: 'destructive',
+ });
}
} catch (error) {
console.error('Booking error:', error);
- alert('An error occurred while creating the booking');
+ toast({
+ title: 'Error',
+ description: 'An error occurred while creating the booking',
+ variant: 'destructive',
+ });
}
};
diff --git a/components/dashboard/dashboard-header.tsx b/components/dashboard/dashboard-header.tsx
index a7355fb..759e810 100644
--- a/components/dashboard/dashboard-header.tsx
+++ b/components/dashboard/dashboard-header.tsx
@@ -10,6 +10,7 @@ import { useToast } from '@/hooks/use-toast';
import { NotificationBell, AnnouncementsModal } from '@/components/notifications/announcements';
import { UserProfile } from '@/components/user/user-profile';
import { ModeToggle } from '@/components/ui/mode-toggle';
+import type { AppConfig } from '@/lib/app-config';
interface DashboardHeaderProps {
user: {
@@ -28,6 +29,23 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
const [showAnnouncements, setShowAnnouncements] = useState(false);
const [showUserProfile, setShowUserProfile] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
+ const [appConfig, setAppConfig] = useState(null);
+
+ useEffect(() => {
+ const fetchAppConfig = async () => {
+ try {
+ const response = await fetch('/api/config');
+ if (response.ok) {
+ const config = await response.json();
+ setAppConfig(config);
+ }
+ } catch (error) {
+ console.error('Error fetching app config:', error);
+ }
+ };
+
+ fetchAppConfig();
+ }, []);
// Fetch unread announcements count on component mount
useEffect(() => {
@@ -78,7 +96,7 @@ export function DashboardHeader({ user }: DashboardHeaderProps) {
-
TT Booking
+ {appConfig?.clubName || 'TT Booking'}
{user.role === 'admin' &&
Admin }
diff --git a/lib/app-config.ts b/lib/app-config.ts
new file mode 100644
index 0000000..fa30973
--- /dev/null
+++ b/lib/app-config.ts
@@ -0,0 +1,91 @@
+import { db } from '@/lib/db';
+import { settings } from '@/lib/db/schema';
+import { eq } from 'drizzle-orm';
+
+export interface AppConfig {
+ clubName: string;
+ sportName: string;
+ appTitle: string;
+ appDescription: string;
+}
+
+const defaultConfig: AppConfig = {
+ clubName: 'TT Club',
+ sportName: 'Table Tennis',
+ appTitle: 'Table Tennis Booking System',
+ appDescription: 'Book your table tennis court slots with ease',
+};
+
+let cachedConfig: AppConfig | null = null;
+let cacheTime: number = 0;
+const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
+
+/**
+ * Get app configuration with caching
+ * This function fetches the club/brand settings from the database
+ */
+export async function getAppConfig(): Promise {
+ const now = Date.now();
+
+ // Return cached config if still valid
+ if (cachedConfig && now - cacheTime < CACHE_DURATION) {
+ return cachedConfig;
+ }
+
+ try {
+ // Fetch all brand/club settings
+ const configSettings = await db
+ .select()
+ .from(settings)
+ .where(eq(settings.key, 'club_name'))
+ .union(db.select().from(settings).where(eq(settings.key, 'sport_name')))
+ .union(db.select().from(settings).where(eq(settings.key, 'app_title')))
+ .union(db.select().from(settings).where(eq(settings.key, 'app_description')));
+
+ // Build config object
+ const config: AppConfig = { ...defaultConfig };
+
+ configSettings.forEach((setting) => {
+ switch (setting.key) {
+ case 'club_name':
+ config.clubName = setting.value;
+ break;
+ case 'sport_name':
+ config.sportName = setting.value;
+ break;
+ case 'app_title':
+ config.appTitle = setting.value;
+ break;
+ case 'app_description':
+ config.appDescription = setting.value;
+ break;
+ }
+ });
+
+ // Cache the result
+ cachedConfig = config;
+ cacheTime = now;
+
+ return config;
+ } catch (error) {
+ console.error('Error fetching app config:', error);
+ // Return default config on error
+ return defaultConfig;
+ }
+}
+
+/**
+ * Invalidate the cache (call when settings are updated)
+ */
+export function invalidateAppConfigCache() {
+ cachedConfig = null;
+ cacheTime = 0;
+}
+
+/**
+ * Get app config for client components (no database access)
+ * This should be used with server-side rendered props
+ */
+export function getDefaultAppConfig(): AppConfig {
+ return { ...defaultConfig };
+}
diff --git a/scripts/seed-brand-settings.ts b/scripts/seed-brand-settings.ts
new file mode 100644
index 0000000..815e526
--- /dev/null
+++ b/scripts/seed-brand-settings.ts
@@ -0,0 +1,46 @@
+#!/usr/bin/env tsx
+
+import { db } from '../lib/db';
+import { settings } from '../lib/db/schema';
+import { eq } from 'drizzle-orm';
+
+async function seedBrandSettings() {
+ console.log('🌱 Seeding brand settings...');
+
+ const brandSettings = [
+ { key: 'club_name', value: 'TT Club' },
+ { key: 'sport_name', value: 'Table Tennis' },
+ { key: 'app_title', value: 'Table Tennis Booking System' },
+ { key: 'app_description', value: 'Book your table tennis court slots with ease' },
+ ];
+
+ for (const setting of brandSettings) {
+ try {
+ // Check if setting exists
+ const existing = await db.select().from(settings).where(eq(settings.key, setting.key)).limit(1);
+
+ if (existing.length === 0) {
+ // Insert new setting
+ await db.insert(settings).values({
+ id: crypto.randomUUID(),
+ key: setting.key,
+ value: setting.value,
+ updatedAt: new Date(),
+ });
+ console.log(`✅ Added setting: ${setting.key} = ${setting.value}`);
+ } else {
+ console.log(`⚠️ Setting already exists: ${setting.key}`);
+ }
+ } catch (error) {
+ console.error(`❌ Error adding setting ${setting.key}:`, error);
+ }
+ }
+
+ console.log('✅ Brand settings seeded successfully!');
+ process.exit(0);
+}
+
+seedBrandSettings().catch((error) => {
+ console.error('❌ Error seeding brand settings:', error);
+ process.exit(1);
+});
diff --git a/tsconfig.json b/tsconfig.json
index 0690c80..c250659 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -27,5 +27,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "exclude": ["node_modules", "scripts/old-seeds/**/*"]
}