diff --git a/package-lock.json b/package-lock.json index 91b1c47..629e863 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@hello-pangea/dnd": "^18.0.1", "@mantine/core": "^8.0.2", + "@mantine/form": "^8.0.2", "@mantine/hooks": "^8.0.2", "@mantine/notifications": "^8.0.2", "@tanstack/react-query": "^5.80.6", @@ -943,6 +944,19 @@ "react-dom": "^18.x || ^19.x" } }, + "node_modules/@mantine/form": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.1.0.tgz", + "integrity": "sha512-bCLdeWRbuYfG7l5b7nSKYEdWEWLOl55g/dyifRjFEr9pOK9ZNCj7oCOtXp0Lk1QJGqopZoaKxb5mvajEwDFXoQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "klona": "^2.0.6" + }, + "peerDependencies": { + "react": "^18.x || ^19.x" + } + }, "node_modules/@mantine/hooks": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.0.2.tgz", @@ -2713,8 +2727,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-equals": { "version": "5.2.2", @@ -3105,6 +3118,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", diff --git a/package.json b/package.json index 28079f2..4c1358e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@hello-pangea/dnd": "^18.0.1", "@mantine/core": "^8.0.2", + "@mantine/form": "^8.0.2", "@mantine/hooks": "^8.0.2", "@mantine/notifications": "^8.0.2", "@tanstack/react-query": "^5.80.6", diff --git a/src/hooks/useAPI.tsx b/src/hooks/useAPI.tsx index 1293b15..0ff23a6 100644 --- a/src/hooks/useAPI.tsx +++ b/src/hooks/useAPI.tsx @@ -71,36 +71,29 @@ export function useMutationAPI() { const response = await fetch(`${url}/${endpoint}`, options); 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); + } } - if (typeof responseData.detail === 'object') { - const errorResponse = JSON.stringify({ - status: response.status, - statusText: response.statusText, - method: options.method, - endpoint: endpoint, - msg: responseData.detail.message, - }); - throw new Error(errorResponse); - } - 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({ diff --git a/src/pages/homepage/components/SettingsButton/SettingsButton.tsx b/src/pages/homepage/components/SettingsButton/SettingsButton.tsx index 9f54628..64b479d 100644 --- a/src/pages/homepage/components/SettingsButton/SettingsButton.tsx +++ b/src/pages/homepage/components/SettingsButton/SettingsButton.tsx @@ -247,8 +247,15 @@ function SettingsButton() { color: 'var(--mantine-color-primary-0)', }} onClick={() => { - setCurrentThemePlaylist('All'); - handleThemeChange(allThemes, true); + const themeList = JSON.parse( + themePlaylistData?.find( + (t: ThemePlaylist) => t?.name === currentThemePlaylist, + )?.themeList ?? '[]', + ); + const filteredThemes = allThemes.filter((t: Theme) => + themeList.includes(t.id), + ); + handleThemeChange(filteredThemes, false); }} > diff --git a/src/pages/settings/components/Applications/Applications.tsx b/src/pages/settings/components/Applications/Applications.tsx index 4ad214f..81a384b 100644 --- a/src/pages/settings/components/Applications/Applications.tsx +++ b/src/pages/settings/components/Applications/Applications.tsx @@ -14,6 +14,7 @@ import { Text, TextInput, } from '@mantine/core'; +import { isNotEmpty, useForm } from '@mantine/form'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useEffect, useState } from 'react'; @@ -36,10 +37,19 @@ const ApplicationSettings = () => { url: '', icon: '', baseUrl: '', - filterName: '', + filterName: 'Default', index: 0, enabled: 1, }; + const form = useForm({ + initialValues: defaultApplication, + validate: { + name: isNotEmpty('Name is required'), + url: isNotEmpty('URL is required'), + icon: isNotEmpty('Icon is required'), + filterName: isNotEmpty('Filter name is required'), + }, + }); const appsEndpoint = 'api/applications'; const appsOrderEndpoint = 'api/applicationOrder'; const { data: applicationData, isFetching: isAppsFetching } = useQueryAPI({ @@ -48,7 +58,6 @@ const ApplicationSettings = () => { const [applications, setApplications] = useState([]); const [applicationList, setApplicationList] = useState([]); const { isMobile, reorder } = useCommonHooks(); - const [newApplication, setNewApplication] = useState(defaultApplication); const { mutateAsync } = useMutationAPI(); const [searchValue, setSearchValue] = useState(''); const [debouncedSearch] = useDebouncedValue(searchValue, 500); @@ -80,7 +89,7 @@ const ApplicationSettings = () => { const handleClose = () => { closeModal(); - setNewApplication(defaultApplication); + form.reset(); }; useEffect(() => { if (applicationData && !isAppsFetching) { @@ -116,137 +125,124 @@ const ApplicationSettings = () => { handleClose={handleClose} title={'Add a new application'} body={ - - - { - setNewApplication({ - ...newApplication, - name: event.target.value, - }); - }} - /> - { - setNewApplication({ - ...newApplication, - baseUrl: event.target.value, - }); - }} - /> - - - { - setNewApplication({ - ...newApplication, - url: event.target.value, - }); - }} - /> - - - { + return { value: icon, label: icon }; + })} + comboboxProps={{ + zIndex: 3000, + }} + renderOption={(item: ComboboxLikeRenderOptionInput) => { + const Icon = allIcons[item.option.value]; + return ( + + {item.option.value} + + + ); + }} + searchable + maxDropdownHeight={175} + onDropdownOpen={() => setIsOpen(true)} + onDropdownClose={() => setIsOpen(false)} + withAsterisk + /> + + - - - - - - + + + + + + + } /> { categoryIndex: 0, filterName: '', }; + const form = useForm({ + initialValues: defaultGroup, + validate: { + categoryName: isNotEmpty('Category name is required'), + filterName: isNotEmpty('Filter name is required'), + }, + }); const bookmarksEndpoint = 'api/bookmarks'; const { data: bookmarkData, isFetched: bookmarksIsFetched } = useQueryAPI({ endpoint: bookmarksEndpoint, @@ -27,7 +35,6 @@ const BookmarkSettings = () => { }); const [dragging, setDragging] = useState(false); const [groups, setGroups] = useState([]); - const [newGroup, setNewGroup] = useState(defaultGroup); const [groupList, setGroupList] = useState([]); const { isMobile, reorder } = useCommonHooks(); const { mutateAsync } = useMutationAPI(); @@ -37,8 +44,8 @@ const BookmarkSettings = () => { const [isLoading, setIsLoading] = useState(false); const handleClose = () => { - setNewGroup(defaultGroup); closeModal(); + form.reset(); }; function onDragEnd(result: any) { @@ -118,61 +125,60 @@ const BookmarkSettings = () => { handleClose={handleClose} title={'Add a new Bookmark Group'} body={ - - - { - setNewGroup({ - ...newGroup, - categoryName: event.target.value, - }); - }} - /> - { - setNewGroup({ - ...newGroup, - filterName: event.target.value, - }); - }} - /> - +
{ + mutateAsync({ + method: 'POST', + endpoint: categoriesEndpoint, + body: { + ...values, + categoryIndex: groups.length, + }, + }); + handleClose(); + })} + > + + + + + - - - - - - + + + + + + + } /> (bookmarks); const [isOpen, setIsOpen] = useState(false); @@ -193,7 +201,7 @@ function DebouncedGroupSettingsItem({ borderRadius: '100%', backgroundColor: 'var(--mantine-color-accent-0)', position: 'absolute', - top: isMobile ? 140 : 78, + top: isMobile ? 140 : 92, right: 18, zIndex: 10, fontSize: '1.25rem', @@ -307,99 +315,95 @@ function DebouncedGroupSettingsItem({ - - - { - setNewBookmark({ - ...newBookmark, - name: event.target.value, - }); - }} - /> - { - setNewBookmark({ - ...newBookmark, - category: event.target.value, - }); - }} - /> - - - { + mutateAsync({ + method: 'POST', + endpoint: 'api/bookmarks', + body: { ...values, index: bookmarks.length }, + }); + setPopoverOpened(false); + form.reset(); + })} + > + + + + + + + + + { - setNewBookmark({ - ...newBookmark, - url: event.target.value, - }); - }} - /> - - - + - - - + form.reset(); + }} + > + Cancel + + + + diff --git a/src/pages/settings/components/Search/SearchProviders.tsx b/src/pages/settings/components/Search/SearchProviders.tsx index aac7e0d..523a043 100644 --- a/src/pages/settings/components/Search/SearchProviders.tsx +++ b/src/pages/settings/components/Search/SearchProviders.tsx @@ -9,6 +9,7 @@ import { Stack, TextInput, } from '@mantine/core'; +import { isNotEmpty, useForm } from '@mantine/form'; import { useDebouncedValue, useDisclosure } from '@mantine/hooks'; import { useCommonHooks, useMutationAPI, useQueryAPI } from 'hooks'; import { useEffect, useState } from 'react'; @@ -27,6 +28,14 @@ const SearchProviderSettings = () => { url: '', prefix: '', }; + const form = useForm({ + initialValues: defaultProvider, + validate: { + name: isNotEmpty('Name is required'), + url: isNotEmpty('URL is required'), + prefix: isNotEmpty('Prefix is required'), + }, + }); const endpoint = 'api/providers'; const { data: providerData, isFetched: providerDataIsFetched } = useQueryAPI({ endpoint: endpoint, @@ -34,7 +43,6 @@ const SearchProviderSettings = () => { const [providers, setProviders] = useState([]); const { isMobile } = useCommonHooks(); const { mutateAsync } = useMutationAPI(); - const [newProvider, setNewProvider] = useState(defaultProvider); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [searchValue, setSearchValue] = useState(''); const [debouncedSearch] = useDebouncedValue(searchValue, 500); @@ -42,8 +50,8 @@ const SearchProviderSettings = () => { const [isLoading, setIsLoading] = useState(false); const handleClose = () => { - setNewProvider(defaultProvider); closeModal(); + form.reset(); }; useEffect(() => { @@ -70,74 +78,68 @@ const SearchProviderSettings = () => { handleClose={handleClose} title={'Add a new Search Provider'} body={ - - - +
{ + mutateAsync({ + method: 'POST', + endpoint: endpoint, + body: values, + }); + handleClose(); + })} + > + + + + + + { - setNewProvider({ - ...newProvider, - name: event.target.value, - }); - }} - /> - { - setNewProvider({ - ...newProvider, - prefix: event.target.value, - }); - }} + withAsterisk /> + + + + + - { - setNewProvider({ - ...newProvider, - url: event.target.value, - }); - }} - /> - - - - - - + } /> { hostname: '', macAddress: '', }; + const form = useForm({ + initialValues: defaultWolClient, + validate: { + hostname: isNotEmpty('Hostname is required'), + macAddress: matches( + /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/, + 'Invalid MAC Address', + ), + }, + }); const endpoint = 'api/wolClients'; const { data: wolClientsData, isFetched: wolClientsIsFetched } = useQueryAPI({ endpoint: endpoint, @@ -32,7 +43,6 @@ const WolClientSettings = () => { const [wolClients, setWolClients] = useState([]); const { mutateAsync } = useMutationAPI(); const { isMobile } = useCommonHooks(); - const [newWolClient, setNewWolClient] = useState(defaultWolClient); const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false); const [searchValue, setSearchValue] = useState(''); const [debouncedSearch] = useDebouncedValue(searchValue, 500); @@ -40,8 +50,8 @@ const WolClientSettings = () => { const [isLoading, setIsLoading] = useState(false); const handleClose = () => { - setNewWolClient(defaultWolClient); closeModal(); + form.reset(); }; useEffect(() => { @@ -64,62 +74,56 @@ const WolClientSettings = () => { handleClose={handleClose} title={'Add a new WoL Client'} body={ - - - { - setNewWolClient({ - ...newWolClient, - hostname: event.target.value, - }); - }} - /> - { - setNewWolClient({ - ...newWolClient, - macAddress: event.target.value, - }); - }} - /> - - - - - - - +
{ + mutateAsync({ + method: 'POST', + endpoint: endpoint, + body: values, + }); + handleClose(); + })} + > + + + + + + + + + + + +
} />