Apply lot of improvements

This commit is contained in:
Arnaud 2024-08-29 18:45:52 +02:00
parent 9324c5ec0e
commit 8fcaa80248
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
52 changed files with 1826 additions and 814 deletions

26
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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>
);

View 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} />
</>
);
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -0,0 +1,9 @@
.cell-state--custom {
display: flex;
}
.cell-stateIcon {
position: relative;
top: 2px;
margin-right: 0.5rem;
}

View File

@ -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>
);
};

View 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>
</>
);
}

View 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%;
}

View 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>
);
}

View 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;
}
}

View 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>
</>
);
}

View 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);
}

View 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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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);
}
}

View File

@ -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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View File

@ -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))}
/>
</>
);

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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}

View 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>;
}

View 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;
}

View 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
View 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;
}

View File

@ -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%;
}

View File

@ -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} />;

View File

@ -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>

View File

@ -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>
</>

View 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;
}

View File

@ -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>
),
});

View File

@ -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>
</>

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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;
}

View File

@ -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>
</>
),

View File

@ -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>
);
}

View File

@ -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());
}
}

View File

@ -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) || "[]");
},
};

View File

@ -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";

View File

@ -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));
},
};

View 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
View 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);
},
};

View File

@ -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;
};

View 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);
},
};

View File

@ -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,
});
});
});