From 799bf03cd5da68e385d76a4b8bc2acd26cf4ff1b Mon Sep 17 00:00:00 2001 From: Ravinou Date: Mon, 21 Apr 2025 10:54:10 +0200 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E2=9C=A8=20add=20a=20new=20progres?= =?UTF-8?q?s=20bar=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contexts/LoaderContext.tsx | 25 ++++++++ package-lock.json | 120 ++++++++++++++++++++++++++++++++----- package.json | 2 + pages/_app.tsx | 29 ++++++--- styles/default.css | 14 +++++ 5 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 contexts/LoaderContext.tsx diff --git a/contexts/LoaderContext.tsx b/contexts/LoaderContext.tsx new file mode 100644 index 0000000..c296054 --- /dev/null +++ b/contexts/LoaderContext.tsx @@ -0,0 +1,25 @@ +import { createContext, useContext } from 'react'; +import NProgress from 'nprogress'; + +type LoaderContextType = { + start: () => void; + stop: () => void; +}; + +const LoaderContext = createContext({ + start: () => {}, + stop: () => {}, +}); + +export const LoaderProvider = ({ children }: { children: React.ReactNode }) => { + const start = () => NProgress.start(); + const stop = () => NProgress.done(); + + return ( + + {children} + + ); +}; + +export const useLoader = () => useContext(LoaderContext); diff --git a/package-lock.json b/package-lock.json index 4d8376f..c3fb24e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "next": "^15.2.5", "next-auth": "^4.24.10", "nodemailer": "^6.10.0", + "nprogress": "^0.2.0", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", @@ -33,6 +34,7 @@ "@types/bcryptjs": "^2.4.6", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.17", + "@types/nprogress": "^0.2.3", "@types/react": "^18.3.18", "@types/supertest": "^6.0.2", "eslint-config-next": "^15.3.1", @@ -1218,6 +1220,13 @@ "@types/node": "*" } }, + "node_modules/@types/nprogress": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz", + "integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -3753,21 +3762,6 @@ "node": ">= 0.6" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5290,6 +5284,12 @@ "node": ">=6.0.0" } }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, "node_modules/oauth": { "version": "0.9.15", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", @@ -7632,6 +7632,96 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.0.tgz", + "integrity": "sha512-PDQcByT0ZfF2q7QR9d+PNj3wlNN4K6Q8JoHMwFyk252gWo4gKt7BF8Y2+KBgDjTFBETXZ/TkBEUY7NIIY7A/Kw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.0.tgz", + "integrity": "sha512-m+eO21yg80En8HJ5c49AOQpFDq+nP51nu88ZOMCorvw3g//8g1JSUsEiPSiFpJo1KCTQ+jm9H0hwXK49H/RmXg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.0.tgz", + "integrity": "sha512-H0Kk04ZNzb6Aq/G6e0un4B3HekPnyy6D+eUBYPJv9Abx8KDYgNMWzKt4Qhj57HXV3sTTjsfc1Trc1SxuhQB+Tg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.0.tgz", + "integrity": "sha512-k8GVkdMrh/+J9uIv/GpnHakzgDQhrprJ/FbGQvwWmstaeFG06nnAoZCJV+wO/bb603iKV1BXt4gHG+s2buJqZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.0.tgz", + "integrity": "sha512-a7kUbqa/k09xPjfCl0RSVAvEjAkYBYxUzSVAzk2ptXiNEL+4bDBo9wNC43G/osLA/EOGzG4CuNRFnQyIHfkRgQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz", + "integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index c5c0345..94d6012 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "next": "^15.2.5", "next-auth": "^4.24.10", "nodemailer": "^6.10.0", + "nprogress": "^0.2.0", "react": "^18.3.1", "react-chartjs-2": "^5.3.0", "react-dom": "^18.3.1", @@ -38,6 +39,7 @@ "@types/bcryptjs": "^2.4.6", "@types/node": "^22.10.2", "@types/nodemailer": "^6.4.17", + "@types/nprogress": "^0.2.3", "@types/react": "^18.3.18", "@types/supertest": "^6.0.2", "eslint-config-next": "^15.3.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index 813d076..8fde42f 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -4,22 +4,33 @@ import { ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { SessionProvider } from 'next-auth/react'; import { AppProps } from 'next/app'; +import Router from 'next/router'; +import NProgress from 'nprogress'; //Components import Layout from '../Components/UI/Layout/Layout'; +import { LoaderProvider } from '~/contexts/LoaderContext'; + +NProgress.configure({ showSpinner: false }); + +Router.events.on('routeChangeStart', () => NProgress.start()); +Router.events.on('routeChangeComplete', () => NProgress.done()); +Router.events.on('routeChangeError', () => NProgress.done()); export default function MyApp({ Component, pageProps }: AppProps) { return ( - - - - - BorgWarehouse - - - - + + + + + + BorgWarehouse + + + + + ); } diff --git a/styles/default.css b/styles/default.css index f0623b5..77f5905 100644 --- a/styles/default.css +++ b/styles/default.css @@ -20,6 +20,20 @@ body { display: none; } +#nprogress { + pointer-events: none; +} + +#nprogress .bar { + background: #6d4aff; + position: fixed; + z-index: 9999; + top: 0; + left: 0; + width: 100%; + height: 2px; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } From daa199dfb093b905d686b3083ba08e6308b182d0 Mon Sep 17 00:00:00 2001 From: Ravinou Date: Mon, 21 Apr 2025 11:57:22 +0200 Subject: [PATCH 2/7] =?UTF-8?q?refactor:=20=E2=9A=A1=20remove=20spinners?= =?UTF-8?q?=20for=20nprogress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Components/UI/Switch/Switch.tsx | 39 ++++---- Containers/RepoManage/RepoManage.tsx | 98 ++++++++++--------- .../AppriseAlertSettings.tsx | 54 ++++------ .../AppriseMode/AppriseMode.tsx | 35 +++---- .../AppriseURLs/AppriseURLs.tsx | 27 ++--- .../EmailAlertSettings/EmailAlertSettings.tsx | 31 +++--- .../EmailSettings/EmailSettings.tsx | 21 ++-- .../Integrations/Integrations.tsx | 46 ++++----- .../PasswordSettings/PasswordSettings.tsx | 42 +++----- .../UsernameSettings/UsernameSettings.tsx | 35 +++---- pages/api/v1/notif/apprise/test.ts | 2 +- pages/login.tsx | 14 +-- 12 files changed, 202 insertions(+), 242 deletions(-) diff --git a/Components/UI/Switch/Switch.tsx b/Components/UI/Switch/Switch.tsx index fe1531a..f32fa42 100644 --- a/Components/UI/Switch/Switch.tsx +++ b/Components/UI/Switch/Switch.tsx @@ -1,6 +1,7 @@ import { Optional } from '~/types'; import classes from './Switch.module.css'; -import { SpinnerCircularFixed } from 'spinners-react'; +import { useLoader } from '~/contexts/LoaderContext'; +import { useEffect } from 'react'; type SwitchProps = { switchName: string; @@ -12,29 +13,29 @@ type SwitchProps = { }; export default function Switch(props: SwitchProps) { + const { start, stop } = useLoader(); + + useEffect(() => { + if (props.loading) { + start(); + } else { + stop(); + } + }, [props.loading, start, stop]); + return (
diff --git a/Containers/RepoManage/RepoManage.tsx b/Containers/RepoManage/RepoManage.tsx index 6ee7407..6bd4f8b 100644 --- a/Containers/RepoManage/RepoManage.tsx +++ b/Containers/RepoManage/RepoManage.tsx @@ -1,15 +1,14 @@ -import classes from './RepoManage.module.css'; -import { IconAlertCircle, IconX } from '@tabler/icons-react'; -import { useState } from 'react'; +import { IconAlertCircle, IconExternalLink, IconX } from '@tabler/icons-react'; +import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import Select from 'react-select'; import { toast, ToastOptions } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; -import { useForm, Controller } from 'react-hook-form'; -import { SpinnerDotted } from 'spinners-react'; -import Select from 'react-select'; -import Link from 'next/link'; -import { IconExternalLink } from '@tabler/icons-react'; -import { alertOptions, Repository, Optional } from '~/types'; +import { useLoader } from '~/contexts/LoaderContext'; +import { alertOptions, Optional, Repository } from '~/types'; +import classes from './RepoManage.module.css'; type RepoManageProps = { mode: 'add' | 'edit'; @@ -53,20 +52,25 @@ export default function RepoManage(props: RepoManageProps) { const [deleteDialog, setDeleteDialog] = useState(false); const [isLoading, setIsLoading] = useState(false); + const { start, stop } = useLoader(); //router.query.slug is undefined for few milliseconds on first render for a direct URL access (https://github.com/vercel/next.js/discussions/11484). //If I call repoManage with edit mode (props), i'm firstly waiting that router.query.slug being available before rendering. if (props.mode === 'edit') { if (!router.query.slug) { - return ; + start(); + return; } else if (!targetRepo) { + stop(); router.push('/404'); } } //Delete a repo const deleteHandler = async (repositoryName?: string) => { + start(); if (!repositoryName) { + stop(); toast.error('Repository name not found', toastOptions); router.replace('/'); return; @@ -105,6 +109,9 @@ export default function RepoManage(props: RepoManageProps) { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); + }) + .finally(() => { + stop(); }); }; @@ -141,10 +148,11 @@ export default function RepoManage(props: RepoManageProps) { //Form submit Handler for ADD or EDIT a repo const formSubmitHandler = async (dataForm: DataForm) => { - //Loading button on submit to avoid multiple send. setIsLoading(true); + start(); //Verify that the SSH key is unique if (!(await isSSHKeyUnique(dataForm.sshkey))) { + stop(); setIsLoading(false); return; } @@ -182,6 +190,10 @@ export default function RepoManage(props: RepoManageProps) { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); + }) + .finally(() => { + stop(); + setIsLoading(false); }); //EDIT a repo } else if (props.mode == 'edit') { @@ -219,6 +231,10 @@ export default function RepoManage(props: RepoManageProps) { toast.error('An error has occurred', toastOptions); router.replace('/'); console.log(error); + }) + .finally(() => { + stop(); + setIsLoading(false); }); } }; @@ -254,24 +270,24 @@ export default function RepoManage(props: RepoManageProps) {
The data will not be recoverable and it will not be possible to go back.
- {isLoading ? ( - - ) : ( - <> - - - - )} + <> + + +
) : ( @@ -450,21 +466,15 @@ export default function RepoManage(props: RepoManageProps) { /> - {isLoading ? ( -
- -
- ) : ( - - )} + + {props.mode == 'edit' ? ( - )} + {info && ( Notification successfully sent. @@ -173,7 +162,6 @@ export default function AppriseAlertSettings() { )} )} - {error && } diff --git a/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx b/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx index b36a92d..2da7af1 100644 --- a/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx +++ b/Containers/UserSettings/AppriseAlertSettings/AppriseMode/AppriseMode.tsx @@ -1,12 +1,11 @@ -import { useEffect } from 'react'; -import classes from '../../UserSettings.module.css'; -import { useState } from 'react'; -import { SpinnerCircularFixed } from 'spinners-react'; +import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { Optional, AppriseModeEnum, AppriseModeDTO } from '~/types'; +import { AppriseModeDTO, AppriseModeEnum, Optional } from '~/types'; +import classes from '../../UserSettings.module.css'; //Components import Error from '~/Components/UI/Error/Error'; +import { useLoader } from '~/contexts/LoaderContext'; import { useFormStatus } from '~/hooks'; type AppriseModeDataForm = { @@ -19,10 +18,10 @@ export default function AppriseMode() { register, handleSubmit, formState: { errors }, - } = useForm({ mode: 'onBlur' }); + } = useForm({ mode: 'onChange' }); - const { isLoading, isSaved, error, setIsLoading, handleSuccess, handleError, clearError } = - useFormStatus(); + const { error, setIsLoading, handleSuccess, handleError, clearError } = useFormStatus(); + const { start, stop } = useLoader(); const [displayStatelessURL, setDisplayStatelessURL] = useState(false); const [appriseMode, setAppriseMode] = useState>( @@ -61,6 +60,7 @@ export default function AppriseMode() { const modeFormSubmitHandler = async (data: AppriseModeDataForm) => { clearError(); setIsLoading(true); + start(); try { const response = await fetch('/api/v1/notif/apprise/mode', { @@ -79,6 +79,9 @@ export default function AppriseMode() { } } catch (error) { handleError('The Apprise mode change has failed'); + } finally { + stop(); + setIsLoading(false); } }; @@ -87,23 +90,9 @@ export default function AppriseMode() { {/* APPRISE MODE SELECTION */}
Apprise mode
-
- {isLoading && ( - - )} - {isSaved && ( -
✅ Apprise mode has been saved.
- )} -
{error && } -
+