diff --git a/package-lock.json b/package-lock.json index 30571a8..4f6b0ba 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": "^0.0.40", + "@codex-storage/marketplace-ui-components": "^0.0.41", "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -425,9 +425,9 @@ "peer": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.40.tgz", - "integrity": "sha512-ttyf7Yxk1QFSmdISVXNjGNZ0i4PlNkU9RCZxjU8cbPLMIfAoMBaKsptNzZxxifsBrNLLqthZujOTOdILsQHGIg==", + "version": "0.0.41", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.41.tgz", + "integrity": "sha512-zAbiN7yzpCDpGgGfqbJTutDjsxIKCj3QZ0wWuk3sk74nqhv16LGjvYTL6r2E35LG69Fp+yOGk3wC6t+zSmYcYw==", "dependencies": { "lucide-react": "^0.453.0" }, diff --git a/package.json b/package.json index e447ae3..c971bad 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.40", + "@codex-storage/marketplace-ui-components": "^0.0.41", "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", diff --git a/src/assets/icons/choose-cid.svg b/src/assets/icons/choose-cid.svg new file mode 100644 index 0000000..1bc2a0f --- /dev/null +++ b/src/assets/icons/choose-cid.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/commitment.svg b/src/assets/icons/commitment.svg new file mode 100644 index 0000000..481c959 --- /dev/null +++ b/src/assets/icons/commitment.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/durability.svg b/src/assets/icons/durability.svg new file mode 100644 index 0000000..068f115 --- /dev/null +++ b/src/assets/icons/durability.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 0000000..014ef66 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/assets/icons/preset.svg b/src/assets/icons/preset.svg new file mode 100644 index 0000000..805a299 --- /dev/null +++ b/src/assets/icons/preset.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/assets/icons/request-duration.svg b/src/assets/icons/request-duration.svg new file mode 100644 index 0000000..d174d46 --- /dev/null +++ b/src/assets/icons/request-duration.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/components/AppBar/appBar.css b/src/components/AppBar/appBar.css index 85747d7..084a830 100644 --- a/src/components/AppBar/appBar.css +++ b/src/components/AppBar/appBar.css @@ -10,7 +10,7 @@ border-right: 12px solid transparent; position: sticky; top: 0; - z-index: 1; + z-index: 2; @media (min-width: 1000px) { & { diff --git a/src/components/CardNumbers/CardNumbers.css b/src/components/CardNumbers/CardNumbers.css index 00e2a65..36f0674 100644 --- a/src/components/CardNumbers/CardNumbers.css +++ b/src/components/CardNumbers/CardNumbers.css @@ -1,87 +1,72 @@ -.cardNumber { - border-radius: var(--codex-border-radius); - border: 1px solid var(--codex-border-color); - font-family: var(--codex-font-family); - padding: 0.5rem 1rem; - background-color: rgb(56 56 56); - display: flex; - flex-direction: column; -} +.card-number { + --codex-card-number-label-color: #7b7b7b; + --codex-card-number-unit-color: #969696; -.cardNumber-container { - display: flex; - flex-direction: column; -} + &[aria-invalid] { + --codex-card-number-label-color: var(--codex-input-color-error); + --codex-card-number-unit-color: var(--codex-input-color-error); + } -.cardNumber--error { - border-color: rgb(var(--codex-color-error)); -} - -.cardNumber-errorText, -.cardNumber-helperText { - height: 2rem; - display: inline-block; - margin-top: 0.25rem; - padding: 0 0.5rem; -} - -.cardNumber-errorText { - color: rgb(var(--codex-color-error)); -} - -.cardNumber-title { - display: inline-block; - font-size: 0.9rem; -} - -.cardNumber-data { - font-size: 2rem; - color: var(--codex-color-primary); - margin-bottom: 0.5rem; -} - -.cardNumber-data:focus-visible { - outline: 1px solid var(--codex-border-color); - outline-offset: 0.25rem; -} - -.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; -} + label { + color: var(--codex-card-number-label-color); + } -.cardNumber-dataContainer .button-icon { - position: relative; - top: 3px; -} + input { + font-family: Inter; + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: -0.015em; + width: 100%; + background-color: transparent; -.cardNumber-tooltip { - color: var(--codex-color-disabled); - display: flex; -} + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } -.cardNumber-info { - display: flex; - align-items: center; - gap: 0.5rem; -} + & { + -moz-appearance: textfield; + } + } -.cardNumber .input { - min-width: 0; - width: 65px; - height: 2.5rem; -} + .tooltip { + position: absolute; + right: 0px; + top: 0px; + } -.cardNumber .inputGroup-select { - height: 2.5rem; - padding: 0.25rem 1rem; + svg { + color: var(--codex-card-number-unit-color); + } + + /* svg::after { + content: attr(data-title); + background-color: #2f2f2f; + color: #fff; + padding: 8px; + border-radius: 4px; + font-size: 12px; + line-height: 14px; + display: block; + white-space: nowrap; + position: absolute; + right: 1rem; + overflow: visible; + } */ + + span { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: var(--codex-card-number-unit-color); + position: absolute; + top: 54px; + right: 16px; + } } diff --git a/src/components/CardNumbers/CardNumbers.tsx b/src/components/CardNumbers/CardNumbers.tsx index 5e11bcd..03aa7f7 100644 --- a/src/components/CardNumbers/CardNumbers.tsx +++ b/src/components/CardNumbers/CardNumbers.tsx @@ -1,110 +1,36 @@ -import { ButtonIcon } from "@codex-storage/marketplace-ui-components"; +import { Input, Tooltip } from "@codex-storage/marketplace-ui-components"; import "./CardNumbers.css"; -import { Check, CircleX, Pencil } from "lucide-react"; -import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react"; +import { ChangeEvent, useState } from "react"; import { classnames } from "../../utils/classnames"; +import InfoIcon from "../../assets/icons/info.svg?react"; +import { attributes } from "../../utils/attributes"; type Props = { - title: string; - data: string; + unit: string; + value: string; onChange: (value: string) => void; onValidation?: (value: string) => string; className?: string; - - /** - * If true, the caret will be set at the end of the input - * Default is true - */ - repositionCaret?: boolean; - + title: string; + id: string; helper: string; }; export function CardNumbers({ - title, - data, + id, + unit, + value, onValidation, onChange, - helper, + title, className = "", - repositionCaret = true, + helper, }: Props) { - const [isDirty, setIsDirty] = useState(false); const [error, setError] = useState(""); - const ref = useRef(null); - const replaceCaret = useCallback( - (el: HTMLElement) => { - if (!repositionCaret) { - return; - } - - // Place the caret at the end of the element - const target = document.createTextNode(""); - el.appendChild(target); - // do not move caret if element was not focused - const isTargetFocused = document.activeElement === el; - if (target !== null && target.nodeValue !== null && isTargetFocused) { - const sel = window.getSelection(); - if (sel !== null) { - const range = document.createRange(); - range.setStart(target, target.nodeValue.length); - range.collapse(true); - sel.removeAllRanges(); - sel.addRange(range); - } - if (el instanceof HTMLElement) el.focus(); - } - }, - [repositionCaret] - ); - - const updateText = useCallback( - (text: string | null) => { - const current = ref.current; - - if (current && text) { - current.textContent = text; - replaceCaret(current); - } - }, - [replaceCaret, ref] - ); - - useEffect(() => { - updateText(data); - setIsDirty(false); - }, [data, updateText]); - - const onEditingClick = () => { - const current = ref.current; - - if (isDirty) { - onChange?.(current?.textContent || ""); - } else if (current) { - current.focus(); - replaceCaret(current); - } - }; - - const onInput = (e: ChangeEvent) => { - const text = e.currentTarget.textContent; - - setIsDirty(text !== data); - - if (!text) { - setError("A value is required"); - return; - } - - if (text?.length > 10) { - e.currentTarget.textContent = text.slice(0, 10); - replaceCaret(e.currentTarget); - setError("The value is too long"); - return; - } - - updateText(text); + const onInternalChange = (e: ChangeEvent) => { + const text = e.currentTarget.value; + onChange(e.currentTarget.value); const msg = onValidation?.(text); @@ -116,28 +42,24 @@ export function CardNumbers({ setError(""); }; - const onBlur = () => { - if (error === "") { - if (isDirty) { - onChange?.(ref.current?.textContent || ""); - } - } else { - updateText(data); - } - - setIsDirty(false); - setError(""); - }; - - const Icon = error - ? () => - : isDirty - ? () => - : () => ; - return ( -
-
+ + + + + + {unit} + + {/*
<> @@ -163,7 +85,7 @@ export function CardNumbers({ {error} ) : ( {helper} - )} + )} */}
); } diff --git a/src/components/FileCellRender/FileCell.tsx b/src/components/FileCellRender/FileCell.tsx index 8408886..75b7ff7 100644 --- a/src/components/FileCellRender/FileCell.tsx +++ b/src/components/FileCellRender/FileCell.tsx @@ -19,9 +19,10 @@ type Props = { purchaseCid: string; index: number; data: CodexDataContent[]; + onMetadata?: (requestId: string, metadata: FileMetadata) => void; }; -export function FileCell({ requestId, purchaseCid, data }: Props) { +export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) { const [cid, setCid] = useState(purchaseCid); const [metadata, setMetadata] = useState({ filename: "-", @@ -46,6 +47,11 @@ export function FileCell({ requestId, purchaseCid, data }: Props) { mimetype, uploadedAt, }); + onMetadata?.(requestId, { + filename, + mimetype, + uploadedAt, + }); } } }); diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 91b59ae..389820d 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -205,6 +205,7 @@ export function Files({ limit }: Props) { label="Folder" Icon={PlusIcon} variant="outline" + size="small" disabled={!!error || !folder} onClick={onFolderCreate}>
diff --git a/src/components/Files/PurchaseHistory.tsx b/src/components/Files/PurchaseHistory.tsx index 59a58e3..1c805c0 100644 --- a/src/components/Files/PurchaseHistory.tsx +++ b/src/components/Files/PurchaseHistory.tsx @@ -10,7 +10,7 @@ import { TruncateCell } from "../TruncateCell/TruncateCell"; import { CodexPurchase } from "@codex-storage/sdk-js"; import PurchaseHistoryIcon from "../../assets/icons/purchase-history-outline.svg?react"; import { useState } from "react"; -import { PurchaseUtils } from "../StorageRequestSetup/purchase.util"; +import { PurchaseUtils } from "../Purchase/purchase.utils"; type Props = { purchases: CodexPurchase[]; diff --git a/src/components/Purchase/PurchasesTable.tsx b/src/components/Purchase/PurchasesTable.tsx new file mode 100644 index 0000000..69abefd --- /dev/null +++ b/src/components/Purchase/PurchasesTable.tsx @@ -0,0 +1,134 @@ +import { + Cell, + Row, + Spinner, + Table, + TabSortState, +} from "@codex-storage/marketplace-ui-components"; +import { Times } from "../../utils/times"; +import { useState } from "react"; +import { FileCell } from "../../components/FileCellRender/FileCell"; +import { useData } from "../../hooks/useData"; +import { useQuery } from "@tanstack/react-query"; +import { CodexSdk } from "../../sdk/codex"; +import { Promises } from "../../utils/promises"; +import { CodexPurchase } from "@codex-storage/sdk-js"; +import { TruncateCell } from "../TruncateCell/TruncateCell"; +import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender"; +import { PurchaseUtils } from "./purchase.utils"; + +type Props = {}; + +type SortFn = (a: CodexPurchase, b: CodexPurchase) => number; + +export function PurchasesTable({}: Props) { + const [metadata, setMetadata] = useState<{ [key: string]: number }>({}); + const content = useData(); + const { data, isPending } = useQuery({ + queryFn: () => + CodexSdk.marketplace() + .purchases() + .then((s) => Promises.rejectOnError(s)), + queryKey: ["purchases"], + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + initialData: [], + + // Throw the error to the error boundary + throwOnError: true, + }); + + const onMetadata = ( + requestId: string, + { uploadedAt }: { uploadedAt: number } + ) => { + setMetadata((m) => ({ ...m, [requestId]: uploadedAt })); + setSortFn(() => + PurchaseUtils.sortByUploadedAt("desc", { + ...metadata, + [requestId]: uploadedAt, + }) + ); + }; + + const [sortFn, setSortFn] = useState(() => + PurchaseUtils.sortByUploadedAt("desc", metadata) + ); + + const onSortByDuration = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByDuration(state)); + + const onSortByReward = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByReward(state)); + + const onSortByState = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByState(state)); + + const onSortByUploadedAt = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata)); + + const headers = [ + ["file", onSortByUploadedAt], + ["request id"], + ["duration", onSortByDuration], + ["slots"], + ["reward", onSortByReward], + ["proof probability"], + ["state", onSortByState], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const sorted = sortFn ? [...data].sort(sortFn) : data; + + const rows = sorted.map((p, index) => { + const r = p.request; + const ask = p.request.ask; + const duration = parseInt(p.request.ask.duration, 10); + const pf = parseInt(p.request.ask.proofProbability, 10); + + return ( + , + , + {Times.pretty(duration)}, + {ask.slots.toString()}, + {ask.reward + " CDX"}, + {pf.toString()}, + , + ]}> + ); + }); + + console.info(metadata); + + if (isPending) { + return ( +
+ +
+ ); + } + + return ( + <> + + + ); +} diff --git a/src/components/Purchase/purchase.utils.ts b/src/components/Purchase/purchase.utils.ts new file mode 100644 index 0000000..102bbba --- /dev/null +++ b/src/components/Purchase/purchase.utils.ts @@ -0,0 +1,43 @@ +import { TabSortState } from "@codex-storage/marketplace-ui-components" +import { CodexPurchase } from "@codex-storage/sdk-js" + +export const PurchaseUtils = { + sortById: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => { + + return state === "desc" + ? b.requestId + .toLocaleLowerCase() + .localeCompare(a.requestId.toLocaleLowerCase()) + : a.requestId + .toLocaleLowerCase() + .localeCompare(b.requestId.toLocaleLowerCase()) + }, + sortByState: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? b.state + .toLocaleLowerCase() + .localeCompare(a.state.toLocaleLowerCase()) + : a.state + .toLocaleLowerCase() + .localeCompare(b.state.toLocaleLowerCase()) + , + sortByDuration: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? Number(b.request.ask.duration) - Number(a.request.ask.duration) + : Number(a.request.ask.duration) - Number(b.request.ask.duration) + , + sortByReward: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? Number(b.request.ask.reward) - Number(a.request.ask.reward) + : Number(a.request.ask.reward) - Number(b.request.ask.reward) + , + sortByUploadedAt: (state: TabSortState, table: Record) => + (a: CodexPurchase, b: CodexPurchase) => { + console.info(table) + return state === "desc" + ? (table[b.requestId] || 0) - (table[a.requestId] || 0) + : (table[a.requestId] || 0) - (table[b.requestId] || 0) + } + , +} \ No newline at end of file diff --git a/src/components/StorageRequestSetup/StorageRequestAvailability.css b/src/components/StorageRequestSetup/StorageRequestAvailability.css index 3ce11a4..8768c3d 100644 --- a/src/components/StorageRequestSetup/StorageRequestAvailability.css +++ b/src/components/StorageRequestSetup/StorageRequestAvailability.css @@ -3,21 +3,3 @@ display: flex; align-items: center; } - -.storageRequestFileChooser-dropdown-success { - animation-duration: 3s; - animation-name: cid-selected; - border-radius: var(--codex-border-radius); -} - -@keyframes cid-selected { - 0% { - box-shadow: 0 0 0 0px var(--codex-color-primary-variant); - } - 50% { - box-shadow: 0 0 0 3px var(--codex-color-primary-variant); - } - 100% { - box-shadow: 0 0 0 0px var(--codex-color-primary-variant); - } -} diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.css b/src/components/StorageRequestSetup/StorageRequestCreate.css index 09e57a1..2b746c1 100644 --- a/src/components/StorageRequestSetup/StorageRequestCreate.css +++ b/src/components/StorageRequestSetup/StorageRequestCreate.css @@ -1,5 +1,35 @@ -@media (min-width: 801px) { - .storageRequestCreate { - min-width: 700px; +.storage-request { + .modal dialog { + width: 80%; + max-width: 100% !important; + } + + header { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 16px; + + small { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: #969696; + } + } + + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + } + + .upload-file { + margin-bottom: 16px; } } diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.tsx b/src/components/StorageRequestSetup/StorageRequestCreate.tsx index a352b32..936eded 100644 --- a/src/components/StorageRequestSetup/StorageRequestCreate.tsx +++ b/src/components/StorageRequestSetup/StorageRequestCreate.tsx @@ -16,6 +16,7 @@ import { useStorageRequestMutation } from "./useStorageRequestMutation"; import { Plus } from "lucide-react"; import "./StorageRequestCreate.css"; import { StorageRequestError } from "./StorageRequestError"; +import PurchaseIcon from "../../assets/icons/purchase.svg?react"; const CONFIRM_STATE = 2; @@ -35,7 +36,7 @@ export function StorageRequestCreate() { const [storageRequest, setStorageRequest] = useState( defaultStorageRequest ); - const steps = useRef(["File", "Criteria", "Success"]); + const steps = useRef(["Select File", "Select Request Criteria", "Success"]); const { state, dispatch } = useStepperReducer(); const { mutateAsync, error } = useStorageRequestMutation(dispatch, state); @@ -117,15 +118,21 @@ export function StorageRequestCreate() { const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next"; return ( - <> +
); } diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.css b/src/components/StorageRequestSetup/StorageRequestFileChooser.css index dfd0d5d..54746ff 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.css +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.css @@ -1,3 +1,35 @@ +.file-chooser { + .input { + width: 100%; + } + + hr { + flex: 1; + border: 1px solid #96969633; + margin-top: 24px; + margin-bottom: 24px; + + + span { + font-family: Inter; + font-size: 11px; + font-weight: 500; + line-height: 12px; + letter-spacing: 0.02em; + text-align: left; + color: #696969; + } + } + + .upload { + margin-top: 16px; + margin-bottom: 16px; + } + + input { + width: 100%; + } +} + .storageRequestFileChooser-hr { margin: 1.5rem 0; } @@ -5,18 +37,3 @@ .storageRequestFileChooser-input { width: 100%; } - -.storageRequestFileChooser-separator { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 0; -} - -.storageRequestFileChooser-or { - font-size: 1.25rem; -} - -.storageRequestFileChooser-dropdown .dropdown-input { - width: 100%; -} diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx index 1ab77df..dae27c7 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx @@ -1,7 +1,6 @@ import { CodexSdk } from "../../sdk/codex"; import "./StorageRequestFileChooser.css"; import { ChangeEvent, useEffect } from "react"; -import { classnames } from "../../utils/classnames"; import { Dropdown, DropdownOption, @@ -11,6 +10,8 @@ import { import { useData } from "../../hooks/useData"; import { StorageRequestComponentProps } from "./types"; import { useQueryClient } from "@tanstack/react-query"; +import ChooseCidIcon from "../../assets/icons/choose-cid.svg?react"; +import UploadIcon from "../../assets/icons/upload.svg?react"; export function StorageRequestFileChooser({ storageRequest, @@ -55,49 +56,42 @@ export function StorageRequestFileChooser({ }) || []; return ( - <> - Choose a CID - - +
+
+ +
Choose a CID
+
-
-
- OR -
+
+
+ OR +
- -
- Upload a file -
- - The CID will be automatically copied after your upload. - -
+
+ +
Upload
+
- +
); } diff --git a/src/components/StorageRequestSetup/StorageRequestReview.css b/src/components/StorageRequestSetup/StorageRequestReview.css index 33ea913..f761926 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.css +++ b/src/components/StorageRequestSetup/StorageRequestReview.css @@ -1,3 +1,155 @@ +.request-review { + > header { + border-bottom: 1px solid #96969633; + padding-bottom: 16px; + + div { + line-height: 8px; + } + } + + .presets { + display: flex; + gap: 16px; + margin-bottom: 16px; + + > div { + height: 74px; + position: relative; + } + + > div:first-child { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + + span { + display: block; + font-family: Inter; + font-size: 10px; + font-weight: 400; + line-height: 12.1px; + letter-spacing: 0.01em; + text-transform: uppercase; + } + + small { + font-family: Inter; + font-size: 14px; + font-weight: 500; + line-height: 18px; + letter-spacing: -0.011em; + color: #969696cc; + } + } + + > div:nth-child(n + 2) { + --codex-preset-border-color: #494949; + --codex-preset-color: #969696; + border: 1px solid var(--codex-preset-border-color); + border-radius: 12px; + padding: 16px; + flex: 1; + box-sizing: border-box; + overflow: hidden; + display: flex; + align-items: flex-end; + cursor: pointer; + transition: 0.35s box-shadow; + + &:hover { + box-shadow: 0 0 0 2px var(--codex-preset-border-color); + } + + &[aria-selected] { + --codex-preset-border-color: #6fcb94; + --codex-preset-color: #6fcb94; + } + + svg { + position: absolute; + right: 0; + top: 0; + color: var(--codex-preset-color); + } + + span { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: var(--codex-preset-color); + + + span { + background: #6fcb9433; + padding: 2px 8px; + font-family: Inter; + font-size: 11px; + font-weight: 500; + line-height: 12px; + letter-spacing: 0.02em; + color: #6fcb94; + border-radius: 16px; + margin-left: 4px; + } + } + } + } + + .row { + border-top: 1px solid #96969633; + margin-top: 16px; + margin-bottom: 16px; + padding-top: 16px; + gap: 8px; + } + + .grid { + display: grid; + gap: 16px; + + @media (max-width: 800px) { + & { + grid-template-columns: 1fr; + } + + .storageRequestReview-presets { + flex-direction: column; + } + + .storageRequestReview-presets-blocs { + flex-direction: column; + } + + .storageRequestReview-alert { + flex-direction: column; + } + + .storageRequestReview-expiration { + min-width: 100%; + } + } + + @media (min-width: 801px) { + & { + grid-template-columns: 1fr 1fr 1fr; + } + } + } + + footer { + display: flex; + gap: 16px; + margin-bottom: 16px; + + > * { + flex: 1; + } + } +} + .storageRequestReview-hr { margin-bottom: 1.5rem; margin-top: 0rem; @@ -5,7 +157,7 @@ .storageRequestReview-numbers { display: grid; - gap: 0.5rem; + gap: 16px; } .storageRequestReview-range { diff --git a/src/components/StorageRequestSetup/StorageRequestReview.tsx b/src/components/StorageRequestSetup/StorageRequestReview.tsx index ab1ae8f..5729aed 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.tsx +++ b/src/components/StorageRequestSetup/StorageRequestReview.tsx @@ -3,12 +3,14 @@ import "./StorageRequestReview.css"; import { Alert } from "@codex-storage/marketplace-ui-components"; import { CardNumbers } from "../CardNumbers/CardNumbers"; import { FileWarning } from "lucide-react"; -import { classnames } from "../../utils/classnames"; -import { - AvailabilityUnit, - StorageRequest, - StorageRequestComponentProps, -} from "./types"; +import { StorageRequest, StorageRequestComponentProps } from "./types"; +import DurabilityIcon from "../../assets/icons/durability.svg?react"; +import AlphaIcon from "../../assets/icons/alpha.svg?react"; +import PresetIcon from "../../assets/icons/preset.svg?react"; +import CommitmentIcon from "../../assets/icons/commitment.svg?react"; +import RequestDurationIcon from "../../assets/icons/request-duration.svg?react"; +import { attributes } from "../../utils/attributes"; +import { Strings } from "../../utils/strings"; type Durability = { nodes: number; @@ -32,14 +34,14 @@ const findDurabilityIndex = (d: Durability) => { return durabilities.findIndex((d) => JSON.stringify(d) === s); }; -const units = ["days", "minutes", "hours", "days", "months", "years"]; +// const units = ["days", "minutes", "hours", "days", "months", "years"]; export function StorageRequestReview({ dispatch, onStorageRequestChange, storageRequest, }: StorageRequestComponentProps) { - const [durability, setDurability] = useState(1); + const [durability, setDurability] = useState(2); const isInvalidConstrainst = useCallback( (nodes: number, tolerance: number) => { @@ -124,7 +126,7 @@ export function StorageRequestReview({ }; const isInvalidAvailability = (data: string) => { - const [value, unit = "days"] = data.split(" "); + const [value] = data.split(" "); const error = isInvalidNumber(value); @@ -136,9 +138,9 @@ export function StorageRequestReview({ // unit += "s"; // } - if (!units.includes(unit)) { - return "Invalid unit must one of: minutes, hours, days, months, years"; - } + // if (!units.includes(unit)) { + // return "Invalid unit must one of: minutes, hours, days, months, years"; + // } return ""; }; @@ -156,7 +158,7 @@ export function StorageRequestReview({ onUpdateDurability({ proofProbability: Number(value) }); const onAvailabilityChange = (value: string) => { - const [availability, availabilityUnit = "days"] = value.split(" "); + const [availability] = value.split(" "); // if (!availabilityUnit.endsWith("s")) { // availabilityUnit += "s"; @@ -164,7 +166,7 @@ export function StorageRequestReview({ onStorageRequestChange({ availability: Number(availability), - availabilityUnit: availabilityUnit as AvailabilityUnit, + availabilityUnit: "months", }); }; @@ -189,157 +191,148 @@ export function StorageRequestReview({ // return data.availabilityUnit; // }; - const availability = `${storageRequest.availability} ${storageRequest.availabilityUnit}`; + const availability = storageRequest.availability; return ( -
- Durability -
- - - -
- -
-
- Define your durability profile -

+

+
+ +
+
Define your Durability Profile
+ Select the appropriate level of data storage reliability ensuring your information is protected and accessible. -

+
-
-
onDurabilityChange(0)} - className={classnames( - ["storageRequestReview-presets-bloc"], - [ - "storageRequestReview-presets--selected", - durability <= 0 || durability > 3, - ] - )}> -
- +
+
+
+
+ +
+ Durability + Suggested Defaults
-

Custom

onDurabilityChange(1)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 1] - )}> -
- -
-

Low

+ {...attributes({ + "aria-selected": durability <= 0 || durability > 3, + })} + onClick={() => onDurabilityChange(0)}> + Custom +
onDurabilityChange(2)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 2] - )}> -
- -
-

Medium

+ {...attributes({ + "aria-selected": durability == 1, + })} + onClick={() => onDurabilityChange(1)}> + Low +
onDurabilityChange(3)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 3] - )}> -
- -
-

High

+ {...attributes({ + "aria-selected": durability == 2, + })} + onClick={() => onDurabilityChange(2)}> + Medium + Recommanded + +
+
onDurabilityChange(3)}> + High +
-
- {/* */} +
+ + + +
- Commitment +
+ +
Commitment
+
-
- - - -
- {/* */} -
+
+ + + +
-
+
+ +
Request Duration
+
-
- - } - title="Warning" - variant="warning" - className="storageRequestReview-alert"> - If no suitable hosts are found for the CID {storageRequest.cid}{" "} - matching your storage requirements, you will incur a charge a small - amount of tokens. - -
+
+ + } + title="Warning" + variant="warning" + className="storageRequestReview-alert"> + If no suitable hosts are found for the CID{" "} + {Strings.shortId(storageRequest.cid)} matching your storage + requirements, you will incur a charge a small amount of tokens. + +
+
); } diff --git a/src/components/StorageRequestSetup/purchase.util.ts b/src/components/StorageRequestSetup/purchase.util.ts deleted file mode 100644 index e59ad4a..0000000 --- a/src/components/StorageRequestSetup/purchase.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TabSortState } from "@codex-storage/marketplace-ui-components" -import { CodexPurchase } from "@codex-storage/sdk-js" - -export const PurchaseUtils = { - sortById: (state: TabSortState) => - (a: CodexPurchase, b: CodexPurchase) => { - - return state === "desc" - ? b.requestId - .toLocaleLowerCase() - .localeCompare(a.requestId.toLocaleLowerCase()) - : a.requestId - .toLocaleLowerCase() - .localeCompare(b.requestId.toLocaleLowerCase()) - }, -} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 791f9c6..a144b1c 100644 --- a/src/index.css +++ b/src/index.css @@ -142,6 +142,10 @@ ul { padding: 0; } +dialog { + padding: 0; +} + input, button, textarea, diff --git a/src/routes/dashboard/availabilities.css b/src/routes/dashboard/availabilities.css index c32ee1f..73d526d 100644 --- a/src/routes/dashboard/availabilities.css +++ b/src/routes/dashboard/availabilities.css @@ -70,7 +70,7 @@ .button { top: 0; - bottom: 0; + bottom: 0px; left: 0; right: 0; position: absolute; diff --git a/src/routes/dashboard/purchases.css b/src/routes/dashboard/purchases.css index df90d3a..afb1ae9 100644 --- a/src/routes/dashboard/purchases.css +++ b/src/routes/dashboard/purchases.css @@ -1,12 +1,16 @@ -.purchases-modal { - margin: auto; -} +.purchases { + > div:first-child { + padding: 1rem 0; + display: flex; + align-items: center; + justify-content: flex-end; + } -.purchases-actions { - padding: 1rem; - display: flex; - align-items: center; - justify-content: flex-end; + .table { + table thead tr th { + background-color: #14141499; + } + } } .purchases-loader { diff --git a/src/routes/dashboard/purchases.tsx b/src/routes/dashboard/purchases.tsx index 08d6056..dbf0039 100644 --- a/src/routes/dashboard/purchases.tsx +++ b/src/routes/dashboard/purchases.tsx @@ -1,100 +1,20 @@ -import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; -import { CodexSdk } from "../../sdk/codex"; -import { - Cell, - Row, - Spinner, - Table, -} from "@codex-storage/marketplace-ui-components"; import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate"; import "./purchases.css"; -import { FileCell } from "../../components/FileCellRender/FileCell"; -import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender"; -import { Promises } from "../../utils/promises"; -import { TruncateCell } from "../../components/TruncateCell/TruncateCell"; -import { Times } from "../../utils/times"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; import { ErrorBoundary } from "@sentry/react"; -import { useData } from "../../hooks/useData"; +import { PurchasesTable } from "../../components/Purchase/PurchasesTable"; const Purchases = () => { - const content = useData(); - const { data, isPending } = useQuery({ - queryFn: () => - CodexSdk.marketplace() - .purchases() - .then((s) => Promises.rejectOnError(s)), - queryKey: ["purchases"], - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, - - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, - - initialData: [], - - // Throw the error to the error boundary - throwOnError: true, - }); - - if (isPending) { - return ( -
- -
- ); - } - - const headers = [ - "file", - "request id", - "duration", - "slots", - "reward", - "proof probability", - "state", - ]; - - const rows = data.map((p, index) => { - const r = p.request; - const ask = p.request.ask; - const duration = parseInt(p.request.ask.duration, 10); - const pf = parseInt(p.request.ask.proofProbability, 10); - - return ( - , - , - {Times.pretty(duration)}, - {ask.slots.toString()}, - {ask.reward + " CDX"}, - {pf.toString()}, - , - ]}> - ); - }); - return ( -
-
+
+
-
+
+ +
); };