Skip to content

Commit 71574fb

Browse files
committed
now it accepts dupicates
1 parent a0294ff commit 71574fb

File tree

11 files changed

+612
-2
lines changed

11 files changed

+612
-2
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE "events" (
2+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
3+
"slug" varchar(255) NOT NULL,
4+
"title" varchar(255) NOT NULL,
5+
"description" text,
6+
"venue" varchar(255),
7+
"date" timestamp,
8+
"time" varchar(50),
9+
"image_data" "bytea",
10+
"image_mime_type" varchar(100),
11+
"created_at" timestamp DEFAULT now() NOT NULL,
12+
"updated_at" timestamp DEFAULT now() NOT NULL,
13+
CONSTRAINT "events_slug_unique" UNIQUE("slug")
14+
);

public/sw.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/app/api/events/route.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,25 @@ export async function POST(req: Request) {
7070
}
7171
}
7272

73-
const slug = title.toLowerCase().replace(/\s+/g, "-");
73+
let slug = title.toLowerCase().replace(/\s+/g, "-");
74+
let slugExists = true;
75+
let attempt = 1;
76+
let newSlug = slug;
77+
78+
// Check if slug exists and append number if it does
79+
while (slugExists && attempt < 10) {
80+
const existingEvent = await db.query.events.findFirst({
81+
where: (events, { eq }) => eq(events.slug, newSlug),
82+
});
83+
84+
if (!existingEvent) {
85+
slugExists = false;
86+
slug = newSlug;
87+
} else {
88+
newSlug = `${slug}-${attempt}`;
89+
attempt++;
90+
}
91+
}
7492

7593
const result = await db
7694
.insert(events)

src/app/events/past/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { db } from '@/lib/db';
2+
import { events } from '@/lib/db/schema';
3+
import { lt } from 'drizzle-orm';
4+
import { format } from 'date-fns';
5+
6+
export default async function PastEventsPage() {
7+
const currentDate = format(new Date(), 'yyyy-MM-dd');
8+
9+
const pastEvents = await db
10+
.select()
11+
.from(events)
12+
.where(lt(events.endDate, currentDate))
13+
.orderBy(events.endDate);
14+
15+
return (
16+
<div className="space-y-6">
17+
<h1 className="text-2xl font-bold text-gray-900">Past Events</h1>
18+
{pastEvents.length === 0 ? (
19+
<p className="text-gray-500">No past events found.</p>
20+
) : (
21+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
22+
{pastEvents.map((event) => (
23+
<div key={event.id} className="rounded-lg border border-gray-200 p-4 shadow-sm">
24+
<h3 className="text-lg font-medium">{event.title}</h3>
25+
<p className="text-sm text-gray-500">
26+
{format(new Date(event.startDate), 'MMM d, yyyy')} - {format(new Date(event.endDate), 'MMM d, yyyy')}
27+
</p>
28+
<p className="mt-2 text-sm text-gray-700">{event.description}</p>
29+
</div>
30+
))}
31+
</div>
32+
)}
33+
</div>
34+
);
35+
}

src/app/events/present/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { db } from '@/lib/db';
2+
import { events } from '@/lib/db/schema';
3+
import { eq, gte } from 'drizzle-orm';
4+
import { format } from 'date-fns';
5+
6+
export default async function PresentEventsPage() {
7+
const currentDate = format(new Date(), 'yyyy-MM-dd');
8+
9+
const presentEvents = await db
10+
.select()
11+
.from(events)
12+
.where(gte(events.endDate, currentDate))
13+
.orderBy(events.startDate);
14+
15+
return (
16+
<div className="space-y-6">
17+
<h1 className="text-2xl font-bold text-gray-900">Present Events</h1>
18+
{presentEvents.length === 0 ? (
19+
<p className="text-gray-500">No current events found.</p>
20+
) : (
21+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
22+
{presentEvents.map((event) => (
23+
<div key={event.id} className="rounded-lg border border-gray-200 p-4 shadow-sm">
24+
<h3 className="text-lg font-medium">{event.title}</h3>
25+
<p className="text-sm text-gray-500">
26+
{format(new Date(event.startDate), 'MMM d, yyyy')} - {format(new Date(event.endDate), 'MMM d, yyyy')}
27+
</p>
28+
<p className="mt-2 text-sm text-gray-700">{event.description}</p>
29+
</div>
30+
))}
31+
</div>
32+
)}
33+
</div>
34+
);
35+
}

src/components/AppLayout.tsx

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import Link from 'next/link';
5+
import { usePathname } from 'next/navigation';
6+
import { EventList } from './EventList';
7+
import { events } from '@/lib/db/schema';
8+
import type { InferSelectModel } from 'drizzle-orm';
9+
10+
type Event = InferSelectModel<typeof events>;
11+
12+
interface AppLayoutProps {
13+
children: React.ReactNode;
14+
events: Event[];
15+
isAdmin?: boolean;
16+
}
17+
18+
export function AppLayout({ children, events, isAdmin = false }: AppLayoutProps) {
19+
const [sidebarOpen, setSidebarOpen] = useState(false);
20+
const pathname = usePathname();
21+
22+
// Don't show sidebar on auth pages
23+
const isAuthPage = pathname.startsWith('/login') || pathname.startsWith('/register');
24+
25+
if (isAuthPage) {
26+
return <>{children}</>;
27+
}
28+
29+
return (
30+
<div className="flex h-screen bg-gray-100">
31+
{/* Mobile sidebar backdrop */}
32+
{sidebarOpen && (
33+
<div
34+
className="fixed inset-0 z-20 bg-black bg-opacity-50 lg:hidden"
35+
onClick={() => setSidebarOpen(false)}
36+
/>
37+
)}
38+
39+
{/* Sidebar */}
40+
<div
41+
className={`fixed inset-y-0 left-0 z-30 w-80 transform bg-white shadow-lg transition-transform duration-300 ease-in-out lg:static lg:translate-x-0 ${
42+
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
43+
}`}
44+
>
45+
<div className="flex h-full flex-col overflow-y-auto border-r border-gray-200">
46+
<div className="flex h-16 items-center justify-between border-b border-gray-200 px-4">
47+
<Link href="/" className="text-xl font-semibold text-gray-900">
48+
Event Manager
49+
</Link>
50+
<button
51+
onClick={() => setSidebarOpen(false)}
52+
className="rounded-md p-2 text-gray-500 hover:bg-gray-100 lg:hidden"
53+
>
54+
<svg
55+
className="h-6 w-6"
56+
fill="none"
57+
stroke="currentColor"
58+
viewBox="0 0 24 24"
59+
xmlns="http://www.w3.org/2000/svg"
60+
>
61+
<path
62+
strokeLinecap="round"
63+
strokeLinejoin="round"
64+
strokeWidth={2}
65+
d="M6 18L18 6M6 6l12 12"
66+
/>
67+
</svg>
68+
</button>
69+
</div>
70+
71+
<nav className="flex-1 overflow-y-auto px-4 py-2">
72+
<div className="space-y-1">
73+
<Link
74+
href="/"
75+
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md ${
76+
pathname === '/'
77+
? 'bg-gray-100 text-gray-900'
78+
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
79+
}`}
80+
>
81+
<span className="truncate">Home</span>
82+
</Link>
83+
84+
<Link
85+
href="/events/present"
86+
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md ${
87+
pathname === '/events/present'
88+
? 'bg-gray-100 text-gray-900'
89+
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
90+
}`}
91+
>
92+
<span className="truncate">Present Events</span>
93+
</Link>
94+
95+
<Link
96+
href="/events/past"
97+
className={`group flex items-center px-3 py-2 text-sm font-medium rounded-md ${
98+
pathname === '/events/past'
99+
? 'bg-gray-100 text-gray-900'
100+
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
101+
}`}
102+
>
103+
<span className="truncate">Past Events</span>
104+
</Link>
105+
</div>
106+
107+
<div className="mt-8">
108+
<h3 className="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">
109+
All Events
110+
</h3>
111+
<div className="mt-1">
112+
<EventList events={events} isAdmin={isAdmin} />
113+
</div>
114+
</div>
115+
</nav>
116+
117+
<div className="border-t border-gray-200 p-4">
118+
<div className="flex items-center space-x-4">
119+
<div className="h-10 w-10 rounded-full bg-gray-300"></div>
120+
<div className="flex-1 min-w-0">
121+
<p className="text-sm font-medium text-gray-900 truncate">
122+
{isAdmin ? 'Admin User' : 'Guest User'}
123+
</p>
124+
<p className="text-sm text-gray-500 truncate">
125+
{isAdmin ? 'Administrator' : 'Viewer'}
126+
</p>
127+
</div>
128+
{isAdmin && (
129+
<Link
130+
href="/admin/settings"
131+
className="text-sm text-blue-600 hover:text-blue-800"
132+
>
133+
Settings
134+
</Link>
135+
)}
136+
</div>
137+
</div>
138+
</div>
139+
</div>
140+
141+
{/* Main content */}
142+
<div className="flex flex-1 flex-col overflow-hidden">
143+
<header className="flex h-16 items-center justify-between border-b border-gray-200 bg-white px-4 lg:px-6">
144+
<button
145+
onClick={() => setSidebarOpen(true)}
146+
className="rounded-md p-2 text-gray-500 hover:bg-gray-100 lg:hidden"
147+
>
148+
<svg
149+
className="h-6 w-6"
150+
fill="none"
151+
stroke="currentColor"
152+
viewBox="0 0 24 24"
153+
xmlns="http://www.w3.org/2000/svg"
154+
>
155+
<path
156+
strokeLinecap="round"
157+
strokeLinejoin="round"
158+
strokeWidth={2}
159+
d="M4 6h16M4 12h16M4 18h16"
160+
/>
161+
</svg>
162+
</button>
163+
<div className="flex-1 lg:pl-4">
164+
<h1 className="text-lg font-medium text-gray-900">
165+
{pathname === '/' ? 'Dashboard' : (pathname.split('/').pop() || '').charAt(0).toUpperCase() + (pathname.split('/').pop() || '').slice(1)}
166+
</h1>
167+
</div>
168+
<div className="flex items-center space-x-4">
169+
<Link
170+
href={isAdmin ? '/admin' : '/login'}
171+
className="text-sm font-medium text-gray-700 hover:text-gray-900"
172+
>
173+
{isAdmin ? 'Admin Panel' : 'Login'}
174+
</Link>
175+
</div>
176+
</header>
177+
178+
<main className="flex-1 overflow-y-auto bg-gray-50 p-4 lg:p-6">
179+
{children}
180+
</main>
181+
</div>
182+
</div>
183+
);
184+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
// import { startCleanupScheduler } from '@/lib/cleanup';
5+
6+
export function CleanupInitializer() {
7+
useEffect(() => {
8+
if (typeof window !== 'undefined') {
9+
// Only run on client side
10+
// startCleanupScheduler();
11+
}
12+
13+
return () => {
14+
// Clean up if needed
15+
};
16+
}, []);
17+
18+
return null; // This component doesn't render anything
19+
}

0 commit comments

Comments
 (0)