Skip to content

Commit

Permalink
Add event cards for each day to Schedule (#163)
Browse files Browse the repository at this point in the history
* setup basic schedule page that fetches from sanity

* feat: add event types to sanity

* feat: add event announcements and miscellaneous events

* feat: add time until event

* fix: use Portable Text with announcement title

* fix: commit pnpm-lock.yaml

* feat: add mobile responsiveness

* fix: display hosts when organization is undefined

* feat: tabs stick to the top when scrolling near the top of screen on mobile screens

* refactor: event type background

* Revert "refactor: event type background" - doesn't work on preview

This reverts commit 594aca7.

* fix: change font sizes

* fix: make announcement dates look cleaner

* fix: switch from function calls to JSX component and extract converting dates to PST to utility function

* fix: add metadata and maintenance variable

* refactor: dynamically add Tailwind class based on event type

* refactor: put EventProps into separate file
  • Loading branch information
waalbert authored Jan 7, 2024
1 parent 3bdac53 commit ba38682
Show file tree
Hide file tree
Showing 14 changed files with 767 additions and 270 deletions.
15 changes: 15 additions & 0 deletions apps/sanity/schemas/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,21 @@ export default defineType({
type: "string",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "eventType",
title: "Event Type",
type: "string",
options: {
list: [
{ title: "Main", value: "Main" },
{ title: "Workshop", value: "Workshop" },
{ title: "Social", value: "Social" },
{ title: "Announcement", value: "Announcement" },
{ title: "Miscellaneous", value: "Miscellaneous" },
],
},
validation: (Rule) => Rule.required(),
}),
defineField({
name: "location",
title: "Location",
Expand Down
2 changes: 2 additions & 0 deletions apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"@types/three": "^0.158.2",
"axios": "^1.6.2",
"clsx": "^2.0.0",
"date-fns-tz": "^2.0.0",
"dayjs": "^1.11.10",
"framer-motion": "^10.16.14",
"lucide-react": "^0.292.0",
"next": "13.5.6",
Expand Down
12 changes: 12 additions & 0 deletions apps/site/src/app/schedule/EventProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default interface EventProps {
now: Date;
title: string;
eventType: string;
location?: string | undefined;
virtual?: string | undefined;
startTime: Date;
endTime: Date;
organization?: string | undefined;
hosts?: string[] | undefined;
description: JSX.Element;
}
32 changes: 32 additions & 0 deletions apps/site/src/app/schedule/components/EventAnnouncement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import convertToPST from "@/lib/utils/convertToPST";

const dateTimeFormat = new Intl.DateTimeFormat("en", {
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
});

interface EventAnnouncementProps {
description: JSX.Element;
startTime: Date;
endTime: Date;
}

export default function EventAnnouncement({
description,
startTime,
endTime,
}: EventAnnouncementProps) {
const startTimeInPST = convertToPST(startTime);
const endTimeInPST = convertToPST(endTime);

return (
<div className="text-white bg-[#0F6722] p-5 mb-6 rounded-2xl text-center">
<div className="text-2xl">{description}</div>
<p className="mb-2 text-lg">
{dateTimeFormat.formatRange(startTimeInPST, endTimeInPST)} PST
</p>
</div>
);
}
51 changes: 51 additions & 0 deletions apps/site/src/app/schedule/components/EventCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import EventRegular from "./EventRegular";
import EventAnnouncement from "./EventAnnouncement";
import EventMiscellaneous from "./EventMiscellaneous";
import EventProps from "../EventProps";

export default function EventCard({
now,
title,
eventType,
virtual,
startTime,
endTime,
organization,
hosts,
description,
}: EventProps) {
if (eventType === "Announcement") {
// description is used as the prop as opposed to title because description is a Portable Text object
// that can reflect all text formats put in Sanity
return (
<EventAnnouncement
description={description}
startTime={startTime}
endTime={endTime}
/>
);
} else if (eventType === "Miscellaneous") {
return (
<EventMiscellaneous
title={title}
startTime={startTime}
endTime={endTime}
description={description}
/>
);
} else {
return (
<EventRegular
now={now}
title={title}
eventType={eventType}
virtual={virtual}
startTime={startTime}
endTime={endTime}
organization={organization}
hosts={hosts}
description={description}
/>
);
}
}
25 changes: 25 additions & 0 deletions apps/site/src/app/schedule/components/EventMiscellaneous.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import dayjs from "dayjs";

interface EventMiscellaneousProps {
title: string;
startTime: Date;
endTime: Date;
description: JSX.Element;
}

export default function EventMiscellaneous({
title,
startTime,
endTime,
description,
}: EventMiscellaneousProps) {
const durationInHours = dayjs(endTime).diff(dayjs(startTime), "hour");

return (
<div className="text-white bg-[#973228] p-5 mb-6 rounded-2xl text-right text-lg">
<h3 className="text-2xl font-bold mb-3">{title}</h3>
{description}
<p>{durationInHours} hours</p>
</div>
);
}
112 changes: 112 additions & 0 deletions apps/site/src/app/schedule/components/EventRegular.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import convertToPST from "@/lib/utils/convertToPST";
import EventProps from "../EventProps";
dayjs.extend(relativeTime);

const dateTimeFormat = new Intl.DateTimeFormat("en", {
hour: "numeric",
minute: "numeric",
});

interface EventTypeProps {
eventType: string;
}

interface EventMomentProps {
now: Date;
startTimeInPST: Date;
endTimeInPST: Date;
}

interface EventBackgroundColors {
Main: string;
Workshop: string;
Social: string;
}

const eventBackgroundColors: EventBackgroundColors = {
Main: "bg-[#DFBA73]",
Workshop: "bg-[#94A9BD]",
Social: "bg-[#DFA9A9]",
};

function EventTypeComponent({ eventType }: EventTypeProps) {
return (
<div
className={`inline-block px-4 py-1.5 font-semibold text-[#0D272D] ${
eventBackgroundColors[eventType as keyof EventBackgroundColors]
} rounded-2xl sm:py-2`}
>
{eventType}
</div>
);
}

function EventMomentComponent({
now,
startTimeInPST,
endTimeInPST,
}: EventMomentProps) {
if (now > endTimeInPST) {
const dEnd = dayjs(endTimeInPST);
const timeAfterEnd = dEnd.from(now);
return (
<p className="text-white/50 text-right mb-0">
Ended {timeAfterEnd}
</p>
);
} else {
if (now > startTimeInPST) {
return <p className="text-white text-right mb-0">Happening Now!</p>;
} else {
const dStart = dayjs(startTimeInPST);
const timeUntilStart = dStart.from(now);
return (
<p className="text-white/50 text-right mb-0">
Starting {timeUntilStart}
</p>
);
}
}
}

export default function EventRegular({
now,
title,
eventType,
virtual,
startTime,
endTime,
organization,
hosts,
description,
}: EventProps) {
const startTimeInPST = convertToPST(startTime);
const endTimeInPST = convertToPST(endTime);

return (
<div className="text-[#FFFCE2] bg-[#432810] p-5 mb-6 rounded-2xl sm:text-lg">
<div className="mb-3 sm:flex sm:justify-between sm:items-center">
<h3 className="mb-3 text-2xl font-bold text-[#FFDA7B] sm:mb-0">
{title}
</h3>
<EventTypeComponent eventType={eventType} />
</div>
<p className="mb-2">
Hosted by:{" "}
{organization === undefined ? hosts?.join(", ") : organization}
</p>
<p className="mb-2">
{dateTimeFormat.formatRange(startTimeInPST, endTimeInPST)} PST |{" "}
<a href={virtual}>Meeting Link</a>
</p>
{description}
<EventMomentComponent
now={now}
startTimeInPST={startTimeInPST}
endTimeInPST={endTimeInPST}
/>
</div>
);
}
65 changes: 65 additions & 0 deletions apps/site/src/app/schedule/components/getSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { z } from "zod";
import { cache } from "react";
import { client } from "@/lib/sanity/client";
import { SanityDocument } from "@/lib/sanity/types";
import { formatInTimeZone } from "date-fns-tz";

const Events = z.array(
SanityDocument.extend({
_type: z.literal("event"),
title: z.string(),
eventType: z.string(),
location: z.string().optional(),
virtual: z.string().url().optional(),
startTime: z
.string()
.datetime()
.transform((time) => new Date(time)),
endTime: z
.string()
.datetime()
.transform((time) => new Date(time)),
organization: z.string().optional(),
hosts: z.array(z.string()).optional(),
description: z.array(
z.object({
_key: z.string(),
markDefs: z.array(
z.object({
_type: z.string(),
href: z.optional(z.string()),
_key: z.string(),
}),
),
children: z.array(
z.object({
text: z.string(),
_key: z.string(),
_type: z.literal("span"),
marks: z.array(z.string()),
}),
),
_type: z.literal("block"),
style: z.literal("normal"),
}),
),
}),
);

export const getSchedule = cache(async () => {
const events = Events.parse(
await client.fetch("*[_type == 'event'] | order(startTime asc)"),
);
const eventsByDay = new Map<string, z.infer<typeof Events>>();

events.forEach((event) => {
const key = formatInTimeZone(
new Date(event.startTime),
"America/Los_Angeles",
"MM/dd/yyyy",
);
eventsByDay.set(key, [...(eventsByDay.get(key) ?? []), event]);
});

return Array.from(eventsByDay.values());
});
31 changes: 27 additions & 4 deletions apps/site/src/app/schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { PortableText } from "@portabletext/react";

import ShiftingCountdown from "./components/ShiftingCountdown/ShiftingCountdown";
import { getSchedule } from "./components/getSchedule";
import SchedulePage from "./sections/SchedulePage";

export const revalidate = 60;

export default function Schedule() {
export const metadata: Metadata = {
title: "Schedule | IrvineHacks 2024",
};

export default async function Schedule() {
if (process.env.MAINTENANCE_MODE_SCHEDULE) {
redirect("/");
}

const events = await getSchedule();

const schedule = events.map((days) =>
days.map(({ description, ...day }) => ({
...day,
description: <PortableText value={description} />,
})),
);

return (
<>
<section className="h-full w-full">
<section className="h-full w-full mb-12">
<div className="m-36">
<ShiftingCountdown />
</div>
<div className="h-96 flex justify-center align-middle">
Placeholder for Schedule
<div className="flex justify-center">
<SchedulePage schedule={schedule} />
</div>
</section>
</>
Expand Down
3 changes: 3 additions & 0 deletions apps/site/src/app/schedule/sections/SchedulePage.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.TabsTrigger[data-state="active"] {
background-color: #fba80a;
}
Loading

0 comments on commit ba38682

Please sign in to comment.