Added thumbnails and updated background pages

This commit is contained in:
Sean Edmunds
2025-07-21 16:28:43 -05:00
parent 2a53d9f16a
commit 063bc10094
12 changed files with 603 additions and 392 deletions

54
package-lock.json generated
View File

@@ -9,10 +9,10 @@
"version": "1.0.0",
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^8.1.3",
"@mantine/form": "^8.1.3",
"@mantine/hooks": "^8.1.3",
"@mantine/notifications": "^8.1.3",
"@mantine/core": "^8.2.1",
"@mantine/form": "^8.2.1",
"@mantine/hooks": "^8.2.1",
"@mantine/notifications": "^8.2.1",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-query-persist-client": "^5.83.0",
@@ -25,7 +25,7 @@
"recharts": "^3.1.0"
},
"devDependencies": {
"@types/node": "^24.0.14",
"@types/node": "^24.0.15",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",
@@ -930,9 +930,9 @@
}
},
"node_modules/@mantine/core": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.1.3.tgz",
"integrity": "sha512-2WOPC8GSN3MApet0MccSn6LaXRhcP6SVtZnbuHoqJ/atrfK7kLE66ILr4OXov7JAj1ASJ4Xk0bOXmu5fBExAvQ==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.2.1.tgz",
"integrity": "sha512-KxvydotyFRdrRbqULUX2G35/GddPFju9XQUv/vdDWu1ytIWZViTguc+WSj1aBd0DtfRrSaofU5ezZISEXVrPBA==",
"license": "MIT",
"dependencies": {
"@floating-ui/react": "^0.26.28",
@@ -943,15 +943,15 @@
"type-fest": "^4.27.0"
},
"peerDependencies": {
"@mantine/hooks": "8.1.3",
"@mantine/hooks": "8.2.1",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/form": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.1.3.tgz",
"integrity": "sha512-OoSVv2cyjKRZ+C4Rw63VsnO3qjKGZHJkd6DSJTVRQHXfDr10hxmC5yXgxGKsxGQ+xFd4ZCdtzPUU2BoWbHfZAA==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.2.1.tgz",
"integrity": "sha512-XXGdO3IgTNcDl6lP7FIysEaH0rP6CfOGulxQA3ot+hHA8/JxmHWoWeoKOBE8I6QW7UYqaBxARxgBHLMcp8qASw==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -962,34 +962,34 @@
}
},
"node_modules/@mantine/hooks": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.1.3.tgz",
"integrity": "sha512-yL4SbyYjrkmtIhscswajNz9RL0iO2+V8CMtOi0KISch2rPNvTAJNumFuZaXgj4UHeDc0JQYSmcZ+EW8NGm7xcQ==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.2.1.tgz",
"integrity": "sha512-gnRDk5FXCD9fa0AjlAj9otCsZL9QJzVrpYZk9KjOEoP5XR1TEE2F9/rGbajh1UVjPnD3jUlNLRJMH0YHTlA65A==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
}
},
"node_modules/@mantine/notifications": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.1.3.tgz",
"integrity": "sha512-Xy6f/l1yLTo77hz8X80sOuY+HW80e1rn8ucygx9TAexK5+XtyriOv26TQ3EJ6Ej5jlchtZRFEUJ4tJGRWjGCNg==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.2.1.tgz",
"integrity": "sha512-9s4oN0ruX7AqUghZIXEwx9/t+fHATe/kvn5O5Zum+MFoOH46p8k4ljNb6BBDKTtT3clRHx7KHTR7OrLWlDEjrw==",
"license": "MIT",
"dependencies": {
"@mantine/store": "8.1.3",
"@mantine/store": "8.2.1",
"react-transition-group": "4.4.5"
},
"peerDependencies": {
"@mantine/core": "8.1.3",
"@mantine/hooks": "8.1.3",
"@mantine/core": "8.2.1",
"@mantine/hooks": "8.2.1",
"react": "^18.x || ^19.x",
"react-dom": "^18.x || ^19.x"
}
},
"node_modules/@mantine/store": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.1.3.tgz",
"integrity": "sha512-rO72LfSJqSNCwufqJxTWiHMyOR6sR3mqAcnBcw/f5aTvyOYoHZzlm4q4+TL8/2vYGRVsr9YM2Ez6HQ1vk/RR8g==",
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.2.1.tgz",
"integrity": "sha512-4DAVJcI5Sa8Zez/23DV4eipYbqRfw7r+UkuWczPFyhAU+BZdfIql1gFipojAlR0FpWbB8aL0F7wFKNKfkZ0YCQ==",
"license": "MIT",
"peerDependencies": {
"react": "^18.x || ^19.x"
@@ -1846,9 +1846,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
"integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
"version": "24.0.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
"integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -11,10 +11,10 @@
},
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^8.1.3",
"@mantine/form": "^8.1.3",
"@mantine/hooks": "^8.1.3",
"@mantine/notifications": "^8.1.3",
"@mantine/core": "^8.2.1",
"@mantine/form": "^8.2.1",
"@mantine/hooks": "^8.2.1",
"@mantine/notifications": "^8.2.1",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query-devtools": "^5.83.0",
"@tanstack/react-query-persist-client": "^5.83.0",
@@ -27,7 +27,7 @@
"recharts": "^3.1.0"
},
"devDependencies": {
"@types/node": "^24.0.14",
"@types/node": "^24.0.15",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.7.0",

View File

@@ -125,6 +125,48 @@ export function useCommonHooks() {
return k;
}
function getObjectDiff(original: any, current: any) {
const changes: any = {};
// Check current object's properties
for (const [key, value] of Object.entries(current)) {
if (!(key in original)) {
changes[key] = {
oldValue: undefined,
newValue: value,
};
continue;
}
const originalValue = original[key];
const currentValue = value;
// Handle different types of comparisons
if (
originalValue !== currentValue &&
String(originalValue) !== String(currentValue) &&
JSON.stringify(originalValue) !== JSON.stringify(currentValue)
) {
changes[key] = {
oldValue: originalValue,
newValue: currentValue,
};
}
}
// Check for removed properties
for (const key of Object.keys(original)) {
if (!(key in current)) {
changes[key] = {
oldValue: original[key],
newValue: undefined,
};
}
}
return Object.keys(changes).length === 0 ? null : changes;
}
return {
isMobile,
theme,
@@ -132,6 +174,7 @@ export function useCommonHooks() {
urlPattern,
fixDigits,
reorder,
getObjectDiff,
handleThemeChange,
handleDelete,
themeName,

View File

@@ -0,0 +1,38 @@
import { Group, Image, Modal, Text } from '@mantine/core';
export function BackgroundLightbox({
opened,
close,
background,
}: {
opened: boolean;
close: () => void;
background: string;
}) {
return (
<Modal.Root opened={opened} onClose={close} size={'auto'} centered zIndex={250}>
<Group
px={'1rem'}
onClick={close}
w={'100%'}
pos={'absolute'}
top={'1rem'}
justify='space-between'
style={{ zIndex: 251 }}
>
<Text style={{ color: 'var(--mantine-color-primary-0)' }}>{background}</Text>
<Modal.CloseButton c={'red'} bg={'none'} />
</Group>
<Modal.Overlay opacity={0.1} />
<Modal.Content>
<Modal.Body p={'1rem'}>
<Image
style={{ cursor: 'zoom-out' }}
onClick={close}
src={`../../api/backgroundImages/${background}`}
/>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}

View File

@@ -12,7 +12,7 @@ import {
Text,
TextInput,
} from '@mantine/core';
import { useDebouncedValue, useElementSize } from '@mantine/hooks';
import { useDebouncedValue, useDisclosure, useElementSize } from '@mantine/hooks';
import { useCommonHooks } from 'hooks';
import { useFileUploadAPI } from 'hooks/useAPI';
import { useEffect, useState } from 'react';
@@ -24,6 +24,7 @@ import {
scrollAreaClasses,
textInputClasses,
} from 'styles';
import { BackgroundLightbox } from './BackgroundLightbox';
import { DeleteBackground } from './DeleteBackground';
import { EditBackground } from './EditBackground';
@@ -38,7 +39,10 @@ export function BackgroundModal({
backgroundList: string[];
inUseBackgrounds: string[];
}) {
const [opened, { open, close }] = useDisclosure(false);
const [lightboxName, setLightboxName] = useState<string>('');
const { ref, width } = useElementSize();
const [hovered, setHovered] = useState<string>('');
const [background, setBackground] = useState<File | null>(null);
const [fileName, setFileName] = useState<string>('');
const [newFileName, setNewFileName] = useState<string>('');
@@ -64,6 +68,10 @@ export function BackgroundModal({
}, [debouncedSearch]);
return (
<>
{opened && (
<BackgroundLightbox opened={opened} close={close} background={lightboxName} />
)}
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
@@ -79,13 +87,13 @@ export function BackgroundModal({
}}
fullScreen={isMobile}
centered
keepMounted
keepMounted={false}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text fw={600} fz={'1.5rem'} c={'var(--mantine-color-primary-0)'}>
<Text fw={600} fz={'1.5rem'} c={'var(--mantine-color-background-0)'}>
Manage Backgrounds
</Text>
</Group>
@@ -194,18 +202,35 @@ export function BackgroundModal({
.includes(debouncedSearch.toLocaleLowerCase()),
)
.map((background) => (
<Group key={background} display={'flex'} pos={'relative'}>
<Group
onMouseEnter={() => {
setHovered(background);
}}
onMouseLeave={() => {
setHovered('');
}}
key={background}
display={'flex'}
pos={'relative'}
style={{
borderRadius: '.5rem',
backgroundColor:
hovered === background
? 'var(--mantine-color-search-0)'
: undefined,
}}
>
<Text
style={{
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-accent-0)',
color: 'var(--mantine-color-background-0)',
backgroundColor: 'var(--mantine-color-primary-0)',
padding: '.25rem',
borderRadius: '.5rem',
position: 'absolute',
top: '-.5rem',
top: '-.25rem',
left: 0,
zIndex: 3,
boxShadow: '2px -2px var(--mantine-color-primary-0)',
boxShadow: '2px 2px var(--mantine-color-accent-0)',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
@@ -219,12 +244,17 @@ export function BackgroundModal({
style={{
aspectRatio: '16/9',
width: '100%',
backgroundImage: `url(../../api/backgroundImages/${background})`,
backgroundImage: `url(../../api/backgroundImages/thumbs/${background})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
padding: '2rem',
margin: '1rem',
cursor: 'zoom-in',
}}
onClick={() => {
setLightboxName(background);
open();
}}
/>
<ActionIcon
@@ -344,9 +374,15 @@ export function BackgroundModal({
disabled={!background}
onClick={() => {
if (background) {
const extension = background.name.substring(
background.name.lastIndexOf('.'),
);
const validatedName = newFileName.endsWith(extension)
? newFileName
: `${newFileName}${extension}`;
const newBackground = new File(
[background],
newFileName || background.name,
validatedName || background.name,
{ type: background.type },
);
fd.append('background', newBackground || '');
@@ -385,5 +421,6 @@ export function BackgroundModal({
</Modal.Body>
</Modal.Content>
</Modal.Root>
</>
);
}

View File

@@ -46,7 +46,7 @@ export function DeleteBackground({
style={{
aspectRatio: '16/9',
height: '100%',
backgroundImage: `url(../../api/backgroundImages/${fileName})`,
backgroundImage: `url(../../api/backgroundImages/thumbs/${fileName})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',

View File

@@ -1,4 +1,4 @@
import { Button, Card, Group, Stack, Text, TextInput } from '@mantine/core';
import { Button, Group, Image, Stack, Text, TextInput } from '@mantine/core';
import { useCommonHooks, useMutationAPI } from 'hooks';
import { useState } from 'react';
import { textInputClasses } from 'styles';
@@ -27,7 +27,6 @@ export function EditBackground({
align='center'
style={{
width: '100%',
height: '30rem',
backgroundColor: 'var(--mantine-color-search-0)',
padding: '1rem',
borderRadius: '.5rem',
@@ -42,7 +41,7 @@ export function EditBackground({
Updating the name will update the associated themes.
</Text>
)}
<Group h={'100%'} mb={'1rem'}>
<Stack h={'100%'} mb={'1rem'}>
<TextInput
className={textInputClasses.settingsItem}
classNames={textInputClasses}
@@ -56,19 +55,8 @@ export function EditBackground({
: undefined
}
/>
<Card
style={{
aspectRatio: '16/9',
height: '100%',
backgroundImage: `url(../../api/backgroundImages/${fileName})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
padding: '2rem',
margin: '1rem',
}}
/>
</Group>
<Image src={`../../api/backgroundImages/thumbs/${fileName}`} />
</Stack>
<Group>
<Button
mt={'auto'}

View File

@@ -114,7 +114,7 @@ export function ThemeCard({
style={{
backgroundImage:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../api/backgroundImages/${cardTheme?.backgroundImage})`
? `linear-gradient(${cardTheme?.backgroundColor}33, ${cardTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${cardTheme?.backgroundImage})`
: undefined,
backgroundSize:
cardTheme?.backgroundImage.toLocaleLowerCase() !== 'none'

View File

@@ -14,7 +14,7 @@ export function ThemeMiniCard({ miniTheme }: { miniTheme: Theme | undefined }) {
style={{
backgroundImage:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${miniTheme?.backgroundColor}33, ${miniTheme?.backgroundColor}33), url(../../api/backgroundImages/${miniTheme?.backgroundImage})`
? `linear-gradient(${miniTheme?.backgroundColor}33, ${miniTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${miniTheme?.backgroundImage})`
: undefined,
backgroundSize:
miniTheme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined,

View File

@@ -5,6 +5,7 @@ import {
ColorPicker,
ColorSwatch,
Group,
Image,
Modal,
ScrollArea,
Stack,
@@ -42,7 +43,7 @@ export function ThemeModal({
closePicker: () => void;
handleSaveTheme: () => void;
}) {
const { isMobile, theme } = useCommonHooks();
const { isMobile, theme, getObjectDiff } = useCommonHooks();
const viewportRef = useRef<HTMLDivElement>(null);
const [query, setQuery] = useState('');
const [hovered, setHovered] = useState(-1);
@@ -71,8 +72,8 @@ export function ThemeModal({
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})`
? `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,
@@ -491,7 +492,8 @@ export function ThemeModal({
</Stack>
)}
{!pickerOpen && !backgroundPicker && (
<Stack h={'100%'} align='center' justify='center' gap={4}>
<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}
@@ -501,6 +503,14 @@ export function ThemeModal({
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
@@ -511,7 +521,102 @@ export function ThemeModal({
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}>{key}</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={`../../api/backgroundImages/thumbs/${defaultTheme[key as keyof Theme] as string}`}
alt='None'
/>
<Text>→</Text>
<Image
w={'40%'}
src={`../../api/backgroundImages/thumbs/${designerTheme[key as keyof Theme] as string}`}
alt='None'
/>
</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 });

View File

@@ -10,7 +10,7 @@ export function ThemeModalContent({ selectedTheme }: { selectedTheme: Theme }) {
style={{
backgroundImage:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${selectedTheme?.backgroundColor}33, ${selectedTheme?.backgroundColor}33), url(../../api/backgroundImages/${selectedTheme?.backgroundImage})`
? `linear-gradient(${selectedTheme?.backgroundColor}33, ${selectedTheme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${selectedTheme?.backgroundImage})`
: undefined,
backgroundSize:
selectedTheme?.backgroundImage.toLocaleLowerCase() !== 'none'

View File

@@ -61,7 +61,7 @@ export function ThemeSelect({
color: theme?.primaryColor,
backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/${theme?.backgroundImage})`
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})`
: undefined,
backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none' ? 'cover' : undefined,
@@ -88,7 +88,7 @@ export function ThemeSelect({
style={{
backgroundImage:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/${theme?.backgroundImage})`
? `linear-gradient(${theme?.backgroundColor}33, ${theme?.backgroundColor}33), url(../../api/backgroundImages/thumbs/${theme?.backgroundImage})`
: undefined,
backgroundSize:
theme?.backgroundImage.toLocaleLowerCase() !== 'none'