Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
key-moon committed May 31, 2024
1 parent 033e81e commit f075a35
Show file tree
Hide file tree
Showing 28 changed files with 9,769 additions and 29,284 deletions.
18,095 changes: 0 additions & 18,095 deletions website/.pnp.cjs

This file was deleted.

2,110 changes: 0 additions & 2,110 deletions website/.pnp.loader.mjs

This file was deleted.

68 changes: 68 additions & 0 deletions website/app/component/ContestCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import { Card, CardContent, Typography, Box, IconButton, CardActionArea, Tooltip, Grid, useTheme } from '@mui/material';
import { Alarm, AccessTime, ArrowForward } from '@mui/icons-material';
import ContestTimer from './ContestTimer';
import { ColorName, Colors } from '~/lib/ratings';

interface ContestCardProps {
contestName: string;
contestScreenName: string;
startTime: Date;
endTime: Date;
contestType: 'algorithm' | 'heuristic';
contestColor: ColorName;
}

function capitalize(s: string) {
return s.charAt(0).toUpperCase() + s.slice(1);
}

export default (({ contestName, contestScreenName, startTime, endTime, contestType, contestColor }) => {
const theme = useTheme();

return (
<Card sx={{ marginBottom: 2, borderRadius: '8px', border: '1px solid #ccc', backgroundColor: Colors[contestColor][theme.palette.mode].background }}>
<CardActionArea href={`/contests/${contestScreenName}`}>
<CardContent>
<Grid container spacing={2} justifyContent="center" alignItems="center">
<Grid item xs={1}>
<Tooltip title={capitalize(contestType)}>
<Box sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: theme.palette.background.paper,
marginRight: 2,
border: '1px solid #ccc',
fontSize: '1rem',
fontWeight: 'bold',
}}>
{contestType[0].toUpperCase()}
</Box>
</Tooltip>
</Grid>
<Grid item xs={8}>
<Typography variant="h6">{contestName}</Typography>
</Grid>
<Grid item xs={2}>
<ContestTimer
startDate={startTime}
endDate={endTime}
contentOnOver='Contest is over!'
timeDeltaProps={{
color: theme.palette.mode === 'dark' ? 'primary' : 'secondary',
}}
/>
</Grid>
<Grid item xs={1}>
<ArrowForward />
</Grid>
</Grid>
</CardContent>
</CardActionArea>
</Card>
);
}) satisfies React.FC<ContestCardProps>;
66 changes: 66 additions & 0 deletions website/app/component/ContestTimer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect, useState } from "react";
import TimeDelta from "./datetime/timeDelta";
import type { SvgIconProps, TypographyProps } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import { AvTimer, HourglassTop } from "@mui/icons-material";

const ContestTimer: React.FC<
{
startDate: string | number | Date;
endDate: string | number | Date;
contentOnOver: string;
iconProps?: SvgIconProps;
timeDeltaProps?: Omit<React.ComponentProps<typeof TimeDelta>, "msec" | "innerComponent">;
} & Omit<TypographyProps, "key">
> = ({ startDate, endDate, contentOnOver, iconProps, timeDeltaProps, ...other }) => {
const [now, setNow] = useState(Date.now());

useEffect(() => {
const update = () => setNow(Date.now());
update();
const id = setInterval(update, 1000);
return () => clearInterval(id);
}, []);

const startTime = new Date(startDate).getTime();
const endTime = new Date(endDate).getTime();

const InnerComponent: React.FC<{
value: string;
unit: string;
isFirst: boolean;
isLast: boolean;
}> = ({ value, unit, isFirst, isLast }) => {
return (
<Stack direction="row" alignItems="baseline">
<Typography {...other} textAlign="right" suppressHydrationWarning>
{value}
</Typography>
<Typography {...other} fontSize="95%">
{unit}
</Typography>
</Stack>
);
};

if (now < endTime) {
const [anchorTime, Icon] =
now < startTime ? [startTime, HourglassTop] : [endTime, AvTimer];
return (
<Stack direction="row" justifyContent="flex-end">
<Icon {...iconProps} />
<TimeDelta
msec={anchorTime - now}
gap={0.9}
innerComponent={InnerComponent}
minWidth={130}
{...timeDeltaProps}
/>
</Stack>
);
} else {
return <>{contentOnOver}</>;
}
};

export default ContestTimer;
15 changes: 15 additions & 0 deletions website/app/component/Rating.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Typography } from '@mui/material';

export function ColorizedRatingText({ rating }: { rating: number }): JSX.Element {
let color = '';
if (rating >= 4) {
color = 'green';
} else if (rating >= 2) {
color = 'orange';
} else {
color = 'red';
}

return <Typography style={{ color }}>{rating}</Typography>;
}
Empty file.
16 changes: 16 additions & 0 deletions website/app/component/datetime/localDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { NoSsr } from "@mui/material";
import { formatInTimeZone } from "date-fns-tz";
import React from "react";
import { getLocalTimezone } from "~/lib/util";

const LocalDate: React.FC<{
date: string | number | Date;
formatStr?: string;
}> = ({ date, formatStr = "yyyy-MM-dd HH:mm zzz" }) => {
const format = (timezone: string) =>
formatInTimeZone(date, timezone, formatStr);

return <NoSsr fallback={format("UTC")}>{format(getLocalTimezone())}</NoSsr>;
};

export default LocalDate;
35 changes: 35 additions & 0 deletions website/app/component/datetime/localDateRange.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NoSsr } from "@mui/material";
import { formatInTimeZone } from "date-fns-tz";
import React from "react";
import { getLocalTimezone } from "~/lib/util";

const LocalDateRange: React.FC<{
startDate: string | number | Date;
endDate: string | number | Date;
dateFormatStr?: string;
timeFormatStr?: string;
timeZoneFormatStr?: string;
}> = ({
startDate,
endDate,
dateFormatStr = "yyyy-MM-dd",
timeFormatStr = "HH:mm",
timeZoneFormatStr = "zzz",
}) => {
const format = (timezone: string) => {
const startDateStr = formatInTimeZone(startDate, timezone, dateFormatStr);
const startTimeStr = formatInTimeZone(startDate, timezone, timeFormatStr);
const endDateStr = formatInTimeZone(endDate, timezone, dateFormatStr);
const endTimeStr = formatInTimeZone(endDate, timezone, timeFormatStr);
const tzStr = formatInTimeZone(startDate, timezone, timeZoneFormatStr);
if (startDateStr === endDateStr) {
return `${startDateStr} ${startTimeStr} - ${endTimeStr} ${tzStr}`;
} else {
return `${startDateStr} ${startTimeStr} - ${endDateStr} ${endTimeStr} ${tzStr}`;
}
};

return <NoSsr fallback={format("UTC")}>{format(getLocalTimezone())}</NoSsr>;
};

export default LocalDateRange;
30 changes: 30 additions & 0 deletions website/app/component/datetime/localDateTimeField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Box, NoSsr, TextField, Typography } from "@mui/material";
import { formatInTimeZone } from "date-fns-tz";
import React, { ComponentProps, useState } from "react";
import { getLocalTimezone } from "~/lib/util";

const LocalDateTimeField: React.FC<
Omit<ComponentProps<typeof TextField>, "type" | "defaultValue"> & { defaultValue?: Date }
> = props => {
const { defaultValue, name, onChange, ...rest } = props;

const format = (timezone: string) =>
defaultValue && formatInTimeZone(defaultValue, timezone, "yyyy-MM-dd@HH:mm:ss").replace("@", "T");

const [isoString, setISOString] = useState<string | undefined>(defaultValue && defaultValue.toISOString());

return <NoSsr fallback={<Box><Typography>loading...</Typography></Box>}>
<TextField
type="datetime-local"
defaultValue={format(getLocalTimezone())}
onChange={event => {
setISOString(new Date(event.target.value).toISOString());
if (onChange) onChange(event);
}}
{...rest}
/>
<input hidden={true} type="string" name={name} readOnly value={isoString} />
</NoSsr>;
};

export default LocalDateTimeField;
118 changes: 118 additions & 0 deletions website/app/component/datetime/timeDelta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { StackProps } from "@mui/material";
import { Stack, Typography } from "@mui/material";
import React from "react";

const units = [
{ base: 1000, unit: ["ms", "msecs", "milliseconds"], maxDigit: 3 },
{ base: 60, unit: ["s", "secs", "seconds"], maxDigit: 2 },
{ base: 60, unit: ["m", "mins", "minutes"], maxDigit: 2 },
{ base: 24, unit: ["h", "hours", "hours"], maxDigit: 2 },
{ base: Infinity, unit: ["d", "days", "days"], maxDigit: 0 },
];

const defaultInnerComponent: React.FC<{
value: string;
unit: string;
isFirst: boolean;
isLast: boolean;
}> = ({ value, unit, isFirst, isLast }) => {
return (
<>
<Typography variant="inherit" component="span">
{value}
</Typography>
{isLast ? null : (
<Typography variant="inherit" component="span">
:
</Typography>
)}
</>
);
};

const TimeDelta: React.FC<
{
msec: number;
precision?: "ms" | "seconds" | "minutes" | "hours" | "days" | number;
headMinUnit?: "ms" | "seconds" | "minutes" | "hours" | "days";
headMaxUnit?: "ms" | "seconds" | "minutes" | "hours" | "days";
unitType?: "shortest" | "short" | "long";
zeroFill?: boolean;
fillHead?: boolean; // true: 01h 02m 03s, false: 1h 02m 03s
gap?: string | number;

innerComponent?: React.FC<{
value: string;
unit: string;
isFirst: boolean;
isLast: boolean;
}>;
} & StackProps
> = ({
msec,
precision = "seconds",
headMinUnit = "ms",
headMaxUnit = "days",
unitType = "shortest",
zeroFill = true,
fillHead = false,
gap = "1px",
innerComponent: Component = defaultInnerComponent,
...other
}) => {
const isNegative = msec < 0;
const rounded = Math.round(Math.abs(msec));

const unitIndex = ["shortest", "short", "long"].indexOf(unitType);

const result = [];

let remain = rounded;
let display = false;
let minUnitAchieved = false;
for (let i = 0; i < units.length; i++) {
const { base, unit, maxDigit } = units[i];
display ||= typeof(precision) == "string" ? unit.includes(precision) : true;
minUnitAchieved ||= unit.includes(headMinUnit);

const current = remain % base;
remain = (remain - current) / base;

if (!display) continue;

const isHead =
unit.includes(headMaxUnit) || (remain === 0 && minUnitAchieved);
const isTail = result.length == 0;

let currentStr = current.toString();
if (zeroFill && (!isHead || fillHead)) {
currentStr = currentStr.padStart(maxDigit, "0");
}
if (isHead && isNegative) {
currentStr = "-" + currentStr;
}
const unitStr = unit[unitIndex];

result.unshift(
<Component
key={unitStr}
value={currentStr}
unit={unitStr}
isFirst={isHead}
isLast={isTail}
/>
);
if (isHead) break;
}
if (typeof(precision) == "number") {
while (precision < result.length) result.pop();
}

return (
<Stack direction="row" alignItems="baseline" justifyContent="flex-end" gap={gap} {...other}>
{result}
</Stack>
);
};

export default TimeDelta;
Loading

0 comments on commit f075a35

Please sign in to comment.