Merge remote-tracking branch 'origin/main' into feat/events-page

pull/159/head
Aditya Thakral 2 years ago
commit f3d9a08a73
  1. 17
      .drone.yml
  2. 13
      .vscode/settings.json
  3. 10
      check-lockfile.js
  4. 68
      components/Bubble.module.css
  5. 17
      components/Bubble.tsx
  6. 6
      components/Code.module.css
  7. 9
      components/Code.tsx
  8. 4
      components/ConnectWithUs.tsx
  9. 7
      components/EmailSignup.module.css
  10. 21
      components/EmailSignup.tsx
  11. 5
      components/Footer.module.css
  12. 6
      components/Footer.tsx
  13. 7
      components/HorizontalLine.module.css
  14. 7
      components/HorizontalLine.tsx
  15. 4
      components/Image.tsx
  16. 8
      components/MiniTechTalkCard.module.css
  17. 36
      components/MiniTechTalkCard.tsx
  18. 12
      components/Navbar.tsx
  19. 133
      components/OrganizedContent.module.css
  20. 306
      components/OrganizedContent.tsx
  21. 50
      components/OrganizedContent/Header.module.css
  22. 38
      components/OrganizedContent/Header.tsx
  23. 84
      components/OrganizedContent/ReadAll.tsx
  24. 54
      components/OrganizedContent/Section.tsx
  25. 69
      components/OrganizedContent/static.ts
  26. 6
      components/Pre.module.css
  27. 9
      components/Pre.tsx
  28. 20
      components/ShapesBackground.module.css
  29. 308
      components/ShapesBackground.tsx
  30. 13
      components/TeamMember.module.css
  31. 8
      components/TeamMember.tsx
  32. 122
      components/TeamMemberCard.module.css
  33. 93
      components/TeamMemberCard.tsx
  34. 22
      components/TechTalkCard.module.css
  35. 43
      components/TechTalkCard.tsx
  36. 5
      components/Theme.tsx
  37. 82
      components/playground.tsx
  38. 8
      content/about/code-of-conduct/additional-information.md
  39. 9
      content/about/code-of-conduct/addressing-grievances.md
  40. 13
      content/about/code-of-conduct/confidentiality.md
  41. 13
      content/about/code-of-conduct/consequences-of-inappropriate-behaviour.md
  42. 6
      content/about/code-of-conduct/contact-information.md
  43. 10
      content/about/code-of-conduct/expected-behaviour.md
  44. 19
      content/about/code-of-conduct/experiencing-unacceptable-behaviour.md
  45. 6
      content/about/code-of-conduct/license-information-and-attribution.md
  46. 13
      content/about/code-of-conduct/purpose.md
  47. 6
      content/about/code-of-conduct/revision.md
  48. 12
      content/about/code-of-conduct/scope-and-spaces.md
  49. 16
      content/about/code-of-conduct/unacceptable-behaviour.md
  50. 10
      content/about/constitution/amendments-and-procedures.md
  51. 8
      content/about/constitution/code-of-conduct.md
  52. 27
      content/about/constitution/committees.md
  53. 6
      content/about/constitution/dissolution.md
  54. 27
      content/about/constitution/duties-of-officers.md
  55. 10
      content/about/constitution/executive-council.md
  56. 9
      content/about/constitution/finances.md
  57. 13
      content/about/constitution/meetings.md
  58. 10
      content/about/constitution/membership.md
  59. 6
      content/about/constitution/name.md
  60. 30
      content/about/constitution/officers.md
  61. 10
      content/about/constitution/purpose.md
  62. 6
      content/about/constitution/revision.md
  63. 10
      content/about/constitution/use-of-club-resources.md
  64. 16
      content/about/index.mdx
  65. 16
      content/advice/academic-advice.mdx
  66. 98
      content/advice/coop-advice.mdx
  67. 42
      content/advice/misc-advice.mdx
  68. 8
      content/get-involved.mdx
  69. 6
      content/meet-the-team/execs/01-kallen-tu.md
  70. 6
      content/meet-the-team/execs/02-gordon-le.md
  71. 6
      content/meet-the-team/execs/03-nakul-vijhani.md
  72. 6
      content/meet-the-team/execs/04-neil-parikh.md
  73. 6
      content/meet-the-team/execs/05-max-erenberg.md
  74. 6
      content/meet-the-team/execs/06-codey.md
  75. 114
      content/meet-the-team/programme-committee.json
  76. 18
      content/meet-the-team/systems-committee.json
  77. 38
      content/meet-the-team/website-committee.json
  78. 6
      content/news/2002/fall/2002-09-16-sjdutoit.md
  79. 6
      content/news/2002/fall/2002-09-16-sjdutoit.news.md
  80. 4
      content/news/2002/fall/2002-09-18-sjdutoit.md
  81. 4
      content/news/2002/fall/2002-09-30-sjdutoit.md
  82. 4
      content/news/2002/fall/2002-10-29-sjdutoit.md
  83. 6
      content/news/2002/winter/2002-02-03-sjdutoit.md
  84. 6
      content/news/2002/winter/2002-02-03-sjdutoit.news.md
  85. 6
      content/news/2002/winter/2002-02-04-sjdutoit.md
  86. 6
      content/news/2002/winter/2002-02-04-sjdutoit.news.md
  87. 4
      content/news/2002/winter/2002-04-15-sjdutoit.md
  88. 4
      content/news/2002/winter/2002-04-22-sjdutoit.md
  89. 6
      content/news/2003/fall/2003-09-17-sfllaw.md
  90. 6
      content/news/2003/fall/2003-09-17-sfllaw.news.md
  91. 6
      content/news/2003/spring/2003-05-14-sjdutoit.md
  92. 6
      content/news/2003/spring/2003-05-14-sjdutoit.news.md
  93. 4
      content/news/2003/spring/2003-06-05-sfllaw.md
  94. 6
      content/news/2003/spring/2003-07-03-sfllaw.md
  95. 6
      content/news/2003/spring/2003-07-03-sfllaw.news.md
  96. 4
      content/news/2003/spring/2003-07-09-ja2morri.md
  97. 4
      content/news/2003/spring/2003-08-06-ja2morri.md
  98. 4
      content/news/2003/spring/2003-08-12-ja2morri.md
  99. 6
      content/news/2003/winter/2003-01-13-sfllaw.md
  100. 6
      content/news/2003/winter/2003-01-13-sfllaw.news.md
  101. Some files were not shown because too many files have changed in this diff Show More

@ -4,8 +4,15 @@ type: docker
name: node16
steps:
- name: check-lockfile
image: node:16
commands:
- node ./check-lockfile.js
- name: install-deps
image: node:16
depends_on:
- check-lockfile
commands:
- npm install
@ -30,6 +37,16 @@ steps:
commands:
- npm run export
- name: deploy (staging)
image: node:16
depends_on:
- export
environment:
TOKEN:
from_secret: STAGING_TOKEN
commands:
- 'curl -XPOST -H "Authorization: $TOKEN" -H "X-Branch: $DRONE_BRANCH" "https://csclub.uwaterloo.ca/~a3thakra/update-csc/"'
trigger:
event:
exclude:

@ -4,9 +4,7 @@
"eslint.codeActionsOnSave.mode": "all",
"[css]": {
"editor.suggest.insertMode": "replace",
"gitlens.codeLens.scopes": [
"document"
],
"gitlens.codeLens.scopes": ["document"],
"editor.formatOnSave": true
},
"[javascript]": {
@ -38,5 +36,10 @@
"files.exclude": {
"node_modules": true
},
"editor.tabSize": 2
}
"editor.tabSize": 2,
"files.eol": "\n",
"[markdown]": {
"editor.wordWrap": "on",
"editor.quickSuggestions": false
}
}

@ -0,0 +1,10 @@
const lockfile = require('./package-lock.json')
if (lockfile.lockfileVersion !== 2) {
console.error(`
Please upgrade to npm v7 and revert changes to the lockfile.
- \`npm i -g npm\` to upgrade.
`.trim())
process.exit(1)
}

@ -1,7 +1,71 @@
.bubble {
.container {
position: relative;
padding: calc(36rem / 16) 0;
overflow-x: clip;
}
.bubble {
--border-radius: calc(5000rem / 16);
display: flex;
flex-direction: row;
position: absolute;
top: 0;
height: 100%;
width: 100%;
justify-content: center;
align-items: flex-start;
}
.bubble:nth-child(odd) {
.bar {
height: 100%;
width: 100%;
}
.decoration {
display: none;
width: calc(128rem / 16);
}
.margin {
display: none;
}
.content {
position: relative;
z-index: 1;
}
.container:nth-child(odd) .bar {
background-color: var(--primary-accent-light);
}
@media only screen and (min-width: calc(1350rem / 16)) {
.container:nth-child(odd) .decoration {
display: block;
position: absolute;
}
.container:nth-child(4n + 1) .bar {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
transform: translateX(-12.5vw);
}
.container:nth-child(4n + 1) .decoration {
top: calc(-50rem / 16);
right: 8vw;
}
.container:nth-child(4n + 3) .bar {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
transform: translateX(12.5vw);
}
.container:nth-child(4n + 3) .decoration {
top: calc(-50rem / 16);
left: 8vw;
transform: rotateY(180deg);
}
}

@ -1,13 +1,24 @@
import React from "react";
import { Image } from "@/components/Image";
import { DefaultLayout } from "./DefaultLayout";
import styles from "./Bubble.module.css";
export default function Bubble(props: { children: React.ReactNode }) {
export function Bubble(props: { children: React.ReactNode }) {
return (
<div className={styles.bubble}>
<DefaultLayout>{props.children}</DefaultLayout>
<div className={styles.container}>
<div className={styles.bubble} aria-hidden>
<div className={styles.bar} />
<Image
className={styles.decoration}
src="/images/bubble-decoration.svg"
/>
</div>
<div className={styles.content}>
<DefaultLayout>{props.children}</DefaultLayout>
</div>
</div>
);
}

@ -0,0 +1,6 @@
.code {
padding: 0 calc(4rem / 16);
background: var(--code-background);
border-radius: calc(5rem / 16);
word-wrap: break-word;
}

@ -0,0 +1,9 @@
import React, { HTMLAttributes } from "react";
import styles from "./Code.module.css";
export function Code(props: HTMLAttributes<HTMLElement>) {
const classes = [styles.code, props.className ?? ""];
return <code {...props} className={classes.join(" ")} />;
}

@ -18,9 +18,9 @@ export function ConnectWithUs() {
<SocialLinks color="gradient" size="big" />
</div>
{/* TODO: fix feedback form link */}
<p>
Send feedback through our <Link href="#">Feedback Form</Link>
Send feedback through our{" "}
<Link href="https://bit.ly/uwcsclub-feedback-form">Feedback Form</Link>
</p>
</section>
);

@ -1,7 +1,3 @@
.container form {
box-sizing: border-box;
}
.header {
color: var(--primary-accent);
font-weight: 600;
@ -9,8 +5,9 @@
}
.button {
margin-top: calc(34rem / 16);
margin-top: calc(26rem / 16);
display: block;
width: fit-content;
}
@media only screen and (max-width: calc(768rem / 16)) {

@ -1,21 +1,24 @@
import React from "react";
import { Button } from "./Button";
import { Input } from "./Input";
import styles from "./EmailSignup.module.css";
export function EmailSignup() {
return (
<section className={styles.container}>
<h1 className={styles.header}>Join Our Mailing List!</h1>
<form className={styles.form} action="">
<Input type="text" placeholder="Full Name*" required />
<Input type="email" placeholder="Email*" required />
<Button type="submit" className={styles.button}>
Subscribe
</Button>
</form>
<h1 className={styles.header}>Join our mailing list!</h1>
<p>
Join our mailing list to receive email notifications about important
news and upcoming events!
</p>
<Button
isLink={true}
href="https://mailman.csclub.uwaterloo.ca/postorius/lists/csc-general.csclub.uwaterloo.ca/"
className={styles.button}
>
Subscribe
</Button>
</section>
);
}

@ -23,6 +23,11 @@
text-align: center;
}
.email {
color: unset;
text-decoration: unset;
}
@media only screen and (max-width: calc(768rem / 16)) {
.footer {
height: calc(120rem / 16);

@ -1,3 +1,4 @@
import Link from "next/link";
import React from "react";
import { SocialLinks } from "./SocialLinks";
@ -9,7 +10,10 @@ export function Footer() {
<footer className={styles.footer}>
<div className={styles.container}>
<div className={styles.text}>
Have questions? Email us at XX@XXX.COM
Have questions? Email us at{" "}
<Link href="mailto:exec@csclub.uwaterloo.ca">
<a className={styles.email}>exec@csclub.uwaterloo.ca</a>
</Link>
</div>
<SocialLinks color="white" size="small" />
</div>

@ -0,0 +1,7 @@
.line {
display: block;
margin: calc(34rem / 16) 0;
height: calc(1rem / 16);
border: none;
background-color: var(--primary-heading);
}

@ -0,0 +1,7 @@
import React from "react";
import styles from "./HorizontalLine.module.css";
export function HorizontalLine() {
return <hr className={styles.line} />;
}

@ -1,6 +1,10 @@
import React, { ImgHTMLAttributes } from "react";
export function Image(props: ImgHTMLAttributes<HTMLImageElement>) {
if (props.src?.startsWith("http://") || props.src?.startsWith("https://")) {
return <img {...props} />;
}
const { src: relativeSrc = "" } = props;
let absoluteSrc = process.env.NEXT_PUBLIC_BASE_PATH ?? "/";

@ -1,15 +1,16 @@
.card {
.card > a {
display: flex;
flex-direction: row;
box-sizing: border-box;
padding: calc(16rem / 16);
color: var(--purple-2);
font-size: 1rem;
color: inherit;
text-decoration: inherit;
}
.card aside {
max-width: calc(142rem / 16);
margin-right: calc(45rem / 16);
margin-right: 1rem;
display: flex;
justify-content: center;
align-items: center;
@ -29,6 +30,7 @@
margin: 0;
margin-top: calc(4rem / 16);
font-size: calc(18rem / 16);
color: var(--primary-heading);
}
.card section {

@ -1,3 +1,4 @@
import Link from "next/link";
import React from "react";
import { Image } from "./Image";
@ -5,19 +6,36 @@ import { Image } from "./Image";
import styles from "./MiniTechTalkCard.module.css";
interface MiniTechTalkProps {
name: string;
short: string;
poster?: string;
slug: string;
title: string;
presentors: string[];
poster: string;
}
export function MiniTechTalkCard({ name, poster, short }: MiniTechTalkProps) {
export function MiniTechTalkCard({
slug,
title,
presentors,
poster,
}: MiniTechTalkProps) {
const presentorsStr = presentors.join(", ");
return (
<article className={styles.card}>
<aside>{poster && <Image alt={name} src={poster} />}</aside>
<div className={styles.content}>
<h1>{name}</h1>
<p>{short}</p>
</div>
<Link href={`/resources/tech-talks/${slug}`}>
<a>
<aside>
<Image
alt={`Thumbnail of tech talk by ${presentorsStr}: ${title}`}
src={poster}
/>
</aside>
<div className={styles.content}>
<h1>{title}</h1>
<p>{presentorsStr}</p>
</div>
</a>
</Link>
</article>
);
}

@ -62,6 +62,10 @@ const menu: Menu = [
name: "Services",
route: "/resources/services",
},
{
name: "Machine Usage",
route: "/resources/machine-usage-agreement",
},
{
name: "Tech Talks",
route: "/resources/tech-talks",
@ -70,6 +74,14 @@ const menu: Menu = [
name: "CS Club Wiki",
route: "https://wiki.csclub.uwaterloo.ca",
},
{
name: "Advice",
route: "/resources/advice/coop",
},
{
name: "Internships",
route: "https://github.com/uwcsc/winter2022-internships",
},
],
},
];

@ -2,20 +2,28 @@
display: flex;
}
.wrapper h1 {
margin: 1rem 0;
font-size: calc(24rem / 16);
font-weight: 600;
color: var(--primary-accent);
}
.content {
display: flex;
flex-direction: column;
width: 100%;
}
.content h1 {
font-size: calc(24rem / 16);
color: var(--primary-accent);
}
.content h2,
.content h3 {
font-size: calc(18rem / 16);
margin-top: calc(24rem / 16);
margin-bottom: calc(16rem / 16);
}
.nav {
top: calc(20rem / 16);
position: sticky;
height: 100%;
margin: calc(8rem / 16) calc(32rem / 16) 0 0;
color: var(--primary-heading);
font-weight: 500;
@ -29,7 +37,7 @@
border-bottom: calc(1rem / 16) solid var(--primary-accent-light);
align-items: center;
height: calc(40rem / 16);
width: calc(284rem / 16);
width: calc(200rem / 16);
padding: 0 calc(14rem / 16);
cursor: pointer;
}
@ -41,7 +49,7 @@
}
.selected {
background-color: var(--primary-accent-dim);
background-color: var(--primary-accent-lightest);
color: var(--primary-accent);
font-weight: 700;
}
@ -110,8 +118,113 @@
padding-left: calc(8rem / 16);
}
.link,
.link:visited {
color: inherit;
text-decoration: none;
}
.burger {
display: none;
}
.mobileNavTitle {
display: none;
}
@media only screen and (max-width: calc(768rem / 16)) {
.burger {
display: flex;
position: fixed;
border: none;
bottom: calc(32rem / 16);
left: calc(16rem / 16);
width: calc(62rem / 16);
height: calc(57rem / 16);
cursor: pointer;
z-index: 9;
background: var(--primary-accent-light);
border-radius: calc(5rem / 16);
transition: transform 0.3s ease-in-out;
transform: translateY(calc(94rem / 16));
padding: calc(11rem / 16) calc(9rem / 16);
}
.burgerVisible {
transform: translateY(0);
}
.burger > svg {
width: 100%;
height: 100%;
stroke: var(--primary-accent);
}
.navItem {
width: auto;
padding: 0 calc(16rem / 16);
}
.content h1 {
font-size: calc(18rem / 16);
}
.content h2,
.content h3,
.content h4 {
font-size: calc(18rem / 16);
margin-top: calc(24rem / 16);
margin-bottom: calc(8rem / 16);
}
.nav {
display: none;
position: fixed;
top: 0;
left: 0;
overflow-y: auto;
z-index: 30;
margin: 0;
background: var(--primary-accent-lighter);
width: calc(288rem / 16);
transform: translateX(-100vw);
transition: 0.5s;
}
.navMobileBackground {
position: fixed;
visibility: hidden;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 20;
background-color: var(--navbar-page-overlay);
opacity: 0;
transition: 0.5s;
}
.mobileNavOpen {
transform: translateX(0);
}
.mobileNavTitle {
display: flex;
font-size: calc(24rem / 16);
font-weight: 700;
margin: calc(14rem / 16);
margin-top: calc(39rem / 16);
}
.show.navMobileBackground {
visibility: visible;
opacity: 100%;
}
}

@ -1,19 +1,20 @@
import React, { ReactNode, ComponentType } from "react";
import NextLink from "next/link";
import React, {
ReactNode,
ComponentType,
useState,
useRef,
useEffect,
useCallback,
} from "react";
import styles from "./OrganizedContent.module.css";
export interface LinkProps {
className?: string;
id: string;
children: ReactNode;
}
type Link = ComponentType<LinkProps>;
interface Section {
id: string;
title: string;
Content: ComponentType;
}
const READ_ALL_TITLE = "Read All";
@ -21,41 +22,83 @@ export const READ_ALL_ID = "read-all";
interface Props {
sections: Section[];
currentId: string;
id: string;
children: ReactNode;
pageTitle: string;
link: Link;
}
export function OrganizedContent(props: Props) {
const sections = createSections(props.sections);
const currentIndex = sections.findIndex(({ id }) => id === props.currentId);
export function OrganizedContent({
sections,
id,
children,
pageTitle,
link: Link,
}: Props) {
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const currentIndex = sections.findIndex(
({ id: sectionId }) => sectionId === id
);
if (currentIndex < 0) {
throw new Error(`Section with ID ${props.currentId} was not found`);
throw new Error(`Section with ID ${id} was not found`);
}
const section = sections[currentIndex];
const isReadAll = section.id === READ_ALL_ID;
const ref = useRef<HTMLDivElement>(null);
const isVisible = useOnScreen(ref.current);
const burgerVisible = useBurger(isVisible);
useEffect(() => {
mobileNavOpen
? (document.body.style.overflow = "hidden")
: (document.body.style.overflow = "visible");
}, [mobileNavOpen]);
return (
<div className={styles.wrapper}>
<Nav sections={sections} currentIndex={currentIndex} link={props.link} />
<div className={styles.wrapper} ref={ref}>
<div
className={
mobileNavOpen
? `${styles.navMobileBackground} ${styles.show}`
: styles.navMobileBackground
}
onClick={() => setMobileNavOpen(false)}
/>
<Nav
sections={sections}
currentIndex={currentIndex}
link={Link}
pageTitle={pageTitle}
mobileNavOpen={mobileNavOpen}
setMobileNavOpen={setMobileNavOpen}
/>
<div className={styles.content}>
{isReadAll ? (
<section.Content />
children
) : (
<>
<div>
<section>
<h1>{section.title}</h1>
<section.Content />
</div>
{children}
</section>
<Footer
sections={sections}
currentIndex={currentIndex}
link={props.link}
link={Link}
/>
</>
)}
</div>
<button
className={`${styles.burger} ${
burgerVisible ? styles.burgerVisible : ""
}`}
onClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<Burger />
</button>
</div>
);
}
@ -64,11 +107,29 @@ interface NavProps {
sections: Section[];
currentIndex: number;
link: Link;
pageTitle: string;
mobileNavOpen: boolean;
setMobileNavOpen: (mobileNavOpen: boolean) => void;
}
function Nav({ sections, currentIndex, link: Link }: NavProps) {
function Nav({
sections,
currentIndex,
link: Link,
pageTitle,
mobileNavOpen,
setMobileNavOpen,
}: NavProps) {
const navStyles = mobileNavOpen
? [styles.nav, styles.mobileNavOpen]
: [styles.nav];
return (
<div className={styles.nav}>
<nav
className={navStyles.join(" ")}
onClick={(event) => event.stopPropagation()}
>
<h1 className={styles.mobileNavTitle}>{pageTitle}</h1>
{sections.map((section, index) => {
const classNames = [styles.navItem];
@ -81,17 +142,20 @@ function Nav({ sections, currentIndex, link: Link }: NavProps) {
}
return (
<Link
className={classNames.join(" ")}
id={section.id}
<div
onClick={() => {
setMobileNavOpen(false);
}}
key={section.id}
>
<span className={styles.marker} />
<div>{section.title}</div>
</Link>
<Link className={classNames.join(" ")} id={section.id}>
<span className={styles.marker} />
<div>{section.title}</div>
</Link>
</div>
);
})}
</div>
</nav>
);
}
@ -136,26 +200,82 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) {
);
}
function createSections(sections: Section[]) {
return [
{
id: READ_ALL_ID,
title: READ_ALL_TITLE,
Content() {
return (
<>
{sections.map(({ id, title, Content: SectionContent }) => (
<div key={id}>
<h1>{title}</h1>
<SectionContent />
</div>
))}
</>
);
},
},
...sections,
];
function useDebounce(func: () => void, delay = 300) {
const timerRef = useRef<number | undefined>(undefined);
return useCallback(() => {
if (timerRef.current != null) {
return;
}
timerRef.current = window.setTimeout(() => {
func();
timerRef.current = undefined;
}, delay);
}, [func, delay]);
}
export interface SectionWithContent {
section: Section;
Content: ComponentType;
}
export function createReadAllSection(
sections: Section[],
content: false
): Section;
export function createReadAllSection(
sections: SectionWithContent[],
content: true
): SectionWithContent;
export function createReadAllSection(
sections: SectionWithContent[] | Section[],
content = true
): SectionWithContent | Section {
const readAllSection = {
id: READ_ALL_ID,
title: READ_ALL_TITLE,
};
return content
? {
section: readAllSection,
Content: function ReadAllContent() {
return (
<>
{(sections as SectionWithContent[]).map(
({ section: { id, title }, Content }) => (
<section key={id}>
<h1>{title}</h1>
<Content />
</section>
)
)}
</>
);
},
}
: readAllSection;
}
export interface LinkProps {
className?: string;
id: string;
children: ReactNode;
}
export function createLink(page: string) {
let base = page.startsWith("/") ? page : `/${page}`;
base = base.endsWith("/") ? base : `${base}/`;
return function Link({ className, id, children }: LinkProps) {
const href = id === READ_ALL_ID ? base : base + id;
return (
<NextLink href={href}>
<a className={`${styles.link} ${className ?? ""}`}>{children}</a>
</NextLink>
);
};
}
function Arrow({ direction }: { direction: "left" | "right" }) {
@ -173,3 +293,91 @@ function Arrow({ direction }: { direction: "left" | "right" }) {
</svg>
);
}
function useOnScreen(element: HTMLDivElement | null) {
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) =>
setIntersecting(entry.isIntersecting)
);
if (element) {
observer.observe(element);
}
// Remove the observer as soon as the component is unmounted
return () => {
observer.disconnect();
};
}, [element]);
return isIntersecting;
}
function useBurger(componentIsVisible: boolean): boolean {
const [prevScrollPos, setPrevScrollPos] = useState(0);
const [burgerVisible, setBurgerVisible] = useState(true);
const handleScroll = useDebounce(() => {
// find current scroll position
const currentScrollPos = window.pageYOffset;
setBurgerVisible(
componentIsVisible &&
((prevScrollPos > currentScrollPos &&
prevScrollPos - currentScrollPos > 70) ||
currentScrollPos < 10)
);
// set state to new scroll position
setPrevScrollPos(currentScrollPos);
});
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return burgerVisible;
}
// Inlining this svg because we want to fill in colors using css variables
function Burger() {
return (
<svg
width="30"
height="23"
viewBox="0 0 30 23"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="28"
y1="2"
x2="2"
y2="2"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="28"
y1="11.375"
x2="2"
y2="11.375"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
<line
x1="28"
y1="20.75"
x2="2"
y2="20.75"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

@ -0,0 +1,50 @@
.page {
margin-top: calc(60rem / 16);
margin-bottom: calc(40rem / 16);
}
.headerContainer {
display: flex;
flex-direction: row;
align-items: flex-end;
padding-bottom: 1rem;
border-bottom: calc(1rem / 16) solid var(--primary-heading);
}
.header {
line-height: 1;
color: var(--primary-heading);
font-size: calc(48rem / 16);
margin: 0 0 0 calc(36rem / 16);
text-align: center;
}
.imageRight {
flex-direction: row-reverse;
}
.imageRight .header {
text-align: left;
margin-left: 0;
}
@media only screen and (max-width: calc(768rem / 16)) {
.headerContainer {
flex-direction: column;
align-items: center;
border: none;
}
.header {
font-size: calc(24rem / 16);
margin: 1.5rem 0 0 0;
}
.headerImage {
width: calc(100rem / 16);
}
.description {
display: none;
}
}

@ -0,0 +1,38 @@
import React, { ReactNode } from "react";
import { Image } from "@/components/Image";
import styles from "./Header.module.css";
export interface Props {
title: string;
image: string;
children: ReactNode;
description?: string;
imagePosition?: "left" | "right";
}
export function Header({
title,
image,
children,
description,
imagePosition,
}: Props) {
return (
<main className={styles.page}>
<header
className={`${styles.headerContainer} ${
imagePosition === "right" ? styles.imageRight : ""
}`}
>
<Image src={image} className={styles.headerImage} />
<div>
<h1 className={styles.header}>{title}</h1>
{description && <p className={styles.description}>{description}</p>}
</div>
</header>
{children}
</main>
);
}

@ -0,0 +1,84 @@
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import React, { ComponentType } from "react";
import {
createLink,
createReadAllSection,
LinkProps,
OrganizedContent,
} from "@/components/OrganizedContent";
import { GetShapesConfig } from "../ShapesBackground";
import { Header } from "./Header";
export interface SerializedSection {
section: {
id: string;
title: string;
};
content: MDXRemoteSerializeResult;
}
export interface Props {
sections: SerializedSection[];
}
export interface Options {
pagePath: string;
title: string;
image: string;
getShapesConfig?: GetShapesConfig;
imagePosition?: "left" | "right";
link?: ComponentType<LinkProps>;
description?: string;
}
export function createReadAllPage({
title,
image,
pagePath,
getShapesConfig,
link,
description,
imagePosition,
}: Options) {
const Link = link ?? createLink(pagePath);
function Page({ sections }: Props) {
const readAllSection = createReadAllSection(
sections.map(({ section, content }) => ({
section,
Content() {
return <MDXRemote {...content} />;
},
})),
true
);
return (
<Header
title={title}
image={image}
description={description}
imagePosition={imagePosition}
>
<OrganizedContent
id={readAllSection.section.id}
sections={[
readAllSection.section,
...sections.map(({ section }) => section),
]}
pageTitle={title}
link={Link}
>
<readAllSection.Content />
</OrganizedContent>
</Header>
);
}
Page.getShapesConfig = getShapesConfig;
return Page;
}

@ -0,0 +1,54 @@
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import React from "react";
import { createLink, OrganizedContent } from "@/components/OrganizedContent";
import { Header } from "./Header";
import { Options } from "./ReadAll";
interface Section {
id: string;
title: string;
}
export interface Props {
content: MDXRemoteSerializeResult;
sections: Section[];
current: number;
}
export function createSectionPage({
title,
image,
pagePath,
getShapesConfig,
link,
description,
imagePosition,
}: Options) {
const Link = link ?? createLink(pagePath);
function Page(this: void, { content, sections, current }: Props) {
return (
<Header
title={title}
image={image}
description={description}
imagePosition={imagePosition}
>
<OrganizedContent
sections={sections}
id={sections[current].id}
pageTitle={title}
link={Link}
>
<MDXRemote {...content} />
</OrganizedContent>
</Header>
);
}
Page.getShapesConfig = getShapesConfig;
return Page;
}

@ -0,0 +1,69 @@
import { ParsedUrlQuery } from "querystring";
import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from "next";
import { createReadAllSection } from "@/components/OrganizedContent";
import {
getSectionNamesForPage,
getSectionsForPage,
} from "@/lib/organized-content";
import { Props as ReadAllProps } from "./ReadAll";
import { Props as SectionProps } from "./Section";
export function createReadAllGetStaticProps(pagePath: string) {
return (async () => {
const sections = await getSectionsForPage(pagePath);
return {
props: {
sections: sections.map(({ name: id, data: { title, content } }) => ({
section: {
id,
title,
},
content,
})),
},
};
}) as GetStaticProps<ReadAllProps>;
}
interface SectionParams extends ParsedUrlQuery {
section: string;
}
export function createSectionGetStaticProps(pagePath: string) {
return async function getStaticProps(
context: GetStaticPropsContext<SectionParams>
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const currentName = context.params!.section;
const fullSections = await getSectionsForPage(pagePath);
const current = fullSections.findIndex(({ name }) => name === currentName);
const sections = fullSections.map(({ name, data: { title } }) => ({
id: name,
title,
}));
return {
props: {
content: fullSections[current].data.content,
current: current + 1,
sections: [createReadAllSection(sections, false), ...sections],
},
};
} as GetStaticProps<SectionProps, SectionParams>;
}
export function createSectionGetStaticPaths(pagePath: string) {
return async function getStaticPaths() {
const names = await getSectionNamesForPage(pagePath);
return {
paths: names.map((section) => ({ params: { section } })),
fallback: false,
};
} as GetStaticPaths<SectionParams>;
}

@ -0,0 +1,6 @@
.pre {
padding: calc(10rem / 16);
background: var(--code-background);
overflow-x: auto;
border-radius: calc(5rem / 16);
}

@ -0,0 +1,9 @@
import React, { HTMLAttributes } from "react";
import styles from "./Pre.module.css";
export function Pre(props: HTMLAttributes<HTMLPreElement>) {
const classes = [styles.pre, props.className ?? ""];
return <pre {...props} className={classes.join(" ")} />;