refactor: apprise alert and switch component

This commit is contained in:
Ravinou 2025-01-26 13:37:13 +01:00
commit 940367e6b2
No known key found for this signature in database
GPG key ID: EEEE670C40F6A4D7
4 changed files with 157 additions and 236 deletions

View file

@ -1,157 +1,84 @@
/* Wrapper styles */
.switchWrapper {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
/* Switch container */
.switch {
display: flex;
align-items: center;
gap: 10px;
}
.switchDescription {
display: flex;
margin: 8px 0px 0px 0px;
color: #6c737f;
font-size: 0.875rem;
}
.pureMaterialSwitch {
z-index: 0;
/* Label */
.switchLabel {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
position: relative;
display: inline-block;
color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.87);
font-family: var(
--pure-material-font,
'Roboto',
'Segoe UI',
BlinkMacSystemFont,
system-ui,
-apple-system
);
font-size: 16px;
line-height: 1.5;
user-select: none;
}
/* Input */
.pureMaterialSwitch > input {
appearance: none;
-moz-appearance: none;
-webkit-appearance: none;
z-index: -1;
position: absolute;
right: 6px;
top: -8px;
display: block;
margin: 0;
border-radius: 50%;
.switchLabel input {
display: none;
}
/* Slider */
.switchSlider {
position: relative;
width: 40px;
height: 40px;
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
outline: none;
opacity: 0;
transform: scale(1);
pointer-events: none;
transition:
opacity 0.3s 0.1s,
transform 0.2s 0.1s;
height: 20px;
background: #ccc;
border-radius: 12px;
transition: #ccc 0.3s ease;
}
/* Span */
.pureMaterialSwitch > span {
display: inline-block;
width: 100%;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
color: #494b7a;
}
/* Track */
.pureMaterialSwitch > span::before {
content: '';
float: right;
display: inline-block;
margin: 5px 0 5px 30px;
border-radius: 7px;
width: 36px;
height: 14px;
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
vertical-align: top;
transition:
background-color 0.2s,
opacity 0.2s;
}
/* Thumb */
.pureMaterialSwitch > span::after {
.switchSlider::after {
content: '';
position: absolute;
top: 2px;
right: 16px;
left: 2px;
width: 16px;
height: 16px;
background: #fff;
border-radius: 50%;
width: 20px;
height: 20px;
background-color: rgb(var(--pure-material-onprimary-rgb, 255, 255, 255));
box-shadow:
0 3px 1px -2px rgba(0, 0, 0, 0.2),
0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
transition:
background-color 0.2s,
transform 0.2s;
transition: transform 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
/* Checked */
.pureMaterialSwitch > input:checked {
right: -10px;
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
/* Checked styles */
.switchLabel input:checked + .switchSlider {
background: #704dff;
}
.pureMaterialSwitch > input:checked + span::before {
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
.switchLabel input:checked + .switchSlider::after {
transform: translateX(20px);
}
.pureMaterialSwitch > input:checked + span::after {
background-color: rgb(var(--pure-material-primary-rgb, 109, 74, 255));
transform: translateX(16px);
/* Disabled styles */
.switchLabel input:disabled + .switchSlider {
background: #e0e0e0;
}
/* Active */
.pureMaterialSwitch > input:active {
opacity: 1;
transform: scale(0);
transition:
transform 0s,
opacity 0s;
.switchLabel input:disabled + .switchSlider::after {
background: #bdbdbd;
}
.pureMaterialSwitch > input:active + span::before {
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
/* Switch text */
.switchText {
font-size: 1rem;
color: #494b7a;
font-weight: 500;
}
.pureMaterialSwitch > input:checked:active + span::before {
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
/* Description */
.switchDescription {
font-size: 0.875rem;
color: #6c737f;
margin-top: 4px;
}
/* Disabled */
.pureMaterialSwitch > input:disabled + span {
cursor: wait;
}
/* .pureMaterialSwitch > input:disabled {
opacity: 0;
}
.pureMaterialSwitch > input:disabled + span {
color: rgb(var(--pure-material-onsurface-rgb, 0, 0, 0));
opacity: 0.38;
cursor: default;
}
.pureMaterialSwitch > input:disabled + span::before {
background-color: rgba(var(--pure-material-onsurface-rgb, 0, 0, 0), 0.38);
}
.pureMaterialSwitch > input:checked:disabled + span::before {
background-color: rgba(var(--pure-material-primary-rgb, 109, 74, 255), 0.6);
} */

View file

@ -1,35 +1,45 @@
//Lib
import { Optional } from '~/types';
import classes from './Switch.module.css';
import { SpinnerCircularFixed } from 'spinners-react';
type SwitchProps = {
switchName: string;
switchDescription: string;
checked: Optional<boolean>;
disabled: boolean;
loading?: boolean;
onChange: (checked: boolean) => void;
};
export default function Switch(props: SwitchProps) {
return (
<>
<div className={classes.switchWrapper}>
<div className={classes.switch}>
<label className={classes.pureMaterialSwitch}>
<input
checked={props.checked}
disabled={props.disabled}
type='checkbox'
onChange={(e) => props.onChange(e.target.checked)}
<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'
/>
<span>{props.switchName}</span>
</label>
</div>
<div className={classes.switchDescription}>
<span>{props.switchDescription}</span>
</div>
) : (
<>
<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.switchText}>{props.switchName}</span>
</label>
</div>
</>
<p className={classes.switchDescription}>{props.switchDescription}</p>
</div>
);
}

View file

@ -1,22 +1,27 @@
//Lib
import { useEffect } from 'react';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import classes from '../UserSettings.module.css';
import { useState } from 'react';
import { SpinnerCircularFixed } from 'spinners-react';
import { IconExternalLink } from '@tabler/icons-react';
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 AppriseURLs from './AppriseURLs/AppriseURLs';
import Error from '~/Components/UI/Error/Error';
import Switch from '~/Components/UI/Switch/Switch';
import { useFormStatus } from '~/hooks/useFormStatus';
import { Optional } from '~/types';
import AppriseMode from './AppriseMode/AppriseMode';
import AppriseURLs from './AppriseURLs/AppriseURLs';
type AppriseAlertDataForm = {
appriseAlert: boolean;
};
export default function AppriseAlertSettings() {
//Var
const toastOptions = {
const toastOptions: ToastOptions = {
position: 'top-right',
autoClose: 5000,
hideProgressBar: false,
@ -24,16 +29,14 @@ export default function AppriseAlertSettings() {
pauseOnHover: true,
draggable: true,
progress: undefined,
//Callback > re-enabled button after notification.
onClose: () => setDisabled(false),
};
const { error, handleError, clearError } = useFormStatus();
////State
const [checkIsLoading, setCheckIsLoading] = useState(true);
const [error, setError] = useState();
const [disabled, setDisabled] = useState(false);
const [checked, setChecked] = useState();
const [testIsLoading, setTestIsLoading] = useState(false);
const [isSendingTestNotification, setIsSendingTestNotification] = useState(false);
const [isSwitchDisabled, setIsSwitchDisabled] = useState(true);
const [isAlertEnabled, setIsAlertEnabled] = useState<Optional<boolean>>(undefined);
const [info, setInfo] = useState(false);
////LifeCycle
@ -48,12 +51,14 @@ export default function AppriseAlertSettings() {
'Content-type': 'application/json',
},
});
setChecked((await response.json()).appriseAlert);
setCheckIsLoading(false);
const data: AppriseAlertDataForm = await response.json();
setIsAlertEnabled(data.appriseAlert);
setIsSwitchDisabled(false);
} catch (error) {
setError('Fetching apprise alert setting failed. Contact your administrator.');
console.log('Fetching apprise alert setting failed.');
setCheckIsLoading(false);
setIsSwitchDisabled(true);
setIsAlertEnabled(false);
handleError('Fetching apprise alert setting failed.');
}
};
getAppriseAlert();
@ -61,11 +66,9 @@ export default function AppriseAlertSettings() {
////Functions
//Switch to enable/disable Apprise notifications
const onChangeSwitchHandler = async (data) => {
//Remove old error
setError();
//Disabled button
setDisabled(true);
const onChangeSwitchHandler = async (data: AppriseAlertDataForm) => {
clearError();
setIsSwitchDisabled(true);
await fetch('/api/account/updateAppriseAlert', {
method: 'PUT',
headers: {
@ -77,36 +80,28 @@ export default function AppriseAlertSettings() {
console.log(response);
if (response.ok) {
if (data.appriseAlert) {
setChecked(!checked);
setIsAlertEnabled(!isAlertEnabled);
setIsSwitchDisabled(false);
toast.success('Apprise notifications enabled.', toastOptions);
} else {
setChecked(!checked);
setIsAlertEnabled(!isAlertEnabled);
setIsSwitchDisabled(false);
toast.success('Apprise notifications disabled.', toastOptions);
}
} else {
setError('Update apprise alert setting failed.');
setTimeout(() => {
setError();
setDisabled(false);
}, 4000);
handleError('Update apprise alert setting failed.');
}
})
.catch((error) => {
handleError('Update Apprise failed. Contact your administrator.');
console.log(error);
setError('Update Apprise failed. Contact your administrator.');
setTimeout(() => {
setError();
setDisabled(false);
}, 4000);
});
};
//Send Apprise test notification to services
const onSendTestAppriseHandler = async () => {
//Loading
setTestIsLoading(true);
//Remove old error
setError();
clearError();
setIsSendingTestNotification(true);
try {
const response = await fetch('/api/account/sendTestApprise', {
method: 'POST',
@ -116,23 +111,21 @@ export default function AppriseAlertSettings() {
body: JSON.stringify({ sendTestApprise: true }),
});
const result = await response.json();
if (!response.ok) {
setTestIsLoading(false);
setError(result.message);
setIsSendingTestNotification(false);
handleError(result.message);
} else {
setTestIsLoading(false);
setIsSendingTestNotification(false);
setInfo(true);
setTimeout(() => {
setInfo(false);
}, 4000);
}
} catch (error) {
setTestIsLoading(false);
setIsSendingTestNotification(false);
console.log(error);
setError('Send notification failed. Contact your administrator.');
setTimeout(() => {
setError();
}, 4000);
handleError('Send notification failed. Contact your administrator.');
}
};
@ -153,51 +146,42 @@ export default function AppriseAlertSettings() {
</div>
<div className={classes.setting}>
<div className={classes.bwFormWrapper}>
{/* NOTIFY SWITCH */}
{checkIsLoading ? (
<SpinnerCircularFixed
size={30}
thickness={150}
speed={150}
color='#704dff'
secondaryColor='#c3b6fa'
/>
) : (
<Switch
checked={checked}
disabled={disabled}
switchName='Notify my Apprise services'
switchDescription='You will receive an alert on all your services every 24H if you have a down status.'
onChange={(e) => onChangeSwitchHandler({ appriseAlert: e })}
/>
)}
{/* APPRISE SERVICES URLS */}
<AppriseURLs />
{/* APPRISE MODE SELECTION */}
<AppriseMode />
{/* APPRISE TEST BUTTON */}
{testIsLoading ? (
<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>
)}
{info && (
<span style={{ marginLeft: '10px', color: '#119300' }}>
Notification successfully sent.
</span>
<Switch
loading={isAlertEnabled === undefined}
checked={isAlertEnabled}
disabled={isSwitchDisabled}
switchName='Notify my Apprise services'
switchDescription='You will receive an alert on all your services every 24H if you have a down status.'
onChange={(e) => onChangeSwitchHandler({ appriseAlert: e })}
/>
{isAlertEnabled && (
<>
<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>
)}
{info && (
<span style={{ marginLeft: '10px', color: '#119300' }}>
Notification successfully sent.
</span>
)}
</>
)}
{error && <Error message={error} />}
</div>

View file

@ -387,7 +387,7 @@
.headerFormAppriseUrls {
font-weight: 500;
color: #494b7a;
margin: 40px 0px 10px 0px;
margin-bottom: 10px;
display: flex;
padding-right: 5px;
}