From 2354399117ec2c71cda90757fbf5d0230f1aa2b3 Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 1 Dec 2022 00:54:35 +0900 Subject: [PATCH 001/296] Ping module rework --- package.json | 2 ++ src/pages/api/modules/ping.ts | 31 +++++++++++++------------------ yarn.lock | 28 +++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index bef8a0f18..8f72efb94 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "next": "12.2.0", "next-i18next": "^11.3.0", "nzbget-api": "^0.0.3", + "ping": "^0.4.2", "prism-react-renderer": "^1.3.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -75,6 +76,7 @@ "@next/eslint-plugin-next": "^12.1.4", "@types/dockerode": "^3.3.9", "@types/node": "17.0.1", + "@types/ping": "^0.4.1", "@types/react": "17.0.1", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.30.7", diff --git a/src/pages/api/modules/ping.ts b/src/pages/api/modules/ping.ts index de5dcc79d..1ac40498b 100644 --- a/src/pages/api/modules/ping.ts +++ b/src/pages/api/modules/ping.ts @@ -1,26 +1,21 @@ -import axios from 'axios'; -import https from 'https'; +import ping from 'ping'; import { NextApiRequest, NextApiResponse } from 'next'; async function Get(req: NextApiRequest, res: NextApiResponse) { // Parse req.body as a ServiceItem const { url } = req.query; - const agent = new https.Agent({ rejectUnauthorized: false }); - await axios - .get(url as string, { httpsAgent: agent }) - .then((response) => { - res.status(response.status).json(response.statusText); - }) - .catch((error) => { - if (error.response) { - res.status(error.response.status).json(error.response.statusText); - } else { - res.status(500).json('Server Error'); - } - }); - // // Make a request to the URL - // const response = await axios.get(url); - // // Return the response + // Parse url as URL object + const parsedUrl = new URL(url as string); + // Ping the URL + const response = await ping.promise.probe(parsedUrl.hostname, { + timeout: 1, + }); + // Return 200 if the alive property is true + if (response.alive) { + return res.status(200).end(); + } + // Return 404 if the alive property is false + return res.status(404).end(); } export default async (req: NextApiRequest, res: NextApiResponse) => { diff --git a/yarn.lock b/yarn.lock index 87dd94f19..ea55cdc95 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2179,6 +2179,13 @@ __metadata: languageName: node linkType: hard +"@types/ping@npm:^0.4.1": + version: 0.4.1 + resolution: "@types/ping@npm:0.4.1" + checksum: 9b94837fe66df70558c5a42b0e0c8371b4950ab56b96c42c8df809ff2cf52477dd0a7e01d2e6b38af8bb6683b3dcb54587960b96b4b1f3d40fdb529aea348ad0 + languageName: node + linkType: hard + "@types/prettier@npm:^2.1.5": version: 2.6.3 resolution: "@types/prettier@npm:2.6.3" @@ -4843,6 +4850,7 @@ __metadata: "@tanstack/react-query": ^4.2.1 "@types/dockerode": ^3.3.9 "@types/node": 17.0.1 + "@types/ping": ^0.4.1 "@types/react": 17.0.1 "@types/uuid": ^8.3.4 "@typescript-eslint/eslint-plugin": ^5.30.7 @@ -4874,6 +4882,7 @@ __metadata: next: 12.2.0 next-i18next: ^11.3.0 nzbget-api: ^0.0.3 + ping: ^0.4.2 prettier: ^2.7.1 prism-react-renderer: ^1.3.5 react: ^18.2.0 @@ -6897,6 +6906,16 @@ __metadata: languageName: node linkType: hard +"ping@npm:^0.4.2": + version: 0.4.2 + resolution: "ping@npm:0.4.2" + dependencies: + q: 1.x + underscore: ^1.12.0 + checksum: 43992c76fb3294734248753f2028d9fab3b919dbfae79a5ea6df7e81fc2d6d555dd0b195d6c3dbc5c89aa9dba1cd8eb58d5ecedad103ecfee64df516e5f3665b + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.5 resolution: "pirates@npm:4.0.5" @@ -7064,7 +7083,7 @@ __metadata: languageName: node linkType: hard -"q@npm:^1.4.1": +"q@npm:1.x, q@npm:^1.4.1": version: 1.5.1 resolution: "q@npm:1.5.1" checksum: 147baa93c805bc1200ed698bdf9c72e9e42c05f96d007e33a558b5fdfd63e5ea130e99313f28efc1783e90e6bdb4e48b67a36fcc026b7b09202437ae88a1fb12 @@ -8177,6 +8196,13 @@ __metadata: languageName: node linkType: hard +"underscore@npm:^1.12.0": + version: 1.13.6 + resolution: "underscore@npm:1.13.6" + checksum: d5cedd14a9d0d91dd38c1ce6169e4455bb931f0aaf354108e47bd46d3f2da7464d49b2171a5cf786d61963204a42d01ea1332a903b7342ad428deaafaf70ec36 + languageName: node + linkType: hard + "unique-filename@npm:^1.1.1": version: 1.1.1 resolution: "unique-filename@npm:1.1.1" From b2f51495274d27da44394de4e829619cfac512ff Mon Sep 17 00:00:00 2001 From: ajnart Date: Thu, 1 Dec 2022 01:35:17 +0900 Subject: [PATCH 002/296] Ping module overall Removed useless code leftover --- .../en/layout/add-service-app-shelf.json | 6 ---- src/components/AppShelf/AddAppShelfItem.tsx | 34 ++----------------- src/modules/ping/PingModule.tsx | 4 +-- src/tools/types.ts | 22 ------------ 4 files changed, 4 insertions(+), 62 deletions(-) diff --git a/public/locales/en/layout/add-service-app-shelf.json b/public/locales/en/layout/add-service-app-shelf.json index 735110906..ee5d2676e 100644 --- a/public/locales/en/layout/add-service-app-shelf.json +++ b/public/locales/en/layout/add-service-app-shelf.json @@ -113,12 +113,6 @@ "advancedOptions": { "title": "Advanced options", "form": { - "httpStatusCodes": { - "label": "HTTP Status Codes", - "placeholder": "Select valid status codes", - "clearButtonLabel": "Clear selection", - "nothingFound": "Nothing found" - }, "openServiceInNewTab": { "label": "Open service in new tab" }, diff --git a/src/components/AppShelf/AddAppShelfItem.tsx b/src/components/AppShelf/AddAppShelfItem.tsx index 01dd3da81..3b89d1c85 100644 --- a/src/components/AppShelf/AddAppShelfItem.tsx +++ b/src/components/AppShelf/AddAppShelfItem.tsx @@ -7,7 +7,6 @@ import { Image, LoadingOverlay, Modal, - MultiSelect, PasswordInput, Select, Space, @@ -25,7 +24,7 @@ import { useTranslation } from 'next-i18next'; import { useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { useConfig } from '../../tools/state'; -import { tryMatchPort, ServiceTypeList, StatusCodes, Config } from '../../tools/types'; +import { tryMatchPort, ServiceTypeList, Config } from '../../tools/types'; import apiKeyPaths from './apiKeyPaths.json'; import Tip from '../layout/Tip'; @@ -121,7 +120,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) { password: props.password ?? undefined, openedUrl: props.openedUrl ?? undefined, ping: props.ping ?? true, - status: props.status ?? ['200'], newTab: props.newTab ?? true, }, validate: { @@ -139,12 +137,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) { } return null; }, - status: (value: string[]) => { - if (!value.length) { - return t('modal.form.validation.noStatusCodeSelected'); - } - return null; - }, }, }); @@ -190,12 +182,6 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) { if (newForm.openedUrl === '') newForm.openedUrl = undefined; if (newForm.category === null) newForm.category = undefined; if (newForm.ping === true) newForm.ping = undefined; - if ( - (newForm.status.length === 1 && newForm.status[0] === '200') || - newForm.ping === false - ) { - delete newForm.status; - } // If service already exists, update it. if (config.services && config.services.find((s) => s.id === newForm.id)) { setConfig({ @@ -451,26 +437,10 @@ export function AddAppShelfItemForm(props: AddAppShelfItemFormProps) { - {form.values.ping && ( - - )} @@ -78,7 +78,7 @@ export default function PingComponent(props: any) { } > {null} diff --git a/src/tools/types.ts b/src/tools/types.ts index 68d8d75e7..83f5b24de 100644 --- a/src/tools/types.ts +++ b/src/tools/types.ts @@ -35,28 +35,6 @@ interface ConfigModule { }; } -export const StatusCodes = [ - { value: '200', label: '200 - OK', group: 'Sucessful responses' }, - { value: '204', label: '204 - No Content', group: 'Sucessful responses' }, - { value: '301', label: '301 - Moved Permanently', group: 'Redirection responses' }, - { value: '302', label: '302 - Found / Moved Temporarily', group: 'Redirection responses' }, - { value: '304', label: '304 - Not Modified', group: 'Redirection responses' }, - { value: '307', label: '307 - Temporary Redirect', group: 'Redirection responses' }, - { value: '308', label: '308 - Permanent Redirect', group: 'Redirection responses' }, - { value: '400', label: '400 - Bad Request', group: 'Client error responses' }, - { value: '401', label: '401 - Unauthorized', group: 'Client error responses' }, - { value: '403', label: '403 - Forbidden', group: 'Client error responses' }, - { value: '404', label: '404 - Not Found', group: 'Client error responses' }, - { value: '405', label: '405 - Method Not Allowed', group: 'Client error responses' }, - { value: '408', label: '408 - Request Timeout', group: 'Client error responses' }, - { value: '410', label: '410 - Gone', group: 'Client error responses' }, - { value: '429', label: '429 - Too Many Requests', group: 'Client error responses' }, - { value: '500', label: '500 - Internal Server Error', group: 'Server error responses' }, - { value: '502', label: '502 - Bad Gateway', group: 'Server error responses' }, - { value: '503', label: '503 - Service Unavailable', group: 'Server error responses' }, - { value: '054', label: '504 - Gateway Timeout Error', group: 'Server error responses' }, -]; - export const Targets = [ { value: '_blank', label: 'New Tab' }, { value: '_top', label: 'Same Window' }, From d5a3b3f3baee1a3feabb4660186027bdc8032da8 Mon Sep 17 00:00:00 2001 From: Meierschlumpf Date: Sun, 4 Dec 2022 17:36:30 +0100 Subject: [PATCH 003/296] =?UTF-8?q?=E2=9C=A8=20Add=20new=20config=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 27 +- .../customization/page-appearance.json | 4 + .../en/settings/general/config-changer.json | 3 + .../en/settings/general/module-enabler.json | 2 +- .../en/settings/general/search-engine.json | 4 + src/components/Config/ConfigChanger.tsx | 60 +++-- src/components/Config/SaveConfig.tsx | 99 ------- src/components/Dashboard/Dashboard.tsx | 16 ++ .../Dashboard/Tiles/Service/Service.tsx | 91 +++++++ .../Dashboard/Tiles/TileWrapper.tsx | 48 ++++ src/components/Dashboard/Tiles/definition.tsx | 76 ++++++ src/components/Dashboard/Tiles/type.ts | 3 + src/components/Dashboard/Views/DetailView.tsx | 5 + src/components/Dashboard/Views/EditView.tsx | 5 + src/components/Dashboard/Views/main.tsx | 39 +++ src/components/Dashboard/Views/store.ts | 11 + .../Dashboard/Wrappers/Sidebar/Sidebar.tsx | 72 +++++ .../Wrappers/gridstack/init-gridstack.ts | 68 +++++ .../Wrappers/gridstack/use-gridstack.ts | 231 ++++++++++++++++ .../SearchNewTabSwitch/SearchNewTabSwitch.tsx | 57 ---- src/components/Settings/AdvancedSettings.tsx | 86 ------ .../Settings/AppCardWidthSelector.tsx | 34 --- src/components/Settings/ColorSelector.tsx | 93 ------- .../Settings/Common/ConfigActions.tsx | 102 +++++++ .../Common/ConfigActions/CreateCopyModal.tsx | 68 +++++ .../LanguageSelect.tsx} | 4 +- .../Common/SearchEngineEnabledSwitch.tsx | 44 +++ .../Settings/Common/SearchEngineSelector.tsx | 126 +++++++++ .../Settings/Common/SearchNewTabSwitch.tsx | 49 ++++ src/components/Settings/CommonSettings.tsx | 103 ++----- src/components/Settings/Credits.tsx | 2 +- .../Customization/BackgroundChanger.tsx | 43 +++ .../Settings/Customization/ColorSelector.tsx | 104 ++++++++ .../Customization/CustomCssChanger.tsx | 44 +++ .../Settings/Customization/FaviconChanger.tsx | 43 +++ .../Settings/Customization/LayoutSelector.tsx | 164 ++++++++++++ .../Customization/LogoImageChanger.tsx | 43 +++ .../Customization/MetaTitleChanger.tsx | 43 +++ .../Customization/OpacitySelector.tsx | 60 +++++ .../Customization/PageTitleChanger.tsx | 43 +++ .../{ => Customization}/ShadeSelector.tsx | 69 +++-- .../Settings/CustomizationSettings.tsx | 35 +++ src/components/Settings/GrowthSelector.tsx | 30 --- src/components/Settings/ModuleEnabler.tsx | 56 ---- src/components/Settings/OpacitySelector.tsx | 46 ---- src/components/Settings/SettingsMenu.tsx | 16 +- .../WidgetsPositionSwitch.tsx | 58 ---- src/components/layout/Background.tsx | 6 +- src/components/layout/Head/Head.tsx | 34 +++ .../layout/Head/SafariStatusBarStyle.tsx | 12 + src/components/layout/Layout.tsx | 49 ++-- src/components/layout/Logo.tsx | 23 +- src/components/layout/header/Header.tsx | 40 +-- src/components/layout/header/HeaderConfig.tsx | 28 -- .../layout/header/Search.tsx} | 73 ++--- .../layout/header/safariStatusBarStyle.tsx | 13 - src/components/layout/useCardStyles.ts | 22 ++ src/config/init.ts | 15 ++ src/config/provider.tsx | 45 ++++ src/config/store.ts | 35 +++ src/hooks/use-resize.ts | 23 ++ src/modules/index.ts | 1 - src/modules/search/index.ts | 1 - src/pages/_app.tsx | 20 +- src/pages/api/modules/search.ts | 19 -- src/pages/index.tsx | 22 +- src/types/area.ts | 22 ++ src/types/category.ts | 5 + src/types/config.ts | 19 ++ src/types/integration.ts | 51 ++++ src/types/service.ts | 54 ++++ src/types/settings.ts | 61 +++++ src/types/shape.ts | 10 + src/types/tile.ts | 7 + src/types/wrapper.ts | 4 + yarn.lock | 252 +++++++++--------- 76 files changed, 2461 insertions(+), 1034 deletions(-) delete mode 100644 src/components/Config/SaveConfig.tsx create mode 100644 src/components/Dashboard/Dashboard.tsx create mode 100644 src/components/Dashboard/Tiles/Service/Service.tsx create mode 100644 src/components/Dashboard/Tiles/TileWrapper.tsx create mode 100644 src/components/Dashboard/Tiles/definition.tsx create mode 100644 src/components/Dashboard/Tiles/type.ts create mode 100644 src/components/Dashboard/Views/DetailView.tsx create mode 100644 src/components/Dashboard/Views/EditView.tsx create mode 100644 src/components/Dashboard/Views/main.tsx create mode 100644 src/components/Dashboard/Views/store.ts create mode 100644 src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx create mode 100644 src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts create mode 100644 src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts delete mode 100644 src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx delete mode 100644 src/components/Settings/AdvancedSettings.tsx delete mode 100644 src/components/Settings/AppCardWidthSelector.tsx delete mode 100644 src/components/Settings/ColorSelector.tsx create mode 100644 src/components/Settings/Common/ConfigActions.tsx create mode 100644 src/components/Settings/Common/ConfigActions/CreateCopyModal.tsx rename src/components/Settings/{LanguageSwitch.tsx => Common/LanguageSelect.tsx} (96%) create mode 100644 src/components/Settings/Common/SearchEngineEnabledSwitch.tsx create mode 100644 src/components/Settings/Common/SearchEngineSelector.tsx create mode 100644 src/components/Settings/Common/SearchNewTabSwitch.tsx create mode 100644 src/components/Settings/Customization/BackgroundChanger.tsx create mode 100644 src/components/Settings/Customization/ColorSelector.tsx create mode 100644 src/components/Settings/Customization/CustomCssChanger.tsx create mode 100644 src/components/Settings/Customization/FaviconChanger.tsx create mode 100644 src/components/Settings/Customization/LayoutSelector.tsx create mode 100644 src/components/Settings/Customization/LogoImageChanger.tsx create mode 100644 src/components/Settings/Customization/MetaTitleChanger.tsx create mode 100644 src/components/Settings/Customization/OpacitySelector.tsx create mode 100644 src/components/Settings/Customization/PageTitleChanger.tsx rename src/components/Settings/{ => Customization}/ShadeSelector.tsx (54%) create mode 100644 src/components/Settings/CustomizationSettings.tsx delete mode 100644 src/components/Settings/GrowthSelector.tsx delete mode 100644 src/components/Settings/ModuleEnabler.tsx delete mode 100644 src/components/Settings/OpacitySelector.tsx delete mode 100644 src/components/WidgetsPositionSwitch/WidgetsPositionSwitch.tsx create mode 100644 src/components/layout/Head/Head.tsx create mode 100644 src/components/layout/Head/SafariStatusBarStyle.tsx delete mode 100644 src/components/layout/header/HeaderConfig.tsx rename src/{modules/search/SearchModule.tsx => components/layout/header/Search.tsx} (80%) delete mode 100644 src/components/layout/header/safariStatusBarStyle.tsx create mode 100644 src/components/layout/useCardStyles.ts create mode 100644 src/config/init.ts create mode 100644 src/config/provider.tsx create mode 100644 src/config/store.ts create mode 100644 src/hooks/use-resize.ts delete mode 100644 src/modules/search/index.ts delete mode 100644 src/pages/api/modules/search.ts create mode 100644 src/types/area.ts create mode 100644 src/types/category.ts create mode 100644 src/types/config.ts create mode 100644 src/types/integration.ts create mode 100644 src/types/service.ts create mode 100644 src/types/settings.ts create mode 100644 src/types/shape.ts create mode 100644 src/types/tile.ts create mode 100644 src/types/wrapper.ts diff --git a/package.json b/package.json index 8f72efb94..6307f33e6 100644 --- a/package.json +++ b/package.json @@ -32,27 +32,27 @@ "@dnd-kit/utilities": "^3.2.0", "@emotion/react": "^11.10.5", "@emotion/server": "^11.10.0", - "@mantine/carousel": "^5.1.0", - "@mantine/core": "^5.7.2", - "@mantine/dates": "^5.7.2", - "@mantine/dropzone": "^5.7.2", - "@mantine/form": "^5.7.2", - "@mantine/hooks": "^5.7.2", - "@mantine/modals": "^5.7.2", - "@mantine/next": "^5.2.3", - "@mantine/notifications": "^5.7.2", - "@mantine/prism": "^5.0.0", + "@mantine/carousel": "^5.9.0", + "@mantine/core": "^5.9.0", + "@mantine/dates": "^5.9.0", + "@mantine/dropzone": "^5.9.0", + "@mantine/form": "^5.9.0", + "@mantine/hooks": "^5.9.0", + "@mantine/modals": "^5.9.0", + "@mantine/next": "^5.9.0", + "@mantine/notifications": "^5.9.0", + "@mantine/prism": "^5.9.0", "@nivo/core": "^0.79.0", "@nivo/line": "^0.79.1", - "@tabler/icons": "^1.78.0", + "@tabler/icons": "^1.106.0", "@tanstack/react-query": "^4.2.1", - "add": "^2.0.6", "axios": "^0.27.2", "consola": "^2.15.3", "cookies-next": "^2.1.1", "dayjs": "^1.11.6", "dockerode": "^3.3.2", "embla-carousel-react": "^7.0.0", + "fily-publish-gridstack": "^0.0.13", "framer-motion": "^6.5.1", "i18next": "^21.9.1", "i18next-browser-languagedetector": "^6.1.5", @@ -69,7 +69,8 @@ "sharp": "^0.30.7", "systeminformation": "^5.12.1", "uuid": "^8.3.2", - "yarn": "^1.22.19" + "yarn": "^1.22.19", + "zustand": "^4.1.4" }, "devDependencies": { "@next/bundle-analyzer": "^12.1.4", diff --git a/public/locales/en/settings/customization/page-appearance.json b/public/locales/en/settings/customization/page-appearance.json index 63fa96886..2a1e0424b 100644 --- a/public/locales/en/settings/customization/page-appearance.json +++ b/public/locales/en/settings/customization/page-appearance.json @@ -1,6 +1,10 @@ { "pageTitle": { "label": "Page Title", + "placeholder": "Homarr" + }, + "metaTitle": { + "label": "Meta Title", "placeholder": "Homarr 🦞" }, "logo": { diff --git a/public/locales/en/settings/general/config-changer.json b/public/locales/en/settings/general/config-changer.json index ad4ac012d..ccd79607a 100644 --- a/public/locales/en/settings/general/config-changer.json +++ b/public/locales/en/settings/general/config-changer.json @@ -7,6 +7,9 @@ "form": { "configName": { "label": "Config name", + "validation": { + "required": "Config name is required" + }, "placeholder": "Your new config name" }, "submitButton": "Confirm" diff --git a/public/locales/en/settings/general/module-enabler.json b/public/locales/en/settings/general/module-enabler.json index 179753b6f..26c1dae88 100644 --- a/public/locales/en/settings/general/module-enabler.json +++ b/public/locales/en/settings/general/module-enabler.json @@ -1,3 +1,3 @@ { - "title": "Module enabler" + "title": "Enabled modules" } \ No newline at end of file diff --git a/public/locales/en/settings/general/search-engine.json b/public/locales/en/settings/general/search-engine.json index 8d419fcf8..fa9ae412f 100644 --- a/public/locales/en/settings/general/search-engine.json +++ b/public/locales/en/settings/general/search-engine.json @@ -5,10 +5,14 @@ "placeholderTip": "%s can be used as a placeholder for the query." }, "customEngine": { + "title": "Custom search engine", "label": "Query URL", "placeholder": "Custom query URL" }, "searchNewTab": { "label": "Open search results in new tab" + }, + "searchEnabled": { + "label": "Search enabled" } } \ No newline at end of file diff --git a/src/components/Config/ConfigChanger.tsx b/src/components/Config/ConfigChanger.tsx index 0e129bd2c..666e1e487 100644 --- a/src/components/Config/ConfigChanger.tsx +++ b/src/components/Config/ConfigChanger.tsx @@ -1,20 +1,33 @@ import { Center, Loader, Select, Tooltip } from '@mantine/core'; -import { setCookie } from 'cookies-next'; +import { useQuery } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; -import { useConfig } from '../../tools/state'; +import { useState } from 'react'; +import { useConfigContext } from '../../config/provider'; export default function ConfigChanger() { - const { config, loadConfig, setConfig, getConfigs } = useConfig(); - const [configList, setConfigList] = useState([]); - const [value, setValue] = useState(config.name); const { t } = useTranslation('settings/general/config-changer'); + const { name: configName } = useConfigContext(); + //const loadConfig = useConfigStore((x) => x.loadConfig); + + const { data: configs, isLoading, isError } = useConfigsQuery(); + const [activeConfig, setActiveConfig] = useState(configName); + + const onConfigChange = (value: string) => { + // TODO: check what should happen here with @manuel-rw + // Wheter it should check for the current url and then load the new config only on index + // Or it should always load the selected config and open index or ? + setActiveConfig(value); + /* + loadConfig(e ?? 'default'); + setCookie('config-name', e ?? 'default', { + maxAge: 60 * 60 * 24 * 30, + sameSite: 'strict', + }); + */ + }; - useEffect(() => { - getConfigs().then((configs) => setConfigList(configs)); - }, [config]); // If configlist is empty, return a loading indicator - if (configList.length === 0) { + if (isLoading || !configs || configs?.length === 0 || !configName) { return (
@@ -23,23 +36,22 @@ export default function ConfigChanger() { ); } - // return { - loadConfig(e ?? 'default'); - setCookie('config-name', e ?? 'default', { - maxAge: 60 * 60 * 24 * 30, - sameSite: 'strict', - }); - }} - data={ - // If config list is empty, return the current config - configList.length === 0 ? [config.name] : configList - } + value={activeConfig} + onChange={onConfigChange} + data={configs} /> ); } + +const useConfigsQuery = () => { + return useQuery({ + queryKey: ['config/get-all'], + queryFn: fetchConfigs, + }); +}; + +const fetchConfigs = async () => (await (await fetch('/api/configs')).json()) as string[]; diff --git a/src/components/Config/SaveConfig.tsx b/src/components/Config/SaveConfig.tsx deleted file mode 100644 index 327f50523..000000000 --- a/src/components/Config/SaveConfig.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Button, Group, Modal, TextInput } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { showNotification } from '@mantine/notifications'; -import axios from 'axios'; -import fileDownload from 'js-file-download'; -import { useState } from 'react'; -import { useTranslation } from 'next-i18next'; -import { - IconCheck as Check, - IconDownload as Download, - IconPlus as Plus, - IconTrash as Trash, - IconX as X, -} from '@tabler/icons'; -import { useConfig } from '../../tools/state'; - -export default function SaveConfigComponent(props: any) { - const [opened, setOpened] = useState(false); - const { config, setConfig } = useConfig(); - const { t } = useTranslation('settings/general/config-changer'); - const form = useForm({ - initialValues: { - configName: config.name, - }, - }); - function onClick(e: any) { - if (config) { - fileDownload(JSON.stringify(config, null, '\t'), `${config.name}.json`); - } - } - return ( - - setOpened(false)} title={t('modal.title')}> -
{ - setConfig({ ...config, name: values.configName }); - setOpened(false); - showNotification({ - title: t('modal.events.configSaved.title'), - icon: , - color: 'green', - autoClose: 1500, - radius: 'md', - message: t('modal.events.configSaved.message', { configName: values.configName }), - }); - })} - > - - - - - -
- - - -
- ); -} diff --git a/src/components/Dashboard/Dashboard.tsx b/src/components/Dashboard/Dashboard.tsx new file mode 100644 index 000000000..8bde2af95 --- /dev/null +++ b/src/components/Dashboard/Dashboard.tsx @@ -0,0 +1,16 @@ +import { DashboardDetailView } from './Views/DetailView'; +import { DashboardEditView } from './Views/EditView'; +import { useEditModeStore } from './Views/store'; + +interface DashboardProps {} + +export const Dashboard = () => { + const isEditMode = useEditModeStore((x) => x.enabled); + + return ( + <> + {/* The following elemens are splitted because gridstack doesn't reinitialize them when using same item. */} + {isEditMode ? : } + + ); +}; diff --git a/src/components/Dashboard/Tiles/Service/Service.tsx b/src/components/Dashboard/Tiles/Service/Service.tsx new file mode 100644 index 000000000..1f326aff8 --- /dev/null +++ b/src/components/Dashboard/Tiles/Service/Service.tsx @@ -0,0 +1,91 @@ +import { Card, Center, Text, UnstyledButton } from '@mantine/core'; +import { NextLink } from '@mantine/next'; +import { createStyles } from '@mantine/styles'; +import { ServiceType } from '../../../../types/service'; +import { useCardStyles } from '../../../layout/useCardStyles'; +import { useEditModeStore } from '../../Views/store'; +import { BaseTileProps } from '../type'; + +interface ServiceTileProps extends BaseTileProps { + service: ServiceType; +} + +export const ServiceTile = ({ className, service }: ServiceTileProps) => { + const isEditMode = useEditModeStore((x) => x.enabled); + + const { cx, classes } = useStyles(); + + const { + classes: { card: cardClass }, + } = useCardStyles(); + + const inner = ( + <> + + {service.name} + +
+ +
+ + ); + + return ( + + {isEditMode && + { + /**/ + }}{' '} + {/* TODO: change to serviceMenu */} + {!service.url || isEditMode ? ( + + {inner} + + ) : ( + + {inner} + + )} + {/**/} + + ); +}; + +const useStyles = createStyles((theme, _params, getRef) => { + return { + image: { + ref: getRef('image'), + maxHeight: '80%', + maxWidth: '80%', + transition: 'transform 100ms ease-in-out', + }, + serviceName: { + ref: getRef('serviceName'), + }, + button: { + height: '100%', + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + gap: 4, + }, + link: { + [`&:hover .${getRef('image')}`]: { + // TODO: add styles for image when hovering card + }, + [`&:hover .${getRef('serviceName')}`]: { + // TODO: add styles for service name when hovering card + }, + }, + }; +}); diff --git a/src/components/Dashboard/Tiles/TileWrapper.tsx b/src/components/Dashboard/Tiles/TileWrapper.tsx new file mode 100644 index 000000000..9c2a89cda --- /dev/null +++ b/src/components/Dashboard/Tiles/TileWrapper.tsx @@ -0,0 +1,48 @@ +import { ReactNode, RefObject } from 'react'; + +interface GridstackTileWrapperProps { + id: string; + type: 'service' | 'module'; + x?: number; + y?: number; + width?: number; + height?: number; + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; + itemRef: RefObject; + children: ReactNode; +} + +export const GridstackTileWrapper = ({ + id, + type, + x, + y, + width, + height, + minWidth, + minHeight, + maxWidth, + maxHeight, + children, + itemRef, +}: GridstackTileWrapperProps) => ( +
+ {children} +
+); diff --git a/src/components/Dashboard/Tiles/definition.tsx b/src/components/Dashboard/Tiles/definition.tsx new file mode 100644 index 000000000..48e249779 --- /dev/null +++ b/src/components/Dashboard/Tiles/definition.tsx @@ -0,0 +1,76 @@ +import { IntegrationsType } from '../../../types/integration'; +import { ServiceTile } from './Service/Service'; +/*import { CalendarTile } from './calendar'; +import { ClockTile } from './clock'; +import { DashDotTile } from './dash-dot'; +import { WeatherTile } from './weather';*/ + +type TileDefinitionProps = { + [key in keyof IntegrationsType | 'service']: { + minWidth?: number; + minHeight?: number; + maxWidth?: number; + maxHeight?: number; + component: React.ElementType; + }; +}; + +// TODO: change components for other modules +export const Tiles: TileDefinitionProps = { + service: { + component: ServiceTile, + minWidth: 2, + maxWidth: 12, + minHeight: 2, + maxHeight: 12, + }, + bitTorrent: { + component: CalendarTile, + minWidth: 4, + maxWidth: 12, + minHeight: 5, + maxHeight: 12, + }, + calendar: { + component: CalendarTile, + minWidth: 4, + maxWidth: 12, + minHeight: 5, + maxHeight: 12, + }, + clock: { + component: ClockTile, + minWidth: 4, + maxWidth: 12, + minHeight: 2, + maxHeight: 12, + }, + dashDot: { + component: DashDotTile, + minWidth: 4, + maxWidth: 9, + minHeight: 5, + maxHeight: 14, + }, + torrentNetworkTraffic: { + component: CalendarTile, + minWidth: 4, + maxWidth: 12, + minHeight: 5, + maxHeight: 12, + }, + useNet: { + component: CalendarTile, + minWidth: 4, + maxWidth: 12, + minHeight: 5, + maxHeight: 12, + }, + weather: { + component: WeatherTile, + minWidth: 4, + maxWidth: 12, + minHeight: 2, + maxHeight: 12, + }, +}; diff --git a/src/components/Dashboard/Tiles/type.ts b/src/components/Dashboard/Tiles/type.ts new file mode 100644 index 000000000..e5e860af9 --- /dev/null +++ b/src/components/Dashboard/Tiles/type.ts @@ -0,0 +1,3 @@ +export interface BaseTileProps { + className?: string; +} diff --git a/src/components/Dashboard/Views/DetailView.tsx b/src/components/Dashboard/Views/DetailView.tsx new file mode 100644 index 000000000..ff7f93168 --- /dev/null +++ b/src/components/Dashboard/Views/DetailView.tsx @@ -0,0 +1,5 @@ +import { DashboardView } from './main'; + +export const DashboardDetailView = () => { + return ; +}; diff --git a/src/components/Dashboard/Views/EditView.tsx b/src/components/Dashboard/Views/EditView.tsx new file mode 100644 index 000000000..1d9689152 --- /dev/null +++ b/src/components/Dashboard/Views/EditView.tsx @@ -0,0 +1,5 @@ +import { DashboardView } from './main'; + +export const DashboardEditView = () => { + return ; +}; diff --git a/src/components/Dashboard/Views/main.tsx b/src/components/Dashboard/Views/main.tsx new file mode 100644 index 000000000..ea327713c --- /dev/null +++ b/src/components/Dashboard/Views/main.tsx @@ -0,0 +1,39 @@ +import { Group, Stack } from '@mantine/core'; +import { useMemo } from 'react'; +import { useConfigContext } from '../../../config/provider'; +import { ServiceTile } from '../Tiles/Service/Service'; +import { DashboardSidebar } from '../Wrappers/Sidebar/Sidebar'; + +export const DashboardView = () => { + const wrappers = useWrapperItems(); + + return ( + + {/**/} + + {wrappers.map( + (item) => + item.type === 'category' + ? 'category' // + : 'wrapper' // + )} + + {/**/} + + ); +}; + +const useWrapperItems = () => { + const { config } = useConfigContext(); + + return useMemo( + () => + config + ? [ + ...config.categories.map((c) => ({ ...c, type: 'category' })), + ...config.wrappers.map((w) => ({ ...w, type: 'wrapper' })), + ].sort((a, b) => a.position - b.position) + : [], + [config?.categories, config?.wrappers] + ); +}; diff --git a/src/components/Dashboard/Views/store.ts b/src/components/Dashboard/Views/store.ts new file mode 100644 index 000000000..5c9fbafc2 --- /dev/null +++ b/src/components/Dashboard/Views/store.ts @@ -0,0 +1,11 @@ +import create from 'zustand'; + +interface EditModeState { + enabled: boolean; + toggleEditMode: () => void; +} + +export const useEditModeStore = create((set) => ({ + enabled: false, + toggleEditMode: () => set((state) => ({ enabled: !state.enabled })), +})); diff --git a/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx new file mode 100644 index 000000000..fc11cc99f --- /dev/null +++ b/src/components/Dashboard/Wrappers/Sidebar/Sidebar.tsx @@ -0,0 +1,72 @@ +import { Card } from '@mantine/core'; +import { RefObject } from 'react'; +import { Tiles } from '../../Tiles/definition'; +import { GridstackTileWrapper } from '../../Tiles/TileWrapper'; +import { useGridstack } from '../gridstack/use-gridstack'; + +interface DashboardSidebarProps { + location: 'right' | 'left'; +} + +export const DashboardSidebar = ({ location }: DashboardSidebarProps) => { + const { refs, items, integrations } = useGridstack('sidebar', location); + + const minRow = useMinRowForFullHeight(refs.wrapper); + + return ( + +
+ {items.map((service) => { + const { component: TileComponent, ...tile } = Tiles['service']; + return ( + + + + ); + })} + {Object.entries(integrations).map(([k, v]) => { + const { component: TileComponent, ...tile } = Tiles[k as keyof typeof Tiles]; + + return ( + + + + ); + })} +
+
+ ); +}; + +const useMinRowForFullHeight = (wrapperRef: RefObject) => { + return wrapperRef.current ? Math.floor(wrapperRef.current!.offsetHeight / 64) : 2; +}; diff --git a/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts new file mode 100644 index 000000000..646562ba4 --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/init-gridstack.ts @@ -0,0 +1,68 @@ +import { GridStack, GridStackNode } from 'fily-publish-gridstack'; +import { MutableRefObject, RefObject } from 'react'; +import { IntegrationsType } from '../../../../types/integration'; +import { ServiceType } from '../../../../types/service'; + +export const initializeGridstack = ( + areaType: 'wrapper' | 'category' | 'sidebar', + wrapperRef: RefObject, + gridRef: MutableRefObject, + itemRefs: MutableRefObject>>, + areaId: string, + items: ServiceType[], + integrations: IntegrationsType, + isEditMode: boolean, + events: { + onChange: (changedNode: GridStackNode) => void; + onAdd: (addedNode: GridStackNode) => void; + } +) => { + if (!wrapperRef.current) return; + // calculates the currently available count of columns + const columnCount = areaType === 'sidebar' ? 4 : Math.floor(wrapperRef.current.offsetWidth / 64); + const minRow = areaType !== 'sidebar' ? 1 : Math.floor(wrapperRef.current.offsetHeight / 64); + // initialize gridstack + gridRef.current = GridStack.init( + { + column: columnCount, + margin: 10, + cellHeight: 64, + float: true, + alwaysShowResizeHandle: 'mobile', + acceptWidgets: true, + disableOneColumnMode: true, + staticGrid: !isEditMode, + minRow, + }, + // selector of the gridstack item (it's eather category or wrapper) + `.grid-stack-${areaType}[data-${areaType}='${areaId}']` + ); + const grid = gridRef.current; + + // Add listener for moving items around in a wrapper + grid.on('change', (_, el) => { + const nodes = el as GridStackNode[]; + const firstNode = nodes.at(0); + if (!firstNode) return; + events.onChange(firstNode); + }); + + // Add listener for moving items in config from one wrapper to another + grid.on('added', (_, el) => { + const nodes = el as GridStackNode[]; + const firstNode = nodes.at(0); + if (!firstNode) return; + events.onAdd(firstNode); + }); + grid.batchUpdate(); + grid.removeAll(false); + items.forEach( + ({ id }) => + itemRefs.current[id] && grid.makeWidget(itemRefs.current[id].current as HTMLDivElement) + ); + Object.keys(integrations).forEach( + (key) => + itemRefs.current[key] && grid.makeWidget(itemRefs.current[key].current as HTMLDivElement) + ); + grid.batchUpdate(false); +}; diff --git a/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts new file mode 100644 index 000000000..5f75d1adb --- /dev/null +++ b/src/components/Dashboard/Wrappers/gridstack/use-gridstack.ts @@ -0,0 +1,231 @@ +import { GridStack, GridStackNode } from 'fily-publish-gridstack'; +import { + createRef, + LegacyRef, + MutableRefObject, + RefObject, + useEffect, + useLayoutEffect, + useMemo, + useRef, +} from 'react'; +import { useConfigContext } from '../../../../config/provider'; +import { useConfigStore } from '../../../../config/store'; +import { useResize } from '../../../../hooks/use-resize'; +import { IntegrationsType } from '../../../../types/integration'; +import { ServiceType } from '../../../../types/service'; +import { TileBaseType } from '../../../../types/tile'; +import { useEditModeStore } from '../../Views/store'; +import { initializeGridstack } from './init-gridstack'; + +interface UseGristackReturnType { + items: ServiceType[]; + integrations: Partial; + refs: { + wrapper: RefObject; + items: MutableRefObject>>; + gridstack: MutableRefObject; + }; +} + +export const useGridstack = ( + areaType: 'wrapper' | 'category' | 'sidebar', + areaId: string +): UseGristackReturnType => { + const isEditMode = useEditModeStore((x) => x.enabled); + const { config, name: configName } = useConfigContext(); + const updateConfig = useConfigStore((x) => x.updateConfig); + // define reference for wrapper - is used to calculate the width of the wrapper + const wrapperRef = useRef(null); + // references to the diffrent items contained in the gridstack + const itemRefs = useRef>>({}); + // reference of the gridstack object for modifications after initialization + const gridRef = useRef(); + // width of the wrapper (updating on page resize) + const { width, height } = useResize(wrapperRef); + + const items = useMemo( + () => + config?.services.filter( + (x) => + x.area.type === areaType && + (x.area.type === 'sidebar' + ? x.area.properties.location === areaId + : x.area.properties.id === areaId) + ) ?? [], + [config] + ); + const integrations = useMemo(() => { + if (!config) return; + return (Object.entries(config.integrations) as [keyof IntegrationsType, TileBaseType][]) + .filter( + ([k, v]) => + v.area.type === areaType && + (v.area.type === 'sidebar' + ? v.area.properties.location === areaId + : v.area.properties.id === areaId) + ) + .reduce((prev, [k, v]) => { + prev[k] = v as unknown as any; + return prev; + }, {} as IntegrationsType); + }, [config]); + + // define items in itemRefs for easy access and reference to items + if ( + Object.keys(itemRefs.current).length !== + items.length + Object.keys(integrations ?? {}).length + ) { + items.forEach(({ id }: { id: keyof typeof itemRefs.current }) => { + itemRefs.current[id] = itemRefs.current[id] || createRef(); + }); + Object.keys(integrations ?? {}).forEach((k) => { + itemRefs.current[k] = itemRefs.current[k] || createRef(); + }); + } + + // change column count depending on the width and the gridRef + useEffect(() => { + if (areaType === 'sidebar') return; + gridRef.current?.column(Math.floor(width / 64), 'moveScale'); + }, [gridRef, width]); + + const onChange = isEditMode + ? (changedNode: GridStackNode) => { + if (!configName) return; + + const itemType = changedNode.el?.getAttribute('data-type'); + const itemId = changedNode.el?.getAttribute('data-id'); + if (!itemType || !itemId) return; + + // Updates the config and defines the new position of the item + updateConfig(configName, (previous) => { + const currentItem = + itemType === 'service' + ? previous.services.find((x) => x.id === itemId) + : previous.integrations[itemId as keyof typeof previous.integrations]; + if (!currentItem) return previous; + + currentItem.shape = { + location: { + x: changedNode.x ?? currentItem.shape.location.x, + y: changedNode.y ?? currentItem.shape.location.y, + }, + size: { + width: changedNode.w ?? currentItem.shape.size.width, + height: changedNode.h ?? currentItem.shape.size.height, + }, + }; + + if (itemType === 'service') { + return { + ...previous, + services: [ + ...previous.services.filter((x) => x.id !== itemId), + { ...(currentItem as ServiceType) }, + ], + }; + } + + const integrationsCopy = { ...previous.integrations }; + integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any; + return { + ...previous, + integrations: integrationsCopy, + }; + }); + } + : () => {}; + + const onAdd = isEditMode + ? (addedNode: GridStackNode) => { + if (!configName) return; + + const itemType = addedNode.el?.getAttribute('data-type'); + const itemId = addedNode.el?.getAttribute('data-id'); + if (!itemType || !itemId) return; + + // Updates the config and defines the new position and wrapper of the item + updateConfig(configName, (previous) => { + const currentItem = + itemType === 'service' + ? previous.services.find((x) => x.id === itemId) + : previous.integrations[itemId as keyof typeof previous.integrations]; + + if (!currentItem) return previous; + + if (areaType === 'sidebar') { + currentItem.area = { + type: areaType, + properties: { + location: areaId as 'right' | 'left', + }, + }; + } else { + currentItem.area = { + type: areaType, + properties: { + id: areaId, + }, + }; + } + + currentItem.shape = { + location: { + x: addedNode.x ?? currentItem.shape.location.x, + y: addedNode.y ?? currentItem.shape.location.y, + }, + size: { + width: addedNode.w ?? currentItem.shape.size.width, + height: addedNode.h ?? currentItem.shape.size.height, + }, + }; + + if (itemType === 'service') { + return { + ...previous, + services: [ + ...previous.services.filter((x) => x.id !== itemId), + { ...(currentItem as ServiceType) }, + ], + }; + } + + const integrationsCopy = { ...previous.integrations }; + integrationsCopy[itemId as keyof typeof integrationsCopy] = currentItem as any; + return { + ...previous, + integrations: integrationsCopy, + }; + }); + } + : () => {}; + + // initialize the gridstack + useLayoutEffect(() => { + initializeGridstack( + areaType, + wrapperRef, + gridRef, + itemRefs, + areaId, + items, + integrations ?? {}, + isEditMode, + { + onChange, + onAdd, + } + ); + }, [items.length, wrapperRef.current, Object.keys(integrations ?? {}).length]); + + return { + items, + integrations: integrations ?? {}, + refs: { + items: itemRefs, + wrapper: wrapperRef, + gridstack: gridRef, + }, + }; +}; diff --git a/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx b/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx deleted file mode 100644 index 00ebb88f4..000000000 --- a/src/components/SearchNewTabSwitch/SearchNewTabSwitch.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React, { useState } from 'react'; -import { createStyles, Switch, Group } from '@mantine/core'; -import { useTranslation } from 'next-i18next'; -import { useConfig } from '../../tools/state'; - -const useStyles = createStyles((theme) => ({ - root: { - position: 'relative', - '& *': { - cursor: 'pointer', - }, - }, - - icon: { - pointerEvents: 'none', - position: 'absolute', - zIndex: 1, - top: 3, - }, - - iconLight: { - left: 4, - color: theme.white, - }, - - iconDark: { - right: 4, - color: theme.colors.gray[6], - }, -})); - -export function SearchNewTabSwitch() { - const { config, setConfig } = useConfig(); - const { classes, cx } = useStyles(); - const defaultPosition = config?.settings?.searchNewTab ?? true; - const [openInNewTab, setOpenInNewTab] = useState(defaultPosition); - const { t } = useTranslation('settings/general/search-engine'); - const toggleOpenInNewTab = () => { - setOpenInNewTab(!openInNewTab); - setConfig({ - ...config, - settings: { - ...config.settings, - searchNewTab: !openInNewTab, - }, - }); - }; - - return ( - -
- toggleOpenInNewTab()} size="md" /> -
- {t('searchNewTab.label')} -
- ); -} diff --git a/src/components/Settings/AdvancedSettings.tsx b/src/components/Settings/AdvancedSettings.tsx deleted file mode 100644 index 3d5b7bd96..000000000 --- a/src/components/Settings/AdvancedSettings.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { TextInput, Button, Stack, Textarea } from '@mantine/core'; -import { useForm } from '@mantine/form'; -import { useTranslation } from 'next-i18next'; -import { useConfig } from '../../tools/state'; -import { ColorSelector } from './ColorSelector'; -import { OpacitySelector } from './OpacitySelector'; -import { AppCardWidthSelector } from './AppCardWidthSelector'; -import { ShadeSelector } from './ShadeSelector'; -import { GrowthSelector } from './GrowthSelector'; - -export default function TitleChanger() { - const { config, setConfig } = useConfig(); - const { t } = useTranslation('settings/customization/page-appearance'); - - const form = useForm({ - initialValues: { - title: config.settings.title, - logo: config.settings.logo, - favicon: config.settings.favicon, - background: config.settings.background, - customCSS: config.settings.customCSS, - }, - }); - - const saveChanges = (values: { - title?: string; - logo?: string; - favicon?: string; - background?: string; - customCSS?: string; - }) => { - setConfig({ - ...config, - settings: { - ...config.settings, - title: values.title, - logo: values.logo, - favicon: values.favicon, - background: values.background, - customCSS: values.customCSS, - }, - }); - }; - - return ( - -
saveChanges(values))}> - - - - - -