From 8fcaa80248453f02ecb75e22f6856704c818bf61 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Thu, 29 Aug 2024 18:45:52 +0200 Subject: [PATCH] Apply lot of improvements --- package-lock.json | 26 ++ package.json | 7 +- public/button-loader.svg | 23 -- src/App.tsx | 9 +- src/CodexUrllSettings/CodexUrlSettings.tsx | 51 +++ src/components/CardNumbers/CardNumbers.css | 50 ++- src/components/CardNumbers/CardNumbers.tsx | 112 ++++- .../CustomStateCellRender.css | 9 + .../CustomStateCellRender.tsx | 49 +++ src/components/FileCellRender/FIleCell.tsx | 58 +++ src/components/FileCellRender/FileCell.css | 20 + src/components/Files/CidCopyButton.tsx | 39 ++ src/components/Files/FileDetails.css | 78 ++++ src/components/Files/FileDetails.tsx | 92 +++++ src/components/Files/Files.css | 110 +++++ src/components/Files/Files.tsx | 163 ++++++++ src/components/LogLevel/LogLevel.tsx | 6 +- src/components/Manifests/Manitests.css | 129 ------ src/components/Manifests/Manitests.tsx | 256 ------------ src/components/Range/Range.css | 11 + src/components/Range/Range.tsx | 45 ++ .../StorageRequestDone.tsx | 30 ++ .../StorageRequestFileChooser.tsx | 55 +-- .../StorageRequestReview.css | 35 +- .../StorageRequestReview.tsx | 391 ++++++++++++++---- .../StorageRequestSetup.css | 40 +- .../StorageRequestStepper.tsx | 157 +++---- .../TruncateCellRender/TruncateCellRender.tsx | 4 + src/components/Welcome/Welcome.css | 30 ++ src/components/Welcome/Welcome.tsx | 21 + src/hooks/useData.tsx | 49 +++ src/index.css | 17 +- src/routes/dashboard.tsx | 41 +- src/routes/dashboard/about.tsx | 2 +- src/routes/dashboard/favorites.tsx | 4 +- src/routes/dashboard/help.css | 35 ++ src/routes/dashboard/help.tsx | 38 +- src/routes/dashboard/index.tsx | 49 +-- src/routes/dashboard/purchases.css | 6 - src/routes/dashboard/purchases.tsx | 56 ++- src/routes/dashboard/settings.css | 19 +- src/routes/dashboard/settings.tsx | 18 +- src/routes/index.tsx | 9 +- src/sdk/codex.ts | 48 ++- src/utils/browser-storage.ts | 18 - src/utils/constants.ts | 2 + src/utils/dates.ts | 16 +- src/utils/favorite-storage.tsx | 17 + src/utils/file-storage.ts | 27 ++ src/utils/files.ts | 11 +- src/utils/purchases-storage.ts | 13 + src/workers/upload-worker.ts | 39 -- 52 files changed, 1826 insertions(+), 814 deletions(-) delete mode 100644 public/button-loader.svg create mode 100644 src/CodexUrllSettings/CodexUrlSettings.tsx create mode 100644 src/components/CustomStateCellRender/CustomStateCellRender.css create mode 100644 src/components/CustomStateCellRender/CustomStateCellRender.tsx create mode 100644 src/components/FileCellRender/FIleCell.tsx create mode 100644 src/components/FileCellRender/FileCell.css create mode 100644 src/components/Files/CidCopyButton.tsx create mode 100644 src/components/Files/FileDetails.css create mode 100644 src/components/Files/FileDetails.tsx create mode 100644 src/components/Files/Files.css create mode 100644 src/components/Files/Files.tsx delete mode 100644 src/components/Manifests/Manitests.css delete mode 100644 src/components/Manifests/Manitests.tsx create mode 100644 src/components/Range/Range.css create mode 100644 src/components/Range/Range.tsx create mode 100644 src/components/StorageRequestSetup/StorageRequestDone.tsx create mode 100644 src/components/TruncateCellRender/TruncateCellRender.tsx create mode 100644 src/components/Welcome/Welcome.css create mode 100644 src/components/Welcome/Welcome.tsx create mode 100644 src/hooks/useData.tsx create mode 100644 src/routes/dashboard/help.css delete mode 100644 src/utils/browser-storage.ts create mode 100644 src/utils/favorite-storage.tsx create mode 100644 src/utils/file-storage.ts create mode 100644 src/utils/purchases-storage.ts delete mode 100644 src/workers/upload-worker.ts diff --git a/package-lock.json b/package-lock.json index ba1a81c..233e265 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "classnames": "^2.5.1", "idb-keyval": "^6.2.1", "lucide-react": "^0.424.0", + "pretty-ms": "^9.1.0", "react": "^18.3.1", "react-dom": "^18.3.1" }, @@ -3048,6 +3049,17 @@ "node": ">=6" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3154,6 +3166,20 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", + "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 6c3b111..46f838d 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,16 @@ "React" ], "dependencies": { + "@codex/marketplace-ui-components": "@codex/marketplace-ui-components#master", + "@codex/sdk-js": "@codex/marketplace-ui#master", "@tanstack/react-query": "^5.51.15", "@tanstack/react-router": "^1.45.7", "classnames": "^2.5.1", "idb-keyval": "^6.2.1", "lucide-react": "^0.424.0", + "pretty-ms": "^9.1.0", "react": "^18.3.1", - "react-dom": "^18.3.1", - "@codex/sdk-js": "@codex/marketplace-ui#master", - "@codex/marketplace-ui-components": "@codex/marketplace-ui-components#master" + "react-dom": "^18.3.1" }, "devDependencies": { "@tanstack/router-devtools": "^1.45.7", diff --git a/public/button-loader.svg b/public/button-loader.svg deleted file mode 100644 index b623e1a..0000000 --- a/public/button-loader.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - diff --git a/src/App.tsx b/src/App.tsx index 0c71b8e..69f51f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,5 @@ -import { ReactNode, useEffect } from "react"; +import { ReactNode } from "react"; import "./App.css"; -import { useNetwork } from "./network/useNetwork.ts"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; const queryClient = new QueryClient(); @@ -10,12 +9,6 @@ interface Props { } function App({ children }: Props) { - const online = useNetwork(); - - useEffect(() => { - console.info("The network is now", online ? "online" : "offline"); - }, [online]); - return ( {children} ); diff --git a/src/CodexUrllSettings/CodexUrlSettings.tsx b/src/CodexUrllSettings/CodexUrlSettings.tsx new file mode 100644 index 0000000..691c9f5 --- /dev/null +++ b/src/CodexUrllSettings/CodexUrlSettings.tsx @@ -0,0 +1,51 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { Button, Input, Toast } from "@codex/marketplace-ui-components"; +import { CircleCheck } from "lucide-react"; +import { CodexSdk } from "../sdk/codex"; + +export function CodexUrlSettings() { + const queryClient = useQueryClient(); + const [url, setUrl] = useState(""); + const [toast, setToast] = useState({ time: 0, message: "" }); + + useEffect(() => { + CodexSdk.url().then((u) => setUrl(u)); + }, []); + + const onChange = (e: React.FormEvent) => { + const value = e.currentTarget.value; + if (value) { + setUrl(value); + } + }; + + const onClick = () => { + CodexSdk.updateURL(url).then(() => { + queryClient.invalidateQueries(); + setToast({ message: "Settigns saved successfully.", time: Date.now() }); + }); + }; + + const Check = () => ( + + ); + + console.info({ url }); + + return ( + <> + + + + + ); +} diff --git a/src/components/CardNumbers/CardNumbers.css b/src/components/CardNumbers/CardNumbers.css index a0064ee..a51730e 100644 --- a/src/components/CardNumbers/CardNumbers.css +++ b/src/components/CardNumbers/CardNumbers.css @@ -2,17 +2,29 @@ border-radius: var(--codex-border-radius); border: 1px solid var(--codex-border-color); font-family: var(--codex-font-family); - padding: 1.5rem; + padding: 0.5rem 1rem; background-color: rgb(56 56 56); + display: flex; + flex-direction: column; +} + +.cardNumber--error { + border-color: rgb(var(--codex-color-error)); +} + +.cardNumber--error .cardNumber-tooltip { + color: rgb(var(--codex-color-error)); } .cardNumber-title { display: inline-block; + font-size: 0.9rem; } .cardNumber-data { - font-size: 3rem; + font-size: 2rem; color: var(--codex-color-primary); + margin-bottom: 0.5rem; } .cardNumber-data:focus-visible { @@ -22,10 +34,36 @@ .cardNumber-dataContainer { position: relative; + flex: 1; + + display: flex; + align-items: flex-start; + gap: 0.5rem; + position: relative; + top: 0px; + /* --codex-button-icon-background: var(--codex-color-primary); */ + justify-content: space-between; + align-items: center; } -.cardNumber-dataIcon { - position: absolute; - right: 0; - bottom: 0; +.cardNumber-dataContainer .buttonIcon { + position: relative; + top: 3px; +} + +.cardNumber-tooltip { + color: var(--codex-color-disabled); + display: flex; +} + +.cardNumber-info { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.cardNumber .input { + min-width: 0; + width: 100px; + height: 2.5rem; } diff --git a/src/components/CardNumbers/CardNumbers.tsx b/src/components/CardNumbers/CardNumbers.tsx index 99caece..0fff4c7 100644 --- a/src/components/CardNumbers/CardNumbers.tsx +++ b/src/components/CardNumbers/CardNumbers.tsx @@ -1,33 +1,107 @@ -import { SimpleText } from "@codex/marketplace-ui-components"; +import { ButtonIcon, Input, Tooltip } from "@codex/marketplace-ui-components"; import "./CardNumbers.css"; -import { Pencil } from "lucide-react"; +import { Check, Info, Pencil, ShieldAlert } from "lucide-react"; +import { ChangeEvent, useEffect, useState } from "react"; +import { classnames } from "../../utils/classnames"; type Props = { title: string; data: string; comment?: string; - editable?: boolean; + onChange?: (value: number) => void; + hasError?: boolean; }; -export function CardNumbers({ title, data, comment, editable }: Props) { +export function CardNumbers({ + title, + data, + comment, + hasError = false, + onChange, +}: Props) { + const [editing, setEditing] = useState(false); + const [value, setValue] = useState(data); + + useEffect(() => { + setValue(data); + }, [data]); + + const onEditingClick = () => setEditing(!editing); + + const onInputChange = (e: ChangeEvent) => { + setValue(e.currentTarget.value); + }; + + const onButtonClick = () => { + setEditing(false); + onChange?.(parseInt(value, 10)); + }; + + if (editing) { + return ( +
+
+ + +
+
+ {title} + {comment && ( + + {hasError ? ( + + ) : ( + + )} + + )} +
+
+ ); + } + + const DataContainer = editing ? ( + <> + + + + ) : ( + <> +

{data}

+ }> + + ); + return ( -
- {title} -
-

- {data} -

- {editable && ( -
- -
+
+
{DataContainer}
+
+ {title} + {comment && ( + + {hasError ? : } + )}
- {comment && ( - - {comment} - - )}
); } diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.css b/src/components/CustomStateCellRender/CustomStateCellRender.css new file mode 100644 index 0000000..6f6c512 --- /dev/null +++ b/src/components/CustomStateCellRender/CustomStateCellRender.css @@ -0,0 +1,9 @@ +.cell-state--custom { + display: flex; +} + +.cell-stateIcon { + position: relative; + top: 2px; + margin-right: 0.5rem; +} diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.tsx b/src/components/CustomStateCellRender/CustomStateCellRender.tsx new file mode 100644 index 0000000..ab68992 --- /dev/null +++ b/src/components/CustomStateCellRender/CustomStateCellRender.tsx @@ -0,0 +1,49 @@ +import { CheckCircle, CircleDashed, ShieldAlert } from "lucide-react"; +import "./CustomStateCellRender.css"; +import { StateCell, Tooltip } from "@codex/marketplace-ui-components"; + +// Import css +StateCell; + +type Props = { + state: string; + message: string | undefined; +}; + +export const CustomStateCellRender = ({ state, message }: Props) => { + const icons = { + pending: CircleDashed, + submitted: CircleDashed, + started: CircleDashed, + finished: CheckCircle, + cancelled: ShieldAlert, + }; + + const states = { + cancelled: "error", + pending: "warning", + started: "loading", + submitted: "loading", + finished: "success", + }; + + const Icon = icons[state as keyof typeof icons] || CircleDashed; + + return ( +

+ {message ? ( + + + + ) : ( + + )} + + {state} +

+ ); +}; diff --git a/src/components/FileCellRender/FIleCell.tsx b/src/components/FileCellRender/FIleCell.tsx new file mode 100644 index 0000000..876eada --- /dev/null +++ b/src/components/FileCellRender/FIleCell.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from "react"; +import { Tooltip, WebFileIcon } from "@codex/marketplace-ui-components"; +import "./FileCell.css"; +import { FileMetadata, FilesStorage } from "../../utils/file-storage"; +import { PurchaseStorage } from "../../utils/purchases-storage"; + +type Props = { + requestId: string; + purchaseCid: string; + index: number; +}; + +export function FileCell({ requestId, purchaseCid }: Props) { + const [cid, setCid] = useState(purchaseCid); + const [metadata, setMetadata] = useState({ + name: "N/A.jpg", + mimetype: "N/A", + uploadedAt: new Date(0, 0, 0, 0, 0, 0), + }); + + useEffect(() => { + PurchaseStorage.get(requestId).then((cid) => { + if (cid) { + setCid(cid); + + FilesStorage.get(cid).then((data) => { + if (data) { + console.info("data", data); + setMetadata(data); + } + }); + } + }); + }, [requestId]); + + let name = metadata.name; + + if (name.length > 10) { + const [filename, ext] = metadata.name.split("."); + name = filename.slice(0, 10) + "..." + ext; + } + + const cidTruncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5); + + return ( + <> +
+ +
+ {name} + + {cidTruncated} + +
+
+ + ); +} diff --git a/src/components/FileCellRender/FileCell.css b/src/components/FileCellRender/FileCell.css new file mode 100644 index 0000000..cd5520c --- /dev/null +++ b/src/components/FileCellRender/FileCell.css @@ -0,0 +1,20 @@ +.fileCell { + display: flex; + gap: 0.75rem; +} + +.fileCell-subtitle { + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; +} + +.fileCell-tooltip { + display: flex; + align-items: center; +} + +.fileCell .tooltip:hover:after { + left: -33%; +} diff --git a/src/components/Files/CidCopyButton.tsx b/src/components/Files/CidCopyButton.tsx new file mode 100644 index 0000000..d32a5b3 --- /dev/null +++ b/src/components/Files/CidCopyButton.tsx @@ -0,0 +1,39 @@ +import { useRef, useState } from "react"; +import { COPY_DURATION, ICON_SIZE } from "../../utils/constants.ts"; +import { Copy } from "lucide-react"; +import { Button } from "@codex/marketplace-ui-components"; + +type CopyButtonProps = { + cid: string; +}; + +export function CidCopyButton({ cid }: CopyButtonProps) { + const [copied, setCopied] = useState(false); + const timeout = useRef(null); + + const onCopy = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + + navigator.clipboard.writeText(cid); + + setCopied(true); + + timeout.current = setTimeout(() => { + setCopied(false); + }, COPY_DURATION); + }; + + const label = copied ? "Copied !" : "Copy CID"; + + const Icon = () => ; + + return ( + + ); +} diff --git a/src/components/Files/FileDetails.css b/src/components/Files/FileDetails.css new file mode 100644 index 0000000..2963830 --- /dev/null +++ b/src/components/Files/FileDetails.css @@ -0,0 +1,78 @@ +.fileDetails { + position: fixed; + transition: transform 0.25s; + background-color: var(--codex-background-secondary); + z-index: 2; + justify-content: space-between; +} + +.fileDetails-header { + padding: 0.75rem 1.5rem; + border-bottom: 1px solid var(--codex-border-color); + display: flex; + align-items: center; +} + +.fileDetails-headerTitle { + flex-grow: 1; +} + +.files-backdrop { + z-index: 2; +} + +.fileDetails-body { + padding: 0; +} + +.fileDetails-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + display: grid; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid var(--codex-border-color); +} + +.fileDetails-gridColumn { + grid-column: span 2 / span 2; + color: var(--codex-text-contrast); +} + +.fileDetails-actions { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.75rem 1.5rem; +} + +@media (min-width: 1000px) { + .fileDetails { + width: 300px; + height: 100%; + bottom: 0; + top: 0; + transform: translatex(300px); + right: 0; + } + + .fileDetails[aria-expanded] { + transform: translatex(0); + z-index: 10; + } +} + +@media (max-width: 999px) { + .fileDetails { + width: 100%; + height: auto; + bottom: 0; + top: auto; + transform: translatey(1000px); + left: 0; + padding-bottom: 1.5rem; + } + + .fileDetails[aria-expanded] { + transform: translatey(0); + z-index: 10; + } +} diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx new file mode 100644 index 0000000..5fff66f --- /dev/null +++ b/src/components/Files/FileDetails.tsx @@ -0,0 +1,92 @@ +import { ButtonIcon, Button } from "@codex/marketplace-ui-components"; +import { CodexDataContent } from "@codex/sdk-js"; +import { X, DownloadIcon } from "lucide-react"; +import { attributes } from "../../utils/attributes"; +import { PrettyBytes } from "../../utils/bytes"; +import { ICON_SIZE } from "../../utils/constants"; +import { Dates } from "../../utils/dates"; +import { CidCopyButton } from "./CidCopyButton"; +import "./FileDetails.css"; +import { FileMetadata } from "../../utils/file-storage"; + +type Props = { + details: (CodexDataContent & FileMetadata) | undefined; + onClose: () => void; + expanded: boolean; +}; + +export function FileDetails({ onClose, details, expanded }: Props) { + const attr = attributes({ "aria-expanded": expanded }); + const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/"; + + const Icon = () => ; + + const onDownload = () => window.open(url + details?.cid, "_target"); + + return ( + <> +
+ +
+ {details && ( + <> +
+ File details + +
+ +
+
+

CID:

+

{details.cid}

+
+ +
+

File name:

+

{details.name}

+
+ +
+

Date:

+

+ {Dates.format(details.uploadedAt).toString()} +

+
+ +
+

Mimetype:

+

{details.mimetype}

+
+ +
+

Size:

+

+ {PrettyBytes(details.manifest.datasetSize)} +

+
+ +
+

Protected:

+

+ {details.manifest.protected ? "Yes" : "No"} +

+
+ +
+ + + +
+
+ + )} +
+ + ); +} diff --git a/src/components/Files/Files.css b/src/components/Files/Files.css new file mode 100644 index 0000000..fbd5693 --- /dev/null +++ b/src/components/Files/Files.css @@ -0,0 +1,110 @@ +.files { + border-radius: var(--codex-border-radius); + border: 1px solid var(--codex-border-color); + background-color: var(--codex-background-secondary); + padding: 1rem 1.5rem; +} + +.files-title { + font-weight: bold; + font-size: 1.125rem; + line-height: 1.75rem; +} + +.files-file:not(:last-child) { + padding-bottom: 0.75rem; +} + +.files-fileContent { + display: flex; + gap: 0.75rem; +} + +.files-file:not(:last-child) .files-fileContent { + border-bottom: 1px solid var(--codex-border-color); +} + +.files-file:not(:last-child) .files-fileContent { + padding-bottom: 0.75rem; +} + +.files-fileIcon { + padding: 0.5rem; + border: 1px solid var(--codex-border-color); + border-radius: var(--codex-border-radius); + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; +} + +.files-fileData { + display: flex; + justify-content: space-between; + align-items: center; + flex-grow: 1; +} + +.files-fileActions { + display: flex; + align-items: center; + border: 1px solid var(--codex-border-color); + border-radius: var(--codex-border-radius); + padding: 0.5rem; + gap: 0.75rem; +} + +.files-fileStar { + transition: + fill 0.35s, + stroke 0.35s; +} + +.files-fileFavorite { + fill: yellow; + stroke: yellow; +} + +.files-header { + margin-bottom: 0.75rem; +} + +.files-headerTabs { + display: flex; + margin-top: 1rem; + gap: 1rem; + position: relative; +} + +.files-headerTabs::after { + width: 100%; + background-color: var(--codex-background-light); + content: " "; + position: absolute; + height: 2px; + top: 11px; + top: 31px; +} + +.files-headerTab { + display: flex; + align-items: center; + gap: 0.25rem; + padding-bottom: 1rem; + cursor: pointer; + transition: 0.35s opacity; + z-index: 1; +} + +.files-headerTab:not(.files-headerTab--active) { + opacity: 0.7; +} + +.files-headerTab:hover { + opacity: 0.85; +} + +.files-headerTab--active { + border-bottom: 2px solid var(--codex-color-contrast); +} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx new file mode 100644 index 0000000..371cdc8 --- /dev/null +++ b/src/components/Files/Files.tsx @@ -0,0 +1,163 @@ +import { Download, FilesIcon, ReceiptText, Star } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { PrettyBytes } from "../../utils/bytes.ts"; +import { Dates } from "../../utils/dates.ts"; +import "./Files.css"; +import { ICON_SIZE, SIDE_DURATION } from "../../utils/constants.ts"; +import { + ButtonIcon, + EmptyPlaceholder, + WebFileIcon, +} from "@codex/marketplace-ui-components"; +import { FileDetails } from "./FileDetails.tsx"; +import { classnames } from "../../utils/classnames.ts"; +import { FavoriteStorage } from "../../utils/favorite-storage.tsx"; +import { useData } from "../../hooks/useData.tsx"; + +type StarIconProps = { + isFavorite: boolean; +}; + +function StarIcon({ isFavorite }: StarIconProps) { + if (isFavorite) { + return ( + + ); + } + + return ; +} + +export function Files() { + const files = useData(); + const cid = useRef(""); + const [expanded, setExpanded] = useState(false); + const [favorites, setFavorites] = useState([]); + const [selected, setSelected] = useState<"all" | "favorites">("all"); + + useEffect(() => { + FavoriteStorage.list().then((cids) => setFavorites(cids)); + }, []); + + const onClose = () => { + setExpanded(false); + + setTimeout(() => { + cid.current = ""; + }, SIDE_DURATION); + }; + + const onSelected = () => + setSelected(selected === "all" ? "favorites" : "all"); + + const onDetails = (id: string) => { + cid.current = id; + setExpanded(true); + }; + + const onToggleFavorite = (cid: string) => { + if (favorites.includes(cid)) { + FavoriteStorage.delete(cid); + setFavorites(favorites.filter((c) => c !== cid)); + } else { + FavoriteStorage.add(cid); + setFavorites([...favorites, cid]); + } + }; + + const items = []; + + if (selected === "favorites") { + items.push(...files.filter((f) => favorites.includes(f.cid))); + } else { + items.push(...files); + } + + const details = items.find((c) => c.cid === cid.current); + + const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/"; + + return ( +
+
+
Files
+
+
+ + All files +
+ +
+ + Favorites +
+
+
+ +
+ {items.length ? ( + items.map((c) => ( +
+
+
+ +
+
+
+ {c.name} +
+ + {PrettyBytes(c.manifest.datasetSize)} -{" "} + {Dates.format(c.uploadedAt).toString()} - ... + {c.cid.slice(-5)} + +
+
+
+ window.open(url + c.cid, "_blank")} + Icon={() => }> + + onToggleFavorite(c.cid)} + Icon={() => ( + + )}> + + onDetails(c.cid)} + Icon={() => ( + + )}> +
+
+
+
+ )) + ) : ( +
+ +
+ )} +
+ + +
+ ); +} diff --git a/src/components/LogLevel/LogLevel.tsx b/src/components/LogLevel/LogLevel.tsx index 38b9f34..97bdb6b 100644 --- a/src/components/LogLevel/LogLevel.tsx +++ b/src/components/LogLevel/LogLevel.tsx @@ -4,7 +4,7 @@ import { useContext, useState } from "react"; import { ErrorBoundaryContext } from "../../contexts/ErrorBoundaryContext"; import { CodexSdk } from "../../sdk/codex"; import "./LogLevel.css"; -import { Button, Card, Select, Toast } from "@codex/marketplace-ui-components"; +import { Button, Select, Toast } from "@codex/marketplace-ui-components"; import { CircleCheck } from "lucide-react"; export function LogLevel() { @@ -60,7 +60,7 @@ export function LogLevel() { ); return ( - + <> +
+ {labels.map((l) => ( +
+ {l} +
+ ))} +
+
+ ); +} diff --git a/src/components/StorageRequestSetup/StorageRequestDone.tsx b/src/components/StorageRequestSetup/StorageRequestDone.tsx new file mode 100644 index 0000000..5656886 --- /dev/null +++ b/src/components/StorageRequestSetup/StorageRequestDone.tsx @@ -0,0 +1,30 @@ +import { CircleCheck } from "lucide-react"; +import { useEffect } from "react"; + +type Props = { + onChangeNextState: (value: "enable" | "disable") => void; +}; + +// TODO define style in placeholder component +export function StorageRequestDone({ onChangeNextState }: Props) { + useEffect(() => { + onChangeNextState("enable"); + }, [onChangeNextState]); + + return ( +
+
+ +
+ Your request is being processed. +
+ Processing your request may take some time. Once completed, it will + appear in your purchase list. You can safely close this dialog. +
+
+ ); +} diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx index 098cb87..f3077d9 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx @@ -1,4 +1,3 @@ -import { useQuery } from "@tanstack/react-query"; import { CodexSdk } from "../../sdk/codex"; import "./StorageRequestFileChooser.css"; import { ChangeEvent, useEffect, useRef, useState } from "react"; @@ -10,18 +9,14 @@ import { Upload, WebFileIcon, } from "@codex/marketplace-ui-components"; +import { useData } from "../../hooks/useData"; type Props = { - onToggleNext: (enable: boolean) => void; + onChangeNextState: (value: "enable" | "disable") => void; }; -export function StorageRequestFileChooser({ onToggleNext }: Props) { - const { data } = useQuery({ - queryFn: () => CodexSdk.data().then((data) => data.cids()), - queryKey: ["cids"], - refetchOnWindowFocus: false, - refetchOnMount: false, - }); +export function StorageRequestFileChooser({ onChangeNextState }: Props) { + const files = useData(); const [cid, setCid] = useState(""); const cache = useRef(""); @@ -30,48 +25,54 @@ export function StorageRequestFileChooser({ onToggleNext }: Props) { cache.current = val || ""; setCid(val || ""); - onToggleNext(!!val); + onChangeNextState(!val ? "disable" : "enable"); }); return () => { WebStorage.set("storage-request-step-1", cache.current || ""); }; - }, [onToggleNext]); + }, [onChangeNextState]); - if (data?.error) { - // TODO error - return ""; - } + // if (data?.error) { + // // TODO error + // return ""; + // } const onSelected = (o: DropdownOption) => { - onToggleNext(!!o.subtitle); setCid(o.subtitle || ""); + onChangeNextState(!o.subtitle ? "disable" : "enable"); cache.current = o.subtitle || ""; }; const onChange = (e: ChangeEvent) => { - onToggleNext(!!e.currentTarget.value); setCid(e.currentTarget.value); + onChangeNextState(!e.currentTarget.value ? "disable" : "enable"); cache.current = e.currentTarget.value; }; - const onSuccess = (data: string) => { - onToggleNext(true); + const onSuccess = (data: string, file: File) => { + WebStorage.set(data, { + type: file.type, + name: file.name, + }); + + onChangeNextState("enable"); + setCid(data); cache.current = data; }; const onDelete = () => { setCid(""); - onToggleNext(false); + onChangeNextState("disable"); }; const options = - data?.data.content.map((c) => { + files.map((f) => { return { - Icon: () => , - title: c.manifest.filename, - subtitle: c.cid, + Icon: () => , + title: f.name, + subtitle: f.cid, }; }) || []; @@ -84,6 +85,8 @@ export function StorageRequestFileChooser({ onToggleNext }: Props) { - CodexSdk.data().then((data) => data.upload.bind(CodexSdk)) - } + provider={() => CodexSdk.data().then((data) => data.upload.bind(data))} /> ); diff --git a/src/components/StorageRequestSetup/StorageRequestReview.css b/src/components/StorageRequestSetup/StorageRequestReview.css index 8f9e941..fe68a66 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.css +++ b/src/components/StorageRequestSetup/StorageRequestReview.css @@ -60,6 +60,39 @@ .storageRequestReview-numbers { display: grid; - grid-template-columns: 1fr 1fr 1fr; gap: 0.5rem; } + +.storageRequestReview-range { + margin: 0.5rem 0 1rem 0; + font-size: 0.85rem; +} + +.storageRequestReview-alert .alert-message { + font-size: 0.9rem; +} + +.storageRequestReview-range--disabled .range { + opacity: 0.5; +} +/* +.storageRequestReview-range--disabled .range::-webkit-slider-thumb { + background-color: var(--codex-background-light); +} */ + +@media (max-width: 800px) { + .storageRequestReview-numbers { + grid-template-columns: 1fr 1fr; + } + + .storageRequestReview-legend { + flex-direction: column; + align-items: flex-start; + } +} + +@media (min-width: 801px) { + .storageRequestReview-numbers { + grid-template-columns: 1fr 1fr 1fr; + } +} diff --git a/src/components/StorageRequestSetup/StorageRequestReview.tsx b/src/components/StorageRequestSetup/StorageRequestReview.tsx index 08331ab..ff8ef37 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.tsx +++ b/src/components/StorageRequestSetup/StorageRequestReview.tsx @@ -1,126 +1,364 @@ -import { useEffect, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { WebStorage } from "../../utils/web-storage"; -import { - StorageAvailabilityValue, - StorageDurabilityStepValue, - StoragePriceStepValue, -} from "./types"; import "./StorageRequestReview.css"; -import { Alert, SimpleText } from "@codex/marketplace-ui-components"; +import { Alert } from "@codex/marketplace-ui-components"; import { CardNumbers } from "../CardNumbers/CardNumbers"; +import { Range } from "../Range/Range"; +import { FileWarning } from "lucide-react"; +import { classnames } from "../../utils/classnames"; const plurals = (type: "node" | "token" | "second" | "minute", value: number) => `${value} ${type}` + (value > 1 ? "s" : ""); type Props = { - onToggleNext: (next: boolean) => void; + onChangeNextState: (value: "enable" | "disable") => void; }; -export function StorageRequestReview({ onToggleNext }: Props) { +export type AvailabilityUnit = + | "days" + | "months" + | "years" + | "minutes" + | "hours"; + +type Data = { + availability: number; + availabilityUnit: AvailabilityUnit; + tolerance: number; + proofProbability: number; + nodes: number; + reward: number; + collateral: number; + expiration: number; +}; + +type Durability = { + nodes: number; + tolerance: number; + proofProbability: number; +}; + +const durabilities = [ + { nodes: 2, tolerance: 0, proofProbability: 1 }, + { nodes: 3, tolerance: 1, proofProbability: 2 }, + { nodes: 4, tolerance: 2, proofProbability: 3 }, + { nodes: 5, tolerance: 3, proofProbability: 4 }, + { nodes: 6, tolerance: 4, proofProbability: 5 }, +]; + +type Price = { + reward: number; + collateral: number; +}; + +const prices = [ + { + reward: 5, + collateral: 5, + }, + { + reward: 10, + collateral: 10, + }, + { + reward: 50, + collateral: 20, + }, +]; + +const findDurabilityIndex = (d: Durability) => { + const s = JSON.stringify({ + nodes: d.nodes, + tolerance: d.tolerance, + proofProbability: d.proofProbability, + }); + + return durabilities.findIndex((d) => JSON.stringify(d) === s); +}; + +const findPriceIndex = (d: Price) => { + const s = JSON.stringify({ + reward: d.reward, + collateral: d.collateral, + }); + + return prices.findIndex((p) => JSON.stringify(p) === s); +}; + +export function StorageRequestReview({ onChangeNextState }: Props) { const [cid, setCid] = useState(""); - const [availability, setAvailability] = useState({ - unit: "days", - value: 0, + const [errors, setErrors] = useState({ + nodes: "", + tolerance: "", + proofProbability: "", }); - const [durability, setDurability] = useState({ - nodes: 0, - proofProbability: 0, - tolerance: 0, - }); - const [price, setPrice] = useState({ - collateral: 0, - expiration: 0, - reward: 0, + const [durability, setDurability] = useState(1); + const [price, setPrice] = useState(1); + const [data, setData] = useState({ + availabilityUnit: "days", + availability: 1, + tolerance: 1, + proofProbability: 1, + nodes: 3, + reward: 10, + collateral: 10, + expiration: 300, }); useEffect(() => { Promise.all([ + WebStorage.get("storage-request-criteria"), WebStorage.get("storage-request-step-1"), - WebStorage.get("storage-request-step-2"), - WebStorage.get("storage-request-step-3"), - WebStorage.get("storage-request-step-4"), - ]).then(([cid, availability, durability, price]) => { - setCid(cid || ""); + ]).then(([d, cid]) => { + if (d) { + setData(d); - if (availability) { - setAvailability(availability); + const index = findDurabilityIndex({ + nodes: d.nodes, + tolerance: d.tolerance, + proofProbability: d.proofProbability, + }); + + setDurability(index + 1); + + const pindex = findPriceIndex({ + reward: d.reward, + collateral: d.collateral, + }); + + setPrice(pindex + 1); + } else { + WebStorage.set("storage-request-criteria", { + availabilityUnit: "days", + availability: 1, + tolerance: 1, + proofProbability: 1, + nodes: 3, + reward: 10, + collateral: 10, + expiration: 300, + }); } - if (durability) { - setDurability(durability); + if (cid) { + setCid(cid); } - if (price) { - setPrice(price); - } - - onToggleNext(true); + onChangeNextState("enable"); }); - }, [onToggleNext]); + }, [onChangeNextState]); + + const updateData = (p: Partial) => { + setData((d) => { + const newData = { ...d, ...p }; + + WebStorage.set("storage-request-criteria", newData); + + return newData; + }); + }; + + const onDurabilityRangeChange = (e: ChangeEvent) => { + const l = parseInt(e.currentTarget.value, 10); + + const durability = durabilities[l - 1]; + + updateData(durability); + setDurability(l); + setErrors({ nodes: "", tolerance: "", proofProbability: "" }); + }; + + const onPriceRangeChange = (e: ChangeEvent) => { + const l = parseInt(e.currentTarget.value, 10); + + const price = prices[l - 1]; + + updateData(price); + setPrice(l); + }; + + const isUnvalidConstrainst = (nodes: number, tolerance: number) => { + const ecK = nodes - tolerance; + const ecM = tolerance; + + return ecK <= 1 || ecK < ecM; + }; + + const onNodesChange = (nodes: number) => { + setErrors((e) => ({ ...e, tolerance: "" })); + + if (isUnvalidConstrainst(nodes, data.tolerance)) { + setErrors((e) => ({ + ...e, + nodes: + "The data does not match Codex contrainst. Try with other values.", + })); + return; + } + + updateData({ nodes }); + + const index = findDurabilityIndex({ + nodes: nodes, + tolerance: data.tolerance, + proofProbability: data.proofProbability, + }); + + setDurability(index + 1); + }; + + const onToleranceChange = (tolerance: number) => { + setErrors((e) => ({ ...e, tolerance: "" })); + + if (tolerance > data.nodes) { + setErrors((e) => ({ + ...e, + tolerance: "The tolerance cannot be greater than the nodes.", + })); + return; + } + + if (isUnvalidConstrainst(data.nodes, tolerance)) { + setErrors((e) => ({ + ...e, + tolerance: + "The data does not match Codex contrainst. Try with other values.", + })); + return; + } + + updateData({ tolerance }); + + const index = findDurabilityIndex({ + nodes: data.nodes, + tolerance: tolerance, + proofProbability: data.proofProbability, + }); + + setDurability(index + 1); + }; + + const onProofProbabilityChange = (proofProbability: number) => { + updateData({ proofProbability }); + + const index = findDurabilityIndex({ + nodes: data.nodes, + tolerance: data.tolerance, + proofProbability: proofProbability, + }); + + setDurability(index + 1); + }; + + const onAvailabilityChange = (availability: number) => + updateData({ availability }); + + const onRewardChange = (reward: number) => { + updateData({ reward }); + + const index = findPriceIndex({ + reward, + collateral: data.collateral, + }); + + setPrice(index + 1); + }; + + const onCollateralChange = (collateral: number) => { + updateData({ collateral }); + + const index = findPriceIndex({ + collateral, + reward: data.reward, + }); + + setPrice(index + 1); + }; return (
- Review your request + Choose your criteria +
+ + + +
+ +
- - - - - - + data={data.availability.toString()} + comment={"Contract duration in " + data.availabilityUnit} + onChange={onAvailabilityChange}> + onChange={onRewardChange}> + onChange={onCollateralChange}>
- -

- This request with CID {" "} - {cid} will expire in - {plurals("minute", price.expiration)} - after the start. -

- - + } + title="Warning" + variant="warning" + className="storageRequestReview-alert"> + This request with CID + {cid} will expire in + {plurals("minute", data.expiration)} + after the start.
+ If no suitable hosts are found matching your storage requirements, you + will incur a charge of X tokens. +

-

Price comparaison with the market

-
@@ -142,7 +380,6 @@ export function StorageRequestReview({ onToggleNext }: Props) { Excellent
-
diff --git a/src/components/StorageRequestSetup/StorageRequestSetup.css b/src/components/StorageRequestSetup/StorageRequestSetup.css index b03bb1d..3a62156 100644 --- a/src/components/StorageRequestSetup/StorageRequestSetup.css +++ b/src/components/StorageRequestSetup/StorageRequestSetup.css @@ -1,21 +1,18 @@ .storageRequest { background-color: var(--codex-background); background-color: var(--codex-background); - padding: 0.5rem 1rem; border-radius: var(--codex-border-radius); transition: transform 0.15s; - position: fixed; max-width: 800px; - left: 50%; - top: 50%; - transform: translate(-50%, -50%) scale(0); overflow-y: auto; + overflow-x: hidden; opacity: 0; + z-index: -1; } .storageRequest-open { - transform: translate(-50%, -50%) scale(1); opacity: 1; + z-index: 10; } .storageRequest-title { @@ -81,14 +78,26 @@ text-align: center; } -.storageRequest .inputGroup-input { - width: 100%; -} - @media (max-width: 800px) { .storageRequest { margin: auto; width: 100%; + position: absolute; + top: 0; + left: 0; + min-height: 100%; + display: flex; + align-items: center; + + .alert { + flex-direction: column; + align-items: flex-start; + } + + .stepper-body, + .stepper { + width: calc(100% - 3rem); + } } } @@ -97,4 +106,15 @@ margin: auto; width: 85%; } + + .storageRequest { + left: 50%; + top: 50%; + transform: translate(-50%, -50%) scale(0); + position: fixed; + } + + .storageRequest-open { + transform: translate(-50%, -50%) scale(1); + } } diff --git a/src/components/StorageRequestSetup/StorageRequestStepper.tsx b/src/components/StorageRequestSetup/StorageRequestStepper.tsx index e2d6147..189439f 100644 --- a/src/components/StorageRequestSetup/StorageRequestStepper.tsx +++ b/src/components/StorageRequestSetup/StorageRequestStepper.tsx @@ -1,34 +1,29 @@ import { StorageRequestFileChooser } from "../../components/StorageRequestSetup/StorageRequestFileChooser"; -import { useEffect, useRef, useState } from "react"; -import { StorageRequestAvailability } from "../../components/StorageRequestSetup/StorageRequestAvailability"; -import { StorageRequestDurability } from "../../components/StorageRequestSetup/StorageRequestDurability"; -import { StorageRequestPrice } from "../../components/StorageRequestSetup/StorageRequestPrice"; +import { useCallback, useEffect, useRef, useState } from "react"; import { WebStorage } from "../../utils/web-storage"; import { STEPPER_DURATION } from "../../utils/constants"; import { StorageRequestReview } from "./StorageRequestReview"; import { CodexCreateStorageRequestInput } from "@codex/sdk-js"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CodexSdk } from "../../sdk/codex"; -import { - StorageAvailabilityValue, - StorageDurabilityStepValue, - StoragePriceStepValue, -} from "./types"; +import { StorageAvailabilityUnit } from "./types"; import { Backdrop, Stepper } from "@codex/marketplace-ui-components"; import { classnames } from "../../utils/classnames"; +import { StorageRequestDone } from "./StorageRequestDone"; +import { PurchaseStorage } from "../../utils/purchases-storage"; -function calculateAvailability(value: StorageAvailabilityValue) { - switch (value.unit) { +function calculateAvailability(value: number, unit: StorageAvailabilityUnit) { + switch (unit) { case "minutes": - return 60 * value.value; + return 60 * value; case "hours": - return 60 * 60 * value.value; + return 60 * 60 * value; case "days": - return 24 * 60 * 60 * value.value; + return 24 * 60 * 60 * value; case "months": - return 30 * 24 * 60 * 60 * value.value; + return 30 * 24 * 60 * 60 * value; case "years": - return 365 * 30 * 60 * 60 * value.value; + return 365 * 30 * 60 * 60 * value; } } @@ -41,37 +36,32 @@ type Props = { export function StorageRequestStepper({ className, open, onClose }: Props) { const [progress, setProgress] = useState(true); const [step, setStep] = useState(0); - const steps = useRef([ - "File", - "Availability", - "Durability", - "Price", - "Review", - ]); + const steps = useRef(["File", "Criteria", "Success"]); const [isNextDisable, setIsNextDisable] = useState(true); const queryClient = useQueryClient(); + const { mutateAsync, isPending, isError, error } = useMutation({ mutationKey: ["debug"], mutationFn: (input: CodexCreateStorageRequestInput) => CodexSdk.marketplace().then((marketplace) => marketplace.createStorageRequest(input) ), - onSuccess: async (data) => { + onSuccess: async (data, { cid }) => { if (data.error) { // TODO report error console.error(data); } else { - await Promise.all([ - WebStorage.delete("storage-request-step"), - WebStorage.delete("storage-request-step-1"), - WebStorage.delete("storage-request-step-2"), - WebStorage.delete("storage-request-step-3"), - WebStorage.delete("storage-request-step-4"), - ]); - - setStep(0); + // setStep((s) => (s = 1)); queryClient.invalidateQueries({ queryKey: ["purchases"] }); - onClose(); + + let requestId = data.data; + + if (!requestId.startsWith("0x")) { + console.debug("No prefix detected"); + requestId = "0x" + requestId; + } + + PurchaseStorage.set(requestId, cid); } }, }); @@ -86,6 +76,11 @@ export function StorageRequestStepper({ className, open, onClose }: Props) { }); }, []); + const onChangeNextState = useCallback( + (s: "enable" | "disable") => setIsNextDisable(s === "disable"), + [] + ); + if (isError) { // TODO Report error console.error(error); @@ -94,53 +89,37 @@ export function StorageRequestStepper({ className, open, onClose }: Props) { const components = [ StorageRequestFileChooser, - StorageRequestAvailability, - StorageRequestDurability, - StorageRequestPrice, + // StorageRequestAvailability, + // StorageRequestDurability, + // StorageRequestPrice, StorageRequestReview, + StorageRequestDone, ]; const onChangeStep = async (s: number, state: "before" | "end") => { + if (s === -1) { + setStep(0); + setIsNextDisable(true); + onClose(); + return; + } + if (state === "before") { setProgress(true); return; } - if (s === -1) { - setIsNextDisable(true); - setProgress(false); - onClose(); - return; - } - - if (s === steps.current.length) { + if (s >= steps.current.length) { setIsNextDisable(true); setProgress(false); - const [cid, availability, durability, price] = await Promise.all([ - WebStorage.get("storage-request-step-1"), - WebStorage.get("storage-request-step-2"), - WebStorage.get("storage-request-step-3"), - WebStorage.get("storage-request-step-4"), - ]); - - if (!cid || !availability || !durability || !price) { - return; + if (s >= steps.current.length) { + setStep(0); + WebStorage.delete("storage-request-step"); + WebStorage.delete("storage-request-criteria"); } - const { reward, collateral, expiration } = price; - const { nodes, proofProbability, tolerance } = durability; - - mutateAsync({ - cid, - collateral, - duration: calculateAvailability(availability), - expiry: expiration * 60, - nodes, - proofProbability, - tolerance, - reward, - }); + onClose(); return; } @@ -150,9 +129,49 @@ export function StorageRequestStepper({ className, open, onClose }: Props) { setIsNextDisable(true); setProgress(false); setStep(s); + + if (s == 2) { + setIsNextDisable(true); + setProgress(false); + + const [cid, criteria] = await Promise.all([ + WebStorage.get("storage-request-step-1"), + // TODO define criteria interface + // eslint-disable-next-line + WebStorage.get("storage-request-criteria"), + ]); + + if (!cid || !criteria) { + return; + } + + const { + availabilityUnit = "days", + availability, + reward, + collateral, + expiration, + nodes, + proofProbability, + tolerance, + } = criteria; + + mutateAsync({ + cid, + collateral, + duration: calculateAvailability(availability, availabilityUnit), + expiry: expiration * 60, + nodes, + proofProbability, + tolerance, + reward, + }); + } else { + setIsNextDisable(false); + } }; - const Body = components[step]; + const Body = components[step] || components[0]; return ( <> @@ -165,7 +184,7 @@ export function StorageRequestStepper({ className, open, onClose }: Props) { )}> setIsNextDisable(false)} />} + Body={} step={step} onChangeStep={onChangeStep} progress={progress || isPending} diff --git a/src/components/TruncateCellRender/TruncateCellRender.tsx b/src/components/TruncateCellRender/TruncateCellRender.tsx new file mode 100644 index 0000000..10d84a0 --- /dev/null +++ b/src/components/TruncateCellRender/TruncateCellRender.tsx @@ -0,0 +1,4 @@ +export function TruncateCellRender(cid: string) { + const truncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5); + return {truncated}; +} diff --git a/src/components/Welcome/Welcome.css b/src/components/Welcome/Welcome.css new file mode 100644 index 0000000..705a0e3 --- /dev/null +++ b/src/components/Welcome/Welcome.css @@ -0,0 +1,30 @@ +.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; +} + +.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 new file mode 100644 index 0000000..e2a3956 --- /dev/null +++ b/src/components/Welcome/Welcome.tsx @@ -0,0 +1,21 @@ +import { SimpleText } from "@codex/marketplace-ui-components"; +import "./Welcome.css"; +import { Link } from "@tanstack/react-router"; +import { ChevronRight } from "lucide-react"; + +export function Welcome() { + return ( +
+

Welcome to Codex Marketplace

+
+ + You can start using Codex for testing purpose by uploading new files. + +
+ + + Explore more content + +
+ ); +} diff --git a/src/hooks/useData.tsx b/src/hooks/useData.tsx new file mode 100644 index 0000000..ed0b0e5 --- /dev/null +++ b/src/hooks/useData.tsx @@ -0,0 +1,49 @@ +import { useQuery } from "@tanstack/react-query"; +import { FilesStorage } from "../utils/file-storage"; +import { CodexSdk } from "../sdk/codex"; + +export function useData() { + const { data = [] } = useQuery({ + queryFn: () => + CodexSdk.data().then(async (data) => { + const res = await data.cids(); + + if (res.error) { + // TODO error + return []; + } + + const metadata = await FilesStorage.list(); + + return res.data.content.map((content, index) => { + const value = metadata.find(([cid]) => content.cid === cid); + + if (!value) { + return { + ...content, + mimetype: "N/A", + uploadedAt: new Date(0, 0, 0, 0, 0, 0), + name: "N/A" + index, + }; + } + + const { + mimetype = "", + name = "", + uploadedAt = new Date(0, 0, 0, 0, 0, 0), + } = value[1]; + + return { + ...content, + mimetype, + name, + uploadedAt, + }; + }); + }), + queryKey: ["cids"], + refetchOnWindowFocus: false, + }); + + return data; +} diff --git a/src/index.css b/src/index.css index 4323663..c15b467 100644 --- a/src/index.css +++ b/src/index.css @@ -6,7 +6,11 @@ --codex-background: rgb(23 23 23); --codex-color: #e1e4d9; --codex-color-contrast: #f8f8f8; - --codex-color-error: #f85723; + --codex-color-error: 239, 68, 68; + --codex-color-warning: 234, 179, 8; + --codex-color-success: 20, 184, 166; + --codex-color-blue: 30, 64, 175; + --codex-color-grey: 170, 170, 170; --codex-color-primary: #c1f0a4; --codex-color-primary-variant: #c1f0a4cc; --codex-color-on-primary: #333; @@ -22,7 +26,6 @@ BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; - --codex-color-warning: rgb(234 179 8); -webkit-tap-highlight-color: transparent; -webkit-text-size-adjust: 100%; @@ -31,7 +34,7 @@ font-feature-settings: normal; font-variation-settings: normal; tab-size: 4; - font-size: 0.875rem; + font-size: 1.15rem; font-size: var(--codex-font-size); color-scheme: dark; color: var(--codex-color); @@ -62,12 +65,14 @@ html { min-height: 100%; display: flex; + max-width: 100%; } body { margin: 0; flex: 1; display: flex; + max-width: 100%; } ul, @@ -92,6 +97,7 @@ main { flex: 1; display: flex; flex-direction: column; + max-width: 100%; } hr { @@ -124,4 +130,9 @@ a { .root { display: flex; flex: 1; + max-width: 100%; +} + +.page { + max-width: 100%; } diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index d48767d..a6f941c 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -3,17 +3,9 @@ import "./dashboard.css"; import { MenuItem, MenuItemComponentProps, - NetworkIndicator, Page, } from "@codex/marketplace-ui-components"; -import { - Home, - Star, - ShoppingBag, - Server, - Settings, - HelpCircle, -} from "lucide-react"; +import { Home, ShoppingBag, Server, Settings, HelpCircle } from "lucide-react"; import { ICON_SIZE } from "../utils/constants"; import { NodeIndicator } from "../components/NodeIndicator/NodeIndicator"; import { HttpNetworkIndicator } from "../components/HttpNetworkIndicator/HttpNetworkIndicator"; @@ -36,15 +28,6 @@ const Layout = () => { ), }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Favorites - - ), - }, { type: "separator", }, @@ -77,6 +60,17 @@ const Layout = () => { ), }, + { + type: "separator", + }, + { + type: "menu-item", + Component: (p: MenuItemComponentProps) => ( + + Help + + ), + }, { type: "menu-item", Component: (p: MenuItemComponentProps) => ( @@ -86,17 +80,6 @@ const Layout = () => { ), }, - { - type: "separator", - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - Help - - ), - }, ] satisfies MenuItem[]; return } items={items} Right={Right} />; diff --git a/src/routes/dashboard/about.tsx b/src/routes/dashboard/about.tsx index 9cc58f8..525aa97 100644 --- a/src/routes/dashboard/about.tsx +++ b/src/routes/dashboard/about.tsx @@ -88,7 +88,7 @@ const About = () => { + href={"/api/codex/v1/data/" + c.cid}> Download
diff --git a/src/routes/dashboard/favorites.tsx b/src/routes/dashboard/favorites.tsx index 85acde2..72b0307 100644 --- a/src/routes/dashboard/favorites.tsx +++ b/src/routes/dashboard/favorites.tsx @@ -1,13 +1,13 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorBoundary } from "../../components/ErrorBoundary/ErrorBoundary"; -import { Manifests } from "../../components/Manifests/Manitests"; +import { Files } from "../../components/Files/Files"; export const Route = createFileRoute("/dashboard/favorites")({ component: () => ( <> ""}>
- +
diff --git a/src/routes/dashboard/help.css b/src/routes/dashboard/help.css new file mode 100644 index 0000000..a4c0908 --- /dev/null +++ b/src/routes/dashboard/help.css @@ -0,0 +1,35 @@ +.help { + max-width: 600px; + margin: auto; +} + +.help-title { + margin-bottom: 4rem; + margin-top: 4rem; +} + +.help-itemTitle { + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: bold; + margin-bottom: 0.75rem; +} + +.help-itemIcon { + color: var(--codex-color-disabled); + width: 2rem; + height: auto; +} + +.help-itemBody { + font-size: 1.125rem; +} + +.help-item { + padding-bottom: 2rem; + margin-bottom: 2rem; + border-bottom: 1px solid var(--codex-border-color); + gap: 1rem; + display: flex; + align-items: flex-start; +} diff --git a/src/routes/dashboard/help.tsx b/src/routes/dashboard/help.tsx index db7bfe2..b253909 100644 --- a/src/routes/dashboard/help.tsx +++ b/src/routes/dashboard/help.tsx @@ -1,5 +1,41 @@ import { createFileRoute } from "@tanstack/react-router"; +import "./help.css"; +import { HelpCircle } from "lucide-react"; +import { SimpleText } from "@codex/marketplace-ui-components"; export const Route = createFileRoute("/dashboard/help")({ - component: () =>
Hello /dashboard/help!
, + component: () => ( +
+
+
+

You might be wondering...

+ +
+ +
+

Looking for help to build Codex?

+ + Yes, you should refer to the documentation. If you do need more + help, ask to the github project. + +
+
+ +
+ +
+

Looking for help to build Codex?

+ + Yes, you should refer to the documentation. If you do need more + help, ask to the github project. + +
+
+
+ {/* ""}> + + */} +
+
+ ), }); diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 7bc97c6..3be0e67 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1,20 +1,23 @@ import { createFileRoute } from "@tanstack/react-router"; -import { Debug } from "../../components/Debug/Debug.tsx"; import { ErrorBoundary } from "../../components/ErrorBoundary/ErrorBoundary.tsx"; -import { LogLevel } from "../../components/LogLevel/LogLevel.tsx"; -import { Manifests } from "../../components/Manifests/Manitests.tsx"; -import { NodeSpaceAllocation } from "../../components/NodeSpaceAllocation/NodeSpaceAllocation.tsx"; -import { - Card, - EmptyPlaceholder, - Upload, -} from "@codex/marketplace-ui-components"; +import { Files } from "../../components/Files/Files.tsx"; +import { Card, Upload } from "@codex/marketplace-ui-components"; import { CodexSdk } from "../../sdk/codex.ts"; +import { Welcome } from "../../components/Welcome/Welcome.tsx"; +import { FilesStorage } from "../../utils/file-storage.ts"; export const Route = createFileRoute("/dashboard/")({ component: About, }); +const onSuccess = (cid: string, file: File) => { + FilesStorage.set(cid, { + name: file.name, + mimetype: file.type, + uploadedAt: new Date(), + }); +}; + function About() { return ( <> @@ -24,41 +27,21 @@ function About() { - CodexSdk.data().then((data) => data.upload.bind(CodexSdk)) + CodexSdk.data().then((data) => data.upload.bind(data)) } + onSuccess={onSuccess} /> ""}> - - - - ""}> - - - - - - ""}> - - - +
""}> - - -
- -
- ""}> - +
diff --git a/src/routes/dashboard/purchases.css b/src/routes/dashboard/purchases.css index 703c898..375c399 100644 --- a/src/routes/dashboard/purchases.css +++ b/src/routes/dashboard/purchases.css @@ -1,13 +1,7 @@ .purchases-modal { - z-index: -1; - position: fixed; margin: auto; } -.purchases-modal-open { - z-index: 1; -} - .purchases-actions { padding: 1rem; display: flex; diff --git a/src/routes/dashboard/purchases.tsx b/src/routes/dashboard/purchases.tsx index 81ac91a..054c25d 100644 --- a/src/routes/dashboard/purchases.tsx +++ b/src/routes/dashboard/purchases.tsx @@ -3,17 +3,13 @@ import { createFileRoute } from "@tanstack/react-router"; import { CodexSdk } from "../../sdk/codex"; import { Plus } from "lucide-react"; import { useState } from "react"; -import { - BreakCellRender, - Button, - DefaultCellRender, - DurationCellRender, - StateCellRender, - Table, -} from "@codex/marketplace-ui-components"; +import { Button, Cell, Table } from "@codex/marketplace-ui-components"; import { StorageRequestStepper } from "../../components/StorageRequestSetup/StorageRequestStepper"; import "./purchases.css"; import { classnames } from "../../utils/classnames"; +import { FileCell } from "../../components/FileCellRender/FIleCell"; +import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender"; +import prettyMilliseconds from "pretty-ms"; const Purchases = () => { const [open, setOpen] = useState(false); @@ -28,41 +24,36 @@ const Purchases = () => { } if (data?.error) { - console.error(data.data); return
Error: {data.data.message}
; // TODO Manage error } const headers = [ - "id", - "state", + "cid", "duration", "slots", "reward", "proof probability", - "error", + "state", ]; - const cells = [ - BreakCellRender, - StateCellRender({ cancelling: "success" }), - DurationCellRender, - DefaultCellRender, - DefaultCellRender, - DefaultCellRender, - DefaultCellRender, - ]; + const sorted = [...(data?.data || [])].reverse(); + const cells = + sorted.map((p, index) => { + const r = p.request; + const ask = p.request.ask; + const duration = parseInt(p.request.ask.duration, 10) * 1000; + const pf = parseInt(p.request.ask.proofProbability, 10) * 1000; - const purchases = - data?.data.map((p) => [ - p.requestId.toString(), - p.state, - p.request.ask.duration.toString(), - p.request.ask.slots.toString(), - p.request.ask.reward.toString(), - p.request.ask.proofProbability.toString(), - p.error, - ]) || []; + return [ + , + , + , + , + , + , + ]; + }) || []; return (
@@ -83,7 +74,8 @@ const Purchases = () => { open={open} onClose={() => setOpen(false)} /> - + + {!open &&
} ); }; diff --git a/src/routes/dashboard/settings.css b/src/routes/dashboard/settings.css index 0f09f05..9d36fa4 100644 --- a/src/routes/dashboard/settings.css +++ b/src/routes/dashboard/settings.css @@ -1,5 +1,18 @@ .settings { - display: flex; - flex-direction: column; - gap: 0.75rem; + border-radius: var(--codex-border-radius); + border: 1px solid var(--codex-border-color); + background-color: var(--codex-background-secondary); + padding: 1rem 1.5rem; + margin: 1rem 1.5rem; +} + +.settings-title { + font-weight: bold; + font-size: 1.125rem; + line-height: 1.75rem; + margin-bottom: 0.75rem; +} + +.settings-input { + margin-bottom: 0.75rem; } diff --git a/src/routes/dashboard/settings.tsx b/src/routes/dashboard/settings.tsx index df6c2a9..e673b16 100644 --- a/src/routes/dashboard/settings.tsx +++ b/src/routes/dashboard/settings.tsx @@ -1,15 +1,26 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorBoundary } from "../../components/ErrorBoundary/ErrorBoundary"; import "./settings.css"; +import { LogLevel } from "../../components/LogLevel/LogLevel"; +import { CodexUrlSettings } from "../../CodexUrllSettings/CodexUrlSettings"; export const Route = createFileRoute("/dashboard/settings")({ component: () => ( <> ""}> -
-

Settings

+
+ ""}> + + +
- {/*
+
+ ""}> + + +
+ + {/*
*/} -
), diff --git a/src/routes/index.tsx b/src/routes/index.tsx index d02a618..c59bca3 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,7 +1,12 @@ -import { createFileRoute, Link } from "@tanstack/react-router"; +import { createFileRoute, Link, redirect } from "@tanstack/react-router"; export const Route = createFileRoute("/")({ component: Index, + beforeLoad: async () => { + throw redirect({ + to: "/dashboard", + }); + }, }); function Index() { @@ -9,7 +14,7 @@ function Index() {

Welcome Home!

- Go to dashboard + Go to dashboard
); } diff --git a/src/sdk/codex.ts b/src/sdk/codex.ts index c006bb5..a898653 100644 --- a/src/sdk/codex.ts +++ b/src/sdk/codex.ts @@ -1,3 +1,49 @@ import { Codex } from "@codex/sdk-js"; +import { WebStorage } from "../utils/web-storage"; -export const CodexSdk = new Codex(import.meta.env.VITE_CODEX_API_URL); +export class CodexSdk { + static _client: Codex; + static _url: string; + + static client() { + if (this._client) { + return Promise.resolve(this._client); + } + + return WebStorage.get("codex-node-url") + .then((url) => { + this._url = url || import.meta.env.VITE_CODEX_API_URL; + this._client = new Codex(this._url); + + console.info({ url: this._url }); + }) + .then(() => this._client); + } + + static url() { + return this.client().then(() => this._url); + } + + static updateURL(url: string) { + this._url = url; + this._client = new Codex(url); + + return WebStorage.set("codex-node-url", url); + } + + static debug() { + return this.client().then((client) => client.debug()); + } + + static data() { + return this.client().then((client) => client.data()); + } + + static node() { + return this.client().then((client) => client.node()); + } + + static marketplace() { + return this.client().then((client) => client.marketplace()); + } +} diff --git a/src/utils/browser-storage.ts b/src/utils/browser-storage.ts deleted file mode 100644 index c7337e0..0000000 --- a/src/utils/browser-storage.ts +++ /dev/null @@ -1,18 +0,0 @@ -// TODO remove this for WebStorage -export const BrowserStorage = { - toggle(key: string, value: string) { - const previous = JSON.parse(window.localStorage.getItem(key) || "[]"); - - if (previous.includes(value)) { - const values = previous.filter((v: string) => v !== value); - window.localStorage.setItem(key, JSON.stringify(values)); - return; - } - - window.localStorage.setItem(key, JSON.stringify([...previous, value])); - }, - - values(key: string) { - return JSON.parse(window.localStorage.getItem(key) || "[]"); - }, -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9776641..6364325 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,3 +7,5 @@ export const SIDE_DURATION = 3000; export const ICON_SIZE = "1.25rem"; export const STEPPER_DURATION = 500; + +export const EXPLORER_URL = "https://explorer.testnet.codex.storage/tx"; diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 8ef8131..434c910 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,8 +1,12 @@ export const Dates = { - format(date: string) { - return new Intl.DateTimeFormat("en-GB", { - dateStyle: "medium", - timeStyle: "short", - }).format(new Date(date)); - }, + format(date: string | Date) { + if (!date) { + return "N/A"; + } + + return new Intl.DateTimeFormat("en-GB", { + dateStyle: "medium", + timeStyle: "short", + }).format(new Date(date)); + }, }; diff --git a/src/utils/favorite-storage.tsx b/src/utils/favorite-storage.tsx new file mode 100644 index 0000000..a589b03 --- /dev/null +++ b/src/utils/favorite-storage.tsx @@ -0,0 +1,17 @@ +import { createStore, del, keys, set } from "idb-keyval"; + +const store = createStore("favorites", "favorites"); + +export const FavoriteStorage = { + list() { + return keys(store); + }, + + delete(key: string) { + return del(key, store); + }, + + async add(key: string) { + return set(key, "1", store); + }, +}; diff --git a/src/utils/file-storage.ts b/src/utils/file-storage.ts new file mode 100644 index 0000000..54560d3 --- /dev/null +++ b/src/utils/file-storage.ts @@ -0,0 +1,27 @@ +import { createStore, entries, get, set } from "idb-keyval"; + +const store = createStore("files", "files"); + +export type FileMetadata = { + mimetype: string; + uploadedAt: Date; + name: string; +}; + +export const FilesStorage = { + list() { + return entries(store); + }, + + // delete(key: string) { + // return del(key, store); + // }, + + async get(cid: string) { + return get(cid, store); + }, + + async set(cid: string, metadata: FileMetadata) { + return set(cid, metadata, store); + }, +}; diff --git a/src/utils/files.ts b/src/utils/files.ts index 3382132..7ab1ad7 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -1,5 +1,10 @@ export const Files = { - isImage(type: string) { - return type.startsWith("image"); - }, + isImage(type: string) { + return type.startsWith("image"); + }, +}; + +export type CodexFileMetadata = { + type: string; + name: string; }; diff --git a/src/utils/purchases-storage.ts b/src/utils/purchases-storage.ts new file mode 100644 index 0000000..738af3f --- /dev/null +++ b/src/utils/purchases-storage.ts @@ -0,0 +1,13 @@ +import { createStore, get, set } from "idb-keyval"; + +const store = createStore("purchases", "purchases"); + +export const PurchaseStorage = { + async get(key: string) { + return get(key, store); + }, + + async set(key: string, cid: string) { + return set(key, cid, store); + }, +}; diff --git a/src/workers/upload-worker.ts b/src/workers/upload-worker.ts deleted file mode 100644 index f3cec04..0000000 --- a/src/workers/upload-worker.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Codex } from "@codex/sdk-js"; - -const codex = new Codex(import.meta.env.VITE_CODEX_API_URL); -let abort: () => void; - -self.addEventListener("message", function (e) { - const { type, ...rest } = e.data; - - if (type === "abort") { - console.debug("Aborting request"); - - abort?.(); - - return; - } - - const onProgress = (loaded: number, total: number) => { - self.postMessage({ - type: "progress", - loaded, - total, - }); - }; - - return codex - .data() - .then((data) => data.upload(rest.file, onProgress)) - .then((result) => { - abort = result.abort; - - return result.result; - }) - .then((value) => { - self.postMessage({ - type: "completed", - value, - }); - }); -});