Skip to content

Commit

Permalink
Merge pull request #104 from TheLab-ms/feature/detail-page
Browse files Browse the repository at this point in the history
  • Loading branch information
ATechAdventurer authored Jul 26, 2023
2 parents f4d372b + 12d9bd2 commit 0ec8eb7
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 32 deletions.
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"next-auth": "^4.22.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-toastify": "^9.1.3",
"zod-formik-adapter": "^1.2.0"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- A unique constraint covering the columns `[eventId,accountId]` on the table `RSVP` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateIndex
CREATE UNIQUE INDEX "RSVP_eventId_accountId_key" ON "RSVP"("eventId", "accountId");
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,6 @@ model RSVP {
account Account @relation(fields: [accountId], references: [id])
accountId String
createdAt DateTime @default(now())
@@unique([eventId, accountId])
}
8 changes: 4 additions & 4 deletions src/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function Header(props: HeaderProps) {
return (
<header>
<nav
className="mx-auto flex max-w-7xl items-center justify-between p-6 lg:px-8"
className="mx-auto flex items-center justify-between p-6 lg:px-8"
aria-label="Global"
>
<div className="flex lg:flex-1">
Expand All @@ -28,7 +28,7 @@ export default function Header(props: HeaderProps) {
/>
</Link>
</div>
<div className="max-w-7xl lg:flex lg:flex-1 lg:justify-end">
<div className="lg:flex lg:flex lg:justify-center">
<div className="flex items-center">
{session ? (
<span className="text-sm font-semibold leading-6 text-gray-900 mr-3">
Expand Down Expand Up @@ -56,8 +56,8 @@ export default function Header(props: HeaderProps) {
</div>
</nav>

<div className="mx-auto flex w-full max-w-7xl items-center justify-between rounded-md primary p-2 lg:px-8">
<h1 className="text-2xl lg:mb-0 font-bold text-white pt-0 pb-0">
<div className="primary w-full rounded-md p-5">
<h1 className="text-4xl lg:mb-0 font-bold text-white pt-0 pb-0">
{props.title || ''}
</h1>
<div className="text-white">
Expand Down
9 changes: 6 additions & 3 deletions src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { ReactNode } from "react"
import Header from "./header"
import { ToastContainer } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';

interface LayoutProps {
children: ReactNode;
Expand All @@ -8,11 +10,12 @@ interface LayoutProps {

export default function Layout({ children, headerText }: LayoutProps) {
return (
<>
<div className="mx-10">
<Header title={headerText} />
<div className="flex flex-col mx-auto max-w-7xl" id="contents">
<div className="flex flex-col mx-auto" id="contents">
{children}
</div>
</>
<ToastContainer />
</div>
)
}
69 changes: 69 additions & 0 deletions src/pages/api/events/rsvp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth';
import { authOptions } from '../auth/[...nextauth]';
import { prisma } from '@/helpers/db';
import { CreateRSVPSchema } from '@/schemas/api/createRSVP';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (!req.method || (req.method !== 'POST' && req.method !== 'DELETE')) {
res.status(405).json({ message: 'Method not allowed' });
return;
}

const session = await getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: 'Unauthorized' });
return;
}
let rsvpData = null;
try {
rsvpData = CreateRSVPSchema.parse(JSON.parse(req.body));
} catch (error) {
console.log(error);
res.status(400).json({
message: 'The data sent is not JSON',
});
return;
}
const { eventId, operation } = rsvpData;

const event = await prisma.event.findUnique({
where: {
id: eventId,
},
});

if (!event) {
res.status(404).json({ message: 'Event not found' });
return;
}
console.log(
`Attempting to ${operation} RSVP for event ${eventId} for user ${session.user.id}`
);
if (operation === 'create') {
const rsvp = await prisma.rSVP.create({
data: {
eventId: rsvpData.eventId,
accountId: session.user.id,
},
});
res.status(200).send(true);
return;
}

if (operation === 'remove') {
const rsvp = await prisma.rSVP.delete({
where: {
eventId_accountId: {
eventId: eventId,
accountId: session.user.id,
},
},
});
res.status(200).send(true);
return;
}
}
157 changes: 132 additions & 25 deletions src/pages/events/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,120 @@ import { GetServerSideProps } from 'next';
import Layout from '../../components/layout';
import { prisma } from '@/helpers/db';
import { CompleteEvent } from '@/schemas';
import { useSession } from 'next-auth/react';
import { getServerSession } from 'next-auth';
import { authOptions } from '../api/auth/[...nextauth]';
import { useState } from 'react';
import { toast } from 'react-toastify';


interface EventDetailsProps {
event: CompleteEvent;
isRSVPed: boolean;
isOwner: boolean;
}

const EventDetails = ({
event,
isRSVPed,
isOwner
}: EventDetailsProps) => {
const { title, category: { title: categoryName }, location: { title: locationName }, startTime, endTime, reqMaterials, description } = event;
const session = useSession();
const [userRSVPed, setUserRSVPed] = useState<boolean>(isRSVPed);
const { title, category: { title: categoryName }, location: { title: locationName }, startTime, endTime, reqMaterials, description, exclusivity, specialNotes } = event;

const dateOptions: Intl.DateTimeFormatOptions = { weekday: 'long', month: 'long', day: 'numeric' };
const timeOptions: Intl.DateTimeFormatOptions = { hour: 'numeric', minute: 'numeric', hour12: true };
const formattedStartDate = new Date(startTime).toLocaleDateString('en-US', dateOptions).replace(/\d{1,2}(st|nd|rd|th)/, '$& ');
const formattedStartTime = new Date(startTime).toLocaleTimeString('en-US', timeOptions);
const formattedEndTime = new Date(endTime).toLocaleTimeString('en-US', timeOptions);

const handleRSVP = () => {
fetch('/api/events/rsvp', {
method: 'POST',
body: JSON.stringify({ eventId: event.id, operation: userRSVPed ? 'remove' : 'create' }),
}).then((res) => {
if (res.ok) {
setUserRSVPed(!userRSVPed);
toast.success("Successfully RSVP'd", {
position: "top-center",
});
} else {
toast.error("RSVP failed", {
position: "top-center",
});
}
}).catch((err) => {
toast.error("RSVP failed", {
position: "top-center",
});
})
}
return (
<Layout headerText={title} >
<Layout headerText="Event Details" >
<div className="header">
<div className="container">
{/* <h1 className="text-2xl font-bold mt-3 ml-2">{title}</h1> */}
<h2 className="text-sm mb-3 ml-2" style={{ color: "#5BA1C9" }}>{categoryName}</h2>
<div className="grid grid-cols-2">
<div>
<h3 className="text-base ml-2"><u>Date</u>: {formattedStartDate}</h3>
<h3 className="text-base ml-2"><u>Time</u>: {formattedStartTime} - {formattedEndTime}</h3>
<h3 className="text-base ml-2 mb-3"><u>Location</u>: {locationName}</h3>
<h3 className="text-base ml-2"><u>Exclusivity</u>: Members</h3>
<h3 className="text-base ml-2 mb-3"><u>Prerequisites</u>: N/A</h3>
<h3 className="text-base ml-2"><u>Required</u>:</h3>
<h3 className="text-base ml-2 mb-3">{reqMaterials}</h3>
</div>
<div>
<h3 className="text-base ml-2"><u>Description</u>:</h3>
<h3 className="text-base ml-2">{description}</h3>
</div>
</div>
<div className="container m-auto p-10">
<table className="table-auto m-auto">
<tbody>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Title</td>
<td className="px-4 py-2">{title}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Location</td>
<td className="px-4 py-2">{locationName}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Category</td>
<td className="px-4 py-2">{categoryName}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Start Time</td>
<td className="px-4 py-2">{formattedStartDate} @ {formattedStartTime} - {formattedEndTime}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Exclusivity</td>
<td className="px-4 py-2">{exclusivity}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Required Materials</td>
<td className="px-4 py-2">{reqMaterials}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Description</td>
<td className="px-4 py-2">{description}</td>
</tr>
<tr className="border-t border-gray-200">
<td className="px-4 py-2 font-semibold">Special Notes</td>
<td className="px-4 py-2">{specialNotes}</td>
</tr>
{session.status === "authenticated" && <tr>
<td className="px-4 py-2 font-semibold">Actions</td>
<td className="px-4 py-2">
{!isOwner && <button className="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mx-2" onClick={handleRSVP}>{userRSVPed ? "Cancel RSVP" : "RSVP"}</button>}
{isOwner &&
<>
<button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mx-2">
Edit
</button>
<button className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded mx-2">
Delete
</button>
</>
}
</td>
</tr>}
</tbody>
</table>
</div>
</div>
</Layout>
);
};

export const getServerSideProps: GetServerSideProps = async (context) => {

let isUserRSVPed = false;
let isOwner = false;
if (!context.params || !context.params.id) {
return {
notFound: true,
Expand All @@ -58,8 +129,9 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
notFound: true,
};
}
// Make a prisma call that gets an event by id and includes the category title
const event = await prisma.event.findUnique({

// Create the Prisma call promise without awaiting it
const eventPromise = prisma.event.findUnique({
where: { id },
include: {
category: {
Expand All @@ -75,10 +147,45 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
}
},
});

// Create the session promise without awaiting it
const sessionPromise = getServerSession(
context.req,
context.res,
authOptions
);

// Run the promises in parallel and await the results
const [event, session] = await Promise.all([eventPromise, sessionPromise]);

const fixedEvent = JSON.parse(JSON.stringify(event));

if (!session?.user) {
return {
props: {
event: fixedEvent,
isRSVPed: false
}
}
}
if (session?.user?.id === event?.creatorId) {
isOwner = true;
isUserRSVPed = true;
} else {
const rsvp = await prisma.rSVP.findFirst({
where: {
eventId: id,
accountId: session.user.id,
}
})
if (rsvp) {
isUserRSVPed = true;
}
}
return {
props: { event: fixedEvent },
props: { event: fixedEvent, isRSVPed: isUserRSVPed, isOwner },
};
};
}


export default EventDetails;
Loading

1 comment on commit 0ec8eb7

@vercel
Copy link

@vercel vercel bot commented on 0ec8eb7 Jul 26, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.