feat: migration 3

This commit is contained in:
TrèsAbhi committed 2024-09-06 23:26:35 -05:00
1 parent 6285f33989
commit 4055988478
110 files changed
+28 -76

No files matched your search

@@ -0,0 +1,27 @@
import { Button, ButtonProps, Tooltip } from '@radix-ui/themes';
import { useState } from 'react';
interface CopyButtonProps extends ButtonProps {
copy: () => string | undefined;
}
export function CopyButton({ copy, ...props }: CopyButtonProps) {
const [tooltipOpen, setTooltipOpen] = useState(false);
return (
<Tooltip open={tooltipOpen} content="Copied!">
<Button
{...props}
onClick={() => {
const content = copy();
if (content === undefined) return;
navigator.clipboard.writeText(content);
setTooltipOpen(true);
setTimeout(() => setTooltipOpen(false), 2000);
}}
/>
</Tooltip>
);
}
@@ -0,0 +1,233 @@
import {
ChevronLeftIcon,
ChevronRightIcon,
ChevronUpIcon,
ReloadIcon,
} from '@radix-ui/react-icons';
import {
Button,
ChevronDownIcon,
Flex,
Grid,
IconButton,
Link,
Popover,
Text,
} from '@radix-ui/themes';
import { times } from 'lodash';
import { useState } from 'react';
const DAY_TITLES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const YEAR_OPTIONS = 7;
interface DatePickerProps {
onDateChange?: (date: Date) => void;
defaultDate?: Date;
}
export function DatePicker({
onDateChange,
defaultDate = new Date(),
}: DatePickerProps) {
const [year, setYear] = useState(defaultDate.getFullYear());
const [month, setMonth] = useState(defaultDate.getMonth());
const [day, setDay] = useState(defaultDate.getDate());
const [yearScroll, setYearScroll] = useState(0);
const [yearOffset, setYearOffset] = useState(0);
const [monthOffset, setMonthOffset] = useState(0);
const date = new Date(year + yearOffset, month + monthOffset, day);
const daysInLastMonth = new Date(
year + yearOffset,
month + monthOffset,
0,
).getDate();
const daysInMonth = new Date(
year + yearOffset,
month + monthOffset + 1,
0,
).getDate();
const firstDayOfMonth = new Date(
year + yearOffset,
month + monthOffset,
1,
).getDay();
const lastDayOfMonth = new Date(
year + yearOffset,
month + monthOffset,
daysInMonth,
).getDay();
return (
<Flex direction="column" width="fit-content">
<Flex justify="between" mb="2" align="center" py="2">
<IconButton
size="3"
variant="ghost"
onClick={() => {
if (monthOffset === 0) {
setYearOffset((state) => state - 1);
setMonthOffset(11);
} else {
setMonthOffset((state) => state - 1);
}
}}
ml="2"
>
<ChevronLeftIcon />
</IconButton>
<Flex align="center" gap="2">
<IconButton
size="3"
variant="ghost"
onClick={() => {
setYear(defaultDate.getFullYear());
setMonth(defaultDate.getMonth());
setDay(defaultDate.getDate());
setYearOffset(0);
setMonthOffset(0);
}}
>
<ReloadIcon />
</IconButton>
<Flex gap="1">
<Text>
{date.toLocaleString(undefined, {
month: 'long',
})}
</Text>
<Popover.Root onOpenChange={() => setYearScroll(0)}>
<Popover.Trigger>
<Link underline="always" href="#">
{date.getFullYear()}
</Link>
</Popover.Trigger>
<Popover.Content side="right" align="center">
<Flex direction="column" gap="1">
<IconButton
variant="ghost"
onClick={() => setYearScroll((state) => state + 1)}
mb="2"
>
<ChevronUpIcon />
</IconButton>
{times(YEAR_OPTIONS, (index) => {
const thisYear =
year +
yearOffset -
index +
Math.floor(YEAR_OPTIONS / 2) +
yearScroll;
const selected = year + yearOffset === thisYear;
return (
<Popover.Close key={thisYear}>
<Button
variant={selected ? 'solid' : 'soft'}
radius="large"
onClick={() => {
setYearOffset(thisYear - year);
}}
>
{thisYear}
</Button>
</Popover.Close>
);
})}
<IconButton
variant="ghost"
onClick={() => setYearScroll((state) => state - 1)}
mt="2"
>
<ChevronDownIcon />
</IconButton>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
</Flex>
<IconButton
size="3"
variant="ghost"
onClick={() => {
if (monthOffset === 11) {
setYearOffset((state) => state + 1);
setMonthOffset(0);
} else {
setMonthOffset((state) => state + 1);
}
}}
mr="2"
>
<ChevronRightIcon />
</IconButton>
</Flex>
<Flex mb="1">
{times(7, (index) => (
<Text
style={{
flex: 1,
}}
size="2"
align="center"
key={index}
color="gray"
>
{DAY_TITLES[index]}
</Text>
))}
</Flex>
<Grid columns="7" gap="1" width="fit-content">
{times(firstDayOfMonth, (index) => (
<IconButton radius="large" key={index} disabled>
{daysInLastMonth - firstDayOfMonth + index + 1}
</IconButton>
))}
{times(daysInMonth, (index) => {
const selected =
index === day - 1 && monthOffset === 0 && yearOffset === 0;
return (
<IconButton
radius="large"
key={index}
variant={selected ? 'solid' : 'soft'}
onClick={() => {
const newYear =
year + yearOffset + Math.floor((month + monthOffset) / 12);
const newMonth = (month + monthOffset) % 12;
const newDay = index + 1;
setYear(newYear);
setMonth(newMonth);
setDay(newDay);
setYearOffset(0);
setMonthOffset(0);
onDateChange?.(new Date(newYear, newMonth, newDay));
}}
>
{index + 1}
</IconButton>
);
})}
{times(6 - lastDayOfMonth, (index) => (
<IconButton radius="large" key={index} disabled>
{index + 1}
</IconButton>
))}
</Grid>
</Flex>
);
}
@@ -0,0 +1,7 @@
import { Link, LinkProps } from '@radix-ui/themes';
export function DummyLink({ href = '#', onClick, ...props }: LinkProps) {
return (
<Link href={href} onClick={(event) => event.preventDefault()} {...props} />
);
}
@@ -1,7 +1,7 @@
import { assertSecret } from '@blitzkit/core';
import strings from '@blitzkit/core/lang/en-US.json';
import { REGIONS } from '@blitzkit/core/src/blitzkit/regions';
import { Button, ButtonProps, Dialog, Flex } from '@radix-ui/themes';
import { REGIONS } from '../../../../../../src/constants/regions';
import { Link } from '../../Link';
import { WargamingIcon } from '../../WargamingIcon';
@@ -29,14 +29,10 @@ export function WargamingLoginButton({ children, ...props }: ButtonProps) {
<Link
href={
typeof window !== 'undefined'
? `https://api.worldoftanks.${
region
}/wot/auth/login/?application_id=${assertSecret(
? `https://api.worldoftanks.${region}/wot/auth/login/?application_id=${assertSecret(
process.env.WARGAMING_APPLICATION_ID,
)}&redirect_uri=${encodeURIComponent(
`${location.origin}/auth/wargaming?return=${
location.origin
}${location.pathname}`,
`${location.origin}/auth/wargaming?return=${location.origin}${location.pathname}`,
)}`
: undefined
}
@@ -0,0 +1,13 @@
import { slateDarkA } from '@radix-ui/colors';
import { keyframes, style } from '@vanilla-extract/css';
const animation = keyframes({
'0%': { backgroundColor: slateDarkA.slateA2 },
'50%': { backgroundColor: slateDarkA.slateA3 },
'100%': { backgroundColor: slateDarkA.slateA2 },
});
export const skeleton = style({
borderRadius: 8,
animation: `${animation} 2s ease-in-out infinite`,
});
@@ -0,0 +1,6 @@
import { ComponentProps } from 'react';
import { skeleton } from './index.css';
export function Skeleton({ className, ...props }: ComponentProps<'div'>) {
return <div {...props} className={`${className} ${skeleton}`} />;
}
@@ -0,0 +1,30 @@
import { asset } from '../core/blitzkit/asset';
interface SmallTankIconProps {
id: number;
size?: number;
}
const WIDTH = 70;
const HEIGHT = 24;
export function SmallTankIcon({ id, size = HEIGHT }: SmallTankIconProps) {
return (
<div
style={{
height: size,
overflow: 'hidden',
width: WIDTH * (size / HEIGHT),
}}
>
<img
alt={`Tank ${id}`}
src={asset(`icons/tanks/small/${id}.webp`)}
style={{
transform: `scale(${(size / HEIGHT) * 100}%)`,
transformOrigin: 'top left',
}}
/>
</div>
);
}
@@ -0,0 +1,60 @@
import { Flex, FlexProps, Text } from '@radix-ui/themes';
import { ReactNode } from 'react';
type SpeedometerProps = FlexProps & {
value: ReactNode;
color: string;
fill: number;
label: ReactNode;
};
export function Speedometer({
value,
color,
fill,
label,
...props
}: SpeedometerProps) {
return (
<Flex direction="column" align="center" {...props}>
<svg
width={180}
height={98}
style={{
transform: 'scaleX(-1)',
}}
>
<circle
cx={90}
cy={90}
r={80}
strokeLinecap="round"
fill="none"
stroke={`var(--${color}-3)`}
strokeWidth={16}
strokeDasharray={80 * Math.PI}
strokeDashoffset={80 * Math.PI}
/>
<circle
cx={90}
cy={90}
r={80}
strokeLinecap="round"
fill="none"
stroke={`var(--${color}-indicator)`}
strokeWidth={16}
strokeDasharray={(160 - 80 * fill) * Math.PI}
strokeDashoffset={(160 - 80 * fill) * Math.PI}
/>
</svg>
<Flex direction="column" align="center" mt="-8">
<Text weight="bold" size="8" mt="-1">
{value}
</Text>
{label}
</Flex>
</Flex>
);
}
@@ -0,0 +1,25 @@
import { Table } from '@radix-ui/themes';
import { ColumnHeaderCellProps } from '@radix-ui/themes/dist/cjs/components/table';
interface StickyColumnHeaderCellProps extends ColumnHeaderCellProps {
top?: number | string;
}
export function StickyColumnHeaderCell({
style,
top = 0,
...props
}: StickyColumnHeaderCellProps) {
return (
<Table.ColumnHeaderCell
style={{
position: 'sticky',
top,
background: 'var(--gray-3)',
zIndex: 1,
...style,
}}
{...props}
/>
);
}
@@ -0,0 +1,18 @@
import { Table } from '@radix-ui/themes';
import { RowHeaderCellProps } from '@radix-ui/themes/dist/cjs/components/table';
export function StickyRowHeaderCell({ style, ...props }: RowHeaderCellProps) {
return (
<Table.RowHeaderCell
style={{
display: 'flex',
position: 'sticky',
left: 0,
backgroundColor: 'var(--color-panel)',
boxShadow: 'var(--shadow-6)',
...style,
}}
{...props}
/>
);
}
@@ -0,0 +1,23 @@
'use client';
import { Table } from '@radix-ui/themes';
import { RootProps } from '@radix-ui/themes/dist/cjs/components/table';
import { useEffect, useRef } from 'react';
export function StickyTableRoot({ style, ...props }: RootProps) {
const table = useRef<HTMLTableElement>(null);
useEffect(() => {
table.current
?.querySelector('table')
?.style.setProperty('overflow', 'auto');
}, []);
return (
<Table.Root
{...props}
ref={table}
style={{ overflow: 'hidden', ...style }}
/>
);
}
@@ -0,0 +1,71 @@
import { tankIcon } from '@blitzkit/core/src/blitzkit/tankIcon';
import { classIcons } from '../../packages/website/src/components/ClassIcon';
import { TankDefinition } from '../core/blitzkit/tankDefinitions';
import { ExperimentIcon } from './ExperimentIcon';
import { Link } from './Link';
import { StickyRowHeaderCell } from './StickyRowHeaderCell';
interface TankRowHeaderCellProps {
tank: TankDefinition;
}
export function TankRowHeaderCell({ tank }: TankRowHeaderCellProps) {
const Icon = classIcons[tank.class];
return (
<StickyRowHeaderCell
width={{ initial: '144px', sm: '240px' }}
style={{ overflow: 'hidden' }}
>
<Link href={`/tools/tankopedia/${tank.id}`} tabIndex={-1}>
<img
alt={tank.name}
draggable={false}
src={tankIcon(tank.id)}
style={{
margin: 'calc(-1 * var(--table-cell-padding)) 0',
height: 'calc(100% + 2 * var(--table-cell-padding))',
aspectRatio: '4 / 3',
objectFit: 'cover',
}}
/>
</Link>
<Link
color={
tank.treeType === 'collector'
? 'blue'
: tank.treeType === 'premium'
? 'amber'
: 'gray'
}
highContrast={tank.treeType === 'researchable'}
underline="hover"
wrap="nowrap"
href={`/tools/tankopedia/${tank.id}`}
style={{
paddingLeft: 'var(--space-2)',
display: 'flex',
alignItems: 'center',
gap: 'var(--space-1)',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
size={{ initial: '1', sm: '2' }}
>
<Icon
width="1em"
height="1em"
style={{ minWidth: '1em', minHeight: '1em' }}
/>
{tank.testing && (
<ExperimentIcon style={{ width: '1em', height: '1em' }} />
)}
{tank.name}
</Link>
</StickyRowHeaderCell>
);
}
@@ -0,0 +1,124 @@
import { amberDark, blueDark } from '@radix-ui/colors';
import { Flex, Text } from '@radix-ui/themes';
import { times } from 'lodash';
import { ReactNode } from 'react';
import { ClassHeavy } from '../../packages/website/src/components/ClassIcon/components/ClassHeavy';
import { ClassLight } from '../../packages/website/src/components/ClassIcon/components/ClassLight';
import { ClassMedium } from '../../packages/website/src/components/ClassIcon/components/ClassMedium';
import { ClassTankDestroyer } from '../../packages/website/src/components/ClassIcon/components/ClassTankDestroyer';
import { useEmbedStateCurry } from '../../packages/website/src/stores/embedState/utilities';
import { breakdownConfig } from '../app/tools/embed/configurations/breakdown';
import { toRadiusVar } from '../app/tools/embed/utilities';
import { TankDefinition } from '../core/blitzkit/tankDefinitions';
import { previewCompositeStat } from '../core/statistics/compositeStats';
import { CompositeStatsKey } from '../core/statistics/compositeStats/constants';
import strings from '../lang/en-US.json';
interface BreakdownEmbedWrapperProps {
children: ReactNode;
}
export function BreakdownEmbedWrapper({
children,
}: BreakdownEmbedWrapperProps) {
const { useState } = useEmbedStateCurry<typeof breakdownConfig>();
return (
<Flex direction="column" gap={useState('listGap')} height="100%">
{children}
</Flex>
);
}
interface BreakdownEmbedCardProps {
tank: TankDefinition | null;
}
export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) {
const { useState, useRichText } =
useEmbedStateCurry<typeof breakdownConfig>();
return (
<Flex
flexShrink="0"
direction="column"
style={{
borderRadius: toRadiusVar(useState('cardRadius')),
overflow: 'hidden',
backgroundColor: useState('cardBodyBackgroundColor'),
}}
>
<Flex
justify="center"
style={{
backgroundColor: useState('cardHeaderBackgroundColor'),
}}
p={useState('cardHeaderPadding')}
>
<Text
{...{
...useRichText('cardTitle'),
style:
tank === null
? undefined
: tank.treeType === 'researchable' ||
!useState('cardTitleTypeColor')
? undefined
: {
color:
tank.treeType === 'premium'
? amberDark.amber11
: blueDark.blue11,
},
}}
>
<Flex align="center" gap="1">
{useState('cardTitleClassIcon') && tank !== null && (
<>
{tank.class === 'lightTank' && (
<ClassLight width="1em" height="1em" />
)}
{tank.class === 'mediumTank' && (
<ClassMedium width="1em" height="1em" />
)}
{tank.class === 'heavyTank' && (
<ClassHeavy width="1em" height="1em" />
)}
{tank.class === 'AT-SPG' && (
<ClassTankDestroyer width="1em" height="1em" />
)}
</>
)}{' '}
{tank === null ? 'Total' : tank.name}
</Flex>
</Text>
</Flex>
<Flex p={useState('cardBodyPadding')}>
{times(4, (index) => {
const column = (index + 1) as 1 | 2 | 3 | 4;
const columnState = useState(`column${column}`) as CompositeStatsKey;
const customLabel = useState(`column${column}CustomLabel`);
return (
<Flex
direction="column"
align="center"
gap={useState('columnGap')}
flexGrow="1"
>
<Text {...useRichText('columnValue')}>
{previewCompositeStat(columnState)}
</Text>
<Text {...useRichText('columnLabel')}>
{customLabel.length === 0
? strings.common.composite_stats[columnState]
: customLabel}
</Text>
</Flex>
);
})}
</Flex>
</Flex>
);
}