From 715b8a091f1fc7b0e22e8eb2f57439c8117955cb Mon Sep 17 00:00:00 2001 From: Sean Edmunds Date: Thu, 19 Jun 2025 12:30:03 -0500 Subject: [PATCH] Background uploads --- package-lock.json | 46 ++--- package.json | 6 +- src/hooks/useAPI.tsx | 80 +++++---- .../components/Theming/BackgroundModal.tsx | 163 ++++++++++++++++++ .../components/Theming/ThemePlaylists.tsx | 74 ++++---- .../settings/components/Theming/Themes.tsx | 37 +++- 6 files changed, 309 insertions(+), 97 deletions(-) create mode 100644 src/pages/settings/components/Theming/BackgroundModal.tsx diff --git a/package-lock.json b/package-lock.json index 7a4ba06..e90d3ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,9 +13,9 @@ "@mantine/form": "^8.1.1", "@mantine/hooks": "^8.1.1", "@mantine/notifications": "^8.1.1", - "@tanstack/react-query": "^5.80.7", - "@tanstack/react-query-devtools": "^5.80.7", - "@tanstack/react-query-persist-client": "^5.80.7", + "@tanstack/react-query": "^5.80.10", + "@tanstack/react-query-devtools": "^5.80.10", + "@tanstack/react-query-persist-client": "^5.80.10", "idb-keyval": "^6.2.2", "jotai": "^2.12.5", "react": "^19.1.0", @@ -1592,9 +1592,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz", - "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.10.tgz", + "integrity": "sha512-mUNQOtzxkjL6jLbyChZoSBP6A5gQDVRUiPvW+/zw/9ftOAz+H754zCj3D8PwnzPKyHzGkQ9JbH48ukhym9LK1Q==", "license": "MIT", "funding": { "type": "github", @@ -1612,12 +1612,12 @@ } }, "node_modules/@tanstack/query-persist-client-core": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.80.7.tgz", - "integrity": "sha512-/V+zw5cGseMzzI9SfI5eem+826gMhnouMi0X7Da41dVC96xNvdyed+FHtIKaSXyCm4YpcPUtyA5eHG4Ng7yU8g==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.80.10.tgz", + "integrity": "sha512-3fcLDW4hDkiGfG/NRj6hgl/+G94lsgtx/Wj9jxuUN0LiawKpIMfGrSNCERFPYpJMuTtCyX9A2mqHE++sV13lsA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.80.7" + "@tanstack/query-core": "5.80.10" }, "funding": { "type": "github", @@ -1625,12 +1625,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz", - "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.10.tgz", + "integrity": "sha512-6zM098J8sLy9oU60XAdzUlAH4wVzoMVsWUWiiE/Iz4fd67PplxeyL4sw/MPcVJJVhbwGGXCsHn9GrQt2mlAzig==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.80.7" + "@tanstack/query-core": "5.80.10" }, "funding": { "type": "github", @@ -1641,9 +1641,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.80.7.tgz", - "integrity": "sha512-7Dz/19fVo0i+jgLVBabV5vfGOlLyN5L1w8w1/ogFhe6ItNNsNA+ZgNTbtiKpbR3CcX2WDRRTInz1uMSmHzTsoQ==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.80.10.tgz", + "integrity": "sha512-6JL63fSc7kxyGOLV2w466SxhMn/m7LZk/ximQciy6OpVt+n2A8Mq3S0QwhIzfm4WEwLK/F3OELfzRToQburnYA==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.80.0" @@ -1653,24 +1653,24 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query": "^5.80.10", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-query-persist-client": { - "version": "5.80.7", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.80.7.tgz", - "integrity": "sha512-ZRu+rx43hiamk+oz6cY+ecaviavE+0Lt2jFjuM2zOtvl3Q4/PNHCwd3YrHafvDpJESFmhnMCZb8YfQl304S/8w==", + "version": "5.80.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.80.10.tgz", + "integrity": "sha512-0LcPhQElmffSNXh+omPOOZJnutk7Z7NMTrWl9WP3aH7Xn4OaFXImkyey1ajez5o23D8TFwj3kHmi/85J+E6sgQ==", "license": "MIT", "dependencies": { - "@tanstack/query-persist-client-core": "5.80.7" + "@tanstack/query-persist-client-core": "5.80.10" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.80.7", + "@tanstack/react-query": "^5.80.10", "react": "^18 || ^19" } }, diff --git a/package.json b/package.json index 979fe8e..6419a80 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "@mantine/form": "^8.1.1", "@mantine/hooks": "^8.1.1", "@mantine/notifications": "^8.1.1", - "@tanstack/react-query": "^5.80.7", - "@tanstack/react-query-devtools": "^5.80.7", - "@tanstack/react-query-persist-client": "^5.80.7", + "@tanstack/react-query": "^5.80.10", + "@tanstack/react-query-devtools": "^5.80.10", + "@tanstack/react-query-persist-client": "^5.80.10", "idb-keyval": "^6.2.2", "jotai": "^2.12.5", "react": "^19.1.0", diff --git a/src/hooks/useAPI.tsx b/src/hooks/useAPI.tsx index 0ff23a6..15ed1fd 100644 --- a/src/hooks/useAPI.tsx +++ b/src/hooks/useAPI.tsx @@ -224,37 +224,63 @@ export function useFileUploadAPI() { endpoint: string, body?: object | FormData | undefined | null, ) => { + const id = notifications.show({ + loading: true, + title: ( + + {method} {endpoint} + + ), + message: {'Uploading image...'}, + autoClose: false, + withCloseButton: false, + icon: , + }); const options = { method: method, - headers: headers, body: body ?? undefined, }; - const response = await fetch(url, options as unknown as Body); + const response = await fetch(`${url}/${endpoint}`, options as unknown as Body); if (!response.ok) { const responseData = await response.json(); - if (typeof responseData.detail !== 'string') { - const errorResponse = JSON.stringify({ - status: response.status, - statusText: response.statusText, - method: options.method, - endpoint: endpoint, - msg: responseData.detail[0].msg, - loc: responseData.detail[0].loc, - type: responseData.detail[0].type, - }); - - throw new Error(errorResponse); + if (typeof responseData === 'object') { + if (responseData.error.code === 'ER_DUP_ENTRY') { + setTimeout(() => { + notifications.update({ + id: id, + loading: false, + title: ( + + {method} Failure! + + ), + message: ( + + Entry with that name already exists. + + ), + autoClose: 2500, + icon: , + }); + }, 2500); + } } - const errorResponse = JSON.stringify({ - status: response.status, - statusText: response.statusText, - method: options.method, - endpoint: endpoint, - msg: responseData.detail, - }); - - throw new Error(errorResponse); + return await responseData; } + setTimeout(() => { + notifications.update({ + id: id, + loading: false, + title: ( + + {method} Success! + + ), + message: {response.statusText}, + autoClose: 2500, + icon: , + }); + }, 2500); return await response.json(); }; @@ -270,12 +296,8 @@ export function useFileUploadAPI() { body: object | FormData | undefined | null; }) => callEndpoint(method, endpoint, body), onMutate: () => {}, - onSuccess: (variables) => { - queryClient.invalidateQueries({ queryKey: ['GET', variables.endpoint] }); - if (variables.endpoint?.includes('/')) { - const GetEndpoint = variables.endpoint.split('/')[1]; - queryClient.invalidateQueries({ queryKey: ['GET', `/${GetEndpoint}`] }); - } + onSuccess: (_variables) => { + queryClient.invalidateQueries({ queryKey: ['GET', 'api/backgrounds'] }); }, }); } diff --git a/src/pages/settings/components/Theming/BackgroundModal.tsx b/src/pages/settings/components/Theming/BackgroundModal.tsx new file mode 100644 index 0000000..68593de --- /dev/null +++ b/src/pages/settings/components/Theming/BackgroundModal.tsx @@ -0,0 +1,163 @@ +import { + ActionIcon, + Button, + Card, + FileInput, + Group, + Modal, + ScrollArea, + SimpleGrid, + Text, +} from '@mantine/core'; +import { useCommonHooks } from 'hooks'; +import { useFileUploadAPI, useMutationAPI } from 'hooks/useAPI'; +import { useState } from 'react'; +import { FiTrash } from 'react-icons/fi'; +import { modalClasses } from 'styles'; + +function BackgroundModal({ + backgroundModalOpened, + handleBackgroundModalClose, + backgroundList, +}: { + backgroundModalOpened: boolean; + handleBackgroundModalClose: () => void; + backgroundList: string[]; +}) { + const [background, setBackground] = useState(null); + const [err, setErr] = useState(false); + const { isMobile } = useCommonHooks(); + const { mutateAsync: uploadAsync } = useFileUploadAPI(); + const { mutateAsync } = useMutationAPI(); + const fd = new FormData(); + const handleFileUpload = (e: any) => { + setErr(false); + setBackground(e); + }; + return ( + { + handleBackgroundModalClose(); + setBackground(null); + }} + fullScreen={isMobile} + centered + > + + + + + Backgrounds + + + + + + + {backgroundList + .filter((background) => background !== 'None') + .map((background) => ( + + + {background} + + + { + mutateAsync({ + method: 'DELETE', + endpoint: 'api/backgrounds', + body: { background }, + }); + }} + > + + + + ))} + + + + + + + + ); +} +export default BackgroundModal; diff --git a/src/pages/settings/components/Theming/ThemePlaylists.tsx b/src/pages/settings/components/Theming/ThemePlaylists.tsx index 908c811..46f5608 100644 --- a/src/pages/settings/components/Theming/ThemePlaylists.tsx +++ b/src/pages/settings/components/Theming/ThemePlaylists.tsx @@ -1,4 +1,4 @@ -import { Box, Group, Stack, Text } from '@mantine/core'; +import { Box, Group, SimpleGrid, Stack, Text } from '@mantine/core'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useEffect, useState } from 'react'; @@ -258,41 +258,43 @@ function ThemePlaylists() { mb={isMobile ? '6rem' : undefined} > {themePlaylists.length > 0 ? ( - themePlaylists - .filter((playlist: ThemePlaylist) => playlist.default === 0) - .sort((a: ThemePlaylist, b: ThemePlaylist) => - a.name > b.name ? 1 : -1, - ) - .map((cardPlaylist: ThemePlaylist) => { - return ( - - {cardPlaylist.name === currentThemePlaylist && ( - - )} - - - - - ); - }) + + {themePlaylists + .filter((playlist: ThemePlaylist) => playlist.default === 0) + .sort((a: ThemePlaylist, b: ThemePlaylist) => + a.name > b.name ? 1 : -1, + ) + .map((cardPlaylist: ThemePlaylist) => { + return ( + + {cardPlaylist.name === currentThemePlaylist && ( + + )} + + + + + ); + })} + ) : ( diff --git a/src/pages/settings/components/Theming/Themes.tsx b/src/pages/settings/components/Theming/Themes.tsx index 9d045e7..8ac2f6a 100644 --- a/src/pages/settings/components/Theming/Themes.tsx +++ b/src/pages/settings/components/Theming/Themes.tsx @@ -9,6 +9,7 @@ import { switchClasses, textClasses } from 'styles'; import type { Theme, ThemePlaylist } from 'types'; import NoItemsFound from '../Shared/NoItemsFound'; import SettingsHeader from '../Shared/SettingsHeader'; +import BackgroundModal from './BackgroundModal'; import ThemeCard from './ThemeCard'; import ThemeModal from './ThemeModal'; @@ -27,7 +28,12 @@ function ThemesSettings() { }); const [themePlaylists, setThemePlaylists] = useState([]); const { mutateAsync } = useMutationAPI(); - const [opened, { open, close }] = useDisclosure(false); + const [themeModalOpened, { open: openThemeModal, close: closeThemeModal }] = + useDisclosure(false); + const [ + backgroundModalOpened, + { open: openBackgroundModal, close: closeBackgroundModal }, + ] = useDisclosure(false); const [colorId, setColorId] = useState(''); const [colorValue, setColorValue] = useState('#000000'); const [backgroundList, setBackgroundList] = useState(['None']); @@ -57,11 +63,11 @@ function ThemesSettings() { } = useCommonHooks(); const handleOpen = (theme: Theme) => { setSelectedTheme(theme); - open(); + openThemeModal(); }; const handleClose = () => { - close(); + closeThemeModal(); closePicker(); setSelectedTheme({ id: undefined, @@ -76,6 +82,10 @@ function ThemesSettings() { }); }; + const handleBackgroundModalClose = () => { + closeBackgroundModal(); + }; + const handleSaveTheme = () => { mutateAsync({ method: selectedTheme?.id === undefined ? 'POST' : 'PATCH', @@ -112,7 +122,7 @@ function ThemesSettings() { return ( + - + +