Merge pull request #446 from Ravinou/develop

React v19 + new loader on app
This commit is contained in:
Ravinou 2025-04-21 15:50:51 +02:00 committed by GitHub
commit 05a76a5f2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1222 additions and 477 deletions

View file

@ -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 (
<div className={classes.switchWrapper}>
<div className={classes.switch}>
<label className={classes.switchLabel}>
{props.loading ? (
<SpinnerCircularFixed
size={24}
thickness={120}
speed={100}
color='#704dff'
secondaryColor='#e0dcfc'
<>
<input
type='checkbox'
checked={props.checked || false}
disabled={props.disabled}
onChange={(e) => props.onChange(e.target.checked)}
/>
) : (
<>
<input
type='checkbox'
checked={props.checked || false}
disabled={props.disabled}
onChange={(e) => props.onChange(e.target.checked)}
/>
<span className={classes.switchSlider}></span>
</>
)}
<span className={classes.switchSlider}></span>
</>
<span className={classes.switchText}>{props.switchName}</span>
</label>
</div>

View file

@ -172,7 +172,7 @@
.selectAlertWrapper {
display: flex;
flex-direction: row;
justify-content: start;
justify-content: flex-start;
align-items: center;
margin-top: 1.5rem;
gap: 0.5rem;

View file

@ -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 <SpinnerDotted size={30} thickness={100} speed={180} color='rgba(109, 74, 255, 1)' />;
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) {
<div>The data will not be recoverable and it will not be possible to go back.</div>
</div>
<div className={classes.deleteDialogButtonWrapper}>
{isLoading ? (
<SpinnerDotted size={30} thickness={150} speed={100} color='#6d4aff' />
) : (
<>
<button onClick={props.closeHandler} className={classes.cancelButton}>
Cancel
</button>
<button
onClick={() => {
deleteHandler(targetRepo?.repositoryName);
setIsLoading(true);
}}
className={classes.deleteButton}
>
Yes, delete it !
</button>
</>
)}
<>
<button
onClick={props.closeHandler}
disabled={isLoading}
className={classes.cancelButton}
>
Cancel
</button>
<button
onClick={() => {
deleteHandler(targetRepo?.repositoryName);
setIsLoading(true);
}}
className={classes.deleteButton}
>
Yes, delete it !
</button>
</>
</div>
</div>
) : (
@ -450,21 +466,15 @@ export default function RepoManage(props: RepoManageProps) {
/>
</div>
</div>
{isLoading ? (
<div
style={{
textAlign: 'center',
marginTop: '2rem',
}}
>
<SpinnerDotted size={40} thickness={150} speed={100} color='#6d4aff' />
</div>
) : (
<button type='submit' className='defaultButton' disabled={!isValid || isSubmitting}>
{props.mode == 'edit' && 'Edit'}
{props.mode == 'add' && 'Add'}
</button>
)}
<button
type='submit'
className='defaultButton'
disabled={!isValid || isSubmitting || isLoading}
>
{props.mode == 'edit' && 'Edit'}
{props.mode == 'add' && 'Add'}
</button>
</form>
{props.mode == 'edit' ? (
<button className={classes.littleDeleteButton} onClick={() => setDeleteDialog(true)}>

View file

@ -3,13 +3,11 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { SpinnerCircularFixed } from 'spinners-react';
import classes from '../UserSettings.module.css';
//Components
import Error from '~/Components/UI/Error/Error';
import Switch from '~/Components/UI/Switch/Switch';
import { useFormStatus } from '~/hooks';
import { useLoader } from '~/contexts/LoaderContext';
import { Optional } from '~/types';
import AppriseMode from './AppriseMode/AppriseMode';
import AppriseURLs from './AppriseURLs/AppriseURLs';
@ -29,7 +27,7 @@ export default function AppriseAlertSettings() {
progress: undefined,
};
const { error, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
////State
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
@ -56,7 +54,7 @@ export default function AppriseAlertSettings() {
} catch (error) {
setIsSwitchDisabled(true);
setIsAlertEnabled(false);
handleError('Fetching apprise alert setting failed.');
toast.error('Fetching Apprise alert setting failed', toastOptions);
}
};
getAppriseAlert();
@ -65,7 +63,7 @@ export default function AppriseAlertSettings() {
////Functions
//Switch to enable/disable Apprise notifications
const onChangeSwitchHandler = async (data: AppriseAlertDataForm) => {
clearError();
start();
setIsSwitchDisabled(true);
await fetch('/api/v1/notif/apprise/alert', {
method: 'PUT',
@ -82,20 +80,21 @@ export default function AppriseAlertSettings() {
toastOptions
);
} else {
handleError('Update apprise alert setting failed.');
toast.error('Update Apprise failed', toastOptions);
}
})
.catch((error) => {
handleError('Update Apprise failed. Contact your administrator.');
.catch(() => {
toast.error('Update Apprise failed', toastOptions);
})
.finally(() => {
stop();
setIsSwitchDisabled(false);
});
};
//Send Apprise test notification to services
const onSendTestAppriseHandler = async () => {
clearError();
start();
setIsSendingTestNotification(true);
try {
const response = await fetch('/api/v1/notif/apprise/test', {
@ -104,18 +103,18 @@ export default function AppriseAlertSettings() {
const result = await response.json();
if (!response.ok) {
setIsSendingTestNotification(false);
handleError(result.message);
toast.error(result.message, toastOptions);
} else {
setIsSendingTestNotification(false);
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
} catch (error) {
toast.error('Sending test notification failed', toastOptions);
} finally {
stop();
setIsSendingTestNotification(false);
handleError('Send notification failed');
}
};
@ -148,24 +147,14 @@ export default function AppriseAlertSettings() {
<>
<AppriseURLs />
<AppriseMode />
{isSendingTestNotification ? (
<SpinnerCircularFixed
style={{ marginTop: '20px' }}
size={30}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
) : (
<button
style={{ marginTop: '20px' }}
className='defaultButton'
onClick={() => onSendTestAppriseHandler()}
>
Send a test notification
</button>
)}
<button
disabled={isSendingTestNotification}
style={{ marginTop: '20px' }}
className='defaultButton'
onClick={() => onSendTestAppriseHandler()}
>
Send a test notification
</button>
{info && (
<span style={{ marginLeft: '10px', color: '#119300' }}>
Notification successfully sent.
@ -173,7 +162,6 @@ export default function AppriseAlertSettings() {
)}
</>
)}
{error && <Error message={error} />}
</div>
</div>
</div>

View file

@ -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<AppriseModeDataForm>({ mode: 'onBlur' });
} = useForm<AppriseModeDataForm>({ 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<boolean>(false);
const [appriseMode, setAppriseMode] = useState<Optional<AppriseModeEnum>>(
@ -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 */}
<div className={classes.headerFormAppriseUrls}>
<div style={{ margin: '0px 10px 0px 0px' }}>Apprise mode</div>
<div style={{ display: 'flex' }}>
{isLoading && (
<SpinnerCircularFixed
size={18}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
)}
{isSaved && (
<div className={classes.formIsSavedMessage}> Apprise mode has been saved.</div>
)}
</div>
</div>
{error && <Error message={error} />}
<form className={classes.bwForm} onBlur={handleSubmit(modeFormSubmitHandler)}>
<form className={classes.bwForm} onChange={handleSubmit(modeFormSubmitHandler)}>
<div className='radio-group'>
<label style={{ marginRight: '50px' }}>
<div style={{ display: 'flex' }}>

View file

@ -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, AppriseServicesDTO } from '~/types';
import { AppriseServicesDTO, 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 AppriseURLsDataForm = {
@ -20,8 +19,8 @@ export default function AppriseURLs() {
formState: { errors },
} = useForm<AppriseURLsDataForm>({ mode: 'onBlur' });
const { isLoading, isSaved, error, setIsLoading, handleSuccess, handleError, clearError } =
useFormStatus();
const { isSaved, error, handleSuccess, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
const [appriseServicesList, setAppriseServicesList] = useState<Optional<string>>();
const [fetchError, setFetchError] = useState<Optional<boolean>>();
@ -53,11 +52,12 @@ export default function AppriseURLs() {
//Form submit handler to modify Apprise services
const urlsFormSubmitHandler = async (data: AppriseURLsDataForm) => {
clearError();
start();
if (fetchError) {
handleError('Cannot update Apprise services. Failed to fetch the initial list.');
stop();
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/v1/notif/apprise/services', {
@ -76,6 +76,8 @@ export default function AppriseURLs() {
}
} catch (error) {
handleError('Failed to update your Apprise services.');
} finally {
stop();
}
};
@ -85,15 +87,6 @@ export default function AppriseURLs() {
<div className={classes.headerFormAppriseUrls}>
<div style={{ marginRight: '10px' }}>Apprise URLs</div>
<div style={{ display: 'flex' }}>
{isLoading && (
<SpinnerCircularFixed
size={18}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
)}
{isSaved && (
<div className={classes.formIsSavedMessage}>
Apprise configuration has been saved.

View file

@ -3,9 +3,9 @@ import Link from 'next/link';
import { useEffect, useState } from 'react';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { SpinnerCircularFixed } from 'spinners-react';
import { useLoader } from '~/contexts/LoaderContext';
import { EmailAlertDTO, Optional } from '~/types';
import classes from '../UserSettings.module.css';
import { Optional, EmailAlertDTO } from '~/types';
//Components
import Error from '~/Components/UI/Error/Error';
@ -26,6 +26,7 @@ export default function EmailAlertSettings() {
};
const { error, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
////State
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
@ -61,6 +62,7 @@ export default function EmailAlertSettings() {
//Switch to enable/disable Email notifications
const onChangeSwitchHandler = async (data: EmailAlertDTO) => {
clearError();
start();
setIsSwitchDisabled(true);
await fetch('/api/v1/notif/email/alert', {
method: 'PUT',
@ -84,6 +86,7 @@ export default function EmailAlertSettings() {
handleError('Update email alert setting failed.');
})
.finally(() => {
stop();
setIsSwitchDisabled(false);
});
};
@ -91,6 +94,7 @@ export default function EmailAlertSettings() {
//Send a test notification by email
const onSendTestMailHandler = async () => {
clearError();
start();
setIsSendingTestNotification(true);
try {
const response = await fetch('/api/v1/notif/email/test', {
@ -114,6 +118,8 @@ export default function EmailAlertSettings() {
} catch (error) {
setIsSendingTestNotification(false);
handleError('Send notification failed');
} finally {
stop();
}
};
@ -142,19 +148,14 @@ export default function EmailAlertSettings() {
switchDescription='You will receive an alert every 24H if you have a down status.'
onChange={(e) => onChangeSwitchHandler({ emailAlert: e })}
/>
{isSendingTestNotification ? (
<SpinnerCircularFixed
size={30}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
) : (
<button className='defaultButton' onClick={onSendTestMailHandler}>
Send a test mail
</button>
)}
<button
className='defaultButton'
disabled={isSendingTestNotification}
onClick={onSendTestMailHandler}
>
Send a test mail
</button>
{info && (
<span style={{ marginLeft: '10px', color: '#119300' }}>Mail successfully sent.</span>
)}

View file

@ -1,13 +1,13 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
//Components
import Error from '~/Components/UI/Error/Error';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
import { useFormStatus } from '~/hooks';
import { EmailSettingDTO } from '~/types/api/setting.types';
@ -26,16 +26,18 @@ export default function EmailSettings(props: EmailSettingDTO) {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
formState: { errors, isSubmitting },
} = useForm<EmailSettingDTO>({ mode: 'onChange' });
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
////State
const [info, setInfo] = useState(false);
////Functions
const formSubmitHandler = async (data: EmailSettingDTO) => {
start();
clearError();
setIsLoading(true);
@ -61,6 +63,9 @@ export default function EmailSettings(props: EmailSettingDTO) {
} catch (error) {
reset();
handleError('Updating your email failed.');
} finally {
stop();
setIsLoading(false);
}
};
return (
@ -102,13 +107,9 @@ export default function EmailSettings(props: EmailSettingDTO) {
</p>
<button
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
disabled={isSubmitting || isLoading}
>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Update your email'
)}
Update your email
</button>
</form>
)}

View file

@ -1,19 +1,19 @@
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { IconExternalLink, IconTrash } from '@tabler/icons-react';
import { fromUnixTime } from 'date-fns';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import { fromUnixTime } from 'date-fns';
import { IconTrash, IconExternalLink } from '@tabler/icons-react';
import Link from 'next/link';
import { Optional, IntegrationTokenType, TokenPermissionEnum, TokenPermissionsType } from '~/types';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useFormStatus } from '~/hooks';
import { IntegrationTokenType, Optional, TokenPermissionEnum, TokenPermissionsType } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '~/Components/UI/Error/Error';
import CopyButton from '~/Components/UI/CopyButton/CopyButton';
import Error from '~/Components/UI/Error/Error';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
type IntegrationsDataForm = {
tokenName: string;
@ -36,6 +36,7 @@ export default function Integrations() {
reset,
formState: { errors, isSubmitting, isValid },
} = useForm<IntegrationsDataForm>({ mode: 'onChange' });
const { start, stop } = useLoader();
const { error, handleError, clearError, setIsLoading, isLoading } = useFormStatus();
@ -63,6 +64,7 @@ export default function Integrations() {
});
const fetchTokenList = async () => {
start();
try {
const response = await fetch('/api/v1/integration/token-manager', {
method: 'GET',
@ -74,6 +76,8 @@ export default function Integrations() {
setTokenList(data);
} catch (error) {
handleError('Fetching token list failed.');
} finally {
stop();
}
};
@ -104,6 +108,7 @@ export default function Integrations() {
//Form submit handler to ADD a new token
const formSubmitHandler = async (data: IntegrationsDataForm) => {
start();
clearError();
setIsLoading(true);
@ -123,22 +128,18 @@ export default function Integrations() {
setLastGeneratedToken({ name: data.tokenName, value: result.token });
if (!response.ok) {
setIsLoading(false);
reset();
resetPermissions();
toast.error(result.message, toastOptions);
} else {
reset();
resetPermissions();
fetchTokenList();
setIsLoading(false);
toast.success('🔑 Token generated !', toastOptions);
}
} catch (error) {
reset();
resetPermissions();
setIsLoading(false);
toast.error('Failed to generate a new token', toastOptions);
} finally {
setIsLoading(false);
resetPermissions();
reset();
stop();
}
};
@ -237,11 +238,7 @@ export default function Integrations() {
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting || hasNoPermissionSelected()}
>
{isLoading ? (
<SpinnerDotted size={15} thickness={150} speed={100} color='#fff' />
) : (
'Generate'
)}
Generate
</button>
</form>
{errors.tokenName && errors.tokenName.type === 'maxLength' && (
@ -311,9 +308,6 @@ export default function Integrations() {
disabled={isDeleteLoading}
>
Confirm
{isDeleteLoading && (
<SpinnerDotted size={15} thickness={150} speed={100} color='#fff' />
)}{' '}
</button>
{!isDeleteLoading && (
<button

View file

@ -1,13 +1,12 @@
import { useForm } from 'react-hook-form';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import { useFormStatus } from '~/hooks';
import { PasswordSettingDTO } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '~/Components/UI/Error/Error';
import { useLoader } from '~/contexts/LoaderContext';
export default function PasswordSettings() {
const toastOptions: ToastOptions = {
@ -24,14 +23,15 @@ export default function PasswordSettings() {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
formState: { isSubmitting },
} = useForm<PasswordSettingDTO>({ mode: 'onChange' });
const { start, stop } = useLoader();
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
const { isLoading, setIsLoading } = useFormStatus();
////Functions
const formSubmitHandler = async (data: PasswordSettingDTO) => {
clearError();
start();
setIsLoading(true);
try {
@ -45,18 +45,16 @@ export default function PasswordSettings() {
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
reset();
handleError(result.message);
toast.error(result.message, toastOptions);
} else {
reset();
setIsLoading(false);
toast.success('🔑 Password edited !', toastOptions);
}
} catch (error) {
toast.error('Failed to update password. Please try again.', toastOptions);
} finally {
stop();
reset();
setIsLoading(false);
handleError('Failed to update password. Please try again.');
}
};
return (
@ -69,7 +67,6 @@ export default function PasswordSettings() {
<div className={classes.setting}>
<div className={classes.bwFormWrapper}>
<form onSubmit={handleSubmit(formSubmitHandler)} className={classes.bwForm}>
{error && <Error message={error} />}
<p>
<input
type='password'
@ -78,9 +75,6 @@ export default function PasswordSettings() {
required: true,
})}
/>
{errors.oldPassword && errors.oldPassword.type === 'required' && (
<small className={classes.errorMessage}>This field is required.</small>
)}
</p>
<p>
<input
@ -90,16 +84,12 @@ export default function PasswordSettings() {
required: true,
})}
/>
{errors.newPassword && (
<small className={classes.errorMessage}>This field is required.</small>
)}
</p>
<button className={classes.AccountSettingsButton} disabled={!isValid || isSubmitting}>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Update your password'
)}
<button
className={classes.AccountSettingsButton}
disabled={isLoading || isSubmitting}
>
Update your password
</button>
</form>
</div>

View file

@ -1,15 +1,14 @@
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import { toast, ToastOptions } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useFormStatus } from '~/hooks';
import { UsernameSettingDTO } from '~/types';
import classes from '../UserSettings.module.css';
//Components
import Error from '~/Components/UI/Error/Error';
import Info from '~/Components/UI/Info/Info';
import { useLoader } from '~/contexts/LoaderContext';
export default function UsernameSettings(props: UsernameSettingDTO) {
const toastOptions: ToastOptions = {
@ -26,17 +25,18 @@ export default function UsernameSettings(props: UsernameSettingDTO) {
register,
handleSubmit,
reset,
formState: { errors, isSubmitting, isValid },
formState: { errors, isSubmitting },
} = useForm<UsernameSettingDTO>({ mode: 'onChange' });
const { start, stop } = useLoader();
const { isLoading, error, setIsLoading, handleError, clearError } = useFormStatus();
const { isLoading, setIsLoading } = useFormStatus();
////State
const [info, setInfo] = useState(false);
////Functions
const formSubmitHandler = async (data: UsernameSettingDTO) => {
clearError();
start();
setIsLoading(true);
try {
@ -50,19 +50,17 @@ export default function UsernameSettings(props: UsernameSettingDTO) {
const result = await response.json();
if (!response.ok) {
setIsLoading(false);
reset();
handleError(result.message);
toast.error(result.message, toastOptions);
} else {
reset();
setIsLoading(false);
setInfo(true);
toast.success('Username edited !', toastOptions);
}
} catch (error) {
toast.error('Failed to update username. Please try again.', toastOptions);
} finally {
reset();
stop();
setIsLoading(false);
handleError('Failed to update username. Please try again.');
}
};
return (
@ -86,7 +84,6 @@ export default function UsernameSettings(props: UsernameSettingDTO) {
className={classes.bwForm + ' ' + classes.currentSetting}
>
<p>
{error && <Error message={error} />}
<input
type='text'
placeholder={props.username}
@ -112,13 +109,9 @@ export default function UsernameSettings(props: UsernameSettingDTO) {
</p>
<button
className={classes.AccountSettingsButton}
disabled={!isValid || isSubmitting}
disabled={isLoading || isSubmitting}
>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Update your username'
)}
Update your username
</button>
</form>
)}

View file

@ -0,0 +1,25 @@
import { createContext, useContext } from 'react';
import NProgress from 'nprogress';
type LoaderContextType = {
start: () => void;
stop: () => void;
};
const LoaderContext = createContext<LoaderContextType>({
start: () => {},
stop: () => {},
});
export const LoaderProvider = ({ children }: { children: React.ReactNode }) => {
const start = () => NProgress.start();
const stop = () => NProgress.done();
return (
<LoaderContext.Provider value={{ start, stop }}>
{children}
</LoaderContext.Provider>
);
};
export const useLoader = () => useContext(LoaderContext);

1134
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,38 +13,39 @@
"format": "prettier --write \"{Components,Containers,helpers,pages,styles}/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
},
"dependencies": {
"@tabler/icons-react": "^3.30.0",
"@tabler/icons-react": "^3.31.0",
"async-mutex": "^0.5.0",
"bcryptjs": "^3.0.2",
"chart.js": "^4.4.7",
"chart.js": "^4.4.9",
"date-fns": "^4.1.0",
"lowdb": "^7.0.1",
"next": "^15.2.5",
"next": "^15.3.1",
"next-auth": "^4.24.10",
"nodemailer": "^6.10.0",
"react": "^18.3.1",
"nodemailer": "^6.10.1",
"nprogress": "^0.2.0",
"react": "^19.1.0",
"react-chartjs-2": "^5.3.0",
"react-dom": "^18.3.1",
"react-hook-form": "^7.54.2",
"react-dom": "^19.1.0",
"react-hook-form": "^7.56.0",
"react-select": "^5.10.1",
"react-toastify": "^10.0.6",
"spinners-react": "^1.0.10",
"react-toastify": "^11.0.5",
"swr": "^2.3.3",
"uuid": "^11.0.5"
"uuid": "^11.1.0"
},
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@types/bcryptjs": "^2.4.6",
"@types/node": "^22.10.2",
"@types/node": "^22.14.1",
"@types/nodemailer": "^6.4.17",
"@types/react": "^18.3.18",
"@types/supertest": "^6.0.2",
"@types/nprogress": "^0.2.3",
"@types/react": "^19.1.2",
"@types/supertest": "^6.0.3",
"eslint-config-next": "^15.3.1",
"husky": "^9.1.7",
"node-mocks-http": "^1.16.2",
"prettier": "^3.5.3",
"typescript": "^5.7.2",
"vitest": "^3.1.1"
"typescript": "^5.8.3",
"vitest": "^3.1.2"
}
}

View file

@ -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 (
<SessionProvider session={pageProps.session}>
<Layout>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1'></meta>
<link rel='shortcut icon' href='/favicon.ico' />
<title>BorgWarehouse</title>
</Head>
<ToastContainer stacked />
<Component {...pageProps} />
</Layout>
<LoaderProvider>
<Layout>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1'></meta>
<link rel='shortcut icon' href='/favicon.ico' />
<title>BorgWarehouse</title>
</Head>
<ToastContainer stacked />
<Component {...pageProps} />
</Layout>
</LoaderProvider>
</SessionProvider>
);
}

View file

@ -70,6 +70,6 @@ export default async function handler(
return res.status(200).json({ message: 'Notifications successfully sent.' });
} catch (error) {
return res.status(500).json({ message: `Error: ${error}` });
return res.status(500).json({ message: `${error}` });
}
}

View file

@ -3,7 +3,6 @@ import { signIn, useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { SpinnerDotted } from 'spinners-react';
import { useFormStatus } from '~/hooks';
import { authOptions } from '~/pages/api/auth/[...nextauth]';
@ -11,6 +10,7 @@ import { authOptions } from '~/pages/api/auth/[...nextauth]';
import { GetServerSidePropsContext } from 'next';
import Image from 'next/image';
import { ToastOptions, toast } from 'react-toastify';
import { useLoader } from '~/contexts/LoaderContext';
type LoginForm = {
username: string;
@ -19,7 +19,7 @@ type LoginForm = {
export default function Login() {
const { status } = useSession();
const { register, handleSubmit, reset, setFocus, watch } = useForm<LoginForm>();
const { register, handleSubmit, reset, setFocus } = useForm<LoginForm>();
const router = useRouter();
const toastOptions: ToastOptions = {
position: 'top-center',
@ -37,6 +37,7 @@ export default function Login() {
};
const { isLoading, setIsLoading, handleError, clearError } = useFormStatus();
const { start, stop } = useLoader();
useEffect(() => {
if (status === 'authenticated') {
@ -49,10 +50,9 @@ export default function Login() {
return;
}
const isFormComplete = watch('username') && watch('password');
//Functions
const formSubmitHandler = async (data: LoginForm) => {
start();
setIsLoading(true);
clearError();
const resultat = await signIn('credentials', {
@ -62,11 +62,13 @@ export default function Login() {
});
if (resultat?.error) {
stop();
setFocus('username');
reset();
toast.info('Incorrect credentials', toastOptions);
handleError(resultat.error);
} else {
stop();
setIsLoading(false);
router.replace('/');
}
@ -148,12 +150,8 @@ export default function Login() {
justifyContent: 'center',
}}
>
<button className='signInButton' disabled={isLoading || !isFormComplete}>
{isLoading ? (
<SpinnerDotted size={20} thickness={150} speed={100} color='#fff' />
) : (
'Sign in'
)}
<button className='signInButton' disabled={isLoading}>
Sign in
</button>
</div>
</form>

View file

@ -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;
}