diff --git a/apps/_template/codegen.yml b/apps/_template/codegen.yml new file mode 100644 index 0000000..aede7bd --- /dev/null +++ b/apps/_template/codegen.yml @@ -0,0 +1,7 @@ +overwrite: true +schema: "https://graph.codeday.org/" +documents: "./src/**/*.gql" +generates: + ./src/@types/graphql-modules.d.ts: + plugins: + - typescript-graphql-files-modules diff --git a/apps/_template/gql-loader.js b/apps/_template/gql-loader.js new file mode 100644 index 0000000..0cef9ba --- /dev/null +++ b/apps/_template/gql-loader.js @@ -0,0 +1,108 @@ +"use strict"; +const path = require("path"); +const fs = require("fs"); +const { parse, visit } = require("graphql"); + +function resolveImports(source, filePath, visited = new Set()) { + if (visited.has(filePath)) return []; + visited.add(filePath); + + const dir = path.dirname(filePath); + const lines = source.split(/\r\n|\r|\n/); + let definitions = []; + const queryLines = []; + + for (const line of lines) { + if (line.trim().startsWith("#import")) { + const match = line.match(/#import\s+["']([^"']+)["']/); + if (match) { + const importPath = path.resolve(dir, match[1]); + const importSource = fs.readFileSync(importPath, "utf-8"); + definitions = definitions.concat(resolveImports(importSource, importPath, visited)); + } + } else { + queryLines.push(line); + } + } + + const cleanSource = queryLines.join("\n").trim(); + if (cleanSource) { + const doc = parse(cleanSource); + definitions = definitions.concat(doc.definitions); + } + + return definitions; +} + +function deduplicateDefs(defs) { + const seen = new Set(); + return defs + .slice() + .reverse() + .filter((def) => { + if (def.kind === "FragmentDefinition") { + if (seen.has(def.name.value)) return false; + seen.add(def.name.value); + } + return true; + }) + .reverse(); +} + +function collectFragmentRefs(def) { + const refs = new Set(); + visit(def, { + FragmentSpread(node) { + refs.add(node.name.value); + }, + }); + return refs; +} + +function getTransitiveDeps(name, definitionMap, seen = new Set()) { + if (seen.has(name)) return seen; + seen.add(name); + const def = definitionMap.get(name); + if (def) { + for (const ref of collectFragmentRefs(def)) { + getTransitiveDeps(ref, definitionMap, seen); + } + } + return seen; +} + +module.exports = function (source) { + if (this.cacheable) this.cacheable(); + + const allDefs = resolveImports(source, this.resourcePath); + const uniqueDefs = deduplicateDefs(allDefs); + + const definitionMap = new Map(); + for (const def of uniqueDefs) { + if (def.name) { + definitionMap.set(def.name.value, def); + } + } + + const namedDefs = uniqueDefs.filter( + (d) => (d.kind === "OperationDefinition" || d.kind === "FragmentDefinition") && d.name, + ); + + function stripLoc(key, val) { + return key === "loc" ? undefined : val; + } + + const fullDoc = { kind: "Document", definitions: uniqueDefs }; + let output = `const _doc = ${JSON.stringify(fullDoc, stripLoc)};\n`; + output += `export default _doc;\n\n`; + + for (const def of namedDefs) { + const name = def.name.value; + const deps = getTransitiveDeps(name, definitionMap); + const subsetDefs = uniqueDefs.filter((d) => d.name && deps.has(d.name.value)); + const subsetDoc = { kind: "Document", definitions: subsetDefs }; + output += `export const ${name} = ${JSON.stringify(subsetDoc, stripLoc)};\n`; + } + + return output; +}; diff --git a/apps/_template/next-env.d.ts b/apps/_template/next-env.d.ts new file mode 100644 index 0000000..1970904 --- /dev/null +++ b/apps/_template/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/apps/_template/next.config.ts b/apps/_template/next.config.ts new file mode 100644 index 0000000..4ffd0f0 --- /dev/null +++ b/apps/_template/next.config.ts @@ -0,0 +1,24 @@ +import type { NextConfig } from "next"; +import path from "path"; + +const nextConfig: NextConfig = { + transpilePackages: ["@codeday/topo", "@codeday/topocons"], + turbopack: { + rules: { + "*.gql": { + loaders: [path.resolve("gql-loader.js")], + as: "*.js", + }, + }, + }, + webpack(config) { + config.module.rules.push({ + test: /\.gql$/, + exclude: /node_modules/, + loader: path.resolve("gql-loader.js"), + }); + return config; + }, +}; + +export default nextConfig; diff --git a/apps/_template/package.json b/apps/_template/package.json new file mode 100644 index 0000000..4ab6e16 --- /dev/null +++ b/apps/_template/package.json @@ -0,0 +1,30 @@ +{ + "name": "@codeday/APPNAME", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "codegen": "echo 'Add .gql files to src/ then run: graphql-codegen'" + }, + "dependencies": { + "@codeday/topo": "workspace:*", + "@codeday/topocons": "workspace:*", + "@codeday/utils": "workspace:*", + "graphql": "catalog:", + "graphql-tag": "^2.12.6", + "next": "catalog:", + "react": "catalog:", + "react-dom": "catalog:" + }, + "devDependencies": { + "@codeday/tsconfig": "workspace:*", + "@graphql-codegen/cli": "^5.0.6", + "@graphql-codegen/typescript-graphql-files-modules": "^3.0.0", + "@types/node": "catalog:", + "@types/react": "catalog:", + "@types/react-dom": "catalog:", + "typescript": "catalog:" + } +} diff --git a/apps/_template/src/pages/_app.tsx b/apps/_template/src/pages/_app.tsx new file mode 100644 index 0000000..b3786c2 --- /dev/null +++ b/apps/_template/src/pages/_app.tsx @@ -0,0 +1,12 @@ +import { ThemeProvider, PageDataProvider } from "@codeday/topo/Theme"; +import type { AppProps } from "next/app"; + +export default function App({ Component, pageProps }: AppProps) { + return ( + + + + + + ); +} diff --git a/apps/_template/src/pages/index.tsx b/apps/_template/src/pages/index.tsx new file mode 100644 index 0000000..9d65855 --- /dev/null +++ b/apps/_template/src/pages/index.tsx @@ -0,0 +1,11 @@ +import { Content } from "@codeday/topo/Molecule"; +import { Heading, Text } from "@codeday/topo/Atom"; + +export default function Home() { + return ( + + Hello World + Replace this with your app content. + + ); +} diff --git a/apps/_template/tsconfig.json b/apps/_template/tsconfig.json new file mode 100644 index 0000000..ffc566c --- /dev/null +++ b/apps/_template/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@codeday/tsconfig/nextjs.json", + "compilerOptions": { + "typeRoots": ["./src/@types"] + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/www/src/components/Calendly.tsx b/apps/www/src/components/Calendly.tsx index bdf853e..6dfc409 100644 --- a/apps/www/src/components/Calendly.tsx +++ b/apps/www/src/components/Calendly.tsx @@ -1,51 +1 @@ -import { Box } from "@codeday/topo/Atom"; -import React, { useEffect, useState, useRef } from "react"; - -interface CalendlyProps { - slug: string; - meeting?: string; - calendlyURLParams?: string; - [key: string]: any; -} - -export default function Calendly({ slug, meeting, calendlyURLParams, ...props }: CalendlyProps) { - const holder = useRef(null); - const [hasCalendlyLoaded, setHasCalendlyLoaded] = useState(false); - - const typeOfWindow = typeof window; // For static analysis - const windowCalendly = typeOfWindow !== "undefined" && (window as any)?.Calendly; - - useEffect(() => { - if (window && !(window as any).Calendly) { - const script = document.createElement("script"); - script.src = "https://calendly.com/assets/external/widget.js"; - script.addEventListener("load", () => { - setHasCalendlyLoaded(true); - }); - document.head.appendChild(script); - - const link = document.createElement("link"); - link.href = "https://calendly.com/assets/external/widget.css"; - link.rel = "stylesheet"; - document.body.appendChild(link); - - return () => { - document.head.removeChild(script); - document.body.removeChild(link); - }; - } - }, [typeOfWindow]); - - useEffect(() => { - if (windowCalendly && holder) { - (window as any).Calendly.initInlineWidget({ - url: `https://calendly.com/${slug}${meeting ? `/${meeting}` : ""}${calendlyURLParams || ""}`, - parentElement: holder.current, - prefill: {}, - utm: {}, - }); - } - }, [windowCalendly, hasCalendlyLoaded, holder, slug, meeting]); - - return ; -} +export { CalendlyEmbed as default } from "@codeday/topo/Molecule"; diff --git a/apps/www/src/components/Contact/Employees.tsx b/apps/www/src/components/Contact/Employees.tsx index c0d3547..2357bcf 100644 --- a/apps/www/src/components/Contact/Employees.tsx +++ b/apps/www/src/components/Contact/Employees.tsx @@ -2,7 +2,7 @@ import { Text, Box, Grid, Image, Link } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; import React from "react"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; const titleContents = ["CEO", "President", "VP", "Chief", "Director", "Head", "Manager", "Lead"]; const titlePrecedence = (title: string) => @@ -29,7 +29,7 @@ function dedupeByKey(key: string, arr: any[]) { export default function Employees(props: any) { const { account: { employees, otherTeam, contractors }, - } = useQuery(); + } = usePageData(); const sortedEmployees = dedupeByKey("username", [ ...employees, diff --git a/apps/www/src/components/EventInfo.gql b/apps/www/src/components/EventInfo/EventInfo.gql similarity index 100% rename from apps/www/src/components/EventInfo.gql rename to apps/www/src/components/EventInfo/EventInfo.gql diff --git a/apps/www/src/components/EventInfo/SubscribeBox.tsx b/apps/www/src/components/EventInfo/SubscribeBox.tsx new file mode 100644 index 0000000..96e4ef3 --- /dev/null +++ b/apps/www/src/components/EventInfo/SubscribeBox.tsx @@ -0,0 +1,60 @@ +import { Box, Button, TextInput as Input } from "@codeday/topo/Atom"; +import { useToasts, apiFetch } from "@codeday/topo/utils"; +import React, { useState } from "react"; +import { stringify as urlencode } from "urlencode"; +import { DateTime } from "luxon"; + +import { SubscribeToEvent } from "./EventInfo.gql"; + +export default function SubscribeBox({ event, ...rest }: { event: any; [key: string]: any }) { + const { success, error } = useToasts(); + const [destination, setDestination] = useState(""); + const dtFormat = `yyyyLLdd'T'HHmmss`; + const start = DateTime.fromISO(event.start).toUTC().toFormat(dtFormat); + const end = DateTime.fromISO(event.end).toUTC().toFormat(dtFormat); + const addLinkGoogleParams = urlencode({ + action: "TEMPLATE", + text: event.title, + dates: `${start}Z/${end}Z`, + location: event.location, + sf: "true", + output: "xml", + }); + const addLinkGoogle = `https://www.google.com/calendar/render?${addLinkGoogleParams}`; + return ( + + setDestination(e.target.value)} + placeholder="Phone Number" + display="inline-block" + w="sm" + borderTopRightRadius={0} + borderBottomRightRadius={0} + /> + + + + ); +} diff --git a/apps/www/src/components/EventInfo.tsx b/apps/www/src/components/EventInfo/index.tsx similarity index 68% rename from apps/www/src/components/EventInfo.tsx rename to apps/www/src/components/EventInfo/index.tsx index 5ef1747..427b952 100644 --- a/apps/www/src/components/EventInfo.tsx +++ b/apps/www/src/components/EventInfo/index.tsx @@ -1,69 +1,14 @@ -import { Box, Grid, Text, Heading, Link, Button, TextInput as Input } from "@codeday/topo/Atom"; +import { Box, Grid, Text, Heading, Link, Button } from "@codeday/topo/Atom"; import { Html } from "@codeday/topo/Molecule"; -import { useToasts, apiFetch } from "@codeday/topo/utils"; import TimeAgo from "javascript-time-ago"; import en from "javascript-time-ago/locale/en"; import { DateTime } from "luxon"; import React, { useState, useEffect } from "react"; -import { stringify as urlencode } from "urlencode"; -import { SubscribeToEvent } from "./EventInfo.gql"; +import SubscribeBox from "./SubscribeBox"; const SERVER_TIMEZONE = "America/Los_Angeles"; -function SubscribeBox({ event, ...rest }: { event: any; [key: string]: any }) { - const { success, error } = useToasts(); - const [destination, setDestination] = useState(""); - const dtFormat = `yyyyLLdd'T'HHmmss`; - const start = DateTime.fromISO(event.start).toUTC().toFormat(dtFormat); - const end = DateTime.fromISO(event.end).toUTC().toFormat(dtFormat); - const addLinkGoogleParams = urlencode({ - action: "TEMPLATE", - text: event.title, - dates: `${start}Z/${end}Z`, - location: event.location, - sf: "true", - output: "xml", - }); - const addLinkGoogle = `https://www.google.com/calendar/render?${addLinkGoogleParams}`; - return ( - - setDestination(e.target.value)} - placeholder="Phone Number" - display="inline-block" - w="sm" - borderTopRightRadius={0} - borderBottomRightRadius={0} - /> - - - - ); -} - export default function Event({ event, ...rest }: { event: any; [key: string]: any }) { const [timezone, setTimezone] = useState(SERVER_TIMEZONE); const [twasStart, setTwasStart] = useState(); diff --git a/apps/www/src/components/Highlight.tsx b/apps/www/src/components/Highlight.tsx index 8d284a4..3e25a54 100644 --- a/apps/www/src/components/Highlight.tsx +++ b/apps/www/src/components/Highlight.tsx @@ -1,16 +1 @@ -import { Text } from "@codeday/topo/Atom"; -import { useColorMode } from "@codeday/topo/Theme"; - -export default function Highlight(props: any) { - const { colorMode } = useColorMode(); - return ( - - ); -} +export { Highlight as default } from "@codeday/topo/Atom"; diff --git a/apps/www/src/components/Index/Announcement.tsx b/apps/www/src/components/Index/Announcement.tsx index 757311f..318c989 100644 --- a/apps/www/src/components/Index/Announcement.tsx +++ b/apps/www/src/components/Index/Announcement.tsx @@ -1,10 +1,10 @@ import { Box, Text, Button, Grid } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import { DateTime } from "luxon"; import React from "react"; -import { useQuery } from "../../query"; + const fromIso = (s: string) => { var b = s.split(/\D+/).map(Number); @@ -16,7 +16,7 @@ export default function Announcement(props: any) { const { cms: { announcements }, announcementWebinar: { events }, - } = useQuery(); + } = usePageData(); const now = new Date(); const webinarAnnouncementEndDate = new Date().setDate(now.getDate() + 2); diff --git a/apps/www/src/components/Index/Community.tsx b/apps/www/src/components/Index/Community.tsx deleted file mode 100644 index 0f7cfd1..0000000 --- a/apps/www/src/components/Index/Community.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Box, Grid, Image, Text, Heading } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; -import shuffle from "knuth-shuffle-seeded"; -import React, { useState, ReactElement } from "react"; -import { useInView } from "react-intersection-observer"; -import PageVisibility from "react-page-visibility"; -import Ticker from "react-ticker"; -import truncate from "truncate"; - -import { useQuery } from "../../query"; - -function PhotoTextCard({ photo, text, authors, wip, href }: any) { - return ( - - - - - {authors && - authors.length > 0 && - (authors.length > 1 ? ( - - {authors.map((author: any) => ( - - ))} - - ) : ( - - - - {authors[0].name} - - - ))} - - {truncate(text, 90)}{" "} - {wip && ( - - #work-in-progress - - )} - - - - - ); -} - -function PhotoCard({ photo, authors, wip, eventInfo, projectTitle, href }: any) { - return ( - - {authors && - authors.length > 0 && - (authors.length > 1 ? ( - - {authors.map((author: any) => ( - - ))} - -   - - - ) : ( - - - - {authors[0].name} - - - ))} - {!(authors && authors.length > 0) && eventInfo && ( - - - {eventInfo - ? [ - eventInfo.event?.program?.name, - eventInfo.program?.name, - eventInfo.region?.name, - ].join(" ") - : projectTitle} - - - )} - {projectTitle && ( - - - {projectTitle} - - - )} - - {wip && ( - - - #work-in-progress - - - )} - - ); -} - -function Card({ photo, text, authors, wip, eventInfo, projectTitle, href }: any) { - const elem = - text && text.length > 0 ? ( - - ) : ( - - ); - - return ( - - {elem} - - ); -} - -export default function Community({ seed, ...props }: { seed?: any; [key: string]: any }) { - const [pageIsVisible, setPageIsVisible] = useState(true); - const { ref, inView } = useInView({ rootMargin: "200px" }); - - const { - showcase, - cms: { indexCommunityPhotos, stats }, - } = useQuery(); - - const studentCount = stats?.items?.reduce( - (accum: number, e: any) => accum + e.statStudentCount, - 0, - ); - const studentCountRound = Math.round(studentCount / 10000) * 10000; - const studentCountPrefix = ["More than", "Nearly"][studentCountRound > studentCount ? 1 : 0]; - const showcaseDemos = showcase.projects - .map((p: any) => ({ - ...p, - members: p.members && p.members.map((a: any) => a.account).filter((a: any) => a), - media: - (p.media && p.media.filter((m: any) => m.type === "IMAGE" && m.topic !== "TEAM")[0]) || - null, - })) - .filter((p: any) => p.media && p.members && p.members.length > 0); - - const cards: ReactElement[] = shuffle( - [ - ...showcaseDemos.map((d: any) => ( - - )), - ...( - shuffle(indexCommunityPhotos.items, seed).map((p: any) => ( - - )) || [] - ).slice(0, 25), - ...(shuffle(showcase.photos, seed).map((p: any) => ( - - )) || []), - ], - seed, - ); - const rows = [ - cards.slice(0, Math.floor(cards.length / 2)), - cards.slice(Math.floor(cards.length / 2)), - ]; - - return ( - - - - {pageIsVisible && inView ? ( - {({ index }: { index: number }) => rows[0][index % rows[0].length]} - ) : ( - - )} - - - - - {studentCountPrefix} {studentCountRound.toLocaleString()} students have created amazing - projects at CodeDay events. - - - - - {pageIsVisible && inView ? ( - - {({ index }: { index: number }) => rows[1][index % rows[0].length]} - - ) : ( - - )} - - - - ); -} diff --git a/apps/www/src/components/Index/Community/Card.tsx b/apps/www/src/components/Index/Community/Card.tsx new file mode 100644 index 0000000..79dd814 --- /dev/null +++ b/apps/www/src/components/Index/Community/Card.tsx @@ -0,0 +1,32 @@ +import { Box } from "@codeday/topo/Atom"; +import PhotoTextCard from "./PhotoTextCard"; +import PhotoCard from "./PhotoCard"; + +export default function Card({ photo, text, authors, wip, eventInfo, projectTitle, href }: any) { + const elem = + text && text.length > 0 ? ( + + ) : ( + + ); + + return ( + + {elem} + + ); +} diff --git a/apps/www/src/components/Index/Community/PhotoCard.tsx b/apps/www/src/components/Index/Community/PhotoCard.tsx new file mode 100644 index 0000000..57a4450 --- /dev/null +++ b/apps/www/src/components/Index/Community/PhotoCard.tsx @@ -0,0 +1,89 @@ +import { Box, Image, Text } from "@codeday/topo/Atom"; + +export default function PhotoCard({ photo, authors, wip, eventInfo, projectTitle, href }: any) { + return ( + + {authors && + authors.length > 0 && + (authors.length > 1 ? ( + + {authors.map((author: any) => ( + + ))} + +   + + + ) : ( + + + + {authors[0].name} + + + ))} + {!(authors && authors.length > 0) && eventInfo && ( + + + {eventInfo + ? [ + eventInfo.event?.program?.name, + eventInfo.program?.name, + eventInfo.region?.name, + ].join(" ") + : projectTitle} + + + )} + {projectTitle && ( + + + {projectTitle} + + + )} + + {wip && ( + + + #work-in-progress + + + )} + + ); +} diff --git a/apps/www/src/components/Index/Community/PhotoTextCard.tsx b/apps/www/src/components/Index/Community/PhotoTextCard.tsx new file mode 100644 index 0000000..cfe9518 --- /dev/null +++ b/apps/www/src/components/Index/Community/PhotoTextCard.tsx @@ -0,0 +1,69 @@ +import { Box, Grid, Image, Text } from "@codeday/topo/Atom"; +import truncate from "truncate"; + +export default function PhotoTextCard({ photo, text, authors, wip, href }: any) { + return ( + + + + + {authors && + authors.length > 0 && + (authors.length > 1 ? ( + + {authors.map((author: any) => ( + + ))} + + ) : ( + + + + {authors[0].name} + + + ))} + + {truncate(text, 90)}{" "} + {wip && ( + + #work-in-progress + + )} + + + + + ); +} diff --git a/apps/www/src/components/Index/Community/index.tsx b/apps/www/src/components/Index/Community/index.tsx new file mode 100644 index 0000000..d36472b --- /dev/null +++ b/apps/www/src/components/Index/Community/index.tsx @@ -0,0 +1,106 @@ +import { Box, Heading } from "@codeday/topo/Atom"; +import { Content } from "@codeday/topo/Molecule"; +import shuffle from "knuth-shuffle-seeded"; +import { useState, ReactElement } from "react"; +import { useInView } from "react-intersection-observer"; +import PageVisibility from "react-page-visibility"; +import Ticker from "react-ticker"; + +import { usePageData } from "@codeday/topo/Theme"; +import Card from "./Card"; + +export default function Community({ seed, ...props }: { seed?: any; [key: string]: any }) { + const [pageIsVisible, setPageIsVisible] = useState(true); + const { ref, inView } = useInView({ rootMargin: "200px" }); + + const { + showcase, + cms: { indexCommunityPhotos, stats }, + } = usePageData(); + + const studentCount = stats?.items?.reduce( + (accum: number, e: any) => accum + e.statStudentCount, + 0, + ); + const studentCountRound = Math.round(studentCount / 10000) * 10000; + const studentCountPrefix = ["More than", "Nearly"][studentCountRound > studentCount ? 1 : 0]; + const showcaseDemos = showcase.projects + .map((p: any) => ({ + ...p, + members: p.members && p.members.map((a: any) => a.account).filter((a: any) => a), + media: + (p.media && p.media.filter((m: any) => m.type === "IMAGE" && m.topic !== "TEAM")[0]) || + null, + })) + .filter((p: any) => p.media && p.members && p.members.length > 0); + + const cards: ReactElement[] = shuffle( + [ + ...showcaseDemos.map((d: any) => ( + + )), + ...( + shuffle(indexCommunityPhotos.items, seed).map((p: any) => ( + + )) || [] + ).slice(0, 25), + ...(shuffle(showcase.photos, seed).map((p: any) => ( + + )) || []), + ], + seed, + ); + const rows = [ + cards.slice(0, Math.floor(cards.length / 2)), + cards.slice(Math.floor(cards.length / 2)), + ]; + + return ( + + + + {pageIsVisible && inView ? ( + {({ index }: { index: number }) => rows[0][index % rows[0].length]} + ) : ( + + )} + + + + + {studentCountPrefix} {studentCountRound.toLocaleString()} students have created amazing + projects at CodeDay events. + + + + + {pageIsVisible && inView ? ( + + {({ index }: { index: number }) => rows[1][index % rows[0].length]} + + ) : ( + + )} + + + + ); +} diff --git a/apps/www/src/components/Index/EcoBox.tsx b/apps/www/src/components/Index/EcoBox.tsx index 1510367..5c68b94 100644 --- a/apps/www/src/components/Index/EcoBox.tsx +++ b/apps/www/src/components/Index/EcoBox.tsx @@ -3,32 +3,29 @@ import { Content } from "@codeday/topo/Molecule"; import { Eco } from "@codeday/topocons"; import React from "react"; -import { useQuery } from "../../query"; -import StaticContent from "../StaticContent"; +import { usePageData } from "@codeday/topo/Theme"; export default function EcoBox() { - const { eco, learnMore } = useQuery().cms; + const { eco, learnMore } = usePageData().cms; return ( - - - - - - - - {eco?.items[0]?.value}{" "} - {learnMore?.items[0]?.value || "Learn more."} - - - - + + + + + + + {eco?.items[0]?.value}{" "} + {learnMore?.items[0]?.value || "Learn more."} + + + ); } diff --git a/apps/www/src/components/Index/Hero.tsx b/apps/www/src/components/Index/Hero.tsx index 61ccef8..f958ff4 100644 --- a/apps/www/src/components/Index/Hero.tsx +++ b/apps/www/src/components/Index/Hero.tsx @@ -2,16 +2,16 @@ import { Box, Grid, VisibilityCheckBox, Text, Heading, Button } from "@codeday/t import { MediaPlay as Play, Broadcast } from "@codeday/topocons"; import React from "react"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import useTwitch from "../../useTwitch"; -import VideoLink from "../VideoLink"; +import { VideoLink } from "@codeday/topo/Molecule"; import Live from "./Live"; import Teaser from "./Teaser"; export default function Hero({ ...props }: any) { const { cms: { tagline, mission, explainer }, - } = useQuery(); + } = usePageData(); const twitch = useTwitch(); const taglineBlock = ( diff --git a/apps/www/src/components/Index/Programs/NextEventDate.tsx b/apps/www/src/components/Index/Programs/NextEventDate.tsx new file mode 100644 index 0000000..36e81db --- /dev/null +++ b/apps/www/src/components/Index/Programs/NextEventDate.tsx @@ -0,0 +1,17 @@ +import { Text } from "@codeday/topo/Atom"; +import React from "react"; + +import { nextUpcomingEvent, upcomingEvents, formatInterval } from "../../../utils/time"; + +export default function NextEventDate({ upcoming }: { upcoming: any[] }) { + const next = nextUpcomingEvent(upcoming); + return next ? ( + + {upcomingEvents(upcoming) + .map((e) => formatInterval(e.startsAt, e.endsAt)) + .join("; ")} + + ) : ( + <> + ); +} diff --git a/apps/www/src/components/Index/Programs/ProgramCard.tsx b/apps/www/src/components/Index/Programs/ProgramCard.tsx new file mode 100644 index 0000000..15ac5d8 --- /dev/null +++ b/apps/www/src/components/Index/Programs/ProgramCard.tsx @@ -0,0 +1,39 @@ +import { Box, Button, Image, Text } from "@codeday/topo/Atom"; +import React from "react"; + +import NextEventDate from "./NextEventDate"; + +interface ProgramCardProps { + program: any; +} + +export default function ProgramCard({ program }: ProgramCardProps) { + return ( + + + + + + + {program.name} + + + + + {program.shortDescription} + + + + + + ); +} diff --git a/apps/www/src/components/Index/Programs.gql b/apps/www/src/components/Index/Programs/Programs.gql similarity index 100% rename from apps/www/src/components/Index/Programs.gql rename to apps/www/src/components/Index/Programs/Programs.gql diff --git a/apps/www/src/components/Index/Programs/RegionList.tsx b/apps/www/src/components/Index/Programs/RegionList.tsx new file mode 100644 index 0000000..6ceec48 --- /dev/null +++ b/apps/www/src/components/Index/Programs/RegionList.tsx @@ -0,0 +1,50 @@ +import { Box } from "@codeday/topo/Atom"; +import { UiStar } from "@codeday/topocons"; +import React from "react"; + +interface RegionListProps { + sortedRegions: any[]; + registrationOpenWebnames: string[]; + upcomingNameOverrides: Record; +} + +export default function RegionList({ + sortedRegions, + registrationOpenWebnames, + upcomingNameOverrides, +}: RegionListProps) { + return ( + <> + + {sortedRegions.map((region: any) => ( + + {upcomingNameOverrides[region.webname] || region.name} + {region.upcoming && ( + + + + + {registrationOpenWebnames.includes(region.webname) ? `Registrations open!` : ``} + + )} + + ))} + + + + + + Event planned this season. + + + ); +} diff --git a/apps/www/src/components/Index/Programs.tsx b/apps/www/src/components/Index/Programs/index.tsx similarity index 58% rename from apps/www/src/components/Index/Programs.tsx rename to apps/www/src/components/Index/Programs/index.tsx index 30c1d22..10ec0c3 100644 --- a/apps/www/src/components/Index/Programs.tsx +++ b/apps/www/src/components/Index/Programs/index.tsx @@ -1,35 +1,22 @@ import { Box, Grid, Button, Image, Text, CodeDay, Link } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import { apiFetch } from "@codeday/topo/utils"; -import { UiStar } from "@codeday/topocons"; import { print } from "graphql"; import haversine from "haversine-distance"; import React, { useEffect, useState } from "react"; -import { useQuery } from "../../query"; -import { nextUpcomingEvent, upcomingEvents, formatInterval } from "../../utils/time"; import { GetMyLocation } from "./Programs.gql"; - -function NextEventDate({ upcoming }: { upcoming: any[] }) { - const next = nextUpcomingEvent(upcoming); - return next ? ( - - {upcomingEvents(upcoming) - .map((e) => formatInterval(e.startsAt, e.endsAt)) - .join("; ")} - - ) : ( - <> - ); -} +import NextEventDate from "./NextEventDate"; +import ProgramCard from "./ProgramCard"; +import RegionList from "./RegionList"; export default function Programs() { const { colorMode } = useColorMode(); const { cms: { regions, mainPrograms, codeDayProgram, labsProgram }, clear: { events }, - } = useQuery(); + } = usePageData(); const [geo, setGeo] = useState(); const codeDay = codeDayProgram?.items[0]; const labs = labsProgram?.items[0]; @@ -89,36 +76,11 @@ export default function Programs() { ) - - {sortedRegions.map((region: any) => ( - - {upcomingNameOverrides[region.webname] || region.name} - {region.upcoming && ( - - - - - {registrationOpenWebnames.includes(region.webname) ? `Registrations open!` : ``} - - )} - - ))} - - - - - - Event planned this season. - + {/* More Programs */} @@ -159,32 +121,7 @@ export default function Programs() { {mainPrograms?.items?.map((program: any) => ( - - - - - - - {program.name} - - - - - {program.shortDescription} - - - - - + ))} diff --git a/apps/www/src/components/Index/Quotes/index.tsx b/apps/www/src/components/Index/Quotes/index.tsx index 3bf6a43..65f3d87 100644 --- a/apps/www/src/components/Index/Quotes/index.tsx +++ b/apps/www/src/components/Index/Quotes/index.tsx @@ -3,7 +3,7 @@ import { Content } from "@codeday/topo/Molecule"; import shuffle from "knuth-shuffle-seeded"; import React, { useState, useReducer, useEffect } from "react"; -import { useQuery } from "../../../query"; +import { usePageData } from "@codeday/topo/Theme"; import VideoTestimonialThumbnail from "../../VideoTestimonialThumbnail"; import Globe from "./Globe"; import TextQuote from "./TextQuote"; @@ -18,7 +18,7 @@ interface QuotesProps { export default function Quotes({ seed }: QuotesProps) { const { cms: { quoteRegions, quoteTestimonials }, - } = useQuery(); + } = usePageData(); const textQuotes = shuffle( quoteTestimonials?.items.filter((q: any) => !q.video), seed, diff --git a/apps/www/src/components/Index/Sponsors.tsx b/apps/www/src/components/Index/Sponsors.tsx index dd98dac..e8db271 100644 --- a/apps/www/src/components/Index/Sponsors.tsx +++ b/apps/www/src/components/Index/Sponsors.tsx @@ -1,16 +1,16 @@ import { Grid, Heading, Link, Box, Text, Image } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import React from "react"; -import { useQuery } from "../../query"; + import PreviousCoverageLogos from "../PreviousCoverageLogos"; export default function Sponsors(props: any) { const { colorMode } = useColorMode(); const { cms: { majorSponsors, minorSponsors }, - } = useQuery(); + } = usePageData(); return ( diff --git a/apps/www/src/components/Index/Stats.tsx b/apps/www/src/components/Index/Stats.tsx index c09aa28..838e075 100644 --- a/apps/www/src/components/Index/Stats.tsx +++ b/apps/www/src/components/Index/Stats.tsx @@ -1,10 +1,10 @@ import { Text, Grid, Box } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import React from "react"; import CountUp from "react-countup"; -import { useQuery } from "../../query"; + function rollup(events: any[]): Record { const stats: Record = {}; @@ -47,7 +47,7 @@ export default function Stats(props: any) { const { cms: { stats, quoteRegions }, labs: { statTotalOutcomes }, - } = useQuery(); + } = usePageData(); const rollupStats = rollup(stats.items); const hours = statTotalOutcomes.find((o: any) => o.key === "hoursCount"); diff --git a/apps/www/src/components/Index/Teaser.tsx b/apps/www/src/components/Index/Teaser.tsx index be76fb6..f91f29e 100644 --- a/apps/www/src/components/Index/Teaser.tsx +++ b/apps/www/src/components/Index/Teaser.tsx @@ -1,70 +1,8 @@ -import { RatioBox } from "@codeday/topo/Atom"; -import React, { useState, useReducer, useRef, useEffect } from "react"; -import { useInView } from "react-intersection-observer"; -import PageVisibility from "react-page-visibility"; -import ReactPlayer from "react-player"; +import React from "react"; -// eslint-disable-next-line no-secrets/no-secrets -const videoId = "hXdlNf3YVpMgfGh7cUF2L00THVfG02YmRP"; -const thumbWidth = 640; -const thumbHeight = 480; -const startAt = 4; +import MuxAutoplayVideo from "../MuxAutoplayVideo"; +// eslint-disable-next-line no-secrets/no-secrets export default function Teaser() { - const ref = useRef(null); - const { ref: viewRef, inView } = useInView({ rootMargin: "200px", initialInView: true }); - const [pageVisible, setPageVisible] = useState(true); - const [muted, setMuted] = useState(true); - const [playing, togglePlaying] = useReducer((prev: boolean) => !prev, true); - const [pageLoaded, setPageLoaded] = useState(false); - - useEffect(() => setPageLoaded(true), []); - const onClick = () => { - if (ref.current.getInternalPlayer().muted) { - setMuted(false); - } else { - togglePlaying(); - } - }; - - const onPlay = () => { - if (ref.current.getInternalPlayer().currentTime === 0) { - ref.current.seekTo(startAt); - } - }; - - const bg = `https://image.mux.com/${videoId}/thumbnail.png?width=${thumbWidth}&height=${thumbHeight}&fit_mode=crop&time=${startAt}`; - - return ( - - - {pageLoaded && ( - - )} - - - ); + return ; } diff --git a/apps/www/src/components/Index/Workshops.tsx b/apps/www/src/components/Index/Workshops.tsx index fe00868..f5877b3 100644 --- a/apps/www/src/components/Index/Workshops.tsx +++ b/apps/www/src/components/Index/Workshops.tsx @@ -3,7 +3,7 @@ import { Content } from "@codeday/topo/Molecule"; import { create } from "random-seed"; import React from "react"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { parseIsoString, formatShortDate } from "../../utils/time"; const fixedColors: Record = { @@ -17,7 +17,7 @@ const fixedColors: Record = { const colors = ["green", "blue", "orange", "cyan", "purple", "yellow", "indigo"]; export default function Workshops() { - const { calendar } = useQuery(); + const { calendar } = usePageData(); if (calendar?.events?.length === 0) return <>; diff --git a/apps/www/src/components/Markdown.tsx b/apps/www/src/components/Markdown.tsx index 3a5dcea..a67e1ea 100644 --- a/apps/www/src/components/Markdown.tsx +++ b/apps/www/src/components/Markdown.tsx @@ -1,64 +1 @@ -import { Heading, Text, List, ListItem, Box } from "@codeday/topo/Atom"; -import React from "react"; -import ReactMarkdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; -import RemarkGFM from "remark-gfm"; - -const HEADING_SIZES = ["4xl", "3xl", "xl", "md", "md", "md"]; -const adjustHeadingLevel = (baseHeadingLevel: number, level: number): number => { - const newLevel = baseHeadingLevel + level - 1; - if (newLevel > 6) return 6; - if (newLevel < 1) return 1; - return newLevel; -}; - -interface MarkdownProps { - baseHeadingLevel?: number; - allowHtml?: boolean; - rehypePlugins?: any[]; - [key: string]: any; -} - -const Markdown = ({ baseHeadingLevel, allowHtml, ...props }: MarkdownProps) => { - function h(level: number) { - return (props: any) => { - const adjustedLevel = adjustHeadingLevel(baseHeadingLevel || 1, level); - const m = Math.max(6 - adjustedLevel + 1, 2); - return ( - - ); - }; - } - - const mdTheme = { - h1: h(1), - h2: h(2), - h3: h(3), - h4: h(4), - h5: h(5), - h6: h(6), - tr: (props: any) => , - p: (props: any) => , - ol: (props: any) => , - ul: (props: any) => , - li: (props: any) => , - }; - return ( - - ); -}; - -export default Markdown; +export { Markdown as default } from "@codeday/topo/Molecule"; diff --git a/apps/www/src/components/MuxAutoplayVideo.tsx b/apps/www/src/components/MuxAutoplayVideo.tsx new file mode 100644 index 0000000..4f2878d --- /dev/null +++ b/apps/www/src/components/MuxAutoplayVideo.tsx @@ -0,0 +1,107 @@ +import { Box, RatioBox } from "@codeday/topo/Atom"; +import { UiVolume } from "@codeday/topocons"; +import React, { useState, useReducer, useRef, useEffect } from "react"; +import { useInView } from "react-intersection-observer"; +import PageVisibility from "react-page-visibility"; +import ReactPlayer from "react-player"; + +interface MuxAutoplayVideoProps { + videoId: string; + startAt?: number; + thumbWidth?: number; + thumbHeight?: number; + showUnmuteOverlay?: boolean; + [key: string]: any; +} + +export default function MuxAutoplayVideo({ + videoId, + startAt = 0, + thumbWidth = 640, + thumbHeight = 480, + showUnmuteOverlay = false, + ...props +}: MuxAutoplayVideoProps) { + const ref = useRef(null); + const { ref: viewRef, inView } = useInView({ rootMargin: "200px", initialInView: true }); + const [pageVisible, setPageVisible] = useState(true); + const [muted, setMuted] = useState(true); + const [playing, togglePlaying] = useReducer((prev: boolean) => !prev, true); + const [pageLoaded, setPageLoaded] = useState(false); + + useEffect(() => setPageLoaded(true), []); + const onClick = () => { + if (ref.current.getInternalPlayer().muted) { + setMuted(false); + if (showUnmuteOverlay) { + ref.current.seekTo(0); + } + } else { + togglePlaying(); + } + }; + + const onPlay = () => { + if (ref.current.getInternalPlayer().currentTime === 0) { + ref.current.seekTo(startAt); + } + }; + + // eslint-disable-next-line no-secrets/no-secrets + const bg = `https://image.mux.com/${videoId}/thumbnail.png?width=${thumbWidth}&height=${thumbHeight}&fit_mode=crop&time=${startAt}`; + + return ( + + + {showUnmuteOverlay && ( + + + + + Unmute + + + + )} + {pageLoaded && ( + + )} + + + ); +} diff --git a/apps/www/src/components/Page.tsx b/apps/www/src/components/Page.tsx deleted file mode 100644 index 8668804..0000000 --- a/apps/www/src/components/Page.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { Presence } from "@chakra-ui/react"; -import { Box, CodeDay, Button, Text, Heading } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; -import { Header, SiteLogo, Main, Menu, Footer } from "@codeday/topo/Organism"; -import { DefaultSeo } from "next-seo"; -import Head from "next/head"; -import React, { ReactNode } from "react"; - -import { useFundraise } from "../providers"; -import { useQuery } from "../query"; - -const DOMAIN = "https://www.codeday.org"; -const FUNDRAISE_UP_BUTTON_ID = "XBSBRRMF"; - -interface PageProps { - children?: ReactNode; - title?: string; - darkHeader?: boolean; - slug?: string; - seo?: any; - fun?: boolean; - [key: string]: any; -} - -export default function Page({ children, title, darkHeader, slug, seo }: PageProps) { - const { cms } = useQuery(); - const { mission } = cms || {}; - const { isFundraiseLoaded } = useFundraise(); - const disclaimerTexts = (cms?.globalSponsors?.items || []) - .flatMap((sponsor: any) => sponsor.legalDisclaimer.split(`\n`)) - .filter(Boolean); - - return ( - - - - - {seo ?? ( - - )} - -
- - - - - CodeDay - - - - - - - - - - - - - - - - -
-
{children}
- - {disclaimerTexts && disclaimerTexts.length > 0 && ( - - - - Funding Statements and Disclaimers - - {disclaimerTexts.map((text: string) => ( - - {text} - - ))} - - - )} -
- {""} -
-
-
-
- ); -} diff --git a/apps/www/src/components/Page/DisclaimerFooter.tsx b/apps/www/src/components/Page/DisclaimerFooter.tsx new file mode 100644 index 0000000..bd77ff3 --- /dev/null +++ b/apps/www/src/components/Page/DisclaimerFooter.tsx @@ -0,0 +1,34 @@ +import { Box, Text, Heading } from "@codeday/topo/Atom"; +import { Content } from "@codeday/topo/Molecule"; +import React from "react"; + +interface DisclaimerFooterProps { + disclaimerTexts: string[]; +} + +export default function DisclaimerFooter({ disclaimerTexts }: DisclaimerFooterProps) { + if (!disclaimerTexts || disclaimerTexts.length === 0) return null; + + return ( + + + + Funding Statements and Disclaimers + + {disclaimerTexts.map((text: string) => ( + + {text} + + ))} + + + ); +} diff --git a/apps/www/src/components/Page/NavMenu.tsx b/apps/www/src/components/Page/NavMenu.tsx new file mode 100644 index 0000000..084894f --- /dev/null +++ b/apps/www/src/components/Page/NavMenu.tsx @@ -0,0 +1,57 @@ +import { Presence } from "@chakra-ui/react"; +import { Box, Button } from "@codeday/topo/Atom"; +import React from "react"; + +const FUNDRAISE_UP_BUTTON_ID = "XBSBRRMF"; + +interface NavMenuProps { + isFundraiseLoaded: boolean; +} + +export default function NavMenu({ isFundraiseLoaded }: NavMenuProps) { + return ( + <> + + + + + + + + +
+ + + + ); +} diff --git a/apps/www/src/components/Page.gql b/apps/www/src/components/Page/Page.gql similarity index 100% rename from apps/www/src/components/Page.gql rename to apps/www/src/components/Page/Page.gql diff --git a/apps/www/src/components/Page/index.tsx b/apps/www/src/components/Page/index.tsx new file mode 100644 index 0000000..839bb0d --- /dev/null +++ b/apps/www/src/components/Page/index.tsx @@ -0,0 +1,79 @@ +import { Box, CodeDay } from "@codeday/topo/Atom"; +import { Header, SiteLogo, Main, Menu, Footer } from "@codeday/topo/Organism"; +import { DefaultSeo } from "next-seo"; +import Head from "next/head"; +import React, { ReactNode } from "react"; + +import { useFundraise } from "../../providers"; +import { usePageData } from "@codeday/topo/Theme"; +import DisclaimerFooter from "./DisclaimerFooter"; +import NavMenu from "./NavMenu"; + +const DOMAIN = "https://www.codeday.org"; + +interface PageProps { + children?: ReactNode; + title?: string; + darkHeader?: boolean; + slug?: string; + seo?: any; + fun?: boolean; + [key: string]: any; +} + +export default function Page({ children, title, darkHeader, slug, seo }: PageProps) { + const { cms } = usePageData(); + const { mission } = cms || {}; + const { isFundraiseLoaded } = useFundraise(); + const disclaimerTexts = (cms?.globalSponsors?.items || []) + .flatMap((sponsor: any) => sponsor.legalDisclaimer.split(`\n`)) + .filter(Boolean); + + return ( + + + + + {seo ?? ( + + )} + +
+ + + + + CodeDay + + + + + + +
+
{children}
+ + +
+ {""} +
+
+ + + ); +} diff --git a/apps/www/src/components/Press/PhotoGallery.tsx b/apps/www/src/components/Press/PhotoGallery.tsx index 0226f19..94fc555 100644 --- a/apps/www/src/components/Press/PhotoGallery.tsx +++ b/apps/www/src/components/Press/PhotoGallery.tsx @@ -3,7 +3,7 @@ import { Content } from "@codeday/topo/Molecule"; import shuffle from "knuth-shuffle-seeded"; import React, { useState } from "react"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import Photo from "./Photo"; import PhotoTagPicker from "./PhotoTagPicker"; @@ -16,7 +16,7 @@ export default function PhotoGallery({ seed, ...props }: PhotoGalleryProps) { const [filter, setFilter] = useState(null); const { cms: { pressPhotos }, - } = useQuery(); + } = usePageData(); const photos = shuffle( (pressPhotos?.items || []).map((m: any) => m), seed, diff --git a/apps/www/src/components/PreviousCoverageLogos.tsx b/apps/www/src/components/PreviousCoverageLogos.tsx index d57d295..941d72b 100644 --- a/apps/www/src/components/PreviousCoverageLogos.tsx +++ b/apps/www/src/components/PreviousCoverageLogos.tsx @@ -1,9 +1,8 @@ import { Link, Image } from "@codeday/topo/Atom"; import React from "react"; -import { useQuery } from "../query"; -import { dedupeFirstByKey } from "../utils/arr"; -import StaticContent from "./StaticContent"; +import { usePageData } from "@codeday/topo/Theme"; +import { dedupeFirstByKey } from "@codeday/utils"; interface PreviousCoverageLogosProps { num?: number; @@ -13,14 +12,14 @@ interface PreviousCoverageLogosProps { export default function PreviousCoverageLogos({ num = 5, ...props }: PreviousCoverageLogosProps) { const { cms: { coverageLogos }, - } = useQuery(); + } = usePageData(); const pubs = dedupeFirstByKey( coverageLogos.items.filter((pub: any) => pub.publicationLogo), "publicationName", ).slice(0, num); return ( - + <> {pubs.map((pub: any) => ( ))} - + ); } diff --git a/apps/www/src/components/StaticContent.tsx b/apps/www/src/components/StaticContent.tsx deleted file mode 100644 index f0fd8dc..0000000 --- a/apps/www/src/components/StaticContent.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { createElement, ReactNode, ElementType } from "react"; - -interface StaticContentProps { - children?: ReactNode; - element?: string | ElementType; - [key: string]: any; -} - -// This no longer works in React 18, but is left in place for compatibiltiy. -export default function StaticContent({ children, element = "div", ...props }: StaticContentProps) { - return createElement(element as any, { - ...props, - children, - }); -} diff --git a/apps/www/src/components/VideoLink.tsx b/apps/www/src/components/VideoLink.tsx index b719779..fea3932 100644 --- a/apps/www/src/components/VideoLink.tsx +++ b/apps/www/src/components/VideoLink.tsx @@ -1,25 +1 @@ -import { Box } from "@codeday/topo/Atom"; -import React, { useState, ReactNode } from "react"; -import { Modal } from "react-responsive-modal"; - -import VideoPlayer from "./VideoPlayer"; - -interface VideoLinkProps { - children?: ReactNode; - [key: string]: any; -} - -export default function VideoLink({ children, ...props }: VideoLinkProps) { - const [modalOpen, setModalOpen] = useState(false); - - return ( - <> - setModalOpen(false)}> - - - setModalOpen(true)}> - {children} - - - ); -} +export { VideoLink as default } from "@codeday/topo/Molecule"; diff --git a/apps/www/src/components/VideoPlayer.tsx b/apps/www/src/components/VideoPlayer.tsx index e4e4ec9..91a83aa 100644 --- a/apps/www/src/components/VideoPlayer.tsx +++ b/apps/www/src/components/VideoPlayer.tsx @@ -1,43 +1 @@ -import { Box } from "@codeday/topo/Atom"; -import dynamic from "next/dynamic"; -import React, { useEffect, useRef } from "react"; - -//@ts-ignore -const Hls = dynamic(() => import("hls.js/dist/hls.light.js"), { ssr: false }); - -interface VideoPlayerProps { - url?: string; - poster?: string; - autoPlay?: boolean; - volume?: number; - [key: string]: any; -} - -export default function VideoPlayer({ url, poster, autoPlay, volume, ...props }: VideoPlayerProps) { - const player = useRef(null); - useEffect(() => { - if (typeof window === "undefined" || typeof player === "undefined") return; - if (url?.split(".").pop() !== "m3u8") return; - if (!(Hls as any).isSupported()) return; - - const hls = new (Hls as any)(); - hls.loadSource(url); - hls.attachMedia(player); - }, [typeof window, player, url]); - - return ( - - - - ); -} +export { VideoPlayer as default } from "@codeday/topo/Molecule"; diff --git a/apps/www/src/components/VideoTestimonialThumbnail.tsx b/apps/www/src/components/VideoTestimonialThumbnail.tsx index fe0203e..c87ba7c 100644 --- a/apps/www/src/components/VideoTestimonialThumbnail.tsx +++ b/apps/www/src/components/VideoTestimonialThumbnail.tsx @@ -2,7 +2,7 @@ import { Box } from "@codeday/topo/Atom"; import { MediaPlay } from "@codeday/topocons"; import React from "react"; -import VideoLink from "./VideoLink"; +import { VideoLink } from "@codeday/topo/Molecule"; interface VideoTestimonialThumbnailProps { video: any; diff --git a/apps/www/src/components/Volunteer/PhotoGallery.tsx b/apps/www/src/components/Volunteer/PhotoGallery.tsx index ea9841e..6b8e66b 100644 --- a/apps/www/src/components/Volunteer/PhotoGallery.tsx +++ b/apps/www/src/components/Volunteer/PhotoGallery.tsx @@ -1,9 +1,9 @@ import { Box, Grid, Text, Image } from "@codeday/topo/Atom"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; export default function PhotoGallery(props: any) { - const { cms } = useQuery(); + const { cms } = usePageData(); const volunteerPhotoGallery = cms?.volunteerPhotoGallery?.items || []; return ( diff --git a/apps/www/src/components/Volunteer/PreviewVideo.tsx b/apps/www/src/components/Volunteer/PreviewVideo.tsx index 7836682..41f2d1d 100644 --- a/apps/www/src/components/Volunteer/PreviewVideo.tsx +++ b/apps/www/src/components/Volunteer/PreviewVideo.tsx @@ -1,93 +1,8 @@ -import { Box, RatioBox } from "@codeday/topo/Atom"; -import { UiVolume } from "@codeday/topocons"; -import React, { useState, useReducer, useRef, useEffect } from "react"; -import { useInView } from "react-intersection-observer"; -import PageVisibility from "react-page-visibility"; -import ReactPlayer from "react-player"; +import React from "react"; -// eslint-disable-next-line no-secrets/no-secrets -const videoId = "c1BhPbPJvRjeGvUutUIgrCG5bCsgT021q"; -const thumbWidth = 640; -const thumbHeight = 480; -const startAt = 14; +import MuxAutoplayVideo from "../MuxAutoplayVideo"; +// eslint-disable-next-line no-secrets/no-secrets export default function PreviewVideo(props: any) { - const ref = useRef(null); - const { ref: viewRef, inView } = useInView({ rootMargin: "200px", initialInView: true }); - const [pageVisible, setPageVisible] = useState(true); - const [muted, setMuted] = useState(true); - const [playing, togglePlaying] = useReducer((prev: boolean) => !prev, true); - const [pageLoaded, setPageLoaded] = useState(false); - - useEffect(() => setPageLoaded(true), []); - const onClick = () => { - if (ref.current.getInternalPlayer().muted) { - setMuted(false); - ref.current.seekTo(0); - } else { - togglePlaying(); - } - }; - - const onPlay = () => { - if (ref.current.getInternalPlayer().currentTime === 0) { - ref.current.seekTo(startAt); - } - }; - - const bg = `https://image.mux.com/${videoId}/thumbnail.png?width=${thumbWidth}&height=${thumbHeight}&fit_mode=crop&time=${startAt}`; - - return ( - - - - - - - Unmute - - - - {pageLoaded && ( - - )} - - - ); + return ; } diff --git a/apps/www/src/components/Volunteer/ProgramInfo.tsx b/apps/www/src/components/Volunteer/ProgramInfo.tsx index 112e178..9a46a38 100644 --- a/apps/www/src/components/Volunteer/ProgramInfo.tsx +++ b/apps/www/src/components/Volunteer/ProgramInfo.tsx @@ -2,7 +2,7 @@ import { Box, Grid, Text, Image, List, ListItem, Button } from "@codeday/topo/At import React from "react"; import { formatInterval } from "../../utils/time"; -import ContentfulRichText from "../ContentfulRichText"; +import { ContentfulRichText } from "@codeday/topo/Molecule"; import ProgramShareBlurb from "./ProgramShareBlurb"; import { VOLUNTEER_ROLES } from "./wizardConfig"; diff --git a/apps/www/src/components/Volunteer/ProgramInfoCheck.tsx b/apps/www/src/components/Volunteer/ProgramInfoCheck.tsx index 9633399..ac0da44 100644 --- a/apps/www/src/components/Volunteer/ProgramInfoCheck.tsx +++ b/apps/www/src/components/Volunteer/ProgramInfoCheck.tsx @@ -2,7 +2,7 @@ import { Box, Text, Image, List, ListItem, Checkbox } from "@codeday/topo/Atom"; import React, { useState } from "react"; import { formatInterval } from "../../utils/time"; -import ContentfulRichText from "../ContentfulRichText"; +import { ContentfulRichText } from "@codeday/topo/Molecule"; interface ProgramInfoCheckProps { program: any; diff --git a/apps/www/src/components/Volunteer/ProgramShareBlurb.tsx b/apps/www/src/components/Volunteer/ProgramShareBlurb.tsx index 449dd38..de7236b 100644 --- a/apps/www/src/components/Volunteer/ProgramShareBlurb.tsx +++ b/apps/www/src/components/Volunteer/ProgramShareBlurb.tsx @@ -2,7 +2,7 @@ import { Box, Grid, Text, Heading, Image, Button, Divider, Link } from "@codeday import { UiArrowDown, UiArrowUp, FilePdf } from "@codeday/topocons"; import React, { useState } from "react"; -import ContentfulRichText from "../ContentfulRichText"; +import { ContentfulRichText } from "@codeday/topo/Molecule"; interface ProgramShareBlurbProps { program: any; diff --git a/apps/www/src/components/Volunteer/Testimonials.tsx b/apps/www/src/components/Volunteer/Testimonials.tsx index 5d48038..6a3eee0 100644 --- a/apps/www/src/components/Volunteer/Testimonials.tsx +++ b/apps/www/src/components/Volunteer/Testimonials.tsx @@ -2,7 +2,7 @@ import { Box, Grid, Text, Image } from "@codeday/topo/Atom"; import shuffle from "knuth-shuffle-seeded"; import { useSlideshow } from "../../providers"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; const QUOTE_DURATION = 10000; @@ -12,7 +12,7 @@ interface TestimonialsProps { } export default function Testimonials({ seed, ...props }: TestimonialsProps) { - const { cms } = useQuery(); + const { cms } = usePageData(); const testimonials = shuffle( (cms?.volunteerTestimonials?.items || []).filter( (t: any) => t.quote.split(" ").length <= 6 * 8, diff --git a/apps/www/src/components/Volunteer/Wizard.tsx b/apps/www/src/components/Volunteer/Wizard.tsx deleted file mode 100644 index ce0c9f0..0000000 --- a/apps/www/src/components/Volunteer/Wizard.tsx +++ /dev/null @@ -1,409 +0,0 @@ -import { - Box, - Button, - Text, - Heading, - HStack, - VStack, - TextInput, - Divider, - Checkbox, - Radio, - Link, -} from "@codeday/topo/Atom"; -import { Collapse } from "@codeday/topo/Molecule"; -import { DataCollection } from "@codeday/topo/Molecule"; -import { useToasts } from "@codeday/topo/utils"; -import { debug } from "@codeday/utils"; -import { usePostHog } from "@posthog/react"; -import React, { useState, useReducer, useEffect, RefObject } from "react"; - -import { useMarketing } from "../../providers"; -import { useAfterMountEffect } from "../../utils/useAfterMountEffect"; - -const DEBUG = debug(["www", "components", "Volunteer", "Wizard"]); - -// https://stackoverflow.com/a/48981669 -function groupBy(xs: any[], f: (x: any) => string): Record { - return xs.reduce( - (r: any, v: any, i: number, a: any[], k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), - {}, - ); -} - -const emailRe = new RegExp(".+@.+\\..+"); - -interface WizardProps { - events: any[]; - formRef: RefObject; - startBackground?: string; - startRegion?: string; - startPage?: number; - startSelection?: boolean; - after?: string; -} - -export default function Wizard({ - events, - formRef, - startBackground = "", - startRegion = "", - startPage = 0, - startSelection = false, - after, -}: WizardProps) { - const posthog = usePostHog(); - const { error } = useToasts(); - const regions = new Array( - ...new Set( - events - .filter((e) => !e.dontAcceptVolunteers) - .map((e) => - JSON.stringify({ - name: e.region?.name || e.name, - webname: e.contentfulWebname, - country: e.region?.countryName || "Other", - aliases: e.region?.aliases || [], - }), - ), - ), - ).map((e) => JSON.parse(e)); // json -> string -> json for deduplication - const regionsByCountry = groupBy(regions, (r) => r.country); - - useEffect(() => { - DEBUG("regions", regions); - DEBUG("regionsByCountry", regionsByCountry); - }, [regions, regionsByCountry]); - - let resolvedStartRegion = startRegion; - if (resolvedStartRegion) { - const webnamesToRegion: Record = {}; - regions.forEach((r) => { - webnamesToRegion[r.webname] = r.name; - r.aliases.forEach((alias: string) => { - webnamesToRegion[alias] = r.name; - }); - }); - resolvedStartRegion = webnamesToRegion[resolvedStartRegion] || ""; - } - let resolvedStartPage = startPage; - let resolvedStartBackground = startBackground; - let resolvedStartSelection = startSelection; - if (startRegion && !resolvedStartRegion) { - resolvedStartPage = 0; - resolvedStartBackground = ""; - resolvedStartSelection = false; - } - - const [background, setBackground] = useState(resolvedStartBackground); - const [firstName, setFirstName] = useState(""); - const [lastName, setLastName] = useState(""); - const [email, setEmail] = useState(""); - const [linkedin, setLinkedin] = useState(""); - const { linkedInConversion } = useMarketing(); - const [region, setRegion] = useState(resolvedStartRegion); - const [isOrganize, setIsOrganize] = useState(false); - const [commitmentLevel, setCommitmentLevel] = useState(0); - const [hasSelection, setHasSelection] = useState(resolvedStartSelection); - const [isSubmitting, setIsSubmitting] = useState(false); - const [submitError, setSubmitError] = useState(false); - - useEffect(() => { - posthog?.group("background", background); - }, [background]); - - const pageBackground = ( - - - Are you a student? - - - - - - - ); - const pageRegion = ( - - - Please select a CodeDay City: - - { - // force "Other" to end of list (kind of hacky) - [ - ...Object.keys(regionsByCountry).filter((k) => k !== "Other"), - Object.keys(regionsByCountry).includes("Other") ? "Other" : undefined, - ].map((regionKey) => ( - - {/* Capitalize first letter of region (this is mostly to fix "the United States" looking weird) */} - - {regionKey?.charAt(0).toUpperCase()} - {regionKey?.substring(1)} - - {regionsByCountry[regionKey!]?.map((r) => ( - - { - setRegion(r.name); - setHasSelection(true); - setIsOrganize(false); - }} - > - {r.name} - - - ))} - - )) - } - - {/* Clear region state in case they clicked some other region button before this */} - - - - - - Interested in becoming a CodeDay Organizer? - - CodeDay events around the world are organized by students just like you! - - You'll manage a team, come up with cool ideas, and help hundreds of students in your - community discover CS. - - - (No prior event organizing experience is required, CodeDay Staff will support + guide - you every step of the way) - - - = 0} animateOpacity> - - setCommitmentLevel(e.target.checked ? 1 : 0)}> - I am interested in organizing a CodeDay in my city - - -
-
- = 1} animateOpacity> - - What city/region would you like to organize an event in? - { - setRegion(e.target.value); - setHasSelection(true); - }} - w="md" - display="block" - /> - -
-
-
-
-
-
- ); - useEffect(() => { - if ( - firstName && - lastName && - emailRe.test(email) && - (background === "industry" ? linkedin : true) - ) - setHasSelection(true); - else setHasSelection(false); - }, [firstName, lastName, email, linkedin]); - const pageEmail = ( - - - {/*special logic if the only page user sees is contact info*/} - {resolvedStartPage === 2 - ? background === "industry" - ? "Apply to volunteer for CodeDay Labs" - : `Apply to volunteer for CodeDay ${region}` - : "Let us know how to reach out:"} - - - setFirstName(e.target.value)} - /> - setLastName(e.target.value)} - /> - setEmail(e.target.value)} - /> - {background === "industry" ? ( - { - setLinkedin(e.target.value); - }} - /> - ) : undefined} - - - - ); - - const pageConfirmation = submitError ? ( - - - ☹️ An Error Ocurred - - - Please email volunteer@codeday.org with - your application, as well as the following error code: - - {submitError} - - ) : ( - - - ✅ Got it! - - We'll be in touch over email in the next few days! - - ); - - const pages = [pageBackground, pageRegion, pageEmail, pageConfirmation]; - const pageIds = ["background", "region", "email", "final"]; - - // 'last' should really be 'penultimate' but 'last' is shorter - const [page, navigate] = useReducer( - (prev: number, action: string) => - Math.max( - 0, - action === "next" ? prev + 1 : prev - 1, - action === "last" ? pages.length - 2 : 0, - ), - resolvedStartPage, - ); - const isFinalPage = page === pages.length - 1; - - const [hasStarted, setHasStarted] = useState(false); - useAfterMountEffect(() => { - if (!hasStarted) { - posthog?.capture("volunteer.started", { style: "full" }); - } - setHasStarted(true); - }, [background, hasStarted]); - - useAfterMountEffect(() => { - if (isFinalPage) { - posthog?.capture("volunteer.submitted"); - linkedInConversion("volunteer.submitted"); - if (email) posthog?.identify(posthog?.get_distinct_id(), { email }); - } else { - posthog?.capture("volunteer.partial", { - volunteerPage: pageIds[page], - background: background, - }); - } - }, [page]); - - useEffect(() => setHasSelection(false), [page]); - - async function onClickNext() { - // I wish i could set behavior: 'smooth' here but for some reason - // When i set that it stops working entirely??????????????????"??" - // Apparently you can fix it by modifying chrome flags but i dont want - // it to not work for people who are using the defaults - formRef.current!.scrollIntoView(); - if (hasSelection) { - if (background === "industry" && page === 0) { - // if industry, we want to skip region selection and get them in touch with - // labs team - navigate("last"); - } else if (page === pages.length - 2) { - // if submitting penultimate page, we now have all info - setIsSubmitting(true); - try { - const resp = await fetch("/api/applyAsVolunteer", { - method: "POST", - body: JSON.stringify({ - email, - firstName, - lastName, - linkedin, - region, - isOrganize, - background, - }), - headers: {}, - }); - if (!resp.ok) { - DEBUG(resp); - setSubmitError(`${resp.status}: ${resp.statusText}`); - } else { - if (after) window.location.href = after; - // Do not redirect if there is an error, as otherwise no indication would be shown to the user that their application was not recieved - } - navigate("next"); - } catch (ex: any) { - error(ex.toString()); - } - setIsSubmitting(false); - } else { - navigate("next"); - } - } - } - return ( - - {pages[page]} - {!isFinalPage && page !== 0 && ( - - - - )} - - ); -} diff --git a/apps/www/src/components/Volunteer/Wizard/BackgroundStep.tsx b/apps/www/src/components/Volunteer/Wizard/BackgroundStep.tsx new file mode 100644 index 0000000..a356438 --- /dev/null +++ b/apps/www/src/components/Volunteer/Wizard/BackgroundStep.tsx @@ -0,0 +1,39 @@ +import { Box, Button, Heading, HStack } from "@codeday/topo/Atom"; +import React from "react"; + +interface BackgroundStepProps { + onSelectStudent: () => void; + onSelectIndustry: () => void; + background: string; +} + +export default function BackgroundStep({ + onSelectStudent, + onSelectIndustry, + background, +}: BackgroundStepProps) { + return ( + + + Are you a student? + + + + + + + ); +} diff --git a/apps/www/src/components/Volunteer/Wizard/ConfirmationStep.tsx b/apps/www/src/components/Volunteer/Wizard/ConfirmationStep.tsx new file mode 100644 index 0000000..0468717 --- /dev/null +++ b/apps/www/src/components/Volunteer/Wizard/ConfirmationStep.tsx @@ -0,0 +1,32 @@ +import { Box, Text, Heading, Link } from "@codeday/topo/Atom"; +import React from "react"; + +interface ConfirmationStepProps { + submitError: string | false; +} + +export default function ConfirmationStep({ submitError }: ConfirmationStepProps) { + if (submitError) { + return ( + + + ☹️ An Error Ocurred + + + Please email volunteer@codeday.org with + your application, as well as the following error code: + + {submitError} + + ); + } + + return ( + + + ✅ Got it! + + We'll be in touch over email in the next few days! + + ); +} diff --git a/apps/www/src/components/Volunteer/Wizard/ContactStep.tsx b/apps/www/src/components/Volunteer/Wizard/ContactStep.tsx new file mode 100644 index 0000000..f46941c --- /dev/null +++ b/apps/www/src/components/Volunteer/Wizard/ContactStep.tsx @@ -0,0 +1,75 @@ +import { Box, Heading, VStack, TextInput } from "@codeday/topo/Atom"; +import { DataCollection } from "@codeday/topo/Molecule"; +import React from "react"; + +interface ContactStepProps { + firstName: string; + setFirstName: (v: string) => void; + lastName: string; + setLastName: (v: string) => void; + email: string; + setEmail: (v: string) => void; + linkedin: string; + setLinkedin: (v: string) => void; + background: string; + region: string; + resolvedStartPage: number; +} + +export default function ContactStep({ + firstName, + setFirstName, + lastName, + setLastName, + email, + setEmail, + linkedin, + setLinkedin, + background, + region, + resolvedStartPage, +}: ContactStepProps) { + return ( + + + {/*special logic if the only page user sees is contact info*/} + {resolvedStartPage === 2 + ? background === "industry" + ? "Apply to volunteer for CodeDay Labs" + : `Apply to volunteer for CodeDay ${region}` + : "Let us know how to reach out:"} + + + setFirstName(e.target.value)} + /> + setLastName(e.target.value)} + /> + setEmail(e.target.value)} + /> + {background === "industry" ? ( + { + setLinkedin(e.target.value); + }} + /> + ) : undefined} + + + + ); +} diff --git a/apps/www/src/components/Volunteer/Wizard/RegionStep.tsx b/apps/www/src/components/Volunteer/Wizard/RegionStep.tsx new file mode 100644 index 0000000..453328a --- /dev/null +++ b/apps/www/src/components/Volunteer/Wizard/RegionStep.tsx @@ -0,0 +1,127 @@ +import { + Box, + Button, + Text, + Heading, + TextInput, + Divider, + Checkbox, + Radio, +} from "@codeday/topo/Atom"; +import { Collapse } from "@codeday/topo/Molecule"; +import React from "react"; + +interface RegionStepProps { + regions: any[]; + regionsByCountry: Record; + region: string; + setRegion: (r: string) => void; + isOrganize: boolean; + setIsOrganize: (v: boolean) => void; + setHasSelection: (v: boolean) => void; + commitmentLevel: number; + setCommitmentLevel: (v: number) => void; +} + +export default function RegionStep({ + regions, + regionsByCountry, + region, + setRegion, + isOrganize, + setIsOrganize, + setHasSelection, + commitmentLevel, + setCommitmentLevel, +}: RegionStepProps) { + return ( + + + Please select a CodeDay City: + + { + // force "Other" to end of list (kind of hacky) + [ + ...Object.keys(regionsByCountry).filter((k) => k !== "Other"), + Object.keys(regionsByCountry).includes("Other") ? "Other" : undefined, + ].map((regionKey) => ( + + {/* Capitalize first letter of region (this is mostly to fix "the United States" looking weird) */} + + {regionKey?.charAt(0).toUpperCase()} + {regionKey?.substring(1)} + + {regionsByCountry[regionKey!]?.map((r) => ( + + { + setRegion(r.name); + setHasSelection(true); + setIsOrganize(false); + }} + > + {r.name} + + + ))} + + )) + } + + {/* Clear region state in case they clicked some other region button before this */} + + + + + + Interested in becoming a CodeDay Organizer? + + CodeDay events around the world are organized by students just like you! + + You'll manage a team, come up with cool ideas, and help hundreds of students in your + community discover CS. + + + (No prior event organizing experience is required, CodeDay Staff will support + guide + you every step of the way) + + + = 0} animateOpacity> + + setCommitmentLevel(e.target.checked ? 1 : 0)}> + I am interested in organizing a CodeDay in my city + + +
+
+ = 1} animateOpacity> + + What city/region would you like to organize an event in? + { + setRegion(e.target.value); + setHasSelection(true); + }} + w="md" + display="block" + /> + +
+
+
+
+
+
+ ); +} diff --git a/apps/www/src/components/Volunteer/Wizard/index.tsx b/apps/www/src/components/Volunteer/Wizard/index.tsx new file mode 100644 index 0000000..668f29b --- /dev/null +++ b/apps/www/src/components/Volunteer/Wizard/index.tsx @@ -0,0 +1,256 @@ +import { Box, Button } from "@codeday/topo/Atom"; +import { useToasts } from "@codeday/topo/utils"; +import { debug } from "@codeday/utils"; +import { usePostHog } from "@posthog/react"; +import React, { useState, useReducer, useEffect, RefObject } from "react"; + +import { useMarketing } from "../../../providers"; +import { useAfterMountEffect } from "../../../utils/useAfterMountEffect"; +import BackgroundStep from "./BackgroundStep"; +import ConfirmationStep from "./ConfirmationStep"; +import ContactStep from "./ContactStep"; +import RegionStep from "./RegionStep"; + +const DEBUG = debug(["www", "components", "Volunteer", "Wizard"]); + +// https://stackoverflow.com/a/48981669 +function groupBy(xs: any[], f: (x: any) => string): Record { + return xs.reduce( + (r: any, v: any, i: number, a: any[], k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), + {}, + ); +} + +const emailRe = new RegExp(".+@.+\\..+"); + +const PAGE_COUNT = 4; +const pageIds = ["background", "region", "email", "final"]; + +interface WizardProps { + events: any[]; + formRef: RefObject; + startBackground?: string; + startRegion?: string; + startPage?: number; + startSelection?: boolean; + after?: string; +} + +export default function Wizard({ + events, + formRef, + startBackground = "", + startRegion = "", + startPage = 0, + startSelection = false, + after, +}: WizardProps) { + const posthog = usePostHog(); + const { error } = useToasts(); + const regions = new Array( + ...new Set( + events + .filter((e) => !e.dontAcceptVolunteers) + .map((e) => + JSON.stringify({ + name: e.region?.name || e.name, + webname: e.contentfulWebname, + country: e.region?.countryName || "Other", + aliases: e.region?.aliases || [], + }), + ), + ), + ).map((e) => JSON.parse(e)); // json -> string -> json for deduplication + const regionsByCountry = groupBy(regions, (r) => r.country); + + useEffect(() => { + DEBUG("regions", regions); + DEBUG("regionsByCountry", regionsByCountry); + }, [regions, regionsByCountry]); + + let resolvedStartRegion = startRegion; + if (resolvedStartRegion) { + const webnamesToRegion: Record = {}; + regions.forEach((r) => { + webnamesToRegion[r.webname] = r.name; + r.aliases.forEach((alias: string) => { + webnamesToRegion[alias] = r.name; + }); + }); + resolvedStartRegion = webnamesToRegion[resolvedStartRegion] || ""; + } + let resolvedStartPage = startPage; + let resolvedStartBackground = startBackground; + let resolvedStartSelection = startSelection; + if (startRegion && !resolvedStartRegion) { + resolvedStartPage = 0; + resolvedStartBackground = ""; + resolvedStartSelection = false; + } + + const [background, setBackground] = useState(resolvedStartBackground); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [email, setEmail] = useState(""); + const [linkedin, setLinkedin] = useState(""); + const { linkedInConversion } = useMarketing(); + const [region, setRegion] = useState(resolvedStartRegion); + const [isOrganize, setIsOrganize] = useState(false); + const [commitmentLevel, setCommitmentLevel] = useState(0); + const [hasSelection, setHasSelection] = useState(resolvedStartSelection); + const [isSubmitting, setIsSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(false); + + useEffect(() => { + posthog?.group("background", background); + }, [background]); + + // 'last' should really be 'penultimate' but 'last' is shorter + const [page, navigate] = useReducer( + (prev: number, action: string) => + Math.max( + 0, + action === "next" ? prev + 1 : prev - 1, + action === "last" ? PAGE_COUNT - 2 : 0, + ), + resolvedStartPage, + ); + const isFinalPage = page === PAGE_COUNT - 1; + + useEffect(() => { + if ( + firstName && + lastName && + emailRe.test(email) && + (background === "industry" ? linkedin : true) + ) + setHasSelection(true); + else setHasSelection(false); + }, [firstName, lastName, email, linkedin]); + + const [hasStarted, setHasStarted] = useState(false); + useAfterMountEffect(() => { + if (!hasStarted) { + posthog?.capture("volunteer.started", { style: "full" }); + } + setHasStarted(true); + }, [background, hasStarted]); + + useAfterMountEffect(() => { + if (isFinalPage) { + posthog?.capture("volunteer.submitted"); + linkedInConversion("volunteer.submitted"); + if (email) posthog?.identify(posthog?.get_distinct_id(), { email }); + } else { + posthog?.capture("volunteer.partial", { + volunteerPage: pageIds[page], + background: background, + }); + } + }, [page]); + + useEffect(() => setHasSelection(false), [page]); + + async function onClickNext() { + // I wish i could set behavior: 'smooth' here but for some reason + // When i set that it stops working entirely??????????????????"??" + // Apparently you can fix it by modifying chrome flags but i dont want + // it to not work for people who are using the defaults + formRef.current!.scrollIntoView(); + if (hasSelection) { + if (background === "industry" && page === 0) { + // if industry, we want to skip region selection and get them in touch with + // labs team + navigate("last"); + } else if (page === PAGE_COUNT - 2) { + // if submitting penultimate page, we now have all info + setIsSubmitting(true); + try { + const resp = await fetch("/api/applyAsVolunteer", { + method: "POST", + body: JSON.stringify({ + email, + firstName, + lastName, + linkedin, + region, + isOrganize, + background, + }), + headers: {}, + }); + if (!resp.ok) { + DEBUG(resp); + setSubmitError(`${resp.status}: ${resp.statusText}`); + } else { + if (after) window.location.href = after; + // Do not redirect if there is an error, as otherwise no indication would be shown to the user that their application was not recieved + } + navigate("next"); + } catch (ex: any) { + error(ex.toString()); + } + setIsSubmitting(false); + } else { + navigate("next"); + } + } + } + + const pages = [ + { + setBackground("student"); + navigate("next"); + }} + onSelectIndustry={() => { + setBackground("industry"); + navigate("last"); + }} + />, + , + , + , + ]; + + return ( + + {pages[page]} + {!isFinalPage && page !== 0 && ( + + + + )} + + ); +} diff --git a/apps/www/src/pages/404.gql b/apps/www/src/pages/404.gql index 78a4c79..8b30b73 100644 --- a/apps/www/src/pages/404.gql +++ b/apps/www/src/pages/404.gql @@ -1,4 +1,4 @@ -#import "../components/Page.gql" +#import "../components/Page/Page.gql" query Error404Query { ...PageComponent } diff --git a/apps/www/src/pages/_app.tsx b/apps/www/src/pages/_app.tsx index b498198..333c0d1 100644 --- a/apps/www/src/pages/_app.tsx +++ b/apps/www/src/pages/_app.tsx @@ -1,4 +1,4 @@ -import { ThemeProvider } from "@codeday/topo/Theme"; +import { ThemeProvider, PageDataProvider } from "@codeday/topo/Theme"; import "react-responsive-modal/styles.css"; import { debug } from "@codeday/utils"; @@ -6,7 +6,6 @@ import { AppProps } from "next/app"; import { Fragment, StrictMode, useEffect } from "react"; import { MarketingProvider, FundraiseProvider } from "../providers"; -import { Provider } from "../query"; const DEBUG = debug(["www", "pages", "_app"]); const STRICT_MODE_OR_FRAGMENT = @@ -22,9 +21,9 @@ export default function App({ Component, pageProps }: AppProps) { - + - + diff --git a/apps/www/src/pages/contact.tsx b/apps/www/src/pages/contact.tsx index a397a5e..8e1ce21 100644 --- a/apps/www/src/pages/contact.tsx +++ b/apps/www/src/pages/contact.tsx @@ -12,7 +12,7 @@ import Employees from "../components/Contact/Employees"; import FullProfile from "../components/Contact/FullProfile"; import TextOnly from "../components/Contact/TextOnly"; import Page from "../components/Page"; -import { useQuery } from "../query"; +import { usePageData } from "@codeday/topo/Theme"; import { ContactQuery } from "./contact.gql"; function nl2br(str: string): string { @@ -32,7 +32,7 @@ export default function Home({ seed }: { seed: number }) { account: { employees, otherTeam, volunteers, board, contractors, emeritus, boardEmeritus }, labs, clear, - } = useQuery(); + } = usePageData(); const employeeIds = employees.map((e: any) => e.id); const otherIds = [...employees, ...otherTeam, ...board, ...contractors, ...emeritus].map( diff --git a/apps/www/src/pages/data.tsx b/apps/www/src/pages/data.tsx index 826c2e4..676b02e 100644 --- a/apps/www/src/pages/data.tsx +++ b/apps/www/src/pages/data.tsx @@ -6,12 +6,12 @@ import { DateTime } from "luxon"; import { GetStaticProps } from "next"; import Page from "../components/Page"; -import { useQuery } from "../query"; +import { usePageData } from "@codeday/topo/Theme"; import Error404 from "./404"; import { DataListPublicationsQuery } from "./data.gql"; export default function Home() { - const { cms } = useQuery(); + const { cms } = usePageData(); if (!cms) { return ( diff --git a/apps/www/src/pages/doi/[...doi]/index.tsx b/apps/www/src/pages/doi/[...doi]/index.tsx index f352bef..bb06f0a 100644 --- a/apps/www/src/pages/doi/[...doi]/index.tsx +++ b/apps/www/src/pages/doi/[...doi]/index.tsx @@ -30,7 +30,7 @@ import { useRouter } from "next/router"; import Markdown from "react-markdown"; import Page from "../../../components/Page"; -import { useQuery } from "../../../query"; +import { usePageData } from "@codeday/topo/Theme"; import Error404 from "../../404"; import { PublicationQuery, ListPublicationsQuery } from "./index.gql"; @@ -96,7 +96,7 @@ function FileIcon({ file }: FileIconProps) { } export default function Home() { - const { cms } = useQuery(); + const { cms } = usePageData(); const { query } = useRouter(); if (!cms) { diff --git a/apps/www/src/pages/doi/crossref/[...doi]/index.tsx b/apps/www/src/pages/doi/crossref/[...doi]/index.tsx index 455ae18..60756e2 100644 --- a/apps/www/src/pages/doi/crossref/[...doi]/index.tsx +++ b/apps/www/src/pages/doi/crossref/[...doi]/index.tsx @@ -10,7 +10,7 @@ import { useMemo } from "react"; import xmlbuilder from "xmlbuilder"; import Page from "../../../../components/Page"; -import { useQuery } from "../../../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { PublicationQuery, ListPublicationsQuery } from "../../[...doi]/index.gql"; function licenseToLink(license: string): string | undefined { @@ -206,7 +206,7 @@ function getCrossrefXml(publication: any, id: string): string { } export default function Crossref() { - const { cms } = useQuery(); + const { cms } = usePageData(); const { query } = useRouter(); const id = useMemo(() => `codeday-${Math.random().toString(36).substring(2, 8)}`, []); diff --git a/apps/www/src/pages/donate.tsx b/apps/www/src/pages/donate.tsx index 90f108a..a9bc22e 100644 --- a/apps/www/src/pages/donate.tsx +++ b/apps/www/src/pages/donate.tsx @@ -5,13 +5,13 @@ import { print } from "graphql"; import { GetStaticProps } from "next"; import Page from "../components/Page"; -import { useQuery } from "../query"; +import { usePageData } from "@codeday/topo/Theme"; import { DonateQuery } from "./donate.gql"; export default function Donate() { const { cms: { mission }, - } = useQuery(); + } = usePageData(); return ( diff --git a/apps/www/src/pages/e/[calendarId]/[eventId]/index.gql b/apps/www/src/pages/e/[calendarId]/[eventId]/index.gql index 7ca04bf..21e9481 100644 --- a/apps/www/src/pages/e/[calendarId]/[eventId]/index.gql +++ b/apps/www/src/pages/e/[calendarId]/[eventId]/index.gql @@ -1,4 +1,4 @@ -#import "../../../../components/EventInfo.gql" +#import "../../../../components/EventInfo/EventInfo.gql" query EventByIdQuery($calendarId: String!, $id: ID!) { calendar { diff --git a/apps/www/src/pages/eco.tsx b/apps/www/src/pages/eco.tsx index d8f1351..3ea3e10 100644 --- a/apps/www/src/pages/eco.tsx +++ b/apps/www/src/pages/eco.tsx @@ -1,17 +1,16 @@ import { Heading } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; +import { Content, ContentfulRichText } from "@codeday/topo/Molecule"; import { apiFetch } from "@codeday/topo/utils"; import { print } from "graphql"; import { GetStaticProps } from "next"; import React from "react"; -import ContentfulRichText from "../components/ContentfulRichText"; import Page from "../components/Page"; -import { useQuery } from "../query"; +import { usePageData } from "@codeday/topo/Theme"; import { EcoQuery } from "./eco.gql"; export default function Eco() { - const { details } = useQuery().cms; + const { details } = usePageData().cms; return ( diff --git a/apps/www/src/pages/f/[slug].tsx b/apps/www/src/pages/f/[slug].tsx index c7b4198..6f29257 100644 --- a/apps/www/src/pages/f/[slug].tsx +++ b/apps/www/src/pages/f/[slug].tsx @@ -1,19 +1,18 @@ import { Heading, Image, Skelly, Spinner, Box, Grid } from "@codeday/topo/Atom"; -import { Content, CognitoForm } from "@codeday/topo/Molecule"; +import { Content, CognitoForm, ContentfulRichText } from "@codeday/topo/Molecule"; import { apiFetch } from "@codeday/topo/utils"; import { print } from "graphql"; import { GetStaticProps, GetStaticPaths } from "next"; import { useRouter } from "next/router"; import React from "react"; -import ContentfulRichText from "../../components/ContentfulRichText"; import Page from "../../components/Page"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import Error404 from "../404"; import { FormQuery, ListFormsQuery } from "./form.gql"; export default function Home() { - const { cms } = useQuery(); + const { cms } = usePageData(); const { query } = useRouter(); if (!cms) { diff --git a/apps/www/src/pages/help/[program]/[audience].tsx b/apps/www/src/pages/help/[program]/[audience].tsx index 72f3085..fc42d1e 100644 --- a/apps/www/src/pages/help/[program]/[audience].tsx +++ b/apps/www/src/pages/help/[program]/[audience].tsx @@ -1,15 +1,14 @@ import { Box, Grid, Image, Text, Heading } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { Content, ContentfulRichText } from "@codeday/topo/Molecule"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import { apiFetch } from "@codeday/topo/utils"; import { UiX } from "@codeday/topocons"; import { print } from "graphql"; import { GetStaticProps, GetStaticPaths } from "next"; import React, { useState } from "react"; -import ContentfulRichText from "../../../components/ContentfulRichText"; import Page from "../../../components/Page"; -import { useQuery } from "../../../query"; + import { HelpProgramAudienceQuery, HelpProgramAudiencePathsQuery } from "./audience.gql"; interface AudienceProps { @@ -23,7 +22,7 @@ export default function Audience({ programWebname, audience }: AudienceProps) { if (!programWebname || !audience) return <>; - const { programs, faqs, events } = useQuery().cms || {}; + const { programs, faqs, events } = usePageData().cms || {}; const program = programs?.items[0] || null; const photos = diff --git a/apps/www/src/pages/help/[program]/index.tsx b/apps/www/src/pages/help/[program]/index.tsx index c0e2492..bdb263c 100644 --- a/apps/www/src/pages/help/[program]/index.tsx +++ b/apps/www/src/pages/help/[program]/index.tsx @@ -18,7 +18,7 @@ import { GetStaticProps, GetStaticPaths } from "next"; import React from "react"; import Page from "../../../components/Page"; -import { useQuery } from "../../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { HelpProgramIndexQuery, HelpProgramIndexPathsQuery } from "./index.gql"; const icons: Record = { @@ -34,7 +34,7 @@ interface ProgramProps { } export default function Program({ programWebname }: ProgramProps) { - const { programs, faqs, events } = useQuery().cms || {}; + const { programs, faqs, events } = usePageData().cms || {}; if (!programWebname) return <>; diff --git a/apps/www/src/pages/help/article/[article].tsx b/apps/www/src/pages/help/article/[article].tsx index 86f5709..73479a6 100644 --- a/apps/www/src/pages/help/article/[article].tsx +++ b/apps/www/src/pages/help/article/[article].tsx @@ -1,5 +1,5 @@ import { Text, Heading, Link, Box, List, ListItem } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; +import { Content, ContentfulRichText } from "@codeday/topo/Molecule"; import { apiFetch } from "@codeday/topo/utils"; import { UiArrowRight } from "@codeday/topocons"; import { print } from "graphql"; @@ -7,7 +7,6 @@ import { DateTime } from "luxon"; import { GetStaticProps, GetStaticPaths } from "next"; import React from "react"; -import ContentfulRichText from "../../../components/ContentfulRichText"; import Page from "../../../components/Page"; import { HelpArticleQuery, HelpArticlePathsQuery } from "./article.gql"; diff --git a/apps/www/src/pages/help/index.tsx b/apps/www/src/pages/help/index.tsx index 60897c7..22a86bb 100644 --- a/apps/www/src/pages/help/index.tsx +++ b/apps/www/src/pages/help/index.tsx @@ -12,11 +12,11 @@ import { GetStaticProps } from "next"; import React from "react"; import Page from "../../components/Page"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { HelpIndexQuery } from "./index.gql"; export default function Help() { - const { programs } = useQuery().cms || {}; + const { programs } = usePageData().cms || {}; const programsWithFaqs = programs?.items?.filter((p: any) => p.linkedFrom?.faqs?.items?.length > 0) || []; diff --git a/apps/www/src/pages/index.gql b/apps/www/src/pages/index.gql index 0399cca..781bd39 100644 --- a/apps/www/src/pages/index.gql +++ b/apps/www/src/pages/index.gql @@ -1,7 +1,7 @@ -#import "../components/Page.gql" +#import "../components/Page/Page.gql" #import "../components/Index/Hero.gql" #import "../components/Index/Stats.gql" -#import "../components/Index/Programs.gql" +#import "../components/Index/Programs/Programs.gql" #import "../components/Index/Sponsors.gql" #import "../components/Index/Announcement.gql" #import "../components/Index/Community.gql" diff --git a/apps/www/src/pages/legal/[policy].tsx b/apps/www/src/pages/legal/[policy].tsx index e98718c..fa3891f 100644 --- a/apps/www/src/pages/legal/[policy].tsx +++ b/apps/www/src/pages/legal/[policy].tsx @@ -5,9 +5,9 @@ import { print } from "graphql"; import { GetStaticProps, GetStaticPaths } from "next"; import React from "react"; -import Markdown from "../../components/Markdown"; +import { Markdown } from "@codeday/topo/Molecule"; import Page from "../../components/Page"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { LegalPathsQuery, LegalContentQuery, TermageddonLegalContentQuery } from "./policy.gql"; const TERMAGEDDON_POLICIES = ["tos", "privacy", "cookies", "disclaimer"]; @@ -17,7 +17,7 @@ interface PolicyProps { } export default function Policy({ slug }: PolicyProps) { - const { termageddon, notion } = useQuery(); + const { termageddon, notion } = usePageData(); const page = TERMAGEDDON_POLICIES.includes(slug) ? { content: termageddon.terms[slug], diff --git a/apps/www/src/pages/m/[...slug].tsx b/apps/www/src/pages/m/[...slug].tsx index 4aded6f..187bdfb 100644 --- a/apps/www/src/pages/m/[...slug].tsx +++ b/apps/www/src/pages/m/[...slug].tsx @@ -3,7 +3,7 @@ import { Content } from "@codeday/topo/Molecule"; import { GetStaticProps, GetStaticPaths } from "next"; import React from "react"; -import Calendly from "../../components/Calendly"; +import { CalendlyEmbed as Calendly } from "@codeday/topo/Molecule"; import Page from "../../components/Page"; interface CalendlyPageProps { diff --git a/apps/www/src/pages/press.tsx b/apps/www/src/pages/press.tsx index 169609f..45ac347 100644 --- a/apps/www/src/pages/press.tsx +++ b/apps/www/src/pages/press.tsx @@ -1,16 +1,15 @@ import { Box, Flex, Grid, Text, Heading, Link, Button, Image } from "@codeday/topo/Atom"; -import { Content } from "@codeday/topo/Molecule"; +import { Content, ContentfulRichText } from "@codeday/topo/Molecule"; import { apiFetch } from "@codeday/topo/utils"; import { print } from "graphql"; import { DateTime } from "luxon"; import { GetStaticProps } from "next"; import React from "react"; -import ContentfulRichText from "../components/ContentfulRichText"; import Page from "../components/Page"; import PhotoGallery from "../components/Press/PhotoGallery"; import PreviousCoverageLogos from "../components/PreviousCoverageLogos"; -import { useQuery } from "../query"; +import { usePageData } from "@codeday/topo/Theme"; import { PressQuery } from "./press.gql"; interface PressProps { @@ -20,7 +19,7 @@ interface PressProps { export default function Press({ seed }: PressProps) { const { cms: { mission, pressContact, pressDetails, programs, previousCoverage }, - } = useQuery(); + } = usePageData(); return ( diff --git a/apps/www/src/pages/volunteer/index.tsx b/apps/www/src/pages/volunteer/index.tsx index c381d58..fd8e16c 100644 --- a/apps/www/src/pages/volunteer/index.tsx +++ b/apps/www/src/pages/volunteer/index.tsx @@ -1,6 +1,6 @@ import { Box, Grid, Text, Heading, Link, Button, Divider } from "@codeday/topo/Atom"; import { Content } from "@codeday/topo/Molecule"; -import { useColorMode } from "@codeday/topo/Theme"; +import { useColorMode, usePageData } from "@codeday/topo/Theme"; import { apiFetch } from "@codeday/topo/utils"; import { print } from "graphql"; import { DateTime } from "luxon"; @@ -9,14 +9,14 @@ import { NextSeo } from "next-seo"; import { useRouter } from "next/router"; import React, { useState, useRef } from "react"; -import Highlight from "../../components/Highlight"; +import { Highlight } from "@codeday/topo/Atom"; import Page from "../../components/Page"; import PhotoGallery from "../../components/Volunteer/PhotoGallery"; import PreviewVideo from "../../components/Volunteer/PreviewVideo"; import RemindMe from "../../components/Volunteer/RemindMe"; import Testimonials from "../../components/Volunteer/Testimonials"; import Wizard from "../../components/Volunteer/Wizard"; -import { useQuery } from "../../query"; + import { VolunteerQuery } from "./volunteer.gql"; interface VolunteerProps { @@ -37,7 +37,7 @@ export default function Volunteer({ const formRef = useRef(null); const { colorMode } = useColorMode(); const { asPath, query } = useRouter(); - const { clear } = useQuery(); + const { clear } = usePageData(); const [wizardVisible, setWizardVisible] = useState(false); const secondText = ( diff --git a/apps/www/src/pages/volunteer/share.tsx b/apps/www/src/pages/volunteer/share.tsx index f24aa40..2d84e27 100644 --- a/apps/www/src/pages/volunteer/share.tsx +++ b/apps/www/src/pages/volunteer/share.tsx @@ -8,7 +8,7 @@ import React from "react"; import Page from "../../components/Page"; import VideoTestimonialThumbnail from "../../components/VideoTestimonialThumbnail"; import ProgramInfo from "../../components/Volunteer/ProgramInfo"; -import { useQuery } from "../../query"; +import { usePageData } from "@codeday/topo/Theme"; import { upcomingEvents } from "../../utils/time"; import { VolunteerQuery } from "./volunteer.gql"; @@ -17,7 +17,7 @@ const PROGRAM_WEIGHT = ["primary", "secondary", "minor"]; export default function Volunteer() { const { cms: { volunteerPrograms, testimonials }, - } = useQuery(); + } = usePageData(); const programsWithUpcoming = volunteerPrograms?.items ?.map((program: any) => { diff --git a/apps/www/src/providers/Fundraise.tsx b/apps/www/src/providers/Fundraise.tsx index 4463b3b..c61bd8a 100644 --- a/apps/www/src/providers/Fundraise.tsx +++ b/apps/www/src/providers/Fundraise.tsx @@ -4,6 +4,7 @@ import Head from "next/head"; import Script from "next/script"; import { createContext, useContext, useEffect, useState, ReactNode } from "react"; const DEBUG = debug(["www", "providers", "Fundraise"]); +const FUNDRAISE_UP_ID = process.env.NEXT_PUBLIC_FUNDRAISE_UP_ID || "AHCSATYN"; interface FundraiseContextType { isFundraiseLoaded: boolean; @@ -47,12 +48,12 @@ export function FundraiseProvider({ children }: { children: ReactNode }) { - +