mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-05-05 11:40:23 -05:00
Merge pull request #220 from gnmyt/features/toast-notifications
🗨️ Einführung der Toast-Nachrichten
This commit is contained in:
@@ -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>."
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user