Files
TriggerFrontend/src/pages/settings/components/Theming/Themes/ThemeModal.tsx
Sean Edmunds e0adad34ba Missed logic
2025-07-22 14:27:04 -05:00

673 lines
20 KiB
TypeScript

import {
ActionIcon,
Button,
Checkbox,
ColorPicker,
ColorSwatch,
Group,
Image,
Modal,
ScrollArea,
Stack,
Text,
TextInput,
Tooltip,
UnstyledButton,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react';
import { FiImage, FiSave, FiX } from 'react-icons/fi';
import { modalClasses, scrollAreaClasses, textClasses, textInputClasses } from 'styles';
import { BackgroundProperties, type Theme } from 'types';
import { ThemeModalContent } from './ThemeModalContent';
export function ThemeModal({
opened,
handleClose,
selectedTheme,
setSelectedTheme,
openPicker,
backgroundList,
pickerOpen,
closePicker,
handleSaveTheme,
}: {
opened: boolean;
handleClose: () => void;
selectedTheme: Theme;
setSelectedTheme: (selectedTheme: Theme) => void;
openPicker: () => void;
backgroundList: string[];
pickerOpen: boolean;
closePicker: () => void;
handleSaveTheme: () => void;
}) {
const { isMobile, theme, getObjectDiff } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000');
const [defaultTheme, setDefaultTheme] = useState<Theme>({ ...selectedTheme });
const [designerTheme, setDesignerTheme] = useState<Theme>({
...selectedTheme,
});
const filtered = backgroundList.filter((item: string) =>
item.toLowerCase().includes(query.toLowerCase()),
);
const [backgroundPicker, { open: openBackgroundPicker, close: closeBackgroundPicker }] =
useDisclosure(false);
const re = /(?:\.([^.]+))?$/;
const items = filtered.map((item: string, index: number) => {
return (
<UnstyledButton
data-list-item
key={item}
style={{
width: '100%',
height: '75px',
background:
item !== 'none' && item !== 'None'
? index === hovered
? `linear-gradient(${selectedTheme.backgroundColor}88, ${selectedTheme.backgroundColor}88), no-repeat center url(../../api/backgroundImages/thumbs/${item})`
: `no-repeat center url(../../api/backgroundImages/thumbs/${item})`
: 'repeating-linear-gradient(45deg, #33333366, #33333366 10px, #88888866 10px, #88888866 20px)',
backgroundSize: item !== 'none' ? 'cover' : undefined,
backgroundColor: item === 'none' ? '#00000088' : undefined,
paddingLeft: '.5rem',
alignItems: 'center',
}}
onClick={() =>
setDesignerTheme({
...designerTheme,
backgroundImage: item,
})
}
>
<Group h={'100%'} w={'100%'} justify='space-between'>
<Text
style={{
width: 'min-content',
backdropFilter: 'blur(5px) brightness(.25)',
padding: '.5rem',
borderBottomRightRadius: 4,
marginTop: '-2.15rem',
marginLeft: '-.5rem',
color: 'white',
}}
>
{item.replace(/\.[^/.]+$/, '')}
</Text>
{item !== 'None' && (
<Text
style={{
width: 'min-content',
backdropFilter: 'blur(5px) brightness(.25)',
padding: '.5rem',
marginTop: '2rem',
marginBottom: -1,
borderTopLeftRadius: 4,
color: 'white',
}}
>
{re.exec(item.toUpperCase())?.[1]}
</Text>
)}
</Group>
</UnstyledButton>
);
});
const handleSwatchClick = () => {
closeBackgroundPicker();
openPicker();
setDesignerTheme(selectedTheme);
};
useEffect(() => {
setDesignerTheme(selectedTheme);
if (defaultTheme.id !== selectedTheme.id) {
setDefaultTheme(selectedTheme);
}
}, [selectedTheme]);
return (
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={handleClose}
fullScreen={isMobile}
centered
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text c={'var(--mantine-color-background-0)'}>Name:</Text>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
value={selectedTheme.name}
onChange={(e) =>
setSelectedTheme({ ...selectedTheme, name: e.target.value })
}
styles={{
label: {
color: 'var(--mantine-color-primary-0)',
},
input: {
color: selectedTheme.primaryColor,
backgroundColor: selectedTheme.backgroundColor,
border: 'none',
borderBottom: '1px solid var(--mantine-color-primary-0)',
},
}}
/>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<Group p={'2rem'} w={'100%'} align='flex-start' justify='space-between'>
<Stack display={'flex'} pos={'relative'} w={isMobile ? '100%' : '70%'}>
<ThemeModalContent selectedTheme={designerTheme} />
<Tooltip
label={'Change Primary Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('primaryColor');
setColorValue(selectedTheme.primaryColor);
handleSwatchClick();
}}
color={selectedTheme.primaryColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'6.25rem'}
left={'1.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Accent Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('accentColor');
setColorValue(selectedTheme.accentColor);
handleSwatchClick();
}}
color={selectedTheme.accentColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
bottom={'6rem'}
left={'11rem'}
/>
</Tooltip>
<Tooltip
label={'Change Background Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('backgroundColor');
setColorValue(selectedTheme.backgroundColor);
handleSwatchClick();
}}
color={selectedTheme.backgroundColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
bottom={'10rem'}
right={'3.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Border Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('borderColor');
setColorValue(selectedTheme.borderColor);
handleSwatchClick();
}}
color={selectedTheme.borderColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'4.5rem'}
right={'15%'}
/>
</Tooltip>
<Tooltip
label={'Change Search Color'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ColorSwatch
onClick={() => {
setColorId('searchColor');
setColorValue(selectedTheme.searchColor);
handleSwatchClick();
}}
color={selectedTheme.searchColor}
bd={`1px solid ${selectedTheme.primaryColor}`}
pos={'absolute'}
top={'3.25rem'}
left={'6.5rem'}
/>
</Tooltip>
<Tooltip
label={'Change Background'}
c={'var(--mantine-color-primary-0)'}
bg={'var(--mantine-color-background-0)'}
>
<ActionIcon
onClick={() => {
closePicker();
openBackgroundPicker();
setDesignerTheme(selectedTheme);
}}
pos={'absolute'}
right={0}
>
<FiImage size={20} />
</ActionIcon>
</Tooltip>
</Stack>
<Stack
w={isMobile ? '100%' : '28%'}
h={'436px'}
p={'1rem'}
style={{
borderRadius: '1rem',
backgroundColor:
'color-mix(in srgb, var(--mantine-color-primary-0) 20%, transparent)',
}}
>
{pickerOpen && (
<Stack h={'100%'} align='center' justify='space-between'>
<Text
className={textClasses.sectionTitle}
classNames={textClasses}
style={{
textShadow: theme.textShadowsEnabled
? '1px 1px 1px color-mix(in srgb, var(--mantine-color-accent-0), transparent 50%)'
: undefined,
}}
>
Change{' '}
{BackgroundProperties[colorId as keyof typeof BackgroundProperties]}
</Text>
{colorId === 'backgroundColor' && (
<ColorPicker
value={colorValue}
onChange={(e) => {
setColorValue(e);
setDesignerTheme({
...designerTheme,
backgroundColor: e,
});
}}
format={'hex'}
w={'90%'}
mb={'1.5rem'}
/>
)}
{colorId !== 'backgroundColor' && (
<ColorPicker
value={colorValue}
onChange={(e) => {
setColorValue(e);
setDesignerTheme({
...designerTheme,
[colorId]: e,
});
}}
format={'hexa'}
w={'90%'}
/>
)}
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
w={'90%'}
value={colorValue}
onChange={(e) => setColorValue(e.target.value)}
/>
<Group mt={'auto'} justify='center'>
<Button
color={'green'}
onClick={() => {
switch (colorId) {
case 'primaryColor':
setSelectedTheme({
...selectedTheme,
primaryColor: colorValue,
});
break;
case 'accentColor':
setSelectedTheme({
...selectedTheme,
accentColor: colorValue,
});
break;
case 'backgroundColor':
setSelectedTheme({
...selectedTheme,
backgroundColor: colorValue,
});
break;
case 'borderColor':
setSelectedTheme({
...selectedTheme,
borderColor: colorValue,
});
break;
case 'searchColor':
setSelectedTheme({
...selectedTheme,
searchColor: colorValue,
});
break;
}
closePicker();
}}
>
<FiSave />
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closePicker();
setDesignerTheme({
...designerTheme,
[colorId]: selectedTheme[colorId as keyof Theme],
});
}}
>
<FiX />
</Button>
</Group>
</Stack>
)}
{backgroundPicker && (
<Stack h={'100%'} align='center' justify='center'>
<Group gap={0}>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
placeholder='Search'
value={query.replace(/\.[^/.]+$/, '')}
rightSection={
<FiX
style={{
cursor: 'pointer',
margin: '.25rem',
}}
onClick={() => {
setQuery('');
}}
/>
}
onChange={(event) => {
setQuery(event.currentTarget.value);
setHovered(-1);
}}
onKeyDown={(event) => {
if (event.key === 'ArrowDown') {
event.preventDefault();
setHovered((current) => {
const nextIndex =
current + 1 >= filtered.length ? current : current + 1;
viewportRef.current
?.querySelectorAll('[data-list-item]')
?.[nextIndex]?.scrollIntoView({ block: 'nearest' });
return nextIndex;
});
}
if (event.key === 'ArrowUp') {
event.preventDefault();
setHovered((current) => {
const nextIndex = current - 1 < 0 ? current : current - 1;
viewportRef.current
?.querySelectorAll('[data-list-item]')
?.[nextIndex]?.scrollIntoView({ block: 'nearest' });
return nextIndex;
});
}
}}
/>
</Group>
<ScrollArea.Autosize
viewportRef={viewportRef}
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
style={{
height: 325,
width: '90%',
borderRadius: 8,
}}
>
<Stack gap={0} align='center' display={'block'}>
{items}
</Stack>
</ScrollArea.Autosize>
<Group justify='center'>
<Button
color={'green'}
onClick={() => {
setSelectedTheme({
...selectedTheme,
backgroundImage: designerTheme.backgroundImage,
});
closeBackgroundPicker();
}}
>
<FiSave />
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeBackgroundPicker();
setDesignerTheme({
...designerTheme,
backgroundImage: selectedTheme.backgroundImage,
});
}}
>
<FiX />
</Button>
</Group>
</Stack>
)}
{!pickerOpen && !backgroundPicker && (
<Stack h={'100%'} justify='space-between' align='center' gap={4}>
<Stack w={'100%'} align='center' gap={4}>
<Checkbox
label='Text Shadows'
checked={selectedTheme.textShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
appShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
</Stack>
{JSON.stringify(defaultTheme) !== JSON.stringify(selectedTheme) && (
<>
<Text fz={'1.5rem'}>Changes:</Text>
<ScrollArea.Autosize
w={'100%'}
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
style={{
backgroundColor: 'var(--mantine-color-accent-0)',
borderRadius: 8,
padding: '1rem',
}}
>
<Stack w={'100%'} gap={0}>
{getObjectDiff(defaultTheme, designerTheme) !== null &&
Object.keys(getObjectDiff(defaultTheme, designerTheme)).map(
(key: any) => {
return (
<Stack
w={'100%'}
align='center'
justify='center'
gap={0}
key={key}
>
<Text key={key}>
{
BackgroundProperties[
key as keyof typeof BackgroundProperties
]
}
</Text>
{key === 'name' && (
<Group w={'100%'} justify='center'>
<Text>{defaultTheme[key as keyof Theme]}</Text>
<Text>→</Text>
<Text>{designerTheme[key as keyof Theme]}</Text>
</Group>
)}
{key === 'backgroundImage' && (
<Group w={'100%'} justify='center'>
<Image
w={'40%'}
src={
(
defaultTheme[key as keyof Theme] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${defaultTheme[key as keyof Theme] as string}`
}
/>
<Text>→</Text>
<Image
w={'40%'}
src={
(
designerTheme[key as keyof Theme] as string
).toLocaleLowerCase() === 'none'
? 'https://placehold.co/540x360?text=None'
: `../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}`
}
/>
</Group>
)}
{key.includes('Color') && (
<Group w={'100%'} justify='center'>
<ColorSwatch
color={
defaultTheme[key as keyof Theme] as string
}
/>
<Text>→</Text>
<ColorSwatch
color={
designerTheme[key as keyof Theme] as string
}
/>
</Group>
)}
{key.includes('Shadow') && (
<Group w={'100%'} justify='center'>
{defaultTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
<Text>→</Text>
{designerTheme[key as keyof Theme] === 0 ? (
<Text>Off</Text>
) : (
<Text>On</Text>
)}
</Group>
)}
</Stack>
);
},
)}
</Stack>
</ScrollArea.Autosize>
</>
)}
<Button
onClick={() => {
setSelectedTheme({ ...defaultTheme });
}}
mih={'36px'}
>
Reset Changes
</Button>
</Stack>
)}
</Stack>
</Group>
<Group>
<Button
disabled={JSON.stringify(defaultTheme) === JSON.stringify(selectedTheme)}
color={'green'}
onClick={handleSaveTheme}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
closeBackgroundPicker();
handleClose();
}}
>
Cancel
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}