Merge branch 'main' into feat/organized-content

pull/95/head
William Tran 2 years ago
commit 8102daf956
  1. 21
      .gitlab-ci.yml
  2. 32
      components/Button.module.css
  3. 40
      components/Button.tsx
  4. 6
      components/EventCard.module.css
  5. 13
      components/EventCard.tsx
  6. 67
      components/EventDescriptionCard.module.css
  7. 62
      components/EventDescriptionCard.tsx
  8. 9
      components/EventSetting.module.css
  9. 12
      components/EventSetting.tsx
  10. 36
      components/Footer.module.css
  11. 16
      components/Footer.tsx
  12. 2
      components/Image.tsx
  13. 10
      components/Link.module.css
  14. 28
      components/Link.tsx
  15. 24
      components/MiniEventCard.module.css
  16. 30
      components/MiniEventCard.tsx
  17. 4
      components/Navbar.tsx
  18. 26
      components/NewsCard.module.css
  19. 6
      components/NewsCard.tsx
  20. 118
      components/OrganizedContent.module.css
  21. 240
      components/OrganizedContent.tsx
  22. 23
      components/SocialLinks.module.css
  23. 194
      components/SocialLinks.tsx
  24. 5
      components/TeamMember.module.css
  25. 33
      components/TeamMemberCard.module.css
  26. 27
      components/playground.module.css
  27. 95
      components/playground.tsx
  28. 31
      content/our-supporters.mdx
  29. 2
      content/playground/after-hours.event.mdx
  30. 2
      content/playground/alt-tab.event.mdx
  31. 27
      content/playground/constitution/duties-of-officers.section.mdx
  32. 10
      content/playground/constitution/executive-council.section.mdx
  33. 10
      content/playground/constitution/membership.section.mdx
  34. 6
      content/playground/constitution/name.section.mdx
  35. 30
      content/playground/constitution/officers.section.mdx
  36. 10
      content/playground/constitution/purpose.section.mdx
  37. 2
      content/playground/ootb-react.event.mdx
  38. 16
      next-env.d.ts
  39. 2
      next.config.js
  40. 48
      pages/_app.css
  41. 19
      pages/_app.module.css
  42. 41
      pages/_app.tsx
  43. 42
      pages/about/our-supporters.module.css
  44. 21
      pages/about/our-supporters.tsx
  45. 19
      pages/playground.mdx
  46. BIN
      public/logos/Discord.png
  47. BIN
      public/logos/Facebook.png
  48. BIN
      public/logos/Instagram.png
  49. BIN
      public/logos/Twitch.png
  50. 38
      public/our-supporters/supporters-codey.svg

@ -10,6 +10,9 @@ stages:
- build
- staging
variables:
NEXT_PUBLIC_BASE_PATH: '/~a3thakra/csc'
install_deps:
stage: .pre
script:
@ -25,14 +28,22 @@ build:
script:
- npm run build
pages:
staging:
stage: staging
script:
- npm run export
- mv public src-public
- mv out public
artifacts:
paths:
- public
- out
only:
refs:
- main
deploy_staging:
stage: .post
needs: ["staging"]
script:
- 'curl -XPOST -H "Authorization: Basic $STAGING_SECRET" "https://csclub.uwaterloo.ca/~a3thakra/csc/"'
only:
- main
refs:
- main

@ -0,0 +1,32 @@
.button,
.link {
font-family: "Poppins", "sans-serif";
border-radius: calc(20rem / 16);
background-color: var(--blue-2);
color: white;
border: none;
outline: none;
transition-duration: 0.3s;
font-weight: normal;
text-align: center;
}
.button:hover,
.link:hover {
background-color: var(--teal-2);
cursor: pointer;
}
.link {
text-decoration: none;
}
.small {
padding: calc(5rem / 16) calc(30rem / 16);
font-size: calc(14rem / 16);
}
.normal {
padding: calc(10rem / 16) calc(50rem / 16);
font-size: calc(18rem / 16);
}

@ -0,0 +1,40 @@
import React, { AnchorHTMLAttributes, ButtonHTMLAttributes } from "react";
import styles from "./Button.module.css";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
isLink?: false;
}
interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
isLink: true;
}
type Props = (ButtonProps | LinkProps) & { size?: "small" | "normal" };
export function Button(props: Props) {
const btnSize = props.size ? props.size : "normal";
if (props.isLink) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { size, isLink, ...otherProps } = props;
return (
<a
{...otherProps}
target="_blank"
className={`${styles.link} ${styles[btnSize]} ${
otherProps.className ?? ""
}`}
/>
);
} else {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { size, isLink, ...otherProps } = props;
return (
<button
{...otherProps}
className={`${styles.button} ${styles[btnSize]} ${
otherProps.className ?? ""
}`}
/>
);
}
}

@ -1,7 +1,6 @@
.card {
display: flex;
flex-direction: row;
max-width: calc(1000rem / 16);
box-sizing: border-box;
padding: calc(24rem / 16);
}
@ -21,7 +20,8 @@
}
.registerButton {
width: 100%;
display: block;
font-weight: bold;
}
.content > h1 {
@ -34,7 +34,6 @@
.content,
.content > h2 {
color: var(--purple-2);
font-weight: 400;
font-style: normal;
margin-top: 0;
@ -42,6 +41,7 @@
}
.content > h2 {
color: var(--purple-2);
font-size: 1rem;
margin-bottom: calc(14rem / 16);
}

@ -1,4 +1,5 @@
import React, { ReactNode } from "react";
import { Button } from "./Button";
import styles from "./EventCard.module.css";
import { EventSetting } from "./EventSetting";
@ -29,11 +30,15 @@ export function EventCard({
<aside>
{poster && <Image alt={name} src={poster} />}
{!poster && <div className={styles.spacer}></div>}
{/* TODO: use the <Button /> component */}
{registerLink && (
<button className={styles.registerButton}>
<a href={registerLink}>Register</a>
</button>
<Button
isLink={true}
href={registerLink}
size="small"
className={styles.registerButton}
>
Register
</Button>
)}
</aside>
<section className={styles.content}>

@ -3,7 +3,7 @@
box-sizing: border-box;
max-width: calc(540rem / 16);
padding: calc(24rem / 16);
border-radius: calc(4rem / 16);
border-radius: calc(20rem / 16);
background-color: white;
}
@ -14,50 +14,69 @@
}
.details {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
}
.name {
color: var(--purple-2);
font-weight: bolder;
font-size: calc(18rem / 16);
line-height: calc(27rem / 16);
margin: 0;
}
.desc {
color: var(--purple-2);
margin-top: calc(12rem / 16);
}
.spacer {
height: calc(35rem / 16);
}
.button {
position: absolute;
bottom: 0;
left: 0;
margin: 1rem 0;
}
.logo {
width: calc(30rem / 16);
position: absolute;
bottom: 0;
right: 0;
width: calc(32rem / 16);
margin-left: auto;
}
.setting {
margin: 0;
color: var(--blue-1);
font-weight: bolder;
color: var(--blue-2);
font-size: calc(14rem / 16);
font-weight: 600;
}
.details > footer {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 1rem;
margin-top: auto;
}
@media only screen and (max-width: calc(768rem / 16)) {
.card {
padding: 0;
background-color: #e1eefa;
background-color: transparent;
}
.details {
min-width: calc(150rem / 16);
}
.name,
.setting {
font-size: 1rem;
}
.poster {
width: calc(95rem / 16);
height: calc(95rem / 16);
border: 1px solid var(--purple-2);
box-sizing: border-box;
margin-right: calc(14rem / 16);
}
.desc {
margin: 0;
}
.logo,
.button {
display: none;
}
}

@ -1,32 +1,18 @@
import React, { ReactNode } from "react";
// import { Button } from "./Button";
import React from "react";
import { Button } from "./Button";
import { Image } from "./Image";
import { EventSetting } from "./EventSetting";
import styles from "./EventDescriptionCard.module.css";
import { Discord, Twitch, Instagram, Facebook } from "./SocialLinks";
interface Props {
name: string;
short: string;
online: boolean;
location: string;
date: Date;
poster?: string;
registerLink?: string;
children: ReactNode;
}
function getPlatformURL(platform: string) {
switch (platform) {
case "Twitch":
return "https://www.twitch.tv/uwcsclub";
case "Discord":
return "https://discord.gg/pHfYBCg";
case "Facebook":
return "https://www.facebook.com/uw.computerscienceclub";
case "Instagram":
return "https://www.instagram.com/uwcsclub/";
default:
return;
}
}
/**
@ -43,12 +29,12 @@ export function EventDescriptionCard({
location,
poster,
name,
short,
date,
online,
registerLink,
children,
}: Props) {
const platformURL = getPlatformURL(location);
const Icon = getIcon(location);
return (
<article className={styles.card}>
@ -59,28 +45,38 @@ export function EventDescriptionCard({
<h2 className={styles.setting}>
<EventSetting date={date} online={online} location={location} />
</h2>
<div className={styles.desc}>{children}</div>
<div className={styles.spacer}></div>
<p className={styles.desc}>{short}</p>
<footer>
{registerLink && (
<div className={styles.button}>
<button>
<a href={registerLink}>Register</a>
</button>
<Button isLink={true} href={registerLink} size="small">
Register
</Button>
</div>
)}
{online && platformURL && (
<a target="_blank" href={platformURL} rel="noreferrer">
<Image
className={styles.logo}
alt={location}
src={"logos/" + location + ".png"}
/>
</a>
{online && Icon && (
<div className={styles.logo}>
<Icon color="blue" size="small" />
</div>
)}
</footer>
</div>
</article>
);
}
function getIcon(platform: string) {
switch (platform) {
case "Twitch":
return Twitch;
case "Discord":
return Discord;
case "Instagram":
return Instagram;
case "Facebook":
return Facebook;
default:
return null;
}
}

@ -0,0 +1,9 @@
@media only screen and (max-width: calc(768rem / 16)) {
.separator {
display: none;
}
.setting {
display: block;
}
}

@ -1,4 +1,5 @@
import React from "react";
import styles from "./EventSetting.module.css";
interface Props {
date: Date;
@ -15,13 +16,20 @@ export function EventSetting(props: Props) {
const time = props.date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "numeric",
timeZoneName: "short",
});
const location = props.online ? `Online - ${props.location}` : props.location;
const separator = <span className={styles.separator}> | </span>;
return (
<div>
<time dateTime={props.date.toISOString()}>{`${date} | ${time}`}</time>
{` | ${location}`}
<time className={styles.setting} dateTime={props.date.toISOString()}>
{date}
</time>
{separator}
<span className={styles.setting}>{time}</span>
{separator}
{location}
</div>
);
}

@ -0,0 +1,36 @@
.footer {
box-sizing: border-box;
background: var(--purple-2);
height: calc(66rem / 16);
padding: calc(14rem / 16) 0;
width: 100%;
}
.container {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
max-width: calc(1440rem / 16);
padding: 0 calc(48rem / 16);
margin: 0 auto;
height: 100%;
}
.text {
color: var(--white);
font-style: normal;
text-align: center;
}
@media only screen and (max-width: calc(768rem / 16)) {
.footer {
height: calc(120rem / 16);
}
.container {
flex-direction: column-reverse;
justify-content: space-around;
padding: 0 calc(24rem / 16);
}
}

@ -0,0 +1,16 @@
import React from "react";
import styles from "./Footer.module.css";
import { SocialLinks } from "./SocialLinks";
export function Footer() {
return (
<footer className={styles.footer}>
<div className={styles.container}>
<div className={styles.text}>
Have questions? Email us at XX@XXX.COM
</div>
<SocialLinks color="white" size="small" />
</div>
</footer>
);
}

@ -3,7 +3,7 @@ import React, { ImgHTMLAttributes } from "react";
export function Image(props: ImgHTMLAttributes<HTMLImageElement>) {
const { src: relativeSrc = "" } = props;
let absoluteSrc = process.env.BASE_PATH ?? "/";
let absoluteSrc = process.env.NEXT_PUBLIC_BASE_PATH ?? "/";
if (absoluteSrc.endsWith("/") && relativeSrc.startsWith("/")) {
absoluteSrc += relativeSrc.slice(1);
} else if (absoluteSrc.endsWith("/") || relativeSrc.startsWith("/")) {

@ -0,0 +1,10 @@
.link {
color: var(--blue-2);
transition-duration: 0.3s;
text-decoration: none;
white-space: normal;
}
.link:hover {
color: var(--teal-2);
}

@ -0,0 +1,28 @@
import React from "react";
import styles from "./Link.module.css";
import NextLink from "next/link";
import { LinkProps as NextLinkProps } from "next/link";
type Props = Omit<NextLinkProps, "href"> & { href: string };
export const Link: React.FC<Props> = (props) => {
const { children, ...otherProps } = props;
const { href } = otherProps;
const isExternal = href.includes("http://") || href.includes("https://");
return isExternal ? (
<a
className={styles.link}
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
) : (
<NextLink {...otherProps}>
<a className={styles.link}>{children}</a>
</NextLink>
);
};

@ -1,11 +1,11 @@
.miniEventCard {
max-width: calc(936rem / 16);
.card {
box-sizing: border-box;
position: relative;
color: var(--purple-2);
padding: calc(20rem / 16);
/* FIXME: Check figma for updated size */
font-size: calc(14rem / 16);
}
.card:nth-child(odd) {
background-color: var(--teal-2-20);
}
.name {
@ -19,6 +19,7 @@
}
.info {
color: var(--purple-2);
margin-bottom: calc(12rem / 16);
}
@ -27,18 +28,23 @@
top: 0;
right: 0;
cursor: pointer;
color: var(--blue-2);
margin: calc(20rem / 16);
color: var(--blue-2);
font-size: calc(14rem / 16);
}
.miniEventCard[open] .shortDescription {
.card[open] .shortDescription {
display: none;
}
.miniEventCard[open] .dropDownIcon {
.card[open] .dropDownIcon {
transform: rotate(180deg);
}
.miniEventCard > summary {
.card > summary {
list-style: none;
}
.dropDownIcon {
fill: var(--blue-2);
}

@ -11,22 +11,6 @@ interface Props {
date: Date;
}
const dropDownIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="9"
viewBox="0 0 14 9"
fill="none"
className={styles.dropDownIcon}
>
<path
d="M6.24407 8.12713C6.64284 8.58759 7.35716 8.58759 7.75593 8.12713L13.3613 1.65465C13.9221 1.00701 13.4621 0 12.6053 0H1.39467C0.537918 0 0.0778675 1.00701 0.638743 1.65465L6.24407 8.12713Z"
fill="#1482E3"
/>
</svg>
);
export const MiniEventCard: React.FC<Props> = ({
name,
short,
@ -36,7 +20,7 @@ export const MiniEventCard: React.FC<Props> = ({
online,
}) => {
return (
<details className={styles.miniEventCard}>
<details className={styles.card}>
<summary>
<div onClick={(event) => event.preventDefault()}>
<h2 className={styles.name}>
@ -56,3 +40,15 @@ export const MiniEventCard: React.FC<Props> = ({
</details>
);
};
const dropDownIcon = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="9"
viewBox="0 0 14 9"
className={styles.dropDownIcon}
>
<path d="M6.24407 8.12713C6.64284 8.58759 7.35716 8.58759 7.75593 8.12713L13.3613 1.65465C13.9221 1.00701 13.4621 0 12.6053 0H1.39467C0.537918 0 0.0778675 1.00701 0.638743 1.65465L6.24407 8.12713Z" />
</svg>
);

@ -38,6 +38,10 @@ const menu: NavLink[] = [
name: "Code of Conduct",
route: "/about/code-of-conduct",
},
{
name: "Our Supporters",
route: "/about/our-supporters",
},
],
},
{

@ -1,27 +1,22 @@
.card {
padding: calc(27rem / 16) calc(39rem / 16);
padding: calc(30rem / 16) calc(40rem / 16);
max-width: calc(524rem / 16);
background-color: white;
background-color: var(--white);
border-radius: calc(20rem / 16);
}
.date {
color: var(--purple-2);
font-size: calc(18rem / 16);
font-weight: bold;
line-height: calc(27rem / 16);
margin: 0;
}
.author {
color: var(--purple-1);
font-style: normal;
line-height: calc(21rem / 16);
font-size: calc(14rem / 16);
margin: calc(5rem / 16) 0 calc(7rem / 16) 0;
}
.content {
line-height: calc(21rem / 16);
color: var(--purple-2);
.content > *:first-child {
margin-top: 0;
}
@media only screen and (max-width: calc(768rem / 16)) {
@ -29,4 +24,13 @@
padding: 0;
background-color: transparent;
}
.content > *:first-child {
margin-top: 1rem;
}
.content ul,
.content ol {
padding: 0 1rem;
}
}

@ -14,15 +14,15 @@ export const NewsCard: React.FC<NewsCardProps> = ({
}) => {
return (
<article className={styles.card}>
<h3>
<time className={styles.date} dateTime={date.toISOString()}>
<h1 className={styles.date}>
<time dateTime={date.toISOString()}>
{date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
</h3>
</h1>
<address className={styles.author}>{author}</address>
<div className={styles.content}>{children}</div>
</article>

@ -1,113 +1,115 @@
.organizedContent {
display: flex;
.wrapper {
display: flex;
}
.organizedContent h1 {
font-size: 1.5rem;
margin: 1rem 0 1rem 0;
.wrapper h1 {
margin: 1rem 0;
font-size: calc(24rem / 16);
font-weight: 600;
color: var(--blue-2);
}
.content {
display: flex;
flex-direction: column;
width: 100%;
}
.nav {
margin: 1rem 2rem 0 0.5rem;
margin: calc(8rem / 16) calc(32rem / 16) 0 0;
color: var(--purple-2);
font-weight: 500;
}
.navOption {
.navItem {
display: flex;
overflow: hidden;
white-space: nowrap;
font-size: 0.875rem;
font-size: calc(14rem / 16);
border-bottom: calc(1rem / 16) solid var(--blue-2-25);
align-items: center;
height: calc(40rem / 16);
width: calc(284rem / 16);
padding: 0 calc(14rem / 16);
cursor: pointer;
}
.navLink {
.navItem > div {
width: 100%;
cursor: pointer;
padding: 0.5rem 2rem 0.5rem 0.5rem;
overflow: hidden;
text-overflow: ellipsis;
}
.navLinkSelected {
/* smaller to account for marker width */
padding: 0.5rem 1.15rem 0.5rem 0.5rem;
.selected {
background-color: var(--blue-1-05);
color: var(--blue-2);
font-weight: 700;
}
.readAll {
font-weight: bold;
}
.divider {
border-bottom: 1px solid var(--blue-2);
opacity: 0.25;
width: 100%,
}
.selectedHeadingArea > .navOption {
color: var(--blue-2);
font-weight: bold;
font-weight: 700;
}
.selectedHeadingArea {
background-color: var(--blue-1-05);
.marker {
display: none;
}
.selectedMarker {
.selected .marker {
display: inline;
background-color: var(--blue-2);
height: calc(24rem / 16);
width: calc(4rem / 16);
margin: 0.4rem 0.1rem 0.4rem 0.5rem;
margin-right: 1rem;
}
.footer {
margin-top: 1rem;
margin-top: auto;
display: flex;
justify-content: space-between;
}
.footerSection {
display: flex;
.previous,
.next {
flex: 1;
display: flex;
cursor: pointer;
color: var(--purple-2);
font-size: calc(12rem / 16);
}
.arrowHeading {
color: var(--blue-2);
font-size: 0.875rem;
font-weight: bold;
text-decoration: underline;
text-underline-offset: 0.5rem;
text-decoration-thickness: 0.1rem;
.previous {
margin-right: calc(8rem / 16);
}
.prevNext {
font-size: 0.75rem;
.next {
justify-content: flex-end;
text-align: end;
margin-left: calc(8rem / 16);
}
.nextText {
text-align: end;
.arrowHeading {
color: var(--blue-2);
font-size: calc(14rem / 16);
font-weight: 700;
border-bottom: calc(2rem / 16) solid var(--blue-2);
padding-bottom: calc(4rem / 16);
}
.arrow {
fill: var(--blue-2);
margin-top: 1.7rem;
margin-top: calc(27rem / 16);
}
.prevArrow {
transform: rotate(90deg);
padding-right: 0.5rem;
padding-right: calc(8rem / 16);
}
.nextArrow {
transform: rotate(270deg);
padding-left: 0.5rem;
padding-left: calc(8rem / 16);
}
.footerSection {
cursor: pointer;
display: flex;
}
.footerDivider {
width: 1rem;
}
.burger {
position: fixed;
margin: 0px;

@ -9,27 +9,28 @@ import styles from "./OrganizedContent.module.css";
export interface LinkProps {
className?: string;
url: string;
children: string | ReactNode | (string | ReactNode)[];
id: string;
children: ReactNode;
}
type Link = ComponentType<LinkProps>;
interface Heading {
interface Section {
id: string;
title: string;
url: string;
content: ReactNode;
Content: ComponentType;
}
const READ_ALL_TITLE = "Read All";
export const READ_ALL_ID = "read-all";
interface Props {
headings: Heading[];
currentIndex: number;
sections: Section[];
currentId: string;
link: Link;
children: ReactNode;
}
interface ChildProps {
headings: Heading[];
sections: Section[];
currentIndex: number;
link: Link;
}
@ -40,31 +41,22 @@ interface MobileProps {
childProps?: ChildProps;
}
export const OrganizedContent = ({
headings,
currentIndex,
link: Link,
children,
}: Props) => {
export function OrganizedContent(props: Props) {
const sections = createSections(props.sections);
const currentIndex = sections.findIndex(({ id }) => id === props.currentId);
const [open, setOpen] = useState(false);
//const node = useRef();
//useOnClickOutside(node, () => setOpen(false));
const isReadAll = headings[currentIndex].title === "Read All";
if (currentIndex < 0) {
throw new Error(`Section with ID ${props.currentId} was not found`);
}
const readAllContent = headings
.filter((heading: { title: string }) => heading.title !== "Read All")
.map((heading) => (
<div key={heading.url}>
<h1>{heading.title}</h1>
{heading.content}
</div>
));
const section = sections[currentIndex];
const isReadAll = section.id === READ_ALL_ID;
const childProps: ChildProps = {
headings: headings,
sections: props.sections,
currentIndex: currentIndex,
link: Link,
link: props.link,
};
const mobileProps: MobileProps = {
@ -80,117 +72,105 @@ export const OrganizedContent = ({
}, [open]);
return (
<div className={styles.organizedContent}>
<Nav {...childProps} />
<div>
<div className={styles.wrapper}>
<Nav sections={sections} currentIndex={currentIndex} link={props.link} />
<div className={styles.content}>
{isReadAll ? (
<>{readAllContent}</>
<section.Content />
) : (
<>
<h1>{headings[currentIndex].title}</h1>
{children}
<Footer {...childProps} />
<div>
<h1>{section.title}</h1>
<section.Content />
</div>
<Footer
sections={sections}
currentIndex={currentIndex}
link={props.link}
/>
</>
)}
</div>
<MobileWrapper {...mobileProps} />
</div>
);
};
}
const Nav = ({ headings, currentIndex, link: Link }: ChildProps) => {
interface NavProps {
sections: Section[];
currentIndex: number;
link: Link;
}
function Nav({ sections, currentIndex, link: Link }: NavProps) {
return (
<div className={styles.nav}>
{headings.map((heading, index) => (
<div
className={index === currentIndex ? styles.selectedHeadingArea : ""}
key={heading.url}
>
<div
className={
styles.navOption +
" " +
(heading.title === "Read All" ? styles.readAll : "")
}
{sections.map((section, index) => {
const classNames = [styles.navItem];
if (index === currentIndex) {
classNames.push(styles.selected);
}
if (section.id === READ_ALL_ID) {
classNames.push(styles.readAll);
}
return (
<Link
className={classNames.join(" ")}
id={section.id}
key={section.id}
>
{index === currentIndex && (
<span className={styles.selectedMarker} />
)}
<Link
className={
styles.navLink +
" " +
(index === currentIndex ? styles.navLinkSelected : "")
}
url={heading.url}
>
{heading.title}
</Link>
</div>
<div className={styles.divider}></div>
</div>
))}
<span className={styles.marker} />
<div>{section.title}</div>
</Link>
);
})}
</div>
);
};
}
const Footer = ({ headings, currentIndex, link: Link }: ChildProps) => {
const prevHeading =
currentIndex > 0 && headings[currentIndex - 1].title !== "Read All"
? headings[currentIndex - 1]
interface FooterProps {
sections: Section[];
currentIndex: number;
link: Link;
}
function Footer({ sections, currentIndex, link: Link }: FooterProps) {
const prevSection =
currentIndex > 0 && sections[currentIndex - 1].id !== READ_ALL_ID
? sections[currentIndex - 1]
: undefined;
const nextHeading =
currentIndex < headings.length - 1 &&
headings[currentIndex + 1].title !== "Read All"
? headings[currentIndex + 1]
const nextSection =
currentIndex < sections.length - 1 &&
sections[currentIndex + 1].id !== READ_ALL_ID
? sections[currentIndex + 1]
: undefined;
return (
<div className={styles.footer}>
{prevHeading && (
<Link url={prevHeading.url}>
<div className={styles.footerSection}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="9"
viewBox="0 0 14 9"
className={styles.arrow + " " + styles.prevArrow}
>
<path d="M6.24407 8.12713C6.64284 8.58759 7.35716 8.58759 7.75593 8.12713L13.3613 1.65465C13.9221 1.00701 13.4621 0 12.6053 0H1.39467C0.537918 0 0.0778675 1.00701 0.638743 1.65465L6.24407 8.12713Z" />
</svg>
<div>
<div className={styles.prevNext}>Previous</div>
<div className={styles.arrowHeading}>{prevHeading.title}</div>
</div>
{prevSection && (
<Link className={styles.previous} id={prevSection.id}>
<Arrow direction="left" />
<div>
<div>Previous</div>
<div className={styles.arrowHeading}>{prevSection.title}</div>
</div>
</Link>
)}
<div className={styles.footerDivider}></div>
{nextHeading && (
<Link url={nextHeading.url}>
<div className={styles.footerSection}>
<div>
<div className={styles.prevNext + " " + styles.nextText}>
Next
</div>
<div className={styles.arrowHeading}>{nextHeading.title}</div>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="9"
viewBox="0 0 14 9"
className={styles.arrow + " " + styles.nextArrow}
>
<path d="M6.24407 8.12713C6.64284 8.58759 7.35716 8.58759 7.75593 8.12713L13.3613 1.65465C13.9221 1.00701 13.4621 0 12.6053 0H1.39467C0.537918 0 0.0778675 1.00701 0.638743 1.65465L6.24407 8.12713Z" />
</svg>
{nextSection && (
<Link className={styles.next} id={nextSection.id}>
<div>
<div>Next</div>
<div className={styles.arrowHeading}>{nextSection.title}</div>
</div>
<Arrow direction="right" />
</Link>
)}
</div>
);
};
}
const Burger = ({ open, setOpen }: MobileProps) => {
const [prevScrollPos, setPrevScrollPos] = useState(0);
@ -291,3 +271,41 @@ function useDebounce<T>(value: T, delay?: number): T {
return debouncedValue;
}
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 Arrow({ direction }: { direction: "left" | "right" }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="9"
viewBox="0 0 14 9"
className={`${styles.arrow} ${
direction === "left" ? styles.prevArrow : styles.nextArrow
}`}
>
<path d="M6.24407 8.12713C6.64284 8.58759 7.35716 8.58759 7.75593 8.12713L13.3613 1.65465C13.9221 1.00701 13.4621 0 12.6053 0H1.39467C0.537918 0 0.0778675 1.00701 0.638743 1.65465L6.24407 8.12713Z" />
</svg>
);
}

@ -0,0 +1,23 @@
.link,
.links {
display: flex;
align-items: center;
justify-content: center;
}
.links > * {
margin: 0 calc(10rem / 16);
}
.big > * {
width: calc(40rem / 16);
height: calc(40rem / 16);
}
.blue {
fill: var(--blue-1);
}
.white {
fill: var(--white);
}

@ -0,0 +1,194 @@
import React from "react";
import styles from "./SocialLinks.module.css";
interface Props {
color: "white" | "gradient" | "blue";
size: "small" | "big";
}
export const SocialLinks: React.FC<Props> = (props) => {
return (
<div className={styles.links}>
{links.map((Link, index) => (
<Link {...props} key={index} />
))}
</div>
);
};
const iconList = [
{
name: "Facebook",
image: FacebookSvg,
link: "https://www.facebook.com/uw.computerscienceclub",
},
{
name: "Instagram",
image: InstagramSvg,
link: "https://www.instagram.com/uwcsclub/",
},
{
name: "Twitch",
image: TwitchSvg,
link: "https://www.twitch.tv/uwcsclub",
},
{
name: "Discord",
image: DiscordSvg,
link: "https://discord.gg/pHfYBCg",
},
];
const links = iconList.map((icon) => {
function SocialLink({ size, color }: Props) {
return (
<a
key={icon.link}
href={icon.link}
rel="noreferrer"
target="_blank"
className={`${styles.link} ${size === "big" ? styles.big : ""}`}
>
{icon.image(color)}
</a>
);
}
SocialLink.displayName = icon.name;
return SocialLink;
});
export const [Facebook, Instagram, Twitch, Discord] = links;
function InstagramSvg(color: string) {
return (
<svg
width="33"
height="32"
viewBox="0 0 33 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<linearGradient id="bluegreen-gradient">
<stop offset="0%" stopColor="#1481E3" />
<stop offset="100%" stopColor="#4ED4B2" />
</linearGradient>
<path
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
fillRule="evenodd"
clipRule="evenodd"
d="M3.60352 8.00008C3.60352 6.58559 4.16542 5.22904 5.16561 4.22885C6.16581 3.22865 7.52236 2.66675 8.93685 2.66675H24.9368C26.3513 2.66675 27.7079 3.22865 28.7081 4.22885C29.7083 5.22904 30.2702 6.58559 30.2702 8.00008V24.0001C30.2702 25.4146 29.7083 26.7711 28.7081 27.7713C27.7079 28.7715 26.3513 29.3334 24.9368 29.3334H8.93685C7.52236 29.3334 6.16581 28.7715 5.16561 27.7713C4.16542 26.7711 3.60352 25.4146 3.60352 24.0001V8.00008ZM8.93685 5.33341C8.22961 5.33341 7.55133 5.61437 7.05123 6.11446C6.55113 6.61456 6.27018 7.29284 6.27018 8.00008V24.0001C6.27018 24.7073 6.55113 25.3856 7.05123 25.8857C7.55133 26.3858 8.22961 26.6667 8.93685 26.6667H24.9368C25.6441 26.6667 26.3224 26.3858 26.8225 25.8857C27.3226 25.3856 27.6035 24.7073 27.6035 24.0001V8.00008C27.6035 7.29284 27.3226 6.61456 26.8225 6.11446C26.3224 5.61437 25.6441 5.33341 24.9368 5.33341H8.93685ZM16.9368 12.0001C15.876 12.0001 14.8586 12.4215 14.1084 13.1717C13.3583 13.9218 12.9368 14.9392 12.9368 16.0001C12.9368 17.0609 13.3583 18.0784 14.1084 18.8285C14.8586 19.5787 15.876 20.0001 16.9368 20.0001C17.9977 20.0001 19.0151 19.5787 19.7653 18.8285C20.5154 18.0784 20.9368 17.0609 20.9368 16.0001C20.9368 14.9392 20.5154 13.9218 19.7653 13.1717C19.0151 12.4215 17.9977 12.0001 16.9368 12.0001ZM10.2702 16.0001C10.2702 14.232 10.9726 12.5363 12.2228 11.286C13.473 10.0358 15.1687 9.33342 16.9368 9.33342C18.705 9.33342 20.4007 10.0358 21.6509 11.286C22.9011 12.5363 23.6035 14.232 23.6035 16.0001C23.6035 17.7682 22.9011 19.4639 21.6509 20.7141C20.4007 21.9644 18.705 22.6667 16.9368 22.6667C15.1687 22.6667 13.473 21.9644 12.2228 20.7141C10.9726 19.4639 10.2702 17.7682 10.2702 16.0001ZM24.2702 10.6667C24.8006 10.6667 25.3093 10.456 25.6844 10.081C26.0595 9.70589 26.2702 9.19718 26.2702 8.66675C26.2702 8.13632 26.0595 7.62761 25.6844 7.25253C25.3093 6.87746 24.8006 6.66675 24.2702 6.66675C23.7397 6.66675 23.231 6.87746 22.856 7.25253C22.4809 7.62761 22.2702 8.13632 22.2702 8.66675C22.2702 9.19718 22.4809 9.70589 22.856 10.081C23.231 10.456 23.7397 10.6667 24.2702 10.6667Z"
/>
</svg>
);
}
function DiscordSvg(color: string) {
return (
<svg
width="31"
height="30"
viewBox="0 0 31 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>