Merge branch 'main' into feat/events-year

pull/151/head
Jared He 2 years ago
commit a13f48edfe
  1. 12
      .drone.yml
  2. 10
      check-lockfile.js
  3. 6
      components/Code.module.css
  4. 9
      components/Code.tsx
  5. 4
      components/ConnectWithUs.tsx
  6. 7
      components/EmailSignup.module.css
  7. 21
      components/EmailSignup.tsx
  8. 5
      components/Footer.module.css
  9. 6
      components/Footer.tsx
  10. 2
      components/HorizontalLine.module.css
  11. 4
      components/Image.tsx
  12. 8
      components/MiniTechTalkCard.module.css
  13. 36
      components/MiniTechTalkCard.tsx
  14. 4
      components/Navbar.tsx
  15. 91
      components/OrganizedContent.module.css
  16. 187
      components/OrganizedContent.tsx
  17. 13
      components/OrganizedContent/Header.module.css
  18. 21
      components/OrganizedContent/Header.tsx
  19. 31
      components/OrganizedContent/ReadAll.tsx
  20. 42
      components/OrganizedContent/Section.tsx
  21. 6
      components/Pre.module.css
  22. 9
      components/Pre.tsx
  23. 20
      components/ShapesBackground.module.css
  24. 308
      components/ShapesBackground.tsx
  25. 22
      components/TechTalkCard.module.css
  26. 43
      components/TechTalkCard.tsx
  27. 5
      components/Theme.tsx
  28. 34
      components/playground.tsx
  29. 2
      content/about/code-of-conduct/additional-information.md
  30. 2
      content/about/code-of-conduct/contact-information.md
  31. 2
      content/about/code-of-conduct/experiencing-unacceptable-behaviour.md
  32. 4
      content/about/constitution/code-of-conduct.md
  33. 8
      content/get-involved.mdx
  34. 6
      content/news/2002/fall/2002-09-16-sjdutoit.md
  35. 6
      content/news/2002/fall/2002-09-16-sjdutoit.news.md
  36. 4
      content/news/2002/fall/2002-09-18-sjdutoit.md
  37. 4
      content/news/2002/fall/2002-09-30-sjdutoit.md
  38. 4
      content/news/2002/fall/2002-10-29-sjdutoit.md
  39. 6
      content/news/2002/winter/2002-02-03-sjdutoit.md
  40. 6
      content/news/2002/winter/2002-02-03-sjdutoit.news.md
  41. 6
      content/news/2002/winter/2002-02-04-sjdutoit.md
  42. 6
      content/news/2002/winter/2002-02-04-sjdutoit.news.md
  43. 4
      content/news/2002/winter/2002-04-15-sjdutoit.md
  44. 4
      content/news/2002/winter/2002-04-22-sjdutoit.md
  45. 6
      content/news/2003/fall/2003-09-17-sfllaw.md
  46. 6
      content/news/2003/fall/2003-09-17-sfllaw.news.md
  47. 6
      content/news/2003/spring/2003-05-14-sjdutoit.md
  48. 6
      content/news/2003/spring/2003-05-14-sjdutoit.news.md
  49. 4
      content/news/2003/spring/2003-06-05-sfllaw.md
  50. 6
      content/news/2003/spring/2003-07-03-sfllaw.md
  51. 6
      content/news/2003/spring/2003-07-03-sfllaw.news.md
  52. 4
      content/news/2003/spring/2003-07-09-ja2morri.md
  53. 4
      content/news/2003/spring/2003-08-06-ja2morri.md
  54. 4
      content/news/2003/spring/2003-08-12-ja2morri.md
  55. 6
      content/news/2003/winter/2003-01-13-sfllaw.md
  56. 6
      content/news/2003/winter/2003-01-13-sfllaw.news.md
  57. 4
      content/news/2004/fall/2004-09-15-mbiggs.md
  58. 6
      content/news/2004/fall/2004-09-27-jeperry.md
  59. 6
      content/news/2004/fall/2004-09-27-jeperry.news.md
  60. 6
      content/news/2004/fall/2004-10-23-jeperry.md
  61. 6
      content/news/2004/fall/2004-10-23-jeperry.news.md
  62. 6
      content/news/2004/spring/2004-05-12-mbiggs.md
  63. 6
      content/news/2004/spring/2004-05-12-mbiggs.news.md
  64. 4
      content/news/2004/spring/2004-05-31-zbnichol.md
  65. 6
      content/news/2004/winter/2004-01-22-sfllaw.md
  66. 6
      content/news/2004/winter/2004-01-22-sfllaw.news.md
  67. 6
      content/news/2004/winter/2004-01-25-sfllaw.md
  68. 6
      content/news/2004/winter/2004-01-25-sfllaw.news.md
  69. 4
      content/news/2004/winter/2004-02-02-sfllaw.md
  70. 4
      content/news/2004/winter/2004-02-05-sfllaw.md
  71. 6
      content/news/2004/winter/2004-03-16-sfllaw.md
  72. 6
      content/news/2004/winter/2004-03-16-sfllaw.news.md
  73. 4
      content/news/2004/winter/2004-03-19-sfllaw.md
  74. 4
      content/news/2004/winter/2004-04-01-sfllaw.md
  75. 6
      content/news/2004/winter/2004-04-02-sfllaw.md
  76. 6
      content/news/2004/winter/2004-04-02-sfllaw.news.md
  77. 6
      content/news/2004/winter/2004-04-07-mbiggs.md
  78. 6
      content/news/2004/winter/2004-04-07-mbiggs.news.md
  79. 4
      content/news/2004/winter/2004-04-16-sfllaw.md
  80. 4
      content/news/2004/winter/2004-04-19-sfllaw.md
  81. 6
      content/news/2006/fall/2006-09-21-woconnor.md
  82. 6
      content/news/2006/fall/2006-09-21-woconnor.news.md
  83. 6
      content/news/2006/spring/2006-05-10-hkarau.md
  84. 6
      content/news/2006/spring/2006-05-10-hkarau.news.md
  85. 6
      content/news/2006/spring/2006-05-19-hkarau.md
  86. 6
      content/news/2006/spring/2006-05-19-hkarau.news.md
  87. 4
      content/news/2006/spring/2006-06-04-hkarau.md
  88. 6
      content/news/2006/winter/2006-01-10-ddenisen.md
  89. 6
      content/news/2006/winter/2006-01-10-ddenisen.news.md
  90. 6
      content/news/2007/fall/2007-09-01-dtbartle.md
  91. 6
      content/news/2007/fall/2007-09-01-dtbartle.news.md
  92. 6
      content/news/2007/fall/2007-09-09-dtbartle.md
  93. 6
      content/news/2007/fall/2007-09-09-dtbartle.news.md
  94. 6
      content/news/2007/spring/2007-05-11-mspang.md
  95. 6
      content/news/2007/spring/2007-05-11-mspang.news.md
  96. 6
      content/news/2007/winter/2007-01-12-daltenty.md
  97. 6
      content/news/2007/winter/2007-01-12-daltenty.news.md
  98. 4
      content/news/2008/fall/2008-09-16-dtbartle.md
  99. 4
      content/news/2008/spring/2008-05-04-dtbartle.md
  100. 6
      content/news/2008/spring/2008-05-16-b4taylor.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
@ -38,10 +45,7 @@ steps:
TOKEN:
from_secret: STAGING_TOKEN
commands:
- 'curl -XPOST -H "Authorization: $TOKEN" "https://csclub.uwaterloo.ca/~a3thakra/update-csc/"'
when:
branch:
- main
- 'curl -XPOST -H "Authorization: $TOKEN" -H "X-Branch: $DRONE_BRANCH" "https://csclub.uwaterloo.ca/~a3thakra/update-csc/"'
trigger:
event:

@ -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)
}

@ -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>

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

@ -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",

@ -49,7 +49,7 @@
}
.selected {
background-color: var(--primary-accent-dim);
background-color: var(--primary-accent-lightest);
color: var(--primary-accent);
font-weight: 700;
}
@ -124,7 +124,48 @@
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);
}
@ -138,6 +179,52 @@
}
.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,5 +1,12 @@
import NextLink from "next/link";
import React, { ReactNode, ComponentType } from "react";
import React, {
ReactNode,
ComponentType,
useState,
useRef,
useEffect,
useCallback,
} from "react";
import styles from "./OrganizedContent.module.css";
@ -17,6 +24,7 @@ interface Props {
sections: Section[];
id: string;
children: ReactNode;
pageTitle: string;
link: Link;
}
@ -24,8 +32,10 @@ export function OrganizedContent({
sections,
id,
children,
pageTitle,
link: Link,
}: Props) {
const [mobileNavOpen, setMobileNavOpen] = useState(false);
const currentIndex = sections.findIndex(
({ id: sectionId }) => sectionId === id
);
@ -36,10 +46,34 @@ export function OrganizedContent({
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={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 ? (
children
@ -57,6 +91,14 @@ export function OrganizedContent({
</>
)}
</div>
<button
className={`${styles.burger} ${
burgerVisible ? styles.burgerVisible : ""
}`}
onClick={() => setMobileNavOpen(!mobileNavOpen)}
>
<Burger />
</button>
</div>
);
}
@ -65,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 (
<nav 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];
@ -82,14 +142,17 @@ 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>
);
})}
</nav>
@ -137,6 +200,20 @@ function Footer({ sections, currentIndex, link: Link }: FooterProps) {
);
}
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;
@ -216,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>
);
}

@ -19,6 +19,15 @@
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;
@ -34,4 +43,8 @@
.headerImage {
width: calc(100rem / 16);
}
.description {
display: none;
}
}

@ -8,14 +8,29 @@ export interface Props {
title: string;
image: string;
children: ReactNode;
description?: string;
imagePosition?: "left" | "right";
}
export function Header({ title, image, children }: Props) {
export function Header({
title,
image,
children,
description,
imagePosition,
}: Props) {
return (
<main className={styles.page}>
<header className={styles.headerContainer}>
<header
className={`${styles.headerContainer} ${
imagePosition === "right" ? styles.imageRight : ""
}`}
>
<Image src={image} className={styles.headerImage} />
<h1 className={styles.header}>{title}</h1>
<div>
<h1 className={styles.header}>{title}</h1>
{description && <p className={styles.description}>{description}</p>}
</div>
</header>
{children}
</main>

@ -8,6 +8,8 @@ import {
OrganizedContent,
} from "@/components/OrganizedContent";
import { GetShapesConfig } from "../ShapesBackground";
import { Header } from "./Header";
export interface SerializedSection {
@ -26,13 +28,24 @@ 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, link }: Options) {
export function createReadAllPage({
title,
image,
pagePath,
getShapesConfig,
link,
description,
imagePosition,
}: Options) {
const Link = link ?? createLink(pagePath);
return function Page({ sections }: Props) {
function Page({ sections }: Props) {
const readAllSection = createReadAllSection(
sections.map(({ section, content }) => ({
section,
@ -44,18 +57,28 @@ export function createReadAllPage({ title, image, pagePath, link }: Options) {
);
return (
<Header title={title} image={image}>
<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;
}

@ -1,13 +1,10 @@
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
import React, { ComponentType } from "react";
import React from "react";
import {
createLink,
LinkProps,
OrganizedContent,
} from "@/components/OrganizedContent";
import { createLink, OrganizedContent } from "@/components/OrganizedContent";
import { Header } from "./Header";
import { Options } from "./ReadAll";
interface Section {
id: string;
@ -20,27 +17,38 @@ export interface Props {
current: number;
}
export interface Options {
title: string;
pagePath: string;
image: string;
link?: ComponentType<LinkProps>;
}
export function createSectionPage({ title, image, pagePath, link }: Options) {
export function createSectionPage({
title,
image,
pagePath,
getShapesConfig,
link,
description,
imagePosition,
}: Options) {
const Link = link ?? createLink(pagePath);
return function Page(this: void, { content, sections, current }: Props) {
function Page(this: void, { content, sections, current }: Props) {
return (
<Header title={title} image={image}>
<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,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(" ")} />;
}

@ -0,0 +1,20 @@
.shapesContainer {
position: absolute;
overflow: hidden;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: -10;
}
.shape {
--blue: invert(53%) sepia(80%) saturate(4689%) hue-rotate(189deg)
brightness(92%) contrast(93%);
--teal: invert(76%) sepia(39%) saturate(578%) hue-rotate(110deg)
brightness(91%) contrast(88%);
position: absolute;
filter: var(--blue);
opacity: 20%;
}

@ -0,0 +1,308 @@
import { useWindowDimension } from "hooks/useWindowDimension";
import { useRouter } from "next/router";
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import { Image } from "./Image";
import styles from "./ShapesBackground.module.css";
const MOBILE_WIDTH = 768;
interface Props {
getConfig?: GetShapesConfig;
}
export function ShapesBackground({ getConfig }: Props) {
const [config, setConfig] = useState<ShapesConfig>({});
const [prevWidth, setPrevWidth] = useState<number>(-1);
const [prevRoute, setPrevRoute] = useState<string>("");
const { width, height } = useWindowDimension();
const shapesContainerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
useEffect(() => {
const containerWidth = shapesContainerRef.current?.offsetWidth;
const containerHeight = shapesContainerRef.current?.offsetHeight;
// In general, rerun getShapesConfig() if the screen size changes from desktop to mobile (or vice versa)
if (
containerWidth == null ||
containerHeight == null ||
!(
router.asPath === "/" ||
router.asPath !== prevRoute ||
prevWidth < 0 ||
(prevWidth <= MOBILE_WIDTH && MOBILE_WIDTH < width) ||
(prevWidth > MOBILE_WIDTH && MOBILE_WIDTH >= width)
)
) {
return;
}
setPrevWidth(width);
setPrevRoute(router.asPath);
setConfig(getConfig?.(containerWidth, containerHeight) ?? {});
}, [getConfig, width, height, prevWidth, prevRoute, router.asPath]);
return (
<div className={styles.shapesContainer} ref={shapesContainerRef}>
{Object.entries(config).map(([type, instances]) =>
instances.map((attributes, idx) => (
<Shape
key={idx.toString() + type}
type={type as ShapeType}
style={attributes}
/>
))
)}
</div>
);
}
function Shape(props: { type: ShapeType; style: CSSProperties }) {
return (
<Image
src={`/images/shapes/${props.type}.svg`}
className={styles.shape}
style={props.style}
/>
);
}
export type ShapeType =
| "asterisk"
| "circle"
| "cross"
| "dots"
| "hash"
| "plus"
| "ring"
| "triangle"
| "triangleBig"
| "waves"
| "wavesBig";
export type ShapesConfig = {
[key in ShapeType]?: CSSProperties[];
};
export type GetShapesConfig = (width: number, height: number) => ShapesConfig;
type ShapeSize = {
[key in ShapeType]: {
size: "big" | "small";
width: number;
height: number;
// 0 <= minAngle, maxAngle <= 180
minAngle?: number;
maxAngle?: number;
};
};
const shapeTypes: ShapeType[] = [
"asterisk",
"circle",
"cross",
"dots",
"hash",
"plus",
"ring",
"triangle",
"triangleBig",
"waves",
"wavesBig",
];
const shapesSizes: ShapeSize = {
asterisk: {
size: "big",
width: 168,
height: 168,
},
circle: {
size: "big",
width: 132,
height: 132,
},
cross: {
size: "big",
width: 150,
height: 150,
},
dots: {
size: "big",
width: 232,
height: 250,
},
hash: {
size: "small",
width: 60,
height: 60,
},
plus: {
size: "small",
width: 48,
height: 48,
},
ring: {
size: "small",
width: 70,
height: 70,
},
triangle: {
size: "small",
width: 68,
height: 68,
minAngle: 15,
maxAngle: 26,
},
triangleBig: {
size: "big",
width: 138,
height: 138,
minAngle: 15,
maxAngle: 26,
},
waves: {
size: "small",
width: 102,
height: 50,
},
wavesBig: {
size: "big",
width: 252,
height: 132,
},
};
const shapesBySize = {
big: shapeTypes.filter((shape) => shapesSizes[shape]["size"] == "big"),
small: shapeTypes.filter((shape) => shapesSizes[shape]["size"] == "small"),
};
// Used to generate random shapes in the margins
export const defaultGetShapesConfig = ((pageWidth, pageHeight) => {
if (window.innerWidth <= MOBILE_WIDTH) {
return mobileShapesConfig;
}
const defaultConfig: ShapesConfig = {};
const gap = 20;
const minBoxWidth = 150;
const boxWidth = Math.max(minBoxWidth, (pageWidth - 800 - 2 * gap) / 2);
const boxHeight = 400;
const shapeGenerationProbability = 0.85;
for (let y = 0; y + boxHeight <= pageHeight; y += gap + boxHeight) {
for (let x = 0; x <= 1; ++x) {
if (Math.random() > shapeGenerationProbability) {
continue;
}
const size =
boxWidth > minBoxWidth && (y == 0 || y + 2 * boxHeight > pageHeight)
? "big"
: "small";
const shape: ShapeType = getRandomShape(size);
const verticalOffset = getVerticalOffset(boxHeight, shape);
const horizontalOffset = getHorizontalOffset(boxWidth - 2 * gap, shape);
const shapeWidth = shapesSizes[shape]["width"];
const shapeHeight = shapesSizes[shape]["height"];
const angle = getRandomAngle(shape);
const colour = getRandomColour();
const opacity = getRandomOpacity(colour);
defaultConfig[shape] ??= [];
defaultConfig[shape]?.push({
top: `${((y + verticalOffset) / 16).toFixed(5)}rem`,
left:
x == 0
? `${(((horizontalOffset + gap) / window.innerWidth) * 100).toFixed(
5
)}vw`
: "unset",
right:
x == 1
? `${(((horizontalOffset + gap) / window.innerWidth) * 100).toFixed(
5
)}vw`
: "unset",
width: `${(shapeWidth / 16).toFixed(5)}rem`,
height: `${(shapeHeight / 16).toFixed(5)}rem`,
transform: `rotate(${angle}deg)`,
filter: `var(--${colour})`,
opacity: `${opacity}%`,
});
}
}
return defaultConfig;
}) as GetShapesConfig;
function getRandomShape(size: "big" | "small"): ShapeType {
const idx = Math.floor(Math.random() * shapesBySize[size].length);
return shapesBySize[size][idx];
}
function getVerticalOffset(boxHeight: number, shape: ShapeType): number {
const padding = shapesSizes[shape]["height"];
return Math.floor(Math.random() * (boxHeight - padding));
}
function getHorizontalOffset(boxWidth: number, shape: ShapeType): number {
const padding = shapesSizes[shape]["width"];
return shapesSizes[shape]["size"] == "big"
? Math.floor(Math.random() * (boxWidth - padding / 2) - padding / 2)
: Math.floor(Math.random() * (boxWidth - padding));
}
function getRandomAngle(shape: ShapeType): number {
const minAngle = shapesSizes[shape]["minAngle"] ?? 0;
const maxAngle = shapesSizes[shape]["maxAngle"] ?? 0;
const direction = Math.random() < 0.5 ? 1 : -1;
return (
(minAngle + Math.floor(Math.random() * (maxAngle - minAngle + 1))) *
direction
);
}
function getRandomColour(): "blue" | "teal" {
return Math.random() < 0.7 ? "blue" : "teal";
}
function getRandomOpacity(colour: "blue" | "teal"): number {
if (colour === "blue") {
return Math.random() < 0.8 ? 20 : 25;
} else {
return Math.random() < 0.8 ? 25 : 30;
}
}
// Used for most mobile pages
export const mobileShapesConfig = {
dots: [
{
top: "calc(-6rem / 16)",
left: "calc(-95rem / 16)",
width: "calc(166rem / 16)",
height: "calc(150rem / 16)",
},
],
hash: [
{
top: "calc(88rem / 16)",
right: "15vw",
width: "calc(40rem / 16)",
height: "calc(40rem / 16)",
},
],
triangle: [
{
top: "calc(20rem / 16)",
right: "1vw",
width: "calc(45rem / 16)",
height: "calc(45rem / 16)",
transform: "rotate(17deg)",
},
],
};

@ -24,11 +24,31 @@
font-style: normal;
margin-top: 0;
margin-bottom: 0;
color: var(--blue-2);
color: var(--primary-accent);
}
.content h2,
.content h3,
.content h4 {
font-size: calc(18rem / 16);
}
@media only screen and (max-width: calc(768rem / 16)) {
.card {
flex-direction: column;
}
.card aside {
margin: 0;
margin-bottom: 1rem;
flex: unset;
}
.card aside img {
margin: 0;
}
.content ul {
padding-left: 1rem;
}
}

@ -1,26 +1,57 @@
import React, { ReactNode } from "react";
import { Image } from "./Image";
import { Link } from "./Link";
import styles from "./TechTalkCard.module.css";
interface DownloadLink {
file: string;
type: string;
size?: string;
}
interface TechTalkProps {
name: string;
poster?: string;
title: string;
presentors: string[];
poster: string;
links: DownloadLink[];
children: ReactNode;
}
export function TechTalkCard({ name, poster, children }: TechTalkProps) {
export function TechTalkCard({
title,
poster,
presentors,
links,
children,
}: TechTalkProps) {
return (
<article className={styles.card}>
<aside>
{poster && <Image alt={name} src={poster} />}
{!poster && <div className={styles.spacer}></div>}
<Image
alt={`Thumbnail of tech talk by ${presentors.join(", ")}: ${title}`}
src={poster}
/>
</aside>
<section className={styles.content}>
<h1>{name}</h1>
<h1>{title}</h1>
<div>{children}</div>
<h2>Download:</h2>
<ul>