<>
@@ -165,13 +84,8 @@ export function CardNumbers({
{error ? (
{error}
) : (
-
- {helper}
-
- )}
+ {helper}
+ )} */}
);
}
diff --git a/src/components/ConnectedAccount/ConnectedAccount.css b/src/components/ConnectedAccount/ConnectedAccount.css
new file mode 100644
index 0000000..55f6c46
--- /dev/null
+++ b/src/components/ConnectedAccount/ConnectedAccount.css
@@ -0,0 +1,93 @@
+.connected-account {
+ border-radius: 8px;
+ display: flex;
+ flex-direction: column;
+ min-width: 550px;
+
+ main {
+ flex: 1;
+ }
+
+ > footer {
+ ul {
+ display: flex;
+ list-style-type: none;
+ margin-top: 16px;
+ border: 1px solid #96969633;
+ background-color: #14141499;
+ height: 24px;
+ border-radius: 6px;
+
+ li {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+ text-align: left;
+ color: #969696;
+
+ &[aria-selected] {
+ background: #2f2f2f;
+ color: white;
+ }
+ }
+ }
+
+ button {
+ background-color: #161616;
+ border: 1px solid #96969633;
+ height: 24px;
+ width: 24px;
+ border-radius: 6px;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ svg {
+ position: relative;
+ left: -2px;
+ }
+ }
+
+ > div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 16px;
+ }
+
+ h6 {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ color: #969696;
+ }
+
+ var {
+ font-family: Inter;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.015em;
+ text-align: left;
+ color: white;
+ font-style: normal;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+ text-align: left;
+ color: #7d7d7d;
+ }
+ }
+}
diff --git a/src/components/ConnectedAccount/ConnectedAccount.tsx b/src/components/ConnectedAccount/ConnectedAccount.tsx
new file mode 100644
index 0000000..37ecf33
--- /dev/null
+++ b/src/components/ConnectedAccount/ConnectedAccount.tsx
@@ -0,0 +1,38 @@
+import "./ConnectedAccount.css";
+import { WalletCard } from "./WalletCard";
+import { ProgressCircle } from "./ProgressCircle";
+import ArrowRightIcon from "../../assets/icons/arrow-right.svg?react";
+
+export function ConnectedAccount() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ConnectedAccount/ProgressCircle.css b/src/components/ConnectedAccount/ProgressCircle.css
new file mode 100644
index 0000000..6f73ad1
--- /dev/null
+++ b/src/components/ConnectedAccount/ProgressCircle.css
@@ -0,0 +1,23 @@
+.progress-circle {
+ border-radius: 50%;
+ width: 48px;
+ height: 48px;
+ display: flex;
+ background: #2f2f2f;
+
+ background-image:
+ -webkit-linear-gradient(90deg, #2f2f2f 50%, transparent 50%),
+ -webkit-linear-gradient(270deg, var(--codex-color-primary) 50%, #2f2f2f 50%);
+ background-image: linear-gradient(90deg, #2f2f2f 50%, transparent 50%),
+ linear-gradient(270deg, var(--codex-color-primary) 50%, #2f2f2f 50%);
+
+ div {
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ margin: auto;
+ background: #232323;
+ text-align: center;
+ box-sizing: border-box;
+ }
+}
diff --git a/src/components/ConnectedAccount/ProgressCircle.tsx b/src/components/ConnectedAccount/ProgressCircle.tsx
new file mode 100644
index 0000000..1137d1b
--- /dev/null
+++ b/src/components/ConnectedAccount/ProgressCircle.tsx
@@ -0,0 +1,13 @@
+import "./ProgressCircle.css";
+
+type Props = {
+ value: number;
+};
+
+export function ProgressCircle(_: Props) {
+ return (
+
+ );
+}
diff --git a/src/components/ConnectedAccount/WalletCard.css b/src/components/ConnectedAccount/WalletCard.css
new file mode 100644
index 0000000..4971622
--- /dev/null
+++ b/src/components/ConnectedAccount/WalletCard.css
@@ -0,0 +1,190 @@
+.wallet-card {
+ background-image: -webkit-image-set(
+ url(/img/wallet.webp),
+ url(/img/wallet.png)
+ );
+ background-image: image-set(
+ url(/img/wallet.webp) type("image/webp"),
+ url(/img/wallet.png) type("image/png")
+ );
+
+ background-repeat: no-repeat;
+ background-size: cover;
+ border: 1px solid #96969633;
+ border-radius: 16px;
+ padding: 16px;
+ box-sizing: border-box;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+
+ &::before {
+ content: " ";
+ width: 42%;
+ height: 50%;
+ position: absolute;
+ bottom: -1px;
+ right: 0;
+ background: transparent;
+ backdrop-filter: blur(3px);
+ }
+
+ header {
+ button {
+ background-color: #161616;
+ border: 1px solid #96969633;
+ height: 24px;
+ width: 24px;
+ cursor: pointer;
+ transition: box-shadow 0.35s;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ box-shadow: 0 0 0 3px var(--codex-border-color);
+ }
+
+ &:first-child {
+ border-top-left-radius: 6px;
+ border-bottom-left-radius: 6px;
+ }
+
+ &:nth-child(2) {
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
+ }
+
+ svg {
+ position: relative;
+ left: -2px;
+ }
+ }
+ }
+
+ h6 {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 14.52px;
+ letter-spacing: 0.01em;
+ text-align: left;
+ text-transform: uppercase;
+ }
+
+ span {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ color: #ffffffb2;
+ }
+
+ var {
+ font-family: Inter;
+ font-weight: 500;
+ line-height: 40px;
+ letter-spacing: -0.005em;
+ color: var(--text-strong-950, #ffffff);
+ display: block;
+ font-style: normal;
+ }
+
+ main var {
+ font-size: 32px;
+ }
+
+ svg + svg {
+ position: absolute;
+ left: 0;
+ top: 10px;
+ }
+
+ footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+
+ &::after {
+ content: " ";
+ background-image: url(/icons/select-arrow.svg);
+ background-repeat: no-repeat;
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ right: 0;
+ top: 22px;
+ }
+
+ var {
+ font-size: 20px;
+ line-height: 25px;
+ }
+
+ select {
+ background-color: #161616;
+ border-radius: 8px;
+ border: 1px solid #96969633;
+ padding: 6px 6px 6px 44px;
+ outline: none;
+ -moz-appearance: none; /* Firefox */
+ -webkit-appearance: none; /* Safari and Chrome */
+ appearance: none;
+ position: relative;
+ transition: box-shadow 0.35s;
+
+ &:hover {
+ box-shadow: 0 0 0 3px var(--codex-border-color);
+ }
+
+ &:has(option[value="US"]:checked) {
+ background-image: url(/icons/us-flag.svg);
+ background-position: 10px;
+ background-repeat: no-repeat;
+ background-size: 16px;
+ }
+
+ option {
+ border-radius: 32px;
+ }
+ }
+
+ div {
+ position: relative;
+
+ .row {
+ gap: 8px;
+ }
+ }
+
+ small {
+ color: #3ee089;
+ height: 20px;
+ width: 42px;
+ border-radius: 16px;
+ background-color: #1fc16b29;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ }
+
+ section:first-child {
+ margin-top: 12px;
+ }
+
+ section:nth-child(2) {
+ margin-top: 16px;
+ margin-bottom: 10px;
+ position: relative;
+
+ .wallet-lines {
+ position: absolute;
+ left: 0;
+ top: 10px;
+ }
+ }
+}
diff --git a/src/components/ConnectedAccount/WalletCard.tsx b/src/components/ConnectedAccount/WalletCard.tsx
new file mode 100644
index 0000000..5f6ec8c
--- /dev/null
+++ b/src/components/ConnectedAccount/WalletCard.tsx
@@ -0,0 +1,48 @@
+import "./WalletCard.css";
+import ArrowRightIcon from "../../assets/icons/arrow-right.svg?react";
+import ArrowLeftIcon from "../../assets/icons/arrow-left.svg?react";
+import WalletChart from "../../assets/icons/chart.svg?react";
+import WalletLines from "../../assets/icons/lines.svg?react";
+
+export function WalletCard() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.css b/src/components/CustomStateCellRender/CustomStateCellRender.css
deleted file mode 100644
index 596fb39..0000000
--- a/src/components/CustomStateCellRender/CustomStateCellRender.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.cell-state--custom {
- display: inline-flex;
-}
-
-.cell-stateIcon {
- position: relative;
- top: 2px;
- margin-right: 0.5rem;
-}
-
-.cell-state {
- border-radius: var(--codex-border-radius);
- padding: 0.5rem;
-}
-
-.cell-state--error {
- background-color: rgba(var(--codex-color-error), 0.2);
- color: rgb(var(--codex-color-error));
-}
-
-.cell-state--success {
- background-color: rgba(var(--codex-color-success), 0.2);
- color: rgb(var(--codex-color-success));
-}
-
-.cell-state--warning {
- background-color: rgba(var(--codex-color-warning), 0.2);
- color: rgb(var(--codex-color-warning));
-}
-
-.cell-state--loading {
- background-color: rgba(var(--codex-color-grey), 0.2);
- color: rgb(var(--codex-color-grey));
-}
diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.tsx b/src/components/CustomStateCellRender/CustomStateCellRender.tsx
index 0add909..b7a095e 100644
--- a/src/components/CustomStateCellRender/CustomStateCellRender.tsx
+++ b/src/components/CustomStateCellRender/CustomStateCellRender.tsx
@@ -1,20 +1,21 @@
-import { CheckCircle, CircleDashed, ShieldAlert } from "lucide-react";
-import "./CustomStateCellRender.css";
-import { Cell, Tooltip } from "@codex-storage/marketplace-ui-components";
+import { Cell } from "@codex-storage/marketplace-ui-components";
+import PurchaseStateIcon from "../../assets/icons/purchases-state-pending.svg?react";
+import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
+import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
type Props = {
state: string;
message: string | undefined;
};
-export const CustomStateCellRender = ({ state, message }: Props) => {
+export const CustomStateCellRender = ({ state }: Props) => {
const icons = {
- pending: CircleDashed,
- submitted: CircleDashed,
- started: CircleDashed,
- finished: CheckCircle,
- cancelled: ShieldAlert,
- errored: ShieldAlert,
+ pending: PurchaseStateIcon,
+ submitted: PurchaseStateIcon,
+ started: PurchaseStateIcon,
+ finished: SuccessCircleIcon,
+ cancelled: ErrorCircleIcon,
+ errored: ErrorCircleIcon,
};
const states = {
@@ -26,24 +27,19 @@ export const CustomStateCellRender = ({ state, message }: Props) => {
finished: "success",
};
- const Icon = icons[state as keyof typeof icons] || CircleDashed;
+ const Icon = icons[state as keyof typeof icons] || PurchaseStateIcon;
return (
-
- {message ? (
+
+ {/* {message ? (
-
+
) : (
-
- )}
-
- {state}
+
+ )} */}
+
|
);
diff --git a/src/components/Debug/Debug.tsx b/src/components/Debug/Debug.tsx
index 339724b..9036a8b 100644
--- a/src/components/Debug/Debug.tsx
+++ b/src/components/Debug/Debug.tsx
@@ -1,32 +1,10 @@
-import { useQuery } from "@tanstack/react-query";
-import { CodexSdk } from "../../sdk/codex";
-import { Promises } from "../../utils/promises";
import { Spinner } from "@codex-storage/marketplace-ui-components";
+import { useDebug } from "../../hooks/useDebug";
+
+const throwOnError = true;
export function Debug() {
- const { data, isPending } = useQuery({
- queryFn: () =>
- CodexSdk.debug()
- .info()
- .then((s) => Promises.rejectOnError(s)),
-
- queryKey: ["debug"],
-
- // No need to retry because if the connection to the node
- // is back again, all the queries will be invalidated.
- retry: false,
-
- // The client node should be local, so display the cache value while
- // making a background request looks good.
- staleTime: 0,
-
- // Refreshing when focus returns can be useful if a user comes back
- // to the UI after performing an operation in the terminal.
- refetchOnWindowFocus: true,
-
- // Throw the error to the error boundary
- throwOnError: true,
- });
+ const { data, isPending } = useDebug(throwOnError);
if (isPending) {
return (
diff --git a/src/components/Download/Download.css b/src/components/Download/Download.css
index 7209c51..f3fed7b 100644
--- a/src/components/Download/Download.css
+++ b/src/components/Download/Download.css
@@ -1,13 +1,13 @@
.download {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
+ .input {
+ flex: 1;
-.download-inputContainer {
- flex: 1;
-}
+ input {
+ width: 100%;
+ }
+ }
-.download-input {
- width: 100%;
+ .button {
+ width: 105px;
+ }
}
diff --git a/src/components/Download/Download.tsx b/src/components/Download/Download.tsx
index e6b19af..26f1675 100644
--- a/src/components/Download/Download.tsx
+++ b/src/components/Download/Download.tsx
@@ -14,15 +14,15 @@ export function Download() {
setCid(e.currentTarget.value);
return (
-
+
+
+
+
);
}
diff --git a/src/components/ErrorIcon/ErrorIcon.tsx b/src/components/ErrorIcon/ErrorIcon.tsx
index 8982be3..1f41214 100644
--- a/src/components/ErrorIcon/ErrorIcon.tsx
+++ b/src/components/ErrorIcon/ErrorIcon.tsx
@@ -1,10 +1,9 @@
import { CircleX } from "lucide-react";
-import { SimpleText } from "@codex-storage/marketplace-ui-components";
export function ErrorIcon() {
return (
-
+
-
+
);
}
diff --git a/src/components/FileCellRender/FileCell.tsx b/src/components/FileCellRender/FileCell.tsx
index 814507a..9095046 100644
--- a/src/components/FileCellRender/FileCell.tsx
+++ b/src/components/FileCellRender/FileCell.tsx
@@ -5,43 +5,63 @@ import {
WebFileIcon,
} from "@codex-storage/marketplace-ui-components";
import "./FileCell.css";
-import { FileMetadata, FilesStorage } from "../../utils/file-storage";
-import { PurchaseStorage } from "../../utils/purchases-storage";
+import { WebStorage } from "../../utils/web-storage";
+import { CodexDataContent } from "@codex-storage/sdk-js";
+
+type FileMetadata = {
+ mimetype: string | null;
+ uploadedAt: number;
+ filename: string | null;
+};
type Props = {
requestId: string;
purchaseCid: string;
index: number;
+ data: CodexDataContent[];
+ onMetadata?: (requestId: string, metadata: FileMetadata) => void;
};
-export function FileCell({ requestId, purchaseCid }: Props) {
+export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) {
const [cid, setCid] = useState(purchaseCid);
const [metadata, setMetadata] = useState
({
- name: "N/A.jpg",
- mimetype: "N/A",
- uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(),
+ filename: "-",
+ mimetype: "-",
+ uploadedAt: 0,
});
useEffect(() => {
- PurchaseStorage.get(requestId).then((cid) => {
+ WebStorage.purchases.get(requestId).then((cid) => {
if (cid) {
setCid(cid);
- FilesStorage.get(cid).then((data) => {
- if (data) {
- setMetadata(data);
- }
- });
+ const content = data.find((m) => m.cid === cid);
+ if (content) {
+ const {
+ filename = "-",
+ mimetype = "application/octet-stream",
+ uploadedAt = 0,
+ } = content.manifest;
+ setMetadata({
+ filename,
+ mimetype,
+ uploadedAt,
+ });
+ onMetadata?.(requestId, {
+ filename,
+ mimetype,
+ uploadedAt,
+ });
+ }
}
});
- }, [requestId]);
+ }, [requestId, data, onMetadata]);
- let name = metadata.name.slice(0, 10);
+ let filename = metadata.filename || "-";
- if (metadata.name.length > 10) {
- // const [filename, ext] = metadata.name.split(".");
- // name = filename.slice(0, 10) + "..." + ext;
- name += "...";
+ if (filename.length > 10) {
+ const [name, ext] = filename.split(".");
+ filename = name.slice(0, 10) + "..." + ext;
}
// const cidTruncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5);
@@ -50,10 +70,10 @@ export function FileCell({ requestId, purchaseCid }: Props) {
return (
-
+
- {name}
+ {filename}
diff --git a/src/components/Files/CidCopyButton.tsx b/src/components/Files/CidCopyButton.tsx
index 12d61ab..213d299 100644
--- a/src/components/Files/CidCopyButton.tsx
+++ b/src/components/Files/CidCopyButton.tsx
@@ -1,7 +1,7 @@
import { useRef, useState } from "react";
-import { COPY_DURATION, ICON_SIZE } from "../../utils/constants";
-import { Copy } from "lucide-react";
+import { COPY_DURATION } from "../../utils/constants";
import { Button } from "@codex-storage/marketplace-ui-components";
+import CopyIcon from "../../assets/icons/copy.svg?react";
type CopyButtonProps = {
cid: string;
@@ -27,13 +27,11 @@ export function CidCopyButton({ cid }: CopyButtonProps) {
const label = copied ? "Copied !" : "Copy CID";
- const Icon = () => ;
-
return (
+ Icon={() => }>
);
}
diff --git a/src/components/Files/FileActions.css b/src/components/Files/FileActions.css
new file mode 100644
index 0000000..242ffac
--- /dev/null
+++ b/src/components/Files/FileActions.css
@@ -0,0 +1,19 @@
+.file-actions {
+ > div {
+ display: inline-flex;
+ align-items: center;
+ border: 1px solid var(--codex-border-color);
+ border-radius: var(--codex-border-radius);
+ padding: 0.5rem;
+ background-color: #14141499;
+ gap: 8px;
+ padding: 10px;
+
+ .button-icon {
+ width: 40px;
+ height: 40px;
+ background-color: #2f2f2f;
+ border: 1px solid #96969633;
+ }
+ }
+}
diff --git a/src/components/Files/FileActions.tsx b/src/components/Files/FileActions.tsx
new file mode 100644
index 0000000..18151dd
--- /dev/null
+++ b/src/components/Files/FileActions.tsx
@@ -0,0 +1,47 @@
+import { ButtonIcon, Cell } from "@codex-storage/marketplace-ui-components";
+import { FolderButton } from "./FolderButton";
+import { CodexDataContent } from "@codex-storage/sdk-js";
+import { CodexSdk } from "../../sdk/codex";
+import "./FileActions.css";
+import DownloadIcon from "../../assets/icons/download-file.svg?react";
+import InfoFileIcon from "../../assets/icons/info-file.svg?react";
+
+type Props = {
+ content: CodexDataContent;
+ folders: [string, string[]][];
+ onFolderToggle: (cid: string, folder: string) => void;
+ onDetails: (cid: string) => void;
+};
+
+export function FileActions({
+ content,
+ folders,
+ onFolderToggle,
+ onDetails,
+}: Props) {
+ const url = CodexSdk.url() + "/api/codex/v1/data/";
+
+ return (
+ |
+
+ window.open(url + content.cid, "_blank")}
+ Icon={DownloadIcon}>
+
+ [
+ folder,
+ files.includes(content.cid),
+ ])}
+ onFolderToggle={(folder) => onFolderToggle(content.cid, folder)}
+ />
+
+ onDetails(content.cid)}
+ Icon={InfoFileIcon}>
+
+ |
+ );
+}
diff --git a/src/components/Files/FileCell.css b/src/components/Files/FileCell.css
new file mode 100644
index 0000000..b0a4d65
--- /dev/null
+++ b/src/components/Files/FileCell.css
@@ -0,0 +1,22 @@
+.file-cell {
+ small {
+ word-break: break-all;
+ }
+
+ > div {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+
+ > div {
+ flex: 1;
+ }
+
+ .button-icon {
+ width: 40px;
+ height: 40px;
+ background-color: #14141499;
+ border: 1px solid #96969633;
+ }
+ }
+}
diff --git a/src/components/Files/FileCell.tsx b/src/components/Files/FileCell.tsx
new file mode 100644
index 0000000..987ec94
--- /dev/null
+++ b/src/components/Files/FileCell.tsx
@@ -0,0 +1,51 @@
+import {
+ ButtonIcon,
+ Cell,
+ Toast,
+ WebFileIcon,
+} from "@codex-storage/marketplace-ui-components";
+import { CodexDataContent } from "@codex-storage/sdk-js";
+import { useState } from "react";
+import "./FileCell.css";
+import CopyIcon from "../../assets/icons/copy.svg?react";
+
+type Props = {
+ content: CodexDataContent;
+};
+
+export function FileCell({ content }: Props) {
+ const [toast, setToast] = useState({ time: 0, message: "" });
+
+ const onCopy = (cid: string) => {
+ navigator.clipboard.writeText(cid);
+ setToast({ message: "CID copied to the clipboard.", time: Date.now() });
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+ {content.manifest.filename}
+
+
+ {content.cid}
+
+
+ onCopy(content.cid)}
+ animation="buzz"
+ Icon={(props) => (
+
+ )}>
+
+
+
+ |
+ >
+ );
+}
diff --git a/src/components/Files/FileDetails.css b/src/components/Files/FileDetails.css
index a38bb60..790551b 100644
--- a/src/components/Files/FileDetails.css
+++ b/src/components/Files/FileDetails.css
@@ -1,33 +1,155 @@
-.fileDetails-header {
- padding: 0.75rem 1.5rem;
- border-bottom: 1px solid var(--codex-border-color);
- display: flex;
- align-items: center;
-}
+.file-details {
+ background-color: #232323;
+ border-left: 1px solid #96969633;
+ border-top-left-radius: 16px;
+ border-bottom-left-radius: 16px;
+ padding: 16px;
+ height: 100%;
-.fileDetails-headerTitle {
- flex-grow: 1;
-}
+ header {
+ display: flex;
+ align-items: center;
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
-.fileDetails-body {
- padding: 0;
-}
+ span {
+ flex-grow: 1;
+ }
-.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);
-}
+ .button-icon {
+ background-color: #2f2f2f;
+ border: 1px solid #96969633;
-.fileDetails-gridColumn {
- grid-column: span 2 / span 2;
- color: var(--codex-text-contrast);
-}
+ svg {
+ position: relative;
+ left: -3px;
+ top: -1px;
+ }
+ }
+ }
-.fileDetails-actions {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
- padding: 0.75rem 1.5rem;
+ .preview {
+ background-color: #14141499;
+ border: 1px solid #69696933;
+ height: 150px;
+ margin: auto;
+ border-radius: 10px;
+ margin-bottom: 16px;
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ margin: auto;
+ display: flex;
+ }
+
+ figure {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ height: 100%;
+ margin: 0;
+
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 400;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
+ color: #ffffff33;
+
+ p {
+ margin-top: 8px;
+ }
+ }
+ }
+
+ ul {
+ li {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ display: grid;
+ padding: 8px 0;
+ align-items: center;
+
+ p:first-child {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ }
+
+ p:nth-child(2) {
+ grid-column: span 2 / span 2;
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ color: #ffffffcc;
+ }
+
+ &:last-child p:nth-child(2) {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+
+ color: #6beca1;
+ }
+ }
+ }
+
+ .buttons {
+ padding: 16px 0;
+ border-bottom: 1px solid #96969633;
+ display: flex;
+ gap: 16px;
+
+ button {
+ flex: 1;
+ }
+ }
+
+ .purchases {
+ padding-bottom: 16px;
+
+ header {
+ margin-top: 16px;
+ display: block;
+ border-bottom: 1px solid #96969633;
+ padding-bottom: 16px;
+
+ > span {
+ display: flex;
+ gap: 16px;
+ }
+ }
+
+ thead tr th {
+ border-top: 1px solid #96969633;
+ border-bottom: 1px solid #96969633;
+
+ &:first-child {
+ border-left: 1px solid #96969633;
+ }
+
+ &:last-child {
+ border-right: 1px solid #96969633;
+ }
+ }
+
+ .truncateCell {
+ width: 100px;
+ }
+ }
}
diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx
index 98e1915..4ad1817 100644
--- a/src/components/Files/FileDetails.tsx
+++ b/src/components/Files/FileDetails.tsx
@@ -2,89 +2,157 @@ import {
ButtonIcon,
Button,
Sheets,
+ WebFileIcon,
} from "@codex-storage/marketplace-ui-components";
-import { CodexDataContent } from "@codex-storage/sdk-js";
+import { CodexDataContent, CodexPurchase } from "@codex-storage/sdk-js";
import { PrettyBytes } from "../../utils/bytes";
-import { ICON_SIZE } from "../../utils/constants";
import { Dates } from "../../utils/dates";
import { CidCopyButton } from "./CidCopyButton";
import "./FileDetails.css";
-import { DownloadIcon, X } from "lucide-react";
+import { CodexSdk } from "../../sdk/codex";
+import { FilesUtils } from "./files.utils";
+import DownloadIcon from "../../assets/icons/download-file.svg?react";
+import FileDetailsIcon from "../../assets/icons/file-details.svg?react";
+import CloseIcon from "../../assets/icons/close.svg?react";
+import { useQuery } from "@tanstack/react-query";
+import { Promises } from "../../utils/promises";
+import { PurchaseHistory } from "./PurchaseHistory";
+import { WebStorage } from "../../utils/web-storage";
type Props = {
- details: CodexDataContent | undefined;
+ details: CodexDataContent | null;
onClose: () => void;
- expanded: boolean;
};
-export function FileDetails({ onClose, details, expanded }: Props) {
- const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
+export function FileDetails({ onClose, details }: Props) {
+ const { data: purchases = [] } = useQuery({
+ queryFn: () =>
+ CodexSdk.marketplace()
+ .purchases()
+ .then(async (res) => {
+ if (res.error) {
+ return res;
+ }
- const Icon = () => ;
+ const all: CodexPurchase[] = [];
+ for (const p of res.data) {
+ const cid = await WebStorage.purchases.get(p.requestId);
+ if (cid == details?.cid) {
+ all.push(p);
+ }
+ }
+
+ return {
+ error: false as any,
+ data: all,
+ };
+ })
+ .then((s) => Promises.rejectOnError(s)),
+ queryKey: ["purchases"],
+
+ enabled: !!details,
+
+ // No need to retry because if the connection to the node
+ // is back again, all the queries will be invalidated.
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ // Refreshing when focus returns can be useful if a user comes back
+ // to the UI after performing an operation in the terminal.
+ refetchOnWindowFocus: true,
+
+ initialData: [],
+
+ // Throw the error to the error boundary
+ throwOnError: true,
+ });
+
+ const url = CodexSdk.url() + "/api/codex/v1/data/";
const onDownload = () => window.open(url + details?.cid, "_target");
return (
-
+
<>
{details && (
- <>
-
- File details
-
+
+
+
+
+ {FilesUtils.isImage(details.manifest.mimetype) ? (
+ 
+ ) : (
+
+
+ File Preview not available.
+
+ )}
-
-
+
-
- Date:
-
- {Dates.format(details.manifest.uploadedAt).toString()}
-
-
+
+
-
- Mimetype:
-
- {details.manifest.mimetype}
-
-
-
-
- Size:
-
- {PrettyBytes(details.manifest.datasetSize)}
-
-
-
-
- Protected:
-
- {details.manifest.protected ? "Yes" : "No"}
-
-
-
-
-
-
-
-
+
- >
+
+
+
)}
>
diff --git a/src/components/Files/FileFilters.css b/src/components/Files/FileFilters.css
new file mode 100644
index 0000000..7606270
--- /dev/null
+++ b/src/components/Files/FileFilters.css
@@ -0,0 +1,35 @@
+.filter {
+ padding: 4px 8px;
+ gap: 8px;
+ border-radius: 6px;
+ border: 1px solid #96969633;
+ background-color: #2f2f2f;
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 16px;
+ text-align: left;
+ color: #969696;
+ text-transform: capitalize;
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ transition: box-shadow 0.35s;
+
+ &:hover {
+ box-shadow: 0 0 0 3px var(--codex-border-color);
+ }
+
+ svg {
+ color: #969696;
+ }
+
+ &.filter--active {
+ border-color: var(--codex-color-primary);
+ color: var(--codex-color-primary);
+
+ svg {
+ color: var(--codex-color-primary);
+ }
+ }
+}
diff --git a/src/components/Files/FileFilters.tsx b/src/components/Files/FileFilters.tsx
new file mode 100644
index 0000000..963786a
--- /dev/null
+++ b/src/components/Files/FileFilters.tsx
@@ -0,0 +1,85 @@
+import { CodexDataContent } from "@codex-storage/sdk-js";
+import { FilesUtils } from "./files.utils";
+import { classnames } from "../../utils/classnames";
+import "./FileFilters.css";
+import ImageIcon from "../../assets/icons/image.svg?react";
+import VideoIcon from "../../assets/icons/video.svg?react";
+import ArchiveIcon from "../../assets/icons/archive.svg?react";
+import DocumentIcon from "../../assets/icons/document.svg?react";
+
+type Props = {
+ files: CodexDataContent[];
+ onFilterToggle: (filter: string) => void;
+ selected: string[];
+};
+
+function getIcon(type: string) {
+ switch (type) {
+ case "image": {
+ return ;
+ }
+
+ case "archive": {
+ return ;
+ }
+
+ case "video": {
+ return ;
+ }
+
+ default: {
+ return ;
+ }
+ }
+}
+
+function getType(mimetype: string) {
+ if (FilesUtils.isArchive(mimetype)) {
+ return "archive";
+ }
+
+ if (FilesUtils.isImage(mimetype)) {
+ return "image";
+ }
+
+ if (FilesUtils.isVideo(mimetype)) {
+ return "video";
+ }
+
+ return "document";
+}
+
+export function FilterFilters({ selected, files, onFilterToggle }: Props) {
+ const filters = Array.from(
+ new Set(
+ files
+ .filter((f) => f.manifest.mimetype !== "")
+ .map((file) => getType(file.manifest.mimetype || ""))
+ )
+ );
+
+ const html = filters.map((type) => {
+ const count = files.reduce(
+ (acc, file) =>
+ getType(file.manifest.mimetype || "") === type ? acc + 1 : acc,
+ 0
+ );
+
+ return (
+ onFilterToggle(type)}>
+ {getIcon(type)}
+
+ {type + "s"} ({count})
+
+
+ );
+ });
+
+ return {html} ;
+}
diff --git a/src/components/Files/Files.css b/src/components/Files/Files.css
index e5427f7..a8bd06a 100644
--- a/src/components/Files/Files.css
+++ b/src/components/Files/Files.css
@@ -1,72 +1,27 @@
.files {
- border-radius: var(--codex-border-radius);
- border: 1px solid var(--codex-border-color);
- background-color: var(--codex-background-secondary);
- padding: 1rem 1.5rem;
- margin-bottom: 1rem;
-}
+ section {
+ display: inline-flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ gap: 16px;
-.files-title {
- font-weight: bold;
- font-size: 1.125rem;
- line-height: 1.75rem;
-}
+ .tabs {
+ flex-basis: 75%;
+ }
+ }
-.files-file:not(:last-child) {
- padding-bottom: 0.75rem;
-}
+ .filters {
+ margin-top: 16px;
+ gap: 16px;
+ display: flex;
+ }
-.files-fileContent {
- display: flex;
- gap: 0.75rem;
-}
+ .button {
+ width: 105px;
+ }
-.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;
+ table thead tr th {
+ background-color: #14141499;
+ }
}
diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx
index 65d4d34..fe25afd 100644
--- a/src/components/Files/Files.tsx
+++ b/src/components/Files/Files.tsx
@@ -1,154 +1,226 @@
-import { Download, FilesIcon, ReceiptText, Star } from "lucide-react";
-import { useEffect, useRef, useState } from "react";
+import { ChangeEvent, useEffect, useState } from "react";
import { PrettyBytes } from "../../utils/bytes";
import { Dates } from "../../utils/dates";
import "./Files.css";
-import { ICON_SIZE, SIDE_DURATION } from "../../utils/constants";
import {
- ButtonIcon,
- EmptyPlaceholder,
- WebFileIcon,
Tabs,
+ Input,
+ Button,
+ TabProps,
+ Table,
+ Row,
+ Cell,
+ TabSortState,
} from "@codex-storage/marketplace-ui-components";
import { FileDetails } from "./FileDetails.tsx";
-import { FavoriteStorage } from "../../utils/favorite-storage.tsx";
import { useData } from "../../hooks/useData.tsx";
+import { WebStorage } from "../../utils/web-storage.ts";
+import { classnames } from "../../utils/classnames.ts";
+import { CodexDataContent } from "@codex-storage/sdk-js";
+import { FilesUtils } from "./files.utils.ts";
+import { FilterFilters } from "./FileFilters.tsx";
+import { FileCell } from "./FileCell.tsx";
+import { FileActions } from "./FileActions.tsx";
+import PlusIcon from "../../assets/icons/plus.svg?react";
+import AllFilesIcon from "../../assets/icons/all.svg?react";
+import FavoriteIcon from "../../assets/icons/favorite.svg?react";
-type StarIconProps = {
- isFavorite: boolean;
+type SortFn = (a: CodexDataContent, b: CodexDataContent) => number;
+
+type Props = {
+ limit?: number;
};
-function StarIcon({ isFavorite }: StarIconProps) {
- if (isFavorite) {
- return (
-
- );
- }
-
- return ;
-}
-
-export function Files() {
+export function Files({ limit }: Props) {
const files = useData();
- const cid = useRef ("");
- const [expanded, setExpanded] = useState(false);
- const [favorites, setFavorites] = useState([]);
const [index, setIndex] = useState(0);
+ const [folder, setFolder] = useState("");
+ const [folders, setFolders] = useState<[string, string[]][]>([]);
+ const [error, setError] = useState("");
+ const [details, setDetails] = useState(null);
+ const [sortFn, setSortFn] = useState(() =>
+ FilesUtils.sortByDate("desc")
+ );
+ const [selectedFilters, setSelectedFilters] = useState([]);
useEffect(() => {
- FavoriteStorage.list().then((cids) => setFavorites(cids));
+ WebStorage.folders.list().then((items) => setFolders(items));
}, []);
- const onClose = () => {
- setExpanded(false);
+ const onClose = () => setDetails(null);
- setTimeout(() => {
- cid.current = "";
- }, SIDE_DURATION);
- };
+ const onTabChange = async (i: number) => setIndex(i);
- const onTabChange = (i: number) => setIndex(i);
+ const onDetails = (cid: string) => {
+ const d = files.find((file) => file.cid === cid);
- 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]);
+ if (d) {
+ setDetails(d);
}
};
- const items = [];
+ const onFolderChange = (e: ChangeEvent) => {
+ const val = e.currentTarget.value;
+ setFolder(val);
+ setError("");
- if (index === 1) {
- items.push(...files.filter((f) => favorites.includes(f.cid)));
- } else {
- items.push(...files);
+ if (!val) {
+ return;
+ }
+
+ if (e.currentTarget.checkValidity()) {
+ if (folders.length >= 5) {
+ setError("5 folders limit reached");
+ return;
+ }
+
+ if (FilesUtils.exists(folders, val)) {
+ setError("This folder already exists");
+ return;
+ }
+ } else {
+ setError("9 alpha characters maximum");
+ }
+ };
+
+ const onFolderCreate = () => {
+ WebStorage.folders.create(folder);
+
+ setFolder("");
+ setFolders([...folders, [folder, []]]);
+ };
+
+ // const onFolderDelete = (val: string) => {
+ // WebStorage.folders.delete(val);
+
+ // const currentIndex = folders.findIndex(([name]) => name === val);
+
+ // if (currentIndex + 1 == index) {
+ // setIndex(index - 1);
+ // }
+
+ // setFolders(folders.filter(([name]) => name !== val));
+ // };
+
+ const onFolderToggle = (cid: string, folder: string) => {
+ const current = folders.find(([name]) => name === folder);
+
+ if (!current) {
+ return;
+ }
+
+ const [, files] = current;
+
+ if (files.includes(cid)) {
+ WebStorage.folders.deleteFile(folder, cid);
+ setFolders(FilesUtils.removeCidFromFolder(folders, folder, cid));
+ } else {
+ WebStorage.folders.addFile(folder, cid);
+ setFolders(FilesUtils.addCidToFolder(folders, folder, cid));
+ }
+ };
+
+ const tabs: TabProps[] = folders.map(([folder], index) => ({
+ label: folder,
+ Icon: () => (index === 0 ? : null),
+ // IconAfter:
+ // folder === "Favorites"
+ // ? undefined
+ // : () => (
+ // {
+ // e.preventDefault();
+ // e.stopPropagation();
+
+ // onFolderDelete(folder);
+ // }}>
+ // ),
+ }));
+
+ const onSortByFilename = (state: TabSortState) =>
+ setSortFn(() => FilesUtils.sortByName(state));
+
+ const onSortBySize = (state: TabSortState) =>
+ setSortFn(() => FilesUtils.sortBySize(state));
+
+ const onSortByDate = (state: TabSortState) =>
+ setSortFn(() => FilesUtils.sortByDate(state));
+
+ const onToggleFilter = (filter: string) =>
+ setSelectedFilters(FilesUtils.toggleFilters(selectedFilters, filter));
+
+ const headers = [
+ ["file", onSortByFilename],
+ ["size", onSortBySize],
+ ["date", onSortByDate],
+ ["actions"],
+ ] satisfies [string, ((state: TabSortState) => void)?][];
+
+ const items = FilesUtils.listInFolder(files, folders, index);
+ const filtered = FilesUtils.applyFilters(items, selectedFilters);
+ const sorted = sortFn ? [...filtered].sort(sortFn) : filtered;
+ let rows =
+ sorted.map((c) => (
+ ,
+ | {PrettyBytes(c.manifest.datasetSize)} | ,
+ {Dates.format(c.manifest.uploadedAt).toString()} | ,
+ ,
+ ]}>
+ )) || [];
+
+ if (limit) {
+ rows = rows.slice(0, limit);
}
- const details = items.find((c) => c.cid === cid.current);
-
- const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
+ tabs.unshift({
+ label: "All",
+ Icon: () => ,
+ });
return (
-
-
- Files
- ,
- },
- {
- label: "Favorites",
- Icon: () => ,
- },
- ]}>
-
+
+
+
+
+
-
- {items.length ? (
- items.map((c) => (
-
-
-
-
-
-
-
- {c.manifest.filename}
-
-
- {PrettyBytes(c.manifest.datasetSize)} -{" "}
- {Dates.format(c.manifest.uploadedAt).toString()} - ...
- {c.cid.slice(-5)}
-
-
-
-
- window.open(url + c.cid, "_blank")}
- Icon={() => }>
+
+
+
- onToggleFavorite(c.cid)}
- Icon={() => (
-
- )}>
+
- onDetails(c.cid)}
- Icon={() => (
-
- )}>
-
-
-
-
- ))
- ) : (
-
-
-
- )}
-
+
-
-
+
+
);
}
diff --git a/src/components/Files/FolderButton.css b/src/components/Files/FolderButton.css
new file mode 100644
index 0000000..e26bf49
--- /dev/null
+++ b/src/components/Files/FolderButton.css
@@ -0,0 +1,41 @@
+.folder-button {
+ > div {
+ position: absolute;
+ transform: translate(-110px, -75px);
+ opacity: 0;
+ transition:
+ transform 0.25s,
+ opacity 0.15s;
+ background-color: var(--codex-background);
+ padding: 0.5rem;
+ border-radius: var(--codex-border-radius);
+ width: 150px;
+ right: -40px;
+ border: 1px solid var(--codex-border-color);
+ z-index: -1;
+
+ &[aria-expanded] {
+ z-index: 12;
+ transform: translate(-110px, -200px);
+ opacity: 1;
+ }
+
+ > div {
+ padding: 0.75rem;
+ transition: background-color 0.35s;
+ cursor: pointer;
+ border-radius: var(--codex-border-radius);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ &:hover {
+ background-color: var(--codex-background-light);
+ }
+ }
+
+ svg {
+ color: var(--codex-color-primary);
+ }
+ }
+}
diff --git a/src/components/Files/FolderButton.tsx b/src/components/Files/FolderButton.tsx
new file mode 100644
index 0000000..dbbc2aa
--- /dev/null
+++ b/src/components/Files/FolderButton.tsx
@@ -0,0 +1,53 @@
+import { Backdrop, ButtonIcon } from "@codex-storage/marketplace-ui-components";
+import { CheckCircle } from "lucide-react";
+import "./FolderButton.css";
+import { useState } from "react";
+import { attributes } from "../../utils/attributes";
+import { classnames } from "../../utils/classnames";
+import FolderIcon from "../../assets/icons/folder.svg?react";
+
+type Props = {
+ folders: [string, boolean][];
+ onFolderToggle: (folder: string) => void;
+};
+
+export function FolderButton({ folders, onFolderToggle }: Props) {
+ const [open, setOpen] = useState(false);
+
+ const onClose = () => setOpen(false);
+
+ const onOpen = () => setOpen(true);
+
+ const attr = attributes({ "aria-expanded": open });
+
+ const doesFolderContainFile = folders.reduce(
+ (prev, [, isActive]) => isActive || prev,
+ false
+ );
+
+ return (
+ <>
+
+
+
+
+ {folders.map(([folder, isActive]) => (
+ onFolderToggle(folder)}>
+ {folder}
+ {isActive && }
+
+ ))}
+
+
+
+ >
+ );
+}
diff --git a/src/components/Files/PurchaseHistory.tsx b/src/components/Files/PurchaseHistory.tsx
new file mode 100644
index 0000000..1c805c0
--- /dev/null
+++ b/src/components/Files/PurchaseHistory.tsx
@@ -0,0 +1,67 @@
+import {
+ Row,
+ Cell,
+ Table,
+ TabSortState,
+} from "@codex-storage/marketplace-ui-components";
+import { Times } from "../../utils/times";
+import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender";
+import { TruncateCell } from "../TruncateCell/TruncateCell";
+import { CodexPurchase } from "@codex-storage/sdk-js";
+import PurchaseHistoryIcon from "../../assets/icons/purchase-history-outline.svg?react";
+import { useState } from "react";
+import { PurchaseUtils } from "../Purchase/purchase.utils";
+
+type Props = {
+ purchases: CodexPurchase[];
+};
+
+type SortFn = (a: CodexPurchase, b: CodexPurchase) => number;
+
+export function PurchaseHistory({ purchases }: Props) {
+ const [sortFn, setSortFn] = useState(() =>
+ PurchaseUtils.sortById("desc")
+ );
+
+ const onSortById = (state: TabSortState) =>
+ setSortFn(() => PurchaseUtils.sortById(state));
+
+ const headers = [
+ ["request id", onSortById],
+ ["duration"],
+ ["expiry"],
+ ["status"],
+ ] satisfies [string, ((state: TabSortState) => void)?][];
+
+ const sorted = sortFn ? [...purchases].sort(sortFn) : purchases;
+
+ const rows = sorted.map((p) => {
+ const duration = parseInt(p.request.ask.duration, 10);
+
+ return (
+ ,
+ {Times.pretty(duration)} | ,
+ {p.request.expiry} | ,
+ ,
+ ]}>
+ );
+ });
+
+ if (purchases.length > 0) {
+ return (
+
+ );
+ }
+
+ return "";
+}
diff --git a/src/components/Files/files.utils.test.ts b/src/components/Files/files.utils.test.ts
new file mode 100644
index 0000000..355d92e
--- /dev/null
+++ b/src/components/Files/files.utils.test.ts
@@ -0,0 +1,295 @@
+import { assert, describe, it } from "vitest";
+import { FilesUtils } from "./files.utils";
+
+describe("files", () => {
+ it("sorts by name", async () => {
+ const a = {
+ cid: "", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+ const b = {
+ cid: "", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "b",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+
+ const items = [a, b,]
+
+ const descSorted = items.slice().sort(FilesUtils.sortByName("desc"))
+
+ assert.deepEqual(descSorted, [b, a]);
+
+ const ascSorted = items.slice().sort(FilesUtils.sortByName("asc"))
+
+ assert.deepEqual(ascSorted, [a, b]);
+ });
+
+ it("sorts by size", async () => {
+ const a = {
+ cid: "", manifest: {
+ datasetSize: 1000,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+ const b = {
+ cid: "", manifest: {
+ datasetSize: 2000,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "b",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+
+ const items = [a, b,]
+
+ const descSorted = items.slice().sort(FilesUtils.sortBySize("desc"))
+
+ assert.deepEqual(descSorted, [b, a]);
+
+ const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc"))
+
+ assert.deepEqual(ascSorted, [a, b]);
+ });
+
+ it("sorts by date", async () => {
+ const now = new Date()
+
+ const a = {
+ cid: "", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: now.getTime()
+ }
+ }
+
+ now.setDate(now.getDate() - 1)
+
+ const b = {
+ cid: "", manifest: {
+ datasetSize: 2000,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "b",
+ mimetype: null,
+ uploadedAt: now.getTime()
+ }
+ }
+
+ const items = [a, b,]
+
+ const descSorted = items.slice().sort(FilesUtils.sortBySize("desc"))
+
+ assert.deepEqual(descSorted, [b, a]);
+
+ const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc"))
+
+ assert.deepEqual(ascSorted, [a, b]);
+ });
+
+ it("returns true when a file is an image", async () => {
+ assert.deepEqual(FilesUtils.isImage("image/jpg"), true);
+ assert.deepEqual(FilesUtils.isImage("video/mp4"), false);
+ assert.deepEqual(FilesUtils.isImage(null), false);
+ });
+
+ it("returns true when a file is a video", async () => {
+ assert.deepEqual(FilesUtils.isVideo("video/mp4"), true);
+ assert.deepEqual(FilesUtils.isVideo("image/jpg"), false);
+ assert.deepEqual(FilesUtils.isImage(null), false);
+ });
+
+ it("returns true when a file is an archive", async () => {
+ assert.deepEqual(FilesUtils.isArchive("application/zip"), true);
+ assert.deepEqual(FilesUtils.isArchive("video/mp4"), false);
+ assert.deepEqual(FilesUtils.isArchive(null), false);
+ });
+
+ it("gets the type of a file", async () => {
+ assert.deepEqual(FilesUtils.type("application/zip"), "archive");
+ });
+
+ it("fallbacks to document when the mimetype is not known", async () => {
+ assert.deepEqual(FilesUtils.type("application/octet-stream"), "document");
+ });
+
+ it("removes a cid from a folder", async () => {
+ const folders = [["favorites", ["123", "456"]]] satisfies [string, string[]][]
+ const folder = "favorites"
+ const cid = "456"
+
+ assert.deepEqual(FilesUtils.removeCidFromFolder(folders, folder, cid), [["favorites", ["123"]]]);
+ });
+
+ it("adds a cid from to a folder", async () => {
+ const folders = [["favorites", ["123"]]] satisfies [string, string[]][]
+ const folder = "favorites"
+ const cid = "456"
+
+ assert.deepEqual(FilesUtils.addCidToFolder(folders, folder, cid), [["favorites", ["123", cid]]]);
+ });
+
+ it("returns true when the folder exists", async () => {
+ const folders = [["favorites", []]] satisfies [string, string[]][]
+
+ assert.deepEqual(FilesUtils.exists(folders, "favorites"), true);
+ });
+
+ it("toggles filter", async () => {
+ const filters = FilesUtils.toggleFilters(["images"], "archives")
+
+ assert.deepEqual(filters, ["images", "archives"]);
+ assert.deepEqual(FilesUtils.toggleFilters(filters, "archives"), ["images"]);
+ });
+
+ it("list all files when the first item is selected", async () => {
+ const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][]
+ const files = [
+ {
+ cid: "123", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ },
+ {
+ cid: "456",
+ manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+ ]
+
+
+ assert.deepEqual(FilesUtils.listInFolder(files, folders, 0), files);
+ });
+
+ it("list all files in favorites", async () => {
+ const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][]
+ const files = [
+ {
+ cid: "123", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ },
+ {
+ cid: "456",
+ manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+ ]
+
+
+ assert.deepEqual(FilesUtils.listInFolder(files, folders, 1), [files[0]]);
+ });
+
+ it("returns all files when no filter is selected", async () => {
+ const files = [
+ {
+ cid: "123", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ },
+ {
+ cid: "456",
+ manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: null,
+ uploadedAt: 0
+ }
+ }
+ ]
+
+
+ assert.deepEqual(FilesUtils.applyFilters(files, []), files);
+ });
+
+ it("returns apply filter by mimetype", async () => {
+ const files = [
+ {
+ cid: "123", manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: "image/jpg",
+ uploadedAt: 0
+ }
+ },
+ {
+ cid: "456",
+ manifest: {
+ datasetSize: 0,
+ blockSize: 0,
+ protected: false,
+ treeCid: "",
+ filename: "a",
+ mimetype: "application/zip",
+ uploadedAt: 0
+ }
+ }
+ ]
+
+
+ assert.deepEqual(FilesUtils.applyFilters(files, ["archive"]), [files[1]]);
+ });
+})
\ No newline at end of file
diff --git a/src/components/Files/files.utils.ts b/src/components/Files/files.utils.ts
new file mode 100644
index 0000000..3cffb64
--- /dev/null
+++ b/src/components/Files/files.utils.ts
@@ -0,0 +1,98 @@
+import { TabSortState } from "@codex-storage/marketplace-ui-components";
+import { CodexDataContent } from "@codex-storage/sdk-js";
+
+const archiveMimetypes = [
+ "application/zip",
+ "application/x-rar-compressed",
+ "application/x-tar",
+ "application/gzip",
+ "application/x-7z-compressed",
+ "application/gzip", // for .tar.gz
+ "application/x-bzip2",
+ "application/x-xz",
+];
+
+export const FilesUtils = {
+ isImage(type: string | null) {
+ return !!type && type.startsWith("image");
+ },
+ isVideo(type: string | null) {
+ return !!type && type.startsWith("video");
+ },
+ isArchive(mimetype: string | null) {
+ return !!mimetype && archiveMimetypes.includes(mimetype)
+ },
+ type(mimetype: string | null) {
+ if (FilesUtils.isArchive(mimetype)) {
+ return "archive"
+ }
+
+ if (FilesUtils.isVideo(mimetype)) {
+ return "video"
+ }
+
+ if (FilesUtils.isImage(mimetype)) {
+ return "image"
+ }
+
+ return "document"
+ },
+ sortByName: (state: TabSortState) =>
+ (a: CodexDataContent, b: CodexDataContent) => {
+ const { manifest: { filename: afilename } } = a
+ const { manifest: { filename: bfilename } } = b
+
+ return state === "desc"
+ ? (bfilename || "")
+ .toLocaleLowerCase()
+ .localeCompare((afilename || "").toLocaleLowerCase())
+ : (afilename || "")
+ .toLocaleLowerCase()
+ .localeCompare((bfilename || "").toLocaleLowerCase())
+ },
+ sortBySize: (state: TabSortState) =>
+ (a: CodexDataContent, b: CodexDataContent) => state === "desc"
+ ? b.manifest.datasetSize - a.manifest.datasetSize
+ : a.manifest.datasetSize - b.manifest.datasetSize
+ ,
+ sortByDate: (state: TabSortState) =>
+ (a: CodexDataContent, b: CodexDataContent) => state === "desc"
+ ? new Date(b.manifest.uploadedAt).getTime() -
+ new Date(a.manifest.uploadedAt).getTime()
+ : new Date(a.manifest.uploadedAt).getTime() -
+ new Date(b.manifest.uploadedAt).getTime()
+ ,
+ removeCidFromFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
+ return folders.map(([name, files]) =>
+ name === folder
+ ? [name, files.filter((id) => id !== cid)]
+ : [name, files]
+ )
+ },
+ addCidToFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] {
+ return folders.map(([name, files]) =>
+ name === folder ? [name, [...files, cid]] : [name, files]
+ )
+ },
+ exists(folders: [string, string[]][], name: string) {
+ return !!folders.find(([folder]) => folder === name)
+ },
+ toggleFilters: (filters: string[], filter: string) => filters.includes(filter)
+ ? filters.filter((f) => f !== filter)
+ : [...filters, filter],
+ listInFolder(files: CodexDataContent[], folders: [string, string[]][], index: number) {
+ return index === 0
+ ? files
+ : files.filter((file) => folders[index - 1][1].includes(file.cid));
+ },
+ applyFilters(files: CodexDataContent[], filters: string[]) {
+ return files.filter(
+ (file) => filters.length === 0 || filters.includes(this.type(file.manifest.mimetype))
+ )
+ }
+};
+
+export type CodexFileMetadata = {
+ type: string;
+ name: string;
+};
diff --git a/src/components/HealthChecks/HealthChecks.css b/src/components/HealthChecks/HealthChecks.css
new file mode 100644
index 0000000..6ff3ceb
--- /dev/null
+++ b/src/components/HealthChecks/HealthChecks.css
@@ -0,0 +1,124 @@
+.health-checks {
+ .address {
+ display: flex;
+ position: relative;
+ gap: 16px;
+ flex-direction: column;
+ align-items: flex-start;
+
+ @media (min-width: 1000px) {
+ & {
+ flex-direction: row;
+ align-items: center;
+ }
+ }
+
+ > div {
+ position: relative;
+
+ @media (max-width: 999px) {
+ &:not(.refresh) {
+ width: 100%;
+ }
+ }
+ }
+
+ svg {
+ position: absolute;
+ top: 68px;
+ bottom: 0;
+ right: 18px;
+ }
+
+ .refresh {
+ position: relative;
+ cursor: pointer;
+ cursor: pointer;
+
+ @media (max-width: 999px) {
+ & {
+ top: 0px;
+ left: 0;
+ transform: scale(1.5);
+ right: 0;
+ margin: auto;
+ }
+ }
+
+ @media (min-width: 1000px) {
+ & {
+ top: 18px;
+ }
+ }
+
+ svg {
+ position: initial;
+ }
+
+ &.address--fetching .refresh svg {
+ animation: rotate 2s linear infinite;
+ }
+ }
+
+ input[type="number"] {
+ width: 150px;
+ }
+
+ input[type="number"]::-webkit-outer-spin-button,
+ input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ input[type="number"] {
+ -moz-appearance: textfield;
+ }
+
+ @media (max-width: 999px) {
+ input[type="number"],
+ input {
+ width: 100%;
+ }
+ }
+ }
+
+ p {
+ font-family: Azeret Mono;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 14px;
+ color: #828282;
+ padding-left: 1.25rem;
+ margin-top: 1.75rem;
+ margin-bottom: 3rem;
+ }
+
+ ul {
+ margin-bottom: 32px;
+
+ li {
+ display: flex;
+ align-items: center;
+ padding: 16px 0;
+ gap: 16px;
+ border-top: 1px solid #96969633;
+ border-bottom: 1px solid #96969633;
+
+ &:first-child {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ }
+
+ span {
+ display: flex;
+ align-items: center;
+ width: 20px;
+ height: 20px;
+ justify-content: center;
+ }
+ }
+ }
+}
diff --git a/src/components/HealthChecks/HealthChecks.tsx b/src/components/HealthChecks/HealthChecks.tsx
new file mode 100644
index 0000000..7efb698
--- /dev/null
+++ b/src/components/HealthChecks/HealthChecks.tsx
@@ -0,0 +1,205 @@
+import { useQueryClient } from "@tanstack/react-query";
+import { useEffect, useState } from "react";
+import { useDebug } from "../../hooks/useDebug";
+import { usePersistence } from "../../hooks/usePersistence";
+import { usePortForwarding } from "../../hooks/usePortForwarding";
+import { Input, Spinner } from "@codex-storage/marketplace-ui-components";
+import { classnames } from "../../utils/classnames";
+import "./HealthChecks.css";
+import { CodexSdk } from "../../sdk/codex";
+import { HealthCheckUtil } from "./health-check.utils";
+import { PortForwardingUtil } from "../../hooks/port-forwarding.util";
+import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
+import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
+import DeviceIcon from "../../assets/icons/device.svg?react";
+import RefreshIcon from "../../assets/icons/refresh.svg?react";
+import WarningIcon from "../../assets/icons/warning-circle.svg?react";
+
+type Props = {
+ online: boolean;
+ onStepValid: (valid: boolean) => void;
+};
+
+const throwOnError = false;
+const defaultPort = 8070;
+
+export function HealthChecks({ online, onStepValid }: Props) {
+ const codex = useDebug(throwOnError);
+ const portForwarding = usePortForwarding(codex.data);
+ const persistence = usePersistence(codex.isSuccess);
+ const [isAddressInvalid, setIsAddressInvalid] = useState(false);
+ const [isPortInvalid, setIsPortInvalid] = useState(false);
+ const [address, setAddress] = useState(
+ HealthCheckUtil.removePort(CodexSdk.url())
+ );
+ const [port, setPort] = useState(HealthCheckUtil.getPort(CodexSdk.url()));
+ const queryClient = useQueryClient();
+
+ useEffect(
+ () => {
+ if (codex.isSuccess) {
+ persistence.refetch();
+ portForwarding.refetch().then(({ data }) => {
+ onStepValid(data?.reachable || false);
+ });
+ } else {
+ onStepValid(false);
+ }
+ },
+ // We really do not want to add persistence and portForwarding as
+ // dependencies because it will cause a re-render every time.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [persistence.refetch, onStepValid, portForwarding.refetch, codex.isSuccess]
+ );
+
+ const onAddressChange = (e: React.FormEvent) => {
+ const element = e.currentTarget;
+ const value = e.currentTarget.value;
+
+ setIsAddressInvalid(!element.checkValidity());
+
+ const address = HealthCheckUtil.removePort(value);
+ setAddress(address);
+
+ if (HealthCheckUtil.containsPort(value)) {
+ const p = HealthCheckUtil.getPort(value);
+ setPort(p);
+ }
+ };
+
+ const onPortChange = (e: React.FormEvent) => {
+ const element = e.currentTarget;
+ const value = element.value;
+
+ setIsPortInvalid(!element.checkValidity());
+ setPort(parseInt(value, 10));
+ };
+
+ const onSave = () => {
+ const url = address + ":" + port;
+
+ if (HealthCheckUtil.isUrlInvalid(url)) {
+ return;
+ }
+
+ CodexSdk.updateURL(url)
+ .then(() => queryClient.invalidateQueries())
+ .then(() => codex.refetch());
+ };
+
+ let forwardingPortValue = defaultPort;
+
+ if (codex.isSuccess && codex.data) {
+ const port = PortForwardingUtil.getTcpPort(codex.data);
+ if (!port.error) {
+ forwardingPortValue = port.data;
+ }
+ }
+
+ return (
+
+
+
+
+ {isAddressInvalid ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Port forwarding should be {forwardingPortValue} for TCP and 8090 by
+ default for UDP.
+
+
+
+
+ -
+
+
+
+ Health Check
+
+ -
+
+ {online ? (
+
+ ) : (
+
+ )}
+
+ Internet connection
+
+ -
+
+ {portForwarding.isFetching ? (
+
+ ) : portForwarding.enabled ? (
+
+ ) : (
+
+ )}
+
+ Port forwarding
+
+ -
+
+ {codex.isFetching ? (
+
+ ) : codex.isSuccess ? (
+
+ ) : (
+
+ )}
+
+ Codex connection
+
+ -
+
+ {persistence.isFetching ? (
+
+ ) : persistence.enabled ? (
+
+ ) : (
+
+ )}
+
+ Marketplace
+
+
+
+ );
+}
diff --git a/src/components/HealthChecks/health-check.utils.test.ts b/src/components/HealthChecks/health-check.utils.test.ts
new file mode 100644
index 0000000..4565603
--- /dev/null
+++ b/src/components/HealthChecks/health-check.utils.test.ts
@@ -0,0 +1,148 @@
+import { assert, describe, it } from "vitest";
+import { HealthCheckUtil } from "./health-check.utils";
+
+describe("health check", () => {
+ it("remove the port from an url", async () => {
+ assert.deepEqual(HealthCheckUtil.removePort("http://localhost:8080"), "http://localhost");
+ });
+
+ it("get the port from an url", async () => {
+ assert.deepEqual(HealthCheckUtil.getPort("http://localhost:8080"), 8080);
+ });
+
+ it("get the default port when the url does not contain the port", async () => {
+ assert.deepEqual(HealthCheckUtil.getPort("http://localhost"), 80);
+ });
+
+ it("returns true when the url contains a port", async () => {
+ assert.deepEqual(HealthCheckUtil.containsPort("http://localhost:8080"), true);
+ });
+
+ it("returns false when the url does not contain a port", async () => {
+ assert.deepEqual(HealthCheckUtil.containsPort("http://localhost"), false);
+ });
+
+
+ it("returns true when the url is invalid", async () => {
+ assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://"), true);
+ });
+
+ it("returns false when the url is valid", async () => {
+ assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://localhost:8080"), false);
+ });
+
+ it("returns the tcp port", async () => {
+ const debug = {
+ "id": "a",
+ "addrs": [
+ "/ip4/127.0.0.1/tcp/8070"
+ ],
+ "repo": "",
+ "spr": "",
+ "announceAddresses": [
+ "/ip4/127.0.0.1/tcp/8070"
+ ],
+ "table": {
+ "localNode": {
+ "nodeId": "",
+ "peerId": "",
+ "record": "",
+ "address": "0.0.0.0:8090",
+ "seen": false
+ },
+ "nodes": []
+ },
+ "codex": {
+ "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
+ "revision": "2fb7031e"
+ }
+ }
+ assert.deepEqual(HealthCheckUtil.getTcpPort(debug), { error: false, data: 8070 });
+ });
+
+ it("returns an error when the addr is empty", async () => {
+ const debug = {
+ "id": "a",
+ "addrs": [
+ ],
+ "repo": "",
+ "spr": "",
+ "announceAddresses": [
+ "/ip4/127.0.0.1/tcp/8070"
+ ],
+ "table": {
+ "localNode": {
+ "nodeId": "",
+ "peerId": "",
+ "record": "",
+ "address": "0.0.0.0:8090",
+ "seen": false
+ },
+ "nodes": []
+ },
+ "codex": {
+ "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
+ "revision": "2fb7031e"
+ }
+ }
+ assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
+ });
+
+ it("returns an error when the addr is misformated", async () => {
+ const debug = {
+ "id": "a",
+ "addrs": [
+ "/ip4/127.0.0.1/tcp/hello"
+ ],
+ "repo": "",
+ "spr": "",
+ "announceAddresses": [
+ "/ip4/127.0.0.1/tcp/8070"
+ ],
+ "table": {
+ "localNode": {
+ "nodeId": "",
+ "peerId": "",
+ "record": "",
+ "address": "0.0.0.0:8090",
+ "seen": false
+ },
+ "nodes": []
+ },
+ "codex": {
+ "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
+ "revision": "2fb7031e"
+ }
+ }
+ assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
+ });
+
+ it("returns an error when the port is misformated", async () => {
+ const debug = {
+ "id": "a",
+ "addrs": [
+ "hello"
+ ],
+ "repo": "",
+ "spr": "",
+ "announceAddresses": [
+ "/ip4/127.0.0.1/tcp/8070"
+ ],
+ "table": {
+ "localNode": {
+ "nodeId": "",
+ "peerId": "",
+ "record": "",
+ "address": "0.0.0.0:8090",
+ "seen": false
+ },
+ "nodes": []
+ },
+ "codex": {
+ "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7",
+ "revision": "2fb7031e"
+ }
+ }
+ assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true);
+ });
+})
\ No newline at end of file
diff --git a/src/components/HealthChecks/health-check.utils.ts b/src/components/HealthChecks/health-check.utils.ts
new file mode 100644
index 0000000..65250a4
--- /dev/null
+++ b/src/components/HealthChecks/health-check.utils.ts
@@ -0,0 +1,51 @@
+import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
+
+export const HealthCheckUtil = {
+ removePort(url: string) {
+ const parts = url.split(":")
+ return parts[0] + ":" + parts[1]
+ },
+
+ /*
+ * Extract the port from a protocol + ip + port string
+ */
+ getPort(url: string) {
+ return parseInt(url.split(":")[2] || "80", 10)
+ },
+
+ containsPort(url: string) {
+ return url.split(":").length > 2
+ },
+
+ isUrlInvalid(url: string) {
+ try {
+ new URL(url)
+ return false
+ // We do not need to manage the error because we want to check
+ // if the URL is valid or not only.
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ } catch (_) {
+ return true
+ }
+ },
+
+ getTcpPort(info: CodexDebugInfo): SafeValue {
+ if (info.addrs.length === 0) {
+ return { error: true, data: new CodexError("Not existing address") }
+ }
+
+ const parts = info.addrs[0].split("/")
+
+ if (parts.length < 2) {
+ return { error: true, data: new CodexError("Address misformated") }
+ }
+
+ const port = parseInt(parts[parts.length - 1], 10)
+
+ if (isNaN(port)) {
+ return { error: true, data: new CodexError("Port misformated") }
+ }
+
+ return { error: false, data: port }
+ }
+}
\ No newline at end of file
diff --git a/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx b/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx
deleted file mode 100644
index f06a392..0000000
--- a/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NetworkIndicator } from "@codex-storage/marketplace-ui-components";
-import { useNetwork } from "../../network/useNetwork";
-
-export function HttpNetworkIndicator() {
- const online = useNetwork();
-
- const text = online ? "Online" : "Offline";
-
- return ;
-}
diff --git a/src/components/LogLevel/LogLevel.css b/src/components/LogLevel/LogLevel.css
index a544411..74ac045 100644
--- a/src/components/LogLevel/LogLevel.css
+++ b/src/components/LogLevel/LogLevel.css
@@ -1,7 +1,26 @@
-.logLevel {
+.log-level {
flex-grow: 1;
-}
+ display: flex;
+ gap: 16px;
+ margin-top: 14px;
-.logLevel-select {
- margin-bottom: 0.75rem;
+ > div:first-child {
+ position: relative;
+
+ svg {
+ position: absolute;
+ top: 11px;
+ left: 16px;
+ color: #969696;
+ }
+ }
+
+ .select {
+ background-position: url(/icons/logs.svg);
+ }
+
+ select {
+ border-color: #969696;
+ padding-left: 40px;
+ }
}
diff --git a/src/components/LogLevel/LogLevel.tsx b/src/components/LogLevel/LogLevel.tsx
index c2f3978..eb740a7 100644
--- a/src/components/LogLevel/LogLevel.tsx
+++ b/src/components/LogLevel/LogLevel.tsx
@@ -9,6 +9,8 @@ import {
Toast,
} from "@codex-storage/marketplace-ui-components";
import { Promises } from "../../utils/promises";
+import LogsIcon from "../../assets/icons/logs.svg?react";
+import SaveIcon from "../../assets/icons/save.svg?react";
export function LogLevel() {
const queryClient = useQueryClient();
@@ -37,7 +39,7 @@ export function LogLevel() {
const [toast, setToast] = useState({
time: 0,
message: "",
- variant: "success" as "success" | "error" | "default",
+ variant: "success" as "success" | "error",
});
function onChange(e: React.FormEvent) {
@@ -52,34 +54,40 @@ export function LogLevel() {
};
const levels = [
- ["DEBUG", "DEBUG"],
- ["TRACE", "TRACE"],
- ["INFO", "INFO"],
- ["NOTICE", "NOTICE"],
- ["WARN", "WARN"],
- ["ERROR", "ERROR"],
- ["FATAL", "FATAL"],
+ ["DEBUG", "Debug"],
+ ["TRACE", "Trace"],
+ ["INFO", "Info"],
+ ["NOTICE", "Notice"],
+ ["WARN", "Warn"],
+ ["ERROR", "Error"],
+ ["FATAL", "Fatal"],
] satisfies [string, string][];
return (
- <>
-
+
);
}
diff --git a/src/components/ManifestFetch/ManifestFetch.css b/src/components/ManifestFetch/ManifestFetch.css
new file mode 100644
index 0000000..8c03b3c
--- /dev/null
+++ b/src/components/ManifestFetch/ManifestFetch.css
@@ -0,0 +1,16 @@
+.manifest-fetch {
+ display: flex;
+ gap: 16px;
+
+ .input {
+ flex: 1;
+
+ input {
+ width: 100%;
+ }
+ }
+
+ .button {
+ width: 105px;
+ }
+}
diff --git a/src/components/ManifestFetch/ManifestFetch.tsx b/src/components/ManifestFetch/ManifestFetch.tsx
new file mode 100644
index 0000000..1250ff4
--- /dev/null
+++ b/src/components/ManifestFetch/ManifestFetch.tsx
@@ -0,0 +1,57 @@
+import { Button, Input } from "@codex-storage/marketplace-ui-components";
+import "./ManifestFetch.css";
+import { ChangeEvent, useState } from "react";
+import { CodexSdk } from "../../sdk/codex";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { Promises } from "../../utils/promises";
+
+export function ManifestFetch() {
+ const [cid, setCid] = useState("");
+ const queryClient = useQueryClient();
+
+ const { refetch } = useQuery({
+ queryFn: () => {
+ return CodexSdk.data()
+ .fetchManifest(cid)
+ .then((s) => {
+ if (s.error === false) {
+ setCid("");
+ queryClient.invalidateQueries({ queryKey: ["cids"] });
+ }
+ return Promises.rejectOnError(s);
+ });
+ },
+ queryKey: ["manifest"],
+
+ // Disable the fetch to make it available on refetch only
+ enabled: false,
+
+ // No need to retry because if the connection to the node
+ // is back again, all the queries will be invalidated.
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ refetchOnWindowFocus: false,
+ });
+
+ const onDownload = () => refetch();
+
+ const onCidChange = (e: ChangeEvent) =>
+ setCid(e.currentTarget.value);
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx
new file mode 100644
index 0000000..c921dbd
--- /dev/null
+++ b/src/components/Menu/Menu.tsx
@@ -0,0 +1,171 @@
+import { attributes } from "../../utils/attributes";
+import "./menu.css";
+import { ComponentType, useState } from "react";
+import { classnames } from "../../utils/classnames";
+import { Link } from "@tanstack/react-router";
+import HomeIcon from "../../assets/icons/home.svg?react";
+import ExpandIcon from "../../assets/icons/expand.svg?react";
+import WalletIcon from "../../assets/icons/wallet.svg?react";
+import NodesIcon from "../../assets/icons/wallet.svg?react";
+import FilesIcon from "../../assets/icons/files.svg?react";
+import AnalyticsIcon from "../../assets/icons/analytics.svg?react";
+import Logo from "../../assets/icons/logo.svg?react";
+import Logotype from "../../assets/icons/logotype.svg?react";
+import DeviceIcon from "../../assets/icons/device.svg?react";
+import PeersIcon from "../../assets/icons/peers.svg?react";
+import PurchaseIcon from "../../assets/icons/purchase.svg?react";
+import HostIcon from "../../assets/icons/host.svg?react";
+import LogsIcon from "../../assets/icons/logs.svg?react";
+import SettingsIcon from "../../assets/icons/settings.svg?react";
+import HelpIcon from "../../assets/icons/help.svg?react";
+import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react";
+
+export type MenuItemComponentProps = {
+ onClick: () => void;
+};
+
+export type MenuItem =
+ | {
+ type: "separator";
+ }
+ | {
+ type: "space";
+ }
+ | {
+ type: "item";
+ Component: ComponentType;
+ };
+
+export function Menu() {
+ const [isExpanded, setIsExpanded] = useState(true);
+
+ const onLogoClick = () => {
+ if (isExpanded === false) {
+ setIsExpanded(true);
+ }
+ };
+
+ const onExpandMenu = () => setIsExpanded(!isExpanded);
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/components/Menu/menu.css b/src/components/Menu/menu.css
new file mode 100644
index 0000000..459116b
--- /dev/null
+++ b/src/components/Menu/menu.css
@@ -0,0 +1,255 @@
+.menu {
+ display: flex;
+ flex-direction: column;
+ background-color: #1c1c1c;
+ border-radius: var(--codex-border-radius);
+ transition: left 0.25s;
+ position: sticky;
+ z-index: 10;
+ view-transition-name: main-menu;
+ height: 100%;
+ top: 0;
+ transition:
+ width 0.5s,
+ font-size 0.5s,
+ left 0.05s;
+ min-width: 0;
+ width: 272px;
+ min-width: 80px;
+
+ @media (max-width: 1199px) {
+ & {
+ width: 80px;
+ .items {
+ a {
+ width: 26px;
+ gap: 0;
+ display: flex;
+ justify-content: center;
+
+ span + span {
+ font-size: 0;
+ display: none;
+ }
+ }
+ }
+ }
+ }
+
+ @media (min-width: 1200px) {
+ &[aria-expanded] a[data-title]:hover::after {
+ content: attr(data-title);
+ background-color: #2f2f2f;
+ color: #fff;
+ padding: 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 14px;
+ display: block;
+ white-space: nowrap;
+ position: absolute;
+ right: 1rem;
+ overflow: visible;
+ }
+ }
+
+ &:not([aria-expanded]) {
+ width: 80px;
+
+ .items {
+ a {
+ width: 26px;
+ gap: 0;
+ display: flex;
+ justify-content: center;
+
+ span + span {
+ font-size: 0;
+ display: none;
+ }
+ }
+ }
+ }
+
+ > div {
+ display: flex;
+ flex-direction: column;
+ padding: 12px;
+ position: sticky;
+ top: 0;
+ height: calc(100vh - 24px);
+ overflow: auto;
+ }
+
+ header {
+ padding: 13px;
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ background-color: #060606;
+ border-radius: 8px;
+
+ > svg:first-child {
+ min-width: 30px;
+ }
+
+ @media (min-width: 1200px) {
+ > svg:first-child {
+ cursor: pointer;
+ }
+ }
+
+ div {
+ flex: 1;
+ text-align: right;
+ transition: opacity 0.35s;
+ display: inline-block;
+ overflow: hidden;
+ min-width: 0;
+
+ svg {
+ cursor: pointer;
+ }
+
+ &:hover {
+ animation-name: example;
+ animation-duration: 2.5s;
+ animation-iteration-count: infinite;
+ }
+ }
+ }
+
+ .items {
+ padding-top: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ height: 100%;
+ margin-bottom: 2.5rem;
+ gap: 0.5rem;
+ border-top: 1px solid #96969633;
+
+ &::before {
+ height: 20px;
+ width: 8px;
+ background-color: var(--codex-color-primary);
+ position: absolute;
+ content: " ";
+ transition:
+ top 1s,
+ bottom 1s;
+ border-radius: 4px;
+ left: -16px;
+ }
+
+ &:has(.active:nth-child(1))::before {
+ top: 30px;
+ }
+
+ &:has(.active:nth-child(2))::before {
+ top: 72px;
+ }
+
+ &:has(.active:nth-child(3))::before {
+ top: 115px;
+ }
+
+ &:has(.active:nth-child(4))::before {
+ top: 158px;
+ }
+
+ &:has(.active:nth-child(5))::before {
+ top: 201px;
+ }
+
+ &:has(.active:nth-child(6))::before {
+ top: 244px;
+ }
+
+ &:has(.active:nth-child(8))::before {
+ top: 339px;
+ }
+
+ &:has(.active:nth-child(9))::before {
+ top: 382px;
+ }
+
+ &:has(.active:nth-child(11))::before {
+ top: 475px;
+ }
+
+ &:has(.active:nth-child(12))::before {
+ top: 513px;
+ }
+
+ &:has(.active:nth-child(14))::before {
+ top: calc(100% - 113px);
+ }
+
+ &:has(.active:nth-child(15))::before {
+ top: calc(100% - 70px);
+ }
+
+ &:has(.active:nth-child(16))::before {
+ top: calc(100% - 27px);
+ }
+
+ &:not(:first-child) {
+ margin-top: 0.5rem;
+ }
+
+ hr {
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+ border: 0.1px solid #96969633;
+ width: 100%;
+ }
+
+ section {
+ flex: 1;
+ }
+
+ a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 8px 10px;
+ margin-bottom: 0;
+ text-decoration: none;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ color: #969696;
+ border-radius: 8px;
+ transition: background-color 0.35s;
+ position: relative;
+ margin-left: 6px;
+
+ &:hover:not([aria-disabled="true"]),
+ &.active {
+ background-color: var(--codex-highlight-color);
+ color: #c7c7c7;
+ }
+
+ span:first-child {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ transition: color 1s;
+ }
+
+ span + span {
+ display: inline-block;
+ overflow: hidden;
+ min-width: 0;
+ }
+
+ &.active span:first-child {
+ color: var(--codex-color-primary);
+ }
+ }
+ }
+}
diff --git a/src/components/NodeIndicator/NodeIndicator.tsx b/src/components/NodeIndicator/NodeIndicator.tsx
deleted file mode 100644
index 422a9e3..0000000
--- a/src/components/NodeIndicator/NodeIndicator.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { useQuery, useQueryClient } from "@tanstack/react-query";
-import { useEffect, useState } from "react";
-import { CodexSdk } from "../../sdk/codex";
-import {
- NetworkIndicator,
- Toast,
-} from "@codex-storage/marketplace-ui-components";
-import { Promises } from "../../utils/promises";
-
-const report = false;
-
-export function NodeIndicator() {
- const queryClient = useQueryClient();
- const [toast] = useState({
- time: 0,
- message: "",
- });
-
- const { data, isError } = useQuery({
- queryKey: ["spr"],
- queryFn: async () => {
- return CodexSdk.node()
- .spr()
- .then((data) => Promises.rejectOnError(data, report));
- },
- refetchInterval: 5000,
-
- // No need to retry because we defined a refetch interval
- retry: false,
-
- // The client node should be local, so display the cache value while
- // making a background request looks good.
- staleTime: 0,
-
- // Refreshing when focus returns can be useful if a user comes back
- // to the UI after performing an operation in the terminal.
- refetchOnWindowFocus: true,
-
- // Cache is not useful for the spr endpoint
- gcTime: 0,
- });
-
- const isCodexOnline = !isError && !!data;
-
- useEffect(() => {
- queryClient.invalidateQueries({
- type: "active",
- refetchType: "all",
- });
-
- // Dispatch an event in order to reset Sentry error boundary
- document.dispatchEvent(new CustomEvent("codexinvalidatequeries"));
- }, [queryClient, isCodexOnline]);
-
- return (
- <>
-
-
- >
- );
-}
diff --git a/src/components/NodeSpace/NodeSpace.css b/src/components/NodeSpace/NodeSpace.css
new file mode 100644
index 0000000..8c7c82d
--- /dev/null
+++ b/src/components/NodeSpace/NodeSpace.css
@@ -0,0 +1,13 @@
+.node-space {
+ h6 {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
+ padding-top: 16px;
+ margin-bottom: 16px;
+ border-top: 1px solid #96969633;
+ }
+}
diff --git a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx b/src/components/NodeSpace/NodeSpace.tsx
similarity index 63%
rename from src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx
rename to src/components/NodeSpace/NodeSpace.tsx
index e5cca39..0c65a20 100644
--- a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx
+++ b/src/components/NodeSpace/NodeSpace.tsx
@@ -3,7 +3,7 @@ import Loader from "../../assets/loader.svg";
import { CodexSdk } from "../../sdk/codex";
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
import { Promises } from "../../utils/promises";
-import { nodeSpaceAllocationColors } from "./nodeSpaceAllocation.domain";
+import "./NodeSpace.css";
const defaultSpace = {
quotaMaxBytes: 0,
@@ -12,7 +12,7 @@ const defaultSpace = {
totalBlocks: 0,
};
-export function NodeSpaceAllocation() {
+export function NodeSpace() {
const { data: space, isPending } = useQuery({
queryFn: () =>
CodexSdk.data()
@@ -41,24 +41,27 @@ export function NodeSpaceAllocation() {
const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space;
return (
-
+
+ Disk
+
+
+
);
}
diff --git a/src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts b/src/components/NodeSpace/nodeSpace.domain.ts
similarity index 100%
rename from src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts
rename to src/components/NodeSpace/nodeSpace.domain.ts
diff --git a/src/components/OnBoarding/OnBoardingLayout.css b/src/components/OnBoarding/OnBoardingLayout.css
new file mode 100644
index 0000000..6a1c2cc
--- /dev/null
+++ b/src/components/OnBoarding/OnBoardingLayout.css
@@ -0,0 +1,240 @@
+.onboarding {
+ width: 100%;
+ padding: 16px;
+ display: flex;
+
+ @media (min-width: 1000px) {
+ & {
+ padding: 3rem 6rem;
+ }
+ }
+
+ > section {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ z-index: 1;
+ }
+
+ @media (min-width: 1000px) {
+ > section:first-child {
+ max-width: 500px;
+ }
+ }
+
+ section {
+ flex: 1;
+ }
+
+ &.onboarding--second .alpha {
+ flex: 0.3;
+ }
+
+ &.onboarding--third .alpha {
+ flex: 0.5;
+ }
+
+ section > *:first-child {
+ flex: 0.5;
+ }
+
+ h1 {
+ font-family: Inter;
+ font-size: 36px;
+ font-weight: 300;
+ line-height: 43.57px;
+ letter-spacing: 0.01em;
+
+ b {
+ font-weight: 400;
+ }
+
+ b + b {
+ font-weight: 900;
+ text-transform: uppercase;
+ }
+ }
+
+ footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex: 0;
+
+ ul {
+ display: flex;
+ gap: 8px;
+
+ li {
+ cursor: pointer;
+ width: 12px;
+ height: 12px;
+ background-color: white;
+ display: inline-block;
+ border-radius: 50%;
+ transition: opacity 0.35s;
+
+ &:hover {
+ animation-name: pulse;
+ animation-duration: 2.5s;
+ animation-iteration-count: infinite;
+ }
+
+ &:not([aria-selected]) {
+ opacity: 0.4;
+ }
+
+ &[aria-selected] {
+ box-shadow: 0px 0px 12px 0px #fff;
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ .alpha {
+ > div {
+ margin-top: 4px;
+ display: block;
+ }
+
+ b {
+ font-weight: 500;
+ opacity: 0.6;
+ display: block;
+ }
+
+ a {
+ text-decoration: underline;
+ font-family: Azeret Mono;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 14px;
+ margin-top: 16px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ color: var(--codex-color-error-hexa);
+
+ &:hover {
+ animation-name: example;
+ animation-duration: 2.5s;
+ animation-iteration-count: infinite;
+ }
+ }
+
+ p {
+ margin-top: 1rem;
+ font-family: Azeret Mono;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 16.34px;
+ display: inline-block;
+ color: var(--codex-color-primary);
+ }
+ }
+
+ .main {
+ p {
+ font-family: Azeret Mono;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 16.34px;
+ max-width: 532px;
+ margin-top: 20px;
+ color: var(--codex-input-label-color);
+ }
+
+ label {
+ margin-top: 1rem;
+ }
+
+ .health-checks {
+ .address {
+ .refresh {
+ top: 22px;
+ }
+ }
+ }
+ }
+
+ .get-started {
+ a {
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 29.05px;
+ letter-spacing: 0.01em;
+ margin-top: 32px;
+ font-family: Inter;
+ color: #7bfbaf;
+ gap: 4px;
+ text-decoration: none;
+ border-bottom: 2px solid #7bfbaf;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ }
+ }
+
+ .modal {
+ max-width: 600px;
+ margin: auto;
+
+ h1 {
+ margin-top: 0;
+ margin-bottom: 3rem;
+ }
+
+ p {
+ line-height: 1.5rem;
+ }
+ }
+
+ .navigation {
+ cursor: pointer;
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ border-bottom: none;
+ text-decoration: none;
+ z-index: 1;
+
+ right: 6rem;
+ bottom: 16px;
+
+ &:hover {
+ animation-name: example;
+ animation-duration: 2.5s;
+ animation-iteration-count: infinite;
+ }
+
+ &[aria-disabled="true"] {
+ cursor: not-allowed;
+ }
+ }
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(0deg); /* Start at 0 degrees */
+ }
+ to {
+ transform: rotate(360deg); /* End at 360 degrees */
+ }
+}
+
+@keyframes pulse {
+ 0% {
+ opacity: 0.4;
+ }
+ 30% {
+ opacity: 0.8;
+ }
+ 100% {
+ opacity: 0.4;
+ }
+}
+
+#sentry-feedback {
+ right: 128px;
+}
diff --git a/src/components/OnBoarding/OnBoardingLayout.tsx b/src/components/OnBoarding/OnBoardingLayout.tsx
new file mode 100644
index 0000000..9f752a1
--- /dev/null
+++ b/src/components/OnBoarding/OnBoardingLayout.tsx
@@ -0,0 +1,50 @@
+import { ReactElement } from "react";
+import { classnames } from "../../utils/classnames";
+import Logotype from "../../assets/icons/logotype.svg?react";
+import { attributes } from "../../utils/attributes";
+import "./OnBoardingLayout.css";
+import { BackgroundImage } from "../BackgroundImage/BackgroundImage";
+import { useNavigate } from "@tanstack/react-router";
+
+type Props = {
+ children: ReactElement<{ onStepValid: (isValid: boolean) => void }>;
+ step: number;
+ defaultIsStepValid: boolean;
+};
+
+export function OnBoardingLayout({ children, step }: Props) {
+ const navigate = useNavigate({ from: window.location.pathname });
+
+ return (
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/src/components/Peers/PeerCountryCell.css b/src/components/Peers/PeerCountryCell.css
index ac159a5..b2489b3 100644
--- a/src/components/Peers/PeerCountryCell.css
+++ b/src/components/Peers/PeerCountryCell.css
@@ -1,5 +1,11 @@
-.peerCountry {
+.peer-country {
display: flex;
align-items: center;
gap: 1rem;
+
+ span:first-child {
+ background-color: #141414;
+ border-radius: 50%;
+ padding: 12px;
+ }
}
diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx
index 0737655..e1a2873 100644
--- a/src/components/Peers/PeerCountryCell.tsx
+++ b/src/components/Peers/PeerCountryCell.tsx
@@ -1,69 +1,23 @@
import { Cell } from "@codex-storage/marketplace-ui-components";
-import { PeerPin } from "./types";
-import { useQuery } from "@tanstack/react-query";
import "./PeerCountryCell.css";
-import { useEffect } from "react";
+import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils";
export type Props = {
- address: string;
- onPinAdd: (pin: PeerPin) => void;
+ node: PeerNode;
+ geo: PeerGeo | undefined;
};
-const getFlagEmoji = (countryCode: string) => {
- const codePoints = countryCode
- .toUpperCase()
- .split("")
- .map((char) => 127397 + char.charCodeAt(0));
- return String.fromCodePoint(...codePoints);
-};
-
-export function PeerCountryCell({ address, onPinAdd }: Props) {
- const { data } = useQuery({
- queryFn: () => {
- const [ip] = address.split(":");
-
- return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then(
- (res) => res.json()
- );
- },
- refetchOnMount: true,
-
- queryKey: [address],
-
- // Enable only when the address exists
- enabled: !!address,
-
- // No need to retry because if the connection to the node
- // is back again, all the queries will be invalidated.
- retry: false,
-
- // We can cache the data at Infinity because the relation between
- // country and ip is fixed
- staleTime: Infinity,
-
- // Don't expect something new when coming back to the UI
- refetchOnWindowFocus: false,
- });
-
- useEffect(() => {
- if (data) {
- onPinAdd({
- lat: data.latitude,
- lng: data.longitude,
- });
- }
- }, [data]);
-
+export function PeerCountryCell({ geo }: Props) {
return (
-
- {data ? (
+
+ {geo ? (
<>
- {!!data && getFlagEmoji(data.country_iso)}
- {data?.country}
+ {!!geo && PeerUtils.geCountryEmoji(geo.country_iso)}
+ {geo?.country}
>
) : (
- {address}
+
)}
diff --git a/src/components/Peers/Peers.css b/src/components/Peers/Peers.css
new file mode 100644
index 0000000..18669ad
--- /dev/null
+++ b/src/components/Peers/Peers.css
@@ -0,0 +1,184 @@
+.peers {
+ max-width: 1320px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-bottom: 32px;
+
+ > div {
+ max-width: 1320px;
+ }
+
+ > div:first-child {
+ width: calc(100% - 16px);
+ border: 1px solid #96969633;
+ padding: 16px;
+ border-radius: 16px;
+ position: relative;
+
+ @media (min-width: 1000px) {
+ & {
+ width: calc(100% - 128px - 16px);
+ padding: 16px 16px 16px 128px;
+ height: 600px;
+ }
+ }
+
+ ul {
+ display: none;
+
+ @media (min-width: 1000px) {
+ & {
+ list-style-type: none;
+ width: 71px;
+ position: absolute;
+ left: 16px;
+ top: 16px;
+ display: inline-block;
+ }
+ }
+
+ li {
+ border-bottom: 1px solid #969696cc;
+ padding: 16px 0;
+ text-align: right;
+ }
+
+ li:first-child {
+ font-size: 20px;
+ font-weight: 500;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ color: #7b7b7b;
+ }
+
+ li:not(:first-child) {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ position: relative;
+ }
+
+ li:not(:first-child)::before {
+ content: " ";
+ border: 4px solid var(--codex-color-primary);
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ display: inline-block;
+ position: absolute;
+ left: 0;
+ top: 20px;
+ }
+
+ li:nth-child(3)::before {
+ border-width: 5px;
+ height: 11px;
+ width: 11px;
+ top: 18px;
+ }
+
+ li:nth-child(4)::before {
+ border-width: 6px;
+ height: 12px;
+ width: 12px;
+ top: 16px;
+ }
+ }
+
+ .connections {
+ background-color: #232323;
+ border: 1px solid #96969633;
+ border-radius: 16px;
+ max-width: 280px;
+ padding: 16px;
+ transform: scale(0.7);
+ width: 280px;
+
+ @media (max-width: 999px) {
+ & {
+ position: relative;
+ bottom: -32px;
+ left: -32px;
+ }
+ }
+
+ @media (min-width: 1000px) {
+ & {
+ transform: scale(1);
+ }
+ }
+
+ @media (min-width: 1000px) {
+ & {
+ position: absolute;
+ bottom: 16px;
+ left: 16px;
+ width: 280px;
+ }
+ }
+
+ header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #969696;
+ padding-bottom: 16px;
+ border-bottom: 1px solid #96969633;
+
+ span {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ color: white;
+ }
+ }
+
+ footer {
+ border-top: 1px solid #96969633;
+ padding-top: 16px;
+ }
+ }
+ }
+
+ > div:nth-child(2) {
+ margin-top: 32px;
+ width: 100%;
+ }
+
+ table {
+ td:last-child {
+ div {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ border-radius: 8px;
+
+ &.status--active {
+ color: #1daf61;
+ background-color: #6fcb9433;
+ }
+
+ &.status--inactive {
+ color: #fb3748;
+ background-color: #fb374833;
+ }
+ }
+ }
+ }
+
+ .peers-chart {
+ transform: scale(0.5);
+
+ @media (min-width: 1000px) {
+ & {
+ transform: scale(0.73);
+ }
+ }
+ }
+}
diff --git a/src/components/Peers/Peers.tsx b/src/components/Peers/Peers.tsx
new file mode 100644
index 0000000..93faf98
--- /dev/null
+++ b/src/components/Peers/Peers.tsx
@@ -0,0 +1,106 @@
+import {
+ TabSortState,
+ Row,
+ Cell,
+ Table,
+} from "@codex-storage/marketplace-ui-components";
+import { useCallback, useState } from "react";
+import { PeerCountryCell } from "./PeerCountryCell";
+import "./Peers.css";
+import { PeerGeo, PeerNode, PeerSortFn, PeerUtils } from "./peers.utils";
+import { PeersMap } from "./PeersMap";
+import { useDebug } from "../../hooks/useDebug";
+import { PeersQuality } from "./PeersQuality";
+import { PeersChart } from "./PeersChart";
+import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
+import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
+import PeersIcon from "../../assets/icons/peers.svg?react";
+
+const throwOnError = true;
+
+export const Peers = () => {
+ const { data } = useDebug(throwOnError);
+ const [ips, setIps] = useState >({});
+
+ const onPinAdd = useCallback((node: PeerNode, geo: PeerGeo) => {
+ const [ip = ""] = node.address.split(":");
+ setIps((ips) => ({ ...ips, [ip]: geo }));
+ }, []);
+
+ const [sortFn, setSortFn] = useState(() =>
+ PeerUtils.sortByBoolean("desc")
+ );
+
+ const onSortByCountry = (state: TabSortState) =>
+ setSortFn(() => PeerUtils.sortByCountry(state, ips));
+
+ const onSortActive = (state: TabSortState) =>
+ setSortFn(() => PeerUtils.sortByBoolean(state));
+
+ const headers = [
+ ["Country", onSortByCountry],
+ ["PeerId"],
+ ["Active", onSortActive],
+ ] satisfies [string, ((state: TabSortState) => void)?][];
+
+ const nodes = data?.table.nodes || [];
+ const sorted = sortFn ? nodes.slice().sort(sortFn) : nodes;
+
+ const rows = sorted.map((node) => {
+ const [ip = ""] = node.address.split(":");
+ const geo = ips[ip];
+
+ return (
+ ,
+ | {node.peerId} | ,
+
+ {node.seen ? (
+
+ Active
+
+ ) : (
+
+ Inactive
+
+ )}
+ | ,
+ ]}>
+ );
+ });
+
+ const actives = PeerUtils.countActives(sorted);
+ const degrees = PeerUtils.calculareDegrees(sorted);
+ const good = PeerUtils.isGoodQuality(actives);
+
+ return (
+
+
+
+
+
+ - Legend
+ - 1-3
+ - 3-5
+ - 5 +
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Peers/PeersCard.css b/src/components/Peers/PeersCard.css
new file mode 100644
index 0000000..c5007ed
--- /dev/null
+++ b/src/components/Peers/PeersCard.css
@@ -0,0 +1,23 @@
+.peers-card {
+ position: relative;
+
+ .peers-map {
+ border-top: 1px solid #96969633;
+ border-bottom: 1px solid #96969633;
+ width: 100%;
+
+ svg {
+ width: 100%;
+ }
+ }
+
+ .peers-chart {
+ position: absolute;
+ left: 0;
+ right: 0;
+ }
+
+ footer {
+ padding-top: 16px;
+ }
+}
diff --git a/src/components/Peers/PeersCard.tsx b/src/components/Peers/PeersCard.tsx
new file mode 100644
index 0000000..cd6f2d7
--- /dev/null
+++ b/src/components/Peers/PeersCard.tsx
@@ -0,0 +1,29 @@
+import { PeersMap } from "./PeersMap";
+import "./PeersCard.css";
+import { useDebug } from "../../hooks/useDebug";
+import { PeerUtils } from "./peers.utils";
+import { PeersChart } from "./PeersChart";
+import { PeersQuality } from "./PeersQuality";
+
+const throwOnError = true;
+
+export function PeersCard() {
+ const { data } = useDebug(throwOnError);
+
+ const nodes = data?.table.nodes ?? [];
+ const actives = PeerUtils.countActives(nodes);
+ const degrees = PeerUtils.calculareDegrees(nodes);
+ const good = PeerUtils.isGoodQuality(actives);
+
+ return (
+
+ );
+}
diff --git a/src/components/Peers/PeersChart.css b/src/components/Peers/PeersChart.css
new file mode 100644
index 0000000..e01d60f
--- /dev/null
+++ b/src/components/Peers/PeersChart.css
@@ -0,0 +1,64 @@
+.peers-chart {
+ position: relative;
+ width: 350px;
+ height: 175px;
+ overflow: hidden;
+ margin: auto;
+ left: -32px;
+
+ @media (min-width: 1000px) {
+ & {
+ transform: scale(0.73);
+ }
+ }
+
+ *,
+ &::before {
+ box-sizing: border-box;
+ }
+
+ &::before,
+ &::after {
+ position: absolute;
+ }
+
+ &::before {
+ content: "";
+ width: inherit;
+ height: inherit;
+ border: 45px solid #323232;
+ border-bottom: none;
+ border-top-left-radius: 175px;
+ border-top-right-radius: 175px;
+ }
+
+ div {
+ position: absolute;
+ top: 100%;
+ width: inherit;
+ height: inherit;
+ border: 45px solid #323232;
+ border-top: none;
+ border-bottom-left-radius: 175px;
+ border-bottom-right-radius: 175px;
+ transform-origin: 50% 0;
+ }
+
+ div:nth-child(1) {
+ border-color: var(--codex-color-primary);
+ transform: rotate(calc(var(--codex-peers-degrees) * 1deg));
+ background-color: transparent;
+ }
+
+ span {
+ font-family: Inter;
+ font-size: 38.67px;
+ font-weight: 500;
+ line-height: 48.34px;
+ letter-spacing: -0.005em;
+ text-align: center;
+ position: absolute;
+ bottom: 0;
+ left: calc(50% - 24px);
+ }
+}
diff --git a/src/components/Peers/PeersChart.tsx b/src/components/Peers/PeersChart.tsx
new file mode 100644
index 0000000..3406f87
--- /dev/null
+++ b/src/components/Peers/PeersChart.tsx
@@ -0,0 +1,23 @@
+import "./PeersChart.css";
+
+type Props = {
+ actives: number;
+ degrees: number;
+};
+
+type CustomCSSProperties = React.CSSProperties & {
+ "--codex-peers-degrees": number;
+};
+
+export function PeersChart({ actives, degrees }: Props) {
+ const style: CustomCSSProperties = {
+ "--codex-peers-degrees": degrees,
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/Peers/PeersMap.tsx b/src/components/Peers/PeersMap.tsx
new file mode 100644
index 0000000..d8efdd7
--- /dev/null
+++ b/src/components/Peers/PeersMap.tsx
@@ -0,0 +1,92 @@
+import { getMapJSON } from "dotted-map";
+import DottedMap from "dotted-map/without-countries";
+import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils";
+import { useCallback, useState } from "react";
+import { PeersPin } from "./PeersPin";
+
+// This function accepts the same arguments as DottedMap in the example above.
+const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
+
+type Props = {
+ nodes: PeerNode[];
+ onPinAdd?: (node: PeerNode, geo: PeerGeo) => void;
+};
+
+export function PeersMap({ nodes, onPinAdd }: Props) {
+ // It’s safe to re-create the map at each render, because of the
+ // pre-computation it’s super fast ⚡️
+ const map = new DottedMap({ map: JSON.parse(mapJsonString) });
+
+ const [pins, setPins] = useState<[PeerNode & PeerGeo, number][]>([]);
+
+ const onInternalPinAdd = useCallback(
+ (node: PeerNode, geo: PeerGeo) => {
+ setPins((val) => PeerUtils.incPin(val, { ...node, ...geo }));
+ onPinAdd?.(node, geo);
+ },
+ [setPins, onPinAdd]
+ );
+
+ pins.map(([pin, quantity]) => {
+ let radius = 0.65;
+
+ if (quantity > 3) {
+ radius = 0.85;
+ }
+
+ if (quantity > 5) {
+ radius = 0.95;
+ }
+
+ map.addPin({
+ lat: pin.latitude,
+ lng: pin.longitude,
+ svgOptions: { color: "#d6ff79", radius },
+ });
+ });
+
+ const svgMap = map
+ .getSVG({
+ radius: 0.32,
+ color: "#969696",
+ shape: "circle",
+ backgroundColor: "#141414",
+ })
+ .replace(
+ "",
+ // Include the style into the svg tag for permance reason.
+ // An alternative would be to generated the full svg instead
+ // of the image but it's costful.
+ `
+ `
+ );
+
+ return (
+ <>
+ {nodes.map((node, index) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/src/components/Peers/PeersPin.ts b/src/components/Peers/PeersPin.ts
new file mode 100644
index 0000000..7144651
--- /dev/null
+++ b/src/components/Peers/PeersPin.ts
@@ -0,0 +1,43 @@
+import { useQuery } from "@tanstack/react-query";
+import { PeerGeo, PeerNode } from "./peers.utils";
+import { useEffect } from "react";
+
+type Props = {
+ node: PeerNode
+ onLoad: (node: PeerNode, geo: PeerGeo) => void
+}
+
+export function PeersPin({ node, onLoad }: Props) {
+ const { data } = useQuery({
+ queryFn: () => {
+ const [ip] = node.address.split(":");
+
+ return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then(
+ (res) => res.json()
+ );
+ },
+ queryKey: ["peers", node.address],
+
+ // No need to retry because if the connection to the node
+ // is back again, all the queries will be invalidated.
+ retry: false,
+
+ // We can cache the data at Infinity because the relation between
+ // country and ip is fixed
+ staleTime: Infinity,
+
+ // Don't expect something new when coming back to the UI
+ refetchOnWindowFocus: false,
+
+ refetchOnMount: false,
+ });
+
+ useEffect(() => {
+ if (data) {
+ onLoad(node, data)
+ }
+ }, [data, onLoad, node])
+
+
+ return ""
+}
\ No newline at end of file
diff --git a/src/components/Peers/PeersQuality.css b/src/components/Peers/PeersQuality.css
new file mode 100644
index 0000000..712f541
--- /dev/null
+++ b/src/components/Peers/PeersQuality.css
@@ -0,0 +1,10 @@
+.peers-quality {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+}
diff --git a/src/components/Peers/PeersQuality.tsx b/src/components/Peers/PeersQuality.tsx
new file mode 100644
index 0000000..5fb8fa4
--- /dev/null
+++ b/src/components/Peers/PeersQuality.tsx
@@ -0,0 +1,25 @@
+import "./PeersQuality.css";
+import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react";
+import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react";
+
+type Props = {
+ good: boolean;
+};
+
+export function PeersQuality({ good }: Props) {
+ if (good) {
+ return (
+
+
+ Peer connections in good standing.
+
+ );
+ }
+
+ return (
+
+
+ No peer connection active.
+
+ );
+}
diff --git a/src/components/Peers/peers.utils.test.ts b/src/components/Peers/peers.utils.test.ts
new file mode 100644
index 0000000..74cc315
--- /dev/null
+++ b/src/components/Peers/peers.utils.test.ts
@@ -0,0 +1,74 @@
+import { assert, describe, it } from "vitest";
+import { PeerGeo, PeerUtils } from "./peers.utils";
+
+describe("peers", () => {
+ it("sorts by boolean", async () => {
+ const a = { nodeId: "a", peerId: "", record: "", address: "", seen: false }
+ const b = { nodeId: "a", peerId: "", record: "", address: "", seen: true }
+ const items = [a, b,]
+
+ const descSorted = items.slice().sort(PeerUtils.sortByBoolean("desc"))
+
+ assert.deepEqual(descSorted, [b, a]);
+
+ const ascSorted = items.slice().sort(PeerUtils.sortByBoolean("asc"))
+
+ assert.deepEqual(ascSorted, [a, b]);
+ });
+
+ it("sorts by country", async () => {
+ const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
+ const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
+
+ const table = {
+ "127.0.0.1": {
+ country: "United States"
+ } as PeerGeo,
+ "127.0.0.2": {
+ country: "France"
+ } as PeerGeo,
+ }
+
+ const items = [a, b,]
+
+ const descSorted = items.slice().sort(PeerUtils.sortByCountry("desc", table))
+
+ assert.deepEqual(descSorted, [b, a]);
+
+ const ascSorted = items.slice().sort(PeerUtils.sortByCountry("asc", table))
+
+ assert.deepEqual(ascSorted, [a, b]);
+ });
+
+ it("adds a new pin", async () => {
+ const latLng = { latitude: 0, longitude: 0 } as any
+ const values = PeerUtils.incPin([], latLng)
+
+ assert.deepEqual(values, [[latLng, 1]]);
+ });
+
+ it("increments an existing pin", async () => {
+ const latLng = { lat: 0, lng: 0 } as any
+ const values = PeerUtils.incPin([[latLng, 1]], latLng)
+
+ assert.deepEqual(values, [[latLng, 2]]);
+ });
+
+ it("count active peers nodes", async () => {
+ const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
+ const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
+
+ assert.equal(PeerUtils.countActives([a, b]), 1)
+ });
+
+ it("calculates active peers nodes degrees", async () => {
+ const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false }
+ const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true }
+
+ assert.equal(PeerUtils.calculareDegrees([a, b]), 90)
+ });
+
+ it("returns the country flag", async () => {
+ assert.equal(PeerUtils.geCountryEmoji("FR"), "🇫🇷")
+ });
+})
\ No newline at end of file
diff --git a/src/components/Peers/peers.utils.ts b/src/components/Peers/peers.utils.ts
new file mode 100644
index 0000000..bfd6c05
--- /dev/null
+++ b/src/components/Peers/peers.utils.ts
@@ -0,0 +1,70 @@
+import { TabSortState } from "@codex-storage/marketplace-ui-components";
+
+export type PeerNode = {
+ nodeId: string;
+ peerId: string;
+ record: string;
+ address: string;
+ seen: boolean;
+};
+
+export type PeerGeo = {
+ latitude: number
+ longitude: number
+ country: string
+ country_iso: string
+}
+
+export type PeerSortFn = (a: PeerNode, b: PeerNode) => number;
+
+export const PeerUtils = {
+ sortByBoolean: (state: TabSortState) => (a: PeerNode, b: PeerNode) => {
+ const order = state === "desc" ? 1 : -1;
+ return a?.seen === b?.seen ? 0 : b?.seen ? order : -order;
+ },
+
+ sortByCountry: (state: TabSortState, ipTable: Record) =>
+ (a: PeerNode, b: PeerNode) => {
+ const [ipA = ""] = a.address.split(":")
+ const [ipB = ""] = b.address.split(":")
+ const countryA = ipTable[ipA].country || "";
+ const countryB = ipTable[ipB].country || "";
+
+ return state === "desc"
+ ? countryA.localeCompare(countryB)
+ : countryB.localeCompare(countryA);
+ },
+
+ /**
+ * Increments the number of pin for a location
+ */
+ incPin(val: [PeerNode & PeerGeo, number][], pin: PeerNode & PeerGeo): [PeerNode & PeerGeo, number][] {
+ const [, quantity = 0] =
+ val.find(([p]) => p.latitude === pin.latitude && p.longitude == pin.longitude) || [];
+ const rest = val.filter(([p]) => p.latitude !== pin.latitude || p.longitude !== pin.longitude)
+ return [...rest, [pin, quantity + 1]];
+ },
+
+ countActives: (peers: PeerNode[]) =>
+ peers.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0,
+
+ calculareDegrees: (peers: PeerNode[]) => {
+ const actives = PeerUtils.countActives(peers);
+ const total = peers.length || 1;
+
+ return (actives / total) * 180
+ },
+
+ isGoodQuality(actives: number) {
+ return actives > 0
+ },
+
+ geCountryEmoji: (countryCode: string) => {
+ const codePoints = countryCode
+ .toUpperCase()
+ .split("")
+ .map((char) => 127397 + char.charCodeAt(0));
+ return String.fromCodePoint(...codePoints);
+ }
+
+}
diff --git a/src/components/Peers/types.ts b/src/components/Peers/types.ts
deleted file mode 100644
index a3e1bea..0000000
--- a/src/components/Peers/types.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type PeerPin = {
- lat: number;
- lng: number;
-};
\ No newline at end of file
diff --git a/src/components/Purchase/PurchasesTable.tsx b/src/components/Purchase/PurchasesTable.tsx
new file mode 100644
index 0000000..e8553a1
--- /dev/null
+++ b/src/components/Purchase/PurchasesTable.tsx
@@ -0,0 +1,128 @@
+import {
+ Cell,
+ Row,
+ Spinner,
+ Table,
+ TabSortState,
+} from "@codex-storage/marketplace-ui-components";
+import { Times } from "../../utils/times";
+import { useState } from "react";
+import { FileCell } from "../../components/FileCellRender/FileCell";
+import { useData } from "../../hooks/useData";
+import { useQuery } from "@tanstack/react-query";
+import { CodexSdk } from "../../sdk/codex";
+import { Promises } from "../../utils/promises";
+import { CodexPurchase } from "@codex-storage/sdk-js";
+import { TruncateCell } from "../TruncateCell/TruncateCell";
+import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender";
+import { PurchaseUtils } from "./purchase.utils";
+
+type SortFn = (a: CodexPurchase, b: CodexPurchase) => number;
+
+export function PurchasesTable() {
+ const content = useData();
+ const { data, isPending } = useQuery({
+ queryFn: () =>
+ CodexSdk.marketplace()
+ .purchases()
+ .then((s) => Promises.rejectOnError(s)),
+ queryKey: ["purchases"],
+
+ // No need to retry because if the connection to the node
+ // is back again, all the queries will be invalidated.
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ // Refreshing when focus returns can be useful if a user comes back
+ // to the UI after performing an operation in the terminal.
+ refetchOnWindowFocus: true,
+
+ initialData: [],
+
+ // Throw the error to the error boundary
+ throwOnError: true,
+ });
+
+ // const onMetadata = (
+ // requestId: string,
+ // { uploadedAt }: { uploadedAt: number }
+ // ) => {
+ // setMetadata((m) => ({ ...m, [requestId]: uploadedAt }));
+ // setSortFn(() =>
+ // PurchaseUtils.sortByUploadedAt("desc", {
+ // ...metadata,
+ // [requestId]: uploadedAt,
+ // })
+ // );
+ // };
+
+ const [sortFn, setSortFn] = useState(() =>
+ PurchaseUtils.sortByDuration("desc")
+ );
+
+ const onSortByDuration = (state: TabSortState) =>
+ setSortFn(() => PurchaseUtils.sortByDuration(state));
+
+ const onSortByReward = (state: TabSortState) =>
+ setSortFn(() => PurchaseUtils.sortByReward(state));
+
+ const onSortByState = (state: TabSortState) =>
+ setSortFn(() => PurchaseUtils.sortByState(state));
+
+ // const onSortByUploadedAt = (state: TabSortState) =>
+ // setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata));
+
+ const headers = [
+ ["file"],
+ ["request id"],
+ ["duration", onSortByDuration],
+ ["slots"],
+ ["reward", onSortByReward],
+ ["proof probability"],
+ ["state", onSortByState],
+ ] satisfies [string, ((state: TabSortState) => void)?][];
+
+ const sorted = sortFn ? [...data].sort(sortFn) : data;
+
+ const rows = sorted.map((p, index) => {
+ const r = p.request;
+ const ask = p.request.ask;
+ const duration = parseInt(p.request.ask.duration, 10);
+ const pf = parseInt(p.request.ask.proofProbability, 10);
+
+ return (
+ ,
+ ,
+ {Times.pretty(duration)} | ,
+ {ask.slots.toString()} | ,
+ {ask.reward + " CDX"} | ,
+ {pf.toString()} | ,
+ ,
+ ]}>
+ );
+ });
+
+ if (isPending) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/components/Purchase/purchase.utils.ts b/src/components/Purchase/purchase.utils.ts
new file mode 100644
index 0000000..3005d8b
--- /dev/null
+++ b/src/components/Purchase/purchase.utils.ts
@@ -0,0 +1,42 @@
+import { TabSortState } from "@codex-storage/marketplace-ui-components"
+import { CodexPurchase } from "@codex-storage/sdk-js"
+
+export const PurchaseUtils = {
+ sortById: (state: TabSortState) =>
+ (a: CodexPurchase, b: CodexPurchase) => {
+
+ return state === "desc"
+ ? b.requestId
+ .toLocaleLowerCase()
+ .localeCompare(a.requestId.toLocaleLowerCase())
+ : a.requestId
+ .toLocaleLowerCase()
+ .localeCompare(b.requestId.toLocaleLowerCase())
+ },
+ sortByState: (state: TabSortState) =>
+ (a: CodexPurchase, b: CodexPurchase) => state === "desc"
+ ? b.state
+ .toLocaleLowerCase()
+ .localeCompare(a.state.toLocaleLowerCase())
+ : a.state
+ .toLocaleLowerCase()
+ .localeCompare(b.state.toLocaleLowerCase())
+ ,
+ sortByDuration: (state: TabSortState) =>
+ (a: CodexPurchase, b: CodexPurchase) => state === "desc"
+ ? Number(b.request.ask.duration) - Number(a.request.ask.duration)
+ : Number(a.request.ask.duration) - Number(b.request.ask.duration)
+ ,
+ sortByReward: (state: TabSortState) =>
+ (a: CodexPurchase, b: CodexPurchase) => state === "desc"
+ ? Number(b.request.ask.reward) - Number(a.request.ask.reward)
+ : Number(a.request.ask.reward) - Number(b.request.ask.reward)
+ ,
+ sortByUploadedAt: (state: TabSortState, table: Record) =>
+ (a: CodexPurchase, b: CodexPurchase) => {
+ return state === "desc"
+ ? (table[b.requestId] || 0) - (table[a.requestId] || 0)
+ : (table[a.requestId] || 0) - (table[b.requestId] || 0)
+ }
+ ,
+}
\ No newline at end of file
diff --git a/src/components/RequireAssitance/AssistanceImage.css b/src/components/RequireAssitance/AssistanceImage.css
new file mode 100644
index 0000000..ad742d4
--- /dev/null
+++ b/src/components/RequireAssitance/AssistanceImage.css
@@ -0,0 +1,7 @@
+.assistance-img {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ width: auto;
+ border-top-right-radius: 16px;
+}
diff --git a/src/components/RequireAssitance/AssistanceImage.tsx b/src/components/RequireAssitance/AssistanceImage.tsx
new file mode 100644
index 0000000..2fca9ca
--- /dev/null
+++ b/src/components/RequireAssitance/AssistanceImage.tsx
@@ -0,0 +1,26 @@
+import { classnames } from "../../utils/classnames";
+import "./AssistanceImage.css";
+
+export function AssistanceImage() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/RequireAssitance/RequireAssitance.css b/src/components/RequireAssitance/RequireAssitance.css
new file mode 100644
index 0000000..db48d0e
--- /dev/null
+++ b/src/components/RequireAssitance/RequireAssitance.css
@@ -0,0 +1,37 @@
+.require-assistance {
+ background-color: #0a1410;
+ box-sizing: border-box;
+ padding: 16px;
+ border-radius: 16px;
+ border-left: 4px solid #6ccc93;
+ height: 144px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ width: 500px;
+ flex: 1 1 auto;
+ cursor: pointer;
+ text-decoration: none;
+ position: relative;
+ max-width: 508px;
+
+ h5 {
+ font-family: Azeret Mono;
+ font-size: 10px;
+ font-weight: 400;
+ line-height: 20px;
+ text-align: left;
+ color: #6ccc93;
+ }
+
+ h6 {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 19.36px;
+ letter-spacing: 0.01em;
+ text-align: left;
+ color: #7bfbaf;
+ margin-bottom: 4px;
+ }
+}
diff --git a/src/components/RequireAssitance/RequireAssitance.tsx b/src/components/RequireAssitance/RequireAssitance.tsx
new file mode 100644
index 0000000..ae74d4a
--- /dev/null
+++ b/src/components/RequireAssitance/RequireAssitance.tsx
@@ -0,0 +1,23 @@
+import "./RequireAssitance.css";
+import DiscordIcon from "../../assets/icons/discord.svg?react";
+import { AssistanceImage } from "./AssistanceImage";
+
+export function RequireAssitance() {
+ return (
+
+ Require Assistance?
+
+
+
+
+ Join Codex Discord
+ Get direct access to the Core Team.
+
+
+
+
+ );
+}
diff --git a/src/components/StorageRequestSetup/StorageRequestAvailability.css b/src/components/StorageRequestSetup/StorageRequestAvailability.css
index 3ce11a4..8768c3d 100644
--- a/src/components/StorageRequestSetup/StorageRequestAvailability.css
+++ b/src/components/StorageRequestSetup/StorageRequestAvailability.css
@@ -3,21 +3,3 @@
display: flex;
align-items: center;
}
-
-.storageRequestFileChooser-dropdown-success {
- animation-duration: 3s;
- animation-name: cid-selected;
- border-radius: var(--codex-border-radius);
-}
-
-@keyframes cid-selected {
- 0% {
- box-shadow: 0 0 0 0px var(--codex-color-primary-variant);
- }
- 50% {
- box-shadow: 0 0 0 3px var(--codex-color-primary-variant);
- }
- 100% {
- box-shadow: 0 0 0 0px var(--codex-color-primary-variant);
- }
-}
diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.css b/src/components/StorageRequestSetup/StorageRequestCreate.css
index 09e57a1..2b746c1 100644
--- a/src/components/StorageRequestSetup/StorageRequestCreate.css
+++ b/src/components/StorageRequestSetup/StorageRequestCreate.css
@@ -1,5 +1,35 @@
-@media (min-width: 801px) {
- .storageRequestCreate {
- min-width: 700px;
+.storage-request {
+ .modal dialog {
+ width: 80%;
+ max-width: 100% !important;
+ }
+
+ header {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ margin-bottom: 16px;
+
+ small {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ text-align: left;
+ color: #969696;
+ }
+ }
+
+ h6 {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
+ }
+
+ .upload-file {
+ margin-bottom: 16px;
}
}
diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.tsx b/src/components/StorageRequestSetup/StorageRequestCreate.tsx
index a352b32..8d63361 100644
--- a/src/components/StorageRequestSetup/StorageRequestCreate.tsx
+++ b/src/components/StorageRequestSetup/StorageRequestCreate.tsx
@@ -16,26 +16,27 @@ import { useStorageRequestMutation } from "./useStorageRequestMutation";
import { Plus } from "lucide-react";
import "./StorageRequestCreate.css";
import { StorageRequestError } from "./StorageRequestError";
+import PurchaseIcon from "../../assets/icons/purchase.svg?react";
const CONFIRM_STATE = 2;
const defaultStorageRequest: StorageRequest = {
cid: "",
- availabilityUnit: "days",
+ availabilityUnit: "months",
availability: 1,
tolerance: 1,
proofProbability: 1,
nodes: 3,
reward: 10,
collateral: 10,
- expiration: 300,
+ expiration: 5,
};
export function StorageRequestCreate() {
const [storageRequest, setStorageRequest] = useState(
defaultStorageRequest
);
- const steps = useRef(["File", "Criteria", "Success"]);
+ const steps = useRef(["Select File", "Select Request Criteria", "Success"]);
const { state, dispatch } = useStepperReducer();
const { mutateAsync, error } = useStorageRequestMutation(dispatch, state);
@@ -87,7 +88,7 @@ export function StorageRequestCreate() {
mutateAsync({
...rest,
duration: Times.toSeconds(availability, availabilityUnit),
- expiry: expiration,
+ expiry: expiration * 60,
});
} else {
dispatch({
@@ -117,15 +118,21 @@ export function StorageRequestCreate() {
const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next";
return (
- <>
+
-
+
- >
+
);
}
diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.css b/src/components/StorageRequestSetup/StorageRequestFileChooser.css
index dfd0d5d..54746ff 100644
--- a/src/components/StorageRequestSetup/StorageRequestFileChooser.css
+++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.css
@@ -1,3 +1,35 @@
+.file-chooser {
+ .input {
+ width: 100%;
+ }
+
+ hr {
+ flex: 1;
+ border: 1px solid #96969633;
+ margin-top: 24px;
+ margin-bottom: 24px;
+
+ + span {
+ font-family: Inter;
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 12px;
+ letter-spacing: 0.02em;
+ text-align: left;
+ color: #696969;
+ }
+ }
+
+ .upload {
+ margin-top: 16px;
+ margin-bottom: 16px;
+ }
+
+ input {
+ width: 100%;
+ }
+}
+
.storageRequestFileChooser-hr {
margin: 1.5rem 0;
}
@@ -5,18 +37,3 @@
.storageRequestFileChooser-input {
width: 100%;
}
-
-.storageRequestFileChooser-separator {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 0;
-}
-
-.storageRequestFileChooser-or {
- font-size: 1.25rem;
-}
-
-.storageRequestFileChooser-dropdown .dropdown-input {
- width: 100%;
-}
diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx
index e5f7321..dae27c7 100644
--- a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx
+++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx
@@ -1,7 +1,6 @@
import { CodexSdk } from "../../sdk/codex";
import "./StorageRequestFileChooser.css";
import { ChangeEvent, useEffect } from "react";
-import { classnames } from "../../utils/classnames";
import {
Dropdown,
DropdownOption,
@@ -11,6 +10,8 @@ import {
import { useData } from "../../hooks/useData";
import { StorageRequestComponentProps } from "./types";
import { useQueryClient } from "@tanstack/react-query";
+import ChooseCidIcon from "../../assets/icons/choose-cid.svg?react";
+import UploadIcon from "../../assets/icons/upload.svg?react";
export function StorageRequestFileChooser({
storageRequest,
@@ -48,56 +49,49 @@ export function StorageRequestFileChooser({
const options =
files.map((f) => {
return {
- Icon: () => ,
- title: f.manifest.filename,
+ Icon: () => ,
+ title: f.manifest.filename || "",
subtitle: f.cid,
};
}) || [];
return (
- <>
- Choose a CID
-
-
+
+
-
-
- OR
-
+
+
+ OR
+
-
-
- Upload a file
-
-
- The CID will be automatically copied after your upload.
-
-
+
+
+ Upload
+
- >
+
);
}
diff --git a/src/components/StorageRequestSetup/StorageRequestReview.css b/src/components/StorageRequestSetup/StorageRequestReview.css
index 33ea913..24b8a96 100644
--- a/src/components/StorageRequestSetup/StorageRequestReview.css
+++ b/src/components/StorageRequestSetup/StorageRequestReview.css
@@ -1,110 +1,135 @@
-.storageRequestReview-hr {
- margin-bottom: 1.5rem;
- margin-top: 0rem;
-}
+.request-review {
+ > header {
+ border-bottom: 1px solid #96969633;
+ padding-bottom: 16px;
-.storageRequestReview-numbers {
- display: grid;
- 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-presets {
- display: flex;
- padding: 0 0.5rem 2rem 0.5rem;
- gap: 1rem;
-}
-
-.storageRequestReview-presets-blocs {
- display: flex;
- flex: 1;
- gap: 0.5rem;
-}
-
-.storageRequestReview-presets-bloc {
- flex: 1;
- border-radius: var(--codex-border-radius);
- background-color: rgb(56 56 56);
- align-items: center;
- padding: 1rem;
- align-items: center;
- justify-content: center;
- text-align: center;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- transition: opacity 0.35s;
- cursor: pointer;
- border: 1px solid transparent;
-}
-
-.storageRequest-price {
- display: flex;
- justify-content: center;
-}
-
-.storageRequestReview-presets-title {
- display: flex;
- flex-direction: column;
- justify-content: center;
-}
-
-.storageRequestReview-presets-bloc:not(
- .storageRequestReview-presets--selected
- ):hover {
- border: 1px solid var(--codex-border-color);
-}
-
-.storageRequestReview-presets--selected {
- border: 1px solid var(--codex-color-primary);
-}
-
-.storageRequestReview-alert {
- display: flex;
- gap: 1rem;
- align-items: flex-start;
-}
-
-.storageRequestReview-expiration {
- min-width: 33%;
-}
-
-@media (max-width: 800px) {
- .storageRequestReview-numbers {
- grid-template-columns: 1fr;
+ div {
+ line-height: 8px;
+ }
}
- .storageRequestReview-presets {
- flex-direction: column;
+ .presets {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 16px;
+
+ > div {
+ height: 74px;
+ position: relative;
+ }
+
+ > div:first-child {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+
+ span {
+ display: block;
+ font-family: Inter;
+ font-size: 10px;
+ font-weight: 400;
+ line-height: 12.1px;
+ letter-spacing: 0.01em;
+ text-transform: uppercase;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 18px;
+ letter-spacing: -0.011em;
+ color: #969696cc;
+ }
+ }
+
+ > div:nth-child(n + 2) {
+ --codex-preset-border-color: #494949;
+ --codex-preset-color: #969696;
+ border: 1px solid var(--codex-preset-border-color);
+ border-radius: 12px;
+ padding: 16px;
+ flex: 1;
+ box-sizing: border-box;
+ overflow: hidden;
+ display: flex;
+ align-items: flex-end;
+ cursor: pointer;
+ transition: 0.35s box-shadow;
+
+ &:hover {
+ box-shadow: 0 0 0 2px var(--codex-preset-border-color);
+ }
+
+ &[aria-selected] {
+ --codex-preset-border-color: #6fcb94;
+ --codex-preset-color: #6fcb94;
+ }
+
+ svg {
+ position: absolute;
+ right: 0;
+ top: 0;
+ color: var(--codex-preset-color);
+ }
+
+ span {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ text-align: left;
+ color: var(--codex-preset-color);
+
+ + span {
+ background: #6fcb9433;
+ padding: 2px 8px;
+ font-family: Inter;
+ font-size: 11px;
+ font-weight: 500;
+ line-height: 12px;
+ letter-spacing: 0.02em;
+ color: #6fcb94;
+ border-radius: 16px;
+ margin-left: 4px;
+ }
+ }
+ }
}
- .storageRequestReview-presets-blocs {
- flex-direction: column;
+ .row {
+ border-top: 1px solid #96969633;
+ margin-top: 16px;
+ margin-bottom: 16px;
+ padding-top: 16px;
+ gap: 8px;
}
- .storageRequestReview-alert {
- flex-direction: column;
+ .grid {
+ display: grid;
+ gap: 16px;
+
+ @media (max-width: 800px) {
+ & {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ @media (min-width: 801px) {
+ & {
+ grid-template-columns: 1fr 1fr 1fr;
+ }
+ }
}
- .storageRequestReview-expiration {
- min-width: 100%;
- }
-}
-
-@media (min-width: 801px) {
- .storageRequestReview-numbers {
- grid-template-columns: 1fr 1fr 1fr;
+ footer {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 16px;
+
+ > * {
+ flex: 1;
+ }
}
}
diff --git a/src/components/StorageRequestSetup/StorageRequestReview.tsx b/src/components/StorageRequestSetup/StorageRequestReview.tsx
index ab1ae8f..2c18314 100644
--- a/src/components/StorageRequestSetup/StorageRequestReview.tsx
+++ b/src/components/StorageRequestSetup/StorageRequestReview.tsx
@@ -3,12 +3,14 @@ import "./StorageRequestReview.css";
import { Alert } from "@codex-storage/marketplace-ui-components";
import { CardNumbers } from "../CardNumbers/CardNumbers";
import { FileWarning } from "lucide-react";
-import { classnames } from "../../utils/classnames";
-import {
- AvailabilityUnit,
- StorageRequest,
- StorageRequestComponentProps,
-} from "./types";
+import { StorageRequest, StorageRequestComponentProps } from "./types";
+import DurabilityIcon from "../../assets/icons/durability.svg?react";
+import AlphaIcon from "../../assets/icons/alpha.svg?react";
+import PresetIcon from "../../assets/icons/preset.svg?react";
+import CommitmentIcon from "../../assets/icons/commitment.svg?react";
+import RequestDurationIcon from "../../assets/icons/request-duration.svg?react";
+import { attributes } from "../../utils/attributes";
+import { Strings } from "../../utils/strings";
type Durability = {
nodes: number;
@@ -32,14 +34,14 @@ const findDurabilityIndex = (d: Durability) => {
return durabilities.findIndex((d) => JSON.stringify(d) === s);
};
-const units = ["days", "minutes", "hours", "days", "months", "years"];
+// const units = ["days", "minutes", "hours", "days", "months", "years"];
export function StorageRequestReview({
dispatch,
onStorageRequestChange,
storageRequest,
}: StorageRequestComponentProps) {
- const [durability, setDurability] = useState (1);
+ const [durability, setDurability] = useState(2);
const isInvalidConstrainst = useCallback(
(nodes: number, tolerance: number) => {
@@ -67,10 +69,12 @@ export function StorageRequestReview({
const onUpdateDurability = (data: Partial) => {
onStorageRequestChange(data);
+ const merge = { ...storageRequest, ...data };
+
const index = findDurabilityIndex({
- nodes: storageRequest.nodes,
- tolerance: storageRequest.tolerance,
- proofProbability: storageRequest.proofProbability,
+ nodes: merge.nodes,
+ tolerance: merge.tolerance,
+ proofProbability: merge.proofProbability,
});
setDurability(index + 1);
@@ -124,7 +128,7 @@ export function StorageRequestReview({
};
const isInvalidAvailability = (data: string) => {
- const [value, unit = "days"] = data.split(" ");
+ const [value] = data.split(" ");
const error = isInvalidNumber(value);
@@ -136,9 +140,9 @@ export function StorageRequestReview({
// unit += "s";
// }
- if (!units.includes(unit)) {
- return "Invalid unit must one of: minutes, hours, days, months, years";
- }
+ // if (!units.includes(unit)) {
+ // return "Invalid unit must one of: minutes, hours, days, months, years";
+ // }
return "";
};
@@ -156,7 +160,7 @@ export function StorageRequestReview({
onUpdateDurability({ proofProbability: Number(value) });
const onAvailabilityChange = (value: string) => {
- const [availability, availabilityUnit = "days"] = value.split(" ");
+ const [availability] = value.split(" ");
// if (!availabilityUnit.endsWith("s")) {
// availabilityUnit += "s";
@@ -164,7 +168,7 @@ export function StorageRequestReview({
onStorageRequestChange({
availability: Number(availability),
- availabilityUnit: availabilityUnit as AvailabilityUnit,
+ availabilityUnit: "months",
});
};
@@ -189,157 +193,148 @@ export function StorageRequestReview({
// return data.availabilityUnit;
// };
- const availability = `${storageRequest.availability} ${storageRequest.availabilityUnit}`;
+ const availability = storageRequest.availability;
return (
-
- Durability
-
-
-
-
-
-
-
-
- Define your durability profile
-
+
+
+
+
+ Define your Durability Profile
+
Select the appropriate level of data storage reliability ensuring
your information is protected and accessible.
-
+
-
- onDurabilityChange(0)}
- className={classnames(
- ["storageRequestReview-presets-bloc"],
- [
- "storageRequestReview-presets--selected",
- durability <= 0 || durability > 3,
- ]
- )}>
-
- 
+
+
+
+
+
+
+ Durability
+ Suggested Defaults
- Custom
onDurabilityChange(1)}
- className={classnames(
- ["storageRequestReview-presets-bloc"],
- ["storageRequestReview-presets--selected", durability === 1]
- )}>
-
- 
-
- Low
+ {...attributes({
+ "aria-selected": durability <= 0 || durability > 3,
+ })}
+ onClick={() => onDurabilityChange(0)}>
+ Custom
+
onDurabilityChange(2)}
- className={classnames(
- ["storageRequestReview-presets-bloc"],
- ["storageRequestReview-presets--selected", durability === 2]
- )}>
-
- 
-
- Medium
+ {...attributes({
+ "aria-selected": durability == 1,
+ })}
+ onClick={() => onDurabilityChange(1)}>
+ Low
+
onDurabilityChange(3)}
- className={classnames(
- ["storageRequestReview-presets-bloc"],
- ["storageRequestReview-presets--selected", durability === 3]
- )}>
-
- 
-
- High
+ {...attributes({
+ "aria-selected": durability == 2,
+ })}
+ onClick={() => onDurabilityChange(2)}>
+ Medium
+ Recommanded
+
+
+ onDurabilityChange(3)}>
+ High
+
-
- {/* */}
+
+
+
+
+
- Commitment
+
+
+ Commitment
+
-
+
+
+
+
+
-
+
+
+ Request Duration
+
-
-
- }
- title="Warning"
- variant="warning"
- className="storageRequestReview-alert">
- If no suitable hosts are found for the CID {storageRequest.cid}{" "}
- matching your storage requirements, you will incur a charge a small
- amount of tokens.
-
-
+
+
);
}
diff --git a/src/components/SuccessIcon/SuccessIcon.tsx b/src/components/SuccessIcon/SuccessIcon.tsx
index e53eab6..ea06811 100644
--- a/src/components/SuccessIcon/SuccessIcon.tsx
+++ b/src/components/SuccessIcon/SuccessIcon.tsx
@@ -1,10 +1,9 @@
-import { SimpleText } from "@codex-storage/marketplace-ui-components";
import { CircleCheck } from "lucide-react";
export function SuccessIcon() {
return (
-
+
-
+
);
}
diff --git a/src/components/UploadCard/UploadCard.tsx b/src/components/UploadCard/UploadCard.tsx
new file mode 100644
index 0000000..a437007
--- /dev/null
+++ b/src/components/UploadCard/UploadCard.tsx
@@ -0,0 +1,23 @@
+import { Upload } from "@codex-storage/marketplace-ui-components";
+import { CodexSdk } from "../../sdk/codex";
+import { useQueryClient } from "@tanstack/react-query";
+import UploadIcon from "../../assets/icons/upload.svg?react";
+
+export function UploadCard() {
+ const queryClient = useQueryClient();
+
+ const onSuccess = () => {
+ queryClient.invalidateQueries({ queryKey: ["cids"] });
+ };
+
+ return (
+
+ }
+ />
+
+ );
+}
diff --git a/src/components/UserInfo/UserInfo.css b/src/components/UserInfo/UserInfo.css
new file mode 100644
index 0000000..94238e5
--- /dev/null
+++ b/src/components/UserInfo/UserInfo.css
@@ -0,0 +1,47 @@
+.user-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+
+ @media (min-width: 1000px) {
+ & {
+ flex-direction: row;
+ gap: 32px;
+ align-items: center;
+ }
+ }
+
+ .emoji {
+ position: relative;
+
+ aside {
+ position: absolute;
+ top: -140px;
+ left: 0px;
+ z-index: 1;
+
+ @media (min-width: 1000px) {
+ & {
+ left: 116px;
+ }
+ }
+ }
+
+ .input input {
+ width: 64px;
+ text-align: center;
+ cursor: pointer;
+ }
+ }
+
+ .input input {
+ width: 100%;
+
+ @media (min-width: 1000px) {
+ & {
+ width: inherit;
+ }
+ }
+ }
+}
diff --git a/src/components/UserInfo/UserInfo.tsx b/src/components/UserInfo/UserInfo.tsx
new file mode 100644
index 0000000..6580da7
--- /dev/null
+++ b/src/components/UserInfo/UserInfo.tsx
@@ -0,0 +1,84 @@
+import { ChangeEvent, useState } from "react";
+import "./UserInfo.css";
+import { Input } from "@codex-storage/marketplace-ui-components";
+import EmojiPicker, {
+ EmojiClickData,
+ EmojiStyle,
+ Theme,
+} from "emoji-picker-react";
+import { WebStorage } from "../../utils/web-storage";
+
+type Props = {
+ onNameChange?: (value: string) => void;
+};
+
+export function UserInfo({ onNameChange }: Props) {
+ const [displayName, setDisplayName] = useState(
+ WebStorage.onBoarding.getDisplayName()
+ );
+ const [emoji, setEmoji] = useState(WebStorage.onBoarding.getEmoji());
+ const [areEmojiVisible, setAreEmojiVisible] = useState(false);
+
+ const onDisplayNameChange = (e: ChangeEvent ) => {
+ const value = e.currentTarget.value;
+ WebStorage.onBoarding.setDisplayName(value);
+ setDisplayName(value);
+ onNameChange?.(value);
+ };
+
+ const onDisplayEmoji = () => setAreEmojiVisible(!areEmojiVisible);
+
+ const onEmojiClick = (emojiData: EmojiClickData) => {
+ setEmoji(emojiData.emoji);
+ WebStorage.onBoarding.setEmoji(emojiData.emoji);
+ setAreEmojiVisible(false);
+ };
+
+ return (
+
+
+ {areEmojiVisible && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Versions/Versions.css b/src/components/Versions/Versions.css
new file mode 100644
index 0000000..4e43756
--- /dev/null
+++ b/src/components/Versions/Versions.css
@@ -0,0 +1,30 @@
+.versions {
+ display: flex;
+ gap: 32px;
+ align-items: flex-start;
+
+ > div {
+ width: 50px;
+ text-align: right;
+
+ p {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ color: #99a0ae;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 10px;
+ font-weight: 400;
+ line-height: 12.1px;
+ letter-spacing: 0.01em;
+ color: white;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+ }
+}
diff --git a/src/components/Versions/Versions.tsx b/src/components/Versions/Versions.tsx
new file mode 100644
index 0000000..23e0357
--- /dev/null
+++ b/src/components/Versions/Versions.tsx
@@ -0,0 +1,28 @@
+import { useDebug } from "../../hooks/useDebug";
+import "./Versions.css";
+import { VersionsUtil } from "./versions.utils";
+import AlphaIcon from "../../assets/icons/alpha.svg?react";
+import AlphaText from "../../assets/icons/alphatext.svg?react";
+
+const throwOnError = false;
+
+export function Versions() {
+ const debug = useDebug(throwOnError);
+
+ const version = VersionsUtil.clientVersion(debug.data?.codex.version);
+
+ return (
+
+
+
+ Client
+ VER. {version}
+
+
+ Vault
+ VER. {VersionsUtil.codexVersion()}
+
+
+
+ );
+}
diff --git a/src/components/Versions/versions.utils.test.ts b/src/components/Versions/versions.utils.test.ts
new file mode 100644
index 0000000..56a5115
--- /dev/null
+++ b/src/components/Versions/versions.utils.test.ts
@@ -0,0 +1,9 @@
+import { assert, describe, it } from "vitest";
+import { VersionsUtil } from "./versions.utils";
+
+describe("versions", () => {
+ it("gets the last client version", async () => {
+ const version = "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7"
+ assert.equal(VersionsUtil.clientVersion(version), "v0.1.7")
+ })
+})
\ No newline at end of file
diff --git a/src/components/Versions/versions.utils.ts b/src/components/Versions/versions.utils.ts
new file mode 100644
index 0000000..151ecc3
--- /dev/null
+++ b/src/components/Versions/versions.utils.ts
@@ -0,0 +1,8 @@
+export const VersionsUtil = {
+ codexVersion: () => import.meta.env.PACKAGE_VERSION,
+
+ clientVersion: (version: string | undefined) => {
+ const parts = version?.split("\n") || [""];
+ return parts[parts.length - 1];
+ }
+}
\ No newline at end of file
diff --git a/src/components/WalletLogin/WalletLogin.css b/src/components/WalletLogin/WalletLogin.css
new file mode 100644
index 0000000..4ac3e13
--- /dev/null
+++ b/src/components/WalletLogin/WalletLogin.css
@@ -0,0 +1,64 @@
+.wallet-login {
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ background-color: #252525;
+
+ & {
+ filter: grayscale(30);
+ transition: filter 0.5s;
+ }
+
+ &:hover {
+ filter: none;
+ }
+
+ div {
+ > p {
+ font-family: Inter;
+ font-size: 8px;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: #6e6e6e;
+ display: block;
+ font-size: 10px;
+ }
+ }
+
+ var {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 700;
+ color: #ffffff99;
+ font-style: normal;
+ font-size: 16px;
+ }
+
+ footer {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 32px;
+
+ p {
+ font-family: Inter;
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 10px;
+ text-align: left;
+ color: #3ee089;
+ font-style: normal;
+ }
+
+ a {
+ font-family: Inter;
+ font-size: 10px;
+ font-weight: 700;
+ line-height: 10px;
+ color: #6e6e6e;
+ cursor: pointer;
+ }
+ }
+}
diff --git a/src/components/WalletLogin/WalletLogin.tsx b/src/components/WalletLogin/WalletLogin.tsx
new file mode 100644
index 0000000..d622cef
--- /dev/null
+++ b/src/components/WalletLogin/WalletLogin.tsx
@@ -0,0 +1,20 @@
+import { Strings } from "../../utils/strings";
+import "./WalletLogin.css";
+
+export function WalletConnect() {
+ return (
+
+ 
+
+ Mainnet
+
+ {Strings.shortId("0x5B3D1D5D5C5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D")}
+
+
+
+
+ );
+}
diff --git a/src/components/Welcome/Welcome.css b/src/components/Welcome/Welcome.css
deleted file mode 100644
index 3530439..0000000
--- a/src/components/Welcome/Welcome.css
+++ /dev/null
@@ -1,35 +0,0 @@
-.welcome {
- border-radius: var(--codex-border-radius);
- border: 1px solid var(--codex-border-color);
- background-color: var(--codex-background-secondary);
- padding: 1rem 1.5rem;
- display: flex;
- align-items: flex-start;
- flex-direction: column;
- flex: 1;
-}
-
-.welcome-disclaimer {
- margin: 1rem 0;
-}
-
-.welcome-title {
- font-weight: bold;
- font-size: 1.125rem;
- line-height: 1.75rem;
- margin-bottom: 0.75rem;
-}
-
-.welcome-body {
- flex: 1;
-}
-
-.welcome-link {
- display: flex;
- align-items: center;
- color: var(--codex-color-primary);
-}
-
-.welcome-link:hover {
- text-decoration: underline;
-}
diff --git a/src/components/Welcome/Welcome.tsx b/src/components/Welcome/Welcome.tsx
deleted file mode 100644
index 0c31579..0000000
--- a/src/components/Welcome/Welcome.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { SimpleText } from "@codex-storage/marketplace-ui-components";
-import "./Welcome.css";
-import { Link } from "@tanstack/react-router";
-import { ChevronRight } from "lucide-react";
-
-export function Welcome() {
- return (
-
- Welcome to Codex Marketplace
-
-
- Begin your journey with Codex by uploading new files for testing.
- Experience the power of our decentralized data storage platform and
- explore its features. Your feedback is invaluable as we continue to
- improve!
-
-
-
-
- Explore more content
-
-
- );
-}
diff --git a/src/components/Welcome/WelcomeCard.css b/src/components/Welcome/WelcomeCard.css
new file mode 100644
index 0000000..baa2061
--- /dev/null
+++ b/src/components/Welcome/WelcomeCard.css
@@ -0,0 +1,72 @@
+.welcome-card {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 50%;
+ min-width: 420px;
+
+ .card {
+ position: relative;
+ overflow: hidden;
+ }
+
+ > div {
+ padding: 16px;
+ background-color: #141414;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ }
+
+ main {
+ max-width: 400px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ flex: 1;
+
+ h6 {
+ font-family: Inter;
+ font-size: 20px;
+ font-weight: 400;
+ line-height: 24.2px;
+ color: var(--codex-color-primary);
+ }
+
+ p {
+ margin-top: 16px;
+ margin-bottom: 32px;
+ font-family: Azeret Mono;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 14px;
+ text-align: left;
+ color: #7f948d;
+ }
+
+ div {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ a {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 19.36px;
+ letter-spacing: 0.01em;
+ text-align: left;
+ color: #7bfbaf;
+ display: flex;
+ align-items: center;
+
+ &:nth-child(2) {
+ gap: 12px;
+ }
+ }
+ }
+
+ footer {
+ margin-top: 32px;
+ }
+}
diff --git a/src/components/Welcome/WelcomeCard.tsx b/src/components/Welcome/WelcomeCard.tsx
new file mode 100644
index 0000000..77f6357
--- /dev/null
+++ b/src/components/Welcome/WelcomeCard.tsx
@@ -0,0 +1,67 @@
+import "./WelcomeCard.css";
+import { Link } from "@tanstack/react-router";
+import { ArrowRight } from "lucide-react";
+import { Alert } from "@codex-storage/marketplace-ui-components";
+import { useEffect, useRef, useState } from "react";
+import { classnames } from "../../utils/classnames";
+import Logotype from "../../assets/icons/logotype.svg?react";
+import Logo from "../../assets/icons/logo.svg?react";
+import DiscordIcon from "../../assets/icons/discord.svg?react";
+import WarningIcon from "../../assets/icons/warning.svg?react";
+import { WelcomeImage } from "./WelcomeImage";
+
+export function WelcomeCard() {
+ const ref = useRef(null);
+ const [clientWidth, setClientWidth] = useState(0);
+
+ useEffect(() => {
+ const onResize = () => {
+ setClientWidth(ref.current?.clientWidth || 0);
+ };
+
+ window.addEventListener("resize", onResize);
+
+ return () => {
+ window.removeEventListener("resize", onResize);
+ };
+ }, [setClientWidth]);
+
+ return (
+
+
+
+
+
+
+ Begin your journey with Codex by uploading new files for testing.
+
+
+ Experience the power of our decentralized data storage platform and
+ explore its features. Your feedback is invaluable as we continue to
+ improve!
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Welcome/WelcomeImage.css b/src/components/Welcome/WelcomeImage.css
new file mode 100644
index 0000000..61c036b
--- /dev/null
+++ b/src/components/Welcome/WelcomeImage.css
@@ -0,0 +1,14 @@
+.welcome-img {
+ position: absolute;
+ right: 0px;
+ top: 0px;
+ width: auto;
+ border-top-right-radius: 16px;
+ transition:
+ right 0.35s,
+ right 0.35s;
+
+ &.welcome-img--tiny {
+ right: -180px;
+ }
+}
diff --git a/src/components/Welcome/WelcomeImage.tsx b/src/components/Welcome/WelcomeImage.tsx
new file mode 100644
index 0000000..cfac8fb
--- /dev/null
+++ b/src/components/Welcome/WelcomeImage.tsx
@@ -0,0 +1,30 @@
+import { classnames } from "../../utils/classnames";
+import "./WelcomeImage.css";
+
+type Props = {
+ tiny: boolean;
+};
+
+export function WelcomeImage({ tiny }: Props) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/hooks/port-forwarding.util.ts b/src/hooks/port-forwarding.util.ts
new file mode 100644
index 0000000..6833ca9
--- /dev/null
+++ b/src/hooks/port-forwarding.util.ts
@@ -0,0 +1,27 @@
+import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js"
+
+export const PortForwardingUtil = {
+ check: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port)
+ .then((res) => res.json()),
+
+ getTcpPort(info: CodexDebugInfo): SafeValue {
+ if (info.addrs.length === 0) {
+ return { error: true, data: new CodexError("Not existing address") }
+ }
+
+ const parts = info.addrs[0].split("/")
+
+ if (parts.length < 2) {
+ return { error: true, data: new CodexError("Address misformated") }
+ }
+
+ const port = parseInt(parts[parts.length - 1], 10)
+
+ if (isNaN(port)) {
+ return { error: true, data: new CodexError("Port misformated") }
+ }
+
+ return { error: false, data: port }
+ }
+
+}
\ No newline at end of file
diff --git a/src/hooks/useCodexConnection.tsx b/src/hooks/useCodexConnection.tsx
new file mode 100644
index 0000000..57fb340
--- /dev/null
+++ b/src/hooks/useCodexConnection.tsx
@@ -0,0 +1,35 @@
+import { useQuery } from "@tanstack/react-query";
+import { CodexSdk } from "../sdk/codex";
+import { Promises } from "../utils/promises";
+
+const report = false;
+
+export function useCodexConnection() {
+ const { data, isError, isFetching, refetch } = useQuery({
+ queryKey: ["spr"],
+ queryFn: async () => {
+ return CodexSdk.node()
+ .spr()
+ .then((data) => Promises.rejectOnError(data, report));
+ },
+ refetchInterval: 5000,
+
+ // No need to retry because we defined a refetch interval
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ // Refreshing when focus returns can be useful if a user comes back
+ // to the UI after performing an operation in the terminal.
+ refetchOnWindowFocus: true,
+
+ // Cache is not useful for the spr endpoint
+ gcTime: 0,
+
+ throwOnError: false,
+ });
+
+ return { enabled: !isError && !!data, isFetching, refetch };
+}
diff --git a/src/hooks/useDebug.ts b/src/hooks/useDebug.ts
new file mode 100644
index 0000000..909335f
--- /dev/null
+++ b/src/hooks/useDebug.ts
@@ -0,0 +1,31 @@
+import { useQuery } from "@tanstack/react-query";
+import { CodexSdk } from "../sdk/codex";
+import { Promises } from "../utils/promises";
+
+export function useDebug(throwOnError: boolean) {
+ const { data, isError, isPending, refetch, isSuccess, isFetching } = useQuery({
+ queryFn: () =>
+ CodexSdk.debug()
+ .info()
+ .then((s) => Promises.rejectOnError(s)),
+
+ queryKey: ["debug"],
+
+ // No need to retry because if the connection to the node
+ // is back again, all the queries will be invalidated.
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ // Refreshing when focus returns can be useful if a user comes back
+ // to the UI after performing an operation in the terminal.
+ refetchOnWindowFocus: true,
+
+ // Throw the error to the error boundary
+ throwOnError,
+ });
+
+ return { data, isPending, isError, isSuccess, refetch, isFetching };
+}
diff --git a/src/hooks/usePersistence.tsx b/src/hooks/usePersistence.tsx
new file mode 100644
index 0000000..b66d68d
--- /dev/null
+++ b/src/hooks/usePersistence.tsx
@@ -0,0 +1,36 @@
+import { useQuery } from "@tanstack/react-query";
+import { CodexSdk } from "../sdk/codex";
+import { Promises } from "../utils/promises";
+
+const report = false;
+
+export function usePersistence(isCodexOnline: boolean) {
+ const { data, isError, isFetching, refetch } = useQuery({
+ queryKey: [],
+ queryFn: async () => {
+ return CodexSdk.marketplace()
+ .purchases()
+ .then((data) => Promises.rejectOnError(data, report));
+ },
+
+ refetchInterval: 5000,
+
+ // Enable only when the use has an internet connection
+ enabled: !!isCodexOnline,
+
+ // No need to retry because we defined a refetch interval
+ retry: false,
+
+ // The client node should be local, so display the cache value while
+ // making a background request looks good.
+ staleTime: 0,
+
+ // Refreshing when focus returns can be useful if a user comes back
+ // to the UI after performing an operation in the terminal.
+ refetchOnWindowFocus: true,
+
+ throwOnError: false,
+ });
+
+ return { enabled: !isError && !!data, isFetching, refetch };
+}
diff --git a/src/hooks/usePortForwarding.tsx b/src/hooks/usePortForwarding.tsx
new file mode 100644
index 0000000..f77beb1
--- /dev/null
+++ b/src/hooks/usePortForwarding.tsx
@@ -0,0 +1,42 @@
+import { useQuery } from "@tanstack/react-query";
+import { Errors } from "../utils/errors";
+import { CodexDebugInfo } from "@codex-storage/sdk-js";
+import { PortForwardingUtil } from "./port-forwarding.util";
+
+type PortForwardingResponse = { reachable: boolean };
+
+export function usePortForwarding(info: CodexDebugInfo | undefined) {
+ const { data, isFetching, refetch } = useQuery({
+ queryFn: (): Promise => {
+ const port = PortForwardingUtil.getTcpPort(info!);
+ if (port.error) {
+ Errors.report(port);
+ return Promise.resolve({ reachable: false });
+ } else {
+ return PortForwardingUtil.check(port.data).catch((e) =>
+ Errors.report(e)
+ );
+ }
+ },
+ queryKey: ["port-forwarding"],
+
+ initialData: { reachable: false },
+
+ // Enable only when the use has an internet connection
+ enabled: !!info,
+
+ // No need to retry because we provide a retry button
+ retry: false,
+
+ // The data should not be cached
+ staleTime: 0,
+
+ // The user may try to change the port forwarding and go back
+ // to the tab
+ refetchOnWindowFocus: true,
+
+ throwOnError: false,
+ });
+
+ return { enabled: data.reachable, isFetching, refetch };
+}
diff --git a/src/index.css b/src/index.css
index 82c61e2..a144b1c 100644
--- a/src/index.css
+++ b/src/index.css
@@ -2,31 +2,51 @@
@import url(./assets/css/indicator.css);
@import url(./assets/css/text.css);
+@font-face {
+ font-family: Inter;
+ font-weight: 300 800;
+ src: url(/fonts/Inter-VariableFont.ttf);
+}
+
+@font-face {
+ font-family: "Azeret Mono";
+ font-weight: 400 800;
+ src: url(/fonts/AzeretMono-VariableFont.ttf);
+}
+
:root {
- --codex-background: rgb(23 23 23);
- --codex-color: #e1e4d9;
+ --codex-background: #1c1c1c;
+ --codex-color: white;
--codex-color-contrast: #f8f8f8;
- --codex-color-error: 239, 68, 68;
+ --codex-color-error: 204, 108, 108;
+ --codex-color-error-hexa: #cc6c6c;
--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: #6fcb94;
--codex-color-primary-rgb: 193, 240, 164;
--codex-color-primary-variant: #c1f0a4cc;
--codex-color-on-primary: #333;
--codex-color-disabled: #717171;
--codex-color-light: rgb(150 150 150);
- --codex-border-color: rgb(82 82 82);
+ --codex-border-color: #96969633;
+ --codex-input-border-color: #494949;
--codex-background-secondary: rgb(38 38 38);
+ --codex-highlight-color: #2f2f2f;
--codex-background-light: rgb(64 64 64);
--codex-background-backdrop: rgba(70, 70, 70, 0.75);
--codex-border-radius: 0.5rem;
- --codex-font-size: 0.875rem;
+ --codex-font-size: 14px;
--codex-font-family: Inter, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans,
sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol,
Noto Color Emoji;
+ --codex-input-label-color: #7b7b7b;
+ --codex-input-border-color: #494949;
+ --codex-input-background: #232323;
+ --codex-input-color-error: #fb3748;
+ --codex-row-gap: 16px;
-webkit-tap-highlight-color: transparent;
-webkit-text-size-adjust: 100%;
@@ -39,7 +59,22 @@
font-size: var(--codex-font-size);
color-scheme: dark;
color: var(--codex-color);
- background-color: var(--codex-background);
+ background: #000000; /* Fallback color */
+ background: -webkit-linear-gradient(
+ 246.02deg,
+ #000000 30.36%,
+ #222222 91.05%
+ ); /* For Safari and older Chrome */
+ background: -moz-linear-gradient(
+ 246.02deg,
+ #000000 30.36%,
+ #222222 91.05%
+ ); /* For older Firefox */
+ background: linear-gradient(
+ 246.02deg,
+ #000000 30.36%,
+ #222222 91.05%
+ ); /* Standard syntax */
}
::selection {
@@ -95,23 +130,22 @@ ul {
padding: 0;
}
-main {
+body > main {
flex: 1;
display: flex;
flex-direction: column;
max-width: 100%;
}
-hr {
- border: 0.1px solid var(--codex-border-color);
- width: 100%;
-}
-
ul {
margin: 0;
padding: 0;
}
+dialog {
+ padding: 0;
+}
+
input,
button,
textarea,
@@ -124,9 +158,9 @@ pre {
word-break: break-word;
}
-a {
- text-decoration: inherit;
- color: var(--codex-color);
+a[aria-disabled] {
+ opacity: 0.6;
+ cursor: not-allowed;
}
.root {
@@ -135,6 +169,52 @@ a {
max-width: 100%;
}
-.page {
- max-width: 100%;
+input:-webkit-autofill,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:active {
+ -webkit-transition-delay: 9999s;
+ transition-delay: 9999s;
+}
+
+.row {
+ display: flex;
+ align-items: center;
+}
+
+.gap {
+ gap: var(--codex-row-gap);
+}
+
+.card {
+ border: 1px solid #96969633;
+ border-radius: 16px;
+ padding: 16px;
+ background-color: #232323;
+
+ > header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ margin-bottom: 16px;
+
+ svg {
+ color: #969696;
+ }
+
+ > div {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ h5 {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ }
+ }
}
diff --git a/src/main.tsx b/src/main.tsx
index 3e88972..0d335a0 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -25,6 +25,7 @@ if (import.meta.env.PROD && !import.meta.env.CI) {
Sentry.feedbackIntegration({
// Additional SDK configuration goes in here, for example:
colorScheme: "dark",
+ triggerLabel: "",
}),
],
// Tracing
@@ -40,6 +41,7 @@ if (import.meta.env.PROD && !import.meta.env.CI) {
// Create a new router instance
const router = createRouter({
routeTree,
+ defaultPreload: "viewport",
defaultNotFoundComponent: () => {
return (
void
- ): UploadResponse {
- // const url = CodexSdk.url() + "/api/codex/v1/data";
- // const xhr = new XMLHttpRequest();
-
- // const promise = new Promise>((resolve) => {
- // xhr.upload.onprogress = (evt) => {
- // if (evt.lengthComputable) {
- // onProgress?.(evt.loaded, evt.total);
- // }
- // };
-
- // xhr.open("POST", url, true);
- // xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + file.name + "\"")
- // xhr.send(file);
-
- // xhr.onload = function () {
- // if (xhr.status != 200) {
- // resolve({
- // error: true,
- // data: new CodexError(xhr.responseText, {
- // code: xhr.status,
- // }),
- // });
- // } else {
- // resolve({ error: false, data: xhr.response });
- // }
- // };
-
- // xhr.onerror = function () {
- // resolve({
- // error: true,
- // data: new CodexError("Something went wrong during the file upload."),
- // });
- // };
- // });
-
- // return {
- // result: promise,
- // abort: () => {
- // xhr.abort();
- // },
- // };
- const { result, abort } = super.upload(file, onProgress);
-
- return {
- abort,
- result: result.then((safe) => {
- if (!safe.error) {
- return FilesStorage.set(safe.data, {
- mimetype: file.type,
- name: file.name,
- uploadedAt: new Date().toJSON(),
- }).then(() => safe);
- }
-
- return safe;
- }),
- };
- }
-
- override async cids(): Promise> {
- const res = await super.cids();
-
- if (res.error) {
- return res;
- }
-
- const metadata = await FilesStorage.list();
-
- const content = res.data.content.map((content, index) => {
- if (content.manifest.filename) {
- return content;
- }
-
- const value = metadata.find(([cid]) => content.cid === cid);
-
- if (!value) {
- return {
- cid: content.cid,
- manifest: {
- ...content.manifest,
- mimetype: "N/A",
- uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(),
- filename: "N/A" + index,
- },
- };
- }
-
- const {
- mimetype = "",
- name = "",
- uploadedAt = new Date(0, 0, 0, 0, 0, 0).toJSON(),
- } = value[1];
-
- return {
- cid: content.cid,
- manifest: {
- ...content.manifest,
- mimetype,
- filename: name,
- uploadedAt: uploadedAt,
- },
- };
- });
-
- return { error: false, data: { content } };
- }
}
@@ -156,7 +44,7 @@ class CodexMarketplaceMock extends CodexMarketplace {
return res
}
- await PurchaseStorage.set("0x" + res.data, input.cid)
+ await WebStorage.purchases.set("0x" + res.data, input.cid)
// await PurchaseDatesStorage.set(res.data, new Date().toJSON())
@@ -217,6 +105,34 @@ class CodexMarketplaceMock extends CodexMarketplace {
// ],
// });
// }
+
+ // override reservations(): Promise> {
+ // return Promise.resolve({
+ // error: false,
+ // data: [
+ // {
+ // id: "0x123456789",
+ // availabilityId: "0x12345678910",
+ // requestId: "0x1234567891011",
+ // size: GB * 0.5 + "",
+ // slotIndex: "2",
+ // },
+ // {
+ // id: "0x987654321",
+ // availabilityId: "0x9876543210",
+ // requestId: "0x98765432100",
+ // /**
+ // * Size in bytes
+ // */
+ // size: GB * 0.25 + "",
+ // /**
+ // * Slot Index as hexadecimal string
+ // */
+ // slotIndex: "1",
+ // },
+ // ],
+ // });
+ // }
}
export const CodexSdk = {
@@ -224,3 +140,15 @@ export const CodexSdk = {
marketplace: () => new CodexMarketplaceMock(CodexSdk.url()),
data: () => new CodexDataMock(CodexSdk.url()),
};
+
+
+export const PortForwardingUtil = {
+ ...PUtil,
+ check: (port: number) => {
+ if (import.meta.env.CI) {
+ return Promise.resolve({ reachable: true })
+ }
+
+ return PUtil.check(port)
+ }
+}
\ No newline at end of file
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 3441411..d5991c6 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -11,77 +11,145 @@
// Import Routes
import { Route as rootRoute } from './routes/__root'
+import { Route as OnboardingNameImport } from './routes/onboarding-name'
+import { Route as OnboardingChecksImport } from './routes/onboarding-checks'
import { Route as DashboardImport } from './routes/dashboard'
import { Route as IndexImport } from './routes/index'
import { Route as DashboardIndexImport } from './routes/dashboard/index'
+import { Route as DashboardWalletImport } from './routes/dashboard/wallet'
import { Route as DashboardSettingsImport } from './routes/dashboard/settings'
import { Route as DashboardRequestsImport } from './routes/dashboard/requests'
import { Route as DashboardPurchasesImport } from './routes/dashboard/purchases'
import { Route as DashboardPeersImport } from './routes/dashboard/peers'
+import { Route as DashboardNodesImport } from './routes/dashboard/nodes'
+import { Route as DashboardLogsImport } from './routes/dashboard/logs'
import { Route as DashboardHelpImport } from './routes/dashboard/help'
+import { Route as DashboardFilesImport } from './routes/dashboard/files'
import { Route as DashboardFavoritesImport } from './routes/dashboard/favorites'
import { Route as DashboardDisclaimerImport } from './routes/dashboard/disclaimer'
+import { Route as DashboardDeviceImport } from './routes/dashboard/device'
import { Route as DashboardAvailabilitiesImport } from './routes/dashboard/availabilities'
+import { Route as DashboardAnalyticsImport } from './routes/dashboard/analytics'
import { Route as DashboardAboutImport } from './routes/dashboard/about'
// Create/Update Routes
+const OnboardingNameRoute = OnboardingNameImport.update({
+ id: '/onboarding-name',
+ path: '/onboarding-name',
+ getParentRoute: () => rootRoute,
+} as any)
+
+const OnboardingChecksRoute = OnboardingChecksImport.update({
+ id: '/onboarding-checks',
+ path: '/onboarding-checks',
+ getParentRoute: () => rootRoute,
+} as any)
+
const DashboardRoute = DashboardImport.update({
+ id: '/dashboard',
path: '/dashboard',
getParentRoute: () => rootRoute,
} as any)
const IndexRoute = IndexImport.update({
+ id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any)
const DashboardIndexRoute = DashboardIndexImport.update({
+ id: '/',
path: '/',
getParentRoute: () => DashboardRoute,
} as any)
+const DashboardWalletRoute = DashboardWalletImport.update({
+ id: '/wallet',
+ path: '/wallet',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
const DashboardSettingsRoute = DashboardSettingsImport.update({
+ id: '/settings',
path: '/settings',
getParentRoute: () => DashboardRoute,
} as any)
const DashboardRequestsRoute = DashboardRequestsImport.update({
+ id: '/requests',
path: '/requests',
getParentRoute: () => DashboardRoute,
} as any)
const DashboardPurchasesRoute = DashboardPurchasesImport.update({
+ id: '/purchases',
path: '/purchases',
getParentRoute: () => DashboardRoute,
} as any)
const DashboardPeersRoute = DashboardPeersImport.update({
+ id: '/peers',
path: '/peers',
getParentRoute: () => DashboardRoute,
} as any)
+const DashboardNodesRoute = DashboardNodesImport.update({
+ id: '/nodes',
+ path: '/nodes',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
+const DashboardLogsRoute = DashboardLogsImport.update({
+ id: '/logs',
+ path: '/logs',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
const DashboardHelpRoute = DashboardHelpImport.update({
+ id: '/help',
path: '/help',
getParentRoute: () => DashboardRoute,
} as any)
+const DashboardFilesRoute = DashboardFilesImport.update({
+ id: '/files',
+ path: '/files',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
const DashboardFavoritesRoute = DashboardFavoritesImport.update({
+ id: '/favorites',
path: '/favorites',
getParentRoute: () => DashboardRoute,
} as any)
const DashboardDisclaimerRoute = DashboardDisclaimerImport.update({
+ id: '/disclaimer',
path: '/disclaimer',
getParentRoute: () => DashboardRoute,
} as any)
+const DashboardDeviceRoute = DashboardDeviceImport.update({
+ id: '/device',
+ path: '/device',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
const DashboardAvailabilitiesRoute = DashboardAvailabilitiesImport.update({
+ id: '/availabilities',
path: '/availabilities',
getParentRoute: () => DashboardRoute,
} as any)
+const DashboardAnalyticsRoute = DashboardAnalyticsImport.update({
+ id: '/analytics',
+ path: '/analytics',
+ getParentRoute: () => DashboardRoute,
+} as any)
+
const DashboardAboutRoute = DashboardAboutImport.update({
+ id: '/about',
path: '/about',
getParentRoute: () => DashboardRoute,
} as any)
@@ -104,6 +172,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardImport
parentRoute: typeof rootRoute
}
+ '/onboarding-checks': {
+ id: '/onboarding-checks'
+ path: '/onboarding-checks'
+ fullPath: '/onboarding-checks'
+ preLoaderRoute: typeof OnboardingChecksImport
+ parentRoute: typeof rootRoute
+ }
+ '/onboarding-name': {
+ id: '/onboarding-name'
+ path: '/onboarding-name'
+ fullPath: '/onboarding-name'
+ preLoaderRoute: typeof OnboardingNameImport
+ parentRoute: typeof rootRoute
+ }
'/dashboard/about': {
id: '/dashboard/about'
path: '/about'
@@ -111,6 +193,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardAboutImport
parentRoute: typeof DashboardImport
}
+ '/dashboard/analytics': {
+ id: '/dashboard/analytics'
+ path: '/analytics'
+ fullPath: '/dashboard/analytics'
+ preLoaderRoute: typeof DashboardAnalyticsImport
+ parentRoute: typeof DashboardImport
+ }
'/dashboard/availabilities': {
id: '/dashboard/availabilities'
path: '/availabilities'
@@ -118,6 +207,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardAvailabilitiesImport
parentRoute: typeof DashboardImport
}
+ '/dashboard/device': {
+ id: '/dashboard/device'
+ path: '/device'
+ fullPath: '/dashboard/device'
+ preLoaderRoute: typeof DashboardDeviceImport
+ parentRoute: typeof DashboardImport
+ }
'/dashboard/disclaimer': {
id: '/dashboard/disclaimer'
path: '/disclaimer'
@@ -132,6 +228,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardFavoritesImport
parentRoute: typeof DashboardImport
}
+ '/dashboard/files': {
+ id: '/dashboard/files'
+ path: '/files'
+ fullPath: '/dashboard/files'
+ preLoaderRoute: typeof DashboardFilesImport
+ parentRoute: typeof DashboardImport
+ }
'/dashboard/help': {
id: '/dashboard/help'
path: '/help'
@@ -139,6 +242,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardHelpImport
parentRoute: typeof DashboardImport
}
+ '/dashboard/logs': {
+ id: '/dashboard/logs'
+ path: '/logs'
+ fullPath: '/dashboard/logs'
+ preLoaderRoute: typeof DashboardLogsImport
+ parentRoute: typeof DashboardImport
+ }
+ '/dashboard/nodes': {
+ id: '/dashboard/nodes'
+ path: '/nodes'
+ fullPath: '/dashboard/nodes'
+ preLoaderRoute: typeof DashboardNodesImport
+ parentRoute: typeof DashboardImport
+ }
'/dashboard/peers': {
id: '/dashboard/peers'
path: '/peers'
@@ -167,6 +284,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DashboardSettingsImport
parentRoute: typeof DashboardImport
}
+ '/dashboard/wallet': {
+ id: '/dashboard/wallet'
+ path: '/wallet'
+ fullPath: '/dashboard/wallet'
+ preLoaderRoute: typeof DashboardWalletImport
+ parentRoute: typeof DashboardImport
+ }
'/dashboard/': {
id: '/dashboard/'
path: '/'
@@ -181,27 +305,39 @@ declare module '@tanstack/react-router' {
interface DashboardRouteChildren {
DashboardAboutRoute: typeof DashboardAboutRoute
+ DashboardAnalyticsRoute: typeof DashboardAnalyticsRoute
DashboardAvailabilitiesRoute: typeof DashboardAvailabilitiesRoute
+ DashboardDeviceRoute: typeof DashboardDeviceRoute
DashboardDisclaimerRoute: typeof DashboardDisclaimerRoute
DashboardFavoritesRoute: typeof DashboardFavoritesRoute
+ DashboardFilesRoute: typeof DashboardFilesRoute
DashboardHelpRoute: typeof DashboardHelpRoute
+ DashboardLogsRoute: typeof DashboardLogsRoute
+ DashboardNodesRoute: typeof DashboardNodesRoute
DashboardPeersRoute: typeof DashboardPeersRoute
DashboardPurchasesRoute: typeof DashboardPurchasesRoute
DashboardRequestsRoute: typeof DashboardRequestsRoute
DashboardSettingsRoute: typeof DashboardSettingsRoute
+ DashboardWalletRoute: typeof DashboardWalletRoute
DashboardIndexRoute: typeof DashboardIndexRoute
}
const DashboardRouteChildren: DashboardRouteChildren = {
DashboardAboutRoute: DashboardAboutRoute,
+ DashboardAnalyticsRoute: DashboardAnalyticsRoute,
DashboardAvailabilitiesRoute: DashboardAvailabilitiesRoute,
+ DashboardDeviceRoute: DashboardDeviceRoute,
DashboardDisclaimerRoute: DashboardDisclaimerRoute,
DashboardFavoritesRoute: DashboardFavoritesRoute,
+ DashboardFilesRoute: DashboardFilesRoute,
DashboardHelpRoute: DashboardHelpRoute,
+ DashboardLogsRoute: DashboardLogsRoute,
+ DashboardNodesRoute: DashboardNodesRoute,
DashboardPeersRoute: DashboardPeersRoute,
DashboardPurchasesRoute: DashboardPurchasesRoute,
DashboardRequestsRoute: DashboardRequestsRoute,
DashboardSettingsRoute: DashboardSettingsRoute,
+ DashboardWalletRoute: DashboardWalletRoute,
DashboardIndexRoute: DashboardIndexRoute,
}
@@ -212,29 +348,45 @@ const DashboardRouteWithChildren = DashboardRoute._addFileChildren(
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
+ '/onboarding-checks': typeof OnboardingChecksRoute
+ '/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
+ '/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
+ '/dashboard/device': typeof DashboardDeviceRoute
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
'/dashboard/favorites': typeof DashboardFavoritesRoute
+ '/dashboard/files': typeof DashboardFilesRoute
'/dashboard/help': typeof DashboardHelpRoute
+ '/dashboard/logs': typeof DashboardLogsRoute
+ '/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
+ '/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard/': typeof DashboardIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/onboarding-checks': typeof OnboardingChecksRoute
+ '/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
+ '/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
+ '/dashboard/device': typeof DashboardDeviceRoute
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
'/dashboard/favorites': typeof DashboardFavoritesRoute
+ '/dashboard/files': typeof DashboardFilesRoute
'/dashboard/help': typeof DashboardHelpRoute
+ '/dashboard/logs': typeof DashboardLogsRoute
+ '/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
+ '/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard': typeof DashboardIndexRoute
}
@@ -242,15 +394,23 @@ export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/dashboard': typeof DashboardRouteWithChildren
+ '/onboarding-checks': typeof OnboardingChecksRoute
+ '/onboarding-name': typeof OnboardingNameRoute
'/dashboard/about': typeof DashboardAboutRoute
+ '/dashboard/analytics': typeof DashboardAnalyticsRoute
'/dashboard/availabilities': typeof DashboardAvailabilitiesRoute
+ '/dashboard/device': typeof DashboardDeviceRoute
'/dashboard/disclaimer': typeof DashboardDisclaimerRoute
'/dashboard/favorites': typeof DashboardFavoritesRoute
+ '/dashboard/files': typeof DashboardFilesRoute
'/dashboard/help': typeof DashboardHelpRoute
+ '/dashboard/logs': typeof DashboardLogsRoute
+ '/dashboard/nodes': typeof DashboardNodesRoute
'/dashboard/peers': typeof DashboardPeersRoute
'/dashboard/purchases': typeof DashboardPurchasesRoute
'/dashboard/requests': typeof DashboardRequestsRoute
'/dashboard/settings': typeof DashboardSettingsRoute
+ '/dashboard/wallet': typeof DashboardWalletRoute
'/dashboard/': typeof DashboardIndexRoute
}
@@ -259,42 +419,66 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/dashboard'
+ | '/onboarding-checks'
+ | '/onboarding-name'
| '/dashboard/about'
+ | '/dashboard/analytics'
| '/dashboard/availabilities'
+ | '/dashboard/device'
| '/dashboard/disclaimer'
| '/dashboard/favorites'
+ | '/dashboard/files'
| '/dashboard/help'
+ | '/dashboard/logs'
+ | '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
+ | '/dashboard/wallet'
| '/dashboard/'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
+ | '/onboarding-checks'
+ | '/onboarding-name'
| '/dashboard/about'
+ | '/dashboard/analytics'
| '/dashboard/availabilities'
+ | '/dashboard/device'
| '/dashboard/disclaimer'
| '/dashboard/favorites'
+ | '/dashboard/files'
| '/dashboard/help'
+ | '/dashboard/logs'
+ | '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
+ | '/dashboard/wallet'
| '/dashboard'
id:
| '__root__'
| '/'
| '/dashboard'
+ | '/onboarding-checks'
+ | '/onboarding-name'
| '/dashboard/about'
+ | '/dashboard/analytics'
| '/dashboard/availabilities'
+ | '/dashboard/device'
| '/dashboard/disclaimer'
| '/dashboard/favorites'
+ | '/dashboard/files'
| '/dashboard/help'
+ | '/dashboard/logs'
+ | '/dashboard/nodes'
| '/dashboard/peers'
| '/dashboard/purchases'
| '/dashboard/requests'
| '/dashboard/settings'
+ | '/dashboard/wallet'
| '/dashboard/'
fileRoutesById: FileRoutesById
}
@@ -302,11 +486,15 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
DashboardRoute: typeof DashboardRouteWithChildren
+ OnboardingChecksRoute: typeof OnboardingChecksRoute
+ OnboardingNameRoute: typeof OnboardingNameRoute
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
DashboardRoute: DashboardRouteWithChildren,
+ OnboardingChecksRoute: OnboardingChecksRoute,
+ OnboardingNameRoute: OnboardingNameRoute,
}
export const routeTree = rootRoute
@@ -322,7 +510,9 @@ export const routeTree = rootRoute
"filePath": "__root.tsx",
"children": [
"/",
- "/dashboard"
+ "/dashboard",
+ "/onboarding-checks",
+ "/onboarding-name"
]
},
"/": {
@@ -332,25 +522,45 @@ export const routeTree = rootRoute
"filePath": "dashboard.tsx",
"children": [
"/dashboard/about",
+ "/dashboard/analytics",
"/dashboard/availabilities",
+ "/dashboard/device",
"/dashboard/disclaimer",
"/dashboard/favorites",
+ "/dashboard/files",
"/dashboard/help",
+ "/dashboard/logs",
+ "/dashboard/nodes",
"/dashboard/peers",
"/dashboard/purchases",
"/dashboard/requests",
"/dashboard/settings",
+ "/dashboard/wallet",
"/dashboard/"
]
},
+ "/onboarding-checks": {
+ "filePath": "onboarding-checks.tsx"
+ },
+ "/onboarding-name": {
+ "filePath": "onboarding-name.tsx"
+ },
"/dashboard/about": {
"filePath": "dashboard/about.tsx",
"parent": "/dashboard"
},
+ "/dashboard/analytics": {
+ "filePath": "dashboard/analytics.tsx",
+ "parent": "/dashboard"
+ },
"/dashboard/availabilities": {
"filePath": "dashboard/availabilities.tsx",
"parent": "/dashboard"
},
+ "/dashboard/device": {
+ "filePath": "dashboard/device.tsx",
+ "parent": "/dashboard"
+ },
"/dashboard/disclaimer": {
"filePath": "dashboard/disclaimer.tsx",
"parent": "/dashboard"
@@ -359,10 +569,22 @@ export const routeTree = rootRoute
"filePath": "dashboard/favorites.tsx",
"parent": "/dashboard"
},
+ "/dashboard/files": {
+ "filePath": "dashboard/files.tsx",
+ "parent": "/dashboard"
+ },
"/dashboard/help": {
"filePath": "dashboard/help.tsx",
"parent": "/dashboard"
},
+ "/dashboard/logs": {
+ "filePath": "dashboard/logs.tsx",
+ "parent": "/dashboard"
+ },
+ "/dashboard/nodes": {
+ "filePath": "dashboard/nodes.tsx",
+ "parent": "/dashboard"
+ },
"/dashboard/peers": {
"filePath": "dashboard/peers.tsx",
"parent": "/dashboard"
@@ -379,6 +601,10 @@ export const routeTree = rootRoute
"filePath": "dashboard/settings.tsx",
"parent": "/dashboard"
},
+ "/dashboard/wallet": {
+ "filePath": "dashboard/wallet.tsx",
+ "parent": "/dashboard"
+ },
"/dashboard/": {
"filePath": "dashboard/index.tsx",
"parent": "/dashboard"
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index c9f3b8a..9e9864f 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,23 +1,15 @@
-import { createRootRoute, Outlet } from "@tanstack/react-router";
-import React from "react";
-
-const TanStackRouterDevtools = import.meta.env.PROD
- ? () => null // Render nothing in production
- : React.lazy(() =>
- // Lazy load in development
- import("@tanstack/router-devtools").then((res) => ({
- default: res.TanStackRouterDevtools,
- // For Embedded Mode
- // default: res.TanStackRouterDevtoolsPanel
- }))
- );
+import {
+ createRootRoute,
+ Outlet,
+ ScrollRestoration,
+} from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => {
return (
<>
+
-
>
);
},
diff --git a/src/routes/dashboard.css b/src/routes/dashboard.css
deleted file mode 100644
index b43ee4a..0000000
--- a/src/routes/dashboard.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.dashboard {
- padding: 1.5rem;
- display: grid;
- grid-template-columns: 1fr;
- gap: 0.75rem;
-}
-
-.dashboard-download {
- margin-top: 1rem;
-}
-
-.dashboard-welcome {
- display: flex;
- flex-direction: column;
-}
-
-.dashboard-alert {
- margin-bottom: 0;
-}
-
-@media (min-width: 1000px) {
- .dashboard {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-}
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx
index fea649d..8c47007 100644
--- a/src/routes/dashboard.tsx
+++ b/src/routes/dashboard.tsx
@@ -1,120 +1,39 @@
-import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
-import "./dashboard.css";
import {
- MenuItem,
- MenuItemComponentProps,
- Page,
-} from "@codex-storage/marketplace-ui-components";
-import {
- Home,
- ShoppingBag,
- Server,
- Settings,
- HelpCircle,
- TriangleAlert,
- Earth,
-} from "lucide-react";
-import { ICON_SIZE } from "../utils/constants";
-import { NodeIndicator } from "../components/NodeIndicator/NodeIndicator";
-import { HttpNetworkIndicator } from "../components/HttpNetworkIndicator/HttpNetworkIndicator";
+ createFileRoute,
+ Outlet,
+ ScrollRestoration,
+} from "@tanstack/react-router";
+import "./layout.css";
+import { Menu } from "../components/Menu/Menu";
+import { useState } from "react";
+import { AppBar } from "../components/AppBar/AppBar";
+import { Backdrop } from "@codex-storage/marketplace-ui-components";
const Layout = () => {
- const Right = (
- <>
-
-
- >
- );
+ const [hasMobileMenu, setHasMobileMenu] = useState(false);
- const items = [
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Dashboard
-
- ),
- },
- {
- type: "separator",
- },
- {
- type: "menu-title",
- title: "rent",
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Purchases
-
- ),
- },
- {
- type: "separator",
- },
- {
- type: "menu-title",
- title: "host",
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Sales
-
- ),
- },
- {
- type: "separator",
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
- Help
-
- ),
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Settings
-
- ),
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Peers
-
- ),
- },
- {
- type: "menu-item",
- Component: (p: MenuItemComponentProps) => (
-
-
- Disclaimer
-
- ),
- },
- ] satisfies MenuItem[];
+ const onIconClick = () => {
+ if (window.innerWidth <= 999) {
+ setHasMobileMenu(true);
+ }
+ };
+
+ const onClose = () => setHasMobileMenu(false);
return (
- }
- items={items}
- Right={Right}
- version={import.meta.env.PACKAGE_VERSION}
- />
+
);
};
diff --git a/src/routes/dashboard/about.tsx b/src/routes/dashboard/about.tsx
index ca4faa3..b18c86d 100644
--- a/src/routes/dashboard/about.tsx
+++ b/src/routes/dashboard/about.tsx
@@ -33,7 +33,7 @@ const About = () => {
-
+
{c.manifest.filename}
diff --git a/src/routes/dashboard/analytics.tsx b/src/routes/dashboard/analytics.tsx
new file mode 100644
index 0000000..ab7c726
--- /dev/null
+++ b/src/routes/dashboard/analytics.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/dashboard/analytics")({
+ component: () => Hello /dashboard/analytics! ,
+});
diff --git a/src/routes/dashboard/availabilities.css b/src/routes/dashboard/availabilities.css
index e65ce40..5c5f636 100644
--- a/src/routes/dashboard/availabilities.css
+++ b/src/routes/dashboard/availabilities.css
@@ -1,5 +1,117 @@
.availabilities {
height: 100%;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+
+ dialog {
+ width: 80%;
+ }
+
+ > .card {
+ flex: 1 1 50%;
+ }
+
+ .table {
+ table thead tr th {
+ background-color: #14141499;
+ }
+
+ table tbody tr.availabilty-row {
+ td {
+ background-color: #292929;
+ padding: 6px 12px;
+
+ &:first-child {
+ cursor: pointer;
+ transition: transform 0.35s;
+
+ & svg {
+ transition: transform 0.35s;
+ }
+
+ & svg[aria-expanded] {
+ transform: rotate(-90deg);
+ }
+ }
+ }
+ }
+
+ td {
+ b {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 700;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
+ display: block;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ text-align: left;
+ color: #ffffffcc;
+ }
+ }
+ }
+
+ aside {
+ display: flex;
+ width: 400px;
+ flex: 1 1 30%;
+
+ .card {
+ flex: 1;
+ }
+
+ main {
+ > div {
+ position: relative;
+ }
+
+ > .button {
+ width: 100%;
+ gap: 4px;
+ }
+ }
+
+ .node-space {
+ border-bottom: 1px solid #96969633;
+ padding-bottom: 16px;
+
+ h6 {
+ border-top: none;
+ }
+ }
+
+ footer {
+ padding-top: 16px;
+
+ b {
+ display: block;
+ font-family: Inter;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.015em;
+ text-align: left;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 20px;
+ letter-spacing: -0.006em;
+ text-align: left;
+ color: #969696cc;
+ }
+ }
+ }
}
.availabilities-actions {
@@ -14,24 +126,8 @@
display: block;
}
-.availabilities-create {
- position: absolute;
- margin: auto;
- border-radius: 100%;
- height: 6rem;
- width: 6rem;
-}
-
-.availabilities-create .button-label {
- display: none;
-}
-
.availabilities-header {
position: relative;
- display: flex;
- place-items: center;
- justify-content: center;
- margin-bottom: 2rem;
}
.availabilities-content {
diff --git a/src/routes/dashboard/availabilities.tsx b/src/routes/dashboard/availabilities.tsx
index fe0a012..dc22f94 100644
--- a/src/routes/dashboard/availabilities.tsx
+++ b/src/routes/dashboard/availabilities.tsx
@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import {
+ Button,
SpaceAllocationItem,
Spinner,
} from "@codex-storage/marketplace-ui-components";
@@ -13,11 +14,15 @@ import { AvailabilitiesTable } from "../../components/Availability/Availabilitie
import { AvailabilityEdit } from "../../components/Availability/AvailabilityEdit";
import { Strings } from "../../utils/strings";
import { PrettyBytes } from "../../utils/bytes";
-import { AvailabilitySunburst } from "../../components/Availability/AvailabilitySunburst";
+import { Sunburst } from "../../components/Availability/Sunburst";
import { Errors } from "../../utils/errors";
import { availabilityColors } from "../../components/Availability/availability.colors";
-import { AvailabilityStorage } from "../../utils/availabilities-storage";
import { AvailabilityWithSlots } from "../../components/Availability/types";
+import { WebStorage } from "../../utils/web-storage";
+import { NodeSpace } from "../../components/NodeSpace/NodeSpace";
+import PlusIcon from "../../assets/icons/plus-circle.svg?react";
+import UploadIcon from "../../assets/icons/upload.svg?react";
+import { AvailabilityUtils } from "../../components/Availability/availability.utils";
const defaultSpace = {
quotaMaxBytes: 0,
@@ -51,7 +56,7 @@ export function Availabilities() {
return { ...a, slots: res.data };
})
.then((data) =>
- AvailabilityStorage.get(data.id).then((n) => ({
+ WebStorage.availabilities.get(data.id).then((n) => ({
...data,
name: n || "",
}))
@@ -117,43 +122,63 @@ export function Availabilities() {
allocation.push({
title: "Space remaining",
- // TODO move this to domain
- size:
- space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes,
+ size: AvailabilityUtils.maxValue(space),
color: "transparent",
});
- return (
-
-
- {isPending ? (
-
-
-
- ) : (
- <>
-
-
-
- >
- )}
+ if (isPending) {
+ return (
+
+
+ );
+ }
+
+ const onOpenAvailabilities = () =>
+ document.dispatchEvent(new CustomEvent("codexavailabilitycreate", {}));
+
+ return (
+
+
+
);
}
diff --git a/src/routes/dashboard/device.tsx b/src/routes/dashboard/device.tsx
new file mode 100644
index 0000000..049ff50
--- /dev/null
+++ b/src/routes/dashboard/device.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/dashboard/device")({
+ component: () => Hello /dashboard/device! ,
+});
diff --git a/src/routes/dashboard/favorites.tsx b/src/routes/dashboard/favorites.tsx
index 7b9dd9c..c96d39f 100644
--- a/src/routes/dashboard/favorites.tsx
+++ b/src/routes/dashboard/favorites.tsx
@@ -2,19 +2,16 @@ import { createFileRoute } from "@tanstack/react-router";
import { Files } from "../../components/Files/Files";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
-import { Card } from "@codex-storage/marketplace-ui-components";
export const Route = createFileRoute("/dashboard/favorites")({
component: () => (
<>
(
-
-
-
+
)}>
diff --git a/src/routes/dashboard/files.css b/src/routes/dashboard/files.css
new file mode 100644
index 0000000..ea89917
--- /dev/null
+++ b/src/routes/dashboard/files.css
@@ -0,0 +1,26 @@
+.files-page {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+
+ > .card {
+ margin-bottom: 16px;
+
+ &:first-child {
+ flex: 1 1 70%;
+ }
+ }
+
+ aside {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ flex: 1 1 auto;
+
+ .card:first-child {
+ display: flex;
+ flex-direction: column;
+ /* flex: 1 1 67%; */
+ }
+ }
+}
diff --git a/src/routes/dashboard/files.tsx b/src/routes/dashboard/files.tsx
new file mode 100644
index 0000000..39092ba
--- /dev/null
+++ b/src/routes/dashboard/files.tsx
@@ -0,0 +1,37 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { Files } from "../../components/Files/Files";
+import "./files.css";
+import { UploadCard } from "../../components/UploadCard/UploadCard";
+import { Download } from "../../components/Download/Download";
+import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch";
+import UploadIcon from "../../assets/icons/upload.svg?react";
+import DownloadIcon from "../../assets/icons/download.svg?react";
+import FetchIcon from "../../assets/icons/fetch.svg?react";
+import { Card } from "../../components/Card/Card";
+import FilesIconOutline from "../../assets/icons/files-outline.svg?react";
+
+export const Route = createFileRoute("/dashboard/files")({
+ component: () => (
+
+ }
+ title="Files">
+
+
+
+
+
+ ),
+});
diff --git a/src/routes/dashboard/index.css b/src/routes/dashboard/index.css
new file mode 100644
index 0000000..af9341a
--- /dev/null
+++ b/src/routes/dashboard/index.css
@@ -0,0 +1,96 @@
+.dashboard {
+ .card--main {
+ flex: 1 1 60%;
+
+ &:first-child {
+ filter: grayscale(30);
+ transition: filter 0.5s;
+
+ &:hover {
+ filter: none;
+ }
+ }
+
+ @media (min-width: 2000px) {
+ &:nth-child(n + 1) {
+ flex: 1 1 34%;
+ }
+
+ &:first-child {
+ flex: 1 1 20%;
+ }
+
+ &.card--main--files {
+ flex: 1 1 62%;
+ }
+ }
+ }
+
+ header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 16px;
+
+ h3 {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 700;
+ line-height: 14.52px;
+ letter-spacing: 0.01em;
+ color: #969696cc;
+ text-transform: uppercase;
+ }
+
+ h4 {
+ font-family: Inter;
+ font-size: 32px;
+ font-weight: 400;
+ line-height: 38.73px;
+ letter-spacing: 0.01em;
+ color: white;
+ }
+
+ .emoji {
+ border-radius: 50%;
+ width: 52px;
+ height: 52px;
+ background-color: #4a9a73;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 26px;
+ }
+ }
+
+ > main {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ .column {
+ min-width: 350px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ flex: 1 1 30%;
+
+ .card {
+ flex: 1;
+ }
+ }
+
+ @media (min-width: 2000px) {
+ .column:nth-child(2) {
+ flex: 1 1 15%;
+ }
+
+ .column {
+ flex: 1 1 25%;
+ }
+ }
+
+ .files {
+ flex: 1;
+ flex-basis: 66%;
+ }
+ }
+}
diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx
index 931438f..8a90cb1 100644
--- a/src/routes/dashboard/index.tsx
+++ b/src/routes/dashboard/index.tsx
@@ -1,91 +1,94 @@
import { createFileRoute } from "@tanstack/react-router";
import { Files } from "../../components/Files/Files.tsx";
-import { Alert, Card, Upload } from "@codex-storage/marketplace-ui-components";
-import { CodexSdk } from "../../sdk/codex";
-import { Welcome } from "../../components/Welcome/Welcome.tsx";
-import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder.tsx";
-import { ErrorBoundary } from "@sentry/react";
-import { useQueryClient } from "@tanstack/react-query";
+import { WelcomeCard } from "../../components/Welcome/WelcomeCard.tsx";
import { Download } from "../../components/Download/Download.tsx";
+import "./index.css";
+import { Versions } from "../../components/Versions/Versions.tsx";
+import { WebStorage } from "../../utils/web-storage.ts";
+import { ConnectedAccount } from "../../components/ConnectedAccount/ConnectedAccount.tsx";
+import { NodeSpace } from "../../components/NodeSpace/NodeSpace.tsx";
+import { UploadCard } from "../../components/UploadCard/UploadCard.tsx";
+import { PeersCard } from "../../components/Peers/PeersCard.tsx";
+import { Card } from "../../components/Card/Card.tsx";
+import NodesIcon from "../../assets/icons/nodes.svg?react";
+import WalletIcon from "../../assets/icons/wallet.svg?react";
+import PlusIcon from "../../assets/icons/plus.svg?react";
+import PeersIcon from "../../assets/icons/peers.svg?react";
+import UploadIcon from "../../assets/icons/upload.svg?react";
+import DownloadIcon from "../../assets/icons/download.svg?react";
+import FetchIcon from "../../assets/icons/fetch.svg?react";
+import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch.tsx";
+import FilesIconOutline from "../../assets/icons/files-outline.svg?react";
export const Route = createFileRoute("/dashboard/")({
- component: About,
+ component: Dashboard,
});
-function About() {
- const queryClient = useQueryClient();
+function Dashboard() {
+ const username = WebStorage.onBoarding.getDisplayName();
- const onSuccess = () => {
- queryClient.invalidateQueries({ queryKey: ["cids"] });
- };
+ const emoji = WebStorage.onBoarding.getEmoji();
return (
- <>
-
-
- (
-
- )}>
-
-
-
-
+
+
+
+ }
+ className="card--main"
+ title="Connected Account"
+ buttonLabel="Add Wallet"
+ buttonIcon={() => }>
+
+
- (
-
- )}>
-
-
-
-
+
+ }
+ title="Storage"
+ buttonLabel="Details">
+
+
+ }
+ title="Peers"
+ buttonLabel="Details">
+
+
- (
-
- )}>
-
-
+
-
- The website and the content herein is not intended for public use
- and is for informational and demonstration purposes only.
-
-
-
-
+
+ } title="Upload">
+
+
-
- (
-
-
-
- )}>
-
-
-
- >
+ } title="Download">
+
+
+
+ } title="Fetch manifest">
+
+
+
+
+ }
+ className="card--main card--main--files"
+ title="Files">
+
+
+
+
);
}
diff --git a/src/routes/dashboard/logs.css b/src/routes/dashboard/logs.css
new file mode 100644
index 0000000..75107a5
--- /dev/null
+++ b/src/routes/dashboard/logs.css
@@ -0,0 +1,48 @@
+.logs-card {
+ display: flex;
+ justify-content: space-between;
+ border: 1px solid #96969633;
+ border-radius: 16px;
+ margin-bottom: 16px;
+
+ > div:first-child {
+ padding: 16px;
+ }
+
+ h5 {
+ font-family: Inter;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.015em;
+ text-align: left;
+ }
+
+ small {
+ font-family: Inter;
+ font-size: 12px;
+ font-weight: 400;
+ line-height: 16px;
+ text-align: left;
+ color: #969696cc;
+ }
+
+ .button {
+ width: 187px;
+ gap: 8px;
+ }
+}
+
+.logs {
+ .node {
+ pre {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ text-align: left;
+ color: #757575;
+ }
+ }
+}
diff --git a/src/routes/dashboard/logs.tsx b/src/routes/dashboard/logs.tsx
new file mode 100644
index 0000000..ec5e5f6
--- /dev/null
+++ b/src/routes/dashboard/logs.tsx
@@ -0,0 +1,45 @@
+import { createFileRoute } from "@tanstack/react-router";
+import "./logs.css";
+import { RequireAssitance } from "../../components/RequireAssitance/RequireAssitance";
+import { LogLevel } from "../../components/LogLevel/LogLevel";
+import { useDebug } from "../../hooks/useDebug";
+import LogsIcon from "../../assets/icons/logs.svg?react";
+
+const throwOnError = false;
+
+const Logs = () => {
+ const { data } = useDebug(throwOnError);
+
+ const { table, ...rest } = data ?? {};
+
+ return (
+
+
+
+ Log level
+
+ Manage the type of logs being displayed on your CLI for Codex Node.
+
+
+
+
+
+
+
+
+
+ {JSON.stringify(rest, null, 2)}
+
+
+
+ );
+};
+
+export const Route = createFileRoute("/dashboard/logs")({
+ component: Logs,
+});
diff --git a/src/routes/dashboard/nodes.tsx b/src/routes/dashboard/nodes.tsx
new file mode 100644
index 0000000..c8d1738
--- /dev/null
+++ b/src/routes/dashboard/nodes.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/dashboard/nodes')({
+ component: () => Hello /nodes! ,
+})
diff --git a/src/routes/dashboard/peers.css b/src/routes/dashboard/peers.css
deleted file mode 100644
index 7f308ee..0000000
--- a/src/routes/dashboard/peers.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.peers-map {
- max-width: 1000px;
- width: 100%;
-}
-
-.peers-table {
- margin-top: 1rem;
- width: calc(100% - 4rem);
- max-width: calc(1000px - 4rem);
-}
-
-.peers {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding-bottom: 4rem;
- padding-left: 2rem;
- padding-right: 2rem;
-}
-
-.peers circle[fill="#d6ff79"] {
- /* fill: yellow; */
- animation: dash 3s linear infinite;
- stroke: white;
- stroke-width: 0.6px;
- stroke-dasharray: 0.3;
-}
-
-@keyframes dash {
- from {
- stroke-dashoffset: 2;
- }
- to {
- stroke-dashoffset: 0;
- }
-}
-@keyframes circleAn {
- to {
- /* stroke-dashoffset: 100px; */
- }
-}
diff --git a/src/routes/dashboard/peers.tsx b/src/routes/dashboard/peers.tsx
index 491e867..a9c8827 100644
--- a/src/routes/dashboard/peers.tsx
+++ b/src/routes/dashboard/peers.tsx
@@ -1,107 +1,7 @@
-import { Cell, Row, Table } from "@codex-storage/marketplace-ui-components";
-import { createFileRoute } from "@tanstack/react-router";
-import { getMapJSON } from "dotted-map";
-import DottedMap from "dotted-map/without-countries";
-import { Promises } from "../../utils/promises";
-import { useQuery } from "@tanstack/react-query";
-import { PeerCountryCell } from "../../components/Peers/PeerCountryCell";
-import { useCallback, useState } from "react";
-import { PeerPin } from "../../components/Peers/types";
-import "./peers.css";
-import { CodexSdk } from "../../sdk/codex";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
-
-// This function accepts the same arguments as DottedMap in the example above.
-const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" });
-
-const Peers = () => {
- const [pins, setPins] = useState<[PeerPin, number][]>([]);
- const { data } = useQuery({
- queryFn: () =>
- CodexSdk.debug()
- .info()
- .then((s) => Promises.rejectOnError(s)),
- queryKey: ["debug"],
-
- // No need to retry because if the connection to the node
- // is back again, all the queries will be invalidated.
- retry: false,
-
- // The client node should be local, so display the cache value while
- // making a background request looks good.
- staleTime: 0,
-
- // Refreshing when focus returns can be useful if a user comes back
- // to the UI after performing an operation in the terminal.
- refetchOnWindowFocus: true,
-
- // Throw the error to the error boundary
- throwOnError: true,
- });
-
- const onPinAdd = useCallback((pin: PeerPin) => {
- setPins((val) => {
- const [, quantity = 0] =
- val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || [];
- return [...val, [pin, quantity + 1]];
- });
- }, []);
-
- // It’s safe to re-create the map at each render, because of the
- // pre-computation it’s super fast ⚡️
- const map = new DottedMap({ map: JSON.parse(mapJsonString) });
-
- pins.map(([pin, quantity]) =>
- map.addPin({
- lat: pin.lat,
- lng: pin.lng,
- svgOptions: { color: "#d6ff79", radius: 0.8 * quantity },
- })
- );
-
- const svgMap = map.getSVG({
- radius: 0.42,
- color: "#423B38",
- shape: "circle",
- backgroundColor: "#020300",
- });
-
- const headers = ["Country", "PeerId", "Active"];
-
- const rows =
- (data?.table?.nodes || []).map((node) => (
- ,
- | {node.peerId} | ,
-
- {node.seen ? (
-
- ) : (
-
- )}
- | ,
- ]}>
- )) || [];
-
- return (
-
- {/* }`}) */}
-
-
-
-
-
- );
-};
+import { createFileRoute } from "@tanstack/react-router";
+import { Peers } from "../../components/Peers/Peers";
export const Route = createFileRoute("/dashboard/peers")({
component: () => (
diff --git a/src/routes/dashboard/purchases.css b/src/routes/dashboard/purchases.css
index df90d3a..afb1ae9 100644
--- a/src/routes/dashboard/purchases.css
+++ b/src/routes/dashboard/purchases.css
@@ -1,12 +1,16 @@
-.purchases-modal {
- margin: auto;
-}
+.purchases {
+ > div:first-child {
+ padding: 1rem 0;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ }
-.purchases-actions {
- padding: 1rem;
- display: flex;
- align-items: center;
- justify-content: flex-end;
+ .table {
+ table thead tr th {
+ background-color: #14141499;
+ }
+ }
}
.purchases-loader {
diff --git a/src/routes/dashboard/purchases.tsx b/src/routes/dashboard/purchases.tsx
index 04eb9eb..dbf0039 100644
--- a/src/routes/dashboard/purchases.tsx
+++ b/src/routes/dashboard/purchases.tsx
@@ -1,97 +1,20 @@
-import { useQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router";
-import { CodexSdk } from "../../sdk/codex";
-import {
- Cell,
- Row,
- Spinner,
- Table,
-} from "@codex-storage/marketplace-ui-components";
import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate";
import "./purchases.css";
-import { FileCell } from "../../components/FileCellRender/FileCell";
-import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender";
-import { Promises } from "../../utils/promises";
-import { TruncateCell } from "../../components/TruncateCell/TruncateCell";
-import { Times } from "../../utils/times";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
import { ErrorBoundary } from "@sentry/react";
+import { PurchasesTable } from "../../components/Purchase/PurchasesTable";
const Purchases = () => {
- const { data, isPending } = useQuery({
- queryFn: () =>
- CodexSdk.marketplace()
- .purchases()
- .then((s) => Promises.rejectOnError(s)),
- queryKey: ["purchases"],
-
- // No need to retry because if the connection to the node
- // is back again, all the queries will be invalidated.
- retry: false,
-
- // The client node should be local, so display the cache value while
- // making a background request looks good.
- staleTime: 0,
-
- // Refreshing when focus returns can be useful if a user comes back
- // to the UI after performing an operation in the terminal.
- refetchOnWindowFocus: true,
-
- initialData: [],
-
- // Throw the error to the error boundary
- throwOnError: true,
- });
-
- if (isPending) {
- return (
-
-
-
- );
- }
-
- const headers = [
- "file",
- "request id",
- "duration",
- "slots",
- "reward",
- "proof probability",
- "state",
- ];
-
- const rows = data.map((p, index) => {
- const r = p.request;
- const ask = p.request.ask;
- const duration = parseInt(p.request.ask.duration, 10);
- const pf = parseInt(p.request.ask.proofProbability, 10);
-
- return (
- |
,
- ,
- {Times.pretty(duration)} | ,
- {ask.slots.toString()} | ,
- {ask.reward + " CDX"} | ,
- {pf.toString()} | ,
- ,
- ]}>
- );
- });
-
return (
-
-
+
);
};
diff --git a/src/routes/dashboard/settings.css b/src/routes/dashboard/settings.css
index fd82edf..c75ccc5 100644
--- a/src/routes/dashboard/settings.css
+++ b/src/routes/dashboard/settings.css
@@ -1,11 +1,47 @@
.settings {
- 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;
-}
+ header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ main {
+ margin-top: 16px;
+ padding: 16px;
+ max-width: 800px;
+ z-index: 1;
+ position: relative;
+
+ h3 {
+ font-family: Inter;
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.015em;
+ margin-bottom: 16px;
+ }
+
+ .user-info {
+ padding-bottom: 16px;
+ margin-bottom: 16px;
+ border-bottom: 1px solid #96969633;
+ }
+ }
+
+ .background-img {
+ top: 200px;
+ right: -400px;
+ }
+
+ .refresh {
+ top: 17px;
+ }
+
+ .address svg {
+ top: 55px;
+ }
+}
+/*
.settings-title {
font-weight: bold;
font-size: 1.125rem;
@@ -19,4 +55,4 @@
.settings-debug-loader {
margin: auto;
-}
+} */
diff --git a/src/routes/dashboard/settings.tsx b/src/routes/dashboard/settings.tsx
index e46deee..182fe10 100644
--- a/src/routes/dashboard/settings.tsx
+++ b/src/routes/dashboard/settings.tsx
@@ -1,16 +1,26 @@
import { createFileRoute } from "@tanstack/react-router";
import "./settings.css";
-import { LogLevel } from "../../components/LogLevel/LogLevel";
-import { Debug } from "../../components/Debug/Debug";
-import { CodexUrlSettings } from "../../components/CodexUrllSettings/CodexUrlSettings";
import { ErrorBoundary } from "@sentry/react";
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
-import { useEffect } from "react";
+import { UserInfo } from "../../components/UserInfo/UserInfo";
+import { HealthChecks } from "../../components/HealthChecks/HealthChecks";
+import Logotype from "../../assets/icons/logotype.svg?react";
+import Logo from "../../assets/icons/logo.svg?react";
+import { Versions } from "../../components/Versions/Versions";
+import { BackgroundImage } from "../../components/BackgroundImage/BackgroundImage";
export const Route = createFileRoute("/dashboard/settings")({
component: () => (
- <>
-
+
+
+
+ Personalization
(
)}>
-
+
-
-
+ Connection
+
+ (
+
+ )}>
+ {}} />
+
+
+
+
+
+ {/*
(
-
-
- {/*
-
-
-
-
-
-
-
- */}
- >
+ */}
+
),
});
diff --git a/src/routes/dashboard/wallet.tsx b/src/routes/dashboard/wallet.tsx
new file mode 100644
index 0000000..eee5841
--- /dev/null
+++ b/src/routes/dashboard/wallet.tsx
@@ -0,0 +1,5 @@
+import { createFileRoute } from "@tanstack/react-router";
+
+export const Route = createFileRoute("/dashboard/wallet")({
+ component: () => Hello /dashboard/wallet! ,
+});
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index c59bca3..7f65845 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,20 +1,98 @@
-import { createFileRoute, Link, redirect } from "@tanstack/react-router";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useState } from "react";
+import { Modal } from "@codex-storage/marketplace-ui-components";
+import { ArrowRight } from "lucide-react";
+import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
+import AlphaIcon from "../assets/icons/alpha.svg?react";
+import AlphaText from "../assets/icons/alphatext.svg?react";
+import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react";
export const Route = createFileRoute("/")({
component: Index,
beforeLoad: async () => {
- throw redirect({
- to: "/dashboard",
- });
+ // throw redirect({
+ // to: "/dashboard",
+ // });
},
});
function Index() {
- return (
-
- Welcome Home!
+ const [modal, setModal] = useState(false);
+ const navigate = useNavigate({ from: "/" });
- Go to dashboard
-
+ const onLegalDisclaimerOpen = () => setModal(true);
+
+ const onLegalDisclaimerClose = () => setModal(false);
+
+ const onNextStep = () => navigate({ to: "/onboarding-name" });
+
+ return (
+ <>
+
+ <>
+
+
+
+ Hello,
+ Welcome to Codex Vault
+
+
+ Codex is a durable, decentralised data storage protocol, created
+ so the world community can preserve its most important knowledge
+ without risk of censorship.
+
+
+
+
+ Let’s get started
+
+
+
+ Disclaimer
+
+
+ The website and the content herein is not intended for public
+ use and is for informational and demonstration purposes only.
+
+
+
+
+
+ The website and any associated functionalities are provided on
+ an “as is” basis without any guarantees, warranties, or
+ representations of any kind, either express or implied. The
+ website and any associated functionalities may not reflect the
+ final version of the project and is subject to changes, updates,
+ or removal at any time and without notice.
+
+
+
+
+
+ By accessing and using this website, you agree that we, Logos
+ Collective Association and its affiliates, will not be liable
+ for any direct, indirect, incidental, or consequential damages
+ arising from the use of, or inability to use, this website. Any
+ data, content, or interactions on this site are non-binding and
+ should not be considered final or actionable. Your use of this
+ website is at your sole risk.
+
+
+
+
+
+
+ >
+
+ >
);
}
diff --git a/src/routes/layout.css b/src/routes/layout.css
new file mode 100644
index 0000000..16ec77c
--- /dev/null
+++ b/src/routes/layout.css
@@ -0,0 +1,65 @@
+.layout {
+ display: flex;
+ flex: 1;
+ max-width: 100%;
+
+ > main {
+ flex: 1;
+ background-color: #141414;
+
+ > div {
+ padding: 16px;
+
+ @media (min-width: 1000px) {
+ padding: 24px 48px;
+ }
+ }
+ }
+}
+
+.dashboard-download {
+ margin-top: 1rem;
+}
+
+.dashboard-fetch {
+ margin-top: 1rem;
+}
+
+.dashboard-welcome {
+ display: flex;
+ flex-direction: column;
+}
+
+.dashboard-alert {
+ margin-bottom: 0;
+}
+
+.dashboard-welcome-versions {
+ display: flex;
+ gap: 32px;
+}
+
+.dashboard-welcome-versionContainer {
+ width: 50px;
+ text-align: right;
+}
+
+.dashboard-welcome-versionTitle {
+ font-family: Inter;
+ font-size: 16px;
+ font-weight: 500;
+ line-height: 24px;
+ letter-spacing: -0.011em;
+ color: #99a0ae;
+}
+
+.dashboard-welcome-versionValue {
+ font-family: Inter;
+ font-size: 10px;
+ font-weight: 400;
+ line-height: 12.1px;
+ letter-spacing: 0.01em;
+ color: white;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
diff --git a/src/routes/onboarding-checks.tsx b/src/routes/onboarding-checks.tsx
new file mode 100644
index 0000000..bf9ea25
--- /dev/null
+++ b/src/routes/onboarding-checks.tsx
@@ -0,0 +1,61 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useState } from "react";
+import { attributes } from "../utils/attributes";
+import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react";
+import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
+import { HealthChecks } from "../components/HealthChecks/HealthChecks";
+import { useNetwork } from "../network/useNetwork";
+import { WebStorage } from "../utils/web-storage";
+import AlphaIcon from "../assets/icons/alpha.svg?react";
+
+const OnBoardingChecks = () => {
+ const online = useNetwork();
+ const displayName = WebStorage.onBoarding.getDisplayName();
+ const [isStepValid, setIsStepValid] = useState(false);
+ const navigate = useNavigate({ from: "/onboarding-checks" });
+
+ const onNextStep = () => {
+ if (isStepValid) {
+ navigate({ to: "/dashboard" });
+ }
+ };
+
+ const onStepValid = (valid: boolean) => setIsStepValid(valid);
+
+ return (
+
+ <>
+
+
+
+ Connection /
+ Device Health Check
+
+
+
+
+ Nice to meet you {displayName},
+ Let’s establish our connection.
+
+
+
+
+
+
+
+
+ >
+
+ );
+};
+
+export const Route = createFileRoute("/onboarding-checks")({
+ component: OnBoardingChecks,
+});
diff --git a/src/routes/onboarding-name.tsx b/src/routes/onboarding-name.tsx
new file mode 100644
index 0000000..983c3df
--- /dev/null
+++ b/src/routes/onboarding-name.tsx
@@ -0,0 +1,55 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useState } from "react";
+import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout";
+import { attributes } from "../utils/attributes";
+import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react";
+import { UserInfo } from "../components/UserInfo/UserInfo";
+import { WebStorage } from "../utils/web-storage";
+import AlphaIcon from "../assets/icons/alpha.svg?react";
+
+const OnBoardingName = () => {
+ const [isStepValid, setIsStepValid] = useState(
+ !!WebStorage.onBoarding.getDisplayName()
+ );
+ const navigate = useNavigate({ from: "/onboarding-name" });
+
+ const onNameChange = (value: string) => setIsStepValid(!!value);
+
+ const onNextStep = () => {
+ if (isStepValid) {
+ navigate({ to: "/onboarding-checks" });
+ }
+ };
+
+ return (
+
+ <>
+
+
+
+ Let’s get you setup.
+ What do you want to be called?
+
+
+
+
+
+
+
+
+ >
+
+ );
+};
+
+export const Route = createFileRoute("/onboarding-name")({
+ component: OnBoardingName,
+});
diff --git a/src/utils/availabilities-storage.ts b/src/utils/availabilities-storage.ts
deleted file mode 100644
index f28bd02..0000000
--- a/src/utils/availabilities-storage.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createStore, del, get, set } from "idb-keyval";
-
-const store = createStore("availabilities", "availabilities");
-
-export const AvailabilityStorage = {
- get(key: string) {
- return get (key, store);
- },
-
- delete(key: string) {
- return del(key, store);
- },
-
- async add(key: string, value: string) {
- return set(key, value, store);
- },
-};
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index d9a5c72..ccbe7d7 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -13,3 +13,5 @@ export const EXPLORER_URL = "https://explorer.testnet.codex.storage/tx";
export const GB = 1_073_741_824;
export const TB = 1_099_511_627_776;
+
+export const MOBILE_MAX_WIDTH = 999
\ No newline at end of file
diff --git a/src/utils/dates.ts b/src/utils/dates.ts
index 434c910..d34def9 100644
--- a/src/utils/dates.ts
+++ b/src/utils/dates.ts
@@ -1,12 +1,12 @@
export const Dates = {
- format(date: string | Date) {
+ format(date: number) {
if (!date) {
- return "N/A";
+ return "-";
}
return new Intl.DateTimeFormat("en-GB", {
dateStyle: "medium",
timeStyle: "short",
- }).format(new Date(date));
+ }).format(new Date(date * 1000));
},
};
diff --git a/src/utils/favorite-storage.tsx b/src/utils/favorite-storage.tsx
deleted file mode 100644
index a589b03..0000000
--- a/src/utils/favorite-storage.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createStore, del, keys, set } from "idb-keyval";
-
-const store = createStore("favorites", "favorites");
-
-export const FavoriteStorage = {
- list() {
- return keys(store);
- },
-
- delete(key: string) {
- return del(key, store);
- },
-
- async add(key: string) {
- return set(key, "1", store);
- },
-};
diff --git a/src/utils/file-storage.ts b/src/utils/file-storage.ts
deleted file mode 100644
index 7e9088f..0000000
--- a/src/utils/file-storage.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createStore, entries, get, set } from "idb-keyval";
-
-const store = createStore("files", "files");
-
-export type FileMetadata = {
- mimetype: string;
- uploadedAt: string;
- name: string;
-};
-
-export const FilesStorage = {
- list() {
- return entries(store);
- },
-
- async get(cid: string) {
- return get(cid, store);
- },
-
- async set(cid: string, metadata: FileMetadata) {
- return set(cid, metadata, store);
- },
-};
diff --git a/src/utils/files.ts b/src/utils/files.ts
deleted file mode 100644
index 7ab1ad7..0000000
--- a/src/utils/files.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export const Files = {
- isImage(type: string) {
- return type.startsWith("image");
- },
-};
-
-export type CodexFileMetadata = {
- type: string;
- name: string;
-};
diff --git a/src/utils/purchases-storage.ts b/src/utils/purchases-storage.ts
deleted file mode 100644
index f3a278a..0000000
--- a/src/utils/purchases-storage.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { createStore, get, set } from "idb-keyval";
-
-const store = createStore("purchases", "purchases");
-const storeDates = createStore("purchases", "dates");
-
-export const PurchaseStorage = {
- async get(key: string) {
- return get(key, store);
- },
-
- async set(key: string, cid: string) {
- return set(key, cid, store);
- },
-};
-
-export const PurchaseDatesStorage = {
- async get(key: string) {
- return get(key, storeDates);
- },
-
- async set(key: string, date: string) {
- return set(key, date, storeDates);
- },
-};
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index 82030f9..95c4ea1 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -1,3 +1,8 @@
export const Strings = {
shortId: (id: string) => id.slice(0, 5) + "..." + id.slice(-5),
+
+
+
+
};
+
diff --git a/src/utils/times.ts b/src/utils/times.ts
index a2c5730..1ff5c4d 100644
--- a/src/utils/times.ts
+++ b/src/utils/times.ts
@@ -61,4 +61,33 @@ export const Times = {
return plural(value, "seconds");
},
+
+ unit(value: number) {
+ let seconds = 30 * 24 * 60 * 60;
+
+ if (value >= seconds) {
+ return "months";
+ }
+
+ seconds /= 30;
+ if (value >= seconds) {
+ return "days"
+ }
+
+ return "hours"
+ },
+
+ unitValue(unit: "hours" | "days" | "months") {
+ switch (unit) {
+ case "months": {
+ return 30 * 24 * 60 * 60
+ }
+ case "days": {
+ return 24 * 60 * 60
+ }
+ default: {
+ return 60 * 60
+ }
+ }
+ }
};
diff --git a/src/utils/web-storage.ts b/src/utils/web-storage.ts
index 9f1c8a3..4136c5b 100644
--- a/src/utils/web-storage.ts
+++ b/src/utils/web-storage.ts
@@ -1,4 +1,5 @@
-import { del, get, set } from "idb-keyval";
+import { createStore, del, entries, get, set } from "idb-keyval";
+
export const WebStorage = {
set(key: string, value: unknown) {
@@ -12,4 +13,113 @@ export const WebStorage = {
delete(key: string) {
return del(key);
},
+
+ onBoarding: {
+ getStep() {
+ return parseInt(localStorage.getItem("onboarding-step") || "0", 10)
+ },
+
+ setStep(step: number) {
+ localStorage.setItem("onboarding-step", step.toString())
+ },
+
+ setDisplayName(displayName: string) {
+ localStorage.setItem("display-name", displayName)
+ },
+
+ getDisplayName() {
+ return localStorage.getItem("display-name") || ""
+ },
+
+ setEmoji(emoji: string) {
+ localStorage.setItem("emoji", emoji)
+ },
+
+ getEmoji() {
+ return localStorage.getItem("emoji") || "🤖"
+ },
+ },
+
+ folders: {
+ store: createStore("folders", "folders"),
+
+ create(folder: string) {
+ return set(folder, [], this.store);
+ },
+
+ async list(): Promise<[string, string[]][]> {
+ const items = await entries(this.store) || []
+
+ if (items.length == 0) {
+ return [["Favorites", []]]
+ }
+
+ if (items[0][0] !== "Favorites") {
+ return [["Favorites", []], ...items]
+ }
+
+
+ return items
+ },
+ delete(key: string) {
+ return del(key, this.store);
+ },
+ async addFile(folder: string, cid: string) {
+ const files = await get(folder, this.store) || []
+
+ return set(folder, [...files, cid], this.store)
+ },
+
+ async deleteFile(folder: string, cid: string) {
+ const files = await get(folder, this.store) || []
+
+ return set(folder, files.filter(item => item !== cid), this.store)
+
+ },
+ },
+
+ availabilities: {
+ store: createStore("availabilities", "availabilities"),
+
+ get(key: string) {
+ return get(key, this.store);
+ },
+
+ delete(key: string) {
+ return del(key, this.store);
+ },
+
+ async add(key: string, value: string) {
+ return set(key, value, this.store);
+ },
+ },
+
+ purchases: {
+ store: createStore("purchases", "purchases"),
+
+
+ async get(key: string) {
+ return get(key, this.store);
+ },
+
+ async set(key: string, cid: string) {
+ return set(key, cid, this.store);
+ },
+
+ async entries() {
+ return entries(this.store);
+ },
+
+ dates: {
+ store: createStore("purchases", "dates"),
+
+ async get(key: string) {
+ return get(key, this.store);
+ },
+
+ async set(key: string, date: string) {
+ return set(key, date, this.store);
+ },
+ }
+ }
};
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index bb5e7a0..d2f2e02 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -1,8 +1,10 @@
///
+///
interface ImportMetaEnv {
VITE_CODEX_API_URL: string;
VITE_GEO_IP_URL: string;
+ VITE_DISCORD_LINK: string;
}
interface ImportMeta {
diff --git a/vite.config.ts b/vite.config.ts
index 105be43..b4a2237 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,16 +2,33 @@ import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import packageJson from "./package.json";
+import svgr from "vite-plugin-svgr";
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [TanStackRouterVite(), react()],
+ plugins: [TanStackRouterVite(), react(), svgr({
+ svgrOptions: {
+ plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"],
+ svgoConfig: {
+ floatPrecision: 2,
+ },
+ },
+ // ...
+ })],
define: {
"import.meta.env.PACKAGE_VERSION": JSON.stringify(packageJson.version),
},
build: {
sourcemap: true,
rollupOptions: {
+ output: {
+ manualChunks: {
+ "@sentry/react": ["@sentry/react"],
+ "emoji-picker-react": ["emoji-picker-react"],
+ "dotted-map": ["dotted-map"],
+ "echarts": ["echarts"],
+ }
+ },
onwarn(warning, defaultHandler) {
if (warning.code === "SOURCEMAP_ERROR") {
return;
@@ -20,11 +37,13 @@ export default defineConfig({
defaultHandler(warning);
},
},
+
},
resolve: {
alias: {
"../sdk/codex": "../proxy",
"../../sdk/codex": "../../proxy",
+ "./port-forwarding.util": "../proxy",
},
},
});
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..d73ab47
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,8 @@
+///
+import { defineConfig } from 'vite'
+
+export default defineConfig({
+ test: {
+ include: ["src/**/*.test.ts"]
+ },
+})
| |