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

[UXIT-1539] Events Page · Schedule #704

Merged
merged 12 commits into from
Oct 11, 2024
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',
})
Comment on lines +36 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

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

We already have a function scrollToSection in the maturity model utils folder, in case it can be extracted in the global utils reused here. Not sure, I haven't tried.

Super cool you added this by the way, smooth UX!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Decided not to extract the scrollToSection function for now because it is tightly coupled to the concept of a SectionHash.

By the way, this made me notice that the function scrollToSection uses document.querySelector, which in React is considered an anti-pattern because it directly manipulates the DOM outside of React’s control. Instead, React encourages the use of refs to interact with DOM elements.

Something like

import { useRef } from 'react'

export function scrollToSection(sectionRef: React.RefObject<HTMLElement>) {
  if (!sectionRef.current) {
    console.error(`The referenced element does not exist`)
    return
  }

  sectionRef.current.scrollIntoView({
    block: 'start',
    behavior: 'smooth',
  })
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Indeed!

}
}

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
}
Comment on lines +98 to +101
Copy link
Collaborator

Choose a reason for hiding this comment

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

We already rely on useMediaQuery from usehooks-ts in the project. Maybe it can be useful here as well?

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
Loading