diff --git a/gatsby-node.ts b/gatsby-node.ts index dc54a44833..60d2dbb63d 100644 --- a/gatsby-node.ts +++ b/gatsby-node.ts @@ -1,10 +1,9 @@ -import { ScheduleSession } from "./src/components/Conf/Schedule/ScheduleList" +import { ScheduleSession } from "./src/components/Conf/Schedule/session-list" import { SchedSpeaker } from "./src/components/Conf/Speakers/Speaker" import { GatsbyNode } from "gatsby" import { createOpenGraphImage } from "gatsby-plugin-dynamic-open-graph-images" import * as path from "path" import { glob } from "glob" -import _ from "lodash" import { updateCodeData } from "./scripts/update-code-data/update-code-data" import { organizeCodeData } from "./scripts/update-code-data/organize-code-data" import { sortCodeData } from "./scripts/update-code-data/sort-code-data" @@ -177,13 +176,21 @@ export const createPages: GatsbyNode["createPages"] = async ({ )) as SchedSpeaker[] ).filter(s => s.role.includes("speaker")) - // Create schedule page createPage({ path: "/conf/schedule", component: path.resolve("./src/templates/schedule.tsx"), context: { schedule }, }) + // Create schedule page + createPage({ + path: "/conf/sessions", + component: path.resolve("./src/templates/session.tsx"), + context: { + schedule: withSpeakerInfo(schedule.filter(session => session.speakers)), + }, + }) + // Create schedule events' pages schedule.forEach(event => { const eventSpeakers = speakers.filter(e => @@ -191,7 +198,7 @@ export const createPages: GatsbyNode["createPages"] = async ({ ) createPage({ - path: `/conf/schedule/${event.id}`, + path: `/conf/sessions/${event.id}`, component: path.resolve("./src/templates/event.tsx"), context: { event, @@ -222,6 +229,15 @@ export const createPages: GatsbyNode["createPages"] = async ({ } }) + function withSpeakerInfo(session: ScheduleSession[]) { + return session.map(session => ({ + ...session, + speakers: session.speakers + .map(speaker => speakers.find(s => s.username === speaker.username)) + .filter(Boolean), + })) + } + // Create speakers list page createPage({ path: "/conf/speakers", @@ -239,7 +255,10 @@ export const createPages: GatsbyNode["createPages"] = async ({ createPage({ path: `/conf/speakers/${speaker.username}`, component: path.resolve("./src/templates/speaker.tsx"), - context: { speaker, schedule: speakerSessions }, + context: { + speaker, + schedule: withSpeakerInfo(speakerSessions), + }, }) if (!process.env.GATSBY_CLOUD && !process.env.GITHUB_ACTIONS) { @@ -274,6 +293,11 @@ export const createPages: GatsbyNode["createPages"] = async ({ toPath: "/conf/schedule", }) + createRedirect({ + fromPath: "/conf/schedule/*", + toPath: "/conf/sessions/*", + }) + // redirect swapi with 200 createRedirect({ fromPath: `/swapi-graphql/*`, diff --git a/package.json b/package.json index ea4b5f79d3..ce920501f6 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "scripts": { "build": "gatsby build", "develop": "gatsby develop", - "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"", - "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\"", + "format": "yarn format:check --write", + "format:check": "prettier --cache --check \"**/*.{js,jsx,ts,tsx,json,md}\"", "start": "yarn develop", "serve": "gatsby serve", "clean": "gatsby clean", diff --git a/src/components/Conf/Header/index.tsx b/src/components/Conf/Header/index.tsx index 25a3f6274b..a3eb98e9ba 100644 --- a/src/components/Conf/Header/index.tsx +++ b/src/components/Conf/Header/index.tsx @@ -10,6 +10,7 @@ interface LinkItem { const links: LinkItem[] = [ { text: "Speakers", href: "/conf/speakers/" }, + { text: "Sessions", href: "/conf/sessions/" }, { text: "Schedule", href: "/conf/schedule/" }, { text: "FAQ", href: "/conf/faq/" }, ] diff --git a/src/components/Conf/Schedule/BackLink.tsx b/src/components/Conf/Schedule/BackLink.tsx index 65bd16e200..365f280ef2 100644 --- a/src/components/Conf/Schedule/BackLink.tsx +++ b/src/components/Conf/Schedule/BackLink.tsx @@ -1,13 +1,13 @@ import React from "react" -export const BackLink = ({ kind }: { kind: "speakers" | "schedule" }) => { +export const BackLink = ({ kind }: { kind: "speakers" | "sessions" }) => { return ( - {"<"}  Back to {kind === "speakers" ? "Speakers" : "Schedule"} + <  Back to {kind === "speakers" ? "Speakers" : "Sessions"} ) diff --git a/src/components/Conf/Schedule/Filters.tsx b/src/components/Conf/Schedule/Filters.tsx index 551f128fe2..86c32cc5f4 100644 --- a/src/components/Conf/Schedule/Filters.tsx +++ b/src/components/Conf/Schedule/Filters.tsx @@ -17,108 +17,93 @@ export default function Filters({ onReset, }: FiltersProps) { return ( -
-
-
-
- {Object.values(filterState).flat().length > 0 && ( - - )} - - - -
- {categories.map(option => ( - - 0 - ? "font-medium text-gray-900" - : "text-gray-500" - )} - > - {option.name} - - - ))} -
-
-
-
+
+ {Object.values(filterState).flat().length > 0 && ( + + )} + + + +
+ {categories.map(option => ( + + 0 + ? "font-medium text-gray-900" + : "text-gray-500" + )} + > + {option.name} + + + ))} +
+
+
+
+ + {categories.map((section, sectionIdx) => ( + + + {section.name} + {filterState[section.name].length ? ( + + {filterState[section.name].length} + + ) : null} + -
-
- - {categories.map((section, sectionIdx) => ( - +
+ {section.options.map((option, optionIdx) => ( +
+ { + const { checked, value } = e.target + onFilterChange(section.name, value, checked) + }} + checked={filterState[section.name].includes(option)} + type="checkbox" + className="cursor-pointer h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" + /> +
-
-
-
-
+ {option} + +
+ ))} + + + + ))} +
) } diff --git a/src/components/Conf/Schedule/session-list.tsx b/src/components/Conf/Schedule/session-list.tsx new file mode 100644 index 0000000000..ceb0b7c17d --- /dev/null +++ b/src/components/Conf/Schedule/session-list.tsx @@ -0,0 +1,289 @@ +import { compareAsc } from "date-fns" +import React, { ComponentProps, FC, useEffect, useState } from "react" +import { eventsColors } from "../../../utils/eventsColors" +import Filters from "./Filters" +import { SchedSpeaker } from "../Speakers/Speaker" + +export interface ScheduleSession { + id: string + audience: string + description: string + event_end: string + event_start: string + event_subtype: string + event_type: string + name: string + venue: string + speakers?: SchedSpeaker[] | string + files?: { name: string; path: string }[] +} + +export interface ConcurrentSessions { + [date: string]: ScheduleSession[] +} + +export interface ScheduleSessionsByDay { + [date: string]: ConcurrentSessions +} + +export type CategoryName = "Audience" | "Talk category" | "Event type" + +const filterCategories: Array<{ name: CategoryName; options: string[] }> = [ + { + name: "Audience", + options: ["Beginner", "Intermediate", "Advanced"], + }, + { + name: "Talk category", + options: [ + "Beyond Javascript", + "Spec Fusion", + "Platform and Backend", + "GraphQL and Data", + "GraphQL Security", + "GraphQL in Production", + "GraphQL Clients", + "GraphQL Core", + "Scaling", + "Emerging Community Trends", + ], + }, + { + name: "Event type", + options: [ + "Workshops", + "Unconference", + "Keynote Sessions", + "Sponsor Showcase", + "Session Presentations", + "Lightning Talks", + "Events & Experiences", + ], + }, +] + +function getSessionsByDay( + scheduleData: ScheduleSession[], + initialFilter: + | ((sessions: ScheduleSession[]) => ScheduleSession[]) + | undefined, + filters: Record +) { + const data = initialFilter ? initialFilter(scheduleData) : scheduleData + const filteredSortedSchedule = (data || []).sort((a, b) => + compareAsc(new Date(a.event_start), new Date(b.event_start)) + ) + + const concurrentSessions: ConcurrentSessions = {} + filteredSortedSchedule.forEach(session => { + const audienceFilter = filters.Audience + const talkCategoryFilter = filters["Talk category"] + const eventTypeFilter = filters["Event type"] + + let include = true + if (audienceFilter.length > 0) { + include = include && audienceFilter.includes(session.audience) + } + if (talkCategoryFilter.length > 0) { + include = include && talkCategoryFilter.includes(session.event_subtype) + } + if (eventTypeFilter.length > 0) { + include = include && eventTypeFilter.includes(session.event_type) + } + + if (!include) { + return + } + + if (!concurrentSessions[session.event_start]) { + concurrentSessions[session.event_start] = [] + } + concurrentSessions[session.event_start].push(session) + }) + + const sessionsByDay: ScheduleSessionsByDay = {} + Object.entries(concurrentSessions).forEach(([date, sessions]) => { + const day = date.split(" ")[0] + if (!sessionsByDay[day]) { + sessionsByDay[day] = {} + } + sessionsByDay[day] = { + ...sessionsByDay[day], + [date]: sessions.sort((a, b) => a.venue.localeCompare(b.venue)), + } + }) + + return sessionsByDay +} + +interface Props { + showFilter?: boolean + scheduleData: ScheduleSession[] + filterSchedule?: (sessions: ScheduleSession[]) => ScheduleSession[] +} + +const SessionList: FC = ({ + showFilter = true, + filterSchedule, + scheduleData, +}) => { + const [filtersState, setFiltersState] = useState< + Record + >({ + Audience: [], + "Talk category": [], + "Event type": [], + }) + const [sessionsState, setSessionState] = useState(() => + getSessionsByDay(scheduleData, filterSchedule, filtersState) + ) + + useEffect(() => { + setSessionState( + getSessionsByDay(scheduleData, filterSchedule, filtersState) + ) + }, [filtersState, scheduleData]) + + return ( +
+ {showFilter && ( + { + setFiltersState(prev => ({ + ...prev, + [category]: checked + ? [...prev[category as CategoryName], option] + : prev[category as CategoryName].filter( + option => option !== option + ), + })) + }} + onReset={() => { + setFiltersState({ + Audience: [], + "Talk category": [], + "Event type": [], + }) + }} + /> + )} + {Object.entries(sessionsState).length === 0 ? ( +
+

No sessions found

+
+ ) : ( +
+ {Object.entries(sessionsState).flatMap( + ([date, concurrentSessionsGroup]) => + Object.entries(concurrentSessionsGroup).flatMap( + ([sessionDate, sessions]) => + sessions.flatMap(session => { + const speakers = session.speakers as SchedSpeaker[] + + const borderColor = eventsColors[session.event_type] + + const countSpeakers = + speakers.length > 3 ? 3 : speakers.length + + const gridColumn = `span ${countSpeakers} / span ${countSpeakers}` + return ( + +
+
+
+
+ Recording +
+
+ + {(Number(new Date(session.event_end)) - + Number(new Date(session.event_start))) / + 1000 / + 60} + m +
+
+ + ↗ + +
+ ▶ +
+
+
+ + {session.event_type} + + {session.name} +
+ {speakers.map(s => ( +
+ +
+ {s.name} + {s.company} +
+
+ ))} +
+
+
+ ) + }) + ) + )} +
+ )} +
+ ) +} + +function ClockIcon(props: ComponentProps<"svg">) { + return ( + + + + + ) +} + +export default SessionList diff --git a/src/components/Conf/Speakers/SocialMedia.tsx b/src/components/Conf/Speakers/SocialMedia.tsx index 939d27f137..a09c7cb895 100644 --- a/src/components/Conf/Speakers/SocialMedia.tsx +++ b/src/components/Conf/Speakers/SocialMedia.tsx @@ -27,7 +27,7 @@ export const SocialMediaIcon = ({ switch (service) { case "twitter": - return + return case "linkedin": return case "facebook": diff --git a/src/components/Conf/Thanks/index.tsx b/src/components/Conf/Thanks/index.tsx index 49343ae361..21ff3bf46b 100644 --- a/src/components/Conf/Thanks/index.tsx +++ b/src/components/Conf/Thanks/index.tsx @@ -3,39 +3,66 @@ import ButtonConf from "../Button" const ThanksConf: React.FC = () => { return ( -
-

- Thank you for Attending! -

-
-
- gallery -
-
-

- Thank you to all who joined us for GraphQLConf 2023! We look - forward to seeing you at future events. -
-
- To experience the best of this year's event, be sure to watch - session recordings and slides from speakers, available on the event - schedule for each talk. -

-
- - Explore recordings and slides - + <> + {/*
*/} + {/*
*/} + {/*
*/} + {/*
*/} + {/*

Thank you for Attending!

*/} + {/* */} + {/* Thank you to all who joined us for GraphQLConf 2023! We*/} + {/* look forward to seeing you at future events.To experience the*/} + {/* best of this year's event, be sure to watch session recordings*/} + {/* and slides from speakers, available on the event schedule for*/} + {/* each talk.*/} + {/* */} + {/*
*/} + {/* */} + {/*
*/} + {/*
hello
*/} + {/* */} + {/* Explore recordings and slides*/} + {/* */} + {/*
*/} + {/*
*/} +
+

+ Thank you for Attending! +

+
+
+ gallery +
+
+

+ Thank you to all who joined us for GraphQLConf 2023! We + look forward to seeing you at future events. +
+
+ To experience the best of this year's event, be sure to watch + session recordings and slides from speakers, available on the + event schedule for each talk. +

+
+ + Explore recordings and slides + +
-
-
+ + ) } diff --git a/src/pages/conf/faq.tsx b/src/pages/conf/faq.tsx index 8db33f2dde..a79b9e77b8 100644 --- a/src/pages/conf/faq.tsx +++ b/src/pages/conf/faq.tsx @@ -2,7 +2,6 @@ import React, { ReactNode } from "react" import FooterConf from "../../components/Conf/Footer" import HeaderConf from "../../components/Conf/Header" import LayoutConf from "../../components/Conf/Layout" -import ButtonConf from "../../components/Conf/Button" import SectionConf from "../../components/Conf/Section" import SeoConf from "../../components/Conf/Seo" diff --git a/src/pages/conf/index.tsx b/src/pages/conf/index.tsx index 6dc876fc9c..56042dd11c 100644 --- a/src/pages/conf/index.tsx +++ b/src/pages/conf/index.tsx @@ -2,7 +2,6 @@ import React from "react" import FooterConf from "../../components/Conf/Footer" import HeaderConf from "../../components/Conf/Header" import LayoutConf from "../../components/Conf/Layout" -import ButtonConf from "../../components/Conf/Button" import SpeakersConf from "../../components/Conf/Speakers" import AboutConf from "../../components/Conf/About" import ScheduleGlanceConf from "../../components/Conf/Schedule" @@ -11,36 +10,22 @@ import { CalendarIcon, GlobeIcon } from "@radix-ui/react-icons" import ThanksConf from "../../components/Conf/Thanks" import GalleryConf from "../../components/Conf/Gallery" -export default () => { +export default function Page() { return ( -
-
-
-
- -
-
-
-
- - September 19-21, 2023 -
-
- - San Francisco Bay Area, CA -
-
-
- - View the Schedule - -
-
+
+
+ +
+ + September 19-21, 2023 + + + San Francisco Bay Area, CA
diff --git a/src/templates/EventOgImageTemplate.tsx b/src/templates/EventOgImageTemplate.tsx index 4973c5a037..0ac48a1e58 100644 --- a/src/templates/EventOgImageTemplate.tsx +++ b/src/templates/EventOgImageTemplate.tsx @@ -1,6 +1,6 @@ import React from "react" import { PageProps } from "gatsby" -import { ScheduleSession } from "../components/Conf/Schedule/ScheduleList" +import { ScheduleSession } from "../components/Conf/Schedule/session-list" import { SchedSpeaker } from "../components/Conf/Speakers/Speaker" import { format, parseISO } from "date-fns" import { getEventTitle } from "../utils/eventTitle" diff --git a/src/templates/event.tsx b/src/templates/event.tsx index e25fab95a5..ae33537c0c 100644 --- a/src/templates/event.tsx +++ b/src/templates/event.tsx @@ -8,8 +8,7 @@ import HeaderConf from "../components/Conf/Header" import LayoutConf from "../components/Conf/Layout" import SeoConf from "../components/Conf/Seo" import { SchedSpeaker } from "../components/Conf/Speakers/Speaker" -import { ScheduleSession } from "../components/Conf/Schedule/ScheduleList" -import { format, parseISO } from "date-fns" +import { ScheduleSession } from "../components/Conf/Schedule/session-list" import { Avatar } from "../components/Conf/Speakers/Avatar" import clsx from "clsx" import { @@ -63,48 +62,20 @@ export const EventComponent: FC<{
- {!hideBackButton && } -
-
- - - {/* */} - - - - {format(parseISO(event.event_start), "EEEE, MMM d")} - - - - {/* */} - - + {!hideBackButton && } + {recordingTitle.rating > 0.5 && ( + - - ))} -
- )} +
+ {event.files?.map(({ path }) => ( + <> + + View Full PDF{" "} + + +