Merge pull request #220 from gnmyt/features/toast-notifications

🗨️ Einführung der Toast-Nachrichten
This commit is contained in:
Mathias Wagner
2023-03-02 22:47:53 +01:00
committed by GitHub
11 changed files with 215 additions and 30 deletions
+2
View File
@@ -27,6 +27,7 @@
"dropdown": {
"settings": "Einstellungen",
"changes_applied": "Die Änderungen wurden gespeichert.",
"view_changed": "Die Ansicht wurde gewechselt.",
"changes_unsaved": "Deine Änderungen wurden nicht übernommen. Überprüfe deine Eingabe.",
"invalid": "Eingabe ungültig",
"ping": "Optimaler Ping",
@@ -163,6 +164,7 @@
"failed": "Test fehlgeschlagen",
"recheck": "Bitte überprüfe weitestgehend, ob das öfter passiert.",
"delete": "Test löschen",
"deleted": "Der Test wurde gelöscht",
"average": {
"title": "Durchschnittsgeschwindigkeit",
"description": "<Bold>{{amount}}</Bold> Tests haben ergeben, dass am <Bold>{{date}}</Bold> eine durchschnittliche Download-Geschwindigkeit von <Bold>{{down}} Mbit/s</Bold> und eine Upload-Geschwindigkeit von <Bold>{{up}} Mbit/s</Bold> bestand. Die Tests dauerten im Durchschnitt <Bold>{{duration}} Sekunden</Bold>."
+2
View File
@@ -27,6 +27,7 @@
"dropdown": {
"settings": "Settings",
"changes_applied": "Your changes have been saved.",
"view_changed": "The view has been changed.",
"changes_unsaved": "Your changes were not applied. Check your input.",
"invalid": "Input invalid",
"ping": "Optimal ping",
@@ -163,6 +164,7 @@
"failed": "Test failed",
"recheck": "Please check as far as possible if this happens often.",
"delete": "Delete Test",
"deleted": "The test has been deleted",
"average": {
"title": "Average Speed",
"description": "<Bold>{{amount}}</Bold> Tests have shown that on <Bold>{{date}}</Bold> an average download speed of <Bold>{{down}} Mbps </Bold> and an upload speed of <Bold>{{up}} Mbps</Bold> was passed. The tests took an average of <Bold>{{duration}} Seconds</Bold>."
+13 -10
View File
@@ -13,6 +13,7 @@ import Error from "@/pages/Error";
import {ViewContext, ViewProvider} from "@/common/contexts/View";
import Statistics from "@/pages/Statistics";
import {t} from "i18next";
import {ToastNotificationProvider} from "@/common/contexts/ToastNotification";
const MainContent = () => {
const [view] = useContext(ViewContext);
@@ -37,16 +38,18 @@ const App = () => {
{!translationsLoaded && !translationError && <Loading/>}
{translationError && <Error text="Failed to load translations"/>}
{translationsLoaded && !translationError && <SpeedtestProvider>
<InputDialogProvider>
<ViewProvider>
<ConfigProvider>
<StatusProvider>
<HeaderComponent/>
<MainContent/>
</StatusProvider>
</ConfigProvider>
</ViewProvider>
</InputDialogProvider>
<ToastNotificationProvider>
<InputDialogProvider>
<ViewProvider>
<ConfigProvider>
<StatusProvider>
<HeaderComponent/>
<MainContent/>
</StatusProvider>
</ConfigProvider>
</ViewProvider>
</InputDialogProvider>
</ToastNotificationProvider>
</SpeedtestProvider>}
</>
);
@@ -1,8 +1,25 @@
import React, {useContext, useEffect, useRef, useState} from "react";
import "./styles.sass";
import {
faArrowDown, faArrowUp, faCalendarDays, faCircleNodes, faClock, faClose, faFileExport, faChartSimple,
faGear, faGlobeEurope, faInfo, faKey, faPause, faPingPongPaddleBall, faPlay, faServer, faWandMagicSparkles
faArrowDown,
faArrowUp,
faCalendarDays,
faCircleNodes,
faClock,
faClose,
faFileExport,
faChartSimple,
faGear,
faGlobeEurope,
faInfo,
faKey,
faPause,
faPingPongPaddleBall,
faPlay,
faServer,
faWandMagicSparkles,
faCheck,
faExclamationTriangle
} from "@fortawesome/free-solid-svg-icons";
import {ConfigContext} from "@/common/contexts/Config";
import {StatusContext} from "@/common/contexts/Status";
@@ -18,6 +35,7 @@ import {parseCron, stringifyCron} from "@/common/components/Dropdown/utils/utils
import {changeLanguage, t} from "i18next";
import ViewDialog from "@/common/components/ViewDialog";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
let icon;
@@ -39,6 +57,7 @@ function DropdownComponent() {
const [config, reloadConfig] = useContext(ConfigContext);
const [status, updateStatus] = useContext(StatusContext);
const updateTests = useContext(SpeedtestContext)[1];
const updateToast = useContext(ToastNotificationContext);
const [setDialog] = useContext(InputDialogContext);
const [showViewDialog, setShowViewDialog] = useState(false);
const ref = useRef();
@@ -62,17 +81,19 @@ function DropdownComponent() {
return () => document.removeEventListener("mousedown", onClick);
}, []);
const showFeedback = (customText, reload = true) => setDialog({
title: "MySpeed", description: customText || t('dropdown.changes_applied'), buttonText: t('dialog.okay'),
onSuccess: () => reload ? reloadConfig() : "", onClose: () => reloadConfig()
});
const showFeedback = (customText = "dropdown.changes_applied", reload = true) => {
updateToast(t(customText), customText === "dropdown.changes_unsaved" ? "red" : "green",
customText === "dropdown.changes_unsaved" ? faExclamationTriangle : faCheck);
if (reload) reloadConfig();
}
const patchDialog = async (key, dialog, toggle = true, postValue = (val) => val) => {
toggle ? toggleDropdown() : setDialog();
setTimeout(async () => setDialog({...(await dialog(config[key])),
onSuccess: value => patchRequest(`/config/${key}`, {value: postValue(value)})
.then(res => showFeedback(!res.ok ? t("dropdown.changes_unsaved") : undefined))
.then(res => showFeedback(!res.ok ? "dropdown.changes_unsaved" : undefined))
}), 160);
}
@@ -128,7 +149,7 @@ function DropdownComponent() {
type: "password",
unsetButton: localStorage.getItem("password") != null ? "Sperre aufheben" : undefined,
onClear: () => patchRequest("/config/password", {value: "none"})
.then(() => showFeedback(t("update.password_removed"), false))
.then(() => showFeedback("update.password_removed", false))
.then(() => localStorage.removeItem("password")),
onSuccess: (value) => patchRequest("/config/password", {value})
.then(() => showFeedback(undefined, false))
@@ -209,7 +230,7 @@ function DropdownComponent() {
buttonText: t("dialog.update"),
unsetButton: !value.includes("<uuid>") ? "Deaktivieren" : undefined,
onClear: () => patchRequest("/config/healthChecksUrl", {value: "https://hc-ping.com/<uuid>"})
.then(() => showFeedback(t("update.healthchecks_activated")))
.then(() => showFeedback("update.healthchecks_activated"))
}));
const showIntegrationInfo = () => setDialog({
@@ -226,7 +247,7 @@ function DropdownComponent() {
select: true,
selectOptions: languageOptions,
value: localStorage.getItem("language") || "en",
onSuccess: value => changeLanguage(value, showFeedback())
onSuccess: value => changeLanguage(value, () => showFeedback())
});
}
@@ -1,24 +1,24 @@
import {DialogContext, DialogProvider} from "@/common/contexts/Dialog";
import {InputDialogContext} from "@/common/contexts/InputDialog";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faClose} from "@fortawesome/free-solid-svg-icons";
import {faClose, faWindowRestore} from "@fortawesome/free-solid-svg-icons";
import React, {useContext, useState} from "react";
import {ViewContext} from "@/common/contexts/View";
import ListImage from "./images/list.png";
import StatisticImage from "./images/statistic.png";
import {t} from "i18next";
import "./styles.sass";
import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
export const Dialog = () => {
const close = useContext(DialogContext);
const [setDialog] = useContext(InputDialogContext);
const updateToast = useContext(ToastNotificationContext);
const [view, setView] = useContext(ViewContext);
const [selected, setSelected] = useState(view);
const submitForm = () => {
close();
setView(selected);
setDialog({title: "MySpeed", description: t('dropdown.changes_applied'), buttonText: t('dialog.okay')});
updateToast(t('dropdown.view_changed'), "green", faWindowRestore);
}
return (
@@ -10,6 +10,7 @@ export const InputDialogContext = createContext({});
const DialogArea = ({dialog}) => {
const close = useContext(DialogContext);
const [value, setValue] = useState("");
const [error, setError] = useState(false);
useEffect(() => {
if (dialog.value) setValue(dialog.value);
@@ -37,7 +38,10 @@ const DialogArea = ({dialog}) => {
}
function submit() {
if (!dialog.description && !value) return;
if (!dialog.description && !value) {
setError(true);
return;
}
close(true);
if (dialog.onSuccess) dialog.onSuccess(value);
}
@@ -56,7 +60,8 @@ const DialogArea = ({dialog}) => {
</div>
<div className="dialog-main">
{dialog.description ? <h3 className="dialog-description">{dialog.description}</h3> : ""}
{dialog.placeholder ? <input className="dialog-input" type={dialog.type ? dialog.type : "text"}
{dialog.placeholder ? <input className={"dialog-input" + (error ? " input-error" : "")}
type={dialog.type ? dialog.type : "text"}
placeholder={dialog.placeholder} value={value}
onChange={updateValue}/> : ""}
{dialog.select ? <select value={value} onChange={updateValue} className="dialog-input">
@@ -97,8 +102,9 @@ export const InputDialogProvider = (props) => {
return (
<InputDialogContext.Provider value={[updateDialog]}>
{dialog && (
<DialogProvider close={handleClose} customClass="input-dialog" disableClosing={dialog.disableCloseButton}>
<DialogArea dialog={dialog} />
<DialogProvider close={handleClose} customClass="input-dialog"
disableClosing={dialog.disableCloseButton}>
<DialogArea dialog={dialog}/>
</DialogProvider>
)}
{props.children}
@@ -13,4 +13,7 @@
color: $darker-white
border: none
border-radius: 15px
text-align: center
text-align: center
.input-error
border: 1px solid $red
@@ -0,0 +1,46 @@
import React, {createContext, useRef, useState} from "react";
import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons";
import "./styles.sass";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
export const ToastNotificationContext = createContext({});
export const ToastNotificationProvider = (props) => {
const notificationRef = useRef();
const [timeOutId, setTimeOutId] = useState(null);
const [toastNotification, setToastNotification] = useState(null);
const updateToast = (text, color = "red", icon = faExclamationTriangle) => {
setToastNotification({text, color, icon});
if (timeOutId) {
clearTimeout(timeOutId);
setTimeOutId(null);
}
setTimeOutId(setTimeout(close, 5000));
}
const close = () => {
notificationRef.current.classList.add("toast-hidden");
}
const onAnimationEnd = (event) => {
if (event.animationName === "moveOut")
setToastNotification(null);
}
return (
<ToastNotificationContext.Provider value={updateToast}>
<div className={"toast-notification" + (toastNotification ? "" : " toast-hidden") + " toast-" + toastNotification?.color}
onAnimationEnd={onAnimationEnd} ref={notificationRef} onClick={close}>
{toastNotification && <div className="toast-content">
<FontAwesomeIcon icon={toastNotification.icon} />
<h2>{toastNotification.text}</h2>
</div>}
</div>
{props.children}
</ToastNotificationContext.Provider>
);
}
@@ -0,0 +1 @@
export * from "./ToastNotificationContext";
@@ -0,0 +1,98 @@
@import "@/common/styles/colors"
.toast-notification
position: fixed
bottom: 2rem
right: 1rem
z-index: 5
background-color: $darker-gray
box-shadow: 0 0 1rem $darker-gray
border-radius: 0.5rem
animation: 0.5s moveIn
cursor: pointer
.toast-hidden
visibility: hidden
transition: all 0s 0.5s
animation: 0.5s moveOut
.toast-green
border: 2px solid $green
& .toast-content svg
color: $green
.toast-green:hover
border-color: $green-hover
& .toast-content svg
color: $green-hover
.toast-red
border: 2px solid $red
& .toast-content svg
color: $red
.toast-red:hover
border-color: $red-hover
& .toast-content svg
color: $red-hover
.toast-content
display: flex
align-items: center
padding: 1rem 1rem
color: $white
font-size: 14px
font-weight: 500
.toast-content svg
margin-right: 1rem
width: 2rem
height: 2rem
.toast-content h2
margin: 0
font-size: 1.4rem
@keyframes moveIn
0%
transform: translateX(100%)
60%
transform: translateX(-10%)
100%
transform: translateX(0)
@keyframes moveOut
0%
transform: translateX(0)
60%
transform: translateX(-10%)
100%
transform: translateX(100%)
@media screen and (max-width: 425px)
.toast-notification
bottom: 1rem
right: 1rem
left: 1rem
@keyframes moveIn
0%
transform: translateY(100%)
60%
transform: translateY(-10%)
100%
transform: translateY(0)
@keyframes moveOut
0%
transform: translateY(0)
60%
transform: translateY(-10%)
100%
transform: translateY(100%)
@@ -2,7 +2,7 @@ import React, {useContext, useRef} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faArrowDown, faArrowUp, faClockRotateLeft, faClose,
faInfo, faPingPongPaddleBall
faInfo, faPingPongPaddleBall, faTrashCan
} from "@fortawesome/free-solid-svg-icons";
import {InputDialogContext} from "@/common/contexts/InputDialog";
import {SpeedtestContext} from "@/common/contexts/Speedtests";
@@ -13,9 +13,11 @@ import {errors} from "@/pages/Home/components/Speedtest/utils/errors";
import {tooltips} from "@/pages/Home/components/Speedtest/utils/tooltips";
import {t} from "i18next";
import {ConfigContext} from "@/common/contexts/Config";
import {ToastNotificationContext} from "@/common/contexts/ToastNotification";
function SpeedtestComponent(props) {
const [setDialog] = useContext(InputDialogContext);
const updateToast = useContext(ToastNotificationContext);
const [config] = useContext(ConfigContext);
const updateTests = useContext(SpeedtestContext)[1];
@@ -43,6 +45,7 @@ function SpeedtestComponent(props) {
const fadeOut = () => {
if (ref.current == null) return;
ref.current.classList.add("speedtest-hidden");
updateToast(t("test.deleted"), "green", faTrashCan);
setTimeout(() => updateTests(), 300);
}