mirror of
https://github.com/Ravinou/borgwarehouse
synced 2026-03-14 22:35:46 +01:00
refactor: ⚡ apprise alert and switch component
This commit is contained in:
parent
233b621bc7
commit
940367e6b2
4 changed files with 157 additions and 236 deletions
|
|
@ -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);
|
||||
} */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -387,7 +387,7 @@
|
|||
.headerFormAppriseUrls {
|
||||
font-weight: 500;
|
||||
color: #494b7a;
|
||||
margin: 40px 0px 10px 0px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue