mirror of
https://github.com/codex-storage/codex-marketplace-ui.git
synced 2025-02-23 21:28:26 +00:00
Apply lot of improvements
This commit is contained in:
parent
9324c5ec0e
commit
8fcaa80248
26
package-lock.json
generated
26
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -1,23 +0,0 @@
|
||||
<svg
|
||||
version="1.1"
|
||||
id="loader-1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 50 50"
|
||||
style="enable-background: new 0 0 50 50"
|
||||
xml:space="preserve">
|
||||
<path
|
||||
fill="#FFF"
|
||||
d="M43.935,25.145c0-10.318-8.364-18.683-18.683-18.683c-10.318,0-18.683,8.365-18.683,18.683h4.068c0-8.071,6.543-14.615,14.615-14.615c8.072,0,14.615,6.543,14.615,14.615H43.935z">
|
||||
<animateTransform
|
||||
attributeType="xml"
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 25 25"
|
||||
to="360 25 25"
|
||||
dur="0.6s"
|
||||
repeatCount="indefinite"></animateTransform>
|
||||
</path>
|
||||
</svg>
|
Before Width: | Height: | Size: 660 B |
@ -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 (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
51
src/CodexUrllSettings/CodexUrlSettings.tsx
Normal file
51
src/CodexUrllSettings/CodexUrlSettings.tsx
Normal file
@ -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<HTMLInputElement>) => {
|
||||
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 = () => (
|
||||
<CircleCheck
|
||||
size="1.25rem"
|
||||
fill="var(--codex-color-primary)"
|
||||
stroke="var(--codex-background-light)"></CircleCheck>
|
||||
);
|
||||
|
||||
console.info({ url });
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
id="url"
|
||||
label="Codex client node URL"
|
||||
onChange={onChange}
|
||||
value={url}
|
||||
className="settings-input"></Input>
|
||||
<Button variant="primary" label="Save changes" onClick={onClick}></Button>
|
||||
<Toast message={toast.message} time={toast.time} Icon={Check} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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<HTMLInputElement>) => {
|
||||
setValue(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const onButtonClick = () => {
|
||||
setEditing(false);
|
||||
onChange?.(parseInt(value, 10));
|
||||
};
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div
|
||||
className={classnames(["cardNumber"], ["cardNumber--error", hasError])}>
|
||||
<div className="cardNumber-dataContainer">
|
||||
<Input
|
||||
id={title}
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
type="number"></Input>
|
||||
<ButtonIcon
|
||||
Icon={Check}
|
||||
variant="small"
|
||||
onClick={onButtonClick}></ButtonIcon>
|
||||
</div>
|
||||
<div className="cardNumber-info">
|
||||
<b className="cardNumber-title">{title}</b>
|
||||
{comment && (
|
||||
<Tooltip message={comment} className="cardNumber-tooltip">
|
||||
{hasError ? (
|
||||
<ShieldAlert size={"1rem"} />
|
||||
) : (
|
||||
<Info size={"1rem"} />
|
||||
)}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DataContainer = editing ? (
|
||||
<>
|
||||
<Input
|
||||
id={title}
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
type="number"></Input>
|
||||
<ButtonIcon
|
||||
Icon={Check}
|
||||
variant="small"
|
||||
onClick={onButtonClick}></ButtonIcon>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="cardNumber-data">{data}</p>
|
||||
<ButtonIcon
|
||||
onClick={onEditingClick}
|
||||
variant="small"
|
||||
Icon={() => <Pencil size={"1rem"} />}></ButtonIcon>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="cardNumber">
|
||||
<b className="cardNumber-title">{title}</b>
|
||||
<div className="cardNumber-dataContainer">
|
||||
<p className="cardNumber-data" contentEditable={editable}>
|
||||
{data}
|
||||
</p>
|
||||
{editable && (
|
||||
<div className="cardNumber-dataIcon">
|
||||
<Pencil size={"0.85rem"} />
|
||||
</div>
|
||||
<div
|
||||
className={classnames(["cardNumber"], ["cardNumber--error", hasError])}>
|
||||
<div className="cardNumber-dataContainer">{DataContainer}</div>
|
||||
<div className="cardNumber-info">
|
||||
<b className="cardNumber-title">{title}</b>
|
||||
{comment && (
|
||||
<Tooltip message={comment} className="cardNumber-tooltip">
|
||||
{hasError ? <ShieldAlert size={"1rem"} /> : <Info size={"1rem"} />}
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{comment && (
|
||||
<SimpleText variant="light" size="small">
|
||||
{comment}
|
||||
</SimpleText>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
.cell-state--custom {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cell-stateIcon {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
@ -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 (
|
||||
<p
|
||||
className={
|
||||
"cell-state cell-state--custom cell-state--" +
|
||||
states[state as keyof typeof states]
|
||||
}>
|
||||
{message ? (
|
||||
<Tooltip message={message}>
|
||||
<Icon size={"1rem"} className="cell-stateIcon" />
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Icon size={"1rem"} className="cell-stateIcon" />
|
||||
)}
|
||||
|
||||
<span>{state}</span>
|
||||
</p>
|
||||
);
|
||||
};
|
58
src/components/FileCellRender/FIleCell.tsx
Normal file
58
src/components/FileCellRender/FIleCell.tsx
Normal file
@ -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<FileMetadata>({
|
||||
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<FileMetadata>(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 (
|
||||
<>
|
||||
<div className="fileCell">
|
||||
<WebFileIcon type={metadata.mimetype} />
|
||||
<div>
|
||||
<span className="fileCell-title">{name}</span>
|
||||
<span className="fileCell-subtitle">
|
||||
<Tooltip message={cid}>{cidTruncated}</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
20
src/components/FileCellRender/FileCell.css
Normal file
20
src/components/FileCellRender/FileCell.css
Normal file
@ -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%;
|
||||
}
|
39
src/components/Files/CidCopyButton.tsx
Normal file
39
src/components/Files/CidCopyButton.tsx
Normal file
@ -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<number | null>(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 = () => <Copy size={ICON_SIZE} />;
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={label}
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
Icon={Icon}></Button>
|
||||
);
|
||||
}
|
78
src/components/Files/FileDetails.css
Normal file
78
src/components/Files/FileDetails.css
Normal file
@ -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;
|
||||
}
|
||||
}
|
92
src/components/Files/FileDetails.tsx
Normal file
92
src/components/Files/FileDetails.tsx
Normal file
@ -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 = () => <X size={ICON_SIZE} onClick={onClose} />;
|
||||
|
||||
const onDownload = () => window.open(url + details?.cid, "_target");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="files-backdrop backdrop"
|
||||
onClick={onClose}
|
||||
{...attr}></div>
|
||||
|
||||
<div className="fileDetails" {...attr}>
|
||||
{details && (
|
||||
<>
|
||||
<div className="fileDetails-header">
|
||||
<b className="fileDetails-headerTitle">File details</b>
|
||||
<ButtonIcon variant="small" Icon={Icon}></ButtonIcon>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-body">
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">CID:</p>
|
||||
<p className="fileDetails-gridColumn">{details.cid}</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">File name:</p>
|
||||
<p className="fileDetails-gridColumn">{details.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">Date:</p>
|
||||
<p className="fileDetails-gridColumn">
|
||||
{Dates.format(details.uploadedAt).toString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">Mimetype:</p>
|
||||
<p className="fileDetails-gridColumn">{details.mimetype}</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">Size:</p>
|
||||
<p className="fileDetails-gridColumn">
|
||||
{PrettyBytes(details.manifest.datasetSize)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-grid">
|
||||
<p className="text-secondary">Protected:</p>
|
||||
<p className="fileDetails-gridColumn">
|
||||
{details.manifest.protected ? "Yes" : "No"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fileDetails-actions">
|
||||
<CidCopyButton cid={details.cid} />
|
||||
|
||||
<Button
|
||||
Icon={() => <DownloadIcon size={ICON_SIZE} />}
|
||||
label="Download"
|
||||
onClick={onDownload}></Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
110
src/components/Files/Files.css
Normal file
110
src/components/Files/Files.css
Normal file
@ -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);
|
||||
}
|
163
src/components/Files/Files.tsx
Normal file
163
src/components/Files/Files.tsx
Normal file
@ -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 (
|
||||
<Star size={ICON_SIZE} className="files-fileFavorite files-fileStar" />
|
||||
);
|
||||
}
|
||||
|
||||
return <Star size={ICON_SIZE} className="files-star" />;
|
||||
}
|
||||
|
||||
export function Files() {
|
||||
const files = useData();
|
||||
const cid = useRef<string | null>("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [favorites, setFavorites] = useState<string[]>([]);
|
||||
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 (
|
||||
<div className="files">
|
||||
<div className="files-header">
|
||||
<div className="files-title">Files</div>
|
||||
<div className="files-headerTabs">
|
||||
<div
|
||||
className={classnames(
|
||||
["files-headerTab"],
|
||||
["files-headerTab--active", selected === "all"]
|
||||
)}
|
||||
onClick={onSelected}>
|
||||
<FilesIcon size={"1rem"}></FilesIcon>
|
||||
<span>All files</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames(
|
||||
["files-headerTab"],
|
||||
["files-headerTab--active", selected === "favorites"]
|
||||
)}
|
||||
onClick={onSelected}>
|
||||
<Star size={"1rem"}></Star>
|
||||
<span>Favorites</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="files-fileBody">
|
||||
{items.length ? (
|
||||
items.map((c) => (
|
||||
<div className="files-file" key={c.cid}>
|
||||
<div className="files-fileContent">
|
||||
<div className="files-fileIcon">
|
||||
<WebFileIcon type={c.mimetype} />
|
||||
</div>
|
||||
<div className="files-fileData">
|
||||
<div>
|
||||
<b>{c.name}</b>
|
||||
<div>
|
||||
<small className="files-fileMeta">
|
||||
{PrettyBytes(c.manifest.datasetSize)} -{" "}
|
||||
{Dates.format(c.uploadedAt).toString()} - ...
|
||||
{c.cid.slice(-5)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="files-fileActions">
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => window.open(url + c.cid, "_blank")}
|
||||
Icon={() => <Download size={ICON_SIZE} />}></ButtonIcon>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => onToggleFavorite(c.cid)}
|
||||
Icon={() => (
|
||||
<StarIcon isFavorite={favorites.includes(c.cid)} />
|
||||
)}></ButtonIcon>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => onDetails(c.cid)}
|
||||
Icon={() => (
|
||||
<ReceiptText size={ICON_SIZE} />
|
||||
)}></ButtonIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="files-placeholder">
|
||||
<EmptyPlaceholder
|
||||
title="Nothing to show"
|
||||
message="No data here yet. Start to upload files to see data here."
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FileDetails onClose={onClose} details={details} expanded={expanded} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<Card className="logLevel" title="Debug">
|
||||
<>
|
||||
<Select
|
||||
className="logLevel-select"
|
||||
id="level"
|
||||
@ -73,6 +73,6 @@ export function LogLevel() {
|
||||
fetching={isPending}
|
||||
onClick={onClick}></Button>
|
||||
<Toast message={toast.message} time={toast.time} Icon={Check} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,129 +0,0 @@
|
||||
.manifest {
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--codex-border-radius);
|
||||
background-color: var(--codex-background-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.manifest-content {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.manifest-icon {
|
||||
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;
|
||||
}
|
||||
|
||||
.manifest-data {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.manifest-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--codex-border-color);
|
||||
border-radius: var(--codex-border-radius);
|
||||
padding: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.manifest-details {
|
||||
position: fixed;
|
||||
transition: transform 0.25s;
|
||||
background-color: var(--codex-background-secondary);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.manifest-detailsHeaderTitle {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.manifest-backdrop {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.manifest-details {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.manifest-detailsHeader {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 1px solid var(--codex-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.manifest-detailsBody {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.manifest-detailsGrid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
display: grid;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-bottom: 1px solid var(--codex-border-color);
|
||||
}
|
||||
|
||||
.manifest-detailsGridColumn {
|
||||
grid-column: span 2 / span 2;
|
||||
color: var(--codex-text-contrast);
|
||||
}
|
||||
|
||||
.manifest-detailsActions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
}
|
||||
|
||||
.manifest-star {
|
||||
transition:
|
||||
fill 0.35s,
|
||||
stroke 0.35s;
|
||||
}
|
||||
|
||||
.manifest-favorite {
|
||||
fill: yellow;
|
||||
stroke: yellow;
|
||||
}
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
.manifest-details {
|
||||
width: 300px;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
transform: translatex(300px);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.manifest-details[aria-expanded] {
|
||||
transform: translatex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 999px) {
|
||||
.manifest-details {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
transform: translatey(1000px);
|
||||
left: 0;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.manifest-details[aria-expanded] {
|
||||
transform: translatey(0);
|
||||
}
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
import { CodexDataContent } from "@codex/sdk-js";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Copy,
|
||||
Download,
|
||||
DownloadIcon,
|
||||
ReceiptText,
|
||||
Star,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { attributes } from "../../utils/attributes.ts";
|
||||
import { BrowserStorage } from "../../utils/browser-storage.ts";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { Dates } from "../../utils/dates.ts";
|
||||
import "./Manitests.css";
|
||||
import {
|
||||
COPY_DURATION,
|
||||
ICON_SIZE,
|
||||
SIDE_DURATION,
|
||||
} from "../../utils/constants.ts";
|
||||
import {
|
||||
Button,
|
||||
ButtonIcon,
|
||||
WebFileIcon,
|
||||
} from "@codex/marketplace-ui-components";
|
||||
|
||||
type StarIconProps = {
|
||||
favorites: string;
|
||||
cid: string;
|
||||
};
|
||||
|
||||
function StarIcon({ favorites, cid }: StarIconProps) {
|
||||
if (favorites.includes(cid)) {
|
||||
return (
|
||||
<Star size={ICON_SIZE} className="manifest-favorite manifest-star" />
|
||||
);
|
||||
}
|
||||
|
||||
return <Star size={ICON_SIZE} className="manifest-star" />;
|
||||
}
|
||||
|
||||
export function Manifests() {
|
||||
const { data } = useQuery({
|
||||
queryFn: () => CodexSdk.data().then((data) => data.cids()),
|
||||
queryKey: ["cids"],
|
||||
// refetchOnWindowFocus: false,
|
||||
// refetchOnMount: false,
|
||||
});
|
||||
const cid = useRef<string | null>("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const onClose = () => {
|
||||
setExpanded(false);
|
||||
|
||||
setTimeout(() => {
|
||||
cid.current = "";
|
||||
}, SIDE_DURATION);
|
||||
};
|
||||
|
||||
const onDetails = (id: string) => {
|
||||
// router.navigate({
|
||||
// to: "/dashboard/about",
|
||||
// params: data?.data.content.find((c) => c.cid === cid) || {},
|
||||
// state: data?.data.content.find((c) => c.cid === cid) || true,
|
||||
// });
|
||||
|
||||
cid.current = id;
|
||||
setExpanded(true);
|
||||
};
|
||||
|
||||
const onToggleFavorite = (cid: string) =>
|
||||
BrowserStorage.toggle("favorites", cid);
|
||||
|
||||
if (data?.error) {
|
||||
// TODO error
|
||||
return "";
|
||||
}
|
||||
|
||||
const details = data?.data.content.find((c) => c.cid === cid.current);
|
||||
|
||||
const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
|
||||
const favorites = BrowserStorage.values("favorites");
|
||||
|
||||
return (
|
||||
<>
|
||||
{data?.data.content.map((c) => (
|
||||
<div className="manifest" key={c.cid}>
|
||||
<div className="manifest-content">
|
||||
<div className="manifest-icon">
|
||||
<WebFileIcon type={c.manifest.mimetype} />
|
||||
</div>
|
||||
<div className="manifest-data">
|
||||
<div>
|
||||
<b>{c.manifest.filename}</b>
|
||||
<div>
|
||||
<small className="manifest-meta">
|
||||
{PrettyBytes(c.manifest.datasetSize)} -{" "}
|
||||
{Dates.format(c.manifest.uploadedAt).toString()} - ...
|
||||
{c.cid.slice(-5)}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="manifest-actions">
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => window.open(url + c.cid, "_blank")}
|
||||
Icon={() => <Download size={ICON_SIZE} />}></ButtonIcon>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => onToggleFavorite(c.cid)}
|
||||
Icon={() => (
|
||||
<StarIcon favorites={favorites} cid={c.cid} />
|
||||
)}></ButtonIcon>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => onDetails(c.cid)}
|
||||
Icon={() => <ReceiptText size={ICON_SIZE} />}></ButtonIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<ManifestDetails
|
||||
onClose={onClose}
|
||||
details={details}
|
||||
expanded={expanded}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type ManifestDetailsProps = {
|
||||
details: CodexDataContent | undefined;
|
||||
onClose: () => void;
|
||||
expanded: boolean;
|
||||
};
|
||||
|
||||
function ManifestDetails({ onClose, details, expanded }: ManifestDetailsProps) {
|
||||
const attr = attributes({ "aria-expanded": expanded });
|
||||
const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
|
||||
|
||||
const Icon = () => <X size={ICON_SIZE} onClick={onClose} />;
|
||||
|
||||
const onDownload = () => window.open(url + details?.cid, "_target");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="manifest-backdrop backdrop"
|
||||
onClick={onClose}
|
||||
{...attr}></div>
|
||||
|
||||
<div className="manifest-details" {...attr}>
|
||||
{details && (
|
||||
<>
|
||||
<div className="manifest-detailsHeader">
|
||||
<b className="manifest-detailsHeaderTitle">File details</b>
|
||||
<ButtonIcon variant="small" Icon={Icon}></ButtonIcon>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsBody">
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">CID:</p>
|
||||
<p className="manifest-detailsGridColumn">{details.cid}</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">File name:</p>
|
||||
<p className="manifest-detailsGridColumn">
|
||||
{details.manifest.filename}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">Date:</p>
|
||||
<p className="manifest-detailsGridColumn">
|
||||
{Dates.format(details.manifest.uploadedAt).toString()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">Mimetype:</p>
|
||||
<p className="manifest-detailsGridColumn">
|
||||
{details.manifest.mimetype}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">Size:</p>
|
||||
<p className="manifest-detailsGridColumn">
|
||||
{PrettyBytes(details.manifest.datasetSize)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsGrid">
|
||||
<p className="text-secondary">Protected:</p>
|
||||
<p className="manifest-detailsGridColumn">
|
||||
{details.manifest.protected ? "Yes" : "No"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="manifest-detailsActions">
|
||||
<CopyButton cid={details.cid} />
|
||||
|
||||
<Button
|
||||
Icon={() => <DownloadIcon size={ICON_SIZE} />}
|
||||
label="Download"
|
||||
onClick={onDownload}></Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type CopyButtonProps = {
|
||||
cid: string;
|
||||
};
|
||||
|
||||
function CopyButton({ cid }: CopyButtonProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timeout = useRef<number | null>(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 = () => <Copy size={ICON_SIZE} />;
|
||||
|
||||
return (
|
||||
<Button
|
||||
label={label}
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
Icon={Icon}></Button>
|
||||
);
|
||||
}
|
11
src/components/Range/Range.css
Normal file
11
src/components/Range/Range.css
Normal file
@ -0,0 +1,11 @@
|
||||
.range {
|
||||
width: 100%;
|
||||
accent-color: var(--codex-color-primary);
|
||||
height: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.range-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
45
src/components/Range/Range.tsx
Normal file
45
src/components/Range/Range.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { ChangeEvent } from "react";
|
||||
import "./Range.css";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
max: number;
|
||||
labels: string[];
|
||||
className?: string;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
defaultValue?: number | string;
|
||||
value?: number | string;
|
||||
};
|
||||
|
||||
export function Range({
|
||||
label,
|
||||
max,
|
||||
labels,
|
||||
onChange,
|
||||
defaultValue,
|
||||
value,
|
||||
className = "",
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={className}>
|
||||
{label}
|
||||
<input
|
||||
type="range"
|
||||
max={max}
|
||||
min={1}
|
||||
step="1"
|
||||
className="range"
|
||||
onChange={onChange}
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
/>
|
||||
<div className="range-labels">
|
||||
{labels.map((l) => (
|
||||
<div className="range-label" key={l}>
|
||||
{l}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
30
src/components/StorageRequestSetup/StorageRequestDone.tsx
Normal file
30
src/components/StorageRequestSetup/StorageRequestDone.tsx
Normal file
@ -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 (
|
||||
<div className="emptyPlaceholder" style={{ margin: "auto" }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "1rem",
|
||||
color: "var(--codex-color-primary)",
|
||||
}}>
|
||||
<CircleCheck size="4rem" />
|
||||
</div>
|
||||
<b className="emptyPlaceholder-title">Your request is being processed.</b>
|
||||
<div className="emptyPlaceholder-message">
|
||||
Processing your request may take some time. Once completed, it will
|
||||
appear in your purchase list. You can safely close this dialog.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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<HTMLInputElement>) => {
|
||||
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: () => <WebFileIcon type={c.manifest.mimetype} size={24} />,
|
||||
title: c.manifest.filename,
|
||||
subtitle: c.cid,
|
||||
Icon: () => <WebFileIcon type={f.mimetype} size={24} />,
|
||||
title: f.name,
|
||||
subtitle: f.cid,
|
||||
};
|
||||
}) || [];
|
||||
|
||||
@ -84,6 +85,8 @@ export function StorageRequestFileChooser({ onToggleNext }: Props) {
|
||||
</label>
|
||||
|
||||
<Dropdown
|
||||
label=""
|
||||
id="cid"
|
||||
placeholder="Select or type your CID"
|
||||
onChange={onChange}
|
||||
value={cid}
|
||||
@ -114,9 +117,7 @@ export function StorageRequestFileChooser({ onToggleNext }: Props) {
|
||||
onSuccess={onSuccess}
|
||||
editable={false}
|
||||
onDeleteItem={onDelete}
|
||||
provider={() =>
|
||||
CodexSdk.data().then((data) => data.upload.bind(CodexSdk))
|
||||
}
|
||||
provider={() => CodexSdk.data().then((data) => data.upload.bind(data))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<StorageAvailabilityValue>({
|
||||
unit: "days",
|
||||
value: 0,
|
||||
const [errors, setErrors] = useState({
|
||||
nodes: "",
|
||||
tolerance: "",
|
||||
proofProbability: "",
|
||||
});
|
||||
const [durability, setDurability] = useState<StorageDurabilityStepValue>({
|
||||
nodes: 0,
|
||||
proofProbability: 0,
|
||||
tolerance: 0,
|
||||
});
|
||||
const [price, setPrice] = useState<StoragePriceStepValue>({
|
||||
collateral: 0,
|
||||
expiration: 0,
|
||||
reward: 0,
|
||||
const [durability, setDurability] = useState<number>(1);
|
||||
const [price, setPrice] = useState<number>(1);
|
||||
const [data, setData] = useState<Data>({
|
||||
availabilityUnit: "days",
|
||||
availability: 1,
|
||||
tolerance: 1,
|
||||
proofProbability: 1,
|
||||
nodes: 3,
|
||||
reward: 10,
|
||||
collateral: 10,
|
||||
expiration: 300,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
WebStorage.get<Data>("storage-request-criteria"),
|
||||
WebStorage.get<string>("storage-request-step-1"),
|
||||
WebStorage.get<StorageAvailabilityValue>("storage-request-step-2"),
|
||||
WebStorage.get<StorageDurabilityStepValue>("storage-request-step-3"),
|
||||
WebStorage.get<StoragePriceStepValue>("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<Data>) => {
|
||||
setData((d) => {
|
||||
const newData = { ...d, ...p };
|
||||
|
||||
WebStorage.set("storage-request-criteria", newData);
|
||||
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
const onDurabilityRangeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const l = parseInt(e.currentTarget.value, 10);
|
||||
|
||||
const durability = durabilities[l - 1];
|
||||
|
||||
updateData(durability);
|
||||
setDurability(l);
|
||||
setErrors({ nodes: "", tolerance: "", proofProbability: "" });
|
||||
};
|
||||
|
||||
const onPriceRangeChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div>
|
||||
<span className="storageRequest-title">Review your request</span>
|
||||
<span className="storageRequest-title">Choose your criteria</span>
|
||||
<div className="storageRequestReview-numbers">
|
||||
<CardNumbers
|
||||
title={"Nodes"}
|
||||
data={data.nodes.toString()}
|
||||
comment={errors.nodes || "Storage nodes required"}
|
||||
onChange={onNodesChange}
|
||||
hasError={!!errors.nodes}></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Tolerance"}
|
||||
data={data.tolerance.toString()}
|
||||
comment={errors.tolerance || "Failure nodes tolerated"}
|
||||
onChange={onToleranceChange}
|
||||
hasError={!!errors.tolerance}></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Proof probability"}
|
||||
data={data.proofProbability.toString()}
|
||||
comment={errors.proofProbability || "Proof request frequency"}
|
||||
onChange={onProofProbabilityChange}
|
||||
hasError={!!errors.proofProbability}></CardNumbers>
|
||||
</div>
|
||||
|
||||
<Range
|
||||
onChange={onDurabilityRangeChange}
|
||||
className={classnames(
|
||||
["storageRequestReview-range"],
|
||||
["storageRequestReview-range--disabled", durability === 0]
|
||||
)}
|
||||
labels={["Weak", "Low", "Medium", "High", "Confident"]}
|
||||
max={5}
|
||||
label=""
|
||||
value={durability}
|
||||
/>
|
||||
|
||||
<div className="storageRequestReview-numbers">
|
||||
<CardNumbers
|
||||
title={"Contract duration"}
|
||||
data={availability.value.toString()}
|
||||
comment={"Contract duration in " + availability.unit}
|
||||
editable></CardNumbers>
|
||||
|
||||
<CardNumbers
|
||||
title={"Nodes"}
|
||||
data={durability.nodes.toString()}
|
||||
comment={"Storage nodes required"}
|
||||
editable></CardNumbers>
|
||||
|
||||
<CardNumbers
|
||||
title={"Tolerance"}
|
||||
data={durability.tolerance.toString()}
|
||||
comment={"Failure nodes tolerated"}
|
||||
editable></CardNumbers>
|
||||
|
||||
<CardNumbers
|
||||
title={"Proof probability"}
|
||||
data={durability.proofProbability.toString()}
|
||||
comment={"Proof request frequency"}
|
||||
editable></CardNumbers>
|
||||
data={data.availability.toString()}
|
||||
comment={"Contract duration in " + data.availabilityUnit}
|
||||
onChange={onAvailabilityChange}></CardNumbers>
|
||||
|
||||
<CardNumbers
|
||||
title={"Reward"}
|
||||
data={price.reward.toString()}
|
||||
data={data.reward.toString()}
|
||||
comment={"Reward tokens"}
|
||||
editable></CardNumbers>
|
||||
onChange={onRewardChange}></CardNumbers>
|
||||
|
||||
<CardNumbers
|
||||
title={"Collateral"}
|
||||
data={price.reward.toString()}
|
||||
data={data.reward.toString()}
|
||||
comment={"Penality tokens"}
|
||||
editable></CardNumbers>
|
||||
onChange={onCollateralChange}></CardNumbers>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<SimpleText variant="light">This request with CID </SimpleText>{" "}
|
||||
<b>{cid}</b> <SimpleText variant="light">will expire in </SimpleText>
|
||||
<b>{plurals("minute", price.expiration)} </b>
|
||||
<SimpleText variant="light">after the start.</SimpleText>
|
||||
</p>
|
||||
|
||||
<Alert
|
||||
message="If no suitable hosts are found matching your storage
|
||||
requirements, you will incur a charge of X tokens."
|
||||
variant="warning"
|
||||
<Range
|
||||
className={classnames(
|
||||
["storageRequestReview-range"],
|
||||
["storageRequestReview-range--disabled", price === 0]
|
||||
)}
|
||||
labels={["Low", "Average", "Attractive"]}
|
||||
max={3}
|
||||
label=""
|
||||
onChange={onPriceRangeChange}
|
||||
/>
|
||||
|
||||
<Alert
|
||||
Icon={<FileWarning />}
|
||||
title="Warning"
|
||||
variant="warning"
|
||||
className="storageRequestReview-alert">
|
||||
This request with CID
|
||||
<b> {cid}</b> will expire in
|
||||
<b> {plurals("minute", data.expiration)} </b>
|
||||
after the start. <br />
|
||||
If no suitable hosts are found matching your storage requirements, you
|
||||
will incur a charge of X tokens.
|
||||
</Alert>
|
||||
<hr className="storageRequestReview-hr" />
|
||||
|
||||
<p className="text-center">
|
||||
<b className=" storageRequestReview-title">
|
||||
Price comparaison with the market
|
||||
</b>
|
||||
</p>
|
||||
|
||||
<div className="storageRequestReview-legend">
|
||||
<div className="storageRequestReview-legendItem">
|
||||
<span className="storageRequestReview-legendItemColor storageRequestReview-legendItemColor-cheap"></span>
|
||||
@ -142,7 +380,6 @@ export function StorageRequestReview({ onToggleNext }: Props) {
|
||||
<span>Excellent</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="storageRequestReview-bar">
|
||||
<div className="storageRequestReview-barIndicator"></div>
|
||||
</div>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<string>("storage-request-step-1"),
|
||||
WebStorage.get<StorageAvailabilityValue>("storage-request-step-2"),
|
||||
WebStorage.get<StorageDurabilityStepValue>("storage-request-step-3"),
|
||||
WebStorage.get<StoragePriceStepValue>("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<string>("storage-request-step-1"),
|
||||
// TODO define criteria interface
|
||||
// eslint-disable-next-line
|
||||
WebStorage.get<any>("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) {
|
||||
)}>
|
||||
<Stepper
|
||||
titles={steps.current}
|
||||
Body={() => <Body onToggleNext={() => setIsNextDisable(false)} />}
|
||||
Body={<Body onChangeNextState={onChangeNextState} />}
|
||||
step={step}
|
||||
onChangeStep={onChangeStep}
|
||||
progress={progress || isPending}
|
||||
|
4
src/components/TruncateCellRender/TruncateCellRender.tsx
Normal file
4
src/components/TruncateCellRender/TruncateCellRender.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export function TruncateCellRender(cid: string) {
|
||||
const truncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5);
|
||||
return <span>{truncated}</span>;
|
||||
}
|
30
src/components/Welcome/Welcome.css
Normal file
30
src/components/Welcome/Welcome.css
Normal file
@ -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;
|
||||
}
|
21
src/components/Welcome/Welcome.tsx
Normal file
21
src/components/Welcome/Welcome.tsx
Normal file
@ -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 (
|
||||
<div className="welcome">
|
||||
<p className="welcome-title">Welcome to Codex Marketplace</p>
|
||||
<div className="welcome-body">
|
||||
<SimpleText variant="light">
|
||||
You can start using Codex for testing purpose by uploading new files.
|
||||
</SimpleText>
|
||||
</div>
|
||||
|
||||
<Link to="/dashboard/help" className="welcome-link">
|
||||
Explore more content <ChevronRight size={"1.5rem"} />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
49
src/hooks/useData.tsx
Normal file
49
src/hooks/useData.tsx
Normal file
@ -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;
|
||||
}
|
@ -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%;
|
||||
}
|
||||
|
@ -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 = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
<Link to="/dashboard/favorites" {...p}>
|
||||
<Star size={ICON_SIZE} />
|
||||
Favorites
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
@ -77,6 +60,17 @@ const Layout = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
<Link to="/dashboard/help" {...p}>
|
||||
<HelpCircle size={"1.25rem"} /> Help
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
@ -86,17 +80,6 @@ const Layout = () => {
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
type: "menu-item",
|
||||
Component: (p: MenuItemComponentProps) => (
|
||||
<a {...p}>
|
||||
<HelpCircle size={"1.25rem"} /> Help
|
||||
</a>
|
||||
),
|
||||
},
|
||||
] satisfies MenuItem[];
|
||||
|
||||
return <Page children={<Outlet />} items={items} Right={Right} />;
|
||||
|
@ -88,7 +88,7 @@ const About = () => {
|
||||
<a
|
||||
className="button"
|
||||
target="_blank"
|
||||
href={"http://localhost:8002/api/codex/v1/data/" + c.cid}>
|
||||
href={"/api/codex/v1/data/" + c.cid}>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
@ -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: () => (
|
||||
<>
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<div className="container">
|
||||
<Manifests />
|
||||
<Files />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
|
35
src/routes/dashboard/help.css
Normal file
35
src/routes/dashboard/help.css
Normal file
@ -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;
|
||||
}
|
@ -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: () => <div className="container">Hello /dashboard/help!</div>,
|
||||
component: () => (
|
||||
<div className="container">
|
||||
<div className="container-fluid">
|
||||
<div className="help">
|
||||
<h1 className="help-title">You might be wondering...</h1>
|
||||
|
||||
<div className="help-item">
|
||||
<HelpCircle className="help-itemIcon" size={"1.5rem"} />
|
||||
<div className="help-itemBody">
|
||||
<p className="help-itemTitle">Looking for help to build Codex?</p>
|
||||
<SimpleText variant="light">
|
||||
Yes, you should refer to the documentation. If you do need more
|
||||
help, ask to the github project.
|
||||
</SimpleText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="help-item">
|
||||
<HelpCircle className="help-itemIcon" size={"1rem"} />
|
||||
<div className="help-itemBody">
|
||||
<p className="help-itemTitle">Looking for help to build Codex?</p>
|
||||
<SimpleText variant="light">
|
||||
Yes, you should refer to the documentation. If you do need more
|
||||
help, ask to the github project.
|
||||
</SimpleText>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <ErrorBoundary fallback={() => ""}>
|
||||
<Debug />
|
||||
</ErrorBoundary> */}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
@ -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() {
|
||||
<Upload
|
||||
multiple
|
||||
provider={() =>
|
||||
CodexSdk.data().then((data) => data.upload.bind(CodexSdk))
|
||||
CodexSdk.data().then((data) => data.upload.bind(data))
|
||||
}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<LogLevel />
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<Card title="Node space allocation">
|
||||
<NodeSpaceAllocation />
|
||||
</Card>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<Card title="Empty state">
|
||||
<EmptyPlaceholder
|
||||
title="Nothing to show"
|
||||
message="No data here yet. We will notify you when there's an update."
|
||||
/>
|
||||
</Card>
|
||||
<Welcome />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className="container-fluid">
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<Manifests />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
<div className="container-fluid">
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<Debug />
|
||||
<Files />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</>
|
||||
|
@ -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;
|
||||
|
@ -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 <div>Error: {data.data.message}</div>;
|
||||
// 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 [
|
||||
<FileCell requestId={r.id} purchaseCid={r.content.cid} index={index} />,
|
||||
<Cell value={prettyMilliseconds(duration)} />,
|
||||
<Cell value={ask.slots + " hosts"} />,
|
||||
<Cell value={ask.reward + " tokens"} />,
|
||||
<Cell value={"Every " + prettyMilliseconds(pf)} />,
|
||||
<CustomStateCellRender state={p.state} message={p.error} />,
|
||||
];
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
@ -83,7 +74,8 @@ const Purchases = () => {
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
<Table headers={headers} data={purchases} cells={cells} />
|
||||
|
||||
{!open && <Table headers={headers} cells={cells} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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: () => (
|
||||
<>
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<div className="container">
|
||||
<p>Settings</p>
|
||||
<div className="settings">
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<LogLevel />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* <div className="input-floating">
|
||||
<div className="settings">
|
||||
<ErrorBoundary fallback={() => ""}>
|
||||
<CodexUrlSettings />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
{/* <div className="input-floating">
|
||||
<input
|
||||
className="input input-floating-input"
|
||||
id="input-floating"
|
||||
@ -33,7 +44,6 @@ export const Route = createFileRoute("/dashboard/settings")({
|
||||
Floating
|
||||
</label>
|
||||
</div> */}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
),
|
||||
|
@ -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() {
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!</h3>
|
||||
|
||||
<Link to="/dashboard/index">Go to dashboard</Link>
|
||||
<Link to="/dashboard">Go to dashboard</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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<string>("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());
|
||||
}
|
||||
}
|
||||
|
@ -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) || "[]");
|
||||
},
|
||||
};
|
@ -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";
|
||||
|
@ -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));
|
||||
},
|
||||
};
|
||||
|
17
src/utils/favorite-storage.tsx
Normal file
17
src/utils/favorite-storage.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { createStore, del, keys, set } from "idb-keyval";
|
||||
|
||||
const store = createStore("favorites", "favorites");
|
||||
|
||||
export const FavoriteStorage = {
|
||||
list() {
|
||||
return keys<string>(store);
|
||||
},
|
||||
|
||||
delete(key: string) {
|
||||
return del(key, store);
|
||||
},
|
||||
|
||||
async add(key: string) {
|
||||
return set(key, "1", store);
|
||||
},
|
||||
};
|
27
src/utils/file-storage.ts
Normal file
27
src/utils/file-storage.ts
Normal file
@ -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<string, FileMetadata>(store);
|
||||
},
|
||||
|
||||
// delete(key: string) {
|
||||
// return del(key, store);
|
||||
// },
|
||||
|
||||
async get<T>(cid: string) {
|
||||
return get<T>(cid, store);
|
||||
},
|
||||
|
||||
async set(cid: string, metadata: FileMetadata) {
|
||||
return set(cid, metadata, store);
|
||||
},
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
13
src/utils/purchases-storage.ts
Normal file
13
src/utils/purchases-storage.ts
Normal file
@ -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<string>(key, store);
|
||||
},
|
||||
|
||||
async set(key: string, cid: string) {
|
||||
return set(key, cid, store);
|
||||
},
|
||||
};
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user