Update to the playlist picker
All checks were successful
Deploy to production / build (push) Successful in 20s

This commit is contained in:
2026-10-09 17:55:24 -05:00
parent cfa6520b00
commit 2e0be492f6
7 changed files with 658 additions and 338 deletions

View File

@@ -121,8 +121,6 @@ sudo pm2 start server.cjs
- Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led) - Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led)
- Update Theme playlists - Update Theme playlists
- Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components) - Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components)
- Include mantine-parallax (https://gfazioli.github.io/mantine-parallax#example-presentation)
- Include mantine-depth-select (https://gfazioli.github.io/mantine-depth-select/)
- Explore https://gfazioli.github.io/mantine-scene/ - Explore https://gfazioli.github.io/mantine-scene/
- Explore https://gfazioli.github.io/mantine-window/ - Explore https://gfazioli.github.io/mantine-window/
- Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random) - Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random)

631
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,22 +10,24 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@gfazioli/mantine-border-animate": "^3.0.2", "@gfazioli/mantine-border-animate": "^3.0.5",
"@gfazioli/mantine-parallax": "^4.0.10", "@gfazioli/mantine-depth-select": "^2.0.15",
"@gfazioli/mantine-text-animate": "^4.0.10", "@gfazioli/mantine-parallax": "^4.0.13",
"@gfazioli/mantine-reflection": "^3.0.13",
"@gfazioli/mantine-text-animate": "^4.0.15",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^9.6.2", "@mantine/core": "^9.7.1",
"@mantine/dates": "^9.6.2", "@mantine/dates": "^9.7.1",
"@mantine/form": "^9.6.2", "@mantine/form": "^9.7.1",
"@mantine/hooks": "^9.6.2", "@mantine/hooks": "^9.7.1",
"@mantine/notifications": "^9.6.2", "@mantine/notifications": "^9.7.1",
"@tanstack/react-query": "^5.103.2", "@tanstack/react-query": "^5.104.1",
"@tanstack/react-query-devtools": "^5.103.2", "@tanstack/react-query-devtools": "^5.104.1",
"@tanstack/react-query-persist-client": "^5.103.2", "@tanstack/react-query-persist-client": "^5.104.1",
"@tanstack/react-table": "^9.2.4", "@tanstack/react-table": "^9.2.8",
"@tanstack/react-virtual": "^3.14.13", "@tanstack/react-virtual": "^3.14.14",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"jotai": "^3.0.0", "jotai": "^3.0.1",
"openmeteo": "^1.2.3", "openmeteo": "^1.2.3",
"react": "^19.3.0", "react": "^19.3.0",
"react-dom": "^19.3.0", "react-dom": "^19.3.0",
@@ -35,16 +37,16 @@
"recharts": "^3.10.1" "recharts": "^3.10.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^26.6.2", "@types/node": "^26.6.4",
"@types/react": "^19.3.0", "@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1", "@vitejs/plugin-react": "^6.1.2",
"@vitest/coverage-v8": "^5.0.2", "@vitest/coverage-v8": "^5.0.3",
"@vitest/ui": "^5.0.2", "@vitest/ui": "^5.0.3",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.3.1", "vite": "^8.3.4",
"vite-plugin-dts": "~5.1.1", "vite-plugin-dts": "~5.1.2",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^5.0.2" "vitest": "^5.0.3"
} }
} }

View File

@@ -24,6 +24,7 @@ import 'react-fontpicker-ts/dist/index.css';
import '@gfazioli/mantine-border-animate/styles.css'; import '@gfazioli/mantine-border-animate/styles.css';
import '@gfazioli/mantine-parallax/styles.css'; import '@gfazioli/mantine-parallax/styles.css';
import '@gfazioli/mantine-text-animate/styles.css'; import '@gfazioli/mantine-text-animate/styles.css';
import '@gfazioli/mantine-reflection/styles.css';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Homepage, Settings } from 'pages'; import { Homepage, Settings } from 'pages';
import FontPicker from 'react-fontpicker-ts'; import FontPicker from 'react-fontpicker-ts';

View File

@@ -6,8 +6,8 @@ import { useEffect, useState } from 'react';
import { FiChevronRight } from 'react-icons/fi'; import { FiChevronRight } from 'react-icons/fi';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types'; import type { Theme, ThemePlaylist } from 'types';
import { ThemePlaylistCard } from './components/ThemePlaylistCard';
import { ThemePlaylistModal } from './components/ThemePlaylistModal'; import { ThemePlaylistModal } from './components/ThemePlaylistModal';
import { ThemePlaylistStack } from './components/ThemePlaylistStack';
export function ThemePlaylists() { export function ThemePlaylists() {
const themesEndpoint = 'api/themes'; const themesEndpoint = 'api/themes';
@@ -134,7 +134,7 @@ export function ThemePlaylists() {
/> />
)} )}
<Box> <Box>
<ThemePlaylistCard <ThemePlaylistStack
cardPlaylist={{ cardPlaylist={{
id: undefined, id: undefined,
name: 'All', name: 'All',
@@ -165,7 +165,7 @@ export function ThemePlaylists() {
/> />
)} )}
<Box> <Box>
<ThemePlaylistCard <ThemePlaylistStack
cardPlaylist={{ cardPlaylist={{
id: undefined, id: undefined,
name: 'Backgrounds', name: 'Backgrounds',
@@ -196,7 +196,7 @@ export function ThemePlaylists() {
/> />
)} )}
<Box> <Box>
<ThemePlaylistCard <ThemePlaylistStack
cardPlaylist={{ cardPlaylist={{
id: undefined, id: undefined,
name: 'No Backgrounds', name: 'No Backgrounds',
@@ -227,7 +227,7 @@ export function ThemePlaylists() {
/> />
)} )}
<Box> <Box>
<ThemePlaylistCard <ThemePlaylistStack
cardPlaylist={{ cardPlaylist={{
id: id:
themePlaylists?.find( themePlaylists?.find(
@@ -289,7 +289,7 @@ export function ThemePlaylists() {
/> />
)} )}
<Box> <Box>
<ThemePlaylistCard <ThemePlaylistStack
cardPlaylist={cardPlaylist} cardPlaylist={cardPlaylist}
handleOpen={handleOpen} handleOpen={handleOpen}
allThemes={allThemeData} allThemes={allThemeData}

View File

@@ -1,12 +1,4 @@
import { import { ActionIcon, Button, Overlay, ScrollArea, Stack, Text } from '@mantine/core';
ActionIcon,
Button,
Group,
Overlay,
ScrollArea,
Stack,
Text,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { DeleteItemModal } from 'components'; import { DeleteItemModal } from 'components';
import { useCommonHooks } from 'hooks'; import { useCommonHooks } from 'hooks';
@@ -33,7 +25,7 @@ export function ThemePlaylistCard({
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList, cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
); );
return ( return (
<Group <Stack
gap={0} gap={0}
align='center' align='center'
justify='center' justify='center'
@@ -212,6 +204,6 @@ export function ThemePlaylistCard({
)} )}
</Stack> </Stack>
</Button> </Button>
</Group> </Stack>
); );
} }

View File

@@ -0,0 +1,292 @@
import { Parallax } from '@gfazioli/mantine-parallax';
import { Reflection } from '@gfazioli/mantine-reflection';
import { ActionIcon, Box, Group, Stack, Text } from '@mantine/core';
import { useDebouncedCallback, useDisclosure } from '@mantine/hooks';
import { DeleteItemModal } from 'components';
import { useCommonHooks } from 'hooks';
import { useState } from 'react';
import { FiEdit, FiTrash } from 'react-icons/fi';
import { textClasses } from 'styles';
import type { Theme, ThemePlaylist } from 'types';
import { ThemeCard } from '../../Themes';
export function ThemePlaylistStack({
cardPlaylist,
handleOpen,
allThemes,
}: {
cardPlaylist: any;
handleOpen: (cardPlaylist: ThemePlaylist) => void;
allThemes: Theme[];
}) {
const [hover, setHover] = useState(false);
const { setThemeName, setCurrentThemePlaylist, handleDelete, currentUser } =
useCommonHooks();
const [indexOffset, setIndexOffset] = useState(0);
const defaultLists = ['All', 'Backgrounds', 'No Backgrounds'];
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
const themeList = JSON.parse(
cardPlaylist?.themeList?.name === 'Time of Day' ? '[]' : cardPlaylist.themeList,
);
const missingTheme = {
id: undefined,
name: 'default',
backgroundImage: 'none',
backgroundColor: '#EEEEEE',
searchColor: '#00000033',
primaryColor: '#222222',
accentColor: '#5c5c5c',
borderColor: '#5c5c5c',
randomEnabled: 1,
textShadowsEnabled: 0,
appShadowsEnabled: 0,
themeFont: currentUser.defaultFont,
};
const presentationProps = {
initialRotationX: 60,
initialRotationY: 0,
initialRotationZ: 0,
initialSkewX: -30,
initialSkewY: 30,
disabled: true,
};
const handleMouseWheel = useDebouncedCallback((e: any) => {
e.stopPropagation();
if (e.deltaY > 0) {
setIndexOffset((indexOffset % themeList.length) - 1);
} else {
setIndexOffset((indexOffset % themeList.length) + 1);
}
}, 75);
return (
<Stack
gap={0}
w={300}
pos={'relative'}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
background: hover
? 'radial-gradient(var(--mantine-color-search-0), transparent)'
: undefined,
cursor: 'pointer',
borderRadius: '100%',
}}
onClick={() => {
setCurrentThemePlaylist(cardPlaylist.name);
setThemeName('random');
}}
>
{modalOpened && (
<DeleteItemModal
modalOpened={modalOpened}
handleClose={closeModal}
handleDelete={() => handleDelete(cardPlaylist, 'api/themePlaylists')}
title='Delete Theme Playlist'
body={
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
<Text>Are you sure you want to delete this Theme Playlist?</Text>
<Text
className={textClasses.deleteItem}
classNames={textClasses}
>{`"${cardPlaylist.name}"`}</Text>
<Text c='red'>This action cannot be undone!</Text>
</Stack>
}
/>
)}
{cardPlaylist?.name === 'Time of Day' ? (
<Reflection>
<Stack
w='100%'
h='200px'
justify='center'
pos='relative'
gap={16}
style={{ overflow: 'hidden' }}
>
<Parallax
{...presentationProps}
style={{ position: 'absolute', top: -60, zIndex: 3 }}
>
<ThemeCard
cardTheme={
allThemes.find((theme: Theme) => theme.id === Number(themeList[0])) ||
missingTheme
}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
<Parallax
{...presentationProps}
style={{ position: 'absolute', top: -60, zIndex: 2 }}
>
<ThemeCard
cardTheme={
allThemes.find((theme: Theme) => theme.id === Number(themeList[1])) ||
missingTheme
}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
<Parallax
{...presentationProps}
style={{ position: 'absolute', top: -60, zIndex: 1 }}
>
<ThemeCard
cardTheme={
allThemes.find((theme: Theme) => theme.id === Number(themeList[2])) ||
missingTheme
}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
<Parallax
{...presentationProps}
style={{ position: 'absolute', top: -60, zIndex: 0 }}
>
<ThemeCard
cardTheme={
allThemes.find((theme: Theme) => theme.id === Number(themeList[3])) ||
missingTheme
}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
</Stack>
</Reflection>
) : (
<Reflection>
<Stack
w='100%'
h='200px'
justify='start'
pos='relative'
gap={0}
style={{
overflow: 'hidden',
transition: 'all 0.5s ease-in-out',
overscrollBehavior: 'contain',
}}
onWheel={handleMouseWheel}
>
{themeList
.sort((a: number, b: number) => (a > b ? 1 : -1))
.map((item: number, index: number) => {
let spot = index + indexOffset;
if (spot <= -1) {
spot = index + indexOffset + themeList.length;
} else if (spot >= themeList.length) {
spot = index + indexOffset - themeList.length;
} else {
spot = index + indexOffset;
}
const theme: any = allThemes.find((theme: Theme) => theme.id === item);
return (
<Box
key={theme.id}
style={{
transition: 'all 0.3s ease-in-out',
position: 'absolute',
top: 16 * (spot + 1),
zIndex: themeList.length - spot,
}}
>
<Parallax {...presentationProps}>
<ThemeCard
key={theme.id}
cardTheme={theme}
allThemes={allThemes}
themePlaylists={[]}
setSelectedTheme={() => {}}
openThemeModal={() => {}}
enabled={false}
management={false}
/>
</Parallax>
</Box>
);
})}
</Stack>
</Reflection>
)}
<Group
w={'100%'}
pos={'absolute'}
justify='space-between'
bottom={0}
style={{ zIndex: 1000 }}
>
<Text
style={{
paddingRight: '.25rem',
paddingLeft: '.25rem',
backgroundColor: 'var(--mantine-color-border-0)',
borderRadius: '4px',
color: 'var(--mantine-color-primary-0)',
}}
>
{cardPlaylist.name}
</Text>
<Group gap={0}>
{!defaultLists.includes(cardPlaylist.name) && (
<ActionIcon
style={{
paddingRight: '.25rem',
paddingLeft: '.25rem',
backgroundColor: 'var(--mantine-color-border-0)',
borderRadius: '4px',
color: 'var(--mantine-color-background-0)',
}}
onClick={() => {
handleOpen(cardPlaylist);
}}
>
<FiEdit />
</ActionIcon>
)}
{cardPlaylist.default === 0 && (
<ActionIcon
color='red'
style={{
paddingRight: '.25rem',
paddingLeft: '.25rem',
borderRadius: '4px',
}}
onClick={() => {
openModal();
}}
>
<FiTrash color='black' />
</ActionIcon>
)}
</Group>
</Group>
</Stack>
);
}