mirror of
https://github.com/gnmyt/myspeed.git
synced 2026-02-14 10:18:44 -06:00
Merge pull request #680 from gnmyt/updates/welcome-dialog
👋 Willkommensdialog hinzugefügt
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
82
client/src/common/components/WelcomeDialog/WelcomeDialog.jsx
Normal file
82
client/src/common/components/WelcomeDialog/WelcomeDialog.jsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
BIN
client/src/common/components/WelcomeDialog/banner.webp
Normal file
BIN
client/src/common/components/WelcomeDialog/banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
1
client/src/common/components/WelcomeDialog/index.js
Normal file
1
client/src/common/components/WelcomeDialog/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export {WelcomeDialog as default} from "./WelcomeDialog";
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {DataHelper as default} from "./DataHelper";
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Greetings as default} from "./Greetings";
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {OoklaLicense as default} from "./OoklaLicense";
|
||||
@@ -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
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {ProviderChooser as default} from "./ProviderChooser";
|
||||
@@ -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
|
||||
37
client/src/common/components/WelcomeDialog/styles.sass
Normal file
37
client/src/common/components/WelcomeDialog/styles.sass
Normal 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)
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user