diff --git a/src/app/tools/embed/customize/tanks/page.tsx b/src/app/tools/embed/customize/tanks/page.tsx index fd3b91b5e..77159723d 100644 --- a/src/app/tools/embed/customize/tanks/page.tsx +++ b/src/app/tools/embed/customize/tanks/page.tsx @@ -1,13 +1,11 @@ 'use client'; -import { Box } from '@radix-ui/themes'; import { Dispatch, SetStateAction, use, useState } from 'react'; import { EmbedConfigInputs } from '../../../../../components/EmbedConfigInputs'; import { TanksEmbedCard, TanksEmbedWrapper, } from '../../../../../components/TanksEmbed'; -import { imgur } from '../../../../../core/blitzkit/imgur'; import { tanksDefinitionsArray } from '../../../../../core/blitzkit/tankDefinitions'; import { EmbedConfig, @@ -17,9 +15,11 @@ import { import { extractEmbedConfigDefaults } from '../../utilities'; const tankEmbedConfig = { + showTotal: { type: EmbedConfigType.Boolean, default: true }, + listWidth: { type: EmbedConfigType.Number, default: 320, unit: 'px' }, listGap: { type: EmbedConfigType.Size, default: '2' }, - listMaxTanks: { type: EmbedConfigType.Number, default: 8, pad: true }, + listMaxTanks: { type: EmbedConfigType.Number, default: 4, pad: true }, cardRadius: { type: EmbedConfigType.Radius, default: '2' }, cardHeaderBackgroundColor: { @@ -27,6 +27,12 @@ const tankEmbedConfig = { default: { base: 'gray', variant: '3' }, }, cardHeaderPadding: { type: EmbedConfigType.Size, default: '1' }, + cardTitle: { + type: EmbedConfigType.FullTextControl, + default: { color: undefined, weight: 'bold', size: '3' }, + }, + cardTitleAutoColor: { type: EmbedConfigType.Boolean, default: true }, + cardTitleAutoIcon: { type: EmbedConfigType.Boolean, default: true }, cardBodyBackgroundColor: { type: EmbedConfigType.Color, default: { base: 'gray', variant: '2' }, @@ -34,12 +40,13 @@ const tankEmbedConfig = { cardBodyPadding: { type: EmbedConfigType.Size, default: '2', pad: true }, columnGap: { type: EmbedConfigType.Size, default: '0' }, - columnValueSize: { type: EmbedConfigType.SizeNo0, default: '3' }, - columnValueColor: { type: EmbedConfigType.TextColor, default: undefined }, - columnLabelSize: { type: EmbedConfigType.SizeNo0, default: '2' }, - columnLabelColor: { - type: EmbedConfigType.TextColor, - default: 'gray', + columnValue: { + type: EmbedConfigType.FullTextControl, + default: { color: undefined, size: '3', weight: 'regular' }, + }, + columnLabel: { + type: EmbedConfigType.FullTextControl, + default: { color: 'gray', size: '2', weight: 'regular' }, pad: true, }, @@ -62,29 +69,22 @@ export default function Page() { ); 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 index e75d3aad5..f8a351dce 100644 --- a/src/app/tools/embed/types.ts +++ b/src/app/tools/embed/types.ts @@ -8,6 +8,8 @@ export enum EmbedConfigType { SizeNo0, TextColor, String, + FullTextControl, + Boolean, } export const radixGrays = [ @@ -19,9 +21,15 @@ export const radixGrays = [ 'sand', ] as const; +export const radixTextWeights = ['light', 'regular', 'medium', 'bold'] as const; + export type EmbedConfig = Record; export type EmbedConfigItem = ( + | { + type: EmbedConfigType.Boolean; + default: boolean; + } | { type: EmbedConfigType.Number; default: number; @@ -51,12 +59,24 @@ export type EmbedConfigItem = ( type: EmbedConfigType.TextColor; default: RadixTextColor; } + | { + type: EmbedConfigType.FullTextControl; + default: { + color: RadixTextColor; + size: RadixSizeNo0; + weight: RadixTextWeight; + }; + } ) & { pad?: boolean; }; +export type EmbedConfigItemType = + (EmbedConfigItem & { type: Type })['default']; + 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< diff --git a/src/components/EmbedConfigInputs.tsx b/src/components/EmbedConfigInputs.tsx index f5a3ab5f0..e853c0111 100644 --- a/src/components/EmbedConfigInputs.tsx +++ b/src/components/EmbedConfigInputs.tsx @@ -1,25 +1,37 @@ import * as radixColors from '@radix-ui/colors'; -import { DragHandleDots2Icon } from '@radix-ui/react-icons'; +import { + DragHandleDots2Icon, + EnterFullScreenIcon, + ExitFullScreenIcon, + ImageIcon, +} from '@radix-ui/react-icons'; import { Box, + Button, Card, Flex, + IconButton, + Inset, ScrollArea, Select, + Switch, Text, TextField, } from '@radix-ui/themes'; import { produce } from 'immer'; -import { capitalize, startCase, times } from 'lodash'; +import { capitalize, clamp, startCase, times } from 'lodash'; import { Dispatch, ReactNode, SetStateAction, useCallback, + useEffect, useRef, + useState, } from 'react'; import { EmbedConfig, + EmbedConfigItemType, EmbedConfigType, ExtractEmbedConfigType, RadixColor, @@ -29,7 +41,10 @@ import { radixGrays, RadixRadius, RadixSize, + RadixSizeNo0, RadixTextColor, + RadixTextWeight, + radixTextWeights, } from '../app/tools/embed/types'; import { toColorVar, toRadiusVar } from '../app/tools/embed/utilities'; @@ -37,13 +52,19 @@ interface EmbedConfigInputsProps { config: EmbedConfig; state: ExtractEmbedConfigType; setState: Dispatch>>; + children: ReactNode; } export function EmbedConfigInputs({ config, state, setState, + children, }: EmbedConfigInputsProps) { + const [expanded, setExpanded] = useState(true); + const [backgroundImage, setBackgroundImage] = useState( + 'https://upload.wikimedia.org/wikipedia/commons/f/f6/Swiss_National_Park_131.JPG', + ); const mutateState = useCallback( (recipe: (draft: ExtractEmbedConfigType) => void) => { setState(produce(recipe)); @@ -56,164 +77,133 @@ export function EmbedConfigInputs({ if (!wrapper.current) return; - wrapper.current.style.top = `${parseFloat(wrapper.current.style.top) + event.movementY}px`; - wrapper.current.style.right = `${parseFloat(wrapper.current.style.right) - event.movementX}px`; + wrapper.current.style.top = `${clamp(parseFloat(wrapper.current.style.top) + event.movementY, 32, window.innerHeight - 32 - 32)}px`; + wrapper.current.style.right = `${clamp(parseFloat(wrapper.current.style.right) - event.movementX, 32, window.innerWidth - 32 - 320)}px`; }, []); - - const handlePointerUp = useCallback((event: PointerEvent) => { + const handlePointerUp = useCallback(() => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); }, []); + 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 ( - - { - window.addEventListener('pointermove', handlePointerMove); - window.addEventListener('pointerup', handlePointerUp); - }} + - - + + { + event.preventDefault(); + window.addEventListener('pointermove', handlePointerMove); + window.addEventListener('pointerup', handlePointerUp); + }} + > + + + - - - {Object.entries(config).map(([key, setting]) => { - let control: ReactNode; + + setExpanded((expanded) => !expanded)} + > + {expanded ? : } + + + + - switch (setting.type) { - case EmbedConfigType.Number: { - control = ( - { - mutateState((draft) => { - draft[key] = ( - event.target as HTMLInputElement - ).valueAsNumber; - }); - }} - > - {setting.unit && ( - px - )} - - ); - break; - } + {expanded && ( + + + - case EmbedConfigType.String: { - control = ( - { - mutateState((draft) => { - draft[key] = (event.target as HTMLInputElement).value; - }); - }} - /> - ); - break; - } + {Object.entries(config).map(([key, setting]) => { + let control: ReactNode; - case EmbedConfigType.SizeNo0: - case EmbedConfigType.Size: { - control = ( - { - mutateState((draft) => { - draft[key] = value as RadixSize; - }); - }} - > - - - {times(10, (index) => - setting.type === EmbedConfigType.SizeNo0 && - index === 0 ? null : ( - - {index} - - ), - )} - - - ); - break; - } + switch (setting.type) { + case EmbedConfigType.Boolean: { + control = ( + + mutateState((draft) => { + draft[key] = checked; + }) + } + /> + ); + break; + } - case EmbedConfigType.Radius: { - control = ( - { - mutateState((draft) => { - draft[key] = value as RadixRadius; - }); - }} - > - - - {times(5, (index) => ( - - {index} - - ))} - Full - - - ); - 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]) => ( - + case EmbedConfigType.FullTextControl: { + control = ( + <> + ).color}`} + onValueChange={(value) => { + mutateState((draft) => { + ( + draft[ + key + ] as EmbedConfigItemType + ).color = value as RadixTextColor; + }); + }} + > + + + {' '} - {capitalize(key)} + White - ))} - - - - { - mutateState((draft) => { - (draft[key] as RadixColor).variant = - value as RadixColorVariant; - }); - }} - > - - - {times(12, (index) => { - const variant = - `${index + 1}` as RadixColorVariantRaw; - - return ( - <> - - {' '} - {variant} - - - + + {' '} + Gray + + {Object.entries(radixColors) + .filter( + ([key]) => + !key.endsWith('A') && + !key.endsWith('P3') && + !key.endsWith('Dark') && + !radixGrays.includes(key), + ) + .map(([key]) => ( + - {' '} - {variant} translucent + />{' '} + {capitalize(key)} + + ))} + + + + ).size + } + onValueChange={(value) => { + mutateState((draft) => { + ( + draft[ + key + ] as EmbedConfigItemType + ).size = value as RadixSizeNo0; + }); + }} + > + + + {times(9, (index) => ( + + Size {index + 1} - - ); - })} - - - - ); - break; - } + ))} + + + + ).weight + } + onValueChange={(value) => { + mutateState((draft) => { + ( + draft[ + key + ] as EmbedConfigItemType + ).weight = value as RadixTextWeight; + }); + }} + > + + + {radixTextWeights.map((weight) => ( + + {startCase(weight)} + + ))} + + + + ); + break; + } - case EmbedConfigType.TextColor: { - control = ( - { - mutateState((draft) => { - draft[key] = value as RadixTextColor; - }); - }} - > - - - - { + mutateState((draft) => { + draft[key] = ( + event.target as HTMLInputElement + ).valueAsNumber; + }); + }} + > + {setting.unit && ( + px + )} + + ); + break; + } + + case EmbedConfigType.String: { + control = ( + { + mutateState((draft) => { + draft[key] = ( + event.target as HTMLInputElement + ).value; + }); + }} + /> + ); + break; + } + + case EmbedConfigType.SizeNo0: + case EmbedConfigType.Size: { + control = ( + { + mutateState((draft) => { + draft[key] = value as RadixSize; + }); + }} + > + + + {times(10, (index) => + setting.type === EmbedConfigType.SizeNo0 && + index === 0 ? null : ( + + {index} + + ), + )} + + + ); + break; + } + + case EmbedConfigType.Radius: { + control = ( + { + mutateState((draft) => { + draft[key] = value as RadixRadius; + }); + }} + > + + + {times(5, (index) => ( + + {index} + + ))} + Full + + + ); + break; + } + + case EmbedConfigType.Color: { + control = ( + <> + { + mutateState((draft) => { + (draft[key] as RadixColor).base = + value as RadixColorBase; + }); }} - />{' '} - White - - - + + + {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; + }); }} - />{' '} - Gray - - {Object.entries(radixColors) - .filter( - ([key]) => - !key.endsWith('A') && - !key.endsWith('P3') && - !key.endsWith('Dark') && - !radixGrays.includes(key), - ) - .map(([key]) => ( - + > + + + {times(12, (index) => { + const variant = + `${index + 1}` as RadixColorVariantRaw; + + return ( + <> + + {' '} + {variant} + + + + + {' '} + {variant} translucent + + + ); + })} + + + + ); + break; + } + + case EmbedConfigType.TextColor: { + control = ( + { + mutateState((draft) => { + draft[key] = value as RadixTextColor; + }); + }} + > + + + {' '} - {capitalize(key)} + White - ))} - - - ); - break; - } - } + + {' '} + 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))} - - {control && {control}} - {!control && ( - - Immutable - - )} - - ); - })} - - - + return ( + + + {capitalize(startCase(key))} + + {control && {control}} + {!control && ( + + Immutable + + )} + + ); + })} + + + )} + + {!expanded && ( + + + Expand to configure + + + )} + + + {children} + ); } diff --git a/src/components/TanksEmbed.tsx b/src/components/TanksEmbed.tsx index 9d85a440f..c6b8b12c4 100644 --- a/src/components/TanksEmbed.tsx +++ b/src/components/TanksEmbed.tsx @@ -8,6 +8,10 @@ import { toWidthVar, } from '../app/tools/embed/utilities'; import { TankDefinition } from '../core/blitzkit/tankDefinitions'; +import { ClassHeavy } from './ClassIcon/components/ClassHeavy'; +import { ClassLight } from './ClassIcon/components/ClassLight'; +import { ClassMedium } from './ClassIcon/components/ClassMedium'; +import { ClassTankDestroyer } from './ClassIcon/components/ClassTankDestroyer'; interface TanksEmbedWrapperProps { children: ReactNode; @@ -27,11 +31,12 @@ export function TanksEmbedWrapper({ children, state }: TanksEmbedWrapperProps) { } interface TanksEmbedCardProps { - tank: TankDefinition; + tank: TankDefinition | null; state: TanksEmbedState; } export function TanksEmbedCard({ tank, state }: TanksEmbedCardProps) { + const { color: cardTitleColor, ...restCardTitleProps } = state.cardTitle; return ( - {tank.name} + + + {tank !== null && state.cardTitleAutoIcon && ( + <> + {tank.class === 'lightTank' && ( + + )} + {tank.class === 'mediumTank' && ( + + )} + {tank.class === 'heavyTank' && ( + + )} + {tank.class === 'AT-SPG' && ( + + )} + + )}{' '} + {tank === null ? 'Total' : tank.name} + + @@ -62,10 +100,10 @@ export function TanksEmbedCard({ tank, state }: TanksEmbedCardProps) { gap={state.columnGap} flexGrow="1" > - + 70{state[`column${column}Unit`]} - + {state[`column${column}Label`]}