-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #229 from UTDNebula/JUP-55
JUP 55 - Create Event Page
- Loading branch information
Showing
13 changed files
with
1,303 additions
and
193 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); } | ||
}) | ||
|
||
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
<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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.