Merge branch 'feat/onboarding/first-screen' into mock

# Conflicts:
#	src/utils/errors.ts
#	src/vite-env.d.ts
This commit is contained in:
Arnaud 2024-10-21 14:01:20 +02:00
commit a2bc0ded05
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
29 changed files with 509 additions and 95 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

BIN
public/img/onboarding.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
public/img/onboarding.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -12,6 +12,7 @@ export type State = {
availabilitiesApi: boolean;
createAvailabilityApi: boolean;
reservationsApi: boolean;
portForwarding: boolean;
};
export function DebugErrors() {
@ -128,6 +129,17 @@ export function DebugErrors() {
/>
<label htmlFor="reservations">Reservations API</label>
</div>
<div>
<input
id="portForwarding"
type="checkbox"
name="portForwarding"
onChange={onInputChange}
checked={data.portForwarding || false}
/>
<label htmlFor="reservations">Port forwarding</label>
</div>
</div>
);
}

View File

@ -1,14 +1,10 @@
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useQueryClient } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { CodexSdk } from "../../sdk/codex";
import {
NetworkIndicator,
Toast,
} from "@codex-storage/marketplace-ui-components";
import { Promises } from "../../utils/promises";
const report = false;
export let isCodexOnline: boolean | null = null;
import { useCodexConnection } from "../../hooks/useCodexConnection";
export function NodeIndicator() {
const queryClient = useQueryClient();
@ -16,44 +12,19 @@ export function NodeIndicator() {
time: 0,
message: "",
});
const { data, isError } = useQuery({
queryKey: ["spr"],
queryFn: async () => {
return CodexSdk.node()
.spr()
.then((data) => Promises.rejectOnError(data, report));
},
refetchInterval: 5000,
// No need to retry because we defined a refetch interval
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
// Cache is not useful for the spr endpoint
gcTime: 0,
});
isCodexOnline = !isError && !!data;
const codex = useCodexConnection();
useEffect(() => {
queryClient.invalidateQueries({
type: "active",
refetchType: "all",
});
}, [queryClient, isCodexOnline]);
}, [queryClient, codex.enabled]);
return (
<>
<Toast message={toast.message} time={toast.time} variant="success" />
<NetworkIndicator online={isCodexOnline} text="Codex node" />
<NetworkIndicator online={codex.enabled} text="Codex node" />
</>
);
}

View File

@ -0,0 +1,7 @@
.onboarding-image {
position: absolute;
right: -40px;
max-height: 90%;
width: auto;
z-index: -1;
}

View File

@ -0,0 +1,35 @@
import "./OmBoardingImage.css";
export function OnBoardingImage() {
return (
<picture>
<source
srcSet="/img/onboarding@4x.webp 4x,
/img/onboarding@3x.webp 3x,
/img/onboarding@2x.webp 2x,
/img/onboarding@1.5x.webp 1.5x,
/img/onboarding.webp 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/webp"
/>
<source
srcSet="/img/onboarding@4x.png 4x,
/img/onboarding@3x.png 3x,
/img/onboarding@2x.png 2x,
/img/onboarding@1.5x.png 1.5x,
/img/onboarding.png 1x"
sizes="(max-width: 600px) 100vw,
(max-width: 1200px) 50vw,
33vw"
type="image/png"
/>
<img
src="/img/onboarding.png"
alt="Onboarding Image"
className="onboarding-image"
/>
</picture>
);
}

View File

@ -0,0 +1,100 @@
import { AlphaIcon } from "./AlphaIcon";
import { AlphaText } from "../AlphaText/AlphaText";
import { Modal, SimpleText } from "@codex-storage/marketplace-ui-components";
import { useState } from "react";
import { ArrowRight } from "lucide-react";
type Props = {
onNextStep: () => void;
};
export function OnBoardingStepOne({ onNextStep }: Props) {
const [modal, setModal] = useState(false);
const onLegalDisclaimerOpen = () => setModal(true);
const onLegalDisclaimerClose = () => setModal(false);
return (
<>
<div className="index-column-section">
<div>
<AlphaIcon />
</div>
<div className="index-alphaText">
<p>
<AlphaText></AlphaText>
</p>
<p>
<SimpleText className="index-version" variant="normal">
{import.meta.env.PACKAGE_VERSION}
</SimpleText>
</p>
<p>
<SimpleText
className="index-disclaimer"
variant="error"
onClick={onLegalDisclaimerOpen}>
<a className="index-link">Legal Disclaimer</a>
</SimpleText>
</p>
</div>
</div>
<div className="index-column-section">
<h3 className="index-mainTitle">
Hello,
<br /> Welcome to <span className="index-codex">Codex</span>{" "}
<span className="index-vault">Vault</span>
</h3>
<p className="index-description">
<SimpleText variant="light">
Codex is a durable, decentralised data storage protocol, created so
the world community can preserve its most important knowledge
without risk of censorship.
</SimpleText>
</p>
</div>
<div className="index-column-section ">
<SimpleText variant="primary">
<a onClick={onNextStep} className="index-link index-getStarted">
Lets get started <ArrowRight></ArrowRight>
</a>
</SimpleText>
<Modal onClose={onLegalDisclaimerClose} open={modal}>
<h1 className="disclaimer-title" style={{ marginTop: 0 }}>
Disclaimer
</h1>
<p className="disclaimer-text">
The website and the content herein is not intended for public use
and is for informational and demonstration purposes only.
</p>
<br />
<p className="disclaimer-text">
The website and any associated functionalities are provided on an
as is basis without any guarantees, warranties, or representations
of any kind, either express or implied. The website and any
associated functionalities may not reflect the final version of the
project and is subject to changes, updates, or removal at any time
and without notice.
</p>
<br />
<p className="disclaimer-text">
By accessing and using this website, you agree that we, Logos
Collective Association and its affiliates, will not be liable for
any direct, indirect, incidental, or consequential damages arising
from the use of, or inability to use, this website. Any data,
content, or interactions on this site are non-binding and should not
be considered final or actionable. Your use of this website is at
your sole risk.
</p>
</Modal>
</div>
</>
);
}

View File

@ -0,0 +1,37 @@
.onboarding-check {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.onboarding-check-icon--valid {
color: var(--codex-color-primary);
}
.onboarding-check-icon--invalid {
color: rgb(var(--codex-color-error));
}
.onboarding-check-line {
display: flex;
align-items: center;
gap: 0.5rem;
}
.onboarding-check-refresh {
cursor: pointer;
}
.onboarding-check-refresh--fetching {
animation: rotateAnimation 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg); /* Start at 0 degrees */
}
to {
transform: rotate(360deg); /* End at 360 degrees */
}
}

View File

@ -0,0 +1,114 @@
import { CheckIcon, RefreshCcw, X } from "lucide-react";
import { classnames } from "../../utils/classnames";
import "./OnBoardingStepThree.css";
import { usePortForwarding } from "../../hooks/usePortForwarding";
import { useCodexConnection } from "../../hooks/useCodexConnection";
import { SimpleText } from "@codex-storage/marketplace-ui-components";
import { useEffect } from "react";
type Props = {
online: boolean;
onStepValid: (valid: boolean) => void;
};
export function OnBoardingStepThree({ online, onStepValid }: Props) {
const portForwarding = usePortForwarding(online);
const codex = useCodexConnection();
useEffect(() => {
onStepValid(online && portForwarding.enabled && codex.enabled);
}, [portForwarding.enabled, codex.enabled, onStepValid, online]);
const InternetIcon = online ? CheckIcon : X;
const PortForWarningIcon = portForwarding.enabled ? CheckIcon : X;
const CodexIcon = codex.enabled ? CheckIcon : X;
return (
<div className="index-column-section">
<div
className={classnames(
["onboarding-check"],
["onboarding-check--valid", online]
)}>
<InternetIcon
className={classnames(
["onboarding-check-icon--valid", online],
["onboarding-check-icon--invalid", !online]
)}
/>
<div>
<p>Internet connection</p>
<SimpleText variant="light">
Status indicator for the Internet.
</SimpleText>
</div>
</div>
<div
className={classnames(
["onboarding-check"],
["onboarding-check--valid", portForwarding.enabled]
)}>
<PortForWarningIcon
className={classnames(
["onboarding-check-icon--valid", portForwarding.enabled],
["onboarding-check-icon--invalid", !portForwarding.enabled]
)}
/>
<div className="onboarding-check-line">
<div>
<p>Port forwarding</p>
<SimpleText variant="light">
Status indicator for port forwarding activation.
</SimpleText>
</div>
{!portForwarding.enabled && (
<a
className="onboarding-check-refresh"
onClick={() => portForwarding.refetch()}>
<RefreshCcw
size={"1.25rem"}
className={classnames([
"onboarding-check-refresh--fetching",
portForwarding.isFetching,
])}
/>
</a>
)}
</div>
</div>
<div
className={classnames(
["onboarding-check"],
["onboarding-check--valid", codex.enabled]
)}>
<CodexIcon
className={classnames(
["onboarding-check-icon--valid", codex.enabled],
["onboarding-check-icon--invalid", !codex.enabled]
)}
/>
<div className="onboarding-check-line">
<div>
<p>Codex connection</p>
<SimpleText variant="light">
Status indicator for the Codex network.
</SimpleText>
</div>
{!codex.enabled && (
<a
className="onboarding-check-refresh"
onClick={() => codex.refetch()}>
<RefreshCcw
size={"1.25rem"}
className={classnames([
"onboarding-check-refresh--fetching",
codex.isFetching,
])}
/>
</a>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,27 @@
import { Input } from "@codex-storage/marketplace-ui-components";
import { ChangeEvent, useState } from "react";
type Props = {
onStepValid: (valid: boolean) => void;
};
export function OnBoardingStepTwo({ onStepValid }: Props) {
const [displayName, setDisplayName] = useState("");
const onDisplayNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setDisplayName(e.currentTarget.value);
onStepValid(!!e.currentTarget.value);
};
return (
<>
<div className="index-column-section">
<Input
onChange={onDisplayNameChange}
label="Display name"
id="displayName"
value={displayName}></Input>
</div>
</>
);
}

View File

@ -0,0 +1,33 @@
import { useQuery } from "@tanstack/react-query";
import { CodexSdk } from "../sdk/codex";
import { Promises } from "../utils/promises";
const report = false;
export function useCodexConnection() {
const { data, isError, isFetching, refetch } = useQuery({
queryKey: ["spr"],
queryFn: async () => {
return CodexSdk.node()
.spr()
.then((data) => Promises.rejectOnError(data, report));
},
refetchInterval: 5000,
// No need to retry because we defined a refetch interval
retry: false,
// The client node should be local, so display the cache value while
// making a background request looks good.
staleTime: 0,
// Refreshing when focus returns can be useful if a user comes back
// to the UI after performing an operation in the terminal.
refetchOnWindowFocus: true,
// Cache is not useful for the spr endpoint
gcTime: 0,
});
return { enabled: !isError && !!data, isFetching, refetch };
}

View File

@ -0,0 +1,30 @@
import { useQuery } from "@tanstack/react-query";
import { Echo } from "../utils/echo";
import { Errors } from "../utils/errors";
type PortForwardingResponse = { reachable: boolean };
export function usePortForwarding(online: boolean) {
const { data, isFetching, refetch } = useQuery({
queryFn: (): Promise<PortForwardingResponse> =>
Echo.portForwarding().catch((e) => Errors.report(e)),
queryKey: ["port-forwarding"],
initialData: { reachable: false },
// Enable only when the use has an internet connection
enabled: !!online,
// No need to retry because we provide a retry button
retry: false,
// The data should not be cached
staleTime: 0,
// The user may try to change the port forwarding and go back
// to the tab
refetchOnWindowFocus: true,
});
return { enabled: data.reachable, isFetching, refetch };
}

View File

@ -52,7 +52,22 @@
font-size: var(--codex-font-size);
color-scheme: dark;
color: var(--codex-color);
background: linear-gradient(246.02deg, #000000 30.36%, #222222 91.05%);
background: #000000; /* Fallback color */
background: -webkit-linear-gradient(
246.02deg,
#000000 30.36%,
#222222 91.05%
); /* For Safari and older Chrome */
background: -moz-linear-gradient(
246.02deg,
#000000 30.36%,
#222222 91.05%
); /* For older Firefox */
background: linear-gradient(
246.02deg,
#000000 30.36%,
#222222 91.05%
); /* Standard syntax */
}
::selection {
@ -140,6 +155,12 @@ pre {
a {
text-decoration: inherit;
color: var(--codex-color);
transition: opacity 0.35s;
}
a[aria-disabled] {
opacity: 0.6;
cursor: not-allowed;
}
.root {

View File

@ -411,3 +411,9 @@ const mock = {
export const CodexSdk = {
...mock,
};
export const Echo = {
portForwarding: () => Promise.resolve({ reachable: !DebugErrorsData.portForwarding })
}

View File

@ -1,10 +1,5 @@
.index {
width: 100%;
background-image: url(/bg.png);
background-position: 10px 10px;
background-position: right;
background-repeat: no-repeat;
background-size: auto 90%;
padding: 3rem 6rem;
}
@ -77,7 +72,7 @@
filter: brightness(1);
-webkit-filter: brightness(1);
}
50% {
30% {
filter: brightness(0.7);
-webkit-filter: brightness(0.7);
}
@ -167,7 +162,6 @@
display: inline-block;
border-radius: 50%;
transition: opacity 0.35s;
cursor: pointer;
}
.index-dot:not(.index-dot--active) {
@ -175,10 +169,24 @@
}
.index-dot:hover {
opacity: 0.8;
animation-name: pulse;
animation-duration: 2.5s;
animation-iteration-count: infinite;
}
.index-dot--active {
box-shadow: 0px 0px 13px 2px #fff;
box-shadow: 0px 0px 12px 0px #fff;
opacity: 1;
}
@keyframes pulse {
0% {
opacity: 0.4;
}
30% {
opacity: 0.8;
}
100% {
opacity: 0.4;
}
}

View File

@ -1,14 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import "./index.css";
import { AlphaIcon } from "../components/AlphaIcon/AlphaIcon";
import { AlphaText } from "../components/AlphaText/AlphaText";
import { SimpleText } from "@codex-storage/marketplace-ui-components";
import { ArrowRight } from "lucide-react";
import { CodexLogo } from "../components/CodexLogo/CodexLogo";
import { ArrowRightCircle } from "../components/ArrowRightCircle/ArrowRightCircle";
import { useNetwork } from "../network/useNetwork";
import { NetworkIcon } from "../components/NetworkIcon/NetworkIcon";
import { Logotype } from "../components/Logotype/Logotype";
import { useState } from "react";
import { OnBoardingStepOne } from "../components/OnBoarding/OnBoardingStepOne";
import { OnBoardingStepTwo } from "../components/OnBoarding/OnBoardingStepTwo";
import { classnames } from "../utils/classnames";
import { OnBoardingStepThree } from "../components/OnBoarding/OnBoardingStepThree";
import { attributes } from "../utils/attributes";
import { CodexLogo } from "../components/CodexLogo/CodexLogo";
import { OnBoardingImage } from "../components/OnBoarding/OnBoardingImage";
export const Route = createFileRoute("/")({
component: Index,
@ -20,7 +23,31 @@ export const Route = createFileRoute("/")({
});
function Index() {
const [isStepValid, setIsStepValid] = useState(true);
const [step, setStep] = useState(0);
const online = useNetwork();
const navigate = useNavigate({ from: "/" });
const onStepValid = (valid: boolean) => setIsStepValid(valid);
const onNextStep = () => {
if (!isStepValid) {
return;
}
if (step === 2) {
navigate({ to: "/dashboard" });
return;
}
setStep(step + 1);
setIsStepValid(false);
};
const components = [
<OnBoardingStepOne onNextStep={onNextStep} />,
<OnBoardingStepTwo onStepValid={onStepValid} />,
<OnBoardingStepThree online={online} onStepValid={onStepValid} />,
];
const text = online ? "Network connected" : "Network disconnected";
@ -32,54 +59,31 @@ function Index() {
<Logotype />
</div>
<div className="index-column-section">
<div>
<AlphaIcon />
</div>
<div className="index-alphaText">
<p>
<AlphaText></AlphaText>
</p>
<p>
<SimpleText className="index-version" variant="normal">
{import.meta.env.PACKAGE_VERSION}
</SimpleText>
</p>
<p>
<SimpleText className="index-disclaimer" variant="error">
<a className="index-link">Legal Disclaimer</a>
</SimpleText>
</p>
</div>
</div>
<div className="index-column-section">
<h3 className="index-mainTitle">
Hello,
<br /> Welcome to <span className="index-codex">Codex</span>{" "}
<span className="index-vault">Vault</span>
</h3>
<p className="index-description">
<SimpleText variant="light">
Codex is a durable, decentralised data storage protocol, created
so the world community can preserve its most important knowledge
without risk of censorship.
</SimpleText>
</p>
</div>
<div className="index-column-section ">
<SimpleText variant="primary">
<a href="/dashboard" className="index-link index-getStarted">
Lets get started <ArrowRight></ArrowRight>
</a>
</SimpleText>
{components[step]}
<div className=" ">
<div className="index-dots">
<span className="index-dot index-dot--active"></span>
<span className="index-dot"></span>
<span className="index-dot"></span>
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 0]
)}></span>
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 1]
)}></span>
<span
className={classnames(
["index-dot"],
["index-dot--active", step === 2]
)}></span>
</div>
</div>
</div>
<div className="index-column">
<OnBoardingImage />
</div>
<div className="index-columnRight">
<div className="index-logo">
<div className="index-network">
@ -88,7 +92,10 @@ function Index() {
</div>
<CodexLogo></CodexLogo>
</div>
<a href="/dashboard" className="index-link2">
<a
className="index-link2"
{...attributes({ "aria-disabled": !isStepValid })}
onClick={onNextStep}>
<ArrowRightCircle></ArrowRightCircle>
</a>
</div>

5
src/utils/echo.ts Normal file
View File

@ -0,0 +1,5 @@
export const Echo = {
portForwarding: () => fetch(import.meta.env.VITE_ECHO_URL + "/port/8070")
.then((res) => res.json())
}

2
src/vite-env.d.ts vendored
View File

@ -2,7 +2,7 @@
interface ImportMetaEnv {
VITE_CODEX_API_URL: string;
VITE_GEO_IP_URL: string;
VITE_ECHO_URL: string;
}
interface ImportMeta {

View File

@ -25,6 +25,7 @@ export default defineConfig({
alias: {
"../sdk/codex": "../mock",
"../../sdk/codex": "../../mock",
"../utils/echo": "../mock"
},
},
});