Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JUP 55 - Create Event Page #229

Merged
merged 5 commits into from
May 8, 2024
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
352 changes: 161 additions & 191 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/app/manage/[clubId]/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ const Page = ({ params }: { params: { clubId: string } }) => {
<button className="rounded-md bg-blue-primary p-1 font-semibold text-white">
View members
</button>
<Link
href={`/manage/${params.clubId}/create`}
className="rounded-md bg-blue-primary p-1 font-semibold text-white"
>
Create Event
</Link>
</div>
</>
);
Expand Down
130 changes: 130 additions & 0 deletions src/app/manage/[clubId]/create/CreateEventForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client'

import { useEffect, useState } from "react";
import { type SelectClub } from "@src/server/db/models";
import { createEventSchema } from "@src/utils/formSchemas";
import { useForm } from "react-hook-form";
import { api } from "@src/trpc/react";
import { type z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { UploadIcon } from "@src/icons/Icons";
import EventCardPreview from "./EventCardPreview";
import TimeSelect from "./TimeSelect";
import { type RouterOutputs } from "@src/trpc/shared";

const CreateEventForm = ({ clubId, officerClubs }: { clubId: string, officerClubs: SelectClub[]}) => {
const {
register,
handleSubmit,
watch,
setValue,
getValues,
} = useForm<z.infer<typeof createEventSchema>>({
resolver: zodResolver(createEventSchema),
defaultValues: {
clubId: clubId,
},
mode: "onSubmit",
});
const router = useRouter();
const [watchDescription, watchStartTime] = watch(['description', 'startTime']);
const [eventPreview, setEventPreview] = useState<RouterOutputs['event']['findByFilters']['events'][number]>({
name: "",
clubId,
description: "",
location: "",
liked: false,
id: "",
startTime: new Date(Date.now()),
endTime: new Date(Date.now()),
club: officerClubs.filter((v) => v.id == clubId)[0]!,
});
useEffect(() => {
const subscription = watch((data, info) => {
const { name, clubId, description, location, startTime, endTime } = data;
const club = officerClubs.find((val) => val.id == data.clubId);
if (club) {
setEventPreview({
name: name || "",
clubId: clubId || "",
description: description || "",
location: location || "",
liked: false,
id: "",
startTime: startTime?.toString() === "" || startTime == undefined ? new Date(Date.now()) : new Date(startTime),
endTime: endTime?.toString() === "" || endTime?.toString() == "Invalid Date" || !endTime ? new Date(Date.now()) : new Date(endTime),
club,
});
}
if (info.name == "clubId") {
router.replace(`/manage/${data.clubId}/create`);
}
});
return () => subscription.unsubscribe();
}, [router, watch, officerClubs]);

const createMutation = api.event.create.useMutation({
onSuccess: () => { location.reload(); }
Copy link
Contributor

Choose a reason for hiding this comment

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

On success either show a success message or redirect to the event page using Router.push

})

const onSubmit = handleSubmit((data: z.infer<typeof createEventSchema>) => {
if (!createMutation.isPending) {
createMutation.mutate(data);
}
});

return (<form onSubmit={(e) => void onSubmit(e)} className="w-full flex flex-row flex-wrap justify-start gap-10 overflow-x-clip text-[#4D5E80] pb-4">
<div className="form-fields flex flex-col flex-1 gap-10 min-w-[320px] max-w-[830px]">
<div className="create-dropdown text-2xl font-bold py-2 flex flex-row justify-start whitespace-nowrap gap-1 max-w-full">
<span>Create Club Event <span className="text-[#3361FF]">for</span></span>
<div className="flex-1">
<select {...register("clubId")} className="bg-inherit text-[#3361FF] outline-none text-ellipsis overflow-hidden whitespace-nowrap w-full" defaultValue={clubId}>
{officerClubs.map((club) => {
return (<option key={club.id} value={club.id}>{club.name}</option>)
})}
</select>
</div>
</div>
<div className="event-pic w-full">
<h1 className="font-bold mb-4">Event Picture</h1>
<p className="upload-label text-xs font-bold mb-11">Drag or choose file to upload</p>
<div className="upload-box bg-[#E9EAEF] w-full h-48 rounded-md flex items-center flex-col justify-center gap-6">
<UploadIcon />
<p className="font-bold text-xs">JPEG, PNG, or SVG</p>
</div>
</div>
<div className="event-details w-full flex flex-col gap-4">
<h1 className="font-bold">Event Details</h1>
<div className="event-name">
<label className="text-xs font-bold mb-2 block" htmlFor="name">Event Name</label>
<input type="text"
className="rounded-md shadow-sm placeholder:text-[#7D8FB3] outline-none text-xs w-full p-2"
placeholder="Event name" {...register("name")} />
</div>
<div className="event-location">
<label className="text-xs font-bold mb-2 block" htmlFor="location">Location</label>
<input type="text"
className="rounded-md shadow-sm placeholder:text-[#7D8FB3] outline-none text-xs w-full p-2"
placeholder="123 Fun Street" {...register("location")} />
</div>
<div className="event-description">
<div className="desc-header flex w-full justify-between">
<label className="text-xs font-bold mb-2 block" htmlFor="description">Description</label>
<p className="text-xs">{watchDescription && watchDescription.length} of 1000 Characters used</p>
</div>
<textarea {...register("description")}
className="rounded-md shadow-sm placeholder:text-[#7D8FB3] outline-none text-xs w-full p-2"
placeholder="Event description" />
</div>
</div>
<TimeSelect register={register} setValue={setValue} getValues={getValues} watchStartTime={watchStartTime} />
<input className="bg-[#3361FF] text-white py-6 hover:cursor-pointer font-black text-xs rounded-md" type="submit" value="Create Event" />
</div>
<div className="form-preview w-64 flex flex-col gap-14">
<h1 className="text-lg font-bold">Preview</h1>
{eventPreview && <EventCardPreview event={eventPreview} />}
</div>
</form>)
}
export default CreateEventForm;
64 changes: 64 additions & 0 deletions src/app/manage/[clubId]/create/EventCardPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import EventTimeAlert from "@src/components/events/EventTimeAlert";
import { MoreIcon, PlusIcon } from "@src/icons/Icons";
import type { RouterOutputs } from "@src/trpc/shared"
import { format, isSameDay } from 'date-fns';
import Image from 'next/image';

interface Props {
event: RouterOutputs['event']['findByFilters']['events'][number],
}

const EventCardPreview = ({ event }: Props) => {
return (
<div className="container flex h-96 w-64 flex-col overflow-hidden rounded-lg bg-white shadow-sm transition-shadow hover:shadow-lg">
<div className="relative">
<div className="h-40 w-96">
<Image
src={'/event_default.jpg'}
alt="event image"
fill
objectFit="cover"
/>
<div className="absolute inset-0 p-2">
<EventTimeAlert event={event} />
</div>
</div>
</div>
<div className="flex h-full flex-col p-5">
<div className="space-y-2.5">
<h3 className="font-bold">{event.name}</h3>
<h4 className="text-xs font-bold">
<p
className="hover:text-blue-primary"
>
{event.club.name}
</p>
<div>
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a bit more vertical space between the time and club name to make the card less empty

<span className="text-blue-primary">
{format(event.startTime, 'E, MMM d, p')}
{isSameDay(event.startTime, event.endTime) ? (
<> - {format(event.endTime, 'p')}</>
) : (
<>
{' '}
- <br />
{format(event.endTime, 'E, MMM d, p')}
</>
)}
</span>
</div>
</h4>
</div>
<div className="mt-auto flex flex-row space-x-4">
<p className=" h-10 w-10 rounded-full bg-blue-primary p-1.5 shadow-lg transition-colors hover:bg-blue-700 active:bg-blue-800 hover:cursor-pointer">
<MoreIcon fill="fill-white" />
</p>
<div className="h-10 w-10 rounded-full bg-white p-1.5 shadow-lg hover:cursor-pointer">
<PlusIcon fill="fill-slate-800" />
</div>
</div>
</div>
</div>
);
}
export default EventCardPreview
85 changes: 85 additions & 0 deletions src/app/manage/[clubId]/create/TimeSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client'
import { useEffect, useState } from "react";
import type {
UseFormRegister,
UseFormSetValue,
UseFormGetValues,
} from "react-hook-form";
import type { createEventSchema } from "@src/utils/formSchemas";
import type { z } from "zod";

interface Props {
register: UseFormRegister<z.infer<typeof createEventSchema>>,
setValue: UseFormSetValue<z.infer<typeof createEventSchema>>,
getValues: UseFormGetValues<z.infer<typeof createEventSchema>>,
watchStartTime: Date,
}

const TimeSelect = ({ register, setValue, getValues, watchStartTime }: Props) => {
const [multiDay, setMultiDay] = useState(false);
const [numHours, setNumHours] = useState(2);

useEffect(() => {
// If not multi-day, set end time to start time + numHours
if (!multiDay && watchStartTime !== undefined) {
const date = new Date(watchStartTime);
date.setHours(date.getHours() + numHours);
setValue("endTime", date);
}

// If start time is after end time, set end time to start time
if (new Date(watchStartTime) > new Date(getValues('endTime'))) {
setValue('endTime', watchStartTime);
}
}, [setValue, getValues, watchStartTime, multiDay, numHours])

return (<>
<div className="multi-day flex justify-between w-full">
<div className="left">
<h1 className="font-bold mb-2">Multi-Day Event</h1>
<p className="font-bold text-xs">Does the event last longer than one day?</p>
</div>
<div className="right">
<div className="h-fit flex items-center gap-6">
<div
className={`toggle-multi-day relative hover:cursor-pointer ${multiDay ? "bg-[#3361FF] after:bg-white after:translate-x-[20px]" : "bg-white after:bg-[#E1E5ED]"} w-[50px] h-[30px] rounded-2xl transition-colors duration-150 after:top-[2px] after:absolute after:left-[2px] after:transition-tranform after:duration-150 after:rounded-2xl after:content-[''] after:h-[26px] after:w-[26px]`}
onClick={() => {
// If switching to multiDay, clear endTime
if (!multiDay) {
setValue('endTime', new Date(NaN));
}
setMultiDay(!multiDay);
}} ></div>
<p className="font-bold inline-block w-[30px] text-right text-xs">{multiDay ? "Yes" : "No"}</p>
</div>
</div>
</div>
<div className="event-duration flex gap-32">
<div className="flex-1 justify-end flex flex-col">
<h1 className="font-bold mb-2 block">Duration</h1>
<label htmlFor="startTime" className="text-xs font-bold mb-2">Start Time</label>
<input {...register("startTime")} type="datetime-local" className="outline-none w-full block p-2 text-xs rounded-md text-[#7D8FB3]" />
</div>
{ multiDay ?
<div className="flex-1 justify-end flex flex-col">
<label htmlFor="endTime" className="text-xs font-bold mb-2">End Time</label>
<input {...register("endTime")} type="datetime-local"
onInput={(e) => { if (new Date(e.currentTarget.value) < new Date(watchStartTime)) setValue('endTime', watchStartTime); }}
min={watchStartTime?.toString()}
className="outline-none w-full block p-2 text-xs rounded-md text-[#7D8FB3]"
/>
</div>
:
<div className="flex-1 justify-end flex flex-col">
<label htmlFor="endTime" className="text-xs font-bold mb-2">Number of Hours</label>
<select className="outline-none p-[9px] text-xs rounded-md" id="endTime" value={numHours} onInput={(e) => {setNumHours(Number(e.currentTarget.value))}}>
{Array(24).fill(0).map((_, i) => (
<option className="" value={i+1} key={i}>{i+1} Hour{i+1 > 1 && "s"}</option>
))}
</select>
</div>
}
</div>
</>);
}
export default TimeSelect;
30 changes: 30 additions & 0 deletions src/app/manage/[clubId]/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Header from "@src/components/BaseHeader";
import { getServerAuthSession } from "@src/server/auth";
import { api } from "@src/trpc/server";
import { signInRoute } from "@src/utils/redirect";
import { redirect, notFound } from "next/navigation";
import CreateEventForm from "./CreateEventForm";

const Page = async ({ params }: { params: { clubId: string } }) => {
const session = await getServerAuthSession();
if (!session) {
redirect(signInRoute(`manage/${params.clubId}/create`));
}

const officerClubs = await api.club.getOfficerClubs();
const currentClub = officerClubs.filter(val => {
return val.id == params.clubId
})[0];
if (!currentClub) {
notFound();
}

return (<main className="md:pl-72 h-screen">
<Header />
<div className="flex flex-row justify-between gap-20 px-5">
<CreateEventForm clubId={currentClub.id} officerClubs={officerClubs} />
</div>

</main>)
}
export default Page;
1 change: 0 additions & 1 deletion src/components/events/EventTimeAlert.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
'use server';
import { type SelectEvent } from '@src/server/db/models';
import {
differenceInDays,
Expand Down
16 changes: 16 additions & 0 deletions src/icons/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,19 @@ export const CloseIcon = () => (
/>
</svg>
);

export const UploadIcon = () => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="30"
height="30"
fill="none"
viewBox="0 0 30 30"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M22.35 13.04C21.67 9.59 18.64 7 15 7C12.11 7 9.6 8.64 8.35 11.04C5.34 11.36 3 13.91 3 17C3 20.31 5.69 23 9 23H22C24.76 23 27 20.76 27 18C27 15.36 24.95 13.22 22.35 13.04ZM17 16V20H13V16H10L14.65 11.35C14.85 11.15 15.16 11.15 15.36 11.35L20 16H17Z"
fill="#C3CAD9"/>
</svg>
)
24 changes: 23 additions & 1 deletion src/server/api/routers/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { z } from 'zod';
import { selectEvent } from '@src/server/db/models';
import { type DateRange } from 'react-day-picker';
import { add } from 'date-fns';
import { userMetadataToEvents } from '@src/server/db/schema/users';
import { userMetadataToClubs, userMetadataToEvents } from '@src/server/db/schema/users';
import { createEventSchema } from '@src/utils/formSchemas';
import { TRPCError } from '@trpc/server';
import { events } from '@src/server/db/schema/events';

function isDateRange(value: unknown): value is DateRange {
return Boolean(value && typeof value === 'object' && 'from' in value);
Expand Down Expand Up @@ -232,6 +235,25 @@ export const eventRouter = createTRPCRouter({
),
);
}),
create: protectedProcedure
.input(createEventSchema)
.mutation(async ({ input, ctx }) => {
const { clubId } = input
const userId = ctx.session.user.id;

const isOfficer = await ctx.db.query.userMetadataToClubs.findFirst({
where: and(
eq(userMetadataToClubs.userId, userId),
eq(userMetadataToClubs.clubId, clubId),
inArray(userMetadataToClubs.memberType, ["Officer", "President"])
)
});
if (!isOfficer) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}

await ctx.db.insert(events).values({ ...input });
}),
byName: publicProcedure.input(byNameSchema).query(async ({ input, ctx }) => {
const { name, sortByDate } = input;
try {
Expand Down
Loading