Compare commits
76 Commits
shahanneda
...
main
@ -0,0 +1,84 @@ |
||||
--- |
||||
kind: pipeline |
||||
type: docker |
||||
name: node16 |
||||
|
||||
steps: |
||||
- name: view-staging-url |
||||
image: node:16 |
||||
commands: |
||||
- echo "staging url will be https://${DRONE_BRANCH//\//-}-csc-class-profile-staging-snedadah.k8s.csclub.cloud" |
||||
|
||||
- name: install-deps |
||||
image: node:16 |
||||
commands: |
||||
- npm install |
||||
|
||||
- name: lint |
||||
image: node:16 |
||||
depends_on: |
||||
- install-deps |
||||
commands: |
||||
- npm run lint |
||||
|
||||
- name: build |
||||
image: node:16 |
||||
depends_on: |
||||
- install-deps |
||||
commands: |
||||
- npm run build |
||||
|
||||
- name: export |
||||
image: node:16 |
||||
depends_on: |
||||
- build |
||||
commands: |
||||
- npm run export |
||||
|
||||
- name: publish |
||||
image: plugins/docker:latest |
||||
depends_on: |
||||
- export |
||||
settings: |
||||
username: |
||||
from_secret: HARBOUR_USERNAME |
||||
password: |
||||
from_secret: HARBOUR_PASSWORD |
||||
tags: |
||||
- ${DRONE_BRANCH//\//-} |
||||
- latest |
||||
registry: registry.cloud.csclub.uwaterloo.ca |
||||
repo: registry.cloud.csclub.uwaterloo.ca/snedadah/csc-class-profile-staging |
||||
|
||||
- name: deploy-staging |
||||
image: node:16 |
||||
depends_on: |
||||
- publish |
||||
environment: |
||||
STAGING_AUTH_TOKEN: |
||||
from_secret: STAGING_AUTH_TOKEN |
||||
commands: |
||||
- echo "The docker build tag is ${DRONE_BRANCH//\//-}" |
||||
- 'curl -H "Authorization: $STAGING_AUTH_TOKEN" https://csclub.uwaterloo.ca/~snedadah/webhooks/cscclassprofilestaging?ref=${DRONE_BRANCH//\//-}' |
||||
|
||||
- name: deploy-production |
||||
image: node:16 |
||||
depends_on: |
||||
- export |
||||
environment: |
||||
SSH_KEY: |
||||
from_secret: DEPLOYMENT_SSH_KEY |
||||
commands: |
||||
- 'echo "$SSH_KEY" > /tmp/ssh_key' |
||||
- chmod 600 /tmp/ssh_key |
||||
- ssh -4 -i /tmp/ssh_key www@caffeine.csclub.uwaterloo.ca -o StrictHostKeyChecking=no '~/bin/classprofile/deploy-2022.sh' |
||||
when: |
||||
branch: |
||||
- main |
||||
|
||||
trigger: |
||||
event: |
||||
exclude: |
||||
- pull_request #avoid double build on PRs |
||||
|
||||
|
@ -0,0 +1,5 @@ |
||||
FROM nginx |
||||
COPY ./out /usr/share/nginx/html |
||||
|
||||
COPY staging-nginx.conf /etc/nginx/conf.d |
||||
RUN rm /etc/nginx/conf.d/default.conf |
@ -0,0 +1,65 @@ |
||||
.aboutWrapper { |
||||
position: relative; |
||||
width: 90%; |
||||
} |
||||
|
||||
.about { |
||||
display: flex; |
||||
flex-direction: row; |
||||
padding: calc(45rem / 16); |
||||
} |
||||
|
||||
.about h1 { |
||||
margin: 0; |
||||
} |
||||
|
||||
.about h4 { |
||||
margin: 0; |
||||
} |
||||
|
||||
.about aside { |
||||
flex: 1; |
||||
margin-right: calc(40rem / 16); |
||||
} |
||||
|
||||
.about aside h1 { |
||||
color: var(--secondary-accent) |
||||
} |
||||
|
||||
.about aside p { |
||||
color: var(--primary-accent-lighter) |
||||
} |
||||
|
||||
.about article { |
||||
flex: 3; |
||||
} |
||||
|
||||
.about article p { |
||||
color: var(--primary-text); |
||||
} |
||||
|
||||
.angle { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
width: calc(70rem / 16); |
||||
height: calc(70rem / 16); |
||||
} |
||||
|
||||
.anglePath { |
||||
stroke: var(--primary-accent-light) |
||||
} |
||||
|
||||
.left.angle { |
||||
transform: rotate(180deg); |
||||
top: unset; |
||||
left: unset; |
||||
bottom: 0; |
||||
right: 0; |
||||
} |
||||
|
||||
@media screen and (max-width: 900px) { |
||||
.about { |
||||
flex-direction: column; |
||||
} |
||||
} |
@ -0,0 +1,67 @@ |
||||
import React from "react"; |
||||
|
||||
import styles from "./About.module.css"; |
||||
|
||||
export function About() { |
||||
return ( |
||||
<div className={styles.aboutWrapper} id="about"> |
||||
<AngleDecoration isBottom={false} /> |
||||
<section className={styles.about}> |
||||
<aside> |
||||
<h1>About the Programs</h1> |
||||
<p>Getting an overview of the CS programs in UW</p> |
||||
</aside> |
||||
<article> |
||||
<h4>Computer Science</h4> |
||||
<p> |
||||
Computer Science (CS) is commonly offered by the Faculty of |
||||
Mathematics as a co-op program, with students usually attending 8 |
||||
school and 6 co-op terms in their degree. However, CS is more |
||||
flexible than the other two programs because of the ability to |
||||
choose from a wider range and number of electives, to take terms |
||||
off, and to change their academic schedules to fit their needs. |
||||
</p> |
||||
<h4>Computing and Financial Management</h4> |
||||
<p> |
||||
Computing and Financial Management (CFM) combines the core CS |
||||
courses with electives from areas such as accounting, economics, and |
||||
financial management. This is a joint offering by the Faculty of |
||||
Mathematics and the School of Accounting and Finance. The program is |
||||
offered only as a co-op program with 6 co-op terms. The program is |
||||
offered only as a co-op program with 6 co-op terms. |
||||
</p> |
||||
<h4>CS/BBA</h4> |
||||
<p> |
||||
Joint with Wilfrid Laurier University, the Business Administration |
||||
and Computer Science Double Degree (CS/BBA) is an exclusive offering |
||||
that allows students to gain experience in CS as well as many |
||||
subfields of business. There are 10 school terms and either 4 or 5 |
||||
co-op terms in the usual schedule, so it’s a longer degree with more |
||||
academic terms than CS or CFM. |
||||
</p> |
||||
</article> |
||||
</section> |
||||
<AngleDecoration isBottom={true} /> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
interface AngleDecorationProps { |
||||
isBottom: boolean; |
||||
} |
||||
|
||||
function AngleDecoration({ isBottom }: AngleDecorationProps) { |
||||
return ( |
||||
<svg |
||||
viewBox="0 0 100 100" |
||||
className={isBottom ? `${styles.left} ${styles.angle}` : styles.angle} |
||||
> |
||||
<path |
||||
d="M100,2 L2,2 L2,100" |
||||
fill="none" |
||||
strokeWidth="10" |
||||
className={styles.anglePath} |
||||
/> |
||||
</svg> |
||||
); |
||||
} |
@ -0,0 +1,30 @@ |
||||
.barBackground { |
||||
fill: var(--card-background); |
||||
transition: fill 0.5s ease-out; |
||||
} |
||||
|
||||
.bar { |
||||
fill: var(--primary-accent-light); |
||||
transition: fill 0.5s ease-out; |
||||
} |
||||
|
||||
.barGroup { |
||||
transition: fill 0.5s ease-out; |
||||
} |
||||
|
||||
.barGroup:hover .bar { |
||||
fill: var(--primary-accent); |
||||
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); |
||||
} |
||||
|
||||
.tickLabel { |
||||
font-family: "Inconsolata", monospace; |
||||
font-weight: 800; |
||||
fill: var(--label); |
||||
} |
||||
|
||||
.axisLabel { |
||||
font-family: "Inconsolata", monospace; |
||||
font-weight: 800; |
||||
fill: var(--label); |
||||
} |
@ -0,0 +1,398 @@ |
||||
import { AxisBottom, AxisLeft } from "@visx/axis"; |
||||
import { bottomTickLabelProps } from "@visx/axis/lib/axis/AxisBottom"; |
||||
import { leftTickLabelProps } from "@visx/axis/lib/axis/AxisLeft"; |
||||
import { GridColumns, GridRows } from "@visx/grid"; |
||||
import { Group } from "@visx/group"; |
||||
import { scaleBand, scaleLinear } from "@visx/scale"; |
||||
import { Bar } from "@visx/shape"; |
||||
import { withTooltip } from "@visx/tooltip"; |
||||
import React from "react"; |
||||
import { Color } from "utils/Color"; |
||||
|
||||
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper"; |
||||
|
||||
import styles from "./BarGraph.module.css"; |
||||
|
||||
interface BarGraphProps { |
||||
data: BarGraphData[]; |
||||
/** Width of the entire graph, in pixels. */ |
||||
width: number; |
||||
/** Height of the entire graph, in pixels. */ |
||||
height: number; |
||||
/** Distance between the edge of the graph and the area where the bars are drawn, in pixels. */ |
||||
margin: { |
||||
top: number; |
||||
bottom: number; |
||||
left: number; |
||||
right: number; |
||||
}; |
||||
className?: string; |
||||
/** Font size of the category axis tick labels, in pixels. Default is 16px. */ |
||||
categoryTickLabelSize?: number; |
||||
/** Font size of the value axis tick labels, in pixels. Default is 16px. */ |
||||
valueTickLabelSize?: number; |
||||
/** Label text for the category axis. */ |
||||
categoryAxisLabel?: string; |
||||
/** Font size of the label for the cateogry axis, in pixels. */ |
||||
categoryAxisLabelSize?: number; |
||||
/** Controls the distance between the category axis label and the category axis. */ |
||||
categoryAxisLabelOffset?: number; |
||||
/** Label text for the value axis. */ |
||||
valueAxisLabel?: string; |
||||
/** Font size of the label for the value axis, in pixels. */ |
||||
valueAxisLabelSize?: number; |
||||
/** Controls the distance between the value axis label and the value axis. */ |
||||
valueAxisLabelOffset?: number; |
||||
/** Minimum width of the graph. */ |
||||
minWidth?: number; |
||||
/** Breakpoint width of graph where alernating labels are displayed. Only for Vertical graphs */ |
||||
widthAlternatingLabel?: number; |
||||
/** Space added to the bottom of the graph to show overflowing labels. Only for Vertical graphs */ |
||||
alternatingLabelSpace?: number; |
||||
/** Default position of labels in x-axis, in px. */ |
||||
defaultLabelDy?: string; |
||||
/** Position of lower labels in x-axis, in px. Only for Vertical graphs */ |
||||
lowerLabelDy?: string; |
||||
} |
||||
|
||||
interface BarGraphData { |
||||
category: string; |
||||
value: number; |
||||
} |
||||
|
||||
const DEFAULT_LABEL_SIZE = 16; |
||||
|
||||
type TooltipData = string; |
||||
|
||||
export const BarGraphHorizontal = withTooltip<BarGraphProps, TooltipData>( |
||||
({ |
||||
width, |
||||
height, |
||||
margin, |
||||
data, |
||||
className, |
||||
minWidth = 500, |
||||
categoryTickLabelSize = DEFAULT_LABEL_SIZE, |
||||
valueTickLabelSize = DEFAULT_LABEL_SIZE, |
||||
categoryAxisLabel, |
||||
categoryAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
categoryAxisLabelOffset = 0, |
||||
valueAxisLabel, |
||||
valueAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
valueAxisLabelOffset = 0, |
||||
defaultLabelDy = "0", |
||||
tooltipOpen, |
||||
tooltipLeft, |
||||
tooltipTop, |
||||
tooltipData, |
||||
hideTooltip, |
||||
showTooltip, |
||||
}) => { |
||||
width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth
|
||||
const barPadding = 0.4; |
||||
|
||||
const categoryMax = height - margin.top - margin.bottom; |
||||
const valueMax = width - margin.left - margin.right; |
||||
|
||||
const getCategory = (d: BarGraphData) => d.category; |
||||
const getValue = (d: BarGraphData) => d.value; |
||||
|
||||
const categoryScale = scaleBand({ |
||||
range: [0, categoryMax], |
||||
domain: data.map(getCategory), |
||||
padding: barPadding, |
||||
}); |
||||
|
||||
const valueScale = scaleLinear({ |
||||
range: [0, valueMax], |
||||
nice: true, |
||||
domain: [0, Math.max(...data.map(getValue))], |
||||
}); |
||||
|
||||
const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); |
||||
const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); |
||||
|
||||
return ( |
||||
<div> |
||||
<svg className={className} width={width} height={height}> |
||||
<Group top={margin.top} left={margin.left}> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barWidth = categoryScale.bandwidth(); |
||||
const backgroundBarWidth = barWidth / (1 - barPadding); |
||||
return idx % 2 === 0 ? ( |
||||
<Bar |
||||
className={styles.barBackground} |
||||
key={`bar-${barName}-background`} |
||||
x={0} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
y={categoryPoint(d)! - (backgroundBarWidth - barWidth) / 2} |
||||
width={valueMax} |
||||
height={backgroundBarWidth} |
||||
/> |
||||
) : null; |
||||
})} |
||||
</Group> |
||||
<GridColumns |
||||
scale={valueScale} |
||||
height={categoryMax} |
||||
numTicks={5} |
||||
stroke={Color.label} |
||||
strokeWidth={4} |
||||
strokeDasharray="10" |
||||
strokeLinecap="round" |
||||
/> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barLength = valuePoint(d); |
||||
const barWidth = categoryScale.bandwidth(); |
||||
return ( |
||||
<Group className={styles.barGroup} key={`bar-${barName}`}> |
||||
<Bar |
||||
onMouseMove={(e) => { |
||||
const tooltipPos = getTooltipPosition(e); |
||||
showTooltip({ |
||||
tooltipData: getValue(d).toString(), |
||||
tooltipLeft: tooltipPos.x, |
||||
tooltipTop: tooltipPos.y, |
||||
}); |
||||
}} |
||||
onMouseOut={hideTooltip} |
||||
className={styles.bar} |
||||
x={0} |
||||
y={categoryPoint(d)} |
||||
width={barLength} |
||||
height={barWidth} |
||||
/> |
||||
</Group> |
||||
); |
||||
})} |
||||
</Group> |
||||
<AxisLeft |
||||
scale={categoryScale} |
||||
hideAxisLine |
||||
hideTicks |
||||
tickLabelProps={() => { |
||||
return { |
||||
...leftTickLabelProps(), |
||||
className: styles.tickLabel, |
||||
fontSize: `${categoryTickLabelSize / 16}rem`, |
||||
}; |
||||
}} |
||||
label={categoryAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={categoryAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${categoryAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
<AxisBottom |
||||
scale={valueScale} |
||||
top={categoryMax} |
||||
hideAxisLine |
||||
hideTicks |
||||
numTicks={5} |
||||
tickLabelProps={() => { |
||||
return { |
||||
...bottomTickLabelProps(), |
||||
className: styles.tickLabel, |
||||
dy: defaultLabelDy, |
||||
fontSize: `${valueTickLabelSize / 16}rem`, |
||||
}; |
||||
}} |
||||
label={valueAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={valueAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${valueAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
</Group> |
||||
</svg> |
||||
|
||||
{tooltipOpen && ( |
||||
<TooltipWrapper |
||||
top={tooltipTop} |
||||
left={tooltipLeft} |
||||
header={tooltipData as string} |
||||
></TooltipWrapper> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
); |
||||
|
||||
export const BarGraphVertical = withTooltip<BarGraphProps, TooltipData>( |
||||
({ |
||||
width, |
||||
height, |
||||
margin, |
||||
data, |
||||
className, |
||||
minWidth = 500, |
||||
categoryTickLabelSize = DEFAULT_LABEL_SIZE, |
||||
valueTickLabelSize = DEFAULT_LABEL_SIZE, |
||||
categoryAxisLabel, |
||||
categoryAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
categoryAxisLabelOffset = 0, |
||||
valueAxisLabel, |
||||
valueAxisLabelSize = DEFAULT_LABEL_SIZE, |
||||
valueAxisLabelOffset = 0, |
||||
widthAlternatingLabel = 600, |
||||
alternatingLabelSpace = 80, |
||||
defaultLabelDy = `0px`, |
||||
lowerLabelDy = `30px`, |
||||
tooltipOpen, |
||||
tooltipLeft, |
||||
tooltipTop, |
||||
tooltipData, |
||||
hideTooltip, |
||||
showTooltip, |
||||
}) => { |
||||
width = width < minWidth ? minWidth : width; // Ensuring graph's width >= minWidth
|
||||
const barPadding = 0.4; |
||||
const alternatingLabel = width <= widthAlternatingLabel; |
||||
const final_margin_bottom = alternatingLabel |
||||
? margin.bottom + alternatingLabelSpace |
||||
: margin.bottom; |
||||
|
||||
const categoryMax = width - margin.left - margin.right; |
||||
const valueMax = height - margin.top - final_margin_bottom; |
||||
|
||||
const getCategory = (d: BarGraphData) => d.category; |
||||
const getValue = (d: BarGraphData) => d.value; |
||||
|
||||
const categoryScale = scaleBand({ |
||||
range: [0, categoryMax], |
||||
domain: data.map(getCategory), |
||||
padding: barPadding, |
||||
}); |
||||
|
||||
const valueScale = scaleLinear({ |
||||
range: [valueMax, 0], |
||||
nice: true, |
||||
domain: [0, Math.max(...data.map(getValue))], |
||||
}); |
||||
|
||||
const categoryPoint = (d: BarGraphData) => categoryScale(getCategory(d)); |
||||
const valuePoint = (d: BarGraphData) => valueScale(getValue(d)); |
||||
|
||||
return ( |
||||
<div> |
||||
<svg className={className} width={width} height={height}> |
||||
<Group top={margin.top} left={margin.left}> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barWidth = categoryScale.bandwidth(); |
||||
const backgroundBarWidth = barWidth / (1 - barPadding); |
||||
return idx % 2 === 0 ? ( |
||||
<Bar |
||||
className={styles.barBackground} |
||||
key={`bar-${barName}-background`} |
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
x={categoryPoint(d)! - (backgroundBarWidth - barWidth) / 2} |
||||
y={0} |
||||
width={backgroundBarWidth} |
||||
height={valueMax} |
||||
/> |
||||
) : null; |
||||
})} |
||||
</Group> |
||||
<GridRows |
||||
scale={valueScale} |
||||
width={categoryMax} |
||||
numTicks={5} |
||||
stroke={Color.label} |
||||
strokeWidth={4} |
||||
strokeDasharray="10" |
||||
strokeLinecap="round" |
||||
/> |
||||
<Group> |
||||
{data.map((d, idx) => { |
||||
const barName = `${getCategory(d)}-${idx}`; |
||||
const barHeight = valueMax - valuePoint(d); |
||||
const barWidth = categoryScale.bandwidth(); |
||||
return ( |
||||
<Group className={styles.barGroup} key={`bar-${barName}`}> |
||||
<Bar |
||||
onMouseMove={(e) => { |
||||
const tooltipPos = getTooltipPosition(e); |
||||
showTooltip({ |
||||
tooltipData: getValue(d).toString(), |
||||
tooltipLeft: tooltipPos.x, |
||||
tooltipTop: tooltipPos.y, |
||||
}); |
||||
}} |
||||
onMouseOut={hideTooltip} |
||||
className={styles.bar} |
||||
x={categoryPoint(d)} |
||||
y={valueMax - barHeight} |
||||
width={barWidth} |
||||
height={barHeight} |
||||
/> |
||||
</Group> |
||||
); |
||||
})} |
||||
</Group> |
||||
<AxisBottom |
||||
scale={categoryScale} |
||||
top={valueMax} |
||||
hideAxisLine |
||||
hideTicks |
||||
tickLabelProps={(value, index) => { |
||||
const alternatingDy = |
||||
index % 2 == 0 ? defaultLabelDy : lowerLabelDy; |
||||
return { |
||||
...bottomTickLabelProps(), |
||||
className: styles.tickLabel, |
||||
dy: alternatingLabel ? alternatingDy : defaultLabelDy, |
||||
fontSize: `${categoryTickLabelSize / 16}rem`, |
||||
width: categoryScale.bandwidth(), |
||||
verticalAnchor: "start", |
||||
}; |
||||
}} |
||||
label={categoryAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={categoryAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${categoryAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
<AxisLeft |
||||
scale={valueScale} |
||||
hideAxisLine |
||||
hideTicks |
||||
numTicks={5} |
||||
tickLabelProps={() => { |
||||
return { |
||||
...leftTickLabelProps(), |
||||
className: styles.tickLabel, |
||||
dx: "-0.5rem", |
||||
dy: "0.25rem", |
||||
fontSize: `${valueTickLabelSize / 16}rem`, |
||||
}; |
||||
}} |
||||
label={valueAxisLabel} |
||||
labelClassName={styles.axisLabel} |
||||
labelOffset={valueAxisLabelOffset} |
||||
labelProps={{ |
||||
fontSize: `${valueAxisLabelSize / 16}rem`, |
||||
}} |
||||
/> |
||||
</Group> |
||||
</svg> |
||||
|
||||
{tooltipOpen && ( |
||||
<TooltipWrapper |
||||
top={tooltipTop} |
||||
left={tooltipLeft} |
||||
header={tooltipData as string} |
||||
></TooltipWrapper> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
); |
@ -0,0 +1,15 @@ |
||||
import React from "react"; |
||||
|
||||
export const BodyLink = ({ |
||||
targetBlank = true, |
||||
children, |
||||
href, |
||||
}: { |
||||
href: string; |
||||
targetBlank?: boolean; |
||||
children: React.ReactNode; |
||||
}) => ( |
||||
<a href={href} target={targetBlank ? "_blank" : ""} rel="noreferrer"> |
||||
<b>{children}</b> |
||||
</a> |
||||
); |
@ -0,0 +1,110 @@ |
||||
.container { |
||||
display: flex; |
||||
flex-flow: center; |
||||
align-items: center; |
||||
justify-content: space-between; |
||||
margin: calc(40rem / 16) 0; |
||||
} |
||||
|
||||
.subBox { |
||||
display: inline-block; |
||||
} |
||||
|
||||
.leftItem { |
||||
text-align: right; |
||||
} |
||||
|
||||
.item { |
||||
color: var(--primary-text); |
||||
font-size: calc(28rem / 16); |
||||
position: relative; |
||||
margin: calc(24rem / 16); |
||||
} |
||||
|
||||
.subBox { |
||||
display: flex; |
||||
flex-direction: row; |
||||
align-items: center; |
||||
} |
||||
|
||||
.arrow { |
||||
width: calc(250rem / 16); |
||||
height: calc(20rem / 16); |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
} |
||||
|
||||
|
||||
.item:after { |
||||
content: ''; |
||||
position: absolute; |
||||
width: 100%; |
||||
transform: scaleX(0); |
||||
height: calc(2rem / 16); |
||||
bottom: 0; |
||||
left: 0; |
||||
background-color: var(--primary-accent); |
||||
cursor: pointer; |
||||
transform-origin: bottom right; |
||||
transition: transform 0.25s ease-out; |
||||
} |
||||
|
||||
.item:hover:after { |
||||
transform: scaleX(1); |
||||
transform-origin: bottom left; |
||||
} |
||||
|
||||
.linePath { |
||||
stroke: var(--primary-text); |
||||
} |
||||
|
||||
.arrowPath { |
||||
fill: var(--primary-text); |
||||
} |
||||
|
||||
.right { |
||||
transform: rotate(180deg); |
||||
} |
||||
|
||||
@media screen and (max-width: 1000px) { |
||||
.subBox { |
||||
flex-direction: column; |
||||
align-items: flex-start; |
||||
} |
||||
|
||||
.subBoxLeft { |
||||
flex-direction: column-reverse; |
||||
align-items: flex-end; |
||||
} |
||||
|
||||
|
||||
.item { |
||||
font-size: calc(20rem / 16); |
||||
margin: 0; |
||||
margin-bottom: calc(10rem / 16); |
||||
} |
||||
|
||||
.arrow { |
||||
width: calc(200rem / 16); |
||||
} |
||||
} |
||||
|
||||
@media screen and (max-width: 500px) { |
||||
.container { |
||||
justify-content: center; |
||||
gap: calc(50rem / 16); |
||||
} |
||||
|
||||
.arrow { |
||||
width: 100%; |
||||
} |
||||
} |
||||
|
||||
.containerOnlyRightArrow { |
||||
justify-content: flex-end; |
||||
} |
||||
|
||||
.containerOnlyLeftArrow { |
||||
justify-content: flex-start; |
||||
} |
@ -0,0 +1,88 @@ |
||||
import Link from "next/link"; |
||||
import React from "react"; |
||||
|
||||
import styles from "./BottomNav.module.css"; |
||||
|
||||
interface PagesInfo { |
||||
leftPage?: { |
||||
url: string; |
||||
name: string; |
||||
}; |
||||
rightPage?: { |
||||
url: string; |
||||
name: string; |
||||
}; |
||||
} |
||||
|
||||
export function BottomNav(props: PagesInfo) { |
||||
const onlyRightArrow = props.rightPage && !props.leftPage; |
||||
const onlyLeftArrow = !props.rightPage && props.leftPage; |
||||
return ( |
||||
<div |
||||
className={`${styles.container}
|
||||
${onlyRightArrow ? styles.containerOnlyRightArrow : ""}
|
||||
${onlyLeftArrow ? styles.containerOnlyLeftArrow : ""}`}
|
||||
> |
||||
{props.leftPage ? ( |
||||
<div className={styles.subBox + " " + styles.subBoxLeft}> |
||||
<Link href={props.leftPage.url}> |
||||
<a> |
||||
<Arrow /> |
||||
</a> |
||||
</Link> |
||||
<Link href={props.leftPage.url}> |
||||
<a className={styles.item + " " + styles.leftItem}> |
||||
{props.leftPage.name} |
||||
</a> |
||||
</Link> |
||||
</div> |
||||
) : null} |
||||
{props.rightPage ? ( |
||||
<div className={styles.subBox}> |
||||
<Link href={props.rightPage.url}> |
||||
<a className={styles.item}>{props.rightPage.name}</a> |
||||
</Link> |
||||
<Link href={props.rightPage.url}> |
||||
<a> |
||||
<Arrow isPointingRight /> |
||||
</a> |
||||
</Link> |
||||
</div> |
||||
) : null} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
interface ArrowProps { |
||||
isPointingRight?: boolean; |
||||
} |
||||
|
||||
function Arrow({ isPointingRight }: ArrowProps) { |
||||
return ( |
||||
<svg className={(isPointingRight ? styles.right : "") + " " + styles.arrow}> |
||||
<defs> |
||||
<marker |
||||
id="arrow" |
||||
markerWidth="10" |
||||
markerHeight="10" |
||||
refX="5" |
||||
refY="3" |
||||
orient="auto" |
||||
markerUnits="strokeWidth" |
||||
> |
||||
<path d="M0,0 L0,6 L9,3 z" className={styles.arrowPath} /> |
||||
</marker> |
||||
</defs> |
||||
|
||||
<line |
||||
x1="250" |
||||
y1="10" |
||||
x2="100" |
||||
y2="10" // half of svg height
|
||||
strokeWidth="3" |
||||
markerEnd="url(#arrow)" |
||||
className={styles.linePath} |
||||
/> |
||||
</svg> |
||||
); |
||||
} |
@ -0,0 +1,9 @@ |
||||
.boxplot { |
||||
fill: var(--primary-accent-light); |
||||
transition: fill 0.5s ease-out; |
||||
} |
||||
|
||||
.boxplot:hover { |
||||
fill: var(--primary-accent); |
||||
filter: drop-shadow(0 0 calc(4rem / 16) var(--primary-accent)); |
||||
} |
@ -0,0 +1,316 @@ |
||||
import { AxisLeft, AxisBottom } from "@visx/axis"; |
||||
import { GridRows, GridColumns } from "@visx/grid"; |
||||
import { Group } from "@visx/group"; |
||||
import { Stats } from "@visx/mock-data/lib/generators/genStats"; |
||||
import { Point } from "@visx/point"; |
||||
import { scaleBand, scaleLinear } from "@visx/scale"; |
||||
import { Line } from "@visx/shape"; |
||||
import { BoxPlot as VisxBoxPlot } from "@visx/stats"; |
||||
import { withTooltip } from "@visx/tooltip"; |
||||
import { WithTooltipProvidedProps } from "@visx/tooltip/lib/enhancers/withTooltip"; |
||||
import React from "react"; |
||||
import { Color } from "utils/Color"; |
||||
|
||||
import { getTooltipPosition, TooltipWrapper } from "./TooltipWrapper"; |
||||
|
||||
import styles from "./Boxplot.module.css"; |
||||
|
||||
const DEFAULT_LABEL_SIZE = 16; |
||||
const TICK_LABEL_FONT_WEIGHT = 800; |
||||
|
||||
interface BoxPlotData { |
||||
category: string; |
||||
min: number; |
||||
median: number; |
||||
max: number; |
||||
firstQuartile: number; |
||||
thirdQuartile: number; |
||||
outliers?: number[]; |
||||
} |
||||
|
||||
type TooltipData = Omit<BoxPlotData, "outliers">; |
||||
|
||||
export type StatsPlotProps = { |
||||
data: BoxPlotData[]; |
||||
/** Width of the entire graph, in pixels, greater than 10. */ |
||||
width: number; |
||||
/** Height of the entire graph, in pixels. */ |
||||
height: number; |
||||