This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, JavaScript Mastery.
If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!
Built on Next.js 14, the events application stands as a comprehensive, full-stack platform for managing events. It serves as a hub, spotlighting diverse events taking place globally. Featuring seamless payment processing through Stripe, you have the capability to purchase tickets for any event or even initiate and manage your own events.
If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out.
Follow these steps to set up the project locally on your machine.
globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
* {
list-style: none;
padding: 0;
margin: 0;
scroll-behavior: smooth;
}
body {
font-family: var(--font-poppins)
}
.filter-grey {
filter: brightness(0) saturate(100%) invert(47%) sepia(0%) saturate(217%)
hue-rotate(32deg) brightness(98%) contrast(92%);
}
/* ========================================== TAILWIND STYLES */
@layer utilities {
.wrapper {
@apply max-w-7xl lg:mx-auto p-5 md:px-10 xl:px-0 w-full;
}
.flex-center {
@apply flex justify-center items-center;
}
.flex-between {
@apply flex justify-between items-center;
}
/* TYPOGRAPHY */
/* 64 */
.h1-bold {
@apply font-bold text-[40px] leading-[48px] lg:text-[48px] lg:leading-[60px] xl:text-[58px] xl:leading-[74px];
}
/* 40 */
.h2-bold {
@apply font-bold text-[32px] leading-[40px] lg:text-[36px] lg:leading-[44px] xl:text-[40px] xl:leading-[48px];
}
.h2-medium {
@apply font-medium text-[32px] leading-[40px] lg:text-[36px] lg:leading-[44px] xl:text-[40px] xl:leading-[48px];
}
/* 36 */
.h3-bold {
@apply font-bold text-[28px] leading-[36px] md:text-[36px] md:leading-[44px];
}
.h3-medium {
@apply font-medium text-[28px] leading-[36px] md:text-[36px] md:leading-[44px];
}
/* 32 */
.h4-medium {
@apply font-medium text-[32px] leading-[40px];
}
/* 28 */
.h5-bold {
@apply font-bold text-[28px] leading-[36px];
}
/* 24 */
.p-bold-24 {
@apply font-bold text-[24px] leading-[36px];
}
.p-medium-24 {
@apply font-medium text-[24px] leading-[36px];
}
.p-regular-24 {
@apply font-normal text-[24px] leading-[36px];
}
/* 20 */
.p-bold-20 {
@apply font-bold text-[20px] leading-[30px] tracking-[2%];
}
.p-semibold-20 {
@apply text-[20px] font-semibold leading-[30px] tracking-[2%];
}
.p-medium-20 {
@apply text-[20px] font-medium leading-[30px];
}
.p-regular-20 {
@apply text-[20px] font-normal leading-[30px] tracking-[2%];
}
/* 18 */
.p-semibold-18 {
@apply text-[18px] font-semibold leading-[28px] tracking-[2%];
}
.p-medium-18 {
@apply text-[18px] font-medium leading-[28px];
}
.p-regular-18 {
@apply text-[18px] font-normal leading-[28px] tracking-[2%];
}
/* 16 */
.p-bold-16 {
@apply text-[16px] font-bold leading-[24px];
}
.p-medium-16 {
@apply text-[16px] font-medium leading-[24px];
}
.p-regular-16 {
@apply text-[16px] font-normal leading-[24px];
}
/* 14 */
.p-semibold-14 {
@apply text-[14px] font-semibold leading-[20px];
}
.p-medium-14 {
@apply text-[14px] font-medium leading-[20px];
}
.p-regular-14 {
@apply text-[14px] font-normal leading-[20px];
}
/* 12 */
.p-medium-12 {
@apply text-[12px] font-medium leading-[20px];
}
/* SHADCN OVERRIDES */
.select-field {
@apply w-full bg-grey-50 h-[54px] placeholder:text-grey-500 rounded-full p-regular-16 px-5 py-3 border-none focus-visible:ring-transparent focus:ring-transparent !important;
}
.input-field {
@apply bg-grey-50 h-[54px] focus-visible:ring-offset-0 placeholder:text-grey-500 rounded-full p-regular-16 px-4 py-3 border-none focus-visible:ring-transparent !important;
}
.textarea {
@apply bg-grey-50 flex flex-1 placeholder:text-grey-500 p-regular-16 px-5 py-3 border-none focus-visible:ring-transparent !important;
}
.button {
@apply rounded-full h-[54px] p-regular-16;
}
.select-item {
@apply py-3 cursor-pointer focus:bg-primary-50;
}
.toggle-switch {
@apply bg-gray-300 !important;
}
}
/* ========================================== CLERK STYLES */
.cl-logoImage {
height: 38px;
}
.cl-userButtonBox {
flex-direction: row-reverse;
}
.cl-userButtonOuterIdentifier {
font-size: 16px;
}
.cl-userButtonPopoverCard {
right: 4px !important;
}
.cl-formButtonPrimary:hover,
.cl-formButtonPrimary:focus,
.cl-formButtonPrimary:active {
background-color: #705CF7
}
/* ========================================== REACT-DATEPICKER STYLES */
.datePicker {
width: 100%;
}
.react-datepicker__input-container input {
background-color: transparent;
width: 100%;
outline: none;
margin-left: 16px;
}
.react-datepicker__day--selected {
background-color: #624cf5 !important;
color: #ffffff !important;
border-radius: 4px;
}
.react-datepicker__time-list-item--selected {
background-color: #624cf5 !important;
}
tailwind.config.ts
/** @type {import('tailwindcss').Config} */
import { withUt } from 'uploadthing/tw';
module.exports = withUt({
darkMode: ['class'],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px',
},
},
extend: {
colors: {
primary: {
500: '#624CF5',
50: ' #F6F8FD',
DEFAULT: '#624CF5',
foreground: 'hsl(var(--primary-foreground))',
},
coral: {
500: '#15BF59',
},
grey: {
600: '#545454', // Subdued - color name in figma
500: '#757575',
400: '#AFAFAF', // Disabled - color name in figma
50: '#F6F6F6', // White Grey - color name in figma
},
black: '#000000',
white: '#FFFFFF',
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
foreground: 'hsl(var(--foreground))',
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))',
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))',
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))',
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))',
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))',
},
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))',
},
},
fontFamily: {
poppins: ['var(--font-poppins)'],
},
backgroundImage: {
'dotted-pattern': "url('/assets/images/dotted-pattern.png')",
'hero-img': "url('/assets/images/hero.png')",
},
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)',
},
keyframes: {
'accordion-down': {
from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' },
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
},
},
},
plugins: [require('tailwindcss-animate')],
});
Clerk webhook
import { Webhook } from 'svix'
import { headers } from 'next/headers'
import { WebhookEvent } from '@clerk/nextjs/server'
import { createUser, deleteUser, updateUser } from '@/lib/actions/user.actions'
import { clerkClient } from '@clerk/nextjs'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
// You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET
if (!WEBHOOK_SECRET) {
throw new Error('Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local')
}
// Get the headers
const headerPayload = headers();
const svix_id = headerPayload.get("svix-id");
const svix_timestamp = headerPayload.get("svix-timestamp");
const svix_signature = headerPayload.get("svix-signature");
// If there are no headers, error out
if (!svix_id || !svix_timestamp || !svix_signature) {
return new Response('Error occured -- no svix headers', {
status: 400
})
}
// Get the body
const payload = await req.json()
const body = JSON.stringify(payload);
// Create a new Svix instance with your secret.
const wh = new Webhook(WEBHOOK_SECRET);
let evt: WebhookEvent
// Verify the payload with the headers
try {
evt = wh.verify(body, {
"svix-id": svix_id,
"svix-timestamp": svix_timestamp,
"svix-signature": svix_signature,
}) as WebhookEvent
} catch (err) {
console.error('Error verifying webhook:', err);
return new Response('Error occured', {
status: 400
})
}
// Get the ID and type
const { id } = evt.data;
const eventType = evt.type;
if(eventType === 'user.created') {
const { id, email_addresses, image_url, first_name, last_name, username } = evt.data;
const user = {
clerkId: id,
email: email_addresses[0].email_address,
username: username!,
firstName: first_name,
lastName: last_name,
photo: image_url,
}
const newUser = await createUser(user);
if(newUser) {
await clerkClient.users.updateUserMetadata(id, {
publicMetadata: {
userId: newUser._id
}
})
}
return NextResponse.json({ message: 'OK', user: newUser })
}
if (eventType === 'user.updated') {
const {id, image_url, first_name, last_name, username } = evt.data
const user = {
firstName: first_name,
lastName: last_name,
username: username!,
photo: image_url,
}
const updatedUser = await updateUser(id, user)
return NextResponse.json({ message: 'OK', user: updatedUser })
}
if (eventType === 'user.deleted') {
const { id } = evt.data
const deletedUser = await deleteUser(id!)
return NextResponse.json({ message: 'OK', user: deletedUser })
}
return new Response('', { status: 200 })
}
user.actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { connectToDatabase } from '@/lib/database'
import User from '@/lib/database/models/user.model'
import Order from '@/lib/database/models/order.model'
import Event from '@/lib/database/models/event.model'
import { handleError } from '@/lib/utils'
import { CreateUserParams, UpdateUserParams } from '@/types'
export async function createUser(user: CreateUserParams) {
try {
await connectToDatabase()
const newUser = await User.create(user)
return JSON.parse(JSON.stringify(newUser))
} catch (error) {
handleError(error)
}
}
export async function getUserById(userId: string) {
try {
await connectToDatabase()
const user = await User.findById(userId)
if (!user) throw new Error('User not found')
return JSON.parse(JSON.stringify(user))
} catch (error) {
handleError(error)
}
}
export async function updateUser(clerkId: string, user: UpdateUserParams) {
try {
await connectToDatabase()
const updatedUser = await User.findOneAndUpdate({ clerkId }, user, { new: true })
if (!updatedUser) throw new Error('User update failed')
return JSON.parse(JSON.stringify(updatedUser))
} catch (error) {
handleError(error)
}
}
export async function deleteUser(clerkId: string) {
try {
await connectToDatabase()
// Find user to delete
const userToDelete = await User.findOne({ clerkId })
if (!userToDelete) {
throw new Error('User not found')
}
// Unlink relationships
await Promise.all([
// Update the 'events' collection to remove references to the user
Event.updateMany(
{ _id: { $in: userToDelete.events } },
{ $pull: { organizer: userToDelete._id } }
),
// Update the 'orders' collection to remove references to the user
Order.updateMany({ _id: { $in: userToDelete.orders } }, { $unset: { buyer: 1 } }),
])
// Delete user
const deletedUser = await User.findByIdAndDelete(userToDelete._id)
revalidatePath('/')
return deletedUser ? JSON.parse(JSON.stringify(deletedUser)) : null
} catch (error) {
handleError(error)
}
}
order.model.ts
import { Schema, model, models, Document } from 'mongoose'
export interface IOrder extends Document {
createdAt: Date
stripeId: string
totalAmount: string
event: {
_id: string
title: string
}
buyer: {
_id: string
firstName: string
lastName: string
}
}
export type IOrderItem = {
_id: string
totalAmount: string
createdAt: Date
eventTitle: string
eventId: string
buyer: string
}
const OrderSchema = new Schema({
createdAt: {
type: Date,
default: Date.now,
},
stripeId: {
type: String,
required: true,
unique: true,
},
totalAmount: {
type: String,
},
event: {
type: Schema.Types.ObjectId,
ref: 'Event',
},
buyer: {
type: Schema.Types.ObjectId,
ref: 'User',
},
})
const Order = models.Order || model('Order', OrderSchema)
export default Order
FileUploader.tsx
'use client'
import { useCallback, Dispatch, SetStateAction } from 'react'
import type { FileWithPath } from '@uploadthing/react'
import { useDropzone } from '@uploadthing/react/hooks'
import { generateClientDropzoneAccept } from 'uploadthing/client'
import { Button } from '@/components/ui/button'
import { convertFileToUrl } from '@/lib/utils'
type FileUploaderProps = {
onFieldChange: (url: string) => void
imageUrl: string
setFiles: Dispatch<SetStateAction<File[]>>
}
export function FileUploader({ imageUrl, onFieldChange, setFiles }: FileUploaderProps) {
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
setFiles(acceptedFiles)
onFieldChange(convertFileToUrl(acceptedFiles[0]))
}, [])
const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: 'image/*' ? generateClientDropzoneAccept(['image/*']) : undefined,
})
return (
<div
{...getRootProps()}
className="flex-center bg-dark-3 flex h-72 cursor-pointer flex-col overflow-hidden rounded-xl bg-grey-50">
<input {...getInputProps()} className="cursor-pointer" />
{imageUrl ? (
<div className="flex h-full w-full flex-1 justify-center ">
<img
src={imageUrl}
alt="image"
width={250}
height={250}
className="w-full object-cover object-center"
/>
</div>
) : (
<div className="flex-center flex-col py-5 text-grey-500">
<img src="/assets/icons/upload.svg" width={77} height={77} alt="file upload" />
<h3 className="mb-2 mt-2">Drag photo here</h3>
<p className="p-medium-12 mb-4">SVG, PNG, JPG</p>
<Button type="button" className="rounded-full">
Select from computer
</Button>
</div>
)}
</div>
)
}
DeleteConfirmation.tsx
'use client'
import { useTransition } from 'react'
import { usePathname } from 'next/navigation'
import Image from 'next/image'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
import { deleteEvent } from '@/lib/actions/event.actions'
export const DeleteConfirmation = ({ eventId }: { eventId: string }) => {
const pathname = usePathname()
let [isPending, startTransition] = useTransition()
return (
<AlertDialog>
<AlertDialogTrigger>
<Image src="/assets/icons/delete.svg" alt="edit" width={20} height={20} />
</AlertDialogTrigger>
<AlertDialogContent className="bg-white">
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete?</AlertDialogTitle>
<AlertDialogDescription className="p-regular-16 text-grey-600">
This will permanently delete this event
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() =>
startTransition(async () => {
await deleteEvent({ eventId, path: pathname })
})
}>
{isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
event.action.ts
'use server'
import { revalidatePath } from 'next/cache'
import { connectToDatabase } from '@/lib/database'
import Event from '@/lib/database/models/event.model'
import User from '@/lib/database/models/user.model'
import Category from '@/lib/database/models/category.model'
import { handleError } from '@/lib/utils'
import {
CreateEventParams,
UpdateEventParams,
DeleteEventParams,
GetAllEventsParams,
GetEventsByUserParams,
GetRelatedEventsByCategoryParams,
} from '@/types'
const getCategoryByName = async (name: string) => {
return Category.findOne({ name: { $regex: name, $options: 'i' } })
}
const populateEvent = (query: any) => {
return query
.populate({ path: 'organizer', model: User, select: '_id firstName lastName' })
.populate({ path: 'category', model: Category, select: '_id name' })
}
// CREATE
export async function createEvent({ userId, event, path }: CreateEventParams) {
try {
await connectToDatabase()
const organizer = await User.findById(userId)
if (!organizer) throw new Error('Organizer not found')
const newEvent = await Event.create({ ...event, category: event.categoryId, organizer: userId })
revalidatePath(path)
return JSON.parse(JSON.stringify(newEvent))
} catch (error) {
handleError(error)
}
}
// GET ONE EVENT BY ID
export async function getEventById(eventId: string) {
try {
await connectToDatabase()
const event = await populateEvent(Event.findById(eventId))
if (!event) throw new Error('Event not found')
return JSON.parse(JSON.stringify(event))
} catch (error) {
handleError(error)
}
}
// UPDATE
export async function updateEvent({ userId, event, path }: UpdateEventParams) {
try {
await connectToDatabase()
const eventToUpdate = await Event.findById(event._id)
if (!eventToUpdate || eventToUpdate.organizer.toHexString() !== userId) {
throw new Error('Unauthorized or event not found')
}
const updatedEvent = await Event.findByIdAndUpdate(
event._id,
{ ...event, category: event.categoryId },
{ new: true }
)
revalidatePath(path)
return JSON.parse(JSON.stringify(updatedEvent))
} catch (error) {
handleError(error)
}
}
// DELETE
export async function deleteEvent({ eventId, path }: DeleteEventParams) {
try {
await connectToDatabase()
const deletedEvent = await Event.findByIdAndDelete(eventId)
if (deletedEvent) revalidatePath(path)
} catch (error) {
handleError(error)
}
}
// GET ALL EVENTS
export async function getAllEvents({ query, limit = 6, page, category }: GetAllEventsParams) {
try {
await connectToDatabase()
const titleCondition = query ? { title: { $regex: query, $options: 'i' } } : {}
const categoryCondition = category ? await getCategoryByName(category) : null
const conditions = {
$and: [titleCondition, categoryCondition ? { category: categoryCondition._id } : {}],
}
const skipAmount = (Number(page) - 1) * limit
const eventsQuery = Event.find(conditions)
.sort({ createdAt: 'desc' })
.skip(skipAmount)
.limit(limit)
const events = await populateEvent(eventsQuery)
const eventsCount = await Event.countDocuments(conditions)
return {
data: JSON.parse(JSON.stringify(events)),
totalPages: Math.ceil(eventsCount / limit),
}
} catch (error) {
handleError(error)
}
}
// GET EVENTS BY ORGANIZER
export async function getEventsByUser({ userId, limit = 6, page }: GetEventsByUserParams) {
try {
await connectToDatabase()
const conditions = { organizer: userId }
const skipAmount = (page - 1) * limit
const eventsQuery = Event.find(conditions)
.sort({ createdAt: 'desc' })
.skip(skipAmount)
.limit(limit)
const events = await populateEvent(eventsQuery)
const eventsCount = await Event.countDocuments(conditions)
return { data: JSON.parse(JSON.stringify(events)), totalPages: Math.ceil(eventsCount / limit) }
} catch (error) {
handleError(error)
}
}
// GET RELATED EVENTS: EVENTS WITH SAME CATEGORY
export async function getRelatedEventsByCategory({
categoryId,
eventId,
limit = 3,
page = 1,
}: GetRelatedEventsByCategoryParams) {
try {
await connectToDatabase()
const skipAmount = (Number(page) - 1) * limit
const conditions = { $and: [{ category: categoryId }, { _id: { $ne: eventId } }] }
const eventsQuery = Event.find(conditions)
.sort({ createdAt: 'desc' })
.skip(skipAmount)
.limit(limit)
const events = await populateEvent(eventsQuery)
const eventsCount = await Event.countDocuments(conditions)
return { data: JSON.parse(JSON.stringify(events)), totalPages: Math.ceil(eventsCount / limit) }
} catch (error) {
handleError(error)
}
}
order.action.ts
"use server"
import Stripe from 'stripe';
import { CheckoutOrderParams, CreateOrderParams, GetOrdersByEventParams, GetOrdersByUserParams } from "@/types"
import { redirect } from 'next/navigation';
import { handleError } from '../utils';
import { connectToDatabase } from '../database';
import Order from '../database/models/order.model';
import Event from '../database/models/event.model';
import {ObjectId} from 'mongodb';
import User from '../database/models/user.model';
export const checkoutOrder = async (order: CheckoutOrderParams) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const price = order.isFree ? 0 : Number(order.price) * 100;
try {
const session = await stripe.checkout.sessions.create({
line_items: [
{
price_data: {
currency: 'usd',
unit_amount: price,
product_data: {
name: order.eventTitle
}
},
quantity: 1
},
],
metadata: {
eventId: order.eventId,
buyerId: order.buyerId,
},
mode: 'payment',
success_url: `${process.env.NEXT_PUBLIC_SERVER_URL}/profile`,
cancel_url: `${process.env.NEXT_PUBLIC_SERVER_URL}/`,
});
redirect(session.url!)
} catch (error) {
throw error;
}
}
export const createOrder = async (order: CreateOrderParams) => {
try {
await connectToDatabase();
const newOrder = await Order.create({
...order,
event: order.eventId,
buyer: order.buyerId,
});
return JSON.parse(JSON.stringify(newOrder));
} catch (error) {
handleError(error);
}
}
// GET ORDERS BY EVENT
export async function getOrdersByEvent({ searchString, eventId }: GetOrdersByEventParams) {
try {
await connectToDatabase()
if (!eventId) throw new Error('Event ID is required')
const eventObjectId = new ObjectId(eventId)
const orders = await Order.aggregate([
{
$lookup: {
from: 'users',
localField: 'buyer',
foreignField: '_id',
as: 'buyer',
},
},
{
$unwind: '$buyer',
},
{
$lookup: {
from: 'events',
localField: 'event',
foreignField: '_id',
as: 'event',
},
},
{
$unwind: '$event',
},
{
$project: {
_id: 1,
totalAmount: 1,
createdAt: 1,
eventTitle: '$event.title',
eventId: '$event._id',
buyer: {
$concat: ['$buyer.firstName', ' ', '$buyer.lastName'],
},
},
},
{
$match: {

