-
Notifications
You must be signed in to change notification settings - Fork 7
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
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e61b3a3
Create event API route and create event form schema
BK2004 5205349
create event form
BK2004 cca82c0
merge main
BK2004 2a246d3
Create Event event preview
BK2004 0bd0cb9
fix dropdown formatting. make preview default to empty event. align p…
BK2004 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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