-
Notifications
You must be signed in to change notification settings - Fork 3
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
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
8fb7ed5
to
2ee22ac
Compare
2ee22ac
to
2490c92
Compare
34864e8
to
3ff756d
Compare
On mobile looks good! Tho there's a slight glitch when you scroll down in the first tab and click on the second tab. It doesn't take you back in the view - maybe something we can implement if not too complicated. RPReplay_Final1728557038.mov |
c45188c
to
ab08216
Compare
const tabGroupRef = useRef<HTMLDivElement>(null) | ||
const hasMounted = useRef(false) | ||
|
||
const validDays = schedule!.days.filter((day) => day.events.length > 0) |
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.
I've noticed you use "!" in couple of cases here - is there an advantage over "?" ?
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.
TypeScript thinks that schedule
can be undefined
but, at this point, we know for sure that schedule
isn't undefined
since we check for it with eventHasSchedule
- and only then render the component.
You can postfix an expression with ! to tell TypeScript that you know it's not null or undefined. This works the same as an 'as' assertion.
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.
This is not super safe as schedule
comes from outside the component. The consumer of the component doesn't know that schedule
has to exist, and TypeScript will not complain if they try to pass undefined
.
// Works fine
{eventHasSchedule && <ScheduleSection schedule={schedule} />}
// Should fail but doesn't
<ScheduleSection schedule={schedule} />
As safer solution would be to type schedule
as non nullable:
type ScheduleTabsProps = {
schedule: NonNullable<Event['schedule']>
}
What do you think?
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.
I think this is amazing.
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.
Looking good, well done! Left a few non-blocking comments
ab08216
to
f90e0bb
Compare
const tabGroupRef = useRef<HTMLDivElement>(null) | ||
const hasMounted = useRef(false) | ||
|
||
const validDays = schedule!.days.filter((day) => day.events.length > 0) |
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.
This is not super safe as schedule
comes from outside the component. The consumer of the component doesn't know that schedule
has to exist, and TypeScript will not complain if they try to pass undefined
.
// Works fine
{eventHasSchedule && <ScheduleSection schedule={schedule} />}
// Should fail but doesn't
<ScheduleSection schedule={schedule} />
As safer solution would be to type schedule
as non nullable:
type ScheduleTabsProps = {
schedule: NonNullable<Event['schedule']>
}
What do you think?
type ScheduleTabsProps = { | ||
schedule: Event['schedule'] | ||
} | ||
|
||
const { screens } = theme | ||
|
||
export function ScheduleTabs({ schedule }: ScheduleTabsProps) { | ||
const tabGroupRef = useRef<HTMLDivElement>(null) | ||
const hasMounted = useRef(false) | ||
|
||
const validDays = schedule!.days.filter((day) => day.events.length > 0) | ||
|
||
useEffect(() => { | ||
hasMounted.current = true | ||
}, []) | ||
|
||
function scrollToTabGroup() { | ||
if (hasMounted.current && isScreenBelowLg() && tabGroupRef.current) { | ||
tabGroupRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'start', | ||
}) | ||
} | ||
} |
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.
type ScheduleTabsProps = { | |
schedule: Event['schedule'] | |
} | |
const { screens } = theme | |
export function ScheduleTabs({ schedule }: ScheduleTabsProps) { | |
const tabGroupRef = useRef<HTMLDivElement>(null) | |
const hasMounted = useRef(false) | |
const validDays = schedule!.days.filter((day) => day.events.length > 0) | |
useEffect(() => { | |
hasMounted.current = true | |
}, []) | |
function scrollToTabGroup() { | |
if (hasMounted.current && isScreenBelowLg() && tabGroupRef.current) { | |
tabGroupRef.current.scrollIntoView({ | |
behavior: 'smooth', | |
block: 'start', | |
}) | |
} | |
} | |
type ScheduleTabsProps = { | |
schedule: NonNullable<Event['schedule']> | |
} | |
const { screens } = theme | |
export function ScheduleTabs({ schedule }: ScheduleTabsProps) { | |
const tabGroupRef = useRef<HTMLDivElement>(null) | |
const hasMounted = useRef(false) | |
const validDays = schedule.days.filter((day) => day.events.length > 0) | |
useEffect(() => { | |
hasMounted.current = true | |
}, []) | |
function scrollToTabGroup() { | |
if (hasMounted.current && isScreenBelowLg() && tabGroupRef.current) { | |
tabGroupRef.current.scrollIntoView({ | |
behavior: 'smooth', | |
block: 'start', | |
}) | |
} | |
} |
type ScheduleSectionProps = { | ||
schedule: Event['schedule'] | ||
} | ||
|
||
export function ScheduleSection({ schedule }: ScheduleSectionProps) { | ||
return ( | ||
<PageSection kicker="Join Us" title={schedule?.title || 'Schedule'}> | ||
<ScheduleTabs schedule={schedule} /> | ||
</PageSection> | ||
) | ||
} |
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.
type ScheduleSectionProps = { | |
schedule: Event['schedule'] | |
} | |
export function ScheduleSection({ schedule }: ScheduleSectionProps) { | |
return ( | |
<PageSection kicker="Join Us" title={schedule?.title || 'Schedule'}> | |
<ScheduleTabs schedule={schedule} /> | |
</PageSection> | |
) | |
} | |
type ScheduleSectionProps = { | |
schedule: NonNullable<Event['schedule']> | |
} | |
export function ScheduleSection({ schedule }: ScheduleSectionProps) { | |
return ( | |
<PageSection kicker="Join Us" title={schedule.title || 'Schedule'}> | |
<ScheduleTabs schedule={schedule} /> | |
</PageSection> | |
) | |
} |
type SpeakersSectionProps = { | ||
speakers: 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> | ||
) | ||
} |
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.
type SpeakersSectionProps = { | |
speakers: 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> | |
) | |
} | |
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> | |
) | |
} |
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.
Ahhhh TS Master 🙇🏽♀️
useEffect(() => { | ||
hasMounted.current = true | ||
}, []) |
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.
Nice little optimisation - we can import useIsMounted
from usehooks-ts, which is already part of the project :)
function isScreenBelowLg() { | ||
return window.matchMedia(`(max-width: ${parseInt(screens.md, 10) - 1}px)`) | ||
.matches | ||
} |
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.
We already rely on useMediaQuery
from usehooks-ts in the project. Maybe it can be useful here as well?
tabGroupRef.current.scrollIntoView({ | ||
behavior: 'smooth', | ||
block: 'start', | ||
}) |
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.
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!
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.
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',
})
}
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.
Indeed!
@mirhamasala I added a blocking comment about the prop type of the section components. I believe we should use I also added an event via the CMS, and it worked great. The event I created was added at the end of the list, even though it happened sooner. How about sorting The page looks really good and professional, it will be great for Bangkok! Good job ✨👏 |
Oh, good catch! Thanks so much for the review, Charly! It was super helpful. |
10725fc
to
01dff48
Compare
Anytime 🫡 |
Suspect IssuesThis pull request was deployed and Sentry observed the following issues:
Did you find this useful? React with a 👍 or 👎 |
📝 Description
This pull request introduces a new feature to handle event schedules, including the addition of a schema for schedules, updates to the event data structure, and UI components to display the schedule on event pages. The most important changes include adding a new schedule schema, updating the event data structure, and creating new components for displaying the schedule.
Note
For the sake of speed, we have not implemented the mobile dropdown at this stage, instead opting for a sticky horizontal scrollbar. With Filipa unavailable, we don’t yet have a solid solution for handling the x-scrollbar effectively. This is also a pending task for the Allocators table.
The mobile dropdown will also be required for the Security Maturity Model Table of Contents, and at that point, we can refactor both implementations.
🛠️ Key Changes
Schema and Data Structure Updates:
ScheduleSchema
to define the structure of event schedules, including days and events (src/app/_schemas/event/ScheduleSchema.ts
).EventFrontMatterSchema
to include the newschedule
field (src/app/_schemas/event/FrontMatterSchema.ts
).convertMarkdownToEventData
function to handle the newschedule
field (src/app/_utils/convertMarkdownToEventData.ts
).UI Components:
ScheduleSection
component to display the schedule section on event pages (src/app/events/[slug]/components/ScheduleSection.tsx
). (src/app/events/[slug]/components/ScheduleSection.tsxR1-R17)ScheduleTabs
component to display the schedule in a tabbed format (src/app/events/[slug]/components/ScheduleTabs.tsx
). (src/app/events/[slug]/components/ScheduleTabs.tsxR1-R65)Event Page Integration:
ScheduleSection
component into the event page (src/app/events/[slug]/page.tsx
). (src/app/events/[slug]/page.tsxR18, src/app/events/[slug]/page.tsxR51, src/app/events/[slug]/page.tsxR76-R79)src/content/events/fil-bangkok-2024.md
).Utility Functions:
src/app/events/[slug]/utils/dateUtils.ts
). (src/app/events/[slug]/utils/dateUtils.tsR1-R10)Configuration Update:
public/admin/config.yml
).📌 To-Do Before Merging
🧪 How to Test
http://localhost:3000/admin/index.html
and try to add a new event with an Event Day schedule.📸 Screenshots