New settings pages

This commit is contained in:
Sean Edmunds
2025-03-20 17:09:07 -05:00
parent cecdc84cf6
commit 321d620d3c
28 changed files with 529 additions and 228 deletions

View File

@@ -0,0 +1,312 @@
import { ActionIcon, Box, Button, Group, Overlay, Stack, Text } from '@mantine/core';
import { useCommonHooks, useQueryAPI } from 'hooks';
import { useState } from 'react';
import { FiEdit, FiShuffle, FiTrash, FiX } from 'react-icons/fi';
import { actionIconClasses } from 'styles';
function ThemeCard({
cardTheme,
handleOpen,
handleDeleteTheme,
createTheme,
allThemes,
}: any) {
const { data: themePlaylists } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const [hover, setHover] = useState(false);
const [shiftEdit, setShiftEdit] = useState<number>(0);
const [shiftShuffle, setShiftShuffle] = useState<number>(0);
const {
handleThemeChange,
currentTheme,
setCurrentTheme,
theme,
currentThemePlaylist,
setCurrentThemePlaylist,
} = useCommonHooks();
return (
<Group
gap={0}
align='center'
justify='center'
display={'grid'}
pos={'relative'}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<Text
style={{
position: 'absolute',
top: -10,
left: 6,
paddingRight: 4,
paddingLeft: 4,
backgroundColor: cardTheme.borderColor,
borderRadius: '4px',
color:
cardTheme.borderColor === cardTheme.primaryColor
? cardTheme.accentColor
: cardTheme.primaryColor,
zIndex: 3,
}}
>
{cardTheme.name}
</Text>
<Button
w={'275px'}
h={'115px'}
disabled={currentTheme === 'random'}
style={{
background: `linear-gradient(${cardTheme.backgroundColor}33, ${cardTheme.backgroundColor}33), no-repeat center url(/backgrounds/${cardTheme.backgroundImage})`,
backgroundSize: 'cover',
backgroundColor: cardTheme.backgroundColor,
border: `solid ${cardTheme.borderColor}`,
filter: cardTheme.randomEnabled === 0 ? 'saturate(0)' : undefined,
}}
onClick={() => {
setCurrentTheme(cardTheme.name);
setCurrentThemePlaylist('All');
}}
>
{hover && currentTheme !== 'random' && <Overlay opacity={0.5} zIndex={1} />}
<Group w={'100%'} gap={0} display={'flex'} align='center' justify='center'>
<Stack align='center'>
<Box
style={{
height: '10px',
width: '100px',
backgroundColor: cardTheme.searchColor,
borderRadius: '4px',
borderBottom: `solid 1px ${cardTheme.borderColor}`,
}}
/>
<Group justify='space-between'>
<Box
style={{
height: '8px',
width: '100px',
backgroundColor: cardTheme.accentColor,
borderRadius: '4px',
}}
/>
<Box
style={{
height: '8px',
width: '50px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'}>
<Box
style={{
height: '5px',
width: '40px',
backgroundColor: cardTheme.primaryColor,
borderRadius: '4px',
}}
/>
</Group>
<Group w={'100%'}>
<Box
style={{
height: '10px',
width: '40px',
borderRadius: '4px',
backdropFilter: 'blur(10px) brightness(1.5)',
fontSize: '6px',
justifyContent: 'center',
alignContent: 'center',
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
<Box
style={{
height: '10px',
width: '40px',
borderRadius: '4px',
backdropFilter: 'blur(10px) brightness(1.5)',
fontSize: '6px',
justifyContent: 'center',
alignContent: 'center',
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
<Box
style={{
height: '10px',
width: '40px',
borderRadius: '4px',
backdropFilter: 'blur(10px) brightness(1.5)',
fontSize: '6px',
justifyContent: 'center',
alignContent: 'center',
color: cardTheme.primaryColor,
}}
>
lorem ipsum
</Box>
</Group>
</Stack>
</Group>
</Button>
{currentTheme === 'random' && cardTheme.name === theme.name ? (
<Group>
<Group
onMouseEnter={() => {
setShiftEdit(2);
}}
onMouseLeave={() => {
setShiftEdit(0);
}}
onClick={() => {
setShiftEdit(0);
}}
gap={0}
style={{
zIndex: 300,
position: 'absolute',
left: 0,
top: 27 - shiftEdit,
backgroundColor: shiftEdit === 0 ? 'white' : 'var(--color-background)',
borderRadius: '50%',
padding: '8px',
margin: '4px',
border: 'solid 2px',
borderColor: shiftEdit === 0 ? 'black' : 'var(--color-text-pri)',
transition: 'all 0.2s',
}}
>
<FiEdit
onClick={() => {
handleOpen(cardTheme);
}}
style={{
fontSize: '2em',
fontWeight: 'bold',
color: shiftEdit === 0 ? 'black' : 'var(--color-text-pri)',
}}
/>
</Group>
<Group
onMouseEnter={() => {
setShiftShuffle(2);
}}
onMouseLeave={() => {
setShiftShuffle(0);
}}
onClick={() => {
setShiftShuffle(0);
}}
gap={0}
style={{
zIndex: 300,
position: 'absolute',
right: 0,
top: 27 - shiftShuffle,
backgroundColor: shiftShuffle === 0 ? 'white' : 'var(--color-background)',
borderRadius: '50%',
padding: '8px',
margin: '4px',
border: 'solid 2px',
borderColor: shiftShuffle === 0 ? 'black' : 'var(--color-text-pri)',
transition: 'all 0.2s',
}}
>
<FiShuffle
onClick={() => {
const themeList = JSON.parse(
themePlaylists.find((t: any) => t.name === currentThemePlaylist)
.themeList,
);
const filteredThemes = allThemes.filter((t: any) =>
themeList.includes(t.id),
);
handleThemeChange(filteredThemes, false);
}}
style={{
fontSize: '2em',
fontWeight: 'bold',
color: shiftShuffle === 0 ? 'black' : 'var(--color-text-pri)',
}}
/>
</Group>
</Group>
) : (
<Stack
gap={0}
style={{
zIndex: 2,
padding: '4px',
paddingRight: '0px',
position: 'absolute',
right: 3,
backgroundColor: '#ffffff88',
borderTopLeftRadius: '10px',
borderBottomLeftRadius: '10px',
}}
align='center'
justify='center'
display={
currentTheme === 'random' && cardTheme.name === theme.name ? 'none' : 'flex'
}
>
<ActionIcon
style={{
background: cardTheme.randomEnabled
? 'rgba(255,255,255,1)'
: 'rgba(255,255,255,0.5)',
margin: '2px',
}}
styles={{
root: {
height: '10px',
width: '10px',
},
icon: {
height: '20px',
width: '20px',
fontSize: '24px',
margin: 0,
padding: 0,
},
}}
onClick={() => {
createTheme({
method: 'PATCH',
endpoint: 'api/themes',
body: { ...cardTheme, randomEnabled: !cardTheme.randomEnabled },
});
}}
>
{cardTheme.randomEnabled ? <FiShuffle color='green' /> : <FiX color='red' />}
</ActionIcon>
<ActionIcon
className={actionIconClasses.themeEditButton}
classNames={actionIconClasses}
onClick={() => handleOpen(cardTheme)}
mb={2}
>
<FiEdit color='green' />
</ActionIcon>
<ActionIcon
className={actionIconClasses.themeEditButton}
classNames={actionIconClasses}
onClick={() => handleDeleteTheme(cardTheme)}
>
<FiTrash color='red' />
</ActionIcon>
</Stack>
)}
</Group>
);
}
export default ThemeCard;

View File

@@ -0,0 +1,37 @@
import { Group, Text } from '@mantine/core';
function ThemeMiniCard({ allThemes, item }: any) {
const theme = allThemes.find((theme: any) => theme.id === item);
return (
<Group
gap={0}
align='center'
justify='center'
display={'grid'}
pos={'relative'}
w={'175px'}
h={'60px'}
style={{
background: `linear-gradient(${theme.backgroundColor}33, ${theme.backgroundColor}33), no-repeat center url(/backgrounds/${theme.backgroundImage})`,
backgroundSize: 'cover',
backgroundColor: theme.backgroundColor,
border: `solid ${theme.borderColor}`,
borderRadius: '8px',
}}
>
<Text
bg={theme.backgroundColor}
c={theme.primaryColor}
size='md'
style={{
padding: 4,
borderRadius: '8px',
}}
>
{allThemes.find((theme: any) => theme.id === item)?.name}
</Text>
</Group>
);
}
export default ThemeMiniCard;

View File

@@ -0,0 +1,407 @@
import {
Button,
ColorPicker,
ColorSwatch,
Divider,
Group,
Modal,
ScrollArea,
Stack,
Text,
TextInput,
UnstyledButton,
} from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useEffect, useRef, useState } from 'react';
import { FiSave, FiX } from 'react-icons/fi';
import { modalClasses, textClasses, textInputClasses } from 'styles';
import ThemeModalContent from './ThemeModalContent';
function ThemeModal({
opened,
handleClose,
selectedTheme,
setSelectedTheme,
setColorId,
colorId,
setColorValue,
openPicker,
backgroundList,
pickerOpen,
colorValue,
closePicker,
handleSaveTheme,
}: any) {
const { theme } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
const filtered = backgroundList.filter((item: any) =>
item.toLowerCase().includes(query.toLowerCase()),
);
const re = /(?:\.([^.]+))?$/;
const items = filtered.map((item: any, index: any) => (
<UnstyledButton
data-list-item
key={item}
style={{
width: '100%',
height: '75px',
background:
item !== 'none'
? index === hovered
? `linear-gradient(${selectedTheme.backgroundColor}88, ${selectedTheme.backgroundColor}88), no-repeat center url(/backgrounds/${item})`
: `no-repeat center url(/backgrounds/${item})`
: undefined,
backgroundSize: item !== 'none' ? 'cover' : undefined,
backgroundColor: item === 'none' ? '#00000088' : undefined,
paddingLeft: 8,
alignItems: 'center',
}}
onClick={() =>
setSelectedTheme({
...selectedTheme,
backgroundImage: item,
})
}
>
<Group h={'100%'} w={'100%'} justify='space-between'>
<Text
style={{
width: 'min-content',
height: 'min-content',
backdropFilter: 'blur(10px) brightness(.25)',
padding: 8,
borderRadius: 4,
}}
>
{item.replace(/\.[^/.]+$/, '')}
</Text>
{item !== 'None' && (
<Text
style={{
width: 'min-content',
backdropFilter: 'blur(10px) brightness(.25)',
padding: 8,
marginTop: 34,
borderTopLeftRadius: 4,
color: theme.accentColor,
}}
>
{re.exec(item.toUpperCase())?.[1]}
</Text>
)}
</Group>
</UnstyledButton>
));
useEffect(() => {
if (selectedTheme) {
if (selectedTheme?.backgroundImage === 'none') {
setQuery('');
} else {
setQuery(selectedTheme?.backgroundImage || '');
}
}
}, [selectedTheme]);
return (
<Modal.Root
size={'50%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={handleClose}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url("${theme.backgroundImage}")`,
}}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text>Name:</Text>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
value={selectedTheme.name}
onChange={(e) =>
setSelectedTheme({ ...selectedTheme, name: e.target.value })
}
styles={{
label: {
color: theme.primaryColor,
},
input: {
color: theme.primaryColor,
border: 'none',
borderBottom: `1px solid ${theme.primaryColor}`,
},
}}
/>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body p={'2rem'}>
<ThemeModalContent selectedTheme={selectedTheme} />
<Divider color='black' my='sm' />
<Group w={'100%'} h={'100%'} gap={0}>
<Stack w={'50%'}>
{pickerOpen ? (
<Group justify='center'>
<ColorPicker
value={colorValue}
onChange={setColorValue}
format='hexa'
w={'90%'}
/>
<Group justify='center'>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
w={'55%'}
value={colorValue}
onChange={(e) => setColorValue(e.target.value)}
/>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={() => {
switch (colorId) {
case 'primaryColor':
setSelectedTheme({
...selectedTheme,
primaryColor: colorValue,
});
break;
case 'accentColor':
setSelectedTheme({
...selectedTheme,
accentColor: colorValue,
});
break;
case 'background':
setSelectedTheme({
...selectedTheme,
backgroundColor: colorValue,
});
break;
case 'borderColor':
setSelectedTheme({
...selectedTheme,
borderColor: colorValue,
});
break;
case 'searchBackground':
setSelectedTheme({
...selectedTheme,
searchColor: colorValue,
});
break;
}
closePicker();
}}
>
<FiSave />
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={closePicker}
>
<FiX />
</Button>
<Group w={'100%'} align='center' justify='center' />
</Group>
</Group>
) : (
<Stack h={256} justify='center'>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Primary Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('primaryColor');
openPicker();
setColorValue(selectedTheme.primaryColor);
}}
radius={'sm'}
w={'100px'}
color={selectedTheme.primaryColor}
style={{
cursor: 'pointer',
border: `2px dashed ${theme.primaryColor}`,
}}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Accent Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('accentColor');
openPicker();
setColorValue(selectedTheme.accentColor);
}}
radius={'sm'}
w={'100px'}
color={selectedTheme.accentColor}
style={{
cursor: 'pointer',
border: `2px dashed ${theme.primaryColor}`,
}}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Background Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('background');
openPicker();
setColorValue(selectedTheme.backgroundColor);
}}
radius={'sm'}
w={'100px'}
color={selectedTheme.backgroundColor}
style={{
cursor: 'pointer',
border: `2px dashed ${theme.primaryColor}`,
}}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Border Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('borderColor');
openPicker();
setColorValue(selectedTheme.borderColor);
}}
radius={'sm'}
w={'100px'}
color={selectedTheme.borderColor}
style={{
cursor: 'pointer',
border: `2px dashed ${theme.primaryColor}`,
}}
/>
</Group>
<Group justify='space-between'>
<Text className={textClasses.label} classNames={textClasses}>
Search Background Color
</Text>
<ColorSwatch
onClick={() => {
setColorId('searchBackground');
openPicker();
setColorValue(selectedTheme.searchColor);
}}
radius={'sm'}
w={'100px'}
color={selectedTheme.searchColor}
style={{
cursor: 'pointer',
border: `2px dashed ${theme.primaryColor}`,
}}
/>
</Group>
</Stack>
)}
</Stack>
<Stack h={'100%'} w={'50%'} align='center' justify='center'>
<Group gap={0}>
<Text mr={8}>Search:</Text>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
value={query.replace(/\.[^/.]+$/, '')}
rightSection={
<FiX
style={{
cursor: 'pointer',
margin: '4px',
}}
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}
style={{
height: 150,
width: '90%',
borderRadius: 8,
}}
>
<Stack gap={0} align='center' display={'block'}>
{items}
</Stack>
</ScrollArea.Autosize>
</Stack>
</Group>
<Divider color='black' my='sm' />
<Group pos={'absolute'} bottom={8}>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={handleSaveTheme}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={handleClose}
>
Cancel
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
export default ThemeModal;

View File

@@ -0,0 +1,259 @@
import { Icon } from '@iconify/react/dist/iconify.js';
import { Box, Group, Stack, Text } from '@mantine/core';
import { useState } from 'react';
import { stackClasses } from 'styles';
export default function ThemeModalContent({ selectedTheme }: any) {
const ExampleApp = (previewTheme: any) => {
const [textDecoration, setTextDecoration] = useState<string>('none');
return (
<Box
component='a'
onMouseOver={() => setTextDecoration('underline')}
onMouseOut={() => setTextDecoration('none')}
className={stackClasses.appItemBox}
>
<Stack
style={{
height: '64px',
marginRight: '1em',
marginLeft: '1em',
paddingTop: '15px',
}}
>
<Icon
style={{ color: previewTheme.theme.primaryColor }}
icon={'mdi:check'}
height={32}
width={32}
/>
</Stack>
<Stack
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
flex: 1,
overflow: 'hidden',
gap: 0,
}}
>
<Text
style={{
fontWeight: 400,
fontSize: 11,
fontStyle: 'normal',
color: previewTheme.theme.primaryColor,
fontFamily:
'-apple-system, Jost, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif !important',
margin: 0,
}}
>
{'Example App'}
</Text>
<Text
style={{
fontWeight: 400,
fontSize: 11,
fontStyle: 'italic',
color: previewTheme.theme.accentColor,
fontFamily:
'-apple-system, Jost, BlinkMacSystemFont, Helvetica Neue, Roboto, sans-serif !important',
margin: 0,
textDecoration: textDecoration,
}}
>
{'https://www.example.com'}
</Text>
</Stack>
</Box>
);
};
return (
<Stack
style={{
width: '100%',
background:
selectedTheme.backgroundImage !== 'none'
? `linear-gradient(${selectedTheme.backgroundColor}33, ${selectedTheme.backgroundColor}33), no-repeat center url(/backgrounds/${selectedTheme.backgroundImage})`
: undefined,
backgroundSize: selectedTheme.backgroundImage !== 'none' ? 'cover' : undefined,
backgroundColor:
selectedTheme.backgroundImage === 'none'
? selectedTheme.backgroundColor
: undefined,
borderRadius: '10px',
border: '2px solid',
}}
>
<Group
justify='center'
style={{
color: selectedTheme.primaryColor,
paddingTop: '1rem',
}}
>
{selectedTheme.name}
</Group>
<Text
pl={'1.5rem'}
style={{
textDecoration: 'underline',
fontSize: '20px',
fontWeight: 200,
fontStyle: 'italic',
height: '10px',
textTransform: 'uppercase',
marginTop: '20px',
marginBottom: '20px',
color: selectedTheme.primaryColor,
}}
>
Applications
</Text>
<Stack
style={{
gap: '.25rem',
backgroundSize: 'cover',
borderBottom: '0px solid',
display: 'grid',
gridColumnGap: '5px',
gridRowGap: '5px',
gridTemplateColumns: '1fr 1fr 1fr',
gridTemplateRows: '64px',
padding: '1rem',
paddingTop: '0rem',
justifyContent: 'center',
alignContent: 'center',
}}
>
<ExampleApp theme={selectedTheme} />
<ExampleApp theme={selectedTheme} />
<ExampleApp theme={selectedTheme} />
</Stack>
<Text
pl={'1.5rem'}
style={{
textDecoration: 'underline',
fontSize: '20px',
fontWeight: 200,
fontStyle: 'italic',
height: '10px',
textTransform: 'uppercase',
marginTop: '20px',
marginBottom: '20px',
color: selectedTheme.primaryColor,
}}
>
Bookmarks
</Text>
<Stack
style={{
display: 'flex',
flexDirection: 'row',
flexWrap: 'nowrap',
gridColumnGap: '20px',
gridRowGap: '0px',
gridTemplateColumns: '1fr 1fr 1fr 1fr 1fr',
gridTemplateRows: 'auto',
}}
>
<Stack
style={{
width: '100%',
marginBottom: '0em',
display: 'block',
lineHeight: '1.5rem',
padding: '1rem',
paddingTop: '0rem',
}}
>
<Text
style={{
fontSize: '14px',
fontWeight: 400,
height: '10px',
width: 'auto',
color: selectedTheme.accentColor,
textTransform: 'uppercase',
marginTop: '20px',
marginBottom: '20px',
}}
>
Example Category
</Text>
<Text
style={{
display: 'block',
fontSize: '14px',
lineHeight: 2,
color: selectedTheme.primaryColor,
}}
component='a'
>
{'Link 1'}
</Text>
<Text
style={{
display: 'block',
fontSize: '14px',
lineHeight: 2,
color: selectedTheme.primaryColor,
}}
component='a'
>
{'Link 2'}
</Text>
</Stack>
<Stack
style={{
width: '100%',
marginBottom: '0em',
display: 'block',
lineHeight: '1.5rem',
padding: '1rem',
paddingTop: '0rem',
}}
>
<Text
style={{
fontSize: '14px',
fontWeight: 400,
height: '10px',
width: 'auto',
color: selectedTheme.accentColor,
textTransform: 'uppercase',
marginTop: '20px',
marginBottom: '20px',
}}
>
Example Category
</Text>
<Text
style={{
display: 'block',
fontSize: '14px',
lineHeight: 2,
color: selectedTheme.primaryColor,
}}
component='a'
>
{'Link 1'}
</Text>
<Text
style={{
display: 'block',
fontSize: '14px',
lineHeight: 2,
color: selectedTheme.primaryColor,
}}
component='a'
>
{'Link 2'}
</Text>
</Stack>
</Stack>
</Stack>
);
}

View File

@@ -0,0 +1,223 @@
import {
ActionIcon,
Button,
Group,
Overlay,
ScrollArea,
Stack,
Text,
} from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { FiEdit, FiRefreshCcw, FiTrash } from 'react-icons/fi';
import ThemeMiniCard from './ThemeMiniCard';
function ThemePlaylistCard({
cardPlaylist,
handleOpen,
handleDeletePlaylist,
createPlaylist,
allThemes,
}: any) {
const [hover, setHover] = useState(false);
const { setThemeName, setCurrentThemePlaylist, theme } = useCommonHooks();
const syncableLists = ['All', 'Backgrounds', 'No Backgrounds'];
const themeList = JSON.parse(
cardPlaylist.themeList.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
);
return (
<Group
gap={0}
align='center'
justify='center'
display={'grid'}
pos={'relative'}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
>
<Text
style={{
position: 'absolute',
top: -10,
left: 6,
paddingRight: 4,
paddingLeft: 4,
backgroundColor: theme.borderColor,
borderRadius: '4px',
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
zIndex: 3,
}}
>
{cardPlaylist.name}
</Text>
{syncableLists.includes(cardPlaylist.name) ? (
<ActionIcon
style={{
position: 'absolute',
top: -10,
right: 6,
paddingRight: 4,
paddingLeft: 4,
backgroundColor: theme.borderColor,
borderRadius: '4px',
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
zIndex: 3,
}}
onClick={(event) => {
event.stopPropagation();
if (cardPlaylist.name === 'All') {
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: cardPlaylist.id,
name: 'All',
themeList: JSON.stringify(allThemes.map((theme: any) => theme.id)),
},
});
}
if (cardPlaylist.name === 'Backgrounds') {
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: cardPlaylist.id,
name: 'Backgrounds',
themeList: JSON.stringify(
allThemes
.filter(
(theme: any) => theme.backgroundImage.toLowerCase() !== 'none',
)
.map((theme: any) => theme.id),
),
},
});
}
if (cardPlaylist.name === 'No Backgrounds') {
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: cardPlaylist.id,
name: 'No Backgrounds',
themeList: JSON.stringify(
allThemes
.filter(
(theme: any) => theme.backgroundImage.toLowerCase() === 'none',
)
.map((theme: any) => theme.id),
),
},
});
}
}}
>
<FiRefreshCcw />
</ActionIcon>
) : (
<>
<ActionIcon
style={{
position: 'absolute',
top: -10,
right: 6,
paddingRight: 4,
paddingLeft: 4,
backgroundColor: theme.borderColor,
borderRadius: '4px',
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
zIndex: 3,
}}
onClick={() => {
handleOpen(cardPlaylist);
}}
>
<FiEdit />
</ActionIcon>
{cardPlaylist.default === 0 && (
<ActionIcon
style={{
position: 'absolute',
top: -10,
right: 40,
paddingRight: 4,
paddingLeft: 4,
backgroundColor: theme.borderColor,
borderRadius: '4px',
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
zIndex: 3,
}}
onClick={() => handleDeletePlaylist(cardPlaylist)}
>
<FiTrash />
</ActionIcon>
)}
</>
)}
<Button
w={'275px'}
h={'310px'}
style={{
backgroundSize: 'cover',
backgroundColor: theme.searchColor,
color: theme.accentColor,
border: `solid ${theme.borderColor}`,
}}
onClick={() => {
setCurrentThemePlaylist(cardPlaylist.name);
setThemeName('random');
}}
>
{hover && <Overlay opacity={0.5} zIndex={1} />}
<Stack h={'100%'} w={'100%'}>
{cardPlaylist.name !== 'Time of Day' ? (
<>
<Text mt={8} mb={-8}>
Count: {themeList.length}
</Text>
<ScrollArea h={'255px'} w={'100%'} style={{ zIndex: 2 }}>
<Stack gap={4}>
{themeList.length > 0 &&
themeList.map((item: any) => {
return (
<ThemeMiniCard key={item} item={item} allThemes={allThemes} />
);
})}
</Stack>
</ScrollArea>
</>
) : (
<>
<Text mt={8} mb={-8}>
Timed Themes
</Text>
<ScrollArea h={'255px'} w={'100%'} style={{ zIndex: 2 }}>
<Stack gap={4}>
<ThemeMiniCard item={Number(themeList[0])} allThemes={allThemes} />
<ThemeMiniCard item={Number(themeList[1])} allThemes={allThemes} />
<ThemeMiniCard item={Number(themeList[2])} allThemes={allThemes} />
<ThemeMiniCard item={Number(themeList[3])} allThemes={allThemes} />
</Stack>
</ScrollArea>
</>
)}
</Stack>
</Button>
</Group>
);
}
export default ThemePlaylistCard;

View File

@@ -0,0 +1,275 @@
import {
Button,
Divider,
Group,
Modal,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
} from '@mantine/core';
import { useCommonHooks } from 'hooks';
import { useEffect, useState } from 'react';
import { modalClasses, selectClasses, textInputClasses } from 'styles';
import ThemeMiniCard from './ThemeMiniCard';
function ThemePlaylistModal({
opened,
handleClose,
createPlaylist,
selectedPlaylist,
setSelectedPlaylist,
handleSavePlaylist,
allThemes,
}: any) {
const { theme } = useCommonHooks();
const [themeList, setThemeList] = useState<any>('[]');
useEffect(() => {
if (selectedPlaylist.themeList) {
setThemeList(JSON.parse(selectedPlaylist?.themeList));
}
}, [selectedPlaylist]);
return (
<Modal.Root
size={'50%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={opened}
onClose={() => {
handleClose();
setThemeList('[]');
setSelectedPlaylist({});
}}
style={{
'--color-text-pri': theme.primaryColor,
'--color-text-acc': theme.accentColor,
'--color-background': theme.backgroundColor,
'--color-border': theme.borderColor,
'--color-search-background': theme.searchColor,
'--image-background': `url("${theme.backgroundImage}")`,
}}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text>Name:</Text>
<TextInput
className={textInputClasses.default}
classNames={textInputClasses}
value={selectedPlaylist.name}
disabled={selectedPlaylist.name === 'Time of Day'}
onChange={(e) =>
setSelectedPlaylist({ ...selectedPlaylist, name: e.target.value })
}
styles={{
label: {
color: theme.primaryColor,
},
input: {
color: theme.primaryColor,
border: 'none',
borderBottom: `1px solid ${theme.primaryColor}`,
},
}}
/>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body pt={8}>
{selectedPlaylist.name === 'Time of Day' ? (
<Stack>
<Group>
<Text w={'100px'}>00:00-05:59</Text>
<Select
labelProps={{ display: 'none' }}
placeholder='Select Theme'
defaultValue={themeList['0']}
data={allThemes?.map((theme: any) => {
return {
value: `${theme.id}`,
label: theme.name,
};
})}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
onChange={(data) => {
const newThemeList = themeList;
newThemeList['0'] = data;
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: selectedPlaylist.id,
name: selectedPlaylist.name,
themeList: JSON.stringify(newThemeList),
},
});
}}
/>
</Group>
<Group w={'100%'}>
<Text w={'100px'}>06:00-11:59</Text>
<Select
labelProps={{ display: 'none' }}
placeholder='Select Theme'
defaultValue={themeList['1']}
data={allThemes?.map((theme: any) => {
return {
value: `${theme.id}`,
label: theme.name,
};
})}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
onChange={(data) => {
const newThemeList = themeList;
newThemeList['1'] = data;
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: selectedPlaylist.id,
name: selectedPlaylist.name,
themeList: JSON.stringify(newThemeList),
},
});
}}
/>
</Group>
<Group w={'100%'}>
<Text w={'100px'}>12:00-17:59</Text>
<Select
labelProps={{ display: 'none' }}
placeholder='Select Theme'
defaultValue={themeList['2']}
data={allThemes?.map((theme: any) => {
return {
value: `${theme.id}`,
label: theme.name,
};
})}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
onChange={(data) => {
const newThemeList = themeList;
newThemeList['2'] = data;
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: selectedPlaylist.id,
name: selectedPlaylist.name,
themeList: JSON.stringify(newThemeList),
},
});
}}
/>
</Group>
<Group w={'100%'}>
<Text w={'100px'}>18:00-23:59</Text>
<Select
labelProps={{ display: 'none' }}
placeholder='Select Theme'
defaultValue={themeList['3']}
data={allThemes?.map((theme: any) => {
return {
value: `${theme.id}`,
label: theme.name,
};
})}
classNames={{
input: selectClasses.menuSelectInput,
label: selectClasses.menuSelectLabel,
options: selectClasses.menuSelectOptions,
option: selectClasses.menuSelectOption,
}}
onChange={(data) => {
const newThemeList = themeList;
newThemeList['3'] = data;
createPlaylist({
method: 'PATCH',
endpoint: 'api/themePlaylists',
body: {
id: selectedPlaylist.id,
name: selectedPlaylist.name,
themeList: JSON.stringify(newThemeList),
},
});
}}
/>
</Group>
</Stack>
) : (
<SimpleGrid cols={3}>
{selectedPlaylist.name !== 'Time of Day' &&
allThemes?.map((miniTheme: any) => {
const newThemeList = themeList;
return (
<Group
key={miniTheme.id}
style={{
cursor: 'pointer',
filter:
newThemeList && !newThemeList?.includes(miniTheme.id)
? 'saturate(0)'
: undefined,
}}
onClick={() => {
const newSelectedPlaylist = { ...selectedPlaylist };
if (themeList?.includes(miniTheme.id)) {
newThemeList.splice(themeList.indexOf(miniTheme.id), 1);
} else {
newThemeList.push(miniTheme.id);
}
newSelectedPlaylist.themeList = JSON.stringify(newThemeList);
setSelectedPlaylist(newSelectedPlaylist);
}}
>
<ThemeMiniCard allThemes={allThemes} item={miniTheme.id} />
</Group>
);
})}
</SimpleGrid>
)}
<Divider color='black' my='sm' />
<Group>
<Button
color={'green'}
styles={{ label: { color: 'black' } }}
onClick={handleSavePlaylist}
>
Save
</Button>
<Button
color={'red'}
styles={{ label: { color: 'black' } }}
onClick={() => {
handleClose();
setThemeList('[]');
setSelectedPlaylist({});
}}
>
Cancel
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
export default ThemePlaylistModal;

View File

@@ -0,0 +1,184 @@
import { Box, Button, Group, Stack, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { modals } from '@mantine/modals';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useState } from 'react';
import { FiChevronRight, FiPlus } from 'react-icons/fi';
import { buttonClasses, textClasses } from 'styles';
import ThemePlaylistCard from './ThemePlaylistCard';
import ThemePlaylistModal from './ThemePlaylistModal';
function ThemePlaylists() {
const { data: allThemes } = useQueryAPI({
endpoint: 'api/themes',
});
const { data: themePlaylists } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const { mutateAsync: createPlaylist } = useMutationAPI();
const { mutateAsync: deletePlaylist } = useMutationAPI();
const [opened, { open, close }] = useDisclosure(false);
const [selectedPlaylist, setSelectedPlaylist] = useState<any>({});
const { currentThemePlaylist, setCurrentThemePlaylist, theme } = useCommonHooks();
const handleOpen = (playlist: any) => {
setSelectedPlaylist(playlist);
open();
};
const handleClose = () => {
close();
};
const handleSavePlaylist = () => {
createPlaylist({
method: selectedPlaylist.id === undefined ? 'POST' : 'PATCH',
endpoint: 'api/themePlaylists',
body: selectedPlaylist,
});
close();
};
const openDeleteConfirmModal = (playlist: any) =>
modals.openConfirmModal({
title: 'Are you sure you want to delete this theme playlist?',
labels: { confirm: 'Delete', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () =>
deletePlaylist({
method: 'DELETE',
endpoint: 'api/themePlaylists',
body: playlist,
}),
});
const handleDeletePlaylist = (playlist: any) => {
openDeleteConfirmModal(playlist);
close();
};
return (
<>
<ThemePlaylistModal
opened={opened}
handleClose={handleClose}
selectedPlaylist={selectedPlaylist}
setSelectedPlaylist={setSelectedPlaylist}
handleSavePlaylist={handleSavePlaylist}
allThemes={allThemes}
createPlaylist={createPlaylist}
/>
<Stack h={'100%'} w={'100%'}>
<Group w={'100%'} justify='space-between'>
<Group>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Theme Playlists
</Text>
<Button
mb={'-1.5em'}
className={buttonClasses.tableButton}
classNames={buttonClasses}
onClick={() => {
setSelectedPlaylist({
name: 'New Playlist',
themeList: '[]',
});
open();
}}
>
<FiPlus />
</Button>
</Group>
</Group>
<Group justify='center' align='center' pos={'relative'}>
<Text p={0} className={textClasses.settingsSubTitle} classNames={textClasses}>
Defaults
</Text>
</Group>
<Group justify='center' align='center' pos={'relative'}>
{themePlaylists
?.filter((playlist: any) => playlist.default === 1)
.map((cardPlaylist: any) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: theme.borderColor,
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
currentThemePlaylist={currentThemePlaylist}
setCurrentThemePlaylist={setCurrentThemePlaylist}
handleDeletePlaylist={handleDeletePlaylist}
allThemes={allThemes}
createPlaylist={createPlaylist}
/>
</Box>
</Group>
);
})}
</Group>
<Group justify='center' align='center' pos={'relative'}>
<Text p={0} className={textClasses.settingsSubTitle} classNames={textClasses}>
User Made
</Text>
</Group>
<Group justify='center' align='center' pos={'relative'}>
{themePlaylists
?.filter((playlist: any) => playlist.default === 0)
.map((cardPlaylist: any) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
{cardPlaylist.name === currentThemePlaylist && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 10,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: theme.borderColor,
color:
theme.borderColor === theme.primaryColor
? theme.accentColor
: theme.primaryColor,
}}
/>
)}
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
currentThemePlaylist={currentThemePlaylist}
setCurrentThemePlaylist={setCurrentThemePlaylist}
handleDeletePlaylist={handleDeletePlaylist}
allThemes={allThemes}
createPlaylist={createPlaylist}
/>
</Box>
</Group>
);
})}
</Group>
</Stack>
</>
);
}
export default ThemePlaylists;

View File

@@ -0,0 +1,176 @@
import { Box, Button, Group, Overlay, Stack, Switch, Text } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react';
import { FiChevronRight, FiPlus } from 'react-icons/fi';
import '@mantine/core/styles/ModalBase.css';
import '@mantine/core/styles/Modal.css';
import { modals } from '@mantine/modals';
import { buttonClasses, textClasses } from 'styles';
import ThemeCard from './ThemeCard';
import ThemeModal from './ThemeModal';
function ThemesSettings() {
const { data: allThemes } = useQueryAPI({
endpoint: 'api/themes',
});
const { data: backgroundData, isFetched: isBackgroundFetched } = useQueryAPI({
endpoint: 'api/backgrounds',
});
const { mutateAsync: createTheme } = useMutationAPI();
const { mutateAsync: deleteTheme } = useMutationAPI();
const [opened, { open, close }] = useDisclosure(false);
const [colorId, setColorId] = useState<string>('');
const [colorValue, setColorValue] = useState('#000000');
const [backgroundList, setBackgroundList] = useState<any>(['None']);
const [selectedTheme, setSelectedTheme] = useState<any>({});
const [pickerOpen, { open: openPicker, close: closePicker }] = useDisclosure(false);
const { currentTheme, setCurrentTheme, theme, setCurrentThemePlaylist } =
useCommonHooks();
const handleOpen = (theme: any) => {
setSelectedTheme(theme);
open();
};
const handleClose = () => {
close();
closePicker();
};
const handleSaveTheme = () => {
createTheme({
method: selectedTheme.id === undefined ? 'POST' : 'PATCH',
endpoint: 'api/themes',
body: selectedTheme,
});
close();
};
const openDeleteConfirmModal = (theme: any) =>
modals.openConfirmModal({
title: 'Are you sure you want to delete this theme?',
labels: { confirm: 'Delete', cancel: 'Cancel' },
confirmProps: { color: 'red' },
onConfirm: () =>
deleteTheme({ method: 'DELETE', endpoint: 'api/themes', body: theme }),
});
const handleDeleteTheme = (theme: any) => {
openDeleteConfirmModal(theme);
close();
};
useEffect(() => {
if (backgroundData && isBackgroundFetched) {
setBackgroundList(['None', ...backgroundData]);
}
}, [backgroundData, isBackgroundFetched]);
return (
<>
<ThemeModal
opened={opened}
handleClose={handleClose}
selectedTheme={selectedTheme}
setSelectedTheme={setSelectedTheme}
theme={theme}
setColorId={setColorId}
colorId={colorId}
setColorValue={setColorValue}
openPicker={openPicker}
backgroundList={backgroundList}
pickerOpen={pickerOpen}
colorValue={colorValue}
closePicker={closePicker}
handleSaveTheme={handleSaveTheme}
/>
<Stack h={'100%'} w={'100%'}>
<Group w={'100%'} justify='space-between'>
<Group>
<Text className={textClasses.settingsSubTitle} classNames={textClasses}>
Themes
</Text>
<Button
mb={'-1.5em'}
className={buttonClasses.tableButton}
classNames={buttonClasses}
onClick={() => {
setSelectedTheme({
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
});
open();
}}
>
<FiPlus />
</Button>
</Group>
<Switch
label={<Text c={theme.primaryColor}>Random Theme</Text>}
labelPosition='left'
checked={currentTheme === 'random'}
onChange={(e) => {
setCurrentTheme(e.target.checked ? 'random' : theme.name);
setCurrentThemePlaylist('All');
}}
color={theme.primaryColor}
/>
</Group>
<Group justify='center' align='center' pos={'relative'}>
<Overlay
display={currentTheme === 'random' ? 'flex' : 'none'}
blur={5}
style={{
borderRadius: '10px',
marginTop: '-15px',
marginBottom: '-10px',
boxShadow: '0px 0px 10px 3px rgba(0, 0, 0, .8)',
}}
pos={'absolute'}
/>
{allThemes?.map((cardTheme: any) => (
<Group key={cardTheme.name} pos={'relative'}>
{cardTheme.name === currentTheme && (
<FiChevronRight
style={{
position: 'absolute',
left: -14,
zIndex: 1,
fontSize: '2em',
fontWeight: 'bold',
borderRadius: '50%',
backgroundColor: cardTheme.borderColor,
color:
cardTheme.borderColor === cardTheme.primaryColor
? cardTheme.accentColor
: cardTheme.primaryColor,
}}
/>
)}
<Box
style={{
zIndex:
currentTheme === 'random' && cardTheme.name === theme.name ? 200 : 0,
}}
>
<ThemeCard
allThemes={allThemes}
cardTheme={cardTheme}
handleOpen={handleOpen}
handleDeleteTheme={handleDeleteTheme}
createTheme={createTheme}
/>
</Box>
</Group>
))}
</Group>
</Stack>
</>
);
}
export default ThemesSettings;