From 94623c43eba2209c578365ce3619619d8f04a403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com> Date: Thu, 8 Aug 2024 14:18:22 -0500 Subject: [PATCH] feat: new dynamic system --- .../tools/embed/configurations/breakdown.tsx | 86 +++ src/app/tools/embed/configurations/index.ts | 11 + .../customize/[embed]/components/Boolean.tsx | 18 + .../customize/[embed]/components/Color.tsx | 48 ++ .../tools/embed/customize/[embed]/layout.tsx | 22 + .../tools/embed/customize/[embed]/page.tsx | 524 +++++++++++++++ .../tools/embed/customize/breakdown/page.tsx | 89 --- src/app/tools/embed/types.ts | 106 ---- src/app/tools/embed/utilities.ts | 10 +- .../components/TankSearch/index.tsx | 4 +- src/components/EmbedCustomize.tsx | 598 ------------------ src/components/PageWrapper.tsx | 2 +- src/components/TanksEmbed.tsx | 56 +- src/core/blitzkit/tankDefinitions/index.ts | 6 +- src/stores/embedState/constants.ts | 27 + src/stores/embedState/index.ts | 89 +++ src/stores/embedState/utilities.ts | 31 + 17 files changed, 895 insertions(+), 832 deletions(-) create mode 100644 src/app/tools/embed/configurations/breakdown.tsx create mode 100644 src/app/tools/embed/configurations/index.ts create mode 100644 src/app/tools/embed/customize/[embed]/components/Boolean.tsx create mode 100644 src/app/tools/embed/customize/[embed]/components/Color.tsx create mode 100644 src/app/tools/embed/customize/[embed]/layout.tsx create mode 100644 src/app/tools/embed/customize/[embed]/page.tsx delete mode 100644 src/app/tools/embed/customize/breakdown/page.tsx delete mode 100644 src/app/tools/embed/types.ts delete mode 100644 src/components/EmbedCustomize.tsx create mode 100644 src/stores/embedState/constants.ts create mode 100644 src/stores/embedState/index.ts create mode 100644 src/stores/embedState/utilities.ts diff --git a/src/app/tools/embed/configurations/breakdown.tsx b/src/app/tools/embed/configurations/breakdown.tsx new file mode 100644 index 000000000..6bec434ec --- /dev/null +++ b/src/app/tools/embed/configurations/breakdown.tsx @@ -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(); + + return ( + + {useState('showTotal') && } + + {tanks.slice(0, useState('listMaxTanks')).map((tank) => ( + + ))} + + ); +} diff --git a/src/app/tools/embed/configurations/index.ts b/src/app/tools/embed/configurations/index.ts new file mode 100644 index 000000000..bc39ddb8e --- /dev/null +++ b/src/app/tools/embed/configurations/index.ts @@ -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; + +export const previews = { + breakdown: BreakdownPreview, +} satisfies Record ReactNode>; diff --git a/src/app/tools/embed/customize/[embed]/components/Boolean.tsx b/src/app/tools/embed/customize/[embed]/components/Boolean.tsx new file mode 100644 index 000000000..f01b26107 --- /dev/null +++ b/src/app/tools/embed/customize/[embed]/components/Boolean.tsx @@ -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 ( + state[configKey] as boolean)} + onCheckedChange={() => { + mutateEmbedState((draft) => { + draft[configKey] = !draft[configKey]; + }); + }} + /> + ); +} diff --git a/src/app/tools/embed/customize/[embed]/components/Color.tsx b/src/app/tools/embed/customize/[embed]/components/Color.tsx new file mode 100644 index 000000000..d25a008e5 --- /dev/null +++ b/src/app/tools/embed/customize/[embed]/components/Color.tsx @@ -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(null); + + return ( + <> + + (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}`; + }); + }} + /> + { + 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)}`; + }); + }} + /> + + ); +} diff --git a/src/app/tools/embed/customize/[embed]/layout.tsx b/src/app/tools/embed/customize/[embed]/layout.tsx new file mode 100644 index 000000000..766cdc0d1 --- /dev/null +++ b/src/app/tools/embed/customize/[embed]/layout.tsx @@ -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 ( + {children} + ); +} diff --git a/src/app/tools/embed/customize/[embed]/page.tsx b/src/app/tools/embed/customize/[embed]/page.tsx new file mode 100644 index 000000000..2da7270a1 --- /dev/null +++ b/src/app/tools/embed/customize/[embed]/page.tsx @@ -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(); + + 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 ( + + + + + Customize + + {Object.keys(config).map((configKey) => { + const item = config[configKey]; + let control: ReactNode; + let oneLiner = false; + + switch (item.type) { + case EmbedItemType.Boolean: { + control = ; + oneLiner = true; + break; + } + + case EmbedItemType.Color: { + control = ; + break; + } + } + + return ( + + {capitalize(startCase(configKey as string))} + + {control} + + + ); + })} + + {/* {Object.entries(config).map(([configKeyRaw, configItem]) => { + const configKey = configKeyRaw as keyof ( + | Immutable> + | ExtractEmbedConfigTypes + ); + const stateItem = state[configKey]; + let control: ReactNode; + + if (configItem.type === EmbedItemType.Slider) { + control = ( + + } + config={configItem as EmbedItemType} + /> + ); + } + + // switch (configItem.type) { + // case EmbedConfigType.Slider: { + // control = ( + + // ); + // break; + // } + + // case EmbedConfigType.Boolean: { + // control = ( + // + // mutateState((draft) => { + // (draft[configKey] as boolean) = checked; + // }) + // } + // /> + // ); + // break; + // } + + // case EmbedConfigType.RichText: { + // control = ( + // <> + // ['default']).color}`} + // onValueChange={(value) => { + // mutateState((draft) => { + // ( + // draft[ + // configKey + // ] as EmbedConfigItemType['default'] + // ).color = value as RadixTextColor; + // }); + // }} + // > + // + // + // + // {' '} + // White + // + // + // {' '} + // Gray + // + // {Object.entries(radixColors) + // .filter( + // ([key]) => + // !key.endsWith('A') && + // !key.endsWith('P3') && + // !key.endsWith('Dark') && + // !radixGrays.includes(key), + // ) + // .map(([key]) => ( + // + // {' '} + // {capitalize(key)} + // + // ))} + // + // + // ['default'] + // ).size + // } + // onValueChange={(value) => { + // mutateState((draft) => { + // ( + // draft[ + // configKey + // ] as EmbedConfigItemType['default'] + // ).size = value as RadixSizeWithout0; + // }); + // }} + // > + // + // + // {times(9, (index) => ( + // + // Size {index + 1} + // + // ))} + // + // + // ['default'] + // ).weight + // } + // onValueChange={(value) => { + // mutateState((draft) => { + // ( + // draft[ + // configKey + // ] as EmbedConfigItemType['default'] + // ).weight = value as RadixTextWeight; + // }); + // }} + // > + // + // + // {radixTextWeights.map((weight) => ( + // + // {startCase(weight)} + // + // ))} + // + // + // + // ); + // break; + // } + + // case EmbedConfigType.Number: { + // control = ( + // { + // mutateState((draft) => { + // (draft[configKey] as number) = ( + // event.target as HTMLInputElement + // ).valueAsNumber; + // }); + // }} + // > + // {configItem.unit && ( + // px + // )} + // + // ); + // break; + // } + + // case EmbedConfigType.String: { + // control = ( + // { + // mutateState((draft) => { + // (draft[configKey] as string) = ( + // event.target as HTMLInputElement + // ).value; + // }); + // }} + // /> + // ); + // break; + // } + + // case EmbedConfigType.SizeWithout0: + // case EmbedConfigType.Size: { + // control = ( + // + // mutateState((draft) => { + // (draft[configKey] as RadixSize) = + // `${value}` as RadixSize; + // }) + // } + // /> + // ); + // break; + // } + + // case EmbedConfigType.Radius: { + // control = ( + // + // mutateState((draft) => { + // (draft[configKey] as RadixRadius) = ( + // value === 5 ? 'full' : `${value}` + // ) as RadixRadius; + // }) + // } + // /> + // ); + // break; + // } + + // case EmbedConfigType.Color: { + // control = ( + // <> + // { + // mutateState((draft) => { + // (draft[configKey] as RadixColor).base = + // value as RadixColorBase; + // }); + // }} + // > + // + // + // {Object.entries(radixColors) + // .filter( + // ([key]) => + // !key.endsWith('A') && + // !key.endsWith('P3') && + // !key.endsWith('Dark'), + // ) + // .map(([key]) => ( + // + // {' '} + // {capitalize(key)} + // + // ))} + // + // + + // { + // mutateState((draft) => { + // (draft[configKey] as RadixColor).variant = + // value as RadixColorVariant; + // }); + // }} + // > + // + // + // {times(12, (index) => { + // const variant = + // `${index + 1}` as RadixColorVariantRaw; + + // return ( + // <> + // + // {' '} + // {variant} + // + // + // + // + // {' '} + // {variant} translucent + // + // + // ); + // })} + // + // + // + // ); + // break; + // } + // } + + return ( + + {capitalize(startCase(configKey as string))} + + {control && {control}} + {!control && Immutable} + + ); + })} */} + + + + + + + Example preview + + + + + + + + + + + + ); +} diff --git a/src/app/tools/embed/customize/breakdown/page.tsx b/src/app/tools/embed/customize/breakdown/page.tsx deleted file mode 100644 index 827dcb1ca..000000000 --- a/src/app/tools/embed/customize/breakdown/page.tsx +++ /dev/null @@ -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 ( - ( - - {state.showTotal && } - - {tanks.slice(0, state.listMaxTanks).map((tank) => ( - - ))} - - )} - /> - ); -} diff --git a/src/app/tools/embed/types.ts b/src/app/tools/embed/types.ts deleted file mode 100644 index 40acdc719..000000000 --- a/src/app/tools/embed/types.ts +++ /dev/null @@ -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; - -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; -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 - | '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 = { - [Key in keyof Config]: ({ - type: Config[Key]['type']; - } & EmbedConfigItem)['default']; -}; - -export type EmbedConfigItemType = - (EmbedConfigItem & { type: Type }) diff --git a/src/app/tools/embed/utilities.ts b/src/app/tools/embed/utilities.ts index bf2e6fbaa..2e8be1e58 100644 --- a/src/app/tools/embed/utilities.ts +++ b/src/app/tools/embed/utilities.ts @@ -1,16 +1,16 @@ import { EmbedConfig, - ExtractEmbedConfigType, - RadixColor, + ExtractEmbedConfigTypes, + RadixColorCompound, RadixRadius, -} from './types'; +} from '../../../stores/embedState'; export function extractEmbedConfigDefaults( config: Config, ) { return Object.fromEntries( Object.entries(config).map(([key, value]) => [key, value.default]), - ) as ExtractEmbedConfigType; + ) as ExtractEmbedConfigTypes; } 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})`; } diff --git a/src/app/tools/tankopedia/components/TankSearch/index.tsx b/src/app/tools/tankopedia/components/TankSearch/index.tsx index afb42af69..4c3643891 100644 --- a/src/app/tools/tankopedia/components/TankSearch/index.tsx +++ b/src/app/tools/tankopedia/components/TankSearch/index.tsx @@ -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( 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(); diff --git a/src/components/EmbedCustomize.tsx b/src/components/EmbedCustomize.tsx deleted file mode 100644 index 813519ef6..000000000 --- a/src/components/EmbedCustomize.tsx +++ /dev/null @@ -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: Config; - preview: (state: ExtractEmbedConfigType) => ReactNode; -} - -export function EmbedCustomize({ - config, - preview, -}: EmbedConfigInputsProps) { - const [state, setState] = useState(extractEmbedConfigDefaults(config)); - const [backgroundImage, setBackgroundImage] = useState(imgur('SO13zur')); - const mutateState = useCallback( - (recipe: (draft: Immutable>) => void) => { - setState(produce(recipe)); - }, - [], - ); - - const fileInput = useRef(); - - 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 ( - - - - - Customize - - {Object.entries(config).map(([keyUntyped, setting]) => { - const key = keyUntyped as keyof ( - | Immutable> - | ExtractEmbedConfigType - ); - let control: ReactNode; - - switch (setting.type) { - case EmbedConfigType.Slider: { - control = ( - - ).min - } - max={ - ( - config[ - key - ] as EmbedConfigItemType - ).max - } - onValueChange={([value]) => - mutateState((draft) => { - (draft[key] as number) = value; - }) - } - /> - ); - break; - } - - case EmbedConfigType.Boolean: { - control = ( - - mutateState((draft) => { - (draft[key] as boolean) = checked; - }) - } - /> - ); - break; - } - - case EmbedConfigType.FullTextControl: { - control = ( - <> - ['default']).color}`} - onValueChange={(value) => { - mutateState((draft) => { - ( - draft[ - key - ] as EmbedConfigItemType['default'] - ).color = value as RadixTextColor; - }); - }} - > - - - - {' '} - White - - - {' '} - Gray - - {Object.entries(radixColors) - .filter( - ([key]) => - !key.endsWith('A') && - !key.endsWith('P3') && - !key.endsWith('Dark') && - !radixGrays.includes(key), - ) - .map(([key]) => ( - - {' '} - {capitalize(key)} - - ))} - - - ['default'] - ).size - } - onValueChange={(value) => { - mutateState((draft) => { - ( - draft[ - key - ] as EmbedConfigItemType['default'] - ).size = value as RadixSizeNo0; - }); - }} - > - - - {times(9, (index) => ( - - Size {index + 1} - - ))} - - - ['default'] - ).weight - } - onValueChange={(value) => { - mutateState((draft) => { - ( - draft[ - key - ] as EmbedConfigItemType['default'] - ).weight = value as RadixTextWeight; - }); - }} - > - - - {radixTextWeights.map((weight) => ( - - {startCase(weight)} - - ))} - - - - ); - break; - } - - case EmbedConfigType.Number: { - control = ( - { - mutateState((draft) => { - (draft[key] as number) = ( - event.target as HTMLInputElement - ).valueAsNumber; - }); - }} - > - {setting.unit && ( - px - )} - - ); - break; - } - - case EmbedConfigType.String: { - control = ( - { - mutateState((draft) => { - (draft[key] as string) = ( - event.target as HTMLInputElement - ).value; - }); - }} - /> - ); - break; - } - - case EmbedConfigType.SizeNo0: - case EmbedConfigType.Size: { - control = ( - - mutateState((draft) => { - (draft[key] as RadixSize) = `${value}` as RadixSize; - }) - } - /> - ); - break; - } - - case EmbedConfigType.Radius: { - control = ( - - mutateState((draft) => { - (draft[key] as RadixRadius) = ( - value === 5 ? 'full' : `${value}` - ) as RadixRadius; - }) - } - /> - ); - break; - } - - case EmbedConfigType.Color: { - control = ( - <> - { - mutateState((draft) => { - (draft[key] as RadixColor).base = - value as RadixColorBase; - }); - }} - > - - - {Object.entries(radixColors) - .filter( - ([key]) => - !key.endsWith('A') && - !key.endsWith('P3') && - !key.endsWith('Dark'), - ) - .map(([key]) => ( - - {' '} - {capitalize(key)} - - ))} - - - - { - mutateState((draft) => { - (draft[key] as RadixColor).variant = - value as RadixColorVariant; - }); - }} - > - - - {times(12, (index) => { - const variant = - `${index + 1}` as RadixColorVariantRaw; - - return ( - <> - - {' '} - {variant} - - - - - {' '} - {variant} translucent - - - ); - })} - - - - ); - break; - } - - case EmbedConfigType.TextColor: { - control = ( - { - mutateState((draft) => { - (draft[key] as RadixTextColor) = - value as RadixTextColor; - }); - }} - > - - - - {' '} - White - - - {' '} - Gray - - {Object.entries(radixColors) - .filter( - ([key]) => - !key.endsWith('A') && - !key.endsWith('P3') && - !key.endsWith('Dark') && - !radixGrays.includes(key), - ) - .map(([key]) => ( - - {' '} - {capitalize(key)} - - ))} - - - ); - break; - } - } - - return ( - - {capitalize(startCase(key as string))} - - {control && {control}} - {!control && Immutable} - - ); - })} - - - - - - - Example preview - - - - - - {preview(state)} - - - - - - ); -} diff --git a/src/components/PageWrapper.tsx b/src/components/PageWrapper.tsx index ee94cf614..32df02174 100644 --- a/src/components/PageWrapper.tsx +++ b/src/components/PageWrapper.tsx @@ -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['accentColor']; diff --git a/src/components/TanksEmbed.tsx b/src/components/TanksEmbed.tsx index 86eb61a60..c034e0aa1 100644 --- a/src/components/TanksEmbed.tsx +++ b/src/components/TanksEmbed.tsx @@ -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(); + return ( {children} @@ -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(); + const { color: cardTitleColor, ...restCardTitleProps } = + useState('cardTitle'); + return ( - {tank !== null && state.cardTitleClassIcon && ( + {useState('cardTitleClassIcon') && tank !== null && ( <> {tank.class === 'lightTank' && ( @@ -92,7 +92,7 @@ export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) { - + {times(4, (index) => { const column = (index + 1) as 1 | 2 | 3 | 4; @@ -100,14 +100,14 @@ export function BreakdownEmbedCard({ tank, state }: BreakdownEmbedCardProps) { - - 70{state[`column${column}Unit`]} + + 70{useState(`column${column}Unit`)} - - {state[`column${column}Label`]} + + {useState(`column${column}Label`)} ); diff --git a/src/core/blitzkit/tankDefinitions/index.ts b/src/core/blitzkit/tankDefinitions/index.ts index 652b6f12a..99e6c1590 100644 --- a/src/core/blitzkit/tankDefinitions/index.ts +++ b/src/core/blitzkit/tankDefinitions/index.ts @@ -172,13 +172,13 @@ export const tankDefinitions = fetchCdonLz4( 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, diff --git a/src/stores/embedState/constants.ts b/src/stores/embedState/constants.ts new file mode 100644 index 000000000..77e94bbf8 --- /dev/null +++ b/src/stores/embedState/constants.ts @@ -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 = + EmbedConfigItem & { type: Type }; diff --git a/src/stores/embedState/index.ts b/src/stores/embedState/index.ts new file mode 100644 index 000000000..e0a693b56 --- /dev/null +++ b/src/stores/embedState/index.ts @@ -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; +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 + | '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 = { + [Key in keyof Config]: ({ + type: Config[Key]['type']; + } & EmbedConfigItem)['default']; +}; + +export type EmbedConfig = Record; + +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; + +export const { Provider, use, useMutation, useStore } = createNextSafeStore( + (init: EmbedState) => create()(() => init), +); diff --git a/src/stores/embedState/utilities.ts b/src/stores/embedState/utilities.ts new file mode 100644 index 000000000..9fbe26d6a --- /dev/null +++ b/src/stores/embedState/utilities.ts @@ -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() { + function useState(key: Key) { + return EmbedState.use((state) => { + return (state as EmbedState.ExtractEmbedConfigTypes)[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['default']; + + return { + style: { color }, + ...state, + } satisfies TextProps; + } + + return { useState, useRichText }; +}