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>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.33268 2.66675H26.666C28.1388 2.66675 29.3327 3.86066 29.3327 5.33342V26.6668C29.3327 28.1395 28.1388 29.3334 26.666 29.3334H5.33268C3.85992 29.3334 2.66602 28.1395 2.66602 26.6668V5.33342C2.66602 3.86066 3.85992 2.66675 5.33268 2.66675ZM5.3311 5.33333V26.6667H26.6644V5.33333H5.3311ZM14.8428 16.0059H17.332V24H19.9987V16.0059H22.6419V13.3392H19.9987V12C19.9987 11.2636 20.5957 10.6667 21.332 10.6667H22.6654V8H21.332C19.1229 8 17.332 9.79086 17.332 12V13.3392H14.8428V16.0059Z"
className={color !== "gradient" ? styles[color] : ""}
fill={color === "gradient" ? "url(#bluegreen-gradient)" : ""}
/>
</svg>
);
}

View File

@ -17,12 +17,11 @@
}
.name {
font-weight: bold;
color: var(--blue-1);
font-weight: 700;
color: var(--blue-2);
}
.role {
font-weight: 600;
color: var(--purple-2);
line-height: 1.875;
}

View File

@ -9,8 +9,6 @@
row-gap: 0;
justify-items: start;
align-items: start;
max-width: calc(847rem / 16);
}
.picture {
@ -31,47 +29,26 @@
grid-area: name;
margin: 0;
color: var(--blue-1);
color: var(--blue-2);
font-size: calc(36rem / 16);
line-height: 1.1;
font-weight: 700;
font-weight: 600;
}
.role {
grid-area: role;
margin: 0;
color: var(--purple-2);
font-size: calc(24rem / 16);
line-height: 1.7;
line-height: calc(40 / 24);
font-weight: 600;
}
.description {
grid-area: description;
margin: 0;
line-height: 1.875;
}
@media screen and (max-width: calc(768rem / 16)) {
.card {
grid-template-columns: calc(90rem / 16) auto;
column-gap: calc(22rem / 16);
max-width: calc(460rem / 16);
}
.picture {
max-width: calc(90rem / 16);
max-height: calc(90rem / 16);
}
.name,
.role,
.description {
line-height: 1.5;
}
.description > *:first-child {
margin-top: 0;
}
/* TODO: Use the correct mobile styles from figma

View File

@ -1,7 +1,3 @@
.miniEventCardDemo > *:nth-child(odd) {
background: #e1eefa;
}
.newsDemo {
padding: calc(50rem / 16);
background-color: var(--off-white);
@ -13,7 +9,7 @@
font-weight: bold;
color: var(--purple-2);
font-size: calc(24rem / 16);
line-height: calc(36rem / 16);
line-height: calc(36 / 24);
margin-bottom: calc(14rem / 16);
}
@ -21,7 +17,7 @@
font-style: normal;
font-weight: normal;
font-size: calc(14rem / 16);
line-height: calc(21rem / 16);
line-height: calc(21 / 14);
white-space: pre-line;
color: var(--purple-2);
vertical-align: baseline;
@ -53,7 +49,6 @@
}
.teamMemberDemo {
padding: calc(10rem / 16) calc(50rem / 16) calc(30rem / 16) calc(50rem / 16);
max-width: calc(847rem / 16);
}
@ -62,7 +57,7 @@
color: var(--purple-2);
font-weight: 600;
font-size: calc(24rem / 16);
line-height: calc(36rem / 16);
line-height: calc(36 / 24);
}
.teamMemberDemo > hr {
@ -81,13 +76,25 @@
column-gap: calc(53rem / 16);
}
.linkDemo {
padding: calc(50rem / 16);
background-color: var(--off-white);
}
.linkTitle {
font-weight: bold;
color: var(--purple-2);
font-size: calc(24rem / 16);
}
@media only screen and (max-width: calc(768rem / 16)) {
.newsDemo,
.eventDescriptionCardDemo {
background-color: #e1eefa;
padding: calc(20rem / 16);
background-color: var(--off-white);
}
.eventDescriptionCardDemo > * {
margin: calc(50rem / 16);
margin: 0;
}
}

View File

@ -11,27 +11,24 @@ import AltTab, {
metadata as altTabEventMetadata,
} from "../content/playground/alt-tab.event.mdx";
import ReadAll, {
metadata as readAllOrganizedContentMetadata,
} from "../content/playground/constitution/read-all.organized-content.mdx";
import Name, {
metadata as nameOrganizedContentMetadata,
} from "../content/playground/constitution/name.organized-content.mdx";
} from "../content/playground/constitution/name.section.mdx";
import Purpose, {
metadata as purposeOrganizedContentMetadata,
} from "../content/playground/constitution/purpose.organized-content.mdx";
} from "../content/playground/constitution/purpose.section.mdx";
import Membership, {
metadata as membershipOrganizedContentMetadata,
} from "../content/playground/constitution/membership.organized-content.mdx";
} from "../content/playground/constitution/membership.section.mdx";
import Officers, {
metadata as officersOrganizedContentMetadata,
} from "../content/playground/constitution/officers.organized-content.mdx";
} from "../content/playground/constitution/officers.section.mdx";
import Duties, {
metadata as dutiesOrganizedContentMetadata,
} from "../content/playground/constitution/duties-of-officers.organized-content.mdx";
} from "../content/playground/constitution/duties-of-officers.section.mdx";
import ExecutiveCouncil, {
metadata as executiveCouncilOrganizedContentMetadata,
} from "../content/playground/constitution/executive-council.organized-content.mdx";
} from "../content/playground/constitution/executive-council.section.mdx";
import UnavailableContent, {
metadata as unavailableMetadata,
@ -43,11 +40,13 @@ import CodeyInfo, {
import { MiniEventCard } from "./MiniEventCard";
import { NewsCard } from "./NewsCard";
import { Link } from "./Link";
import { EventCard } from "./EventCard";
import { EventDescriptionCard } from "./EventDescriptionCard";
import { TeamMember } from "./TeamMember";
import { TeamMemberCard } from "./TeamMemberCard";
import { OrganizedContent, LinkProps } from "./OrganizedContent";
import { Button } from "./Button";
const events = [
{ Content: OOTBReact, metadata: OOTBReactEventMetadata },
@ -56,14 +55,13 @@ const events = [
];
const constitution = [
{ content: <ReadAll />, ...readAllOrganizedContentMetadata },
{ content: <Name />, ...nameOrganizedContentMetadata },
{ content: <Purpose />, ...purposeOrganizedContentMetadata },
{ content: <Membership />, ...membershipOrganizedContentMetadata },
{ content: <Officers />, ...officersOrganizedContentMetadata },
{ content: <Duties />, ...dutiesOrganizedContentMetadata },
{ Content: Name, ...nameOrganizedContentMetadata },
{ Content: Purpose, ...purposeOrganizedContentMetadata },
{ Content: Membership, ...membershipOrganizedContentMetadata },
{ Content: Officers, ...officersOrganizedContentMetadata },
{ Content: Duties, ...dutiesOrganizedContentMetadata },
{
content: <ExecutiveCouncil />,
Content: ExecutiveCouncil,
...executiveCouncilOrganizedContentMetadata,
},
];
@ -99,6 +97,31 @@ export function NewsCardDemo() {
);
}
export function ButtonDemo() {
return (
<>
<h3>Standard buttons</h3>
<p>
<Button isLink={true} href="/">
Link
</Button>
</p>
<p>
<Button>Button</Button>
</p>
<h3>Small buttons</h3>
<p>
<Button isLink={true} href="/" size="small">
Small Link
</Button>
</p>
<p>
<Button size="small">Small Button</Button>
</p>
</>
);
}
export function EventDescriptionCardDemo() {
return (
<div className={styles.eventDescriptionCardDemo}>
@ -106,9 +129,7 @@ export function EventDescriptionCardDemo() {
<EventDescriptionCard
{...metadata}
key={metadata.name + metadata.date.toString()}
>
{metadata.short}
</EventDescriptionCard>
/>
))}
</div>
);
@ -162,31 +183,37 @@ export function TeamMemberCardDemo() {
);
}
export function LinkDemo() {
return (
<div className={styles.linkDemo}>
<div className={styles.linkTitle}>Elections</div>
<p>
To find out when and where the next elections will be held, keep an eye
on the <Link href="https://csclub.uwaterloo.ca/news/">News</Link>. For
details on the elections, read our{" "}
<Link href="https://csclub.uwaterloo.ca/about/constitution">
Constitution
</Link>
.
</p>
</div>
);
}
export function OrganizedContentDemo() {
const sections = constitution;
const [index, setIndex] = useState(0);
const [id, setId] = useState(sections[0].id);
function FakeLink({ className, url, children }: LinkProps) {
function FakeLink({ className, id, children }: LinkProps) {
return (
<div
className={className}
onClick={() => {
const target = sections.findIndex((section) => section.url === url);
if (target >= 0) {
setIndex(target);
}
}}
>
<div className={className} onClick={() => setId(id)}>
{children}
</div>
);
}
return (
<OrganizedContent headings={sections} currentIndex={index} link={FakeLink}>
{sections[index].content}
</OrganizedContent>
<OrganizedContent sections={sections} currentId={id} link={FakeLink} />
);
}

View File

@ -0,0 +1,31 @@
## Mathematics Endowment Fund
The [Mathematics Endowment Fund](https://uwaterloo.ca/math-endowment-fund/) has provided funding for a variety of projects, events, and hardware:
- A new disk shelf (24 disks x 2TB) for the NetApp filers donated by CSCF and MFCF, to be used to improve home directories
- Ginkgo, a server to improve CSC web hosting services
- Drives for Potassium-Benzoate, the server that runs [mirror.csclub.uwaterloo.ca](http://mirror.csclub.uwaterloo.ca/)
- Potassium-Benzoate, the server that runs [mirror.csclub.uwaterloo.ca](http://mirror.csclub.uwaterloo.ca/)
- High-Fructose Corn Syrup, a server used for high performance computing
- Sodium-Benzoate, the server that previously ran [mirror.csclub.uwaterloo.ca](http://mirror.csclub.uwaterloo.ca/)
- A gigabit switch for the MathSoc offices (including ours)
- Academic talks
- Linux CDs for first-year students
- Caffeine, our primary server
- Various books for the CSC library
The Computer Science club graciously thanks the Mathematics Endowment Fund for their financial support.
## MathSoc Capital Improvements Fund
The [MathSoc Capital Improvements Fund](http://mathsoc.uwaterloo.ca/council/policies/Capital+Improvements+Fund) has provided funding of new office hardware:
- Strombola and Bit-Shifter hardware upgrade (new motherboard, CPU and RAM)
- 2x 8-port 10Gbps SFP+ network cards for the Math Student Orgs routers/switches
- Natural-Flavours hardware upgrade (new motherboard, CPU and RAM)
## Student Life Endowment Fund
The [Student Life Endowment Fund](https://feds.ca/funding#fund-slef) has provided funding of new hardware:
- Biloba, a server to improve CSC web hosting services

View File

@ -1,7 +1,7 @@
export const metadata = {
name: "Afterhours: Personal Relationships",
short: "Learn how React works and make your own version!",
date: new Date("2021-03-02 2:00 PM"),
date: new Date("March 25, 2021 19:00:00 GMT-4"),
online: false,
location: "MC",
registerLink: "http://csclub.uwaterloo.ca/",

View File

@ -1,7 +1,7 @@
export const metadata = {
name: "Alt-Tab",
short: "CSC is proud to present to you Alt-Tab!",
date: new Date("March 25, 2021 19:00:00"),
date: new Date("March 25, 2021 19:00:00 GMT-4"),
online: true,
location: "Twitch",
poster: "/images/playground/alt-tab.jpg",

View File

@ -0,0 +1,27 @@
export const metadata = {
title: "5. Duties of Officers",
id: "duties-of-officers"
};
The duties of the President shall be:
to call and preside at all general, special, and executive meetings of the Club, except during the election of officers;
to appoint special committees of the Club and the membership and chairs of such committees, with the approval of the Executive Council; and
to audit, or to appoint a representative to audit, the financial records of the club at the end of each academic term.
with the approval of the Faculty Advisor, rule on any point of procedure under the constitution that arises outside of a meeting.
The duties of the Vice-President shall be:
to assume the duties of the President in the event of the President's absence;
to chair the Programme Committee;
to appoint members to and remove members from the Programme Committee;
to ensure that Club events are held regularly; and
to assume those duties of the President that are delegated to them by the President.
The duties of the Secretary shall be:
to keep minutes of all Club meetings;
to care for all Club correspondence; and
manage any persons appointed to internal positions by the Executive.
The duties of the Treasurer shall be:
to collect dues and maintain all financial and membership records;
to produce a financial or membership statement when requested.
The duties of the System Administrator shall be:
to chair the Systems Committee;
to appoint members to and remove members from the Systems Committee.
to ensure that the duties of the Systems Committee are performed.

View File

@ -0,0 +1,10 @@
export const metadata = {
title: "6. Executive Council",
id: "executive-council"
};
The Executive Council shall consist of the present officers of the Club and the Faculty Advisor (as a non-voting member) and has the power to run the affairs of this club within the limits of this constitution. This includes the power to overrule or issue directions to any officer.
The Executive Council may appoint people to various positions to help manage the Club.
The Executive Council must obey any instructions given to it by the members at a meeting and can be overruled by them.
The Executive Council can act by consensus achieved on their mailing list.
Minutes of the Executive Council meetings shall be available for inspection by any member of the Club and shall be filed with the Club records. On request, a member shall be shown the archive of any thread on the Executive Council mailing list which resulted in a decision being made.

View File

@ -0,0 +1,10 @@
export const metadata = {
title: "3. Membership",
id: "membership"
};
In compliance with MathSoc regulations and in recognition of the club being primarily targeted at undergraduate students, full membership is open to all Social Members of the Mathematics Society and restricted to the same.
Affiliate membership in this Club shall be open to all members of the University community, including alumni. Affiliate members shall have all the rights of full members except for the rights of voting and holding executive office.
Membership shall be accounted for on a termly basis, where a term begins at the start of lectures in Winter or Spring, and at the start of Orientation Week in Fall.
A person is not a member until he or she has paid the current membership fee and has been enrolled in the member database. The termly membership fee is set from time to time by the Executive. Under conditions approved by the Executive, a member who purchases a membership at the end of the current term may be given membership for both the current term and the next term. If the membership fee changes, then this does not affect the validity of any membership terms already paid for.
The Club may grant access to its systems, either free of charge or for a fee, to members of the University community in order to offer them services. This does not constitute membership.

View File

@ -0,0 +1,6 @@
export const metadata = {
title: "1. Name",
id: "name"
};
The name of this organization shall be the "Computer Science Club of the University of Waterloo".

View File

@ -0,0 +1,30 @@
export const metadata = {
title: "4. Officers",
id: "officers"
};
The officers of the Club shall be:
President
Vice-President
Secretary
Treasurer
System Administrator
There shall additionally be a Faculty Advisor, selected by the Executive from time to time from among the faculty of the School of Computer Science. The Faculty Advisor shall be an ex-officio affiliate member of the Club.
The choice of officers shall be limited to full members of the Club.
All officers, other than the System Administrator, shall be elected at a meeting to be held no later than two weeks after the start of lectures in each term.
The election of officers shall be accomplished by the following procedure:
Before the end of the prior term, the then-Executive shall choose a willing Chief Returning Officer, who is responsible for carrying out elections according to this procedure.
The CRO shall set the date and time of the election meeting, and set the nomination period. The nomination shall be at least one week long and shall end at least 24 hours before the start of the election meeting.
Announcements of the election and the nomination procedure must be distributed to all members by the members' mailing list, and should also be advertised by posters in the MC building.
During the nomination period, the Chief Returning Officer (CRO) shall be available to receive nominations for the posts of officers of the club, either in person, by email, by depositing nomination forms in the CSC's mailbox in the MathSoc office, or by writing the nomination in a place in the CSC office to be specified by the CRO.
A nomination shall consist of the nominee's userid, and post(s) nominated for. Nominees must be full members of the Computer Science Club. A member may decline a nomination at any point prior to the taking of the vote.
The election shall commence with the offering of memberships for sale. After a reasonable time, control of the meeting is given to the CRO who will preside over the election of the President, Vice-President, Treasurer, and Secretary, in that order.
During each election, if the position has no nominees, the CRO will take nominations from the floor. Any present, eligible member can be nominated.
Each election shall be carried out by secret vote, in a manner to be decided on by the CRO, with the approval of the members at the meeting. A simple heads-down-hands-up method is considered acceptable.
The CRO shall not vote except to break a tie.
The CRO may, if feasible, accept absentee ballots from full members. No absentee vote from a member shall be counted if the member is present at the time the vote is taken. The CRO shall make a best effort to ensure that absentee ballots are compatible with the method of voting chosen; if this is not possible (for instance, if the CRO is overruled by the membership), then the absentee votes shall not be counted.
Immediately after the vote is taken, the CRO will announce the results of the election and the winner will be removed from subsequent contests. If, due to lack of candidates (because there were no nominations, or candidates withdrew or were eliminated), there is no one elected to an office, then the members at the meeting will decide whether or not to hold extra elections in accordance with the procedure for vacancies. If they choose not to, this does not prevent the Executive or a group of members from calling extra elections later in the term in accordance with the usual vacancy provisions.
Following the elections, it is the responsibility of the new executive to select a System Administrator. The selection of System Administrator must then be ratified by the members at the meeting. If a suitable System Administrator is not available, then the executive may delay their selection until one becomes available. In this case the selection of System Administrator must be ratified at the next meeting of the Club.
Any two offices may be held by a single person with the approval of the President (if any), and the explicit approval of the members.
In the case of a resignation of an officer or officers, including the President, or if a vacancy occurs for any other reason, the Executive, members at a meeting, or any ten (10) members may call extra elections to replace such officer(s). If extra elections are held, they are held for all vacant offices.
Whenever extra elections are held, they shall follow the usual election procedure. If they are held after elections failed to elect an officer, then the nomination period may be shortened to less than a week in order to allow the extra elections to take place at the same date and time in the following week. The Executive (or the ten (10) members who called the election) may appoint a replacement CRO if the previous CRO is unwilling or unable to fulfill their duties.

View File

@ -0,0 +1,10 @@
export const metadata = {
title: "2. Purpose",
id: "purpose"
};
The Club is organized and will be operated exclusively for educational and scientific purposes in furtherance of:
promoting an increased knowledge of computer science and its applications;
promoting a greater interest in computer science and its applications; and
providing a means of communication between persons having interest in computer science.
The above purposes will be fulfilled by the organization of discussions and lectures with professionals and academics in the field of computer science and related fields, the maintenance of a library of materials related to computer science, the maintenance of an office containing the library as an aid to aim (1.3) above, and such other means as decided by the club membership.

View File

@ -1,7 +1,7 @@
export const metadata = {
name: "Out of the Box: React",
short: "Out of the Box is a series of code-along projects that explore what's under the hood of modern web frameworks.",
date: new Date("March 23, 2021 19:00:00"),
date: new Date("March 23, 2021 19:00:00 GMT-4"),
online: true,
location: "Twitch",
poster: "/images/playground/intro-ootb.jpg",

16
next-env.d.ts vendored
View File

@ -49,16 +49,24 @@ declare module "*.team-member.mdx" {
export default ReactComponent;
}
declare module "*.organized-content.mdx" {
declare module "*.section.mdx" {
import { ComponentType } from "react";
interface OrganizedContentMetadata {
interface SectionMetadata {
title: string;
url: string;
id: string;
}
const ReactComponent: ComponentType;
export const metadata: OrganizedContentMetadata;
export const metadata: SectionMetadata;
export default ReactComponent;
}
declare module "*.mdx" {
import { ComponentType } from "react";
const ReactComponent: ComponentType;
export default ReactComponent;
}

View File

@ -5,5 +5,5 @@ const withMDX = require("@next/mdx")({
module.exports = withMDX({
pageExtensions: ["ts", "tsx", "mdx"],
trailingSlash: true,
basePath: process.env.BASE_PATH,
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
});

View File

@ -1,26 +1,33 @@
body {
font-family: "Poppins", "sans-serif";
font-size: 1rem;
margin: 0;
/* Default is light theme */
--white: #ffffff;
--off-white: #fdf8f5;
--teal-1: #76ffdc;
--teal-2: #4ed4b2;
--teal-2-20: #4ed4b234;
--blue-1: #5caff9;
--blue-1-05: #5caff90d;
--blue-1-20: #5caff934;
--blue-2: #1482e3;
--light-blue-1: #E1EEFA;
--light-blue-2: #C0E1FE;
--blue-2-25: #1482e340;
--purple-1: #525284;
--purple-2: #2a2a62;
--black: #000000;
--gradient-blue-green: linear-gradient(
99.94deg,
#1481e3 -17.95%,
#4ed4b2 172.82%
);
color: var(--black);
font-family: "Poppins", "sans-serif";
font-size: 1rem;
font-weight: 400;
line-height: calc(30 / 16);
margin: 0;
}
/* FIXME: Dark theme is the same as light theme right now */
@ -44,8 +51,33 @@ body {
);
}
@media only screen and (max-width: calc(375rem / 16)) {
body {
font-size: calc(14rem / 16);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-style: normal;
line-height: 1.5;
color: var(--purple-2);
}
h1 {
font-size: calc(48rem / 16);
font-weight: 700;
}
h2 {
font-size: calc(36rem / 16);
font-weight: 700;
}
h3 {
font-size: calc(24rem / 16);
font-weight: 600;
}
h4 {
font-size: calc(18rem / 16);
font-weight: 600;
}

19
pages/_app.module.css Normal file
View File

@ -0,0 +1,19 @@
.appContainer {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* This makes the footer stay at the bottom, even if there's not much content
* on the screen.
*/
.contentContainer {
flex-grow: 1;
}
.defaultLayout {
margin: 0 auto;
max-width: calc(800rem / 16);
padding: 0 calc(20rem / 16);
padding-bottom: calc(20rem / 16);
}

View File

@ -1,18 +1,49 @@
import React from "react";
import { AppProps } from "next/app";
import React, { ComponentType, ReactNode } from "react";
import { NextComponentType, NextPageContext } from "next";
import { AppProps as DefaultAppProps } from "next/app";
import { MDXProvider } from "@mdx-js/react";
import { ThemeProvider } from "../components/theme";
import { Navbar } from "../components/Navbar";
import { Footer } from "../components/Footer";
import { Link } from "../components/Link";
import styles from "./_app.module.css";
import "./_app.css";
import "./font.css";
export default function App({ Component, pageProps }: AppProps): JSX.Element {
const Layout = Component.Layout ?? DefaultLayout;
return (
<ThemeProvider theme="light">
<MDXProvider components={{}}>
{/* <Navbar /> */}
<Component {...pageProps} />
<MDXProvider components={{ a: Link }}>
<div className={styles.appContainer}>
<Navbar />
{/* Wrapping content with a div to allow for a display: block parent */}
<div className={styles.contentContainer}>
<Layout>
<Component {...pageProps} />
</Layout>
</div>
<Footer />
</div>
</MDXProvider>
</ThemeProvider>
);
}
function DefaultLayout(props: { children: React.ReactNode }) {
return <div className={styles.defaultLayout}>{props.children}</div>;
}
type PageComponent = NextComponentType<
NextPageContext,
unknown,
Record<string, unknown>
> & {
Layout?: ComponentType<{ children: ReactNode }>;
};
type AppProps = DefaultAppProps & {
Component: PageComponent;
};

View File

@ -0,0 +1,42 @@
.headerContainer {
display: flex;
flex-direction: row;
align-items: flex-end;
padding-bottom: 1rem;
border-bottom: calc(1rem / 16) solid var(--purple-2);
}
.header {
color: var(--purple-2);
font-size: calc(48rem / 16);
margin: 0 1rem 0 0;
text-align: center;
}
.content h2 {
font-weight: 600;
font-size: calc(24rem / 16);
color: var(--blue-2);
margin-top: calc(35rem / 16);
}
@media only screen and (max-width: calc(768rem / 16)) {
.headerContainer {
flex-direction: column-reverse;
align-items: center;
border: none;
}
.header {
font-size: calc(24rem / 16);
}
.codey {
width: calc(100rem / 16);
}
.content ol,
.content ul {
padding: 0 1rem;
}
}

View File

@ -0,0 +1,21 @@
import React from "react";
import { Image } from "../../components/Image";
import Content from "../../content/our-supporters.mdx";
import styles from "./our-supporters.module.css";
export default function OurSupporters() {
return (
<>
<div className={styles.headerContainer}>
<h1 className={styles.header}>Our Supporters</h1>
<Image
src="our-supporters/supporters-codey.svg"
className={styles.codey}
/>
</div>
<div className={styles.content}>
<Content />
</div>
</>
);
}

View File

@ -2,10 +2,12 @@ import {
MiniEventCardDemo,
NewsCardDemo,
EventDescriptionCardDemo,
LinkDemo,
EventCardDemo,
TeamMemberDemo,
TeamMemberCardDemo,
OrganizedContentDemo,
ButtonDemo,
} from "../components/playground";
import { TeamMemberCard } from "../components/TeamMemberCard";
@ -37,6 +39,14 @@ unavailable
---
## `<Button />`
The `<Button />` is customizable in size and in whether it is an anchor tag or a button tag.
<ButtonDemo />
---
## `<EventDescriptionCard />`
The `<EventDescriptionCard />` component is used on the home page, and uses the
@ -57,7 +67,7 @@ The `<EventCard />` component is used on the events page, and uses the
## `<TeamMember />`
The `<TeamMember />` component has an image of the team member along with their name and role.
The `<TeamMember />` component has an image of the team member along with their name and role.
It is used on the Meet the Team page for non executive members.
<TeamMemberDemo />
@ -73,3 +83,10 @@ display information about the execs: prez, VP, trez, AVP, and syscom overlord.
---
## `<Link />`
The `<Link />` component is used on various pages such as Meet the Team! and Our Supporters
<LinkDemo />
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 900 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

View File

@ -0,0 +1,38 @@
<svg width="164" height="152" viewBox="0 0 164 152" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M60.6987 10.1311C60.6987 10.1311 65.5243 13.8573 67.5873 17.0706C70.5243 21.6451 71.2736 30.4939 71.2736 30.4939L50.5187 30.6942C50.5187 30.6942 51.4565 21.8685 54.3141 17.1987C56.2555 14.026 60.6987 10.1311 60.6987 10.1311Z" fill="#5CAFF9"/>
<path d="M60.7055 15.483C60.7055 15.483 64.1715 17.8748 65.652 19.9389C67.7597 22.8775 68.2919 28.5653 68.2919 28.5653L53.373 28.7093C53.373 28.7093 54.0536 23.0351 56.111 20.031C57.5088 17.99 60.7055 15.483 60.7055 15.483Z" fill="white"/>
<path d="M95.2108 18.1675C95.2108 18.1675 89.2355 19.3787 85.9652 21.3501C81.3096 24.1568 76.7284 31.7643 76.7284 31.7643L95.2599 41.1124C95.2599 41.1124 98.3173 32.7803 97.8166 27.3285C97.4764 23.6246 95.2108 18.1675 95.2108 18.1675Z" fill="#5CAFF9"/>
<path d="M92.5575 23.0171C92.5575 23.0171 88.0539 23.8047 85.6109 25.1942C82.1329 27.1723 78.7914 32.6681 78.7914 32.6681L92.9771 39.824C92.9771 39.824 95.153 33.7721 94.683 29.7706C94.3637 27.052 92.5575 23.0171 92.5575 23.0171Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.9722 122.412C71.9599 124.118 66.6098 124.459 61.1223 123.181C40.6941 118.424 21.5523 102.08 32.3877 66.0153C33.4918 63.333 34.5321 60.7517 35.5289 58.2782C45.9438 32.4358 51.6121 18.3708 75.6814 23.9753C95.5913 28.6113 98.2601 44.8237 100.996 61.4422C101.845 66.6018 102.701 71.8004 104.081 76.704C117.072 85.0493 122.021 97.3949 119.091 109.978C115.301 126.256 100.664 127.341 76.9722 122.412Z" fill="#5CAFF9"/>
<path d="M49.0293 78.6378L54.172 95.5727C54.172 95.5727 12.7709 106.808 11.8631 106.287C10.9554 105.765 7.99875 97.0159 8.64895 95.7023C9.00861 94.9757 22.7298 89.2279 33.7886 85.1795C44.1371 80.4424 49.0293 78.6378 49.0293 78.6378Z" fill="#5CAFF9"/>
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="20" y="16" width="116" height="120">
<path fill-rule="evenodd" clip-rule="evenodd" d="M76.8037 122.373C71.7915 124.079 66.4416 124.42 60.9544 123.142C40.5261 118.385 21.3843 102.041 32.2198 65.9762C33.3238 63.2939 34.3642 60.7126 35.361 58.2392C45.7758 32.3968 51.4442 18.3317 75.5134 23.9362C95.4233 28.5723 98.0921 44.7847 100.828 61.4031C101.677 66.5631 102.533 71.7622 103.913 76.666C116.903 85.0113 121.852 97.3564 118.922 109.939C115.132 126.217 100.495 127.302 76.8037 122.373Z" fill="#5CAFF9"/>
</mask>
<g mask="url(#mask0)">
<path d="M57.0627 89.3998C53.5322 111.35 60.451 134.192 43.0797 121.691C38.2814 120.178 33.901 106.779 37.4316 84.8287C40.9621 62.8786 49.8813 44.5761 64.0963 46.8625C71.8227 48.1053 79.2637 54.6767 74.0675 67.1846C71.5001 73.3646 58.254 81.9934 57.0627 89.3998Z" fill="white"/>
</g>
<ellipse cx="122.342" cy="77.1764" rx="18.989" ry="20.8326" transform="rotate(-21.9209 122.342 77.1764)" fill="#5CAFF9"/>
<circle cx="50.6767" cy="38.7855" r="12.8002" transform="rotate(13.1078 50.6767 38.7855)" stroke="#2A2A62" stroke-width="2.05361"/>
<circle cx="81.0087" cy="45.0912" r="12.8002" transform="rotate(13.1078 81.0087 45.0912)" stroke="#2A2A62" stroke-width="2.05361"/>
<path d="M61.5943 44.7352C61.5943 44.7352 63.8927 44.7025 65.3108 45.0327C66.729 45.3629 68.7765 46.4076 68.7765 46.4076L68.2748 48.5623C68.2748 48.5623 66.1767 47.6951 64.7673 47.367C63.3579 47.0388 61.0926 46.8899 61.0926 46.8899L61.5943 44.7352Z" fill="#2A2A62"/>
<path d="M93.1609 119.081L89.7846 97.0617C89.7846 97.0617 96.9909 91.9125 105.424 92.3482C113.857 92.7839 119.891 98.3109 119.891 98.3109C119.891 98.3109 141.191 115.598 141.269 116.504C141.347 117.411 137.173 125.646 136.058 126.261C135.44 126.601 126.141 124.61 118.656 121.361C101.128 131.273 93.1609 119.081 93.1609 119.081Z" fill="#5CAFF9"/>
<path d="M63.2051 99.4518L69.0121 118.574C69.0121 118.574 36.5634 126.918 35.7775 126.257C34.9916 125.595 31.7487 115.687 32.1481 114.305C32.3691 113.541 42.9271 108.55 51.4931 105.169C59.417 100.962 63.2051 99.4518 63.2051 99.4518Z" fill="#5CAFF9"/>
<g clip-path="url(#clip0)">
<path d="M70.8476 57.937C70.0636 63.696 62.8994 63.3647 58.6038 61.399C54.3164 59.398 53.978 53.6747 55.4535 50.5275C56.9372 47.3451 61.5802 46.3838 65.8676 48.3848C67.7244 49.8198 67.8388 50.9234 68.7764 52.4786C69.8844 54.2591 70.0962 55.4225 70.8476 57.937Z" fill="white"/>
<path d="M61.9285 51.5898C63.7767 52.0201 65.4953 51.4227 65.7671 50.2554C66.0389 49.0882 64.761 47.7931 62.9128 47.3627C61.0646 46.9324 59.3461 47.5298 59.0743 48.697C58.8025 49.8643 60.0804 51.1594 61.9285 51.5898Z" fill="#2A2A62"/>
<path d="M70.0925 60.5456C69.015 61.186 67.6615 61.5764 66.4195 61.8071C64.7841 62.1319 63.2357 62.4026 61.418 61.7565C56.802 60.0504 54.6116 55.9011 55.9178 52.0461C56.8369 49.215 59.6433 47.529 62.845 47.4947C58.7752 46.4727 55.7647 48.5569 54.502 52.3849C53.1303 56.5217 55.7342 61.2872 60.3501 62.9933C64.1101 64.3887 68.0403 63.2986 70.0925 60.5456Z" fill="#2A2A62"/>
<path d="M60.9848 39.0838C61.4843 38.219 60.7674 36.87 59.3837 36.0708C57.9999 35.2717 56.4732 35.3249 55.9737 36.1898C55.4742 37.0546 56.1911 38.4036 57.5749 39.2028C58.9586 40.0019 60.4853 39.9487 60.9848 39.0838Z" fill="white"/>
<path d="M76.2391 43.963C77.8338 43.8599 79.0743 42.9683 79.0099 41.9716C78.9454 40.9749 77.6003 40.2505 76.0056 40.3537C74.4109 40.4569 73.1704 41.3485 73.2349 42.3452C73.2994 43.3419 74.6444 44.0662 76.2391 43.963Z" fill="white"/>
<path opacity="0.5" d="M61.5594 53.1746L63.4289 56.4693L59.8644 59.0186L58.1764 54.4664L61.5594 53.1746Z" fill="#5CAFF9"/>
<path d="M61.9284 51.589C61.9284 51.589 60.5466 56.7261 66.2333 55.7479" stroke="#2A2A62" stroke-width="2.01439" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M62.1003 50.8498C62.1003 50.8498 60.7305 56.7325 57.0458 52.9408" stroke="#2A2A62" stroke-width="2.01439" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M63.6088 55.6943C63.6088 55.6943 58.9926 63.7175 58.1064 53.9674" stroke="#2A2A62" stroke-width="2.01439" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M70.8316 46.5814C70.202 47.5705 70.9875 48.7695 71.8456 48.1325C72.4826 47.6831 73.2615 47.3863 74.1031 47.5822C74.9026 47.7684 75.4702 48.3788 75.8432 49.0633C76.3448 49.9572 77.5082 49.1522 77.3671 48.0434C77.2756 47.4841 77.113 46.8485 76.7979 46.297C75.3218 43.6819 72.4802 44.4547 71.1608 46.1201C71.0371 46.2706 70.9133 46.4211 70.8316 46.5814Z" fill="#2A2A62"/>
<path d="M53.2183 42.4802C52.5888 43.4693 53.3742 44.6683 54.2323 44.0313C54.8693 43.5819 55.6482 43.2851 56.4898 43.481C57.2893 43.6672 57.857 44.2776 58.2299 44.9621C58.7315 45.856 59.8949 45.051 59.7539 43.9422C59.6623 43.3829 59.4997 42.7473 59.1846 42.1958C57.7085 39.5807 54.8669 40.3536 53.5475 42.0189C53.4238 42.1694 53.3 42.3199 53.2183 42.4802Z" fill="#2A2A62"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="25.1365" height="26.2577" fill="white" transform="translate(54.9102 34.8413) rotate(13.1078)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB