From d4667c5ec77177acc5f1ea1f2104287b71b63bed Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 21 Oct 2024 13:04:50 +0200 Subject: [PATCH] Improve onboarding screen --- .../NodeIndicator/NodeIndicator.tsx | 39 +----- .../{AlphaIcon => OnBoarding}/AlphaIcon.tsx | 0 .../OnBoarding/OnBoardingStepOne.tsx | 100 +++++++++++++++ .../OnBoarding/OnBoardingStepThree.css | 37 ++++++ .../OnBoarding/OnBoardingStepThree.tsx | 114 ++++++++++++++++++ .../OnBoarding/OnBoardingStepTwo.tsx | 27 +++++ src/hooks/useCodexConnection.tsx | 33 +++++ src/hooks/usePortForwarding.tsx | 31 +++++ src/index.css | 23 +++- src/routes/index.css | 21 +++- src/routes/index.tsx | 103 ++++++++-------- src/utils/errors.ts | 18 --- src/vite-env.d.ts | 1 + 13 files changed, 440 insertions(+), 107 deletions(-) rename src/components/{AlphaIcon => OnBoarding}/AlphaIcon.tsx (100%) create mode 100644 src/components/OnBoarding/OnBoardingStepOne.tsx create mode 100644 src/components/OnBoarding/OnBoardingStepThree.css create mode 100644 src/components/OnBoarding/OnBoardingStepThree.tsx create mode 100644 src/components/OnBoarding/OnBoardingStepTwo.tsx create mode 100644 src/hooks/useCodexConnection.tsx create mode 100644 src/hooks/usePortForwarding.tsx diff --git a/src/components/NodeIndicator/NodeIndicator.tsx b/src/components/NodeIndicator/NodeIndicator.tsx index d2319d5..306dc88 100644 --- a/src/components/NodeIndicator/NodeIndicator.tsx +++ b/src/components/NodeIndicator/NodeIndicator.tsx @@ -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 ( <> - + ); } diff --git a/src/components/AlphaIcon/AlphaIcon.tsx b/src/components/OnBoarding/AlphaIcon.tsx similarity index 100% rename from src/components/AlphaIcon/AlphaIcon.tsx rename to src/components/OnBoarding/AlphaIcon.tsx diff --git a/src/components/OnBoarding/OnBoardingStepOne.tsx b/src/components/OnBoarding/OnBoardingStepOne.tsx new file mode 100644 index 0000000..dcab971 --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStepOne.tsx @@ -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 ( + <> +
+
+ +
+
+

+ +

+

+ + {import.meta.env.PACKAGE_VERSION} + +

+

+ + Legal Disclaimer + +

+
+
+
+

+ Hello, +
Welcome to Codex{" "} + Vault +

+

+ + Codex is a durable, decentralised data storage protocol, created so + the world community can preserve its most important knowledge + without risk of censorship. + +

+
+
+ + + Let’s get started + + + + +

+ Disclaimer +

+ +

+ The website and the content herein is not intended for public use + and is for informational and demonstration purposes only. +

+ +
+ +

+ 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. +

+ +
+ +

+ 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. +

+
+
+ + ); +} diff --git a/src/components/OnBoarding/OnBoardingStepThree.css b/src/components/OnBoarding/OnBoardingStepThree.css new file mode 100644 index 0000000..7818412 --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStepThree.css @@ -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 */ + } +} diff --git a/src/components/OnBoarding/OnBoardingStepThree.tsx b/src/components/OnBoarding/OnBoardingStepThree.tsx new file mode 100644 index 0000000..216ce6c --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStepThree.tsx @@ -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 ( +
+
+ +
+

Internet connection

+ + Status indicator for the Internet. + +
+
+
+ +
+
+

Port forwarding

+ + Status indicator for port forwarding activation. + +
+ {!portForwarding.enabled && ( + portForwarding.refetch()}> + + + )} +
+
+
+ +
+
+

Codex connection

+ + Status indicator for the Codex network. + +
+ {!codex.enabled && ( + codex.refetch()}> + + + )} +
+
+
+ ); +} diff --git a/src/components/OnBoarding/OnBoardingStepTwo.tsx b/src/components/OnBoarding/OnBoardingStepTwo.tsx new file mode 100644 index 0000000..092a223 --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStepTwo.tsx @@ -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) => { + setDisplayName(e.currentTarget.value); + onStepValid(!!e.currentTarget.value); + }; + + return ( + <> +
+ +
+ + ); +} diff --git a/src/hooks/useCodexConnection.tsx b/src/hooks/useCodexConnection.tsx new file mode 100644 index 0000000..e5f1bb5 --- /dev/null +++ b/src/hooks/useCodexConnection.tsx @@ -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 }; +} diff --git a/src/hooks/usePortForwarding.tsx b/src/hooks/usePortForwarding.tsx new file mode 100644 index 0000000..9dff3a7 --- /dev/null +++ b/src/hooks/usePortForwarding.tsx @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import { Errors } from "../utils/errors"; + +type PortForwardingResponse = { reachable: boolean }; + +export function usePortForwarding(online: boolean) { + const { data, isFetching, refetch } = useQuery({ + queryFn: (): Promise => + fetch(import.meta.env.VITE_ECHO_URL + "/port/8070") + .then((res) => res.json()) + .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 }; +} diff --git a/src/index.css b/src/index.css index 34a8842..7cbd193 100644 --- a/src/index.css +++ b/src/index.css @@ -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 { diff --git a/src/routes/index.css b/src/routes/index.css index 6834a1b..05adf5c 100644 --- a/src/routes/index.css +++ b/src/routes/index.css @@ -77,7 +77,7 @@ filter: brightness(1); -webkit-filter: brightness(1); } - 50% { + 30% { filter: brightness(0.7); -webkit-filter: brightness(0.7); } @@ -167,7 +167,6 @@ display: inline-block; border-radius: 50%; transition: opacity 0.35s; - cursor: pointer; } .index-dot:not(.index-dot--active) { @@ -175,10 +174,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; + } +} diff --git a/src/routes/index.tsx b/src/routes/index.tsx index f986383..eb91fb0 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,14 +1,16 @@ -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"; export const Route = createFileRoute("/")({ component: Index, @@ -20,7 +22,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 = [ + , + , + , + ]; const text = online ? "Network connected" : "Network disconnected"; @@ -32,51 +58,25 @@ function Index() { -
-
- -
-
-

- -

-

- - {import.meta.env.PACKAGE_VERSION} - -

-

- - Legal Disclaimer - -

-
-
-
-

- Hello, -
Welcome to Codex{" "} - Vault -

-

- - Codex is a durable, decentralised data storage protocol, created - so the world community can preserve its most important knowledge - without risk of censorship. - -

-
-
- - - Let’s get started - - + {components[step]} +
- - - + + +
@@ -88,7 +88,10 @@ function Index() { - + diff --git a/src/utils/errors.ts b/src/utils/errors.ts index d9830fc..2b41d02 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -1,23 +1,6 @@ import * as Sentry from "@sentry/browser"; -import { isCodexOnline } from "../components/NodeIndicator/NodeIndicator"; import { CodexError } from "@codex-storage/sdk-js"; -// It would be preferable to completely ignore the error -// when the node is not connected. However, during the -// initial load, we lack this information until the -// SPR response is completed. In the meantime, other -// requests may be initiated, so if the node is not -// connected, we should set the level to 'log'. -const getLogLevel = () => { - switch (isCodexOnline) { - case true: - return "error"; - case null: - return "info"; - case false: - return "log"; - } -}; export const Errors = { report(safe: { error: true, data: CodexError }) { @@ -26,7 +9,6 @@ export const Errors = { code: safe.data.code, errors: safe.data.errors, sourceStack: safe.data.sourceStack, - level: getLogLevel(), }, }); } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index ca737c8..ca7d25d 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,6 +2,7 @@ interface ImportMetaEnv { VITE_CODEX_API_URL: string; + VITE_ECHO_URL: string; } interface ImportMeta {