diff --git a/package-lock.json b/package-lock.json index 6ad30f2..dee7ce7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.7", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.25", - "@codex-storage/sdk-js": "^0.0.10", + "@codex-storage/marketplace-ui-components": "^0.0.26", + "@codex-storage/sdk-js": "^0.0.12", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", @@ -374,9 +374,9 @@ "dev": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.25", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.25.tgz", - "integrity": "sha512-oWR/E0yNWK/Lj2LI3lAqKDzuMw6sTUyYfkhZBbV4yK/4VnGtIIoW0OU+jggishJYAwh3y4s71OfMjWaGGafWlQ==", + "version": "0.0.26", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.26.tgz", + "integrity": "sha512-x8niBv1HRJU6VnKJ5+tv3+yYMxOj0tnMdNaPGzHVXMTHpj5WeHRcqUR1UhxxxnIlnsOjhmjX2tVUYPxpAz+OEw==", "dependencies": { "lucide-react": "^0.453.0" }, @@ -398,9 +398,9 @@ } }, "node_modules/@codex-storage/sdk-js": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.10.tgz", - "integrity": "sha512-DUq4YJF9r+45CaUf+ry0I6cawsfQA2RWbHmxASh9UuUu8L2xNlAbk8Uc2zmMr2HiixGDwO6b+BCtwZkDLBhTPQ==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.12.tgz", + "integrity": "sha512-WjMZ9fkeVFBdDlFViWRDIirk2s4nX0U5r4WixAeieAkDBlRq5nqWcm9rqz5/8u8EQudgk/y3p0vPZc8cpyK4bA==", "dependencies": { "valibot": "^0.32.0" }, diff --git a/package.json b/package.json index 1954636..ee72e3e 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.25", - "@codex-storage/sdk-js": "^0.0.11", + "@codex-storage/marketplace-ui-components": "^0.0.26", + "@codex-storage/sdk-js": "^0.0.12", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", diff --git a/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx b/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx new file mode 100644 index 0000000..79d0658 --- /dev/null +++ b/src/components/ErrorCircleIcon/ErrorCircleIcon.tsx @@ -0,0 +1,21 @@ +type Props = { + className?: string; +}; + +export function ErrorCircleIcon({ className = "" }: Props) { + return ( + + + + + + ); +} diff --git a/src/components/ManifestFetch/ManifestFetch.tsx b/src/components/ManifestFetch/ManifestFetch.tsx index 054081b..99bfa22 100644 --- a/src/components/ManifestFetch/ManifestFetch.tsx +++ b/src/components/ManifestFetch/ManifestFetch.tsx @@ -2,23 +2,26 @@ import { Button, Input } from "@codex-storage/marketplace-ui-components"; import "./ManifestFetch.css"; import { ChangeEvent, useState } from "react"; import { CodexSdk } from "../../sdk/codex"; -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Promises } from "../../utils/promises"; export function ManifestFetch() { const [cid, setCid] = useState(""); + const queryClient = useQueryClient(); const { refetch } = useQuery({ - queryFn: () => - CodexSdk.data() + queryFn: () => { + return CodexSdk.data() .fetchManifest(cid) .then((s) => { if (s.error === false) { setCid(""); + queryClient.invalidateQueries({ queryKey: ["cids"] }); } return Promises.rejectOnError(s); - }), - queryKey: ["cids"], + }); + }, + queryKey: ["manifest"], // Disable the fetch to make it available on refetch only enabled: false, @@ -44,6 +47,7 @@ export function ManifestFetch() {
diff --git a/src/components/OnBoarding/AlphaIcon.tsx b/src/components/OnBoarding/AlphaIcon.tsx index e32783e..3675507 100644 --- a/src/components/OnBoarding/AlphaIcon.tsx +++ b/src/components/OnBoarding/AlphaIcon.tsx @@ -1,4 +1,12 @@ -export function AlphaIcon() { +type Props = { + variant: "primary" | "error"; +}; + +export function AlphaIcon({ variant }: Props) { + const color = + variant === "primary" + ? "var(--codex-color-primary)" + : "var(--codex-color-error-hexa)"; return ( - + ); } diff --git a/src/components/OnBoarding/HealthCheckIcon.tsx b/src/components/OnBoarding/HealthCheckIcon.tsx new file mode 100644 index 0000000..404ee8b --- /dev/null +++ b/src/components/OnBoarding/HealthCheckIcon.tsx @@ -0,0 +1,15 @@ +export function HealthCheckIcon() { + return ( + + + + ); +} diff --git a/src/components/OnBoarding/HealthCheckItem.css b/src/components/OnBoarding/HealthCheckItem.css new file mode 100644 index 0000000..bb3c1c4 --- /dev/null +++ b/src/components/OnBoarding/HealthCheckItem.css @@ -0,0 +1,5 @@ +.onboarding-healthCheckItem { + display: flex; + align-items: center; + gap: 16px; +} diff --git a/src/components/OnBoarding/HealthCheckItem.tsx b/src/components/OnBoarding/HealthCheckItem.tsx new file mode 100644 index 0000000..a039a7d --- /dev/null +++ b/src/components/OnBoarding/HealthCheckItem.tsx @@ -0,0 +1,21 @@ +import { classnames } from "../../utils/classnames"; +import { OnBoardingStatusIcon } from "./OnBoardingStatusIcon"; +import "./HealthCheckItem.css"; + +type Props = { + value: "success" | "failure" | "warning"; + text: string; +}; + +export function HealthCheckItem({ value, text }: Props) { + return ( +
+ +
+

{text}

+
+
+ ); +} diff --git a/src/components/OnBoarding/OmBoardingImage.css b/src/components/OnBoarding/OnBoardingImage.css similarity index 100% rename from src/components/OnBoarding/OmBoardingImage.css rename to src/components/OnBoarding/OnBoardingImage.css diff --git a/src/components/OnBoarding/OnBoardingImage.tsx b/src/components/OnBoarding/OnBoardingImage.tsx index e2780cf..7c269d6 100644 --- a/src/components/OnBoarding/OnBoardingImage.tsx +++ b/src/components/OnBoarding/OnBoardingImage.tsx @@ -1,4 +1,4 @@ -import "./OmBoardingImage.css"; +import "./OnBoardingImage.css"; export function OnBoardingImage() { return ( diff --git a/src/components/OnBoarding/OnBoardingStatusIcon.tsx b/src/components/OnBoarding/OnBoardingStatusIcon.tsx new file mode 100644 index 0000000..a6f5887 --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStatusIcon.tsx @@ -0,0 +1,33 @@ +import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon"; +import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon"; + +type Props = { + value: "success" | "failure" | "warning"; +}; + +export function OnBoardingStatusIcon({ value }: Props) { + switch (value) { + case "success": + return ; + + case "failure": + return ; + + case "warning": + return ( + + + + + + ); + } +} diff --git a/src/components/OnBoarding/OnBoardingStepOne.tsx b/src/components/OnBoarding/OnBoardingStepOne.tsx index dcab971..b23be34 100644 --- a/src/components/OnBoarding/OnBoardingStepOne.tsx +++ b/src/components/OnBoarding/OnBoardingStepOne.tsx @@ -19,7 +19,7 @@ export function OnBoardingStepOne({ onNextStep }: Props) { <>
- +

diff --git a/src/components/OnBoarding/OnBoardingStepThree.css b/src/components/OnBoarding/OnBoardingStepThree.css index 0d49e69..0c56661 100644 --- a/src/components/OnBoarding/OnBoardingStepThree.css +++ b/src/components/OnBoarding/OnBoardingStepThree.css @@ -27,10 +27,6 @@ cursor: pointer; } -.onboarding-check-refresh--fetching { - animation: rotateAnimation 2s linear infinite; -} - .onboarding-group { display: flex; align-items: center; @@ -43,6 +39,75 @@ padding-left: 1rem; } +.onboarding-deviceCheck { + margin-top: 1rem; + margin-bottom: 1rem; + font-family: Azeret Mono; + font-size: 14px; + font-weight: 400; + line-height: 16.34px; + display: inline-block; +} + +.onboarding--deviceCheck-block { + flex: 0.3; +} + +.onboarding-displayName { + margin-bottom: 1.75rem; +} + +.onboarding-healthCheck-item { + display: flex; + align-items: center; + padding: 16px 0; + gap: 16px; + border-top: 1px solid #3c3d3e; + border-bottom: 1px solid #3c3d3e; +} + +.onboarding-healthCheck-itemText { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; +} + +.onboarding-healthChecks { + margin-bottom: 32px; +} + +.onboarding-healthCheck-icon { + display: flex; + align-items: center; + width: 20px; + height: 20px; + justify-content: center; +} + +.onboarding-refresh { + position: relative; + top: 14px; + cursor: pointer; +} + +.onboarding-refresh--fetching { + animation: rotate 2s linear infinite; +} + +.onboarding-portContainer, +.onboarding-addressContainer { + position: relative; +} + +.onboarding--addressSuccessIcon { + position: absolute; + top: 53px; + bottom: 0; + right: 18px; +} + @keyframes rotate { from { transform: rotate(0deg); /* Start at 0 degrees */ diff --git a/src/components/OnBoarding/OnBoardingStepThree.tsx b/src/components/OnBoarding/OnBoardingStepThree.tsx index c104a8b..2de83bc 100644 --- a/src/components/OnBoarding/OnBoardingStepThree.tsx +++ b/src/components/OnBoarding/OnBoardingStepThree.tsx @@ -1,20 +1,21 @@ -import { CheckIcon, RefreshCcw, Save, ShieldAlert, X } from "lucide-react"; import { classnames } from "../../utils/classnames"; import "./OnBoardingStepThree.css"; import { usePortForwarding } from "../../hooks/usePortForwarding"; -import { useCodexConnection } from "../../hooks/useCodexConnection"; -import { - Alert, - ButtonIcon, - Input, - SimpleText, -} from "@codex-storage/marketplace-ui-components"; -import { useEffect, useState } from "react"; +import { Input, SimpleText } from "@codex-storage/marketplace-ui-components"; +import { ClipboardEvent, useEffect, useState } from "react"; import { CodexSdk } from "../../sdk/codex"; import { useQueryClient } from "@tanstack/react-query"; import { usePersistence } from "../../hooks/usePersistence"; import { useDebug } from "../../hooks/useDebug"; import { DebugUtils } from "../../utils/debug"; +import { AlphaIcon } from "./AlphaIcon"; +import { OnBoardingUtils } from "../../utils/onboarding"; +import { RefreshIcon } from "../RefreshIcon/RefreshIcon"; +import { HealthCheckIcon } from "./HealthCheckIcon"; +import { HealthCheckItem } from "./HealthCheckItem"; +import { Strings } from "../../utils/strings"; +import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon"; +import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon"; type Props = { online: boolean; @@ -30,6 +31,7 @@ export function OnBoardingStepThree({ online, onStepValid }: Props) { const persistence = usePersistence(codex.isSuccess); const [url, setUrl] = useState(CodexSdk.url); const queryClient = useQueryClient(); + const [isInvalid, setIsInvalid] = useState(false); useEffect(() => { onStepValid(online && portForwarding.enabled && codex.isSuccess); @@ -38,179 +40,180 @@ export function OnBoardingStepThree({ online, onStepValid }: Props) { useEffect(() => { if (codex.isSuccess) { persistence.refetch(); + portForwarding.refetch(); } - }, [codex.isSuccess]); + }, [persistence, portForwarding, codex.isSuccess]); - const onChange = (e: React.FormEvent) => { + const onAddressChange = (e: React.FormEvent) => { + const [, port] = Strings.splitURLAndPort(url); + const element = e.currentTarget; const value = e.currentTarget.value; - if (value) { - setUrl(value); + + if ( + value.startsWith("http://") === false && + value.startsWith("https://") === false + ) { + setIsInvalid(true); + return; + } + + console.info("isInvalid", isInvalid); + + setIsInvalid(!element.checkValidity()); + setUrl(value + ":" + port); + }; + + const onPaste = (e: ClipboardEvent) => { + const text = e.clipboardData.getData("text"); + + try { + new URL(text); + setUrl(text); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + // Nothing to do here } }; - const onSave = () => + const onPortChange = (e: React.FormEvent) => { + const [address] = Strings.splitURLAndPort(url); + const element = e.currentTarget; + const value = element.value; + + setUrl(address + ":" + value); + }; + + const onSave = () => { + if (isInvalid === true) { + return; + } + CodexSdk.updateURL(url) .then(() => queryClient.invalidateQueries()) .then(() => codex.refetch()); + }; - const InternetIcon = online ? CheckIcon : X; - const PortForWarningIcon = portForwarding.enabled ? CheckIcon : X; - const CodexIcon = codex.isSuccess ? CheckIcon : X; - const PersistenceIcon = persistence.enabled ? CheckIcon : ShieldAlert; - - let hasPortForwarningWarning = false; - let portValue = 0; + let forwardingPortValue = defaultPort; if (codex.isSuccess && codex.data) { const port = DebugUtils.getTcpPort(codex.data); - if (port.error === false && port.data !== defaultPort) { - hasPortForwarningWarning = true; - } if (!port.error) { - portValue = port.data; + forwardingPortValue = port.data; } } + const displayName = OnBoardingUtils.getDisplayName(); + const [address, port] = Strings.splitURLAndPort(url); + return ( -

-
+ <> +
- +
- - -
-
- -
-

Internet connection

- - Status indicator for the Internet. - -
-
-
- -
-
-

Port forwarding

- - Status indicator for port forwarding activation. - - {portValue && ( - <> -
- - TCP Port detected: {portValue}. - - - )} -
- {!portForwarding.enabled && ( - portForwarding.refetch()}> - - - )} -
-
-

Codex

-
-
- -
-
-

Codex connection

- - Status indicator for the Codex network. - -
- {!persistence.enabled && ( - persistence.refetch()}> - - - )} -
-
-
- -
-
-

Marketplace

- - Status indicator for the marketplace on the Codex node. - -
-
-
-
- - {hasPortForwarningWarning && ( - + - It seems like you are using a different port than the default one ( - {defaultPort}). Be sure the port forwarning is enabled for the port - you are running. + Connection /
+ Device Health Check
-
- )} -
+ +
+
+

+ Nice to meet {displayName},
+ Let’s establish our connection. +

+
+
+
+ + {isInvalid ? ( + + ) : ( + + )} +
+
+ + +
+ + +
+
+ +
    +
  • Port forwarding should be default {forwardingPortValue}.
  • +
+ +
+
+ + + + + + Health Check + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+ ); } diff --git a/src/components/OnBoarding/OnBoardingStepTwo.css b/src/components/OnBoarding/OnBoardingStepTwo.css new file mode 100644 index 0000000..c45f7ed --- /dev/null +++ b/src/components/OnBoarding/OnBoardingStepTwo.css @@ -0,0 +1,44 @@ +.onboarding-personalization { + margin-top: 1rem; + font-family: Azeret Mono; + font-size: 14px; + font-weight: 400; + line-height: 16.34px; + display: inline-block; +} + +.onboarding--personalization-block { + flex: 0.3; +} + +.onboarding-addressAndPort { + display: flex; + gap: 1.5rem; + align-items: center; +} + +.onboarding-port { + width: 150px; +} + +.onboarding-port::-webkit-outer-spin-button, +.onboarding-port::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +.onboarding-port[type="number"] { + -moz-appearance: textfield; +} + +.onboarding-portForwardingHelp { + font-family: Azeret Mono; + font-size: 12px; + font-weight: 400; + line-height: 14px; + color: #828282; + padding-left: 1.25rem; + margin-top: 1.75rem; + margin-bottom: 3rem; +} diff --git a/src/components/OnBoarding/OnBoardingStepTwo.tsx b/src/components/OnBoarding/OnBoardingStepTwo.tsx index 092a223..93c88f8 100644 --- a/src/components/OnBoarding/OnBoardingStepTwo.tsx +++ b/src/components/OnBoarding/OnBoardingStepTwo.tsx @@ -1,5 +1,8 @@ -import { Input } from "@codex-storage/marketplace-ui-components"; +import { Input, SimpleText } from "@codex-storage/marketplace-ui-components"; import { ChangeEvent, useState } from "react"; +import { AlphaIcon } from "./AlphaIcon"; +import "./OnBoardingStepTwo.css"; +import { OnBoardingUtils } from "../../utils/onboarding"; type Props = { onStepValid: (valid: boolean) => void; @@ -9,18 +12,34 @@ export function OnBoardingStepTwo({ onStepValid }: Props) { const [displayName, setDisplayName] = useState(""); const onDisplayNameChange = (e: ChangeEvent) => { - setDisplayName(e.currentTarget.value); - onStepValid(!!e.currentTarget.value); + const value = e.currentTarget.value; + OnBoardingUtils.setDisplayName(value); + setDisplayName(value); + onStepValid(!!value); }; return ( <> +
+
+ +
+ + Personalization + +
- +

+ Let’s get you setup.
+ What do you want to be called? +

+
+ +
); diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx index 0737655..5ffef3d 100644 --- a/src/components/Peers/PeerCountryCell.tsx +++ b/src/components/Peers/PeerCountryCell.tsx @@ -52,7 +52,7 @@ export function PeerCountryCell({ address, onPinAdd }: Props) { lng: data.longitude, }); } - }, [data]); + }, [data, onPinAdd]); return ( diff --git a/src/components/RefreshIcon/RefreshIcon.tsx b/src/components/RefreshIcon/RefreshIcon.tsx new file mode 100644 index 0000000..dd73da3 --- /dev/null +++ b/src/components/RefreshIcon/RefreshIcon.tsx @@ -0,0 +1,22 @@ +type Props = { + onClick?: () => void; + className?: string; +}; + +export function RefreshIcon({ className, onClick }: Props) { + return ( + + + + ); +} diff --git a/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx b/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx new file mode 100644 index 0000000..4a19441 --- /dev/null +++ b/src/components/SuccessCheckIcon/SuccessCheckIcon.tsx @@ -0,0 +1,24 @@ +type Props = { + variant: "primary" | "default"; + className?: string; +}; + +export function SuccessCheckIcon({ variant, className = "" }: Props) { + const color = variant === "primary" ? "#1FC16B" : "#444444"; + + return ( + + + + + + ); +} diff --git a/src/index.css b/src/index.css index 880fb88..f47b4af 100644 --- a/src/index.css +++ b/src/index.css @@ -31,6 +31,7 @@ --codex-color-disabled: #717171; --codex-color-light: rgb(150 150 150); --codex-border-color: #2b303b; + --codex-input-border-color: #494949; --codex-background-secondary: rgb(38 38 38); --codex-highlight-color: #2f2f2f; --codex-background-light: rgb(64 64 64); @@ -41,6 +42,10 @@ BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + --codex-input-label-color: #7b7b7b; + --codex-input-border-color: #494949; + --codex-input-background: #232323; + --codex-input-color-error: #fb3748; -webkit-tap-highlight-color: transparent; -webkit-text-size-adjust: 100%; @@ -173,3 +178,11 @@ a[aria-disabled] { .page { max-width: 100%; } + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-transition-delay: 9999s; + transition-delay: 9999s; +} diff --git a/src/routes/index.css b/src/routes/index.css index 164581f..dcf4e53 100644 --- a/src/routes/index.css +++ b/src/routes/index.css @@ -139,15 +139,15 @@ } .index-column-section { - max-width: 450px; + max-width: 500px; } -.index-column-section:not(:first-child) { +.index-column-section { flex: 1; } .index-column-section:first-child { - flex: 0.7; + flex: 0.5; } .index-column-section:last-child { @@ -185,6 +185,10 @@ opacity: 1; } +.index-displayName { + margin-top: 1rem; +} + @keyframes pulse { 0% { opacity: 0.4; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index e0b5fe0..c397de6 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,7 +2,6 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router"; import "./index.css"; 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"; @@ -10,7 +9,6 @@ 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"; import { OnBoardingUtils } from "../utils/onboarding"; @@ -52,7 +50,7 @@ function Index() { , ]; - const text = online ? "Network connected" : "Network disconnected"; + // const text = online ? "Network connected" : "Network disconnected"; return (
@@ -88,13 +86,14 @@ function Index() {