diff --git a/.idea/workspace.xml b/.idea/workspace.xml index c19ba64..ac6185e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,11 +5,14 @@ - + + + + + - - - + + @@ -56,34 +59,34 @@ - { - "keyToString": { - "ModuleVcsDetector.initialDetectionPerformed": "true", - "RunOnceActivity.MCP Project settings loaded": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "RunOnceActivity.typescript.service.memoryLimit.init": "true", - "codeWithMe.voiceChat.enabledByDefault": "false", - "com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true", - "git-widget-placeholder": "main", - "javascript.preferred.runtime.type.id": "node", - "js.debugger.nextJs.config.created.client": "true", - "js.debugger.nextJs.config.created.server": "true", - "junie.onboarding.icon.badge.shown": "true", - "last_opened_file_path": "D:/nextjs_demo/public", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "prettierjs.PrettierConfiguration.Package": "D:\\nextjs_demo\\node_modules\\prettier", - "settings.editor.selected.configurable": "preferences.pluginManager", - "to.speed.mode.migration.done": "true", - "ts.external.directory.path": "D:\\nextjs_demo\\node_modules\\typescript\\lib", - "vue.rearranger.settings.migration": "true" + +}]]> diff --git a/app/api/events/[slug]/route.ts b/app/api/events/[slug]/route.ts new file mode 100644 index 0000000..582ec4b --- /dev/null +++ b/app/api/events/[slug]/route.ts @@ -0,0 +1,75 @@ +import { NextRequest, NextResponse } from 'next/server'; + +import connectDB from '@/lib/mongodb'; +import Event 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 { + 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 = 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 }); + } +} diff --git a/app/api/events/route.ts b/app/api/events/route.ts new file mode 100644 index 0000000..8f4be13 --- /dev/null +++ b/app/api/events/route.ts @@ -0,0 +1,75 @@ +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(); + + const formData = await req.formData(); + + let event; + + try { + event = Object.fromEntries(formData.entries()); + } catch { + 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 }); + + const tags = JSON.parse(formData.get('tags') as string); + const 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: '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 }, { status: 500 }); + } +} diff --git a/app/events/[slug]/page.tsx b/app/events/[slug]/page.tsx new file mode 100644 index 0000000..7bf96e0 --- /dev/null +++ b/app/events/[slug]/page.tsx @@ -0,0 +1,16 @@ +import { Suspense } from 'react'; +import EventDetails from '@/components/EventDetails'; + +const EventDetailsPage = async ({ params }: { params: Promise<{ slug: string }> }) => { + const { slug } = await params; + + return ( +
+ Loading...}> + + +
+ ); +}; + +export default EventDetailsPage; diff --git a/app/layout.tsx b/app/layout.tsx index fc9a49e..f853c21 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,65 +1,54 @@ -import type { Metadata } from 'next'; -import { Geist, Martian_Mono, Schibsted_Grotesk } from 'next/font/google'; -import './globals.css'; -import { ReactNode } from 'react'; -import { cn } from '@/lib/utils'; -import LightRays from '@/components/LightRays'; -import Navbar from '@/components/Navbar'; - -const geist = Geist({ subsets: ['latin'], variable: '--font-sans' }); +import type { Metadata } from "next"; +import { Schibsted_Grotesk, Martian_Mono } from "next/font/google"; +import "./globals.css"; +import LightRays from "@/components/LightRays"; +import Navbar from "@/components/Navbar"; const schibstedGrotesk = Schibsted_Grotesk({ - variable: '--font-schibsted-grotesk', - subsets: ['latin'], + variable: "--font-schibsted-grotesk", + subsets: ["latin"], }); const martianMono = Martian_Mono({ - variable: '--font-martian-mono', - subsets: ['latin'], + variable: "--font-martian-mono", + subsets: ["latin"], }); export const metadata: Metadata = { - title: 'DevEvent', - description: "The Hub for Every Dev Event You Must'nt Miss", + title: "DevEvent", + description: "The Hub for Every Dev Event You Mustn't Miss", }; export default function RootLayout({ - children, -}: Readonly<{ - children: ReactNode; + children, + }: Readonly<{ + children: React.ReactNode; }>) { return ( - + - - + -
- -
+
+ +
-
{children}
- +
+ {children} +
+ ); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 8faad1b..799f5a2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,25 +1,29 @@ import ExploreBtn from '@/components/ExploreBtn'; import EventCard from '@/components/EventCard'; -import { events } from '@/lib/constants'; +import { getEvents } from '@/lib/actions/event.actions'; + +export const dynamic = 'force-dynamic'; + +const Page = async () => { + const events = await getEvents(); -const Page = () => { return (
-

- The Hub for Every Dev
Event You Can't Miss +

+ The Hub for Every Dev
Event You Cannot Miss

-

+

Hackathons, Meetups, and Conferences, All in One Place

-
+

Featured Events

-
    +
      {events.map((event) => ( -
    • +
    • ))} diff --git a/components/BookEvent.tsx b/components/BookEvent.tsx new file mode 100644 index 0000000..e2fd6d1 --- /dev/null +++ b/components/BookEvent.tsx @@ -0,0 +1,50 @@ +'use client'; + +import { useState } from 'react'; +import { createBooking } from '@/lib/actions/booking.actions'; +import posthog from 'posthog-js'; + +const BookEvent = ({ eventId, slug }: { eventId: string; slug: string }) => { + const [email, setEmail] = useState(''); + const [submitted, setSubmitted] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const { success } = await createBooking({ eventId, slug, email }); + + if (success) { + setSubmitted(true); + posthog.capture('event_booked', { eventId, slug, email }); + } else { + console.error('Booking creation failed'); + posthog.captureException('Booking creation failed'); + } + }; + + return ( +
      + {submitted ? ( +

      Thank you for signing up!

      + ) : ( +
      +
      + + setEmail(e.target.value)} + id='email' + placeholder='Enter your email address' + /> +
      + + +
      + )} +
      + ); +}; +export default BookEvent; \ No newline at end of file diff --git a/components/EventDetails.tsx b/components/EventDetails.tsx new file mode 100644 index 0000000..f946500 --- /dev/null +++ b/components/EventDetails.tsx @@ -0,0 +1,94 @@ +import Image from 'next/image'; +import { notFound } from 'next/navigation'; + +import { getEventBySlug } from '@/lib/actions/event.actions'; + +type EventDetailsProps = { + slug: string; +}; + +const EventDetails = async ({ slug }: EventDetailsProps) => { + const event = await getEventBySlug(slug); + + if (!event) { + notFound(); + } + + return ( +
      +
      +

      {event.mode}

      +

      {event.title}

      +

      {event.overview}

      +
      + +
      +
      + {event.title} + +
      +

      About

      +

      {event.description}

      +
      + +
      +

      Agenda

      +
        + {event.agenda.map((item) => ( +
      • {item}
      • + ))} +
      +
      +
      + + +
      +
      + ); +}; + +export default EventDetails; diff --git a/database/booking.model.ts b/database/booking.model.ts new file mode 100644 index 0000000..20e3d67 --- /dev/null +++ b/database/booking.model.ts @@ -0,0 +1,75 @@ +import { Schema, model, models, Document, Types } from 'mongoose'; +import Event from './event.model'; + +// TypeScript interface for Booking document +export interface IBooking extends Document { + eventId: Types.ObjectId; + email: string; + createdAt: Date; + updatedAt: Date; +} + +const BookingSchema = new Schema( + { + eventId: { + type: Schema.Types.ObjectId, + ref: 'Event', + required: [true, 'Event ID is required'], + }, + email: { + type: String, + required: [true, 'Email is required'], + trim: true, + lowercase: true, + validate: { + validator: function (email: string) { + // RFC 5322 compliant email validation regex + const emailRegex = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return emailRegex.test(email); + }, + message: 'Please provide a valid email address', + }, + }, + }, + { + timestamps: true, // Auto-generate createdAt and updatedAt + } +); + +// Pre-save hook to validate that the referenced event exists before creating a booking. +BookingSchema.pre('save', async function () { + const booking = this as IBooking; + + // Only validate eventId if it's new or modified + if (booking.isModified('eventId') || booking.isNew) { + try { + const eventExists = await Event.findById(booking.eventId).select('_id'); + + if (!eventExists) { + const error = new Error(`Event with ID ${booking.eventId} does not exist`); + error.name = 'ValidationError'; + throw error; + } + } catch { + const validationError = new Error('Invalid events ID format or database error'); + validationError.name = 'ValidationError'; + throw validationError; + } + } +}); + +// Create index on eventId for faster queries +BookingSchema.index({ eventId: 1 }); + +// Create compound index for common queries (events bookings by date) +BookingSchema.index({ eventId: 1, createdAt: -1 }); + +// Create index on email for user booking lookups +BookingSchema.index({ email: 1 }); + +// Enforce one booking per events per email +BookingSchema.index({ eventId: 1, email: 1 }, { unique: true, name: 'uniq_event_email' }); +const Booking = models.Booking || model('Booking', BookingSchema); + +export default Booking; diff --git a/database/event.model.ts b/database/event.model.ts new file mode 100644 index 0000000..118c71d --- /dev/null +++ b/database/event.model.ts @@ -0,0 +1,187 @@ +import { Schema, model, models, Document } from 'mongoose'; + +// TypeScript interface for Event document +export interface IEvent extends Document { + title: string; + slug: string; + description: string; + overview: string; + image: string; + venue: string; + location: string; + date: string; + time: string; + mode: string; + audience: string; + agenda: string[]; + organizer: string; + tags: string[]; + createdAt: Date; + updatedAt: Date; +} + +const EventSchema = new Schema( + { + title: { + type: String, + required: [true, 'Title is required'], + trim: true, + maxlength: [100, 'Title cannot exceed 100 characters'], + }, + slug: { + type: String, + unique: true, + lowercase: true, + trim: true, + }, + description: { + type: String, + required: [true, 'Description is required'], + trim: true, + maxlength: [1000, 'Description cannot exceed 1000 characters'], + }, + overview: { + type: String, + required: [true, 'Overview is required'], + trim: true, + maxlength: [500, 'Overview cannot exceed 500 characters'], + }, + image: { + type: String, + required: [true, 'Image URL is required'], + trim: true, + }, + venue: { + type: String, + required: [true, 'Venue is required'], + trim: true, + }, + location: { + type: String, + required: [true, 'Location is required'], + trim: true, + }, + date: { + type: String, + required: [true, 'Date is required'], + }, + time: { + type: String, + required: [true, 'Time is required'], + }, + mode: { + type: String, + required: [true, 'Mode is required'], + enum: { + values: ['online', 'offline', 'hybrid'], + message: 'Mode must be either online, offline, or hybrid', + }, + }, + audience: { + type: String, + required: [true, 'Audience is required'], + trim: true, + }, + agenda: { + type: [String], + required: [true, 'Agenda is required'], + validate: { + validator: (v: string[]) => v.length > 0, + message: 'At least one agenda item is required', + }, + }, + organizer: { + type: String, + required: [true, 'Organizer is required'], + trim: true, + }, + tags: { + type: [String], + required: [true, 'Tags are required'], + validate: { + validator: (v: string[]) => v.length > 0, + message: 'At least one tag is required', + }, + }, + }, + { + timestamps: true, // Auto-generate createdAt and updatedAt + } +); + +// Pre-save hook for slug generation and data normalization. +EventSchema.pre('save', function () { + const event = this as IEvent; + + // Generate slug only if title changed or document is new + if (event.isModified('title') || event.isNew) { + event.slug = generateSlug(event.title); + } + + // Normalize date to ISO format if it's not already + if (event.isModified('date')) { + event.date = normalizeDate(event.date); + } + + // Normalize time format (HH:MM) + if (event.isModified('time')) { + event.time = normalizeTime(event.time); + } +}); + +// Helper function to generate URL-friendly slug +function generateSlug(title: string): string { + return title + .toLowerCase() + .trim() + .replace(/[^a-z0-9\s-]/g, '') // Remove special characters + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-') // Replace multiple hyphens with single hyphen + .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens +} + +// Helper function to normalize date to ISO format +function normalizeDate(dateString: string): string { + const date = new Date(dateString); + if (isNaN(date.getTime())) { + throw new Error('Invalid date format'); + } + return date.toISOString().split('T')[0]; // Return YYYY-MM-DD format +} + +// Helper function to normalize time format +function normalizeTime(timeString: string): string { + // Handle various time formats and convert to HH:MM (24-hour format) + const timeRegex = /^(\d{1,2}):(\d{2})(\s*(AM|PM))?$/i; + const match = timeString.trim().match(timeRegex); + + if (!match) { + throw new Error('Invalid time format. Use HH:MM or HH:MM AM/PM'); + } + + let hours = parseInt(match[1]); + const minutes = match[2]; + const period = match[4]?.toUpperCase(); + + if (period) { + // Convert 12-hour to 24-hour format + if (period === 'PM' && hours !== 12) hours += 12; + if (period === 'AM' && hours === 12) hours = 0; + } + + if (hours < 0 || hours > 23 || parseInt(minutes) < 0 || parseInt(minutes) > 59) { + throw new Error('Invalid time values'); + } + + return `${hours.toString().padStart(2, '0')}:${minutes}`; +} + +// Create unique index on slug for better performance +EventSchema.index({ slug: 1 }, { unique: true }); + +// Create compound index for common queries +EventSchema.index({ date: 1, mode: 1 }); + +const Event = models.Event || model('Event', EventSchema); + +export default Event; diff --git a/database/index.ts b/database/index.ts new file mode 100644 index 0000000..4d765b7 --- /dev/null +++ b/database/index.ts @@ -0,0 +1,7 @@ +// Database models exports +export { default as Event } from './event.model'; +export { default as Booking } from './booking.model'; + +// TypeScript interfaces exports +export type { IEvent } from './event.model'; +export type { IBooking } from './booking.model'; diff --git a/lib/actions/booking.actions.ts b/lib/actions/booking.actions.ts new file mode 100644 index 0000000..23ffb0e --- /dev/null +++ b/lib/actions/booking.actions.ts @@ -0,0 +1,26 @@ +'use server'; + +import Booking from '@/database/booking.model'; + +import connectDB from '@/lib/mongodb'; + +export const createBooking = async ({ + eventId, + slug, + email, +}: { + eventId: string; + slug: string; + email: string; +}) => { + try { + await connectDB(); + + await Booking.create({ eventId, slug, email }); + + return { success: true }; + } catch (e) { + console.error('create booking failed', e); + return { success: false }; + } +}; diff --git a/lib/actions/event.actions.ts b/lib/actions/event.actions.ts new file mode 100644 index 0000000..0b804ce --- /dev/null +++ b/lib/actions/event.actions.ts @@ -0,0 +1,90 @@ +'use server'; + +import Event from '@/database/event.model'; +import { events as fallbackEvents } from '@/lib/constants'; +import connectDB from '@/lib/mongodb'; + +export type EventListItem = { + title: string; + image: string; + slug: string; + location: string; + date: string; + time: string; +}; + +export type EventDetailsItem = EventListItem & { + description: string; + overview: string; + venue: string; + mode: string; + audience: string; + agenda: string[]; + organizer: string; + tags: string[]; +}; + +const toFallbackEventDetails = (event: EventListItem): EventDetailsItem => ({ + ...event, + description: `${event.title} brings developers together for practical sessions, networking, and hands-on learning.`, + overview: + 'Join builders, engineers, and technical leaders for a focused event designed around real-world software development.', + venue: event.location, + mode: 'offline', + audience: 'Developers, founders, and technical teams', + agenda: ['Registration and networking', 'Expert talks and demos', 'Community discussions'], + organizer: 'DevEvent', + tags: ['development', 'community'], +}); + +export const getEvents = async (): Promise => { + try { + await connectDB(); + + const events = await Event.find({}) + .select('title image slug location date time') + .sort({ date: 1 }) + .lean(); + + return events.length > 0 ? events : fallbackEvents; + } catch (error) { + console.error('get events failed', error); + return fallbackEvents; + } +}; + +export const getEventBySlug = async (slug: string): Promise => { + try { + await connectDB(); + + const event = await Event.findOne({ slug }) + .select( + 'title image slug location date time description overview venue mode audience agenda organizer tags' + ) + .lean(); + + if (event) { + return event; + } + } catch (error) { + console.error('get event by slug failed', error); + } + + const fallbackEvent = fallbackEvents.find((event) => event.slug === slug); + return fallbackEvent ? toFallbackEventDetails(fallbackEvent) : null; +}; + +export const getSimilarEventsBySlug = async (slug: string) => { + try { + await connectDB(); + const event = await Event.findOne({ slug }); + + if (!event) { + return []; + } + + return await Event.find({ _id: { $ne: event._id }, tags: { $in: event.tags } }).lean(); + } catch { + return []; + } +}; diff --git a/lib/mongodb.ts b/lib/mongodb.ts new file mode 100644 index 0000000..dfce211 --- /dev/null +++ b/lib/mongodb.ts @@ -0,0 +1,53 @@ +import mongoose, { type Mongoose } from 'mongoose'; + +const MONGODB_URI = process.env.MONGODB_URI; + +if (!MONGODB_URI) { + throw new Error('Please define the MONGODB_URI environment variable.'); +} + +const mongodbUri = MONGODB_URI; + +type MongooseCache = { + conn: Mongoose | null; + promise: Promise | null; +}; + +const globalForMongoose = globalThis as typeof globalThis & { + mongooseCache?: MongooseCache; +}; + +// Keep the connection across hot reloads in development to avoid opening +// a new MongoDB connection every time Next.js reloads server modules. +const cached = globalForMongoose.mongooseCache ?? { + conn: null, + promise: null, +}; + +globalForMongoose.mongooseCache = cached; + +export async function connectToDatabase(): Promise { + if (cached.conn) { + return cached.conn; + } + + if (!cached.promise) { + // Disable command buffering so database errors surface immediately. + cached.promise = mongoose + .connect(mongodbUri, { + bufferCommands: false, + }) + .then((mongooseInstance) => mongooseInstance); + } + + try { + cached.conn = await cached.promise; + } catch (error) { + cached.promise = null; + throw error; + } + + return cached.conn; +} + +export default connectToDatabase; diff --git a/package-lock.json b/package-lock.json index 957eb95..2c03a3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,11 @@ "version": "0.1.0", "dependencies": { "class-variance-authority": "^0.7.1", + "cloudinary": "^2.10.0", "clsx": "^2.1.1", "lucide-react": "^1.14.0", - "next": "16.2.4", + "mongoose": "^9.6.1", + "next": "^16.2.4", "ogl": "^1.0.11", "posthog-js": "^1.372.6", "posthog-node": "^5.33.0", @@ -1598,6 +1600,15 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz", + "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==", + "license": "MIT", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@mswjs/interceptors": { "version": "0.41.8", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.8.tgz", @@ -4176,6 +4187,21 @@ "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==", "license": "MIT" }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==", + "license": "MIT" + }, + "node_modules/@types/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==", + "license": "MIT", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz", @@ -5220,6 +5246,15 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz", + "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -5447,6 +5482,18 @@ "node": ">=8" } }, + "node_modules/cloudinary": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.10.0.tgz", + "integrity": "sha512-sY09kYg7wprkndAOjZBAYqFZqwL+SxnEGcAvksOvFA+5upnFn949UjkEkHKNSwkBtW/xRDd0p6NgbSXZcxkI3w==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.23" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -8251,6 +8298,15 @@ "node": ">=4.0" } }, + "node_modules/kareem": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.3.0.tgz", + "integrity": "sha512-kpSuLD3/7RenBnjnJdOHXCKC8dTd1JzeOiJhN0necWWci6cC+qX+VuwPnMVgb+a4+KNJSfgqahpnfWaeDXCimw==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8587,6 +8643,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -8699,6 +8761,12 @@ "node": ">= 0.8" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "license": "MIT" + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", @@ -8807,6 +8875,104 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mongodb": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz", + "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==", + "license": "Apache-2.0", + "dependencies": { + "@mongodb-js/saslprep": "^1.3.0", + "bson": "^7.2.0", + "mongodb-connection-string-url": "^7.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.806.0", + "@mongodb-js/zstd": "^7.0.0", + "gcp-metadata": "^7.0.1", + "kerberos": "^7.0.0", + "mongodb-client-encryption": ">=7.0.0 <7.1.0", + "snappy": "^7.3.2", + "socks": "^2.8.6" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz", + "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/whatwg-url": "^13.0.0", + "whatwg-url": "^14.1.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/mongoose": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.6.1.tgz", + "integrity": "sha512-3T8/b0plM3ZJPW3WjlzVMIGJEYYTjgDPQ05Qzru3xu3/wOPSFKWYxdwUF2dl8h3NG5dVkzIuOkZdLacnlLf/sA==", + "license": "MIT", + "dependencies": { + "kareem": "3.3.0", + "mongodb": "~7.2", + "mpath": "0.9.0", + "mquery": "6.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mpath": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", + "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz", + "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==", + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9747,7 +9913,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -10635,6 +10800,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sift": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", + "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", + "license": "MIT" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -10671,6 +10842,15 @@ "node": ">=0.10.0" } }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "license": "MIT", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -11118,6 +11298,18 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -11571,6 +11763,28 @@ "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", "license": "Apache-2.0" }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 99dd161..53749ef 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ }, "dependencies": { "class-variance-authority": "^0.7.1", + "cloudinary": "^2.10.0", "clsx": "^2.1.1", "lucide-react": "^1.14.0", - "next": "16.2.4", + "mongoose": "^9.6.1", + "next": "^16.2.4", "ogl": "^1.0.11", "posthog-js": "^1.372.6", "posthog-node": "^5.33.0",