-
Notifications
You must be signed in to change notification settings - Fork 313
Add an intro box to Developer Meetings index #2373
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
base: main
Are you sure you want to change the base?
Changes from 25 commits
665cb75
fa6c87a
162977b
5400790
b79fb02
39733b9
981f76a
a9f6587
01fe659
bbfeddc
76b26ec
56f375f
adf9a13
632b25f
0fc5221
4d81494
f6624f9
c2f6fc0
e06618f
1c56896
e4523bc
3024a5b
2080049
e9a91d0
83654fb
58368f5
ecce463
3edcb99
fae0be7
e97fc0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| export const MEETINGS_INFO = { | ||
| eventLink: "https://discord.com/invite/stellardev?event=1496911886321713163", | ||
| xLink: "https://x.com/BuildOnStellar", | ||
| youtubeLink: "https://www.youtube.com/@StellarDevelopmentFoundation", | ||
| timeZone: "America/Los_Angeles", | ||
| weekday: "Thursday", // must capitalize | ||
| hour: 13, | ||
| minute: 0, | ||
| } as const; |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,243 @@ | ||||||
| import React from "react"; | ||||||
| import Link from "@docusaurus/Link"; | ||||||
|
|
||||||
| import { MEETINGS_INFO } from "../../meetings/schedule"; | ||||||
|
|
||||||
| type DateParts = { | ||||||
| year: number; | ||||||
| month: number; | ||||||
| day: number; | ||||||
| hour?: number; | ||||||
| minute?: number; | ||||||
| second?: number; | ||||||
| weekday?: string; | ||||||
| }; | ||||||
|
|
||||||
| export default function MeetingIndexCard(): React.ReactElement { | ||||||
| const [localMeetingTime, setLocalMeetingTime] = React.useState<string | null>( | ||||||
| null, | ||||||
| ); | ||||||
| const fallbackMeetingTime = formatMeetingTimeFallback(); | ||||||
|
|
||||||
| React.useEffect(() => { | ||||||
| try { | ||||||
| const nextMeeting = getNextMeetingDate(new Date()); | ||||||
| setLocalMeetingTime(formatMeetingDisplay(nextMeeting)); | ||||||
| } catch { | ||||||
| // Keep the fallback display if timezone calculation fails. | ||||||
| } | ||||||
| }, []); | ||||||
|
|
||||||
| return ( | ||||||
| <div className="card margin-bottom--lg"> | ||||||
| <div className="card__body"> | ||||||
| <h2 className="margin-bottom--sm">Developer Meetings</h2> | ||||||
| <p className="margin-bottom--sm" style={{ fontSize: "0.9rem" }}> | ||||||
| These are archived discussions of open Stellar meetings. Anyone can | ||||||
| attend them on {localMeetingTime ?? fallbackMeetingTime}. Join in the{" "} | ||||||
| <Link | ||||||
| href={MEETINGS_INFO.eventLink} | ||||||
| target="_blank" | ||||||
| rel="noreferrer noopener" | ||||||
| > | ||||||
| developer Discord | ||||||
| </Link> | ||||||
| , and follow{" "} | ||||||
| <Link | ||||||
| href={MEETINGS_INFO.xLink} | ||||||
| target="_blank" | ||||||
| rel="noreferrer noopener" | ||||||
| > | ||||||
| @BuildOnStellar | ||||||
|
Contributor
Author
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. My regulators don't like Discord, though I agree this is a chat out of scope for this venue. Just to answer the question, I've had my hand up throughout the entirety of past meetings without being brought up to speak. This leaves most of the latest meetings as just a few SDF team members mimicking their developments and viewpoints, without outside community input.
Suggested change
For the display, I think it'd be a little cleaner without the |
||||||
| </Link>{" "} | ||||||
| on X or the{" "} | ||||||
| <Link | ||||||
| href={MEETINGS_INFO.youtubeLink} | ||||||
| target="_blank" | ||||||
| rel="noreferrer noopener" | ||||||
| > | ||||||
| Stellar Development Foundation | ||||||
| </Link>{" "} | ||||||
| on YouTube for streams and updates. | ||||||
| </p> | ||||||
| <div style={{ display: "flex", flexWrap: "wrap", gap: "0.75rem" }}> | ||||||
| <Link | ||||||
| className="button button--primary" | ||||||
| style={{ color: "var(--ifm-color-white)" }} | ||||||
| to="/meetings" | ||||||
| > | ||||||
| Recent | ||||||
| </Link> | ||||||
| <Link | ||||||
| className="button button--primary" | ||||||
| style={{ color: "var(--ifm-color-white)" }} | ||||||
| to="/meetings/tags/developer" | ||||||
| > | ||||||
| Developer | ||||||
| </Link> | ||||||
| <Link | ||||||
| className="button button--primary" | ||||||
| style={{ color: "var(--ifm-color-white)" }} | ||||||
| to="/meetings/tags/protocol" | ||||||
| > | ||||||
| Protocol | ||||||
| </Link> | ||||||
| </div> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| function formatMeetingTimeFallback(): string { | ||||||
| return formatMeetingSchedule( | ||||||
| MEETINGS_INFO.weekday, | ||||||
| formatMeetingTime(makeDateInTimeZone(), MEETINGS_INFO.timeZone), | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| function formatMeetingDisplay(date: Date): string { | ||||||
| const formatter = new Intl.DateTimeFormat(undefined, { | ||||||
| weekday: "long", | ||||||
| }); | ||||||
| const parts = formatter.formatToParts(date); | ||||||
| const weekday = parts.find((part) => part.type === "weekday")?.value; | ||||||
|
|
||||||
| if (!weekday) { | ||||||
| return formatMeetingTime(date); | ||||||
| } | ||||||
| return formatMeetingSchedule(weekday, formatMeetingTime(date)); | ||||||
| } | ||||||
|
|
||||||
| function formatMeetingSchedule(weekday: string, time: string): string { | ||||||
| return `${weekday}s at ${time}`; | ||||||
| } | ||||||
|
|
||||||
| function formatMeetingTime(date: Date, timeZone?: string): string { | ||||||
| const options: Intl.DateTimeFormatOptions = { | ||||||
| hour: "numeric", | ||||||
| minute: "2-digit", | ||||||
| timeZoneName: timeZone ? "longGeneric" : "short", | ||||||
| }; | ||||||
|
|
||||||
| if (timeZone) { | ||||||
| options.timeZone = timeZone; | ||||||
| } | ||||||
|
|
||||||
| const formatter = new Intl.DateTimeFormat(undefined, options); | ||||||
| return formatter.format(date); | ||||||
| } | ||||||
|
|
||||||
| function datePartsToUtc(parts: DateParts): number { | ||||||
| return Date.UTC( | ||||||
| parts.year, | ||||||
| parts.month - 1, | ||||||
| parts.day, | ||||||
| parts.hour ?? 0, | ||||||
| parts.minute ?? 0, | ||||||
| parts.second ?? 0, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| function getTimeZoneOffset(date: Date, timeZone: string): number { | ||||||
| const dtf = new Intl.DateTimeFormat("en-US", { | ||||||
| timeZone, | ||||||
| hour12: false, | ||||||
| year: "numeric", | ||||||
| month: "2-digit", | ||||||
| day: "2-digit", | ||||||
| hour: "2-digit", | ||||||
| minute: "2-digit", | ||||||
| second: "2-digit", | ||||||
| }); | ||||||
| const parts = dtf.formatToParts(date); | ||||||
| const values = parts.reduce<Record<string, number>>((acc, part) => { | ||||||
| if (part.type !== "literal") { | ||||||
| acc[part.type] = Number(part.value); | ||||||
| } | ||||||
| return acc; | ||||||
| }, {}); | ||||||
| const asUtc = datePartsToUtc({ | ||||||
| year: values.year, | ||||||
| month: values.month, | ||||||
| day: values.day, | ||||||
| hour: values.hour, | ||||||
| minute: values.minute, | ||||||
| second: values.second, | ||||||
| }); | ||||||
| return (asUtc - date.getTime()) / 60000; | ||||||
| } | ||||||
|
|
||||||
| function makeDateInTimeZone( | ||||||
| parts: DateParts = { | ||||||
| year: 2019, | ||||||
| month: 11, | ||||||
| day: 4, | ||||||
| hour: MEETINGS_INFO.hour, | ||||||
| minute: MEETINGS_INFO.minute, | ||||||
| }, | ||||||
| ): Date { | ||||||
| const utcDate = new Date(datePartsToUtc(parts)); | ||||||
| const offsetMinutes = getTimeZoneOffset(utcDate, MEETINGS_INFO.timeZone); | ||||||
| return new Date(utcDate.getTime() - offsetMinutes * 60000); | ||||||
| } | ||||||
|
|
||||||
| function getTimeZoneParts(date: Date, timeZone: string): DateParts { | ||||||
| const dtf = new Intl.DateTimeFormat("en-US", { | ||||||
| timeZone, | ||||||
| weekday: "long", | ||||||
| hour12: false, | ||||||
| year: "numeric", | ||||||
| month: "2-digit", | ||||||
| day: "2-digit", | ||||||
| hour: "2-digit", | ||||||
| minute: "2-digit", | ||||||
| second: "2-digit", | ||||||
| }); | ||||||
| const parts = dtf.formatToParts(date); | ||||||
| const values = parts.reduce<Record<string, string>>((acc, part) => { | ||||||
| if (part.type !== "literal") { | ||||||
| acc[part.type] = part.value; | ||||||
| } | ||||||
| return acc; | ||||||
| }, {}); | ||||||
| return { | ||||||
| year: Number(values.year), | ||||||
| month: Number(values.month), | ||||||
| day: Number(values.day), | ||||||
| hour: Number(values.hour), | ||||||
| minute: Number(values.minute), | ||||||
| second: Number(values.second), | ||||||
| weekday: values.weekday, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| function getNextMeetingDate(now: Date): Date { | ||||||
| const ptParts = getTimeZoneParts(now, MEETINGS_INFO.timeZone); | ||||||
| const weekdayOrder = [ | ||||||
| "Sunday", | ||||||
| "Monday", | ||||||
| "Tuesday", | ||||||
| "Wednesday", | ||||||
| "Thursday", | ||||||
| "Friday", | ||||||
| "Saturday", | ||||||
| ]; | ||||||
| const currentIndex = weekdayOrder.indexOf(ptParts.weekday ?? ""); | ||||||
| const targetIndex = weekdayOrder.indexOf(MEETINGS_INFO.weekday); | ||||||
| const rawDaysAhead = (targetIndex - currentIndex + 7) % 7; | ||||||
| const hasMeetingPassedToday = | ||||||
| rawDaysAhead === 0 && | ||||||
| ((ptParts.hour ?? 0) > MEETINGS_INFO.hour || | ||||||
| ((ptParts.hour ?? 0) === MEETINGS_INFO.hour && | ||||||
| (ptParts.minute ?? 0) >= MEETINGS_INFO.minute)); | ||||||
| const daysAhead = | ||||||
| rawDaysAhead === 0 && hasMeetingPassedToday ? 7 : rawDaysAhead; | ||||||
| const nextMeetingPT = { | ||||||
| year: ptParts.year, | ||||||
| month: ptParts.month, | ||||||
| day: ptParts.day + daysAhead, | ||||||
| hour: MEETINGS_INFO.hour, | ||||||
| minute: MEETINGS_INFO.minute, | ||||||
| }; | ||||||
| return makeDateInTimeZone(nextMeetingPT); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import React, { type ReactNode } from "react"; | ||
| import { useLocation } from "@docusaurus/router"; | ||
| import type { WrapperProps } from "@docusaurus/types"; | ||
| import OriginalBlogPostItems from "@theme-original/BlogPostItems"; | ||
| import type BlogPostItemsType from "@theme/BlogPostItems"; | ||
|
|
||
| import MeetingIndexCard from "../../components/MeetingIndexCard"; | ||
|
JFWooten4 marked this conversation as resolved.
Outdated
|
||
|
|
||
| const MEETING_ROUTE = "/meetings"; | ||
|
|
||
| type Props = WrapperProps<typeof BlogPostItemsType>; | ||
|
|
||
| export default function BlogPostItemsWrapper(props: Props): ReactNode { | ||
| const { pathname } = useLocation(); | ||
|
|
||
| return ( | ||
| <> | ||
| {pathname === MEETING_ROUTE ? <MeetingIndexCard /> : null} | ||
| <OriginalBlogPostItems {...props} /> | ||
| </> | ||
| ); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.