-
Notifications
You must be signed in to change notification settings - Fork 0
implement api routes, server actions and caching, and the event details #4
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,100 @@ | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import connectDB from '@/lib/mongodb'; | ||
| import Event, { IEvent } from '@/database/event.model'; | ||
|
|
||
| // Define params type for Next.js dynamic route | ||
| 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 for params | ||
| const { slug } = await params; | ||
|
|
||
| // Validate slug parameter | ||
| if (!slug || typeof slug !== 'string') { | ||
| return NextResponse.json( | ||
| { message: 'Invalid slug parameter', success: false }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
| // Sanitize slug to prevent injection attacks | ||
| const sanitizedSlug = slug.trim().toLowerCase(); | ||
|
|
||
| // Validate slug format (alphanumeric and hyphens only) | ||
| const slugRegex = /^[a-z0-9-]+$/; | ||
| if (!slugRegex.test(sanitizedSlug)) { | ||
| return NextResponse.json( | ||
| { | ||
| message: 'Invalid slug format. Slug must contain only lowercase letters, numbers, and hyphens', | ||
| success: false | ||
| }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| // 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`, | ||
| success: false | ||
| }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| // Return events data | ||
| return NextResponse.json( | ||
| { | ||
| message: 'Event fetched successfully', | ||
| success: true, | ||
| event, | ||
| }, | ||
| { status: 200 } | ||
| ); | ||
|
|
||
| } catch (error) { | ||
| console.error('Error fetching events by slug:', error); | ||
|
|
||
| // Handle database connection errors | ||
| if (error instanceof Error && error.message.includes('connection')) { | ||
| return NextResponse.json( | ||
| { | ||
| message: 'Database connection error', | ||
| success: false, | ||
| }, | ||
| { status: 503 } | ||
| ); | ||
| } | ||
|
|
||
| // Handle unexpected errors | ||
| return NextResponse.json( | ||
| { | ||
| message: 'Failed to fetch events', | ||
| success: false, | ||
| }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import {NextRequest, NextResponse} from "next/server"; | ||
| import { v2 as cloudinary } from 'cloudinary'; | ||
| import connectDB from "@/lib/mongodb"; | ||
| import Event from '@/database/event.model'; | ||
|
|
||
|
|
||
| export async function POST(req: NextRequest){ try{ | ||
| await connectDB(); | ||
| console.log("1. 🚀 ROUTE: Handing data to the Model..."); | ||
| const formData = await req.formData(); | ||
|
|
||
| let event; | ||
|
|
||
| try { | ||
| event = Object.fromEntries(formData.entries()); | ||
| } catch (e) { | ||
| 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); | ||
|
|
||
|
|
||
| let tags, agenda; | ||
| try { | ||
| tags = JSON.parse(formData.get('tags') as string); | ||
| agenda = JSON.parse(formData.get('agenda') as string); | ||
| } catch (parseError) { | ||
| return NextResponse.json( | ||
| { message: 'Invalid JSON format for tags or agenda' }, | ||
| { status: 400 } | ||
| ); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| 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: 'DevEvent' }, (error, results) => { | ||
| if(error) return reject(error); | ||
| resolve(results); | ||
| }).end(buffer); | ||
| }); | ||
|
|
||
| event.image = (uploadResult as { secure_url: string}).secure_url; | ||
|
|
||
|
|
||
| const createdEvent = await Event.create({ | ||
| ...event, | ||
| tags: tags, | ||
| agenda: agenda, | ||
| }); | ||
|
|
||
| return NextResponse.json({message: 'Event Created successfully', event: createdEvent}, {status: 201}); | ||
|
|
||
| } catch (e) { | ||
| console.error(e); | ||
| return NextResponse.json({message : 'Event Creation Failed', error: e instanceof Error ? e.message: 'Unknown'}, {status: 500}); | ||
| } | ||
| } | ||
|
|
||
| export async function GET(){ | ||
| try { | ||
| await connectDB(); | ||
|
|
||
| const events = await Event.find().sort({createdAt: -1}); | ||
|
|
||
| return NextResponse.json({ message: 'Events fetched successfully', events}, {status: 200}); | ||
|
|
||
| } catch (e) { | ||
| return NextResponse.json({message: 'Event Fetching failed', error: e instanceof Error ? e.message : 'Unknown error'}, { status: 500}); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import {notFound} from "next/navigation"; | ||
| import Image from 'next/image'; | ||
| import {IEvent} from '@/database/event.model'; | ||
| import BookEvent from "@/components/BookEvent"; | ||
| import {getSimilarEventsBySlug} from "@/lib/actions/event.action"; | ||
| import EventCard from "@/components/EventCard"; | ||
| const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL; | ||
|
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 fallback or validation for If -const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
+const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
+if (!BASE_URL) {
+ throw new Error('NEXT_PUBLIC_BASE_URL environment variable is not set');
+}Alternatively, for server components, you could use relative URLs since the fetch happens on the server: -const request = await fetch(`${BASE_URL}/api/events/${slug}`);
+const request = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL || ''}/api/events/${slug}`);Also applies to: 38-38 🤖 Prompt for AI Agents |
||
|
|
||
| const EventDetailItem = ({icon, alt, label} : { icon: string; alt:string; label:string}) => ( | ||
| <div className={"flex flex-row gap-2 items-center"}> <Image src={icon} alt={alt} width={17} height={17}/> | ||
| <p>{label}</p> | ||
| </div> | ||
| ) | ||
|
|
||
| const EventAgenda = ({agendaItems}: {agendaItems: string[]}) => ( | ||
| <div className={"agenda"}> | ||
| <h2>Agenda</h2> | ||
| <ul> | ||
| {agendaItems.map((item) => ( | ||
| <li key={item}>{item}</li> | ||
| ))} | ||
| </ul> | ||
| </div> | ||
| ) | ||
|
|
||
|
|
||
| const EventTags = ({tags}: {tags: string[] }) => ( | ||
| <div className={"flex flex-row gap-1.5 flex-wrap"}> | ||
| {tags.map((tag) => ( | ||
| <div className={"pill"} key={tag}>{tag}</div> | ||
| ))} | ||
| </div> | ||
| ) | ||
|
|
||
|
|
||
| const EventDetailsPage = async ({params}: {params: Promise<{ slug: string }>}) => { | ||
| const { slug } = await params; | ||
| const request = await fetch(`${BASE_URL}/api/events/${slug}`); | ||
|
|
||
| if (!request.ok) { | ||
| return notFound(); | ||
| } | ||
|
|
||
| const { event } = await request.json(); | ||
| const { description, image, overview, date, time, location, mode, agenda, audience, tags, organizer } = event; | ||
|
|
||
| const bookings = 10; | ||
|
|
||
| const similarEvents: IEvent[] = await getSimilarEventsBySlug(slug); | ||
|
|
||
| console.log({similarEvents}); | ||
|
|
||
| return ( | ||
| <section id={"event"}> | ||
| <div className={"header"}> | ||
| <h1>Event Description</h1> | ||
| <p>{description}</p> | ||
| </div> | ||
| <div className={"details"}> | ||
| {/* Left Side - Event Content */} | ||
| <div className={"content"}> | ||
| <Image src={image} alt={"Event Banner"} width={800} height={800} className={"banner"} /> | ||
|
|
||
| <section className={"flex-col-gap-2"}> | ||
| <h2>Overview</h2> | ||
| <p>{overview}</p> | ||
| </section> | ||
|
|
||
| <section className={"flex-col-gap-2"}> | ||
| <h2>Event Details</h2> | ||
| <EventDetailItem icon={"/icons/calendar.svg"} alt={"calendar"} label={date} /> | ||
| <EventDetailItem icon={"/icons/clock.svg"} alt={"clock"} label={time} /> | ||
| <EventDetailItem icon={"/icons/pin.svg"} alt={"pin"} label={location} /> | ||
| <EventDetailItem icon={"/icons/mode.svg"} alt={"mode"} label={mode} /> | ||
| <EventDetailItem icon={"/icons/audience.svg"} alt={"audience"} label={audience} /> | ||
| </section> | ||
|
|
||
| <EventAgenda agendaItems={agenda} /> | ||
|
|
||
| <section className={"flex-col-gap-2"}> | ||
| <h2>About the Organizer</h2> | ||
| <p>{organizer}</p> | ||
| </section> | ||
|
|
||
| <EventTags tags={tags} /> | ||
|
|
||
| </div> | ||
| {/* Right Side - Booking Form*/} | ||
| <aside className={"booking"}> | ||
| <div className={"signup-card"}> | ||
| <h2>Book Your Spot!</h2> | ||
| {bookings > 0 ? ( | ||
| <p className={"text-sm"}> | ||
| Join {bookings} people who have already booked their spot! | ||
| </p> | ||
| ) : ( | ||
| <p className={"text-sm"}>Be the first to book your spot!</p> | ||
| )} | ||
| <BookEvent /> | ||
| </div> | ||
| </aside> | ||
| </div> | ||
|
|
||
| <div className={"flex w-full flex-col gap-4 pt-20"}> | ||
| <h2>Similar Events</h2> | ||
| <div className={"events"}> | ||
| {similarEvents.length > 0 && similarEvents.map((similarEvent: IEvent) => ( | ||
| <EventCard key={String(similarEvent.title)} {...similarEvent} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </section> | ||
| ) | ||
| } | ||
| export default EventDetailsPage | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,17 @@ | ||
| import ExploreBtn from "@/components/ExploreBtn"; | ||
| import EventCard from "@/components/EventCard"; | ||
| 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
+3
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. 🧩 Analysis chain🏁 Script executed: git ls-files | head -20Repository: modichika/dev-events-nextjs16 Length of output: 487 🏁 Script executed: find . -name "page.tsx" -o -name "*.tsx" | grep -E "(app|src)" | head -10Repository: modichika/dev-events-nextjs16 Length of output: 133 🏁 Script executed: fd "page.tsx" --type fRepository: modichika/dev-events-nextjs16 Length of output: 112 🏁 Script executed: cat -n app/page.tsxRepository: modichika/dev-events-nextjs16 Length of output: 1431 🏁 Script executed: cat -n app/api/events/route.tsRepository: modichika/dev-events-nextjs16 Length of output: 2732 🏁 Script executed: find . -name "database*" -type fRepository: modichika/dev-events-nextjs16 Length of output: 55 🏁 Script executed: cat -n database/index.ts 2>/dev/null || cat -n database.ts 2>/dev/null || find . -path "*/database*" -name "*.ts" -o -name "*.js" | head -5Repository: modichika/dev-events-nextjs16 Length of output: 409 🏁 Script executed: cat -n .env.local 2>/dev/null || cat -n .env 2>/dev/null || echo "No env file found"Repository: modichika/dev-events-nextjs16 Length of output: 90 🏁 Script executed: rg "NEXT_PUBLIC_BASE_URL" --type ts --type tsx -A 2 -B 2Repository: modichika/dev-events-nextjs16 Length of output: 100 🏁 Script executed: find . -name "event.model*" -type fRepository: modichika/dev-events-nextjs16 Length of output: 98 🏁 Script executed: cat -n database/event.model.ts 2>/dev/null || find . -path "*/database*" -name "*.ts" | xargs ls -laRepository: modichika/dev-events-nextjs16 Length of output: 6836 🏁 Script executed: rg "use cache" --type ts --type jsRepository: modichika/dev-events-nextjs16 Length of output: 102 🏁 Script executed: rg "cacheLife" --type ts --type js -B 2 -A 2Repository: modichika/dev-events-nextjs16 Length of output: 500 🌐 Web query:
💡 Result: Here’s a concise summary of the Next.js "use cache" directive and the cacheLife() API with docs links.
Sources
🌐 Web query:
💡 Result: Short answer: This happens because Next.js prerenders Server Components at build time — if a Server Component calls fetch() to your app's own Route Handler, that fetch runs during build when no app server is running (or the route is treated as static), causing fetch failures or prerender errors. Recommended fixes: call the backend logic directly (shared function), make the component/runtime dynamic, or move the request to client/runtime. Sources below. What to do (pick one):
Why each works (brief):
References:
If you want, paste your component + route code and I’ll show the minimal change (e.g., how to import the server function or set dynamic = "force-dynamic"). 🏁 Script executed: find . -name "next.config.*" -type fRepository: modichika/dev-events-nextjs16 Length of output: 89 🏁 Script executed: cat -n next.config.js 2>/dev/null || cat -n next.config.ts 2>/dev/null || find . -name "next.config.*" -exec cat -n {} \;Repository: modichika/dev-events-nextjs16 Length of output: 894 🏁 Script executed: rg "cacheLife" --type json --type js --type ts -B 5 -A 5Repository: modichika/dev-events-nextjs16 Length of output: 678 🏁 Script executed: cat package.json | grep -A 2 -B 2 "next"Repository: modichika/dev-events-nextjs16 Length of output: 548 🏁 Script executed: rg "hours" -A 3 -B 3 --type ts --type jsRepository: modichika/dev-events-nextjs16 Length of output: 1530 🏁 Script executed: rg "cacheLife.*hours" --type ts --type jsRepository: modichika/dev-events-nextjs16 Length of output: 109 Avoid fetching your own With 1. Architecture (build-time fetch failure risk): 2. API usage error: Additionally:
|
||
|
|
||
|
|
||
| const Page = () => { | ||
| return ( | ||
| <section> | ||
| <h1 className={"text-center"}>The Hub For Every Dev <br/> Event You Can't Miss</h1> | ||
|
|
@@ -13,8 +20,8 @@ const Page = () => { | |
| <div className={"mt-20 space-y-7"}> | ||
| <h3>Featured Events</h3> | ||
| <ul className={"events"}> | ||
| {events.map((event) => ( | ||
| <li key={event.title}> | ||
| {events && events.length > 0 && events.map((event: IEvent) => ( | ||
| <li key={event.title} className={"list-none"}> | ||
| <EventCard {...event}/> | ||
| </li> | ||
| ))} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| '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) | ||
|
|
||
| } | ||
|
|
||
| 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 | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -13,7 +13,7 @@ interface Props { | |||||
|
|
||||||
| const EventCard = ({title,image, slug, location, time, date} : Props) => { | ||||||
| return ( | ||||||
| <Link href={`/events/${slug}`} id={"event-card"}> | ||||||
| <Link href={`/events/${slug}`} id={"events-card"}> | ||||||
|
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. Duplicate Since Consider using - <Link href={`/events/${slug}`} id={"events-card"}>
+ <Link href={`/events/${slug}`} className={"event-card"}>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| <Image src={image} alt={title} width={410} height={300} className={"poster"}/> | ||||||
| <div className={"flex flex-row gap-2"}> | ||||||
| <Image src={"/icons/pin.svg"} alt={"location"} width={14} height={14}/> | ||||||
|
|
||||||
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.
🧩 Analysis chain
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 55
🏁 Script executed:
# Read the file to see context around the import and lines 32-37 wc -l app/api/events/route.tsRepository: modichika/dev-events-nextjs16
Length of output: 99
🏁 Script executed:
# Read the entire file (if reasonable size) or specific sections cat -n app/api/events/route.tsRepository: modichika/dev-events-nextjs16
Length of output: 2732
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 243
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 55
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 55
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 342
🏁 Script executed:
Repository: modichika/dev-events-nextjs16
Length of output: 87
Cloudinary configuration missing — add explicit setup or validation.
The Cloudinary SDK at line 2 is imported but never explicitly configured. At lines 32-37,
cloudinary.uploader.upload_stream()is called directly without verifying configuration, which will fail silently or with unclear errors if environment variables (CLOUDINARY_URLor individual credentials) are not set. Add explicitcloudinary.config()call with validation or environment variable checks to fail fast with a clear error message.🤖 Prompt for AI Agents