Skip to content

Commit

Permalink
[UXIT-1539] Events Page · Schedule (#704)
Browse files Browse the repository at this point in the history
* Include schedule config to Events

* Style day events cards

* Style desktop Schedule with TabGroup

* Show Schedule

* Test data

* Add SpeakersSection component for displaying speakers in the event page

* CR

* Refactor ScheduleTabs component to filter out days without events

* CR

* CR

* CR

* Revert data
  • Loading branch information
mirhamasala authored Oct 11, 2024
1 parent 2bd3853 commit 0d4c834
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 35 deletions.
55 changes: 51 additions & 4 deletions public/admin/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ image_config: &image_config
label: "Image"
widget: "object"
required: false
collapsed: true
hint: Adding an image is optional. If no image is added, a default fallback image will be used automatically. Simply leave the image field empty to use the fallback.
fields:
- name: "src"
Expand Down Expand Up @@ -738,6 +739,7 @@ collections:
label: "External Link URL"
widget: "object"
required: false
collapsed: true
fields:
- name: "url"
label: "URL"
Expand All @@ -749,6 +751,7 @@ collections:
required: false
default: "View Event Details"
hint: "Text for the external link CTA. Please use proper capitalization. Defaults to 'View Event Details.'"
- *image_config
- name: "luma-calendar-link"
label: "Luma Calendar URL"
widget: "string"
Expand All @@ -758,19 +761,62 @@ collections:
label: "Luma Events Section"
widget: "object"
required: false
hint: Both the section title and the embed URL must be filled out for this section to appear on the page.
collapsed: true
hint: The embed URL must be filled out for this section to appear on the page.
fields:
- name: "title"
label: "Section Title"
widget: "string"
required: false
hint: This is the title for the section header. For example, "FIL Bangkok 2024 Events."
hint: This is the title for the Events section header. For example, "Main Stage Events." Defaults to 'Events.'
- name: "embed-link"
label: "Luma Embed URL"
widget: "string"
required: false
hint: Paste the embed URL for your Luma event here to display the events directly on the page. Include relevant tags when appropriate, e.g., https://lu.ma/embed/calendar/cal-nlDvL4B7Ko1swF0/events?lt=light&tag=FIL%20Bangkok%202024.
- *image_config
- name: "schedule"
label: "Schedule"
widget: "object"
required: false
collapsed: true
fields:
- name: "title"
label: "Section Title"
widget: "string"
required: false
hint: This is the title for the Schedule section header. For example, "Main Stage Schedule." Defaults to 'Schedule.'
- name: "days"
label: "Event Days"
widget: "list"
required: false
fields:
- name: "date"
label: "Date"
widget: "datetime"
- name: "events"
label: "Events"
widget: "list"
fields:
- name: "title"
label: "Event Title"
widget: "string"
- name: "description"
label: "Description"
widget: "text"
- name: "start"
label: "Start Time"
widget: "datetime"
- name: "end"
label: "End Time"
widget: "datetime"
required: false
- name: "location"
label: "Location"
widget: "string"
- name: "url"
label: "URL"
widget: "string"
required: false
- name: "speakers"
label: "Speakers"
widget: "list"
Expand All @@ -791,7 +837,7 @@ collections:
- name: "image"
label: "Image"
widget: "object"
required: false
collapsed: true
fields:
- name: "src"
label: "URL"
Expand All @@ -804,6 +850,7 @@ collections:
label: "Sponsors"
widget: "object"
required: false
collapsed: true
fields:
- name: "first-tier"
label: "First Tier"
Expand Down
2 changes: 2 additions & 0 deletions src/app/_schemas/event/FrontMatterSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/utils/categoryUtils'

import { DynamicBaseDataSchema } from '@/schemas/DynamicDataBaseSchema'
import { ScheduleSchema } from '@/schemas/event/ScheduleSchema'
import { SpeakersSchema } from '@/schemas/event/SpeakerSchema'
import { SponsorsSchema } from '@/schemas/event/SponsorSchema'

Expand Down Expand Up @@ -33,6 +34,7 @@ export const EventFrontMatterSchema = DynamicBaseDataSchema.extend({
.optional(),
startDate: z.coerce.date(),
endDate: z.coerce.date().optional(),
schedule: ScheduleSchema.optional(),
speakers: SpeakersSchema.optional(),
sponsors: SponsorsSchema.optional(),
})
28 changes: 28 additions & 0 deletions src/app/_schemas/event/ScheduleSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod'

const EventSchema = z
.object({
title: z.string(),
description: z.string(),
start: z.coerce.date(),
end: z.coerce.date().optional(),
location: z.string(),
url: z.string().url().optional(),
})
.strict()

const EventDaySchema = z
.object({
date: z.coerce.date(),
events: z.array(EventSchema),
})
.strict()

export const ScheduleSchema = z
.object({
title: z.string().optional(),
days: z.array(EventDaySchema),
})
.strict()

export type Schedule = z.infer<typeof ScheduleSchema>
1 change: 1 addition & 0 deletions src/app/_utils/convertMarkdownToEventData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function convertMarkdownToEventData(data: Record<string, any>) {
startDate: data['start-date'],
endDate: data['end-date'],
image: data.image,
schedule: data.schedule,
speakers: data.speakers,
sponsors: data.sponsors,
seo: data.seo,
Expand Down
17 changes: 17 additions & 0 deletions src/app/events/[slug]/components/ScheduleSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Event } from '@/types/eventType'

import { PageSection } from '@/components/PageSection'

import { ScheduleTabs } from './ScheduleTabs'

type ScheduleSectionProps = {
schedule: NonNullable<Event['schedule']>
}

export function ScheduleSection({ schedule }: ScheduleSectionProps) {
return (
<PageSection kicker="Join Us" title={schedule.title || 'Schedule'}>
<ScheduleTabs schedule={schedule} />
</PageSection>
)
}
101 changes: 101 additions & 0 deletions src/app/events/[slug]/components/ScheduleTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use client'

import { useRef } from 'react'

import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
import theme from 'tailwindcss/defaultTheme'
import { useIsMounted, useMediaQuery } from 'usehooks-ts'

import type { Event } from '@/types/eventType'

import { BasicCard } from '@/components/BasicCard'
import { Heading } from '@/components/Heading'
import { TextLink } from '@/components/TextLink'

import { formatDate, formatTime } from '../utils/dateUtils'

type ScheduleTabsProps = {
schedule: NonNullable<Event['schedule']>
}

const { screens } = theme

export function ScheduleTabs({ schedule }: ScheduleTabsProps) {
const tabGroupRef = useRef<HTMLDivElement>(null)
const isMounted = useIsMounted()
const isScreenBelowLg = useMediaQuery(
`(max-width: ${parseInt(screens.md, 10) - 1}px)`,
)

const validDays = schedule.days
.filter((day) => day.events.length > 0)
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())

function scrollToTabGroup() {
if (isMounted() && isScreenBelowLg && tabGroupRef.current) {
tabGroupRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
})
}
}

return (
<TabGroup
ref={tabGroupRef}
className="relative grid gap-6"
onChange={scrollToTabGroup}
>
<TabList className="sticky top-0 -m-2 flex gap-4 overflow-auto bg-brand-800 p-2 lg:static">
{validDays.map((day) => (
<Tab
key={formatDate(day.date)}
className="whitespace-nowrap rounded-lg p-3 font-bold text-brand-300 focus:brand-outline data-[hover]:bg-brand-700 data-[selected]:bg-brand-700 data-[selected]:text-brand-400"
>
{formatDate(day.date)}
</Tab>
))}
</TabList>
<TabPanels>
{validDays.map((day) => (
<TabPanel
key={formatDate(day.date)}
className="rounded-lg focus:brand-outline"
>
<div className="grid gap-4">
{day.events.map((event) => (
<BasicCard key={event.title}>
<div className="grid gap-6 lg:grid-cols-3">
<div className="flex gap-6 text-brand-300 lg:flex-col lg:gap-1">
<div className="text-sm font-bold">
<span>{formatTime(event.start)}</span>
{event.end && <span>{formatTime(event.end)}</span>}
</div>
<span className="text-sm">{event.location}</span>
</div>
<div className="lg:col-span-2">
<Heading tag="h3" variant="lg">
{event.title}
</Heading>
<p className="mb-4 mt-2 max-w-readable">
{event.description}
</p>
{event.url && (
<TextLink href={event.url}>View Details</TextLink>
)}
</div>
</div>
</BasicCard>
))}
</div>
</TabPanel>
))}
</TabPanels>
</TabGroup>
)
}

function isScreenBelowLg() {
return window.matchMedia(`(max-width: ${parseInt(screens.md, 10) - 1}px)`)
.matches
}
28 changes: 28 additions & 0 deletions src/app/events/[slug]/components/SpeakersSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Event } from '@/types/eventType'

import { CardGrid } from '@/components/CardGrid'
import { KeyMemberCard } from '@/components/KeyMemberCard'
import { PageSection } from '@/components/PageSection'

type SpeakersSectionProps = {
speakers: NonNullable<Event['speakers']>
}

export function SpeakersSection({ speakers }: SpeakersSectionProps) {
return (
<PageSection kicker="speakers" title="Speakers">
<CardGrid cols="mdTwo">
{speakers.map((speaker) => (
<KeyMemberCard
key={speaker.name}
{...speaker}
image={{
...speaker.image,
alt: `Photo of ${speaker.name}`,
}}
/>
))}
</CardGrid>
</PageSection>
)
}
6 changes: 2 additions & 4 deletions src/app/events/[slug]/components/SponsorSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type Sponsor = NonNullable<
>[number]

type SponsorSectionProps = {
sponsors: Event['sponsors']
sponsors: NonNullable<Event['sponsors']>
}

const sponsorTierConfigs = [
Expand Down Expand Up @@ -42,8 +42,6 @@ const sponsorTierConfigs = [
] as const

export function SponsorSection({ sponsors }: SponsorSectionProps) {
if (!sponsors) return null

return (
<PageSection kicker="sponsors" title="Sponsors">
<div className="grid gap-8">
Expand All @@ -53,7 +51,7 @@ export function SponsorSection({ sponsors }: SponsorSectionProps) {
return (
<SponsorGrid
key={tier}
sponsors={sponsors[tier]!}
sponsors={sponsors[tier]}
tier={tier}
gridClassName={gridClassName}
logoImageConfig={logoImageConfig}
Expand Down
Loading

0 comments on commit 0d4c834

Please sign in to comment.