Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions app/api/events/[slug]/route.ts
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 }
);
}
}
79 changes: 79 additions & 0 deletions app/api/events/route.ts
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';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Cloudinary is configured elsewhere in the codebase
rg -n "cloudinary.config" --type ts
rg -n "CLOUDINARY" --type ts

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.ts

Repository: 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.ts

Repository: modichika/dev-events-nextjs16

Length of output: 2732


🏁 Script executed:

# Check if there's any Cloudinary config file or environment setup
fd -type f -name "*.ts" -o -name "*.js" -o -name "*.env*" | head -20

Repository: modichika/dev-events-nextjs16

Length of output: 243


🏁 Script executed:

# Search more broadly for CLOUDINARY references
rg "CLOUDINARY" --type ts --type js -A 2

Repository: modichika/dev-events-nextjs16

Length of output: 55


🏁 Script executed:

# Check for any configuration initialization patterns
rg "config\(" --type ts -B 2 -A 2 | head -40

Repository: modichika/dev-events-nextjs16

Length of output: 55


🏁 Script executed:

# Check project root for any configuration or setup files
ls -la | grep -E "(cloudinary|config|env)"

Repository: modichika/dev-events-nextjs16

Length of output: 342


🏁 Script executed:

# Check if there's a setup file or initialization
fd "middleware|setup|init|config" --extension ts --extension js

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_URL or individual credentials) are not set. Add explicit cloudinary.config() call with validation or environment variable checks to fail fast with a clear error message.

🤖 Prompt for AI Agents
In app/api/events/route.ts around line 2, the Cloudinary SDK is imported but not
configured; before any call to cloudinary.uploader.upload_stream (around lines
32–37) validate required env vars (CLOUDINARY_URL or CLOUDINARY_CLOUD_NAME,
CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET), call cloudinary.config(...) with
those values, and if any are missing throw or log a clear, early error so the
route fails fast with an actionable message; ensure the config step runs once at
module init and not per-request.

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});
}
}
115 changes: 115 additions & 0 deletions app/events/[slug]/page.tsx
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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add fallback or validation for BASE_URL.

If NEXT_PUBLIC_BASE_URL is not set, the fetch call at line 38 will produce an invalid URL like undefined/api/events/..., causing a runtime error. Consider adding a fallback or throwing an explicit error during build/startup.

-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
In app/events/[slug]/page.tsx around line 7 (and usage at line 38), BASE_URL is
read from process.env.NEXT_PUBLIC_BASE_URL without validation which can create
an invalid fetch target (e.g. "undefined/api/..."); add a validation/fallback:
either set BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ??
process.env.NEXT_PUBLIC_VERCEL_URL ?? '' and if empty throw a clear Error during
startup/build, or (preferred for server components) replace the absolute URL
construction with a relative path (e.g. start the fetch with '/api/events/...'
so no env var is required); ensure any error path logs a clear message and avoid
creating URLs when BASE_URL is falsy.


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
15 changes: 11 additions & 4 deletions app/page.tsx
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: modichika/dev-events-nextjs16

Length of output: 487


🏁 Script executed:

find . -name "page.tsx" -o -name "*.tsx" | grep -E "(app|src)" | head -10

Repository: modichika/dev-events-nextjs16

Length of output: 133


🏁 Script executed:

fd "page.tsx" --type f

Repository: modichika/dev-events-nextjs16

Length of output: 112


🏁 Script executed:

cat -n app/page.tsx

Repository: modichika/dev-events-nextjs16

Length of output: 1431


🏁 Script executed:

cat -n app/api/events/route.ts

Repository: modichika/dev-events-nextjs16

Length of output: 2732


🏁 Script executed:

find . -name "database*" -type f

Repository: 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 -5

Repository: 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 2

Repository: modichika/dev-events-nextjs16

Length of output: 100


🏁 Script executed:

find . -name "event.model*" -type f

Repository: 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 -la

Repository: modichika/dev-events-nextjs16

Length of output: 6836


🏁 Script executed:

rg "use cache" --type ts --type js

Repository: modichika/dev-events-nextjs16

Length of output: 102


🏁 Script executed:

rg "cacheLife" --type ts --type js -B 2 -A 2

Repository: modichika/dev-events-nextjs16

Length of output: 500


🌐 Web query:

Next.js 'use cache' directive and cacheLife() API documentation

💡 Result:

Here’s a concise summary of the Next.js "use cache" directive and the cacheLife() API with docs links.

  • What "use cache" does

    • Marks a route/component/function as cacheable (file-level or function/component-level). Use at top of file or top of an async function/component. It’s part of the Cache Components feature and cannot be used with request-time-only APIs (cookies/headers) unless you use "use cache: private". Default server-side revalidation is 15 minutes. [1]
  • Enabling

    • Enable the feature with the cacheComponents flag in next.config (or experimental flags in older versions). [1][3]
  • cacheLife() (API)

    • Use cacheLife (imported from next/cache) inside the scope of the cached function/component to set lifetime profile for that unit. Can be called with an inline profile object (stale, revalidate, expire) or with a named profile defined in next.config. Only one cacheLife call should execute per function invocation. Nested caches combine by respecting the shortest applicable lifetime when outer cache has no explicit profile. [2][3]
  • Key fields (cacheLife profile)

    • stale: controls client-router cache behavior (sent via x-nextjs-stale-time header; minimum 30s enforced).
    • revalidate: server revalidation interval.
    • expire: absolute expiry time. [2]
  • Configuring global/reusable profiles

    • Define named profiles in next.config.cacheLife and reference them with cacheLife(...). Example profiles shown in docs. [3]
  • Notes

    • cacheLife integrates with cacheTag and revalidation APIs for on‑demand/incremental revalidation.
    • Some docs are versioned/experimental; check the current Next.js docs for your Next.js version. [2][4]

Sources

  • "use cache" directive docs. [1]
  • cacheLife function docs. [2]
  • next.config.cacheLife (config) docs. [3]
  • Versioned/experimental note (Next.js v15 docs). [4]

🌐 Web query:

Next.js 16 pre-rendering Server Component fetch own Route Handler build time failure

💡 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):

  1. Call server logic directly (avoid fetching your own /api/route; import the same DB/service function used by the route). This avoids network calls during build. [1][2]
  2. Make the component render at request-time: export const dynamic = "force-dynamic" in the route/page or use a client component (use client) so fetch runs at runtime, not build. Alternatively use cache controls / revalidation as appropriate. [3][4]
  3. If you must call the route via HTTP, ensure it’s an external absolute URL available at runtime (via env var) — don’t rely on http://localhost during build. [2][1]

Why each works (brief):

  • Build-time prerender runs Server Components and static Route Handlers; there’s no local server to service internal HTTP fetches, and Route Handlers themselves can be statically prerendered. So fetching internal routes is an anti-pattern for server components. [1][3][4]

References:

  • Explanation and anti-pattern guidance. [1]
  • StackOverflow explanation and practical workaround (env URL / don't fetch internal routes at build). [2]
  • Next.js docs on Partial Prerendering / when components become dynamic. [3]
  • Vercel blog: Route Handlers are static by default and why fetching them in build/prerender matters. [4]

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 f

Repository: 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 5

Repository: 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 js

Repository: modichika/dev-events-nextjs16

Length of output: 1530


🏁 Script executed:

rg "cacheLife.*hours" --type ts --type js

Repository: modichika/dev-events-nextjs16

Length of output: 109


Avoid fetching your own /api route from a cached Server Component; fix invalid cacheLife() usage

With 'use cache' and cacheLife('hours'), this page is prerendered and cached. However, two issues need addressing:

1. Architecture (build-time fetch failure risk):
Pre-rendered Server Components that fetch your own Route Handlers can fail at build time (no server is listening yet) and add an unnecessary HTTP round trip. Next.js recommends calling data sources directly instead. Import the Event model and call Event.find() directly in this component rather than fetching /api/events.

2. API usage error:
cacheLife('hours') is invalid—'hours' is not a recognized profile. Use an inline profile object instead: cacheLife({ stale: 3600, revalidate: 3600, expire: 86400 }) or define a named profile in next.config.ts under cacheLife and reference it.

Additionally:

  • Replace event.title as the React key (line 24) with event.slug (already exists on IEvent and is unique).
  • Type the JSON response: const events: IEvent[] = await response.json().then(r => r.events) for full type safety.



const Page = () => {
return (
<section>
<h1 className={"text-center"}>The Hub For Every Dev <br/> Event You Can't Miss</h1>
Expand All @@ -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>
))}
Expand Down
43 changes: 43 additions & 0 deletions components/BookEvent.tsx
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

2 changes: 1 addition & 1 deletion components/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate id attribute across multiple card instances.

Since EventCard is rendered in a list, every card will share the same id="events-card", violating HTML uniqueness requirements for id attributes. This can cause issues with accessibility (screen readers), CSS selectors, and document.getElementById.

Consider using className instead if styling is the intent, or remove the id if it's not needed:

-        <Link href={`/events/${slug}`} id={"events-card"}>
+        <Link href={`/events/${slug}`} className={"event-card"}>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Link href={`/events/${slug}`} id={"events-card"}>
<Link href={`/events/${slug}`} className={"event-card"}>
🤖 Prompt for AI Agents
In components/EventCard.tsx around line 16, the Link element uses a fixed
id="events-card" which will be duplicated for every rendered card; remove the id
or replace it with a className (e.g., className="events-card") or generate a
unique id per item (e.g., using the slug or an index) depending on whether you
need a styling hook or a programmatic unique identifier. Ensure accessibility
and any JS that queries the element are updated to use the new class or unique
id.

<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}/>
Expand Down
Loading