Added Theme Playlists

This commit is contained in:
Sean Edmunds
2025-03-13 17:01:20 -05:00
parent 0ba63f57c4
commit 7edbbb6a76
7 changed files with 676 additions and 6 deletions

View File

@@ -27,7 +27,11 @@ function App() {
const { data: allThemes, isFetched: isAllThemesFetched } = useQueryAPI({ const { data: allThemes, isFetched: isAllThemesFetched } = useQueryAPI({
endpoint: 'api/themes', endpoint: 'api/themes',
}); });
const { theme, setTheme, themeName, handleThemeChange } = useCommonHooks(); const { data: themePlaylists, isFetched: isThemePlaylistsFetched } = useQueryAPI({
endpoint: 'api/themePlaylists',
});
const { theme, setTheme, themeName, handleThemeChange, currentThemePlaylist } =
useCommonHooks();
const overrides = createTheme({ const overrides = createTheme({
white: theme?.backgroundColor, white: theme?.backgroundColor,
fontFamily: '-apple-system,Jost,BlinkMacSystemFont,Helvetica Neue,Roboto,sans-serif', fontFamily: '-apple-system,Jost,BlinkMacSystemFont,Helvetica Neue,Roboto,sans-serif',
@@ -54,14 +58,32 @@ function App() {
useEffect(() => { useEffect(() => {
if (allThemes && themeName) { if (allThemes && themeName) {
if (currentThemePlaylist === 'All') {
if (themeName !== 'random') { if (themeName !== 'random') {
const temp = allThemes.find((t: any) => t.name === themeName); const temp = allThemes.find((t: any) => t.name === themeName);
setTheme(temp); setTheme(temp);
} else { } else {
handleThemeChange(allThemes, false); handleThemeChange(allThemes, false);
} }
} else if (currentThemePlaylist === 'Time of Day') {
const themeList = JSON.parse(
themePlaylists.find((t: any) => t.name === currentThemePlaylist).themeList,
);
setTheme(
allThemes.find(
(t: any) => t.id === Number(themeList[Math.floor(new Date().getHours() / 6)]),
),
);
} else {
const themeList = JSON.parse(
themePlaylists.find((t: any) => t.name === currentThemePlaylist).themeList,
);
const filteredThemes = allThemes.filter((t: any) => themeList.includes(t.id));
setTheme('random');
handleThemeChange(filteredThemes, false);
} }
}, [isAllThemesFetched, themeName]); }
}, [isAllThemesFetched, themeName, currentThemePlaylist, isThemePlaylistsFetched]);
return ( return (
<> <>

View File

@@ -22,6 +22,12 @@ export function useCommonHooks() {
key: 'theme', key: 'theme',
defaultValue: 'blackboard', defaultValue: 'blackboard',
}); });
const [currentThemePlaylist, setCurrentThemePlaylist] = useLocalStorage<any>({
key: 'themePlaylist',
defaultValue: 'All',
});
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [providerLabel, setProviderLabel] = useState('Google Search'); const [providerLabel, setProviderLabel] = useState('Google Search');
const [providerURL, setProviderURL] = useState('https://www.google.com/search?q='); const [providerURL, setProviderURL] = useState('https://www.google.com/search?q=');
@@ -52,6 +58,10 @@ export function useCommonHooks() {
if (enabled.length === 0) { if (enabled.length === 0) {
setTheme(themes[0]); setTheme(themes[0]);
save && setThemeName(themes[0].name); save && setThemeName(themes[0].name);
} else if (currentThemePlaylist !== 'All') {
const index = Math.floor(Math.random() * themes.length);
setTheme(themes[index]);
save && setThemeName(themes[index].name);
} else { } else {
const index = Math.floor(Math.random() * enabled.length); const index = Math.floor(Math.random() * enabled.length);
setTheme(enabled[index]); setTheme(enabled[index]);
@@ -72,6 +82,8 @@ export function useCommonHooks() {
setBookmarkFilter, setBookmarkFilter,
currentTheme, currentTheme,
setCurrentTheme, setCurrentTheme,
currentThemePlaylist,
setCurrentThemePlaylist,
searchValue, searchValue,
setSearchValue, setSearchValue,
providerLabel, providerLabel,

View File

@@ -19,6 +19,7 @@ import Categories from './components/Categories';
import Messages from './components/Messages'; import Messages from './components/Messages';
import Personalization from './components/Personalization'; import Personalization from './components/Personalization';
import SearchProviders from './components/SearchProviders'; import SearchProviders from './components/SearchProviders';
import ThemePlaylists from './components/ThemePlaylists';
import Themes from './components/Themes'; import Themes from './components/Themes';
import WolClients from './components/WolClients'; import WolClients from './components/WolClients';
@@ -66,6 +67,9 @@ export function Settings() {
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='themes'> <Tabs.Tab px={isMobile ? '.9rem' : undefined} value='themes'>
{isMobile ? <FiEdit /> : 'Themes'} {isMobile ? <FiEdit /> : 'Themes'}
</Tabs.Tab> </Tabs.Tab>
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='themePlaylists'>
{isMobile ? <FiEdit /> : 'Theme Playlists'}
</Tabs.Tab>
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='providers'> <Tabs.Tab px={isMobile ? '.9rem' : undefined} value='providers'>
{isMobile ? <FiSearch /> : 'Search Providers'} {isMobile ? <FiSearch /> : 'Search Providers'}
</Tabs.Tab> </Tabs.Tab>
@@ -91,6 +95,9 @@ export function Settings() {
<Tabs.Panel value='themes'> <Tabs.Panel value='themes'>
<Themes /> <Themes />
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value='themePlaylists'>
<ThemePlaylists />
</Tabs.Panel>
<Tabs.Panel value='providers'> <Tabs.Panel value='providers'>
<SearchProviders /> <SearchProviders />
</Tabs.Panel> </Tabs.Panel>

View File

@@ -0,0 +1,36 @@
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={'200px'}
h={'75px'}
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}`,
}}
>
<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,208 @@
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 { currentThemePlaylist, 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 !== '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 === '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={'300px'}
style={{
backgroundSize: 'cover',
backgroundColor: theme.primaryColor,
color: theme.accentColor,
border: `solid ${theme.borderColor}`,
}}
onClick={() => {
setCurrentThemePlaylist(cardPlaylist.name);
}}
>
{hover && <Overlay opacity={0.5} zIndex={1} />}
<Stack h={'100%'} w={'100%'}>
{cardPlaylist.name !== 'Time of Day' ? (
<>
<Text>Count: {themeList.length}</Text>
<ScrollArea h={'240px'} w={'100%'} style={{ zIndex: 2 }}>
{themeList.length > 0 &&
themeList.map((item: any) => {
return <ThemeMiniCard key={item} item={item} allThemes={allThemes} />;
})}
</ScrollArea>
</>
) : (
<>
<Text>Timed Themes</Text>
<ScrollArea h={'240px'} w={'100%'} style={{ zIndex: 2 }}>
<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} />
</ScrollArea>
</>
)}
</Stack>
</Button>
</Group>
);
}
export default ThemePlaylistCard;

View File

@@ -0,0 +1,268 @@
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('[]');
}}
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}
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>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>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>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>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={() => {
if (themeList?.includes(miniTheme.id)) {
newThemeList.splice(themeList.indexOf(miniTheme.id), 1);
} else {
newThemeList.push(miniTheme.id);
}
selectedPlaylist.themeList = JSON.stringify(newThemeList);
}}
>
<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}
>
Cancel
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
export default ThemePlaylistModal;

View File

@@ -0,0 +1,117 @@
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 { 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 } = 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'}>
{themePlaylists?.map((cardPlaylist: any) => {
return (
<Group key={cardPlaylist.name} pos={'relative'}>
<Box>
<ThemePlaylistCard
cardPlaylist={cardPlaylist}
handleOpen={handleOpen}
currentThemePlaylist={currentThemePlaylist}
setCurrentThemePlaylist={setCurrentThemePlaylist}
handleDeletePlaylist={handleDeletePlaylist}
allThemes={allThemes}
createPlaylist={createPlaylist}
/>
</Box>
</Group>
);
})}
</Group>
</Stack>
</>
);
}
export default ThemePlaylists;