Merge pull request #680 from gnmyt/updates/welcome-dialog

👋 Willkommensdialog hinzugefügt
This commit is contained in:
Mathias Wagner
2024-05-20 01:02:29 +02:00
committed by GitHub
20 changed files with 409 additions and 3 deletions

View File

@@ -1,8 +1,22 @@
{
"failed": "Failed",
"welcome": {
"step": "Step",
"title": "Welcome to MySpeed!",
"subtext": "Let's set up MySpeed for the first time. Don't worry, it won't take long.",
"provider_title": "Choose a provider",
"provider_subtext": "Select the provider you want to use here. This provider will then perform your tests.",
"data_title": "Set optimal data",
"data_subtext": "Select your contractually agreed speeds in this step",
"ms": "(in ms)",
"mbps": "(in Mbps)",
"accept_title": "Accept the terms",
"accept_subtext": "In this step, we would like to inform you about the Ookla license. Read it and confirm that you have read and accept it by clicking on “Done”."
},
"dialog": {
"okay": "Okay",
"done": "Done",
"continue": "Continue",
"apply": "Yes, apply",
"update": "Update",
"close": "Close",

View File

@@ -11,7 +11,7 @@ import {jsonRequest, patchRequest} from "@/common/utils/RequestUtil";
import {Trans} from "react-i18next";
import {ConfigContext} from "@/common/contexts/Config";
const providers = [
export const providers = [
{id: "ookla", name: "Ookla", image: OoklaImage},
{id: "libre", name: "LibreSpeed", image: LibreImage},
{id: "cloudflare", name: "Cloudflare", image: CloudflareImage}

View File

@@ -0,0 +1,82 @@
import { DialogContext, DialogProvider } from "@/common/contexts/Dialog";
import "./styles.sass";
import { useContext, useState } from "react";
import Greetings from "./steps/Greetings";
import ProviderChooser from "./steps/ProviderChooser";
import DataHelper from "./steps/DataHelper";
import OoklaLicense from "./steps/OoklaLicense";
import {patchRequest} from "@/common/utils/RequestUtil";
import {ConfigContext} from "@/common/contexts/Config";
import {t} from "i18next";
export const Dialog = () => {
const close = useContext(DialogContext);
const [config, reloadConfig] = useContext(ConfigContext);
const [step, setStep] = useState(1);
const [provider, setProvider] = useState("ookla");
const [ping, setPing] = useState(25);
const [download, setDownload] = useState(100);
const [upload, setUpload] = useState(50);
const [animating, setAnimating] = useState(false);
const finish = async () => {
await patchRequest("/config/provider", {value: provider});
if (config.previewMode) {
localStorage.setItem("welcomeShown", "true");
} else {
await patchRequest("/config/ping", {value: ping});
await patchRequest("/config/download", {value: download});
await patchRequest("/config/upload", {value: upload});
}
reloadConfig();
close(true);
}
const continueStep = () => {
if (step === (provider === "ookla" ? 4 : 3)) {
finish();
} else {
setAnimating(true);
setStep(step + 1);
setTimeout(() => {
setAnimating(false);
}, 500);
}
}
return (
<>
<div className="welcome-banner">
<div className={`welcome-inner ${animating ? 'slide-in' : ''}`}>
{step === 1 && <Greetings />}
{step === 2 && <ProviderChooser provider={provider} setProvider={setProvider} />}
{step === 3 && <DataHelper ping={ping} setPing={setPing} download={download}
setDownload={setDownload} upload={upload} setUpload={setUpload} />}
{step === 4 && provider === "ookla" && <OoklaLicense />}
</div>
<div className="welcome-actions">
<h3>{t("welcome.step")} {step}/{provider === "ookla" ? 4 : 3}</h3>
<button className="dialog-btn" onClick={continueStep}>
{step === (provider === "ookla" ? 4 : 3) ? t("dialog.done") : t("dialog.continue")}
</button>
</div>
</div>
</>
)
}
export const WelcomeDialog = (props) => {
return (
<>
<DialogProvider close={props.onClose} disableClosing={true}>
<Dialog />
</DialogProvider>
</>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
export {WelcomeDialog as default} from "./WelcomeDialog";

View File

@@ -0,0 +1,49 @@
import "./styles.sass";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faArrowDown, faArrowUp, faTableTennis} from "@fortawesome/free-solid-svg-icons";
import {t} from "i18next";
export const DataHelper = ({setDownload, download, ping, setPing, upload, setUpload}) => {
return (
<div className="data-helper">
<h2>{t("welcome.data_title")}</h2>
<p>{t("welcome.data_subtext")}</p>
<div className="speeds">
<div className="speed">
<div className="speed-header">
<FontAwesomeIcon icon={faTableTennis}/>
<div className="speed-text">
<h2>{t("latest.ping")}</h2>
<p>{t("welcome.ms")}</p>
</div>
</div>
<input type="number" placeholder={t("latest.ping")} className="dialog-input"
value={ping} onChange={(e) => setPing(e.target.value)}/>
</div>
<div className="speed">
<div className="speed-header">
<FontAwesomeIcon icon={faArrowDown}/>
<div className="speed-text">
<h2>{t("latest.down")}</h2>
<p>{t("welcome.mbps")}</p>
</div>
</div>
<input type="number" placeholder={t("latest.down")} className="dialog-input"
value={download} onChange={(e) => setDownload(e.target.value)}/>
</div>
<div className="speed">
<div className="speed-header">
<FontAwesomeIcon icon={faArrowUp}/>
<div className="speed-text">
<h2>{t("latest.up")}</h2>
<p>{t("welcome.mbps")}</p>
</div>
</div>
<input type="number" placeholder={t("latest.up")} className="dialog-input"
value={upload} onChange={(e) => setUpload(e.target.value)}/>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export {DataHelper as default} from "./DataHelper";

View File

@@ -0,0 +1,45 @@
@import "@/common/styles/colors"
.data-helper
h2
margin: 0 0 0.5rem
color: $darker-white
p
margin: 0
color: $darker-white
.speeds
display: flex
justify-content: center
gap: 2rem
margin-top: 1rem
.speed
display: flex
flex-direction: column
align-items: center
input
box-sizing: border-box
width: 100%
.speed-header
display: flex
align-items: center
margin-bottom: 0.5rem
svg
font-size: 28pt
margin-right: 0.5rem
color: $green
.speed-text h2
margin: 0
color: $darker-white
.speed-text p
margin: 0
color: $subtext

View File

@@ -0,0 +1,13 @@
import Banner from "@/common/components/WelcomeDialog/banner.webp";
import "./styles.sass";
import {t} from "i18next";
export const Greetings = () => {
return (
<div className="welcome-greetings">
<img src={Banner} alt="Welcome banner"/>
<h2>{t("welcome.title")}</h2>
<p>{t("welcome.subtext")}</p>
</div>
);
}

View File

@@ -0,0 +1 @@
export {Greetings as default} from "./Greetings";

View File

@@ -0,0 +1,26 @@
@import "@/common/styles/colors"
.welcome-greetings
display: flex
margin: 1rem 0
flex-direction: column
align-items: center
height: 100%
justify-content: center
text-align: center
gap: 1rem
img
height: 5rem
h2
margin: 0
font-size: 24pt
color: $white
p
margin: 0
padding-left: 2rem
padding-right: 2rem
font-size: 14pt
color: $darker-white

View File

@@ -0,0 +1,30 @@
import "./styles.sass";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFileLines} from "@fortawesome/free-solid-svg-icons";
import {t} from "i18next";
export const documents = [
{url: "https://www.speedtest.net/about/terms", title: "Ookla ToS"},
{url: "https://www.speedtest.net/about/eula", title: "Ookla EULA"},
{url: "https://www.speedtest.net/about/privacy", title: "Ookla GDPR"}
]
export const OoklaLicense = () => {
return (
<div className="ookla-license">
<h2>{t("welcome.accept_title")}</h2>
<p>
{t("welcome.accept_subtext")}
</p>
<div className="documents">
{documents.map((document, index) => (
<a className="document" key={index} href={document.url}
target="_blank" rel="noreferrer">
<FontAwesomeIcon icon={faFileLines} />
<p>{document.title}</p>
</a>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export {OoklaLicense as default} from "./OoklaLicense";

View File

@@ -0,0 +1,29 @@
@import "@/common/styles/colors"
.ookla-license
h2
margin: 0 0 0.5rem
color: $darker-white
p
margin: 0
color: $darker-white
.documents
display: flex
flex-direction: column
margin-top: 1rem
.document
display: flex
align-items: center
margin: 0.3rem 0
color: $green
text-decoration: none
svg
margin-right: 0.5rem
font-size: 1.5rem
p
color: $green

View File

@@ -0,0 +1,22 @@
import "./styles.sass";
import {providers} from "@/common/components/ProviderDialog/ProviderDialog";
import {t} from "i18next";
export const ProviderChooser = ({provider, setProvider}) => {
return (
<div className="provider-chooser">
<h2>{t("welcome.provider_title")}</h2>
<p>{t("welcome.provider_subtext")}</p>
<div className="provider-list">
{providers.map((current) => (
<div className={"provider-item" + (current.id === provider ? " provider-item-active" : "")}
onClick={() => setProvider(current.id)} key={current.id}>
<img src={current.image} alt={current.name}/>
<h2>{current.name}</h2>
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export {ProviderChooser as default} from "./ProviderChooser";

View File

@@ -0,0 +1,43 @@
@import "@/common/styles/colors"
.provider-chooser
h2
margin: 0 0 0.5rem
color: $darker-white
p
margin: 0
color: $darker-white
.provider-list
margin-top: 1rem
display: flex
gap: 1rem
flex-wrap: wrap
.provider-item
display: flex
align-items: center
padding: 0.2rem 1rem
gap: 0.5rem
border-radius: 0.8rem
border: 2px solid $light-gray
color: $darker-white
cursor: pointer
img
width: 3rem
height: 3rem
h2
margin: 0
&:hover
background-color: $darker-gray
.provider-item-active
background-color: $light-gray
&:hover
background-color: $light-gray

View File

@@ -0,0 +1,37 @@
@import "@/common/styles/colors"
.welcome-banner
width: 30rem
display: flex
flex-direction: column
justify-content: space-between
user-select: none
.welcome-inner
height: 100%
.welcome-actions
display: flex
justify-content: space-between
align-items: center
margin-top: 1rem
h3
margin: 0
font-size: 14pt
color: $subtext
.dialog-btn
padding: 0.4rem 1.3rem
border-radius: 0.6rem
.slide-in
animation: slide-in 0.5s forwards
@keyframes slide-in
from
opacity: 0
transform: translateX(10%) rotate(10deg) scale(0.5)
to
opacity: 1
transform: translateX(0)

View File

@@ -2,12 +2,15 @@ import React, {createContext, useContext, useEffect, useState} from "react";
import {InputDialogContext} from "../InputDialog";
import {request} from "@/common/utils/RequestUtil";
import {apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog";
import WelcomeDialog from "@/common/components/WelcomeDialog";
export const ConfigContext = createContext({});
export const ConfigProvider = (props) => {
const [config, setConfig] = useState({});
const [setDialog] = useContext(InputDialogContext);
const [welcomeShown, setWelcomeShown] = useState(false);
const reloadConfig = () => {
request("/config").then(async res => {
@@ -33,8 +36,14 @@ export const ConfigProvider = (props) => {
useEffect(reloadConfig, []);
useEffect(() => {
if (config.previewMode && !localStorage.getItem("welcomeShown")) setWelcomeShown(true);
if (!config.previewMode && config.provider === "none") setWelcomeShown(true);
}, [config]);
return (
<ConfigContext.Provider value={[config, reloadConfig, checkConfig]}>
{welcomeShown && <WelcomeDialog onClose={() => setWelcomeShown(false)}/>}
{props.children}
</ConfigContext.Provider>
)

View File

@@ -104,7 +104,7 @@ module.exports.create = async (type = "auto", retried = false) => {
if (process.env.PREVIEW_MODE === "true") {
await new Promise(resolve => setTimeout(resolve, 5000));
test = {
ping: {latency: Math.floor(Math.random() * 250) + 5},
ping: {latency: Math.floor(Math.random() * 25) + 5},
download: {bytes: Math.floor(Math.random() * 1000000000) + 1000000, elapsed: 10000},
upload: {bytes: Math.floor(Math.random() * 1000000000) + 1000000, elapsed: 10000}
}
@@ -112,7 +112,8 @@ module.exports.create = async (type = "auto", retried = false) => {
test = await this.run(retried);
}
let {ping, download, upload, time} = await parseData.parseData(mode, test);
let {ping, download, upload, time} = await parseData.parseData(process.env.PREVIEW_MODE === "true" ?
"ookla" : mode, test);
let testResult = await tests.create(ping, download, upload, time, test.serverId, type);
console.log(`Test #${testResult} was executed successfully in ${time}s. 🏓 ${ping}${download}${upload}`);
@@ -120,6 +121,7 @@ module.exports.create = async (type = "auto", retried = false) => {
setRunning(false);
sendFinished({ping, download, upload, time}).then(() => "");
} catch (e) {
console.log(e)
if (!retried) return this.create(type, true);
let testResult = await tests.create(-1, -1, -1, null, 0, type, e.message);
await sendError(e.message);