-
Notifications
You must be signed in to change notification settings - Fork 0
implement api routes, server actions and caching, and the event detai… #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
|
|
||
| import connectDB from "@/lib/mongodb"; | ||
| import Event, { IEvent } from "@/database/event.model"; | ||
|
|
||
| // Define route params type for type safety | ||
| type RouteParams = { | ||
| params: Promise<{ | ||
| slug: string; | ||
| }>; | ||
| }; | ||
|
|
||
| /** | ||
| * GET /api/events/[slug] | ||
| * Fetches a single events by its slug | ||
| */ | ||
| export async function GET( | ||
| req: NextRequest, | ||
| { params }: RouteParams | ||
| ): Promise<NextResponse> { | ||
| try { | ||
| // Connect to database | ||
| await connectDB(); | ||
|
|
||
| // Await and extract slug from params | ||
| const { slug } = await params; | ||
|
|
||
| // Validate slug parameter | ||
| if (!slug || typeof slug !== "string" || slug.trim() === "") { | ||
| return NextResponse.json( | ||
| { message: "Invalid or missing slug parameter" }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Sanitize slug (remove any potential malicious input) | ||
| const sanitizedSlug = slug.trim().toLowerCase(); | ||
|
|
||
| // Query events by slug | ||
| const event: IEvent | null = await Event.findOne({ | ||
| slug: sanitizedSlug, | ||
| }).lean(); | ||
|
|
||
| // Handle events not found | ||
| if (!event) { | ||
| return NextResponse.json( | ||
| { message: `Event with slug '${sanitizedSlug}' not found` }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| // Return successful response with events data | ||
| return NextResponse.json( | ||
| { message: "Event fetched successfully", event }, | ||
| { status: 200 } | ||
| ); | ||
| } catch (error) { | ||
| // Log error for debugging (only in development) | ||
| if (process.env.NODE_ENV === "development") { | ||
| console.error("Error fetching events by slug:", error); | ||
| } | ||
|
|
||
| // Handle specific error types | ||
| if (error instanceof Error) { | ||
| // Handle database connection errors | ||
| if (error.message.includes("MONGODB_URI")) { | ||
| return NextResponse.json( | ||
| { message: "Database configuration error" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| // Return generic error with error message | ||
| return NextResponse.json( | ||
| { message: "Failed to fetch events", error: error.message }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
|
|
||
| // Handle unknown errors | ||
| return NextResponse.json( | ||
| { message: "An unexpected error occurred" }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||||||||||
| import { Event } from "@/database"; | ||||||||||||||
| import { v2 as cloudinary } from "cloudinary"; | ||||||||||||||
| import connectDB from "@/lib/mongodb"; | ||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||
|
|
||||||||||||||
| export async function POST(req: NextRequest) { | ||||||||||||||
| try { | ||||||||||||||
| await connectDB(); | ||||||||||||||
|
|
||||||||||||||
| const formData = await req.formData(); | ||||||||||||||
|
|
||||||||||||||
| let event; | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| event = Object.fromEntries(formData.entries()); | ||||||||||||||
| } catch (error) { | ||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { message: "Invalid JSON data format" }, | ||||||||||||||
| { status: 400 } | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const file = formData.get("image") as File; | ||||||||||||||
| if (!file) { | ||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { message: "Image file is required" }, | ||||||||||||||
| { status: 400 } | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| let tags = JSON.parse(formData.get("tags") as string); | ||||||||||||||
| let agenda = JSON.parse(formData.get("agenda") as string); | ||||||||||||||
|
|
||||||||||||||
| const arrayBuffer = await file.arrayBuffer(); | ||||||||||||||
| const buffer = Buffer.from(arrayBuffer); | ||||||||||||||
|
|
||||||||||||||
| const uploadResult = await new Promise((resolve, reject) => { | ||||||||||||||
| cloudinary.uploader | ||||||||||||||
| .upload_stream( | ||||||||||||||
| { resource_type: "image", folder: "dev-events" }, | ||||||||||||||
| (error, result) => { | ||||||||||||||
| if (error) return reject(error); | ||||||||||||||
| resolve(result); | ||||||||||||||
| } | ||||||||||||||
| ) | ||||||||||||||
| .end(buffer); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| event.image = (uploadResult as { secure_url: string }).secure_url; | ||||||||||||||
|
|
||||||||||||||
| const createdEvent = await Event.create({ ...event, tags, agenda }); | ||||||||||||||
|
|
||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { | ||||||||||||||
| message: "Event Created Successfully", | ||||||||||||||
| event: createdEvent, | ||||||||||||||
| }, | ||||||||||||||
| { status: 201 } | ||||||||||||||
| ); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| console.error("Error handling POST request:", e); | ||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { | ||||||||||||||
| message: "Event Creation Failed", | ||||||||||||||
| error: e instanceof Error ? e.message : "Unknown error", | ||||||||||||||
| }, | ||||||||||||||
| { status: 500 } | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| export async function GET() { | ||||||||||||||
| try { | ||||||||||||||
| await connectDB(); | ||||||||||||||
| const events = await Event.find().sort({ createdAt: -1 }); | ||||||||||||||
|
|
||||||||||||||
| if (!events) | ||||||||||||||
| return NextResponse.json({ message: "No event found" }, { status: 404 }); | ||||||||||||||
|
Comment on lines
+77
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect empty check—
🔎 Proposed fix const events = await Event.find().sort({ createdAt: -1 });
- if (!events)
+ if (events.length === 0)
return NextResponse.json({ message: "No event found" }, { status: 404 });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { message: "Events fetched successfully", events }, | ||||||||||||||
| { status: 200 } | ||||||||||||||
| ); | ||||||||||||||
| } catch (e) { | ||||||||||||||
| console.error("Error handling GET request:", e); | ||||||||||||||
| return NextResponse.json( | ||||||||||||||
| { | ||||||||||||||
| message: "Failed to fetch events", | ||||||||||||||
| error: e instanceof Error ? e.message : "Unknown error", | ||||||||||||||
| }, | ||||||||||||||
| { status: 500 } | ||||||||||||||
| ); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { Suspense } from "react"; | ||
| import EventDetails from "@/components/EventDetails"; | ||
|
|
||
| const EventDetailsPage = async ({ | ||
| params, | ||
| }: { | ||
| params: Promise<{ slug: string }>; | ||
| }) => { | ||
| const slug = params.then((p) => p.slug); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CRITICAL: Promise assigned instead of awaited value. Line 9 assigns a Promise to the 🔎 Required fix const EventDetailsPage = async ({
params,
}: {
params: Promise<{ slug: string }>;
}) => {
- const slug = params.then((p) => p.slug);
+ const { slug } = await params;
return (
<main>
<Suspense fallback={<div>Loading...</div>}>
- <EventDetails params={slug} />
+ <EventDetails slug={slug} />
</Suspense>
</main>
);
};🤖 Prompt for AI Agents |
||
|
|
||
| return ( | ||
| <main> | ||
| <Suspense fallback={<div>Loading...</div>}> | ||
| <EventDetails params={slug} /> | ||
| </Suspense> | ||
| </main> | ||
| ); | ||
| }; | ||
| export default EventDetailsPage; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,8 +1,16 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import EventCard from "@/components/EventCard"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ExploreBtn from "@/components/ExploreBtn"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { events } from "@/lib/constants"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IEvent } from "@/database"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { cacheLife } from "next/cache"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Page = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use cache"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cacheLife("hours"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(`${BASE_URL}/api/events`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { events } = await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling and validation for data fetching. The fetch call lacks critical error handling:
This will cause unhandled exceptions if the API is unavailable or returns an error. 🔎 Proposed fix const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
const Page = async () => {
"use cache";
cacheLife("hours");
+
+ if (!BASE_URL) {
+ console.error('NEXT_PUBLIC_BASE_URL is not configured');
+ return <div>Configuration error</div>;
+ }
+
+ try {
const response = await fetch(`${BASE_URL}/api/events`);
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch events: ${response.statusText}`);
+ }
+
const { events } = await response.json();
+
+ if (!events || !Array.isArray(events)) {
+ throw new Error('Invalid events data structure');
+ }
+ } catch (error) {
+ console.error('Error fetching events:', error);
+ return <div>Failed to load events</div>;
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Page = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="text-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -17,11 +25,13 @@ const Page = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-20 space-y-7"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h3>Featured Events</h3> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ul className="events"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {events.map((event) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li key={event.title}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <EventCard {...event} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {events && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.length > 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| events.map((event: IEvent) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className="list-none" key={event.title}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <EventCard {...event} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const BookEvent = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [email, setEmail] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [submitted, setSubmitted] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSubmit = (e: React.FormEvent) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setSubmitted(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Form submission is only simulated, not implemented. The Do you want me to generate an implementation that calls an API endpoint to persist the booking data? 🔎 Proposed fix to add actual API call const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
-
- setTimeout(() => {
- setSubmitted(true);
- }, 1000);
+
+ // Call API to submit booking
+ fetch('/api/bookings', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email }),
+ })
+ .then(response => {
+ if (response.ok) {
+ setSubmitted(true);
+ }
+ })
+ .catch(error => {
+ console.error('Booking failed:', error);
+ // Handle error state
+ });
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div id="book-event"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {submitted ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-sm">Thank you for signing up!</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form onSubmit={handleSubmit}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label htmlFor="email">Email Address</label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={email} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setEmail(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id="email" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Enter your email address" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button type="submit" className="button-submit"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Submit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default BookEvent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unhandled
JSON.parseerrors will crash the request.If
tagsoragendafields contain invalid JSON or are missing,JSON.parsewill throw an uncaught exception, resulting in a 500 error instead of a proper 400 validation error.🔎 Proposed fix
🤖 Prompt for AI Agents