diff --git a/package-lock.json b/package-lock.json index b533c8e..4e4d877 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.7", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "../storybook", + "@codex-storage/marketplace-ui-components": "^0.0.32", "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -421,8 +421,29 @@ "peer": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "resolved": "../storybook", - "link": true + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.32.tgz", + "integrity": "sha512-6vJ9pacl2rzLE3GqBNtbaGEBSxZ6yxb5+ixY/Q6ORtgSgv54LPb6L9VOiwrjrRxD12HEVxosXEeyBLhek7C8tg==", + "dependencies": { + "lucide-react": "^0.453.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@codex-storage/sdk-js": ">=0.0.14", + "postcss-nesting": "^13.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } + }, + "node_modules/@codex-storage/marketplace-ui-components/node_modules/lucide-react": { + "version": "0.453.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz", + "integrity": "sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } }, "node_modules/@codex-storage/sdk-js": { "version": "0.0.15", @@ -5238,4 +5259,4 @@ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5f984d5..ec3dfda 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.31", + "@codex-storage/marketplace-ui-components": "^0.0.32", "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", diff --git a/public/icons/select-arrow.svg b/public/icons/select-arrow.svg new file mode 100644 index 0000000..d68c733 --- /dev/null +++ b/public/icons/select-arrow.svg @@ -0,0 +1,10 @@ + + + diff --git a/public/icons/us-flag.svg b/public/icons/us-flag.svg new file mode 100644 index 0000000..9a4d69f --- /dev/null +++ b/public/icons/us-flag.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/public/img/wallet.png b/public/img/wallet.png new file mode 100644 index 0000000..500ec18 Binary files /dev/null and b/public/img/wallet.png differ diff --git a/public/img/welcome.png b/public/img/welcome.png new file mode 100644 index 0000000..58429ff Binary files /dev/null and b/public/img/welcome.png differ diff --git a/src/components/AccountIcon/AccountIcon.tsx b/src/components/AccountIcon/AccountIcon.tsx new file mode 100644 index 0000000..9875ea1 --- /dev/null +++ b/src/components/AccountIcon/AccountIcon.tsx @@ -0,0 +1,15 @@ +export function AccountIcon() { + return ( + + + + ); +} diff --git a/src/components/ArrowLeftIcon/ArrowLeftIcon.tsx b/src/components/ArrowLeftIcon/ArrowLeftIcon.tsx new file mode 100644 index 0000000..0e4dfa0 --- /dev/null +++ b/src/components/ArrowLeftIcon/ArrowLeftIcon.tsx @@ -0,0 +1,15 @@ +export function ArrowLeftIcon() { + return ( + + + + ); +} diff --git a/src/components/ArrowRightIcon/ArrowRightIcon.tsx b/src/components/ArrowRightIcon/ArrowRightIcon.tsx new file mode 100644 index 0000000..1d5220d --- /dev/null +++ b/src/components/ArrowRightIcon/ArrowRightIcon.tsx @@ -0,0 +1,15 @@ +export function ArrowRightIcon() { + return ( + + + + ); +} diff --git a/src/components/Availability/AvailabilitySpaceAllocation.tsx b/src/components/Availability/AvailabilitySpaceAllocation.tsx index 08f8438..b3495fe 100644 --- a/src/components/Availability/AvailabilitySpaceAllocation.tsx +++ b/src/components/Availability/AvailabilitySpaceAllocation.tsx @@ -2,7 +2,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { AvailabilityState } from "./types"; import { SpaceAllocation } from "@codex-storage/marketplace-ui-components"; import "./AvailabilitySpaceAllocation.css"; -import { nodeSpaceAllocationColors } from "../NodeSpaceAllocation/nodeSpaceAllocation.domain"; +import { nodeSpaceAllocationColors } from "../NodeSpace/nodeSpace.domain"; type Props = { space: CodexNodeSpace; diff --git a/src/components/ConnectedAccount/ConnectedAccount.css b/src/components/ConnectedAccount/ConnectedAccount.css new file mode 100644 index 0000000..290cc1a --- /dev/null +++ b/src/components/ConnectedAccount/ConnectedAccount.css @@ -0,0 +1,7 @@ +.connected-account { + border-radius: 8px; + border: 1px solid #96969633; + + main { + } +} diff --git a/src/components/ConnectedAccount/ConnectedAccount.tsx b/src/components/ConnectedAccount/ConnectedAccount.tsx new file mode 100644 index 0000000..ba5ec7e --- /dev/null +++ b/src/components/ConnectedAccount/ConnectedAccount.tsx @@ -0,0 +1,22 @@ +import { Button } from "@codex-storage/marketplace-ui-components"; +import { AccountIcon } from "../AccountIcon/AccountIcon"; +import "./ConnectedAccount.css"; +import { PlusIcon } from "../PlusIcon/PlusIcon"; +import { WalletCard } from "./WalletCard"; + +export function ConnectedAccount() { + return ( +
+
+
+ +
Connected Account
+
+ +
+
+ +
+
+ ); +} diff --git a/src/components/ConnectedAccount/WalletCard.css b/src/components/ConnectedAccount/WalletCard.css new file mode 100644 index 0000000..e2613c7 --- /dev/null +++ b/src/components/ConnectedAccount/WalletCard.css @@ -0,0 +1,168 @@ +.wallet-card { + background-image: url(/img/wallet.png); + background-repeat: no-repeat; + background-size: cover; + border: 1px solid #96969633; + height: 286px; + border-radius: 16px; + padding: 16px; + box-sizing: border-box; + position: relative; + + &::before { + content: " "; + width: 217px; + height: 148px; + position: absolute; + bottom: -1px; + right: 0; + background: transparent; + backdrop-filter: blur(3px); + } + + header { + button { + background-color: #161616; + border: 1px solid #96969633; + height: 24px; + width: 24px; + cursor: pointer; + transition: box-shadow 0.35s; + + &:hover { + box-shadow: 0 0 0 3px var(--codex-border-color); + } + + &:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + } + + &:nth-child(2) { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + } + } + + h6 { + font-family: Inter; + font-size: 12px; + font-weight: 700; + line-height: 14.52px; + letter-spacing: 0.01em; + text-align: left; + text-transform: uppercase; + } + + span { + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #ffffffb2; + } + + var { + font-family: Inter; + font-weight: 500; + line-height: 40px; + letter-spacing: -0.005em; + color: var(--text-strong-950, #ffffff); + display: block; + font-style: normal; + } + + main var { + font-size: 32px; + } + + footer { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + + &::after { + content: " "; + background-image: url(/icons/select-arrow.svg); + background-repeat: no-repeat; + position: absolute; + width: 16px; + height: 16px; + right: 0; + top: 22px; + } + + var { + font-size: 20px; + line-height: 25px; + } + + select { + background-color: #161616; + border-radius: 8px; + border: 1px solid #96969633; + padding: 6px 6px 6px 44px; + outline: none; + -moz-appearance: none; /* Firefox */ + -webkit-appearance: none; /* Safari and Chrome */ + appearance: none; + position: relative; + transition: box-shadow 0.35s; + + &:hover { + box-shadow: 0 0 0 3px var(--codex-border-color); + } + + &:has(option[value="US"]:checked) { + background-image: url(/icons/us-flag.svg); + background-position: 10px; + background-repeat: no-repeat; + background-size: 16px; + } + + option { + border-radius: 32px; + } + } + + div { + position: relative; + + .row { + gap: 8px; + } + } + + small { + color: #3ee089; + height: 20px; + width: 42px; + border-radius: 16px; + background-color: #1fc16b29; + display: flex; + align-items: center; + justify-content: center; + } + } + + section:first-child { + margin-top: 12px; + } + + section:nth-child(2) { + margin-top: 16px; + margin-bottom: 10px; + height: 90px; + position: relative; + + .wallet-lines { + position: absolute; + left: 0; + top: 10px; + } + } +} diff --git a/src/components/ConnectedAccount/WalletCard.tsx b/src/components/ConnectedAccount/WalletCard.tsx new file mode 100644 index 0000000..0ec4b0a --- /dev/null +++ b/src/components/ConnectedAccount/WalletCard.tsx @@ -0,0 +1,48 @@ +import { WalletChart } from "./WalletChart"; +import { WalletLines } from "./WalletLines"; +import "./WalletCard.css"; +import { ArrowLeftIcon } from "../ArrowLeftIcon/ArrowLeftIcon"; +import { ArrowRightIcon } from "../ArrowRightIcon/ArrowRightIcon"; + +export function WalletCard() { + return ( +
+
+
Wallet
+
+ + +
+
+ +
+
+ TOKEN + 123,223 CDX +
+ +
+ + +
+
+ + +
+ ); +} diff --git a/src/components/ConnectedAccount/WalletChart.tsx b/src/components/ConnectedAccount/WalletChart.tsx new file mode 100644 index 0000000..06d2204 --- /dev/null +++ b/src/components/ConnectedAccount/WalletChart.tsx @@ -0,0 +1,83 @@ +export function WalletChart() { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/src/components/ConnectedAccount/WalletLines.tsx b/src/components/ConnectedAccount/WalletLines.tsx new file mode 100644 index 0000000..389f22e --- /dev/null +++ b/src/components/ConnectedAccount/WalletLines.tsx @@ -0,0 +1,36 @@ +export function WalletLines() { + return ( + + + + + + + + + + + ); +} diff --git a/src/components/Download/Download.css b/src/components/Download/Download.css index 7209c51..f3fed7b 100644 --- a/src/components/Download/Download.css +++ b/src/components/Download/Download.css @@ -1,13 +1,13 @@ .download { - display: flex; - align-items: center; - gap: 0.5rem; -} + .input { + flex: 1; -.download-inputContainer { - flex: 1; -} + input { + width: 100%; + } + } -.download-input { - width: 100%; + .button { + width: 105px; + } } diff --git a/src/components/Download/Download.tsx b/src/components/Download/Download.tsx index e6b19af..80a591f 100644 --- a/src/components/Download/Download.tsx +++ b/src/components/Download/Download.tsx @@ -2,6 +2,7 @@ import { Button, Input } from "@codex-storage/marketplace-ui-components"; import "./Download.css"; import { ChangeEvent, useState } from "react"; import { CodexSdk } from "../../sdk/codex"; +import { DownloadIcon } from "./DownloadIcon"; export function Download() { const [cid, setCid] = useState(""); @@ -14,15 +15,25 @@ export function Download() { setCid(e.currentTarget.value); return ( -
-
+
+
+
+ +
Download
+
+
+
-
- + +
); } diff --git a/src/components/Download/DownloadIcon.tsx b/src/components/Download/DownloadIcon.tsx new file mode 100644 index 0000000..91e7b3b --- /dev/null +++ b/src/components/Download/DownloadIcon.tsx @@ -0,0 +1,15 @@ +export function DownloadIcon() { + return ( + + + + ); +} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index b419a75..5e2b57e 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -279,7 +279,7 @@ export function Files() {
- +
diff --git a/src/components/ManifestFetch/ManifestFetch.css b/src/components/ManifestFetch/ManifestFetch.css index 18c8832..0ae6013 100644 --- a/src/components/ManifestFetch/ManifestFetch.css +++ b/src/components/ManifestFetch/ManifestFetch.css @@ -1,14 +1,17 @@ -.fetch { +.manifest-fetch { + flex: 1; display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 1rem; -} + gap: 16px; -.fetch-inputContainer { - flex: 1; -} + .input { + flex: 1; + + input { + width: 100%; + } + } -.fetch-input { - width: 100%; + .button { + width: 105px; + } } diff --git a/src/components/ManifestFetch/ManifestFetch.tsx b/src/components/ManifestFetch/ManifestFetch.tsx index 99bfa22..44d4e12 100644 --- a/src/components/ManifestFetch/ManifestFetch.tsx +++ b/src/components/ManifestFetch/ManifestFetch.tsx @@ -43,16 +43,14 @@ export function ManifestFetch() { setCid(e.currentTarget.value); return ( -
-
- -
- +
+ +
); } diff --git a/src/components/ManifestFetch/ManifestFetchCard.tsx b/src/components/ManifestFetch/ManifestFetchCard.tsx new file mode 100644 index 0000000..f8dd4b0 --- /dev/null +++ b/src/components/ManifestFetch/ManifestFetchCard.tsx @@ -0,0 +1,18 @@ +import { ReloadManifest } from "./ReloadManifest"; +import { ManifestFetch } from "./ManifestFetch"; + +export function ManifestFetchCard() { + return ( +
+
+
+ +
Fetch Manifest
+
+
+
+ +
+
+ ); +} diff --git a/src/components/ManifestFetch/ReloadManifest.tsx b/src/components/ManifestFetch/ReloadManifest.tsx new file mode 100644 index 0000000..4d702e9 --- /dev/null +++ b/src/components/ManifestFetch/ReloadManifest.tsx @@ -0,0 +1,15 @@ +export function ReloadManifest() { + return ( + + + + ); +} diff --git a/src/components/Menu/NodesIcon.tsx b/src/components/Menu/NodesIcon.tsx index 241836a..cfbcd15 100644 --- a/src/components/Menu/NodesIcon.tsx +++ b/src/components/Menu/NodesIcon.tsx @@ -3,7 +3,7 @@ type Props = { }; export function NodesIcon({ variant }: Props) { - let color = "currentColor"; + let color = "#969696"; if (variant === "success") { color = "#3EE089"; diff --git a/src/components/NodeSpace/NodeSpace.css b/src/components/NodeSpace/NodeSpace.css new file mode 100644 index 0000000..8c7c82d --- /dev/null +++ b/src/components/NodeSpace/NodeSpace.css @@ -0,0 +1,13 @@ +.node-space { + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + padding-top: 16px; + margin-bottom: 16px; + border-top: 1px solid #96969633; + } +} diff --git a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx b/src/components/NodeSpace/NodeSpace.tsx similarity index 53% rename from src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx rename to src/components/NodeSpace/NodeSpace.tsx index e5cca39..dc8c48c 100644 --- a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx +++ b/src/components/NodeSpace/NodeSpace.tsx @@ -1,9 +1,13 @@ import { useQuery } from "@tanstack/react-query"; import Loader from "../../assets/loader.svg"; import { CodexSdk } from "../../sdk/codex"; -import { SpaceAllocation } from "@codex-storage/marketplace-ui-components"; +import { + Button, + SpaceAllocation, +} from "@codex-storage/marketplace-ui-components"; import { Promises } from "../../utils/promises"; -import { nodeSpaceAllocationColors } from "./nodeSpaceAllocation.domain"; +import { NodesIcon } from "../Menu/NodesIcon"; +import "./NodeSpace.css"; const defaultSpace = { quotaMaxBytes: 0, @@ -12,7 +16,7 @@ const defaultSpace = { totalBlocks: 0, }; -export function NodeSpaceAllocation() { +export function NodeSpace() { const { data: space, isPending } = useQuery({ queryFn: () => CodexSdk.data() @@ -41,24 +45,36 @@ export function NodeSpaceAllocation() { const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space; return ( - +
+
+
+ +
Storage
+
+ +
+
+
Disk
+ + +
+
); } diff --git a/src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts b/src/components/NodeSpace/nodeSpace.domain.ts similarity index 100% rename from src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts rename to src/components/NodeSpace/nodeSpace.domain.ts diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx index 632d390..1232280 100644 --- a/src/components/Peers/PeerCountryCell.tsx +++ b/src/components/Peers/PeerCountryCell.tsx @@ -1,63 +1,23 @@ import { Cell } from "@codex-storage/marketplace-ui-components"; -import { useQuery } from "@tanstack/react-query"; import "./PeerCountryCell.css"; -import { useEffect } from "react"; -import { PeerPin, PeerUtils } from "./peers.util"; +import { PeerGeo, PeerNode, PeerUtils } from "./peers.util"; export type Props = { - address: string; - onPinAdd: (pin: PeerPin & { countryIso: string; ip: string }) => void; + node: PeerNode; + geo: PeerGeo | undefined; }; -export function PeerCountryCell({ address, onPinAdd }: Props) { - const { data } = useQuery({ - queryFn: () => { - const [ip] = address.split(":"); - - return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then( - (res) => res.json() - ); - }, - refetchOnMount: true, - - queryKey: [address], - - // Enable only when the address exists - enabled: !!address, - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // We can cache the data at Infinity because the relation between - // country and ip is fixed - staleTime: Infinity, - - // Don't expect something new when coming back to the UI - refetchOnWindowFocus: false, - }); - - useEffect(() => { - if (data) { - onPinAdd({ - lat: data.latitude, - lng: data.longitude, - countryIso: data.country_iso, - ip: data.ip, - }); - } - }, [data, onPinAdd]); - +export function PeerCountryCell({ geo }: Props) { return (
- {data ? ( + {geo ? ( <> - {!!data && PeerUtils.geCountryEmoji(data.country_iso)} - {data?.country} + {!!geo && PeerUtils.geCountryEmoji(geo.country_iso)} + {geo?.country} ) : ( - {address} + )}
diff --git a/src/components/Peers/Peers.css b/src/components/Peers/Peers.css index b540a7f..2a5c2fe 100644 --- a/src/components/Peers/Peers.css +++ b/src/components/Peers/Peers.css @@ -23,13 +23,6 @@ } } - circle[fill="#d6ff79"] { - stroke: var(--codex-color-primary); - stroke-width: 0.6px; - fill: #141414; - animation: circle-pulse 3s infinite; - } - ul { display: none; @@ -145,85 +138,9 @@ } } - main { - > div { - position: relative; - width: 350px; - height: 175px; - overflow: hidden; - transform: scale(0.5); - margin: auto; - left: -32px; - - @media (min-width: 1000px) { - & { - transform: scale(0.73); - } - } - - *, - &::before { - box-sizing: border-box; - } - - &::before, - &::after { - position: absolute; - } - - &::before { - content: ""; - width: inherit; - height: inherit; - border: 45px solid #323232; - border-bottom: none; - border-top-left-radius: 175px; - border-top-right-radius: 175px; - } - - div { - position: absolute; - top: 100%; - width: inherit; - height: inherit; - border: 45px solid #323232; - border-top: none; - border-bottom-left-radius: 175px; - border-bottom-right-radius: 175px; - transform-origin: 50% 0; - } - - div:nth-child(1) { - border-color: var(--codex-color-primary); - transform: rotate(calc(var(--codex-peers-degrees) * 1deg)); - background-color: transparent; - } - } - - span { - font-family: Inter; - font-size: 38.67px; - font-weight: 500; - line-height: 48.34px; - letter-spacing: -0.005em; - text-align: center; - position: absolute; - bottom: 0; - left: calc(50% - 24px); - } - } - footer { border-top: 1px solid #96969633; padding-top: 16px; - display: flex; - align-items: center; - gap: 8px; - font-family: Inter; - font-size: 14px; - font-weight: 400; - line-height: 20px; - letter-spacing: -0.006em; } } } @@ -255,16 +172,3 @@ } } } - -@keyframes circle-pulse { - 0% { - opacity: 1; /* Fully opaque */ - } - 50% { - r: 2; /* Increased radius */ - opacity: 1; /* Slightly transparent */ - } - 100% { - opacity: 1; /* Fully opaque */ - } -} diff --git a/src/components/Peers/Peers.tsx b/src/components/Peers/Peers.tsx index 471118b..11cd5ce 100644 --- a/src/components/Peers/Peers.tsx +++ b/src/components/Peers/Peers.tsx @@ -4,72 +4,40 @@ import { Cell, Table, } from "@codex-storage/marketplace-ui-components"; -import DottedMap from "dotted-map/without-countries"; -import { useRef, useState, useCallback } from "react"; +import { useCallback, useState } from "react"; import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon"; import { PeersIcon } from "../Menu/PeersIcon"; import { PeerCountryCell } from "./PeerCountryCell"; import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon"; -import { useDebug } from "../../hooks/useDebug"; -import { getMapJSON } from "dotted-map"; import "./Peers.css"; -import { PeerPin, PeerSortFn, PeerUtils } from "./peers.util"; - -// This function accepts the same arguments as DottedMap in the example above. -const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" }); - -type CustomCSSProperties = React.CSSProperties & { - "--codex-peers-degrees": number; -}; +import { PeerGeo, PeerNode, PeerSortFn, PeerUtils } from "./peers.util"; +import { PeersMap } from "./PeersMap"; +import { useDebug } from "../../hooks/useDebug"; +import { PeersQuality } from "./PeersQuality"; +import { PeersChart } from "./PeersChart"; const throwOnError = true; export const Peers = () => { - const ips = useRef>({}); - const [pins, setPins] = useState<[PeerPin, number][]>([]); - const [sortFn, setSortFn] = useState(() => - PeerUtils.sortByBoolean("desc") - ); const { data } = useDebug(throwOnError); + const [ips, setIps] = useState>({}); - const onPinAdd = useCallback( - ({ - countryIso, - ip, - ...pin - }: PeerPin & { countryIso: string; ip: string }) => { - setPins((val) => PeerUtils.incPin(val, pin)); - ips.current[ip] = countryIso; - }, - [] - ); - - // It’s safe to re-create the map at each render, because of the - // pre-computation it’s super fast ⚡️ - const map = new DottedMap({ map: JSON.parse(mapJsonString) }); + const onPinAdd = useCallback((node: PeerNode, geo: PeerGeo) => { + const [ip = ""] = node.address.split(":"); + setIps((ips) => ({ ...ips, [ip]: geo })); + }, []); - pins.map(([pin, quantity]) => - map.addPin({ - lat: pin.lat, - lng: pin.lng, - svgOptions: { color: "#d6ff79", radius: 0.8 * quantity }, - }) + const [sortFn, setSortFn] = useState(() => + PeerUtils.sortByBoolean("desc") ); - const svgMap = map.getSVG({ - radius: 0.32, - color: "#969696", - shape: "circle", - backgroundColor: "#141414", - }); - const onSortByCountry = (state: TabSortState) => { if (!state) { setSortFn(null); return; } - setSortFn(() => PeerUtils.sortByCountry(state, ips.current)); + setSortFn(() => PeerUtils.sortByCountry(state, ips)); }; const onSortActive = (state: TabSortState) => { @@ -87,42 +55,41 @@ export const Peers = () => { ["Active", onSortActive], ] satisfies [string, ((state: TabSortState) => void)?][]; - const nodes = data?.table?.nodes || []; + const nodes = data?.table.nodes || []; const sorted = sortFn ? nodes.slice().sort(sortFn) : nodes; - const rows = sorted.map((node) => ( - , - {node.peerId}, - - {node.seen ? ( -
- Active -
- ) : ( -
- Inactive -
- )} -
, - ]}>
- )); + const rows = sorted.map((node) => { + const [ip = ""] = node.address.split(":"); + const geo = ips[ip]; + + return ( + , + {node.peerId}, + + {node.seen ? ( +
+ Active +
+ ) : ( +
+ Inactive +
+ )} +
, + ]}>
+ ); + }); const actives = PeerUtils.countActives(sorted); const degrees = PeerUtils.calculareDegrees(sorted); - const good = actives > 0; - - const styles: CustomCSSProperties = { - "--codex-peers-degrees": degrees, - }; + const good = PeerUtils.isGoodQuality(actives); return (
-
+
  • Legend
  • @@ -135,24 +102,11 @@ export const Peers = () => { Connections -
    -
    -
    - {actives} -
    +
    +
    - {good ? ( - <> - - Peer connections in good standing. - - ) : ( - <> - - No peer connection active. - - )} +
diff --git a/src/components/Peers/PeersCard.css b/src/components/Peers/PeersCard.css new file mode 100644 index 0000000..c5007ed --- /dev/null +++ b/src/components/Peers/PeersCard.css @@ -0,0 +1,23 @@ +.peers-card { + position: relative; + + .peers-map { + border-top: 1px solid #96969633; + border-bottom: 1px solid #96969633; + width: 100%; + + svg { + width: 100%; + } + } + + .peers-chart { + position: absolute; + left: 0; + right: 0; + } + + footer { + padding-top: 16px; + } +} diff --git a/src/components/Peers/PeersCard.tsx b/src/components/Peers/PeersCard.tsx new file mode 100644 index 0000000..a705426 --- /dev/null +++ b/src/components/Peers/PeersCard.tsx @@ -0,0 +1,38 @@ +import { PeersIcon } from "../Menu/PeersIcon"; +import { PeersMap } from "./PeersMap"; +import "./PeersCard.css"; +import { useDebug } from "../../hooks/useDebug"; +import { PeerUtils } from "./peers.util"; +import { PeersChart } from "./PeersChart"; +import { PeersQuality } from "./PeersQuality"; +import { Button } from "@codex-storage/marketplace-ui-components"; + +const throwOnError = true; + +export function PeersCard() { + const { data } = useDebug(throwOnError); + + const nodes = data?.table.nodes ?? []; + const actives = PeerUtils.countActives(nodes); + const degrees = PeerUtils.calculareDegrees(nodes); + const good = PeerUtils.isGoodQuality(actives); + + return ( +
+
+
+ +
Peers
+
+ +
+
+ + +
+
+ +
+
+ ); +} diff --git a/src/components/Peers/PeersChart.css b/src/components/Peers/PeersChart.css new file mode 100644 index 0000000..a500a3f --- /dev/null +++ b/src/components/Peers/PeersChart.css @@ -0,0 +1,65 @@ +.peers-chart { + position: relative; + width: 350px; + height: 175px; + overflow: hidden; + transform: scale(0.5); + margin: auto; + left: -32px; + + @media (min-width: 1000px) { + & { + transform: scale(0.73); + } + } + + *, + &::before { + box-sizing: border-box; + } + + &::before, + &::after { + position: absolute; + } + + &::before { + content: ""; + width: inherit; + height: inherit; + border: 45px solid #323232; + border-bottom: none; + border-top-left-radius: 175px; + border-top-right-radius: 175px; + } + + div { + position: absolute; + top: 100%; + width: inherit; + height: inherit; + border: 45px solid #323232; + border-top: none; + border-bottom-left-radius: 175px; + border-bottom-right-radius: 175px; + transform-origin: 50% 0; + } + + div:nth-child(1) { + border-color: var(--codex-color-primary); + transform: rotate(calc(var(--codex-peers-degrees) * 1deg)); + background-color: transparent; + } + + span { + font-family: Inter; + font-size: 38.67px; + font-weight: 500; + line-height: 48.34px; + letter-spacing: -0.005em; + text-align: center; + position: absolute; + bottom: 0; + left: calc(50% - 24px); + } +} diff --git a/src/components/Peers/PeersChart.tsx b/src/components/Peers/PeersChart.tsx new file mode 100644 index 0000000..3406f87 --- /dev/null +++ b/src/components/Peers/PeersChart.tsx @@ -0,0 +1,23 @@ +import "./PeersChart.css"; + +type Props = { + actives: number; + degrees: number; +}; + +type CustomCSSProperties = React.CSSProperties & { + "--codex-peers-degrees": number; +}; + +export function PeersChart({ actives, degrees }: Props) { + const style: CustomCSSProperties = { + "--codex-peers-degrees": degrees, + }; + + return ( +
+
+ {actives} +
+ ); +} diff --git a/src/components/Peers/PeersMap.css b/src/components/Peers/PeersMap.css new file mode 100644 index 0000000..d88c745 --- /dev/null +++ b/src/components/Peers/PeersMap.css @@ -0,0 +1,21 @@ +.peers-map { + circle[fill="#d6ff79"] { + stroke: var(--codex-color-primary); + stroke-width: 0.6px; + fill: #141414; + animation: circle-pulse 3s infinite; + } +} + +@keyframes circle-pulse { + 0% { + opacity: 1; /* Fully opaque */ + } + 50% { + r: 2; /* Increased radius */ + opacity: 1; /* Slightly transparent */ + } + 100% { + opacity: 1; /* Fully opaque */ + } +} diff --git a/src/components/Peers/PeersMap.tsx b/src/components/Peers/PeersMap.tsx new file mode 100644 index 0000000..7391302 --- /dev/null +++ b/src/components/Peers/PeersMap.tsx @@ -0,0 +1,56 @@ +import { getMapJSON } from "dotted-map"; +import DottedMap from "dotted-map/without-countries"; +import { PeerGeo, PeerNode, PeerUtils } from "./peers.util"; +import { useCallback, useState } from "react"; +import { PeersPin } from "./PeersPin"; +import "./PeersMap.css"; + +// This function accepts the same arguments as DottedMap in the example above. +const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" }); + +type Props = { + nodes: PeerNode[]; + onPinAdd?: (node: PeerNode, geo: PeerGeo) => void; +}; + +export function PeersMap({ nodes, onPinAdd }: Props) { + // It’s safe to re-create the map at each render, because of the + // pre-computation it’s super fast ⚡️ + const map = new DottedMap({ map: JSON.parse(mapJsonString) }); + + const [pins, setPins] = useState<[PeerNode & PeerGeo, number][]>([]); + + const onInternalPinAdd = useCallback( + (node: PeerNode, geo: PeerGeo) => { + setPins((val) => PeerUtils.incPin(val, { ...node, ...geo })); + onPinAdd?.(node, geo); + }, + [setPins] + ); + + pins.map(([pin, quantity]) => + map.addPin({ + lat: pin.latitude, + lng: pin.longitude, + svgOptions: { color: "#d6ff79", radius: 0.4 * quantity }, + }) + ); + + const svgMap = map.getSVG({ + radius: 0.32, + color: "#969696", + shape: "circle", + backgroundColor: "#141414", + }); + + return ( + <> + {nodes.map((node, index) => ( + + ))} +
+ + ); +} diff --git a/src/components/Peers/PeersPin.ts b/src/components/Peers/PeersPin.ts new file mode 100644 index 0000000..d74c596 --- /dev/null +++ b/src/components/Peers/PeersPin.ts @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; +import { PeerGeo, PeerNode } from "./peers.util"; +import { useEffect } from "react"; + +type Props = { + node: PeerNode + onLoad: (node: PeerNode, geo: PeerGeo) => void +} + +export function PeersPin({ node, onLoad }: Props) { + const { data } = useQuery({ + queryFn: () => { + const [ip] = node.address.split(":"); + + return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then( + (res) => res.json() + ); + }, + queryKey: ["peers", node.address], + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // We can cache the data at Infinity because the relation between + // country and ip is fixed + staleTime: Infinity, + + // Don't expect something new when coming back to the UI + refetchOnWindowFocus: false, + + refetchOnMount: false, + }); + + useEffect(() => { + if (data) { + onLoad(node, data) + console.info(node.address) + } + }, [data, onLoad, node]) + + + return "" +} \ No newline at end of file diff --git a/src/components/Peers/PeersQuality.css b/src/components/Peers/PeersQuality.css new file mode 100644 index 0000000..712f541 --- /dev/null +++ b/src/components/Peers/PeersQuality.css @@ -0,0 +1,10 @@ +.peers-quality { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; +} diff --git a/src/components/Peers/PeersQuality.tsx b/src/components/Peers/PeersQuality.tsx new file mode 100644 index 0000000..9521fa2 --- /dev/null +++ b/src/components/Peers/PeersQuality.tsx @@ -0,0 +1,25 @@ +import { ErrorCircleIcon } from "../ErrorCircleIcon/ErrorCircleIcon"; +import { SuccessCheckIcon } from "../SuccessCheckIcon/SuccessCheckIcon"; +import "./PeersQuality.css"; + +type Props = { + good: boolean; +}; + +export function PeersQuality({ good }: Props) { + if (good) { + return ( +
+ + Peer connections in good standing. +
+ ); + } + + return ( +
+ + No peer connection active. +
+ ); +} diff --git a/src/components/Peers/peers.util.test.ts b/src/components/Peers/peers.util.test.ts index 95d4dbf..326a728 100644 --- a/src/components/Peers/peers.util.test.ts +++ b/src/components/Peers/peers.util.test.ts @@ -1,5 +1,5 @@ import { assert, describe, it } from "vitest"; -import { PeerUtils } from "./peers.util"; +import { PeerGeo, PeerUtils } from "./peers.util"; describe("peers", () => { it("sorts by boolean", async () => { @@ -19,10 +19,16 @@ describe("peers", () => { it("sorts by country", async () => { const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false } const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true } + const table = { - "127.0.0.1": "US", - "127.0.0.2": "France", + "127.0.0.1": { + country: "United States" + } as PeerGeo, + "127.0.0.2": { + country: "France" + } as PeerGeo, } + const items = [a, b,] const descSorted = items.slice().sort(PeerUtils.sortByCountry("desc", table)) @@ -35,14 +41,14 @@ describe("peers", () => { }); it("adds a new pin", async () => { - const latLng = { lat: 0, lng: 0 } + const latLng = { latitude: 0, longitude: 0 } as any const values = PeerUtils.incPin([], latLng) assert.deepEqual(values, [[latLng, 1]]); }); it("increments an existing pin", async () => { - const latLng = { lat: 0, lng: 0 } + const latLng = { lat: 0, lng: 0 } as any const values = PeerUtils.incPin([[latLng, 1]], latLng) assert.deepEqual(values, [[latLng, 2]]); diff --git a/src/components/Peers/peers.util.ts b/src/components/Peers/peers.util.ts index 7d40ed5..bfd6c05 100644 --- a/src/components/Peers/peers.util.ts +++ b/src/components/Peers/peers.util.ts @@ -8,10 +8,12 @@ export type PeerNode = { seen: boolean; }; -export type PeerPin = { - lat: number; - lng: number; -}; +export type PeerGeo = { + latitude: number + longitude: number + country: string + country_iso: string +} export type PeerSortFn = (a: PeerNode, b: PeerNode) => number; @@ -21,12 +23,12 @@ export const PeerUtils = { return a?.seen === b?.seen ? 0 : b?.seen ? order : -order; }, - sortByCountry: (state: TabSortState, ipTable: Record) => + sortByCountry: (state: TabSortState, ipTable: Record) => (a: PeerNode, b: PeerNode) => { const [ipA = ""] = a.address.split(":") const [ipB = ""] = b.address.split(":") - const countryA = ipTable[ipA] || ""; - const countryB = ipTable[ipB] || ""; + const countryA = ipTable[ipA].country || ""; + const countryB = ipTable[ipB].country || ""; return state === "desc" ? countryA.localeCompare(countryB) @@ -36,10 +38,10 @@ export const PeerUtils = { /** * Increments the number of pin for a location */ - incPin(val: [PeerPin, number][], pin: PeerPin): [PeerPin, number][] { + incPin(val: [PeerNode & PeerGeo, number][], pin: PeerNode & PeerGeo): [PeerNode & PeerGeo, number][] { const [, quantity = 0] = - val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || []; - const rest = val.filter(([p]) => p.lat !== pin.lat || p.lng !== pin.lng) + val.find(([p]) => p.latitude === pin.latitude && p.longitude == pin.longitude) || []; + const rest = val.filter(([p]) => p.latitude !== pin.latitude || p.longitude !== pin.longitude) return [...rest, [pin, quantity + 1]]; }, @@ -53,6 +55,10 @@ export const PeerUtils = { return (actives / total) * 180 }, + isGoodQuality(actives: number) { + return actives > 0 + }, + geCountryEmoji: (countryCode: string) => { const codePoints = countryCode .toUpperCase() diff --git a/src/components/PlusIcon/PlusIcon.tsx b/src/components/PlusIcon/PlusIcon.tsx new file mode 100644 index 0000000..c4f4795 --- /dev/null +++ b/src/components/PlusIcon/PlusIcon.tsx @@ -0,0 +1,15 @@ +export function PlusIcon() { + return ( + + + + ); +} diff --git a/src/components/UploadCard/UploadCard.tsx b/src/components/UploadCard/UploadCard.tsx new file mode 100644 index 0000000..5e9c1e5 --- /dev/null +++ b/src/components/UploadCard/UploadCard.tsx @@ -0,0 +1,25 @@ +import { Upload, UploadIcon } from "@codex-storage/marketplace-ui-components"; +import { CodexSdk } from "../../sdk/codex"; +import { useQueryClient } from "@tanstack/react-query"; + +export function UploadCard() { + const queryClient = useQueryClient(); + + const onSuccess = () => { + queryClient.invalidateQueries({ queryKey: ["cids"] }); + }; + + return ( +
+
+
+ +
Upload
+
+
+
+ +
+
+ ); +} diff --git a/src/components/Welcome/DiscordIcon.tsx b/src/components/Welcome/DiscordIcon.tsx new file mode 100644 index 0000000..7bb9690 --- /dev/null +++ b/src/components/Welcome/DiscordIcon.tsx @@ -0,0 +1,15 @@ +export function DiscordIcon() { + return ( + + + + ); +} diff --git a/src/components/Welcome/Welcome.css b/src/components/Welcome/Welcome.css deleted file mode 100644 index 3530439..0000000 --- a/src/components/Welcome/Welcome.css +++ /dev/null @@ -1,35 +0,0 @@ -.welcome { - border-radius: var(--codex-border-radius); - border: 1px solid var(--codex-border-color); - background-color: var(--codex-background-secondary); - padding: 1rem 1.5rem; - display: flex; - align-items: flex-start; - flex-direction: column; - flex: 1; -} - -.welcome-disclaimer { - margin: 1rem 0; -} - -.welcome-title { - font-weight: bold; - font-size: 1.125rem; - line-height: 1.75rem; - margin-bottom: 0.75rem; -} - -.welcome-body { - flex: 1; -} - -.welcome-link { - display: flex; - align-items: center; - color: var(--codex-color-primary); -} - -.welcome-link:hover { - text-decoration: underline; -} diff --git a/src/components/Welcome/Welcome.tsx b/src/components/Welcome/Welcome.tsx deleted file mode 100644 index 65a6090..0000000 --- a/src/components/Welcome/Welcome.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import "./Welcome.css"; -import { Link } from "@tanstack/react-router"; -import { ChevronRight } from "lucide-react"; - -export function Welcome() { - return ( -
-

Welcome to Codex Marketplace

-
- - Begin your journey with Codex by uploading new files for testing. - Experience the power of our decentralized data storage platform and - explore its features. Your feedback is invaluable as we continue to - improve! - -
- - - Explore more content - -
- ); -} diff --git a/src/components/Welcome/WelcomeCard.css b/src/components/Welcome/WelcomeCard.css new file mode 100644 index 0000000..4c3c987 --- /dev/null +++ b/src/components/Welcome/WelcomeCard.css @@ -0,0 +1,102 @@ +.welcome-card { + display: flex; + flex-direction: column; + + > div { + padding: 16px; + background-color: #141414; + background-image: url(img/welcome.png); + background-repeat: no-repeat; + background-position: right; + flex: 1; + } + + main { + max-width: 400px; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + + h6 { + font-family: Inter; + font-size: 20px; + font-weight: 400; + line-height: 24.2px; + color: var(--codex-color-primary); + } + + p { + margin-top: 16px; + margin-bottom: 32px; + font-family: Azeret Mono; + font-size: 12px; + font-weight: 400; + line-height: 14px; + text-align: left; + color: #7f948d; + } + + div { + display: flex; + align-items: center; + justify-content: space-between; + } + + a { + font-family: Inter; + font-size: 16px; + font-weight: 600; + line-height: 19.36px; + letter-spacing: 0.01em; + text-align: left; + color: #7bfbaf; + display: flex; + align-items: center; + + &:nth-child(2) { + gap: 12px; + } + } + } + + footer { + margin-top: 32px; + } +} + +.welcome { + border-radius: var(--codex-border-radius); + border: 1px solid var(--codex-border-color); + background-color: var(--codex-background-secondary); + padding: 1rem 1.5rem; + display: flex; + align-items: flex-start; + flex-direction: column; + flex: 1; +} + +.welcome-disclaimer { + margin: 1rem 0; +} + +.welcome-title { + font-weight: bold; + font-size: 1.125rem; + line-height: 1.75rem; + margin-bottom: 0.75rem; +} + +.welcome-body { + flex: 1; +} + +.welcome-link { + display: flex; + align-items: center; + color: var(--codex-color-primary); +} + +.welcome-link:hover { + text-decoration: underline; +} diff --git a/src/components/Welcome/WelcomeCard.tsx b/src/components/Welcome/WelcomeCard.tsx new file mode 100644 index 0000000..f36b3bb --- /dev/null +++ b/src/components/Welcome/WelcomeCard.tsx @@ -0,0 +1,47 @@ +import "./WelcomeCard.css"; +import { Link } from "@tanstack/react-router"; +import { ArrowRight } from "lucide-react"; +import { Logo } from "../Logo/Logo"; +import { Logotype } from "../Logotype/Logotype"; +import { DiscordIcon } from "./DiscordIcon"; +import { Alert } from "@codex-storage/marketplace-ui-components"; + +export function WelcomeCard() { + return ( +
+
+
+
+ + +
+
+
+
+ Begin your journey with Codex by uploading new files for testing. +
+

+ Experience the power of our decentralized data storage platform and + explore its features. Your feedback is invaluable as we continue to + improve! +

+
+ + Explore more content + + + + Join Codex Discord + +
+
+
+ + The website and the content herein is not intended for public use + and is for informational and demonstration purposes only. + +
+
+
+ ); +} diff --git a/src/index.css b/src/index.css index 0271dd1..791f9c6 100644 --- a/src/index.css +++ b/src/index.css @@ -30,7 +30,7 @@ --codex-color-on-primary: #333; --codex-color-disabled: #717171; --codex-color-light: rgb(150 150 150); - --codex-border-color: #2b303b; + --codex-border-color: #96969633; --codex-input-border-color: #494949; --codex-background-secondary: rgb(38 38 38); --codex-highlight-color: #2f2f2f; @@ -130,7 +130,7 @@ ul { padding: 0; } -main { +body > main { flex: 1; display: flex; flex-direction: column; @@ -181,3 +181,36 @@ input:-webkit-autofill:active { .gap { gap: var(--codex-row-gap); } + +.card { + border: 1px solid #96969633; + border-radius: 16px; + padding: 16px; + background-color: #232323; + + > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + + svg { + color: #969696; + } + + > div { + display: flex; + align-items: center; + gap: 12px; + } + + h5 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + } + } +} diff --git a/src/routes/dashboard/favorites.tsx b/src/routes/dashboard/favorites.tsx index 7b9dd9c..c96d39f 100644 --- a/src/routes/dashboard/favorites.tsx +++ b/src/routes/dashboard/favorites.tsx @@ -2,19 +2,16 @@ import { createFileRoute } from "@tanstack/react-router"; import { Files } from "../../components/Files/Files"; import { ErrorBoundary } from "@sentry/react"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; -import { Card } from "@codex-storage/marketplace-ui-components"; export const Route = createFileRoute("/dashboard/favorites")({ component: () => ( <> ( - - - + )}>
diff --git a/src/routes/dashboard/index.css b/src/routes/dashboard/index.css index 68f050e..a1c58e6 100644 --- a/src/routes/dashboard/index.css +++ b/src/routes/dashboard/index.css @@ -1,6 +1,7 @@ .dashboard { display: flex; justify-content: space-between; + margin-bottom: 16px; h3 { font-family: Inter; @@ -32,3 +33,31 @@ font-size: 26px; } } + +.dashboard-body { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.welcome-card, +.connected-account { + min-width: 550px; + display: flex; + flex-direction: column; + flex: 1; +} + +.column { + min-width: 400px; +} + +.files { + flex: 1; + min-width: 100%; +} + +@media (min-width: 1800px) { + .dashboard-body { + } +} diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 944b966..01dc9c6 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1,28 +1,23 @@ import { createFileRoute } from "@tanstack/react-router"; import { Files } from "../../components/Files/Files.tsx"; -import { Alert, Card, Upload } from "@codex-storage/marketplace-ui-components"; -import { CodexSdk } from "../../sdk/codex"; -import { Welcome } from "../../components/Welcome/Welcome.tsx"; +import { WelcomeCard } from "../../components/Welcome/WelcomeCard.tsx"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder.tsx"; import { ErrorBoundary } from "@sentry/react"; -import { useQueryClient } from "@tanstack/react-query"; import { Download } from "../../components/Download/Download.tsx"; -import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch.tsx"; import "./index.css"; import { Versions } from "../../components/Versions/Versions.tsx"; import { WebStorage } from "../../utils/web-storage.ts"; +import { ConnectedAccount } from "../../components/ConnectedAccount/ConnectedAccount.tsx"; +import { NodeSpace } from "../../components/NodeSpace/NodeSpace.tsx"; +import { UploadCard } from "../../components/UploadCard/UploadCard.tsx"; +import { ManifestFetchCard } from "../../components/ManifestFetch/ManifestFetchCard.tsx"; +import { PeersCard } from "../../components/Peers/PeersCard.tsx"; export const Route = createFileRoute("/dashboard/")({ component: Dashboard, }); function Dashboard() { - const queryClient = useQueryClient(); - - const onSuccess = () => { - queryClient.invalidateQueries({ queryKey: ["cids"] }); - }; - const username = WebStorage.onBoarding.getDisplayName(); const emoji = WebStorage.onBoarding.getEmoji(); @@ -40,22 +35,20 @@ function Dashboard() {
-
- ( - - )}> - - - - +
+ +
+ ( + + )}> + + + +
( @@ -64,11 +57,23 @@ function Dashboard() { subtitle="Cannot retrieve the data." /> )}> - - - + +
+ ( + + )}> + + + + +
+ ( )}> - - - +
- - ( - - )}> -
- - - - The website and the content herein is not intended for public use - and is for informational and demonstration purposes only. - -
-
-
- -
- ( - - - - )}> - -
);