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,
- });
- }}
- />
-
-
-
+
+
}
/>
{
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,
- });
- }}
- />
-
+
+
}
/>
(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={
-
-
-
+
+
}
/>
{
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,
- });
- }}
- />
-
-
-
- {
- if (newWolClient) {
- mutateAsync({
- method: 'POST',
- endpoint: endpoint,
- body: newWolClient,
- });
- closeModal();
- }
- }}
- >
- Save
-
- {
- closeModal();
- }}
- >
- Cancel
-
-
-
+
}
/>