Merge branch 'main' into feat/organized-content

This commit is contained in:
William Tran 2021-07-04 14:44:12 -04:00
commit 8102daf956
50 changed files with 1181 additions and 382 deletions

View File

@ -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:
- main
refs:
- main
deploy_staging:
stage: .post
needs: ["staging"]
script:
- 'curl -XPOST -H "Authorization: Basic $STAGING_SECRET" "https://csclub.uwaterloo.ca/~a3thakra/csc/"'
only:
refs:
- main

View File

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

40
components/Button.tsx Normal file
View File

@ -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 ?? ""
}`}
/>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

16
components/Footer.tsx Normal file
View File

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

View File

@ -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("/")) {

View File

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

28
components/Link.tsx Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
.navLink {
width: 100%;
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;
padding: 0.5rem 2rem 0.5rem 0.5rem;
}
.navLinkSelected {
/* smaller to account for marker width */
padding: 0.5rem 1.15rem 0.5rem 0.5rem;
.navItem > div {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.selected {
background-color: var(--blue-1-05);
color: var(--blue-2);
font-weight: 700;
}
.readAll {
font-weight: bold;
font-weight: 700;
}
.divider {
border-bottom: 1px solid var(--blue-2);
opacity: 0.25;
width: 100%,
.marker {
display: none;
}
.selectedHeadingArea > .navOption {
color: var(--blue-2);
font-weight: bold;
}
.selectedHeadingArea {
background-color: var(--blue-1-05);
}
.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);
}
.previous {
margin-right: calc(8rem / 16);
}
.next {
justify-content: flex-end;
text-align: end;
margin-left: calc(8rem / 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;
}
.prevNext {
font-size: 0.75rem;
}
.nextText {
text-align: end;
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;

View File

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

View File

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

194
components/SocialLinks.tsx Normal file
View File

@ -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"
>
<defs>
<linearGradient id="bluegreen-gradient">
<stop offset="0%" stopColor="#1481E3" />
<stop offset="100%" stopColor="#4ED4B2" />
</linearGradient>
</defs>
<g clipPath="url(#clip0)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M24.4845 0.872314C26.1684 0.872314 27.5394 2.20412 27.5394 3.85441V29.8247L24.3355 27.0742L22.5323 25.4529L20.6249 23.7302L21.4147 26.4083H4.51584C2.83192 26.4083 1.46094 25.0765 1.46094 23.4262V3.85441C1.46094 2.20412 2.83192 0.872314 4.51584 0.872314H24.4845V0.872314ZM18.7323 19.7782C22.0256 19.6769 23.2923 17.5778 23.2923 17.5778C23.2923 12.9165 21.1464 9.13822 21.1464 9.13822C19.0005 7.57479 16.959 7.61822 16.959 7.61822L16.7504 7.84984C19.2837 8.6026 20.4609 9.68831 20.4609 9.68831C18.9111 8.86317 17.3911 8.45784 15.9755 8.2986C14.9025 8.18279 13.8743 8.21174 12.9653 8.32755L12.7119 8.35651C12.1904 8.39993 10.9237 8.58812 9.32917 9.26851C8.7778 9.5146 8.44996 9.68831 8.44996 9.68831C8.44996 9.68831 9.68682 8.5447 12.3692 7.79193L12.2202 7.61822C12.2202 7.61822 10.1786 7.57479 8.0327 9.13822C8.0327 9.13822 5.88682 12.9165 5.88682 17.5778C5.88682 17.5778 7.13859 19.6769 10.4319 19.7782C10.4319 19.7782 10.9833 19.1268 11.4304 18.5767C9.5378 18.0266 8.82251 16.8685 8.82251 16.8685L9.23976 17.1146L9.29937 17.158L9.35774 17.1906L9.37512 17.1978L9.43349 17.2304C9.80604 17.4331 10.1786 17.5923 10.5213 17.7226C11.1323 17.9542 11.8625 18.1858 12.7119 18.3451C13.8296 18.5477 15.1409 18.6201 16.5715 18.3596C17.2719 18.2437 17.9872 18.0411 18.7323 17.7371C19.2539 17.5489 19.8351 17.2738 20.446 16.883C20.446 16.883 19.7009 18.07 17.7488 18.6056C18.1958 19.1557 18.7323 19.7782 18.7323 19.7782ZM11.8029 13.0178C10.9535 13.0178 10.2829 13.7416 10.2829 14.6247C10.2829 15.5077 10.9684 16.2316 11.8029 16.2316C12.6523 16.2316 13.3229 15.5077 13.3229 14.6247C13.3378 13.7416 12.6523 13.0178 11.8029 13.0178ZM17.2421 13.0178C16.3927 13.0178 15.7221 13.7416 15.7221 14.6247C15.7221 15.5077 16.4076 16.2316 17.2421 16.2316C18.0915 16.2316 18.7621 15.5077 18.7621 14.6247C18.7621 13.7416 18.0915 13.0178 17.2421 13.0178Z"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
/>
</g>
<defs>
<clipPath id="clip0">
<rect
width="29.8039"
height="28.9524"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
transform="translate(0.217773 0.872314)"
/>
</clipPath>
</defs>
</svg>
);
}
function TwitchSvg(color: string) {
return (
<svg
width="26"
height="31"
viewBox="0 0 26 31"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<linearGradient id="bluegreen-gradient">
<stop offset="0%" stopColor="#1481E3" />
<stop offset="100%" stopColor="#4ED4B2" />
</linearGradient>
<g clipPath="url(#clip0)">
<path
d="M5.83051 0.872314L0.535156 6.15123V25.1553H6.88958V30.4342L12.1849 25.1553H16.4212L25.9529 15.6533V0.872314H5.83051ZM23.8347 14.5975L19.5984 18.8206H15.3622L11.6554 22.5159V18.8206H6.88958V2.98388H23.8347V14.5975Z"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
/>
<path
d="M20.6572 6.6792H18.5391V13.0139H20.6572V6.6792Z"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
/>
<path
d="M14.832 6.6792H12.7139V13.0139H14.832V6.6792Z"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
/>
</g>
<defs>
<clipPath id="clip0">
<rect
width="25.4177"
height="29.5619"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
transform="translate(0.535156 0.872314)"
/>
</clipPath>
</defs>
</svg>
);
}
function FacebookSvg(color: string) {
return (
<svg
width="32"
height="32"
viewBox="0 0 32 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>