feat: new dynamic system

This commit is contained in:
TrèsAbhi committed 2024-08-08 14:18:22 -05:00
1 parent e8b8b19332
commit 94623c43eb
17 files changed
+895 -832

No files matched your search

@@ -0,0 +1,86 @@
import { grayA, grayDark, grayDarkA } from '@radix-ui/colors';
import { use } from 'react';
import {
BreakdownEmbedCard,
BreakdownEmbedWrapper,
} from '../../../../components/TanksEmbed';
import { tankDefinitionsArray } from '../../../../core/blitzkit/tankDefinitions';
import { EmbedConfig } from '../../../../stores/embedState';
import { EmbedItemType } from '../../../../stores/embedState/constants';
import { useEmbedStateCurry } from '../../../../stores/embedState/utilities';
export const breakdownConfig = {
showTotal: { type: EmbedItemType.Boolean, default: true, pad: true },
listWidth: {
type: EmbedItemType.Slider,
default: 320,
min: 128,
max: 640,
},
listGap: { type: EmbedItemType.Size, default: '2' },
listMaxTanks: {
type: EmbedItemType.Slider,
default: 4,
min: 0,
max: 8,
pad: true,
},
cardRadius: { type: EmbedItemType.Radius, default: '2' },
cardHeaderBackgroundColor: {
type: EmbedItemType.Color,
default: grayDarkA.grayA3,
},
cardHeaderPadding: { type: EmbedItemType.Size, default: '1' },
cardTitle: {
type: EmbedItemType.RichText,
default: { color: grayDark.gray12, weight: 'bold', size: '3' },
},
cardTitleTypeColor: { type: EmbedItemType.Boolean, default: true },
cardTitleClassIcon: { type: EmbedItemType.Boolean, default: true },
cardBodyBackgroundColor: {
type: EmbedItemType.Color,
default: grayA.grayA11,
},
cardBodyPadding: {
type: EmbedItemType.Size,
default: '2',
pad: true,
},
columnGap: { type: EmbedItemType.Size, default: '0' },
columnValue: {
type: EmbedItemType.RichText,
default: { color: grayDark.gray12, size: '3', weight: 'regular' },
},
columnLabel: {
type: EmbedItemType.RichText,
default: { color: 'gray', size: '2', weight: 'regular' },
pad: true,
},
column1Unit: { type: EmbedItemType.String, default: '' },
column2Unit: { type: EmbedItemType.String, default: '%' },
column3Unit: { type: EmbedItemType.String, default: '' },
column4Unit: { type: EmbedItemType.String, default: '' },
column1Label: { type: EmbedItemType.String, default: 'Battles' },
column2Label: { type: EmbedItemType.String, default: 'Winrate' },
column3Label: { type: EmbedItemType.String, default: 'WN8' },
column4Label: { type: EmbedItemType.String, default: 'Damage' },
} satisfies EmbedConfig;
export function BreakdownPreview() {
const tanks = use(tankDefinitionsArray);
const { useState } = useEmbedStateCurry<typeof breakdownConfig>();
return (
<BreakdownEmbedWrapper>
{useState('showTotal') && <BreakdownEmbedCard tank={null} />}
{tanks.slice(0, useState('listMaxTanks')).map((tank) => (
<BreakdownEmbedCard key={tank.id} tank={tank} />
))}
</BreakdownEmbedWrapper>
);
}
@@ -0,0 +1,11 @@
import { ReactNode } from 'react';
import { EmbedConfig } from '../../../../stores/embedState';
import { BreakdownPreview, breakdownConfig } from './breakdown';
export const configurations = {
breakdown: breakdownConfig,
} satisfies Record<string, EmbedConfig>;
export const previews = {
breakdown: BreakdownPreview,
} satisfies Record<string, () => ReactNode>;
@@ -0,0 +1,18 @@
import { Switch } from '@radix-ui/themes';
import * as EmbedState from '../../../../../../stores/embedState';
import { EmbedPreviewControllerProps } from '../page';
export function Boolean({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
return (
<Switch
checked={EmbedState.use((state) => state[configKey] as boolean)}
onCheckedChange={() => {
mutateEmbedState((draft) => {
draft[configKey] = !draft[configKey];
});
}}
/>
);
}
@@ -0,0 +1,48 @@
import { Slider } from '@radix-ui/themes';
import { useRef } from 'react';
import * as EmbedState from '../../../../../../stores/embedState';
import { EmbedPreviewControllerProps } from '../page';
export function Color({ configKey }: EmbedPreviewControllerProps) {
const mutateEmbedState = EmbedState.useMutation();
const embedStore = EmbedState.useStore();
const input = useRef<HTMLInputElement>(null);
return (
<>
<input
ref={input}
type="color"
value={EmbedState.use((state) =>
(state[configKey] as string).slice(0, 7),
)}
onChange={(event) => {
mutateEmbedState((draft) => {
let alpha = (draft[configKey] as string).slice(7, 9);
if (alpha.length === 0) alpha = 'ff';
draft[configKey] = `${event.target.value}${alpha}`;
});
}}
/>
<Slider
min={0}
max={255}
value={[
EmbedState.use((state) => {
let alpha = (state[configKey] as string).slice(7, 9);
if (alpha.length === 0) return 255;
return parseInt(alpha, 16);
}),
]}
onValueChange={([value]) => {
mutateEmbedState((draft) => {
if (!input.current) return;
const color = input.current.value;
draft[configKey] = `${color}${value.toString(16)}`;
});
}}
/>
</>
);
}
@@ -0,0 +1,22 @@
import { useMemo } from 'react';
import * as EmbedState from '../../../../../stores/embedState';
import { configurations } from '../../configurations';
import { extractEmbedConfigDefaults } from '../../utilities';
export default function Layout({
params,
children,
}: {
params: { embed: keyof typeof configurations };
children: React.ReactNode;
}) {
const config = configurations[params.embed];
const initialState = useMemo(
() => extractEmbedConfigDefaults(config),
[params.embed],
);
return (
<EmbedState.Provider data={initialState}>{children}</EmbedState.Provider>
);
}
@@ -0,0 +1,524 @@
'use client';
import { ImageIcon } from '@radix-ui/react-icons';
import { Box, Button, Flex, Heading, ScrollArea, Text } from '@radix-ui/themes';
import { capitalize, startCase } from 'lodash';
import { ReactNode, useEffect, useRef, useState } from 'react';
import { NAVBAR_HEIGHT } from '../../../../../components/Navbar';
import PageWrapper from '../../../../../components/PageWrapper';
import { imgur } from '../../../../../core/blitzkit/imgur';
import { EmbedConfig } from '../../../../../stores/embedState';
import { EmbedItemType } from '../../../../../stores/embedState/constants';
import { configurations, previews } from '../../configurations';
import { Boolean } from './components/Boolean';
import { Color } from './components/Color';
export interface EmbedPreviewControllerProps {
configKey: string;
}
export default function Page({
params,
}: {
params: { embed: keyof typeof configurations };
}) {
const config = configurations[params.embed] as EmbedConfig;
const Preview = previews[params.embed];
const [backgroundImage, setBackgroundImage] = useState(imgur('SO13zur'));
const fileInput = useRef<HTMLInputElement>();
useEffect(() => {
fileInput.current = document.createElement('input');
fileInput.current.type = 'file';
fileInput.current.style.display = 'none';
fileInput.current.accept = 'image/*';
fileInput.current.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
setBackgroundImage(URL.createObjectURL(file));
};
}, []);
return (
<PageWrapper padding="0" size={1028 + 640}>
<Flex>
<ScrollArea
scrollbars="vertical"
style={{ height: '100%', maxWidth: 320 }}
>
<Flex direction="column" gap="2" p="4">
<Heading mb="4">Customize</Heading>
{Object.keys(config).map((configKey) => {
const item = config[configKey];
let control: ReactNode;
let oneLiner = false;
switch (item.type) {
case EmbedItemType.Boolean: {
control = <Boolean configKey={configKey} />;
oneLiner = true;
break;
}
case EmbedItemType.Color: {
control = <Color configKey={configKey} />;
break;
}
}
return (
<Flex
direction={oneLiner ? undefined : 'column'}
gap={oneLiner ? '2' : '1'}
justify={oneLiner ? 'between' : undefined}
mb="2"
pb={item.pad ? '6' : undefined}
>
<Text>{capitalize(startCase(configKey as string))}</Text>
<Flex gap="2" align="center">
{control}
</Flex>
</Flex>
);
})}
{/* {Object.entries(config).map(([configKeyRaw, configItem]) => {
const configKey = configKeyRaw as keyof (
| Immutable<ExtractEmbedConfigTypes<Config>>
| ExtractEmbedConfigTypes<Config>
);
const stateItem = state[configKey];
let control: ReactNode;
if (configItem.type === EmbedItemType.Slider) {
control = (
<Slider
state={
stateItem as EmbedConfigItemStateType<EmbedItemType.Slider>
}
config={configItem as EmbedItemType<EmbedItemType.Slider>}
/>
);
}
// switch (configItem.type) {
// case EmbedConfigType.Slider: {
// control = (
// );
// break;
// }
// case EmbedConfigType.Boolean: {
// control = (
// <Switch
// checked={state[configKey] as boolean}
// onCheckedChange={(checked) =>
// mutateState((draft) => {
// (draft[configKey] as boolean) = checked;
// })
// }
// />
// );
// break;
// }
// case EmbedConfigType.RichText: {
// control = (
// <>
// <Select.Root
// value={`${(state[configKey] as EmbedConfigItemType<EmbedConfigType.RichText>['default']).color}`}
// onValueChange={(value) => {
// mutateState((draft) => {
// (
// draft[
// configKey
// ] as EmbedConfigItemType<EmbedConfigType.RichText>['default']
// ).color = value as RadixTextColor;
// });
// }}
// >
// <Select.Trigger />
// <Select.Content>
// <Select.Item value="undefined">
// <Box
// style={{
// display: 'inline-block',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: 'white',
// }}
// />{' '}
// White
// </Select.Item>
// <Select.Item value="gray">
// <Box
// style={{
// display: 'inline-block',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: 'gray',
// }}
// />{' '}
// Gray
// </Select.Item>
// {Object.entries(radixColors)
// .filter(
// ([key]) =>
// !key.endsWith('A') &&
// !key.endsWith('P3') &&
// !key.endsWith('Dark') &&
// !radixGrays.includes(key),
// )
// .map(([key]) => (
// <Select.Item key={key} value={key}>
// <Box
// style={{
// display: 'inline-block',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: toColorVar({
// base: key as RadixColorBase,
// variant: '9',
// }),
// }}
// />{' '}
// {capitalize(key)}
// </Select.Item>
// ))}
// </Select.Content>
// </Select.Root>
// <Select.Root
// value={
// (
// state[
// configKey
// ] as EmbedConfigItemType<EmbedConfigType.RichText>['default']
// ).size
// }
// onValueChange={(value) => {
// mutateState((draft) => {
// (
// draft[
// configKey
// ] as EmbedConfigItemType<EmbedConfigType.RichText>['default']
// ).size = value as RadixSizeWithout0;
// });
// }}
// >
// <Select.Trigger />
// <Select.Content>
// {times(9, (index) => (
// <Select.Item key={index} value={`${index + 1}`}>
// Size {index + 1}
// </Select.Item>
// ))}
// </Select.Content>
// </Select.Root>
// <Select.Root
// value={
// (
// state[
// configKey
// ] as EmbedConfigItemType<EmbedConfigType.RichText>['default']
// ).weight
// }
// onValueChange={(value) => {
// mutateState((draft) => {
// (
// draft[
// configKey
// ] as EmbedConfigItemType<EmbedConfigType.RichText>['default']
// ).weight = value as RadixTextWeight;
// });
// }}
// >
// <Select.Trigger />
// <Select.Content>
// {radixTextWeights.map((weight) => (
// <Select.Item key={weight} value={weight}>
// {startCase(weight)}
// </Select.Item>
// ))}
// </Select.Content>
// </Select.Root>
// </>
// );
// break;
// }
// case EmbedConfigType.Number: {
// control = (
// <TextField.Root
// style={{ width: 64 }}
// type="number"
// value={state[configKey] as number}
// onChange={(event) => {
// mutateState((draft) => {
// (draft[configKey] as number) = (
// event.target as HTMLInputElement
// ).valueAsNumber;
// });
// }}
// >
// {configItem.unit && (
// <TextField.Slot side="right">px</TextField.Slot>
// )}
// </TextField.Root>
// );
// break;
// }
// case EmbedConfigType.String: {
// control = (
// <TextField.Root
// placeholder="Empty"
// style={{ width: 64 }}
// value={state[configKey] as string}
// onChange={(event) => {
// mutateState((draft) => {
// (draft[configKey] as string) = (
// event.target as HTMLInputElement
// ).value;
// });
// }}
// />
// );
// break;
// }
// case EmbedConfigType.SizeWithout0:
// case EmbedConfigType.Size: {
// control = (
// <Slider
// value={[Number(state[configKey])]}
// min={
// configItem.type === EmbedConfigType.SizeWithout0 ? 1 : 0
// }
// max={9}
// onValueChange={([value]) =>
// mutateState((draft) => {
// (draft[configKey] as RadixSize) =
// `${value}` as RadixSize;
// })
// }
// />
// );
// break;
// }
// case EmbedConfigType.Radius: {
// control = (
// <Slider
// value={[
// (state[configKey] as RadixRadius) === 'full'
// ? 5
// : Number(state[configKey]),
// ]}
// min={0}
// max={5}
// onValueChange={([value]) =>
// mutateState((draft) => {
// (draft[configKey] as RadixRadius) = (
// value === 5 ? 'full' : `${value}`
// ) as RadixRadius;
// })
// }
// />
// );
// break;
// }
// case EmbedConfigType.Color: {
// control = (
// <>
// <Select.Root
// value={(state[configKey] as RadixColor).base}
// onValueChange={(value) => {
// mutateState((draft) => {
// (draft[configKey] as RadixColor).base =
// value as RadixColorBase;
// });
// }}
// >
// <Select.Trigger />
// <Select.Content>
// {Object.entries(radixColors)
// .filter(
// ([key]) =>
// !key.endsWith('A') &&
// !key.endsWith('P3') &&
// !key.endsWith('Dark'),
// )
// .map(([key]) => (
// <Select.Item key={key} value={key}>
// <Box
// style={{
// display: 'inline-block',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: toColorVar({
// base: key as RadixColorBase,
// variant: '9',
// }),
// }}
// />{' '}
// {capitalize(key)}
// </Select.Item>
// ))}
// </Select.Content>
// </Select.Root>
// <Select.Root
// value={(state[configKey] as RadixColor).variant}
// onValueChange={(value) => {
// mutateState((draft) => {
// (draft[configKey] as RadixColor).variant =
// value as RadixColorVariant;
// });
// }}
// >
// <Select.Trigger />
// <Select.Content>
// {times(12, (index) => {
// const variant =
// `${index + 1}` as RadixColorVariantRaw;
// return (
// <>
// <Select.Item value={variant}>
// <Box
// style={{
// display: 'inline-block',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: toColorVar({
// base: (state[configKey] as RadixColor)
// .base,
// variant,
// }),
// }}
// />{' '}
// {variant}
// </Select.Item>
// <Select.Item value={`a${variant}`}>
// <Box
// style={{
// display: 'inline-block',
// overflow: 'hidden',
// width: '1em',
// height: '1em',
// verticalAlign: 'text-top',
// borderRadius: toRadiusVar('full'),
// backgroundColor: toColorVar({
// base: (state[configKey] as RadixColor)
// .base,
// variant: `a${variant}`,
// }),
// }}
// >
// <Box
// style={{
// width: '50%',
// height: '100%',
// backgroundColor: toColorVar({
// base: (state[configKey] as RadixColor)
// .base,
// variant,
// }),
// }}
// />
// </Box>{' '}
// {variant} translucent
// </Select.Item>
// </>
// );
// })}
// </Select.Content>
// </Select.Root>
// </>
// );
// break;
// }
// }
return (
<Flex
direction="column"
gap="1"
mb="1"
pb={configItem.pad ? '6' : undefined}
>
<Text>{capitalize(startCase(configKey as string))}</Text>
{control && <Flex gap="1">{control}</Flex>}
{!control && <Text color="red">Immutable</Text>}
</Flex>
);
})} */}
</Flex>
</ScrollArea>
<Box
p="4"
flexGrow="1"
position="sticky"
top={`${NAVBAR_HEIGHT}px`}
height={`calc(100vh - ${NAVBAR_HEIGHT}px)`}
>
<Flex
direction="column"
height="100%"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
boxShadow: 'var(--shadow-3)',
}}
>
<Flex
justify="between"
align="center"
p="2"
pl="3"
style={{
backgroundColor: 'var(--color-panel-solid)',
}}
>
<Text color="gray">Example preview</Text>
<Button
variant="outline"
color="gray"
onClick={() => fileInput.current?.click()}
>
<ImageIcon /> Upload test background
</Button>
</Flex>
<Flex
flexGrow="1"
style={{
background: `url(${backgroundImage}) center / cover no-repeat`,
}}
align="center"
justify="center"
>
<Preview />
</Flex>
</Flex>
</Box>
</Flex>
</PageWrapper>
);
}
@@ -1,89 +0,0 @@
'use client';
import { use } from 'react';
import { EmbedCustomize } from '../../../../../components/EmbedCustomize';
import {
BreakdownEmbedCard,
BreakdownEmbedWrapper,
} from '../../../../../components/TanksEmbed';
import { tanksDefinitionsArray } from '../../../../../core/blitzkit/tankDefinitions';
import {
EmbedConfig,
EmbedConfigType,
ExtractEmbedConfigType,
} from '../../types';
const breakdownEmbedConfig = {
showTotal: { type: EmbedConfigType.Boolean, default: true },
listWidth: { type: EmbedConfigType.Slider, default: 320, min: 128, max: 640 },
listGap: { type: EmbedConfigType.Size, default: '2' },
listMaxTanks: {
type: EmbedConfigType.Slider,
default: 4,
min: 0,
max: 8,
pad: true,
},
cardRadius: { type: EmbedConfigType.Radius, default: '2' },
cardHeaderBackgroundColor: {
type: EmbedConfigType.Color,
default: { base: 'gray', variant: '3' },
},
cardHeaderPadding: { type: EmbedConfigType.Size, default: '1' },
cardTitle: {
type: EmbedConfigType.FullTextControl,
default: { color: undefined, weight: 'bold', size: '3' },
},
cardTitleTypeColor: { type: EmbedConfigType.Boolean, default: true },
cardTitleClassIcon: { type: EmbedConfigType.Boolean, default: true },
cardBodyBackgroundColor: {
type: EmbedConfigType.Color,
default: { base: 'gray', variant: '2' },
},
cardBodyPadding: { type: EmbedConfigType.Size, default: '2', pad: true },
columnGap: { type: EmbedConfigType.Size, default: '0' },
columnValue: {
type: EmbedConfigType.FullTextControl,
default: { color: undefined, size: '3', weight: 'regular' },
},
columnLabel: {
type: EmbedConfigType.FullTextControl,
default: { color: 'gray', size: '2', weight: 'regular' },
pad: true,
},
column1Unit: { type: EmbedConfigType.String, default: '' },
column2Unit: { type: EmbedConfigType.String, default: '%' },
column3Unit: { type: EmbedConfigType.String, default: '' },
column4Unit: { type: EmbedConfigType.String, default: '' },
column1Label: { type: EmbedConfigType.String, default: 'Battles' },
column2Label: { type: EmbedConfigType.String, default: 'Winrate' },
column3Label: { type: EmbedConfigType.String, default: 'WN8' },
column4Label: { type: EmbedConfigType.String, default: 'Damage' },
} satisfies EmbedConfig;
export type BreakdownEmbedState = ExtractEmbedConfigType<
typeof breakdownEmbedConfig
>;
export default function Page() {
const tanks = use(tanksDefinitionsArray);
return (
<EmbedCustomize
config={breakdownEmbedConfig}
preview={(state) => (
<BreakdownEmbedWrapper state={state}>
{state.showTotal && <BreakdownEmbedCard state={state} tank={null} />}
{tanks.slice(0, state.listMaxTanks).map((tank) => (
<BreakdownEmbedCard key={tank.id} tank={tank} state={state} />
))}
</BreakdownEmbedWrapper>
)}
/>
);
}
-106
View File
@@ -1,106 +0,0 @@
import * as radixColors from '@radix-ui/colors';
export enum EmbedConfigType {
Number,
Radius,
Size,
Color,
SizeNo0,
TextColor,
String,
FullTextControl,
Boolean,
Slider,
}
export const radixGrays = [
'gray',
'mauve',
'slate',
'sage',
'olive',
'sand',
] as const;
export const radixTextWeights = ['light', 'regular', 'medium', 'bold'] as const;
export type EmbedConfig = Record<string, EmbedConfigItem>;
export type EmbedConfigItem = (
| {
type: EmbedConfigType.Slider;
default: number;
min: number;
max: number;
}
| {
type: EmbedConfigType.Boolean;
default: boolean;
}
| {
type: EmbedConfigType.Number;
default: number;
unit?: string;
}
| {
type: EmbedConfigType.String;
default: string;
}
| {
type: EmbedConfigType.Radius;
default: RadixRadius;
}
| {
type: EmbedConfigType.Size;
default: RadixSize;
}
| {
type: EmbedConfigType.SizeNo0;
default: RadixSizeNo0;
}
| {
type: EmbedConfigType.Color;
default: RadixColor;
}
| {
type: EmbedConfigType.TextColor;
default: RadixTextColor;
}
| {
type: EmbedConfigType.FullTextControl;
default: {
color: RadixTextColor;
size: RadixSizeNo0;
weight: RadixTextWeight;
};
}
) & {
pad?: boolean;
};
export type RadixRadius = `${1 | 2 | 3 | 4}` | 'full';
export type RadixSize = `${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
export type RadixTextWeight = (typeof radixTextWeights)[number];
export type RadixSizeNo0 = Exclude<RadixSize, '0'>;
export type RadixColor = { base: RadixColorBase; variant: RadixColorVariant };
export type RadixColorBase = Exclude<
keyof typeof radixColors,
`${string}${'A' | 'P3' | 'Dark'}`
>;
type RadixColorGrays = (typeof radixGrays)[number];
export type RadixTextColor =
| Exclude<RadixColorBase, RadixColorGrays>
| 'gray'
| undefined;
export type RadixColorVariantRaw =
`${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`;
export type RadixColorVariant = `${'' | 'a'}${RadixColorVariantRaw}`;
export type ExtractEmbedConfigType<Config extends EmbedConfig> = {
[Key in keyof Config]: ({
type: Config[Key]['type'];
} & EmbedConfigItem)['default'];
};
export type EmbedConfigItemType<Type extends EmbedConfigType> =
(EmbedConfigItem & { type: Type })
+5 -5
View File
@@ -1,16 +1,16 @@
import {
EmbedConfig,
ExtractEmbedConfigType,
RadixColor,
ExtractEmbedConfigTypes,
RadixColorCompound,
RadixRadius,
} from './types';
} from '../../../stores/embedState';
export function extractEmbedConfigDefaults<Config extends EmbedConfig>(
config: Config,
) {
return Object.fromEntries(
Object.entries(config).map(([key, value]) => [key, value.default]),
) as ExtractEmbedConfigType<Config>;
) as ExtractEmbedConfigTypes<Config>;
}
export function toWidthVar(state: number) {
@@ -21,6 +21,6 @@ export function toRadiusVar(state: RadixRadius) {
return `var(--radius-${state})`;
}
export function toColorVar(state: RadixColor) {
export function toColorVar(state: RadixColorCompound) {
return `var(--${state.base}-${state.variant})`;
}
@@ -17,8 +17,8 @@ import { resolveReload } from '../../../../../core/blitzkit/resolveReload';
import {
TankDefinition,
tankDefinitions,
tankDefinitionsArray,
tankNames,
tanksDefinitionsArray,
} from '../../../../../core/blitzkit/tankDefinitions';
import { unionBoundingBox } from '../../../../../core/blitzkit/unionBoundingBox';
import { useAdExempt } from '../../../../../hooks/useAdExempt';
@@ -51,7 +51,7 @@ export const TankSearch = memo<TankSearchProps>(
const awaitedGameDefinitions = use(gameDefinitions);
const awaitedModelDefinitions = use(modelDefinitions);
const awaitedTankDefinitions = use(tankDefinitions);
const awaitedTanksDefinitionsArray = use(tanksDefinitionsArray);
const awaitedTanksDefinitionsArray = use(tankDefinitionsArray);
const awaitedTankNames = use(tankNames);
const filters = TankFilters.use();
const sort = TankopediaSort.use();
-598
View File
@@ -1,598 +0,0 @@
import * as radixColors from '@radix-ui/colors';
import { ImageIcon } from '@radix-ui/react-icons';
import {
Box,
Button,
Flex,
Heading,
ScrollArea,
Select,
Slider,
Switch,
Text,
TextField,
} from '@radix-ui/themes';
import { Immutable, produce } from 'immer';
import { capitalize, startCase, times } from 'lodash';
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import {
EmbedConfig,
EmbedConfigItemType,
EmbedConfigType,
ExtractEmbedConfigType,
RadixColor,
RadixColorBase,
RadixColorVariant,
RadixColorVariantRaw,
radixGrays,
RadixRadius,
RadixSize,
RadixSizeNo0,
RadixTextColor,
RadixTextWeight,
radixTextWeights,
} from '../app/tools/embed/types';
import {
extractEmbedConfigDefaults,
toColorVar,
toRadiusVar,
} from '../app/tools/embed/utilities';
import { imgur } from '../core/blitzkit/imgur';
import { NAVBAR_HEIGHT } from './Navbar';
import PageWrapper from './PageWrapper';
interface EmbedConfigInputsProps<Config extends EmbedConfig> {
config: Config;
preview: (state: ExtractEmbedConfigType<Config>) => ReactNode;
}
export function EmbedCustomize<Config extends EmbedConfig>({
config,
preview,
}: EmbedConfigInputsProps<Config>) {
const [state, setState] = useState(extractEmbedConfigDefaults(config));
const [backgroundImage, setBackgroundImage] = useState(imgur('SO13zur'));
const mutateState = useCallback(
(recipe: (draft: Immutable<ExtractEmbedConfigType<Config>>) => void) => {
setState(produce(recipe));
},
[],
);
const fileInput = useRef<HTMLInputElement>();
useEffect(() => {
fileInput.current = document.createElement('input');
fileInput.current.type = 'file';
fileInput.current.style.display = 'none';
fileInput.current.accept = 'image/*';
fileInput.current.onchange = (event) => {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
setBackgroundImage(URL.createObjectURL(file));
};
}, []);
return (
<PageWrapper padding="0" size={1028 + 640}>
<Flex>
<ScrollArea
scrollbars="vertical"
style={{ height: '100%', maxWidth: 320 }}
>
<Flex direction="column" gap="2" p="4">
<Heading mb="4">Customize</Heading>
{Object.entries(config).map(([keyUntyped, setting]) => {
const key = keyUntyped as keyof (
| Immutable<ExtractEmbedConfigType<Config>>
| ExtractEmbedConfigType<Config>
);
let control: ReactNode;
switch (setting.type) {
case EmbedConfigType.Slider: {
control = (
<Slider
value={[state[key] as number]}
min={
(
config[
key
] as EmbedConfigItemType<EmbedConfigType.Slider>
).min
}
max={
(
config[
key
] as EmbedConfigItemType<EmbedConfigType.Slider>
).max
}
onValueChange={([value]) =>
mutateState((draft) => {
(draft[key] as number) = value;
})
}
/>
);
break;
}
case EmbedConfigType.Boolean: {
control = (
<Switch
checked={state[key] as boolean}
onCheckedChange={(checked) =>
mutateState((draft) => {
(draft[key] as boolean) = checked;
})
}
/>
);
break;
}
case EmbedConfigType.FullTextControl: {
control = (
<>
<Select.Root
value={`${(state[key] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']).color}`}
onValueChange={(value) => {
mutateState((draft) => {
(
draft[
key
] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']
).color = value as RadixTextColor;
});
}}
>
<Select.Trigger />
<Select.Content>
<Select.Item value="undefined">
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: 'white',
}}
/>{' '}
White
</Select.Item>
<Select.Item value="gray">
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: 'gray',
}}
/>{' '}
Gray
</Select.Item>
{Object.entries(radixColors)
.filter(
([key]) =>
!key.endsWith('A') &&
!key.endsWith('P3') &&
!key.endsWith('Dark') &&
!radixGrays.includes(key),
)
.map(([key]) => (
<Select.Item key={key} value={key}>
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: toColorVar({
base: key as RadixColorBase,
variant: '9',
}),
}}
/>{' '}
{capitalize(key)}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<Select.Root
value={
(
state[
key
] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']
).size
}
onValueChange={(value) => {
mutateState((draft) => {
(
draft[
key
] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']
).size = value as RadixSizeNo0;
});
}}
>
<Select.Trigger />
<Select.Content>
{times(9, (index) => (
<Select.Item key={index} value={`${index + 1}`}>
Size {index + 1}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<Select.Root
value={
(
state[
key
] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']
).weight
}
onValueChange={(value) => {
mutateState((draft) => {
(
draft[
key
] as EmbedConfigItemType<EmbedConfigType.FullTextControl>['default']
).weight = value as RadixTextWeight;
});
}}
>
<Select.Trigger />
<Select.Content>
{radixTextWeights.map((weight) => (
<Select.Item key={weight} value={weight}>
{startCase(weight)}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</>
);
break;
}
case EmbedConfigType.Number: {
control = (
<TextField.Root
style={{ width: 64 }}
type="number"
value={state[key] as number}
onChange={(event) => {
mutateState((draft) => {
(draft[key] as number) = (
event.target as HTMLInputElement
).valueAsNumber;
});
}}
>
{setting.unit && (
<TextField.Slot side="right">px</TextField.Slot>
)}
</TextField.Root>
);
break;
}
case EmbedConfigType.String: {
control = (
<TextField.Root
placeholder="Empty"
style={{ width: 64 }}
value={state[key] as string}
onChange={(event) => {
mutateState((draft) => {
(draft[key] as string) = (
event.target as HTMLInputElement
).value;
});
}}
/>
);
break;
}
case EmbedConfigType.SizeNo0:
case EmbedConfigType.Size: {
control = (
<Slider
value={[Number(state[key])]}
min={setting.type === EmbedConfigType.SizeNo0 ? 1 : 0}
max={9}
onValueChange={([value]) =>
mutateState((draft) => {
(draft[key] as RadixSize) = `${value}` as RadixSize;
})
}
/>
);
break;
}
case EmbedConfigType.Radius: {
control = (
<Slider
value={[
(state[key] as RadixRadius) === 'full'
? 5
: Number(state[key]),
]}
min={0}
max={5}
onValueChange={([value]) =>
mutateState((draft) => {
(draft[key] as RadixRadius) = (
value === 5 ? 'full' : `${value}`
) as RadixRadius;
})
}
/>
);
break;
}
case EmbedConfigType.Color: {
control = (
<>
<Select.Root
value={(state[key] as RadixColor).base}
onValueChange={(value) => {
mutateState((draft) => {
(draft[key] as RadixColor).base =
value as RadixColorBase;
});
}}
>
<Select.Trigger />
<Select.Content>
{Object.entries(radixColors)
.filter(
([key]) =>
!key.endsWith('A') &&
!key.endsWith('P3') &&
!key.endsWith('Dark'),
)
.map(([key]) => (
<Select.Item key={key} value={key}>
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: toColorVar({
base: key as RadixColorBase,
variant: '9',
}),
}}
/>{' '}
{capitalize(key)}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<Select.Root
value={(state[key] as RadixColor).variant}
onValueChange={(value) => {
mutateState((draft) => {
(draft[key] as RadixColor).variant =
value as RadixColorVariant;
});
}}
>
<Select.Trigger />
<Select.Content>
{times(12, (index) => {
const variant =
`${index + 1}` as RadixColorVariantRaw;
return (
<>
<Select.Item value={variant}>
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: toColorVar({
base: (state[key] as RadixColor).base,
variant,
}),
}}
/>{' '}
{variant}
</Select.Item>
<Select.Item value={`a${variant}`}>
<Box
style={{
display: 'inline-block',
overflow: 'hidden',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: toColorVar({
base: (state[key] as RadixColor).base,
variant: `a${variant}`,
}),
}}
>
<Box
style={{
width: '50%',
height: '100%',
backgroundColor: toColorVar({
base: (state[key] as RadixColor).base,
variant,
}),
}}
/>
</Box>{' '}
{variant} translucent
</Select.Item>
</>
);
})}
</Select.Content>
</Select.Root>
</>
);
break;
}
case EmbedConfigType.TextColor: {
control = (
<Select.Root
value={`${state[key]}`}
onValueChange={(value) => {
mutateState((draft) => {
(draft[key] as RadixTextColor) =
value as RadixTextColor;
});
}}
>
<Select.Trigger />
<Select.Content>
<Select.Item value="undefined">
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: 'white',
}}
/>{' '}
White
</Select.Item>
<Select.Item value="gray">
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: 'gray',
}}
/>{' '}
Gray
</Select.Item>
{Object.entries(radixColors)
.filter(
([key]) =>
!key.endsWith('A') &&
!key.endsWith('P3') &&
!key.endsWith('Dark') &&
!radixGrays.includes(key),
)
.map(([key]) => (
<Select.Item key={key} value={key}>
<Box
style={{
display: 'inline-block',
width: '1em',
height: '1em',
verticalAlign: 'text-top',
borderRadius: toRadiusVar('full'),
backgroundColor: toColorVar({
base: key as RadixColorBase,
variant: '9',
}),
}}
/>{' '}
{capitalize(key)}
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
break;
}
}
return (
<Flex
direction="column"
gap="1"
mb="1"
pb={setting.pad ? '6' : undefined}
>
<Text>{capitalize(startCase(key as string))}</Text>
{control && <Flex gap="1">{control}</Flex>}
{!control && <Text color="red">Immutable</Text>}
</Flex>
);
})}
</Flex>
</ScrollArea>
<Box
p="4"
flexGrow="1"
position="sticky"
top={`${NAVBAR_HEIGHT}px`}
height={`calc(100vh - ${NAVBAR_HEIGHT}px)`}
>
<Flex
direction="column"
height="100%"
style={{
borderRadius: 'var(--radius-3)',
overflow: 'hidden',
boxShadow: 'var(--shadow-3)',
}}
>
<Flex
justify="between"
align="center"
p="2"
pl="3"
style={{
backgroundColor: 'var(--color-panel-solid)',
}}
>
<Text color="gray">Example preview</Text>
<Button
variant="outline"
color="gray"
onClick={() => fileInput.current?.click()}
>
<ImageIcon /> Upload test background
</Button>
</Flex>
<Flex
flexGrow="1"
style={{
background: `url(${backgroundImage}) center / cover no-repeat`,
}}
align="center"
justify="center"
>
{preview(state)}
</Flex>
</Flex>
</Box>
</Flex>
</PageWrapper>
);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Flex, FlexProps, Theme } from '@radix-ui/themes';
import { ComponentProps } from 'react';
import { RadixSize } from '../app/tools/embed/types';
import { RadixSize } from '../stores/embedState/constants';
type PageWrapperProps = FlexProps & {
color?: ComponentProps<typeof Theme>['accentColor'];
+28 -28
View File
@@ -1,13 +1,10 @@
import { Flex, Text } from '@radix-ui/themes';
import { times } from 'lodash';
import { ReactNode } from 'react';
import { BreakdownEmbedState } from '../app/tools/embed/customize/breakdown/page';
import {
toColorVar,
toRadiusVar,
toWidthVar,
} from '../app/tools/embed/utilities';
import { breakdownConfig } from '../app/tools/embed/configurations/breakdown';
import { toRadiusVar } from '../app/tools/embed/utilities';
import { TankDefinition } from '../core/blitzkit/tankDefinitions';
import { useEmbedStateCurry } from '../stores/embedState/utilities';
import { ClassHeavy } from './ClassIcon/components/ClassHeavy';
import { ClassLight } from './ClassIcon/components/ClassLight';
import { ClassMedium } from './ClassIcon/components/ClassMedium';
@@ -15,18 +12,18 @@ import { ClassTankDestroyer } from './ClassIcon/components/ClassTankDestroyer';
interface BreakdownEmbedWrapperProps {
children: ReactNode;
state: BreakdownEmbedState;
}
export function BreakdownEmbedWrapper({
children,
state,
}: BreakdownEmbedWrapperProps) {
const { useState } = useEmbedStateCurry<typeof breakdownConfig>();
return (
<Flex
direction="column"
gap={state.listGap}
width={toWidthVar(state.listWidth)}
gap={useState('listGap')}
width={`${useState('listWidth')}px`}
>
{children}
</Flex>
@@ -35,30 +32,33 @@ export function BreakdownEmbedWrapper({
interface BreakdownEmbedCardProps {
tank: TankDefinition | null;
state: BreakdownEmbedState;
}
export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) {
const { color: cardTitleColor, ...restCardTitleProps } = state.cardTitle;
export function BreakdownEmbedCard({ tank }: BreakdownEmbedCardProps) {
const { useState, useRichText } =
useEmbedStateCurry<typeof breakdownConfig>();
const { color: cardTitleColor, ...restCardTitleProps } =
useState('cardTitle');
return (
<Flex
direction="column"
style={{
borderRadius: toRadiusVar(state.cardRadius),
borderRadius: toRadiusVar(useState('cardRadius')),
overflow: 'hidden',
backgroundColor: toColorVar(state.cardBodyBackgroundColor),
backgroundColor: useState('cardBodyBackgroundColor'),
}}
>
<Flex
justify="center"
style={{
backgroundColor: toColorVar(state.cardHeaderBackgroundColor),
backgroundColor: useState('cardHeaderBackgroundColor'),
}}
p={state.cardHeaderPadding}
p={useState('cardHeaderPadding')}
>
<Text
color={
state.cardTitleTypeColor
style={{
color: useState('cardTitleTypeColor')
? tank === null
? cardTitleColor
: tank.treeType === 'collector'
@@ -66,12 +66,12 @@ export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) {
: tank.treeType === 'premium'
? 'amber'
: cardTitleColor
: cardTitleColor
}
: cardTitleColor,
}}
{...restCardTitleProps}
>
<Flex align="center" gap="1">
{tank !== null && state.cardTitleClassIcon && (
{useState('cardTitleClassIcon') && tank !== null && (
<>
{tank.class === 'lightTank' && (
<ClassLight width="1em" height="1em" />
@@ -92,7 +92,7 @@ export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) {
</Text>
</Flex>
<Flex p={state.cardBodyPadding}>
<Flex p={useState('cardBodyPadding')}>
{times(4, (index) => {
const column = (index + 1) as 1 | 2 | 3 | 4;
@@ -100,14 +100,14 @@ export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) {
<Flex
direction="column"
align="center"
gap={state.columnGap}
gap={useState('columnGap')}
flexGrow="1"
>
<Text {...state.columnValue}>
70{state[`column${column}Unit`]}
<Text {...useRichText('columnValue')}>
70{useState(`column${column}Unit`)}
</Text>
<Text {...state.columnLabel}>
{state[`column${column}Label`]}
<Text {...useRichText('columnLabel')}>
{useState(`column${column}Label`)}
</Text>
</Flex>
);
+3 -3
View File
@@ -172,13 +172,13 @@ export const tankDefinitions = fetchCdonLz4<TankDefinitions>(
asset('definitions/tanks.cdon.lz4'),
);
export const tanksDefinitionsArray = tankDefinitions.then((tanks) =>
export const tankDefinitionsArray = tankDefinitions.then((tanks) =>
Object.values(tanks).map((tank) => tank),
);
export const tankNames = tanksDefinitionsArray.then((tanks) =>
export const tankNames = tankDefinitionsArray.then((tanks) =>
Promise.all(
tanks.map(async (tank, index) => {
const { id } = (await tanksDefinitionsArray)[index];
const { id } = (await tankDefinitionsArray)[index];
return {
id,
+27
View File
@@ -0,0 +1,27 @@
import { EmbedConfigItem } from '.';
export enum EmbedItemType {
Number,
Radius,
Size,
SizeWithout0,
Color,
String,
RichText,
Boolean,
Slider,
}
export const radixGrays = [
'gray',
'mauve',
'slate',
'sage',
'olive',
'sand',
] as const;
export const radixTextWeights = ['light', 'regular', 'medium', 'bold'] as const;
export type EmbedConfigItemType<Type extends EmbedItemType> =
EmbedConfigItem & { type: Type };
+89
View File
@@ -0,0 +1,89 @@
'use client';
import * as radixColors from '@radix-ui/colors';
import { create } from 'zustand';
import { createNextSafeStore } from '../../core/zustand/createNextSafeStore';
import { EmbedItemType, radixGrays, radixTextWeights } from './constants';
export type RadixSize = `${0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9}`;
export type RadixSizeWithout0 = Exclude<RadixSize, '0'>;
export type RadixRadius = `${1 | 2 | 3 | 4}` | 'full';
export type RadixTextWeight = (typeof radixTextWeights)[number];
export type RadixColorCompound = {
base: RadixColor;
variant: RadixColorVariant;
};
export type RadixTextColor =
| Exclude<RadixColor, RadixColorGrays>
| 'gray'
| undefined;
export type RadixColorVariantRaw =
`${1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12}`;
export type RadixColorVariant = `${'' | 'a'}${RadixColorVariantRaw}`;
export type RadixColor = Exclude<
keyof typeof radixColors,
`${string}${'A' | 'P3' | 'Dark'}`
>;
export type RadixColorGrays = (typeof radixGrays)[number];
export type ExtractEmbedConfigTypes<Config extends EmbedConfig> = {
[Key in keyof Config]: ({
type: Config[Key]['type'];
} & EmbedConfigItem)['default'];
};
export type EmbedConfig = Record<string, EmbedConfigItem>;
export type EmbedConfigItem = (
| {
type: EmbedItemType.Slider;
default: number;
min: number;
max: number;
}
| {
type: EmbedItemType.Boolean;
default: boolean;
}
| {
type: EmbedItemType.Number;
default: number;
unit?: string;
}
| {
type: EmbedItemType.String;
default: string;
}
| {
type: EmbedItemType.Radius;
default: RadixRadius;
}
| {
type: EmbedItemType.Size;
default: RadixSize;
}
| {
type: EmbedItemType.SizeWithout0;
default: RadixSizeWithout0;
}
| {
type: EmbedItemType.Color;
default: string;
}
| {
type: EmbedItemType.RichText;
default: {
color: string;
size: RadixSizeWithout0;
weight: RadixTextWeight;
};
}
) & {
pad?: boolean;
};
export type EmbedState = Record<string, EmbedConfigItem['default']>;
export const { Provider, use, useMutation, useStore } = createNextSafeStore(
(init: EmbedState) => create<EmbedState>()(() => init),
);
+31
View File
@@ -0,0 +1,31 @@
import { TextProps } from '@radix-ui/themes';
import { EmbedConfig } from '.';
import * as EmbedState from './';
import { EmbedConfigItemType, EmbedItemType } from './constants';
export function useEmbedStateCurry<Config extends EmbedConfig>() {
function useState<Key extends keyof Config>(key: Key) {
return EmbedState.use((state) => {
return (state as EmbedState.ExtractEmbedConfigTypes<Config>)[key];
});
}
function useRichText<
Key extends {
[K in keyof Config]: Config[K] extends { type: EmbedItemType.RichText }
? K
: never;
}[keyof Config],
>(key: Key) {
const { color, ...state } = useState(
key,
) as EmbedConfigItemType<EmbedItemType.RichText>['default'];
return {
style: { color },
...state,
} satisfies TextProps;
}
return { useState, useRichText };
}