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,326 +68,359 @@ export function BackgroundModal({
}, [debouncedSearch]);
return (
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={backgroundModalOpened}
onClose={() => {
setBackground(null);
setFileName('');
setNewFileName('');
handleBackgroundModalClose();
setConfirm(false);
setEditing(false);
}}
fullScreen={isMobile}
centered
keepMounted
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text fw={600} fz={'1.5rem'} c={'var(--mantine-color-primary-0)'}>
Manage Backgrounds
</Text>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body p={'1rem'}>
<Stack align='center' w={'100%'}>
{!editing && !confirm && !adding && searchValue !== undefined && (
<Group w={'100%'} justify='center'>
<TextInput
w={'50%'}
className={textInputClasses.default}
classNames={textInputClasses}
autoFocus={!isMobile}
placeholder={'Search...'}
value={searchValue}
onInput={(event: any) => {
setIsLoading(true);
setSearchValue(event.target.value);
}}
style={{
zIndex: 100,
paddingTop: 0,
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
rightSection={
isLoading ? (
<ActionIcon
className={actionIconClasses.loadingSpinner}
classNames={actionIconClasses}
>
<FiLoader />
</ActionIcon>
) : (
searchValue && (
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
onClick={() => setSearchValue('')}
>
<FiX />
</ActionIcon>
)
)
}
/>
<Button
className={buttonClasses.newItemButton}
classNames={buttonClasses}
onClick={() => {
setAdding(true);
}}
>
<FiPlus />
</Button>
</Group>
)}
{confirm && (
<DeleteBackground
fileName={fileName}
setFileName={setFileName}
inUseBackgrounds={inUseBackgrounds}
setBackground={setBackground}
setNewFileName={setNewFileName}
setConfirm={setConfirm}
/>
)}
{editing && (
<EditBackground
fileName={fileName}
setFileName={setFileName}
inUseBackgrounds={inUseBackgrounds}
setBackground={setBackground}
setNewFileName={setNewFileName}
setEditing={setEditing}
/>
)}
{!editing && !confirm && !adding && (
<ScrollArea
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
type={'always'}
style={{
width: '100%',
height: '30rem',
backgroundColor: 'var(--mantine-color-search-0)',
padding: '1rem',
borderRadius: '.5rem',
backgroundFilter: 'blur(5px)',
}}
>
<SimpleGrid w={'100%'} cols={3} p={'.75rem'}>
{backgroundList
.filter((background) => background !== 'None')
.filter(
(background) =>
!debouncedSearch ||
background
.toLocaleLowerCase()
.includes(debouncedSearch.toLocaleLowerCase()),
)
.map((background) => (
<Group key={background} display={'flex'} pos={'relative'}>
<Text
style={{
color: 'var(--mantine-color-primary-0)',
backgroundColor: 'var(--mantine-color-accent-0)',
padding: '.25rem',
borderRadius: '.5rem',
position: 'absolute',
top: '-.5rem',
left: 0,
zIndex: 3,
boxShadow: '2px -2px var(--mantine-color-primary-0)',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
maxWidth: width,
}}
>
{background}
</Text>
<Card
ref={ref}
style={{
aspectRatio: '16/9',
width: '100%',
backgroundImage: `url(../../api/backgroundImages/${background})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
padding: '2rem',
margin: '1rem',
}}
/>
<ActionIcon
style={{
position: 'absolute',
bottom: '.5rem',
right: '2.5rem',
zIndex: 3,
}}
onClick={() => {
setFileName(background);
setEditing(true);
}}
>
<FiEdit />
</ActionIcon>
<ActionIcon
color='var(--mantine-color-red-6)'
style={{
position: 'absolute',
bottom: '.5rem',
right: '.5rem',
zIndex: 3,
}}
onClick={() => {
setFileName(background);
setConfirm(true);
}}
>
<FiTrash color='black' />
</ActionIcon>
</Group>
))}
</SimpleGrid>
</ScrollArea>
)}
{adding && (
<Stack
display={'flex'}
pos={'relative'}
w={'100%'}
align={'center'}
justify='space-between'
p={16}
>
<Group gap={0}>
<Stack gap={0} w={'60%'}>
<FileInput
clearable
accept='image/png,image/jpeg'
value={background}
onChange={handleFileUpload}
label={'Upload Background'}
placeholder={'Select...'}
error={err && 'Please upload a background'}
description={
'Max size: 50MB for upload. Images are resized to 300kb after upload.'
}
pr={'xl'}
pl={0}
styles={{
input: {
color: 'var(--mantine-color-primary-0)',
borderColor: 'var(--mantine-color-primary-0)',
},
placeholder: {
color: 'var(--mantine-color-accent-0)',
},
}}
/>
<TextInput
label={'File Name'}
placeholder={fileName}
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
pr={'xl'}
pl={0}
styles={{
input: {
color: 'var(--mantine-color-primary-0)',
borderColor: 'var(--mantine-color-primary-0)',
},
}}
/>
</Stack>
<Card
<>
{opened && (
<BackgroundLightbox opened={opened} close={close} background={lightboxName} />
)}
<Modal.Root
size={'75%'}
className={modalClasses.editThemeModal}
classNames={modalClasses}
opened={backgroundModalOpened}
onClose={() => {
setBackground(null);
setFileName('');
setNewFileName('');
handleBackgroundModalClose();
setConfirm(false);
setEditing(false);
}}
fullScreen={isMobile}
centered
keepMounted={false}
>
<Modal.Overlay />
<Modal.Content>
<Modal.Header style={{ justifyContent: 'center' }}>
<Group pos={'absolute'}>
<Text fw={600} fz={'1.5rem'} c={'var(--mantine-color-background-0)'}>
Manage Backgrounds
</Text>
</Group>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body p={'1rem'}>
<Stack align='center' w={'100%'}>
{!editing && !confirm && !adding && searchValue !== undefined && (
<Group w={'100%'} justify='center'>
<TextInput
w={'50%'}
className={textInputClasses.default}
classNames={textInputClasses}
autoFocus={!isMobile}
placeholder={'Search...'}
value={searchValue}
onInput={(event: any) => {
setIsLoading(true);
setSearchValue(event.target.value);
}}
style={{
aspectRatio: '16/9',
width: '35%',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundColor: 'var(--mantine-color-search-0)',
padding: '2rem',
margin: '1rem',
marginRight: 0,
zIndex: 100,
paddingTop: 0,
}}
styles={
theme.textShadowsEnabled
? {
label: { boxShadow: '2px 2px var(--mantine-color-border-0)' },
}
: undefined
}
rightSection={
isLoading ? (
<ActionIcon
className={actionIconClasses.loadingSpinner}
classNames={actionIconClasses}
>
<FiLoader />
</ActionIcon>
) : (
searchValue && (
<ActionIcon
className={actionIconClasses.settingsTooltip}
classNames={actionIconClasses}
onClick={() => setSearchValue('')}
>
<FiX />
</ActionIcon>
)
)
}
/>
<Button
className={buttonClasses.newItemButton}
classNames={buttonClasses}
onClick={() => {
setAdding(true);
}}
>
{background ? (
<Image
src={URL.createObjectURL(background)}
w={'100%'}
h={'100%'}
/>
) : (
<Image
src={'https://placehold.co/800x450?text=Upload%20an%20Image'}
w={'100%'}
h={'100%'}
/>
)}
</Card>
<FiPlus />
</Button>
</Group>
<Group w={'100%'} justify='center'>
<Button
w={'min-content'}
disabled={!background}
onClick={() => {
if (background) {
const newBackground = new File(
[background],
newFileName || background.name,
{ type: background.type },
);
fd.append('background', newBackground || '');
uploadAsync({
method: 'POST',
endpoint: 'api/backgrounds',
body: fd,
});
)}
{confirm && (
<DeleteBackground
fileName={fileName}
setFileName={setFileName}
inUseBackgrounds={inUseBackgrounds}
setBackground={setBackground}
setNewFileName={setNewFileName}
setConfirm={setConfirm}
/>
)}
{editing && (
<EditBackground
fileName={fileName}
setFileName={setFileName}
inUseBackgrounds={inUseBackgrounds}
setBackground={setBackground}
setNewFileName={setNewFileName}
setEditing={setEditing}
/>
)}
{!editing && !confirm && !adding && (
<ScrollArea
className={scrollAreaClasses.default}
classNames={scrollAreaClasses}
type={'always'}
style={{
width: '100%',
height: '30rem',
backgroundColor: 'var(--mantine-color-search-0)',
padding: '1rem',
borderRadius: '.5rem',
backgroundFilter: 'blur(5px)',
}}
>
<SimpleGrid w={'100%'} cols={3} p={'.75rem'}>
{backgroundList
.filter((background) => background !== 'None')
.filter(
(background) =>
!debouncedSearch ||
background
.toLocaleLowerCase()
.includes(debouncedSearch.toLocaleLowerCase()),
)
.map((background) => (
<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-background-0)',
backgroundColor: 'var(--mantine-color-primary-0)',
padding: '.25rem',
borderRadius: '.5rem',
position: 'absolute',
top: '-.25rem',
left: 0,
zIndex: 3,
boxShadow: '2px 2px var(--mantine-color-accent-0)',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',
maxWidth: width,
}}
>
{background}
</Text>
<Card
ref={ref}
style={{
aspectRatio: '16/9',
width: '100%',
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
style={{
position: 'absolute',
bottom: '.5rem',
right: '2.5rem',
zIndex: 3,
}}
onClick={() => {
setFileName(background);
setEditing(true);
}}
>
<FiEdit />
</ActionIcon>
<ActionIcon
color='var(--mantine-color-red-6)'
style={{
position: 'absolute',
bottom: '.5rem',
right: '.5rem',
zIndex: 3,
}}
onClick={() => {
setFileName(background);
setConfirm(true);
}}
>
<FiTrash color='black' />
</ActionIcon>
</Group>
))}
</SimpleGrid>
</ScrollArea>
)}
{adding && (
<Stack
display={'flex'}
pos={'relative'}
w={'100%'}
align={'center'}
justify='space-between'
p={16}
>
<Group gap={0}>
<Stack gap={0} w={'60%'}>
<FileInput
clearable
accept='image/png,image/jpeg'
value={background}
onChange={handleFileUpload}
label={'Upload Background'}
placeholder={'Select...'}
error={err && 'Please upload a background'}
description={
'Max size: 50MB for upload. Images are resized to 300kb after upload.'
}
pr={'xl'}
pl={0}
styles={{
input: {
color: 'var(--mantine-color-primary-0)',
borderColor: 'var(--mantine-color-primary-0)',
},
placeholder: {
color: 'var(--mantine-color-accent-0)',
},
}}
/>
<TextInput
label={'File Name'}
placeholder={fileName}
value={newFileName}
onChange={(e) => setNewFileName(e.target.value)}
pr={'xl'}
pl={0}
styles={{
input: {
color: 'var(--mantine-color-primary-0)',
borderColor: 'var(--mantine-color-primary-0)',
},
}}
/>
</Stack>
<Card
style={{
aspectRatio: '16/9',
width: '35%',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundColor: 'var(--mantine-color-search-0)',
padding: '2rem',
margin: '1rem',
marginRight: 0,
}}
>
{background ? (
<Image
src={URL.createObjectURL(background)}
w={'100%'}
h={'100%'}
/>
) : (
<Image
src={'https://placehold.co/800x450?text=Upload%20an%20Image'}
w={'100%'}
h={'100%'}
/>
)}
</Card>
</Group>
<Group w={'100%'} justify='center'>
<Button
w={'min-content'}
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],
validatedName || background.name,
{ type: background.type },
);
fd.append('background', newBackground || '');
uploadAsync({
method: 'POST',
endpoint: 'api/backgrounds',
body: fd,
});
setAdding(false);
setBackground(null);
setNewFileName('');
setErr(false);
} else {
setErr(true);
}
}}
>
Upload
</Button>
<Button
w={'min-content'}
color='var(--mantine-color-red-6)'
onClick={() => {
setAdding(false);
setBackground(null);
setNewFileName('');
setErr(false);
} else {
setErr(true);
}
}}
>
Upload
</Button>
<Button
w={'min-content'}
color='var(--mantine-color-red-6)'
onClick={() => {
setAdding(false);
setBackground(null);
setNewFileName('');
setErr(false);
}}
>
Cancel
</Button>
</Group>
</Stack>
)}
</Stack>
</Modal.Body>
</Modal.Content>
</Modal.Root>
}}
>
Cancel
</Button>
</Group>
</Stack>
)}
</Stack>
</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,27 +492,131 @@ export function ThemeModal({
</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,
});
}}
/>
<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}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
textShadowsEnabled: e.target.checked ? 1 : 0,
});
}}
styles={{
root: { width: '50%' },
body: {
width: '100%',
display: 'flex',
justifyContent: 'space-between',
},
}}
/>
<Checkbox
label='App Shadows'
checked={selectedTheme.appShadowsEnabled === 1}
onChange={(e) => {
setSelectedTheme({
...selectedTheme,
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'