550 lines
15 KiB
TypeScript
550 lines
15 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Button,
|
|
Checkbox,
|
|
ColorPicker,
|
|
ColorSwatch,
|
|
Group,
|
|
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 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 } = 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/${item})`
|
|
: `no-repeat center url(../../api/backgroundImages/${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.name !== selectedTheme.name) {
|
|
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%)',
|
|
}}
|
|
>
|
|
Change {colorId}
|
|
</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%'} align='center' justify='center' gap={4}>
|
|
<Checkbox
|
|
label='Text Shadows'
|
|
checked={selectedTheme.textShadowsEnabled === 1}
|
|
onChange={(e) => {
|
|
setSelectedTheme({
|
|
...selectedTheme,
|
|
textShadowsEnabled: e.target.checked ? 1 : 0,
|
|
});
|
|
}}
|
|
/>
|
|
<Checkbox
|
|
label='App Shadows'
|
|
checked={selectedTheme.appShadowsEnabled === 1}
|
|
onChange={(e) => {
|
|
setSelectedTheme({
|
|
...selectedTheme,
|
|
appShadowsEnabled: e.target.checked ? 1 : 0,
|
|
});
|
|
}}
|
|
/>
|
|
<Button
|
|
onClick={() => {
|
|
setSelectedTheme({ ...defaultTheme });
|
|
}}
|
|
>
|
|
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>
|
|
);
|
|
}
|