Update design

This commit is contained in:
Arnaud 2024-11-10 09:20:38 +07:00
parent 47915d7432
commit ea593359e3
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
50 changed files with 673 additions and 618 deletions

47
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.0.7",
"license": "MIT",
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.41",
"@codex-storage/marketplace-ui-components": "0.0.42",
"@codex-storage/sdk-js": "^0.0.15",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
@ -26,7 +26,6 @@
"devDependencies": {
"@playwright/test": "^1.48.0",
"@svgr/plugin-svgo": "^8.1.0",
"@tanstack/router-devtools": "^1.58.7",
"@tanstack/router-plugin": "^1.58.4",
"@types/node": "^22.7.5",
"@types/react": "^18.3.8",
@ -425,9 +424,9 @@
"peer": true
},
"node_modules/@codex-storage/marketplace-ui-components": {
"version": "0.0.41",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.41.tgz",
"integrity": "sha512-zAbiN7yzpCDpGgGfqbJTutDjsxIKCj3QZ0wWuk3sk74nqhv16LGjvYTL6r2E35LG69Fp+yOGk3wC6t+zSmYcYw==",
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.42.tgz",
"integrity": "sha512-JRs7v5rsxNnH3T30UV+DHuJNr25kJ1a1EAqgthA/0okDrcr9IlOVEvl7XrsNBGRDDbJC5MDJkWo9VD2Jh3gAgQ==",
"dependencies": {
"lucide-react": "^0.453.0"
},
@ -1411,28 +1410,6 @@
"react-dom": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@tanstack/router-devtools": {
"version": "1.58.7",
"resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.58.7.tgz",
"integrity": "sha512-bZL3VDmS63gOW+RKSXRQ7uagATP1k8sM+ucHrcLy98hcVxzYRVwIwVgqTZY2KtUSXgFwb4LXClAdZdiJM9i+gw==",
"dev": true,
"dependencies": {
"clsx": "^2.1.1",
"goober": "^2.1.14"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-router": "^1.58.7",
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@tanstack/router-generator": {
"version": "1.74.2",
"resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.74.2.tgz",
@ -2728,14 +2705,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@ -3592,14 +3561,6 @@
"node": ">=4"
}
},
"node_modules/goober": {
"version": "2.1.14",
"dev": true,
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graphemer": {
"version": "1.4.0",
"dev": true,

View File

@ -25,7 +25,7 @@
"React"
],
"dependencies": {
"@codex-storage/marketplace-ui-components": "^0.0.41",
"@codex-storage/marketplace-ui-components": "0.0.42",
"@codex-storage/sdk-js": "^0.0.15",
"@sentry/browser": "^8.32.0",
"@sentry/react": "^8.31.0",
@ -42,7 +42,6 @@
"devDependencies": {
"@playwright/test": "^1.48.0",
"@svgr/plugin-svgo": "^8.1.0",
"@tanstack/router-devtools": "^1.58.7",
"@tanstack/router-plugin": "^1.58.4",
"@types/node": "^22.7.5",
"@types/react": "^18.3.8",

View File

@ -6,5 +6,5 @@
xmlns="http://www.w3.org/2000/svg">
<path
d="M10.926 8.5H16.176L9.42603 18.25V11.5H4.17603L10.926 1.75V8.5Z"
fill="#3EE089" />
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 214 B

After

Width:  |  Height:  |  Size: 219 B

View File

@ -13,7 +13,11 @@ import FilesIcon from "../../assets/icons/files.svg?react";
import LogsIcon from "../../assets/icons/logs.svg?react";
import HostIcon from "../../assets/icons/host.svg?react";
import SettingsIcon from "../../assets/icons/settings.svg?react";
import WalletIcon from "../../assets/icons/wallet.svg?react";
import NetworkFlashIcon from "../../assets/icons/flash.svg?react";
import PurchasesIcon from "../../assets/icons/purchase.svg?react";
import HelpIcon from "../../assets/icons/help.svg?react";
import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react";
import { WalletConnect } from "../WalletLogin/WalletLogin";
type Props = {
@ -27,6 +31,10 @@ const icons: Record<string, ReactElement> = {
files: <FilesIcon width={24} />,
logs: <LogsIcon width={24} />,
availabilities: <HostIcon width={24} />,
wallet: <WalletIcon width={24} />,
purchases: <PurchasesIcon width={24} />,
help: <HelpIcon width={24} />,
disclaimer: <DisclaimerIcon width={24} />,
};
const descriptions: Record<string, string> = {
@ -36,6 +44,10 @@ const descriptions: Record<string, string> = {
files: "Manage your files in your local vault.",
logs: "Manage your logs and debug console.",
availabilities: "Manage your host data.",
wallet: "Manage your Codex wallet.",
purchases: "Manage your storage requests.",
help: "Quick help resources.",
disclaimer: "Important information.",
};
export function AppBar({ onIconClick }: Props) {
@ -59,6 +71,12 @@ export function AppBar({ onIconClick }: Props) {
const title =
location.pathname.split("/")[2] || location.pathname.split("/")[1];
const networkIconColor = online
? "#3EE089"
: "var(--codex-input-color-error)";
const nodesIconColor = codex.enabled
? "#3EE089"
: "var(--codex-input-color-error)";
return (
<>
@ -79,11 +97,11 @@ export function AppBar({ onIconClick }: Props) {
<aside className="row gap">
<WalletConnect></WalletConnect>
<div className="row gap">
<NetworkFlashIcon />
<NetworkFlashIcon color={networkIconColor} />
<span>Network</span>
</div>
<div className="row gap" onClick={onNodeClick}>
<NodesIcon color="var(--codex-color-primary)" width={20} />
<NodesIcon color={nodesIconColor} width={20} />
<span>Node</span>
</div>
</aside>

View File

@ -5,33 +5,21 @@ import EditIcon from "../../assets/icons/edit.svg?react";
type Props = {
availability: CodexAvailability;
// onEdit: () => void;
};
/* eslint-disable @typescript-eslint/no-unused-vars */
export function AvailabilityActionsCell(_: Props) {
// const onEditClick = async () => {
// const unit = availability.totalSize >= 1_000_000_000 ? "gb" : "mb";
// const totalSize =
// unit === "gb"
// ? availability.totalSize / 1_000_000_000
// : availability.totalSize / 1_000_000;
// await WebStorage.set("availability-step-1", {
// ...availability,
// totalSize,
// totalSizeUnit: unit,
// });
// onEdit();
// };
export function AvailabilityActionsCell({ availability }: Props) {
const onEditClick = async () => {
document.dispatchEvent(
new CustomEvent("codexavailabilityedit", { detail: availability })
);
};
return (
<Cell>
<div className="availability-actions">
<ButtonIcon
variant="small"
// onClick={() => onDetails(content.cid)}
onClick={onEditClick}
Icon={EditIcon}></ButtonIcon>
</div>
</Cell>

View File

@ -1,3 +1,10 @@
.availability-confirm {
header {
margin-top: 16px;
margin-bottom: 16px;
}
}
.availabilitConfirm-bottom {
margin-top: 1.5rem;
display: flex;

View File

@ -1,9 +1,10 @@
import "./AvailabilityForm.css";
import "./AvailabilityConfirm.css";
import { AvailabilityComponentProps } from "./types";
import "./AvailabilityConfirm.css";
import { Info } from "lucide-react";
import { AvailabilitySpaceAllocation } from "./AvailabilitySpaceAllocation";
import { useEffect } from "react";
import { SpaceAllocation } from "@codex-storage/marketplace-ui-components";
import NodesIcon from "../../assets/icons/nodes.svg?react";
export function AvailabilityConfirm({
dispatch,
@ -18,9 +19,40 @@ export function AvailabilityConfirm({
});
}, [dispatch]);
const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space;
const isUpdating = !!availability.id;
const allocated = isUpdating
? quotaReservedBytes - availability.totalSize + quotaUsedBytes
: quotaReservedBytes + quotaUsedBytes;
const remaining =
availability.totalSize > quotaMaxBytes - allocated
? quotaMaxBytes - allocated
: quotaMaxBytes - allocated - availability.totalSize;
return (
<>
<AvailabilitySpaceAllocation availability={availability} space={space} />
<div className="availability-confirm">
<header>
<NodesIcon width={20}></NodesIcon>
<h6>Disk</h6>
</header>
<SpaceAllocation
data={[
{
title: "Allocated",
size: space.quotaUsedBytes,
color: "#FF6E61",
},
{
title: "Available",
size: space.quotaReservedBytes,
color: "#34A0FF",
},
{
title: "Free",
size: remaining,
color: "#6F6F6F",
},
]}></SpaceAllocation>
<div className="availabilitConfirm-bottom">
<div className="availabilitConfirm-iconContainer">
@ -36,6 +68,6 @@ export function AvailabilityConfirm({
</p>
</div>
</div>
</>
</div>
);
}

View File

@ -1,12 +1,48 @@
.availability-edit {
.button div {
> .button {
top: 0;
bottom: 0px;
left: 0;
right: 0;
position: absolute;
margin: auto;
border-radius: 100%;
height: 88px;
width: 88px;
background-color: white;
div {
position: relative;
left: 4px;
width: 40px;
height: 40px;
color: black;
}
}
@media (min-width: 801px) {
.availabilityCreate .stepper-body {
width: 600px;
h6 {
font-family: Inter;
font-size: 16px;
font-weight: 500;
line-height: 24px;
letter-spacing: -0.011em;
text-align: left;
}
svg {
color: #969696;
}
header {
display: flex;
gap: 8px;
}
input {
background-color: transparent;
}
.space-allocation {
margin-bottom: 16px;
}
}

View File

@ -4,19 +4,20 @@ import {
Button,
Modal,
} from "@codex-storage/marketplace-ui-components";
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { AvailabilityForm } from "./AvailabilityForm";
import { Pencil } from "lucide-react";
import { CodexNodeSpace } from "@codex-storage/sdk-js";
import { AvailabilityConfirm } from "./AvailabilityConfirmation";
import { WebStorage } from "../../utils/web-storage";
import { AvailabilityState } from "./types";
import { STEPPER_DURATION } from "../../utils/constants";
import { GB, STEPPER_DURATION } from "../../utils/constants";
import { useAvailabilityMutation } from "./useAvailabilityMutation";
import { AvailabilitySuccess } from "./AvailabilitySuccess";
import { AvailabilityError } from "./AvailabilityError";
import "./AvailabilityEdit.css";
import PlusIcon from "../../assets/icons/plus.svg?react";
import HostIcon from "../../assets/icons/host.svg?react";
import { Times } from "../../utils/times";
type Props = {
space: CodexNodeSpace;
@ -27,8 +28,8 @@ type Props = {
const CONFIRM_STATE = 2;
const defaultAvailabilityData: AvailabilityState = {
totalSize: 1,
duration: 1,
totalSize: 0.5 * GB,
duration: Times.unitValue("days"),
minPrice: 0,
maxCollateral: 0,
totalSizeUnit: "gb",
@ -46,7 +47,7 @@ export function AvailabilityEdit({
);
const { state, dispatch } = useStepperReducer();
const { mutateAsync, error } = useAvailabilityMutation(dispatch, state);
const [availabilityId, setAvailabilityId] = useState<string | null>(null);
const editAvailabilityValue = useRef(0);
useEffect(() => {
Promise.all([
@ -67,24 +68,24 @@ export function AvailabilityEdit({
}, [dispatch]);
// We use a custom event to not re render the sunburst component
useEffect(() => {
const onAvailabilityIdChange = (e: Event) => {
const custom = e as CustomEvent;
setAvailabilityId(custom.detail);
};
// useEffect(() => {
// const onAvailabilityIdChange = (e: Event) => {
// const custom = e as CustomEvent;
// setAvailabilityId(custom.detail);
// };
document.addEventListener(
"codexavailabilityid",
onAvailabilityIdChange,
false
);
// document.addEventListener(
// "codexavailabilityid",
// onAvailabilityIdChange,
// false
// );
return () =>
document.removeEventListener(
"codexavailabilityid",
onAvailabilityIdChange
);
}, []);
// return () =>
// document.removeEventListener(
// "codexavailabilityid",
// onAvailabilityIdChange
// );
// }, []);
const components = [
AvailabilityForm,
@ -111,7 +112,8 @@ export function AvailabilityEdit({
WebStorage.set("availability-step", step);
if (step == CONFIRM_STATE) {
mutateAsync(availability);
const { slots, name, ...rest } = availability as any;
mutateAsync(rest);
} else {
dispatch({
step,
@ -123,18 +125,12 @@ export function AvailabilityEdit({
const onAvailabilityChange = (data: Partial<AvailabilityState>) => {
const val = { ...availability, ...data };
WebStorage.set("availability", val);
setAvailability(val);
};
const onOpen = () => {
if (availability.id) {
WebStorage.set("availability-step", 0);
WebStorage.set("availability", defaultAvailabilityData);
const onOpen = useCallback(() => {
setAvailability(defaultAvailabilityData);
}
editAvailabilityValue.current = 0;
dispatch({
type: "open",
@ -144,7 +140,7 @@ export function AvailabilityEdit({
step: 0,
type: "next",
});
};
}, [editAvailabilityValue, dispatch]);
useEffect(() => {
document.addEventListener("codexavailabilitycreate", onOpen, false);
@ -153,6 +149,40 @@ export function AvailabilityEdit({
document.removeEventListener("codexavailabilitycreate", onOpen);
}, [onOpen]);
const onEdit = useCallback(
(event: Event) => {
const e = event as CustomEvent<AvailabilityState>;
const a = e.detail;
editAvailabilityValue.current = a.totalSize;
WebStorage.set("availability-step", 0);
WebStorage.set("availability", a);
const unit = Times.unit(a.duration);
setAvailability({
...a,
durationUnit: unit as "hours" | "days" | "months",
});
dispatch({
type: "open",
});
dispatch({
step: 0,
type: "next",
});
},
[editAvailabilityValue, dispatch]
);
useEffect(() => {
document.addEventListener("codexavailabilityedit", onEdit, false);
return () => document.removeEventListener("codexavailabilityedit", onEdit);
}, [onEdit, dispatch]);
const onClose = () => dispatch({ type: "close" });
const Body = components[state.step] || (() => <span />);
@ -164,17 +194,18 @@ export function AvailabilityEdit({
<div className="availability-edit">
<Button
label={hasLabel ? "Sale" : ""}
Icon={
!availabilityId ? () => <PlusIcon width={40} fill="#000" /> : Pencil
}
Icon={() => <PlusIcon width={40} fill="#000" />}
onClick={onOpen}
variant="outline"
className={className}
/>
<Modal open={state.open} onClose={onClose} displayCloseButton={false}>
<Modal
open={state.open}
onClose={onClose}
title="Availability"
Icon={HostIcon}>
<Stepper
className="availabilityCreate"
titles={steps.current}
state={state}
dispatch={dispatch}
@ -189,6 +220,7 @@ export function AvailabilityEdit({
availability={availability}
space={space}
error={error}
editAvailabilityValue={editAvailabilityValue.current}
/>
</Stepper>
</Modal>

View File

@ -1,31 +1,65 @@
.availabilityForm-itemInput {
.availability-form {
input {
width: 100%;
font-family: Inter;
font-size: 24px;
font-weight: 500;
line-height: 32px;
letter-spacing: -0.015em;
width: 100%;
}
.availabilityForm-item {
margin-bottom: 1rem;
option {
background-color: #232323;
}
.availabilityForm-item--error .input,
.availabilityForm-item--error .inputGroup-helper,
.availabilityForm-item--error .inputGroup-select {
color: rgb(var(--codex-color-error));
border-color: rgb(var(--codex-color-error));
header {
margin-top: 16px;
margin-bottom: 16px;
}
.availabilityForm-row {
display: flex;
gap: 0.5rem;
.row {
margin-bottom: 16px;
}
.availabilityForm-itemInput-maxSize {
color: var(--codex-color-primary);
padding-right: 0.5rem;
font-size: 0.85rem;
.group {
position: relative;
flex: 1;
margin-top: 16px;
&[aria-invalid] {
.input-group > div > div > div:nth-child(2) {
border-color: var(--codex-input-color-error);
}
label,
svg,
select {
color: var(--codex-input-color-error);
}
}
}
.tooltip {
position: absolute;
right: 0px;
top: 0px;
}
.input-group p {
max-width: inherit;
position: relative;
a {
cursor: pointer;
transition: 0.35s opacity;
color: var(--codex-color-primary);
top: 4px;
right: 4px;
position: absolute;
}
}
.availabilityForm-itemInput-maxSize:hover {
opacity: 0.7;
select {
min-width: min-content;
}
}

View File

@ -1,37 +1,39 @@
import { Input, InputGroup } from "@codex-storage/marketplace-ui-components";
import { ChangeEvent, useEffect, useState } from "react";
import {
Input,
InputGroup,
SpaceAllocation,
Tooltip,
} from "@codex-storage/marketplace-ui-components";
import { ChangeEvent, useEffect } from "react";
import "./AvailabilityForm.css";
import { AvailabilityComponentProps } from "./types";
import { classnames } from "../../utils/classnames";
import { AvailabilitySpaceAllocation } from "./AvailabilitySpaceAllocation";
import {
availabilityMax,
availabilityUnit,
isAvailabilityValid,
} from "./availability.domain";
import NodesIcon from "../../assets/icons/nodes.svg?react";
import InfoIcon from "../../assets/icons/info.svg?react";
import { attributes } from "../../utils/attributes";
import { AvailabilityUtils } from "./availability.utils";
import { Times } from "../../utils/times";
export function AvailabilityForm({
dispatch,
onAvailabilityChange,
availability,
space,
editAvailabilityValue,
}: AvailabilityComponentProps) {
const [availabilityValue, setAvailabilityValue] = useState(
(
availability.totalSize / availabilityUnit(availability.totalSizeUnit)
).toFixed(2)
);
useEffect(() => {
const max = availabilityMax(space);
const isValid = isAvailabilityValid(availability, max);
let max = AvailabilityUtils.maxValue(space);
if (availability.id && editAvailabilityValue) {
max += editAvailabilityValue;
}
const isValid = AvailabilityUtils.isValid(availability, max);
dispatch({
type: "toggle-buttons",
isNextEnable: isValid,
isBackEnable: true,
});
}, [dispatch, space, availability]);
}, [dispatch, space, availability, editAvailabilityValue]);
const onTotalSizeUnitChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const element = e.currentTarget;
@ -42,24 +44,33 @@ export function AvailabilityForm({
});
};
const onDurationUnitChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const onDurationChange = async (e: ChangeEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const unitValue = Times.unitValue(availability.durationUnit);
onAvailabilityChange({
duration: 1,
durationUnit: element.value as "hours" | "days" | "months",
duration: parseInt(element.value) * unitValue,
});
};
const onDurationUnitChange = async (e: ChangeEvent<HTMLSelectElement>) => {
const element = e.currentTarget;
const unit = element.value as "hours" | "days" | "months";
const unitValue = Times.unitValue(unit);
onAvailabilityChange({
duration: unitValue,
durationUnit: unit,
});
};
const onAvailablityChange = async (e: ChangeEvent<HTMLInputElement>) => {
const element = e.currentTarget;
const v = element.value;
const unit = availabilityUnit(availability.totalSizeUnit);
setAvailabilityValue(v);
const unit = AvailabilityUtils.unitValue(availability.totalSizeUnit);
onAvailabilityChange({
[element.name]: parseFloat(v) * unit,
totalSize: parseFloat(v) * unit,
});
};
@ -72,128 +83,162 @@ export function AvailabilityForm({
});
};
// const domain = new AvailabilityDomain(space, availability);
const onMaxSize = () => {
const available =
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
const unit = availabilityUnit(availability.totalSizeUnit);
setAvailabilityValue((available / unit).toFixed(2));
const available = AvailabilityUtils.maxValue(space);
onAvailabilityChange({
totalSize: available,
});
};
const available =
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
const isValid = available >= availability.totalSize;
const unit = availabilityUnit(availability.totalSizeUnit);
const max = available / unit;
let available = AvailabilityUtils.maxValue(space);
if (availability.id && editAvailabilityValue) {
available += editAvailabilityValue;
}
const isValid =
availability.totalSize > 0 && available >= availability.totalSize;
const helper = isValid
? "Total size of sale's storage in bytes"
: "The total size cannot exceed the space available.";
return (
<>
<AvailabilitySpaceAllocation availability={availability} space={space} />
const value = AvailabilityUtils.toUnit(
availability.totalSize,
availability.totalSizeUnit
).toFixed(2);
const unitValue = Times.unitValue(availability.durationUnit);
const duration = availability.duration / unitValue;
return (
<div className="availability-form">
<header>
<NodesIcon width={20}></NodesIcon>
<h6>Disk</h6>
</header>
<SpaceAllocation
data={[
{
title: "Allocated",
size: space.quotaUsedBytes,
color: "#FF6E61",
},
{
title: "Available",
size: space.quotaReservedBytes,
color: "#34A0FF",
},
{
title: "Free",
size: isValid ? available - availability.totalSize : available,
color: "#6F6F6F",
},
]}></SpaceAllocation>
<div className="row gap">
<div className="group" {...attributes({ "aria-invalid": !isValid })}>
<InputGroup
id="totalSize"
name="totalSize"
type="number"
label="Total size"
helper={helper}
className={classnames(
["availabilityForm-item"],
["availabilityForm-item--error", !isValid]
)}
inputClassName="availabilityForm-itemInput"
min={0.01}
max={max.toFixed(2)}
isInvalid={!isValid}
max={available.toFixed(2)}
onChange={onAvailablityChange}
onGroupChange={onTotalSizeUnitChange}
value={availabilityValue}
step={"0.01"}
value={value}
group={[
["gb", "GB"],
["tb", "TB"],
// ["tb", "TB"],
]}
groupValue={availability.totalSizeUnit}
extra={
<a onClick={onMaxSize} className="availabilityForm-itemInput-maxSize">
Use max size
</a>
}
extra={<a onClick={onMaxSize}>Use max size</a>}
/>
<Tooltip message={helper}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
<div className="availabilityForm-item">
<div className="group">
<InputGroup
id="duration"
name="duration"
type="number"
label="Duration"
helper="The duration of the request in seconds"
inputClassName="availabilityForm-itemInput"
min={1}
onChange={onInputChange}
onChange={onDurationChange}
onGroupChange={onDurationUnitChange}
group={[
["hours", "Hours"],
["days", "Days"],
["months", "Months"],
]}
value={availability.duration.toString()}
value={duration.toString()}
groupValue={availability.durationUnit}
/>
<Tooltip message={"The duration of the request in seconds"}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
</div>
<div className="availabilityForm-row">
<div className="availabilityForm-item">
<div>
<div className="row gap">
<div className="group">
<Input
id="minPrice"
name="minPrice"
type="number"
label="Min price"
min={0}
helper="Minimum price to be paid (in amount of tokens)"
inputClassName="availabilityForm-itemInput"
onChange={onInputChange}
value={availability.minPrice.toString()}
/>
<Tooltip message={"Minimum price to be paid (in amount of tokens)"}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
<div className="availabilityForm-item">
<div className="group">
<Input
id="maxCollateral"
name="maxCollateral"
type="number"
label="Max collateral"
min={0}
helper="Maximum collateral user is willing to pay per filled Slot (in amount of tokens)"
inputClassName="availabilityForm-itemInput"
onChange={onInputChange}
value={availability.maxCollateral.toString()}
/>
<Tooltip
message={
"Maximum collateral user is willing to pay per filled Slot (in amount of tokens)"
}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
</div>
</div>
<div className="availabilityForm-item">
<div className="group">
<Input
id="name"
name="name"
type="string"
label="Nickname"
max={9}
helper="You can add a custom name to easily retrieve your sale."
inputClassName="availabilityForm-itemInput"
onChange={onInputChange}
value={availability.name?.toString()}
value={availability.name?.toString() || ""}
maxLength={9}
autoComplete="falsep"
/>
<Tooltip
message={"You can add a custom name to easily retrieve your sale."}>
<InfoIcon></InfoIcon>
</Tooltip>
</div>
</div>
</>
);
}

View File

@ -142,7 +142,6 @@ export function AvailabilitySheetCreate({
<Modal open={state.open} onClose={onClose}>
<Stepper
className="availabilityCreate"
titles={steps.current}
state={state}
dispatch={dispatch}

View File

@ -162,22 +162,22 @@ export function Sunburst({ availabilities, space }: Props) {
if (chart.current) {
chart.current.setOption(option);
chart.current.off("click");
chart.current.on("click", function (params) {
// console.info(params.componentIndex);
// console.info(params.dataIndex);
// chart.current.off("click");
// chart.current.on("click", function (params) {
// // console.info(params.componentIndex);
// // console.info(params.dataIndex);
const index = params.dataIndex;
// const index = params.dataIndex;
const detail =
params.dataIndex === 0 ? null : availabilities[index - 1].id;
// const detail =
// params.dataIndex === 0 ? null : availabilities[index - 1].id;
document.dispatchEvent(
new CustomEvent("codexavailabilityid", {
detail,
})
);
});
// document.dispatchEvent(
// new CustomEvent("codexavailabilityid", {
// detail,
// })
// );
// });
}
return <div id="chart" ref={div} className="sunburst"></div>;

View File

@ -38,12 +38,6 @@ export class AvailabilityDomain {
}
}
export const availabilityUnit = (unit: "gb" | "tb") =>
unit === "gb" ? GB : TB;
export const availabilityMax = (space: CodexNodeSpace) =>
space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes;
export const isAvailabilityValid = (
availability: AvailabilityState,
max: number

View File

@ -1,5 +1,7 @@
import { TabSortState } from "@codex-storage/marketplace-ui-components"
import { AvailabilityWithSlots } from "./types"
import { AvailabilityState, AvailabilityWithSlots } from "./types"
import { GB, TB } from "../../utils/constants";
import { CodexNodeSpace } from "@codex-storage/sdk-js";
export const AvailabilityUtils = {
sortById: (state: TabSortState) =>
@ -33,4 +35,18 @@ export const AvailabilityUtils = {
? b.maxCollateral - a.maxCollateral
: a.maxCollateral - b.maxCollateral
,
toUnit(bytes: number, unit: "gb" | "tb") {
return bytes / this.unitValue(unit || "gb")
},
maxValue(space: CodexNodeSpace) {
return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes
},
unitValue(unit: "gb" | "tb") {
return unit === "tb" ? TB : GB
},
isValid: (
availability: AvailabilityState,
max: number
) => availability.totalSize > 0 && availability.totalSize <= max
}

View File

@ -27,6 +27,7 @@ export type AvailabilityComponentProps = {
onAvailabilityChange: (data: Partial<AvailabilityState>) => void;
availability: AvailabilityState;
error: Error | null;
editAvailabilityValue?: number;
};
export type AvailabilityWithSlots = CodexAvailability & {

View File

@ -7,7 +7,6 @@ import {
StepperAction,
StepperState,
} from "@codex-storage/marketplace-ui-components";
import { Times } from "../../utils/times";
import { CodexSdk } from "../../sdk/codex";
import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js";
@ -25,12 +24,10 @@ export function useAvailabilityMutation(
totalSize,
totalSizeUnit,
duration,
durationUnit = "days",
durationUnit,
name,
...input
}: AvailabilityState) => {
const time = Times.toSeconds(duration, durationUnit);
const fn: (
input: Omit<AvailabilityState, "totalSizeUnit" | "durationUnit">
) => Promise<"" | CodexAvailabilityCreateResponse> = input.id
@ -45,7 +42,7 @@ export function useAvailabilityMutation(
return fn({
...input,
duration: time,
duration,
totalSize: Math.trunc(totalSize),
});
},

View File

@ -35,6 +35,7 @@ export function Card({
label={buttonLabel}
variant="outline"
Icon={buttonIcon}
size="small"
onClick={buttonAction}></Button>
)}
</header>

View File

@ -21,16 +21,6 @@
letter-spacing: -0.015em;
width: 100%;
background-color: transparent;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
& {
-moz-appearance: textfield;
}
}
.tooltip {
@ -67,6 +57,6 @@
color: var(--codex-card-number-unit-color);
position: absolute;
top: 54px;
right: 16px;
right: 48px;
}
}

View File

@ -20,6 +20,7 @@ export function Download() {
placeholder="CID"
inputClassName="download-input"
size={"medium" as any}
autoComplete="off"
onChange={onCidChange}></Input>
<Button label="Download" onClick={onDownload} variant="outline"></Button>
</main>

View File

@ -55,7 +55,7 @@ export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) {
}
}
});
}, [requestId, data]);
}, [requestId, data, onMetadata]);
let filename = metadata.filename || "-";

View File

@ -196,6 +196,7 @@ export function Files({ limit }: Props) {
value={folder}
required={true}
pattern="[A-Za-z0-9_\-]*"
autoComplete="off"
maxLength={9}
size={"medium" as any}
placeholder="Folder name"

View File

@ -4,7 +4,7 @@
gap: 16px;
margin-top: 14px;
> div {
> div:first-child {
position: relative;
svg {

View File

@ -39,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<HTMLSelectElement>) {
@ -86,6 +86,7 @@ export function LogLevel() {
message={toast.message}
time={toast.time}
variant={toast.variant}
duration={400000000}
/>
</div>
);

View File

@ -49,6 +49,7 @@ export function ManifestFetch() {
value={cid}
placeholder="CID"
size={"medium" as any}
autoComplete="off"
onChange={onCidChange}></Input>
<Button label="Fetch" onClick={onDownload} variant="outline"></Button>
</div>

View File

@ -36,9 +36,7 @@ export type MenuItem =
Component: ComponentType<MenuItemComponentProps>;
};
type Props = {};
export function Menu({}: Props) {
export function Menu() {
const [isExpanded, setIsExpanded] = useState(true);
const onLogoClick = () => {
@ -72,7 +70,11 @@ export function Menu({}: Props) {
</span>
<span>Dashboard</span>
</Link>
<Link to="/dashboard/wallet">
<Link
to="/dashboard/wallet"
disabled={true}
aria-disabled={true}
data-title="Coming soon">
<span>
<WalletIcon width={20} height={20} />
</span>

View File

@ -37,7 +37,7 @@
}
@media (min-width: 1200px) {
&:not([aria-expanded="false"]) a[data-title]:hover::after {
&[aria-expanded] a[data-title]:hover::after {
content: attr(data-title);
background-color: #2f2f2f;
color: #fff;

View File

@ -66,14 +66,13 @@
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;

View File

@ -4,6 +4,7 @@ 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 }>;
@ -12,6 +13,8 @@ type Props = {
};
export function OnBoardingLayout({ children, step }: Props) {
const navigate = useNavigate({ from: window.location.pathname });
return (
<div
className={classnames(
@ -29,9 +32,15 @@ export function OnBoardingLayout({ children, step }: Props) {
<footer>
<ul>
<li {...attributes({ "aria-selected": step === 0 })}></li>
<li {...attributes({ "aria-selected": step === 1 })}></li>
<li {...attributes({ "aria-selected": step === 2 })}></li>
<li
{...attributes({ "aria-selected": step === 0 })}
onClick={() => navigate({ to: "/" })}></li>
<li
{...attributes({ "aria-selected": step === 1 })}
onClick={() => navigate({ to: "/onboarding-name" })}></li>
<li
{...attributes({ "aria-selected": step === 2 })}
onClick={() => navigate({ to: "/onboarding-checks" })}></li>
</ul>
</footer>
</section>

View File

@ -24,16 +24,26 @@ export function PeersMap({ nodes, onPinAdd }: Props) {
setPins((val) => PeerUtils.incPin(val, { ...node, ...geo }));
onPinAdd?.(node, geo);
},
[setPins]
[setPins, onPinAdd]
);
pins.map(([pin, quantity]) =>
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: 0.1 * quantity },
})
);
svgOptions: { color: "#d6ff79", radius },
});
});
const svgMap = map
.getSVG({
@ -60,7 +70,7 @@ export function PeersMap({ nodes, onPinAdd }: Props) {
opacity: 1; /* Fully opaque */
}
50% {
r: 2; /* Increased radius */
r: 1.5; /* Increased radius */
opacity: 1; /* Slightly transparent */
}
100% {

View File

@ -17,12 +17,9 @@ import { TruncateCell } from "../TruncateCell/TruncateCell";
import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender";
import { PurchaseUtils } from "./purchase.utils";
type Props = {};
type SortFn = (a: CodexPurchase, b: CodexPurchase) => number;
export function PurchasesTable({}: Props) {
const [metadata, setMetadata] = useState<{ [key: string]: number }>({});
export function PurchasesTable() {
const content = useData();
const { data, isPending } = useQuery({
queryFn: () =>
@ -49,21 +46,21 @@ export function PurchasesTable({}: Props) {
throwOnError: true,
});
const onMetadata = (
requestId: string,
{ uploadedAt }: { uploadedAt: number }
) => {
setMetadata((m) => ({ ...m, [requestId]: uploadedAt }));
setSortFn(() =>
PurchaseUtils.sortByUploadedAt("desc", {
...metadata,
[requestId]: uploadedAt,
})
);
};
// const onMetadata = (
// requestId: string,
// { uploadedAt }: { uploadedAt: number }
// ) => {
// setMetadata((m) => ({ ...m, [requestId]: uploadedAt }));
// setSortFn(() =>
// PurchaseUtils.sortByUploadedAt("desc", {
// ...metadata,
// [requestId]: uploadedAt,
// })
// );
// };
const [sortFn, setSortFn] = useState<SortFn>(() =>
PurchaseUtils.sortByUploadedAt("desc", metadata)
PurchaseUtils.sortByDuration("desc")
);
const onSortByDuration = (state: TabSortState) =>
@ -75,11 +72,11 @@ export function PurchasesTable({}: Props) {
const onSortByState = (state: TabSortState) =>
setSortFn(() => PurchaseUtils.sortByState(state));
const onSortByUploadedAt = (state: TabSortState) =>
setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata));
// const onSortByUploadedAt = (state: TabSortState) =>
// setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata));
const headers = [
["file", onSortByUploadedAt],
["file"],
["request id"],
["duration", onSortByDuration],
["slots"],
@ -104,7 +101,6 @@ export function PurchasesTable({}: Props) {
purchaseCid={r.content.cid}
index={index}
data={content}
onMetadata={onMetadata}
/>,
<TruncateCell value={r.id} />,
<Cell>{Times.pretty(duration)}</Cell>,
@ -116,8 +112,6 @@ export function PurchasesTable({}: Props) {
);
});
console.info(metadata);
if (isPending) {
return (
<div className="purchases-loader">
@ -128,7 +122,7 @@ export function PurchasesTable({}: Props) {
return (
<>
<Table headers={headers} rows={rows} defaultSortIndex={0} />
<Table headers={headers} rows={rows} defaultSortIndex={2} />
</>
);
}

View File

@ -34,7 +34,6 @@ export const PurchaseUtils = {
,
sortByUploadedAt: (state: TabSortState, table: Record<string, number>) =>
(a: CodexPurchase, b: CodexPurchase) => {
console.info(table)
return state === "desc"
? (table[b.requestId] || 0) - (table[a.requestId] || 0)
: (table[a.requestId] || 0) - (table[b.requestId] || 0)

View File

@ -1,9 +1,7 @@
import { classnames } from "../../utils/classnames";
import "./AssistanceImage.css";
type Props = {};
export function AssistanceImage({}: Props) {
export function AssistanceImage() {
return (
<picture>
<source

View File

@ -13,6 +13,7 @@
cursor: pointer;
text-decoration: none;
position: relative;
max-width: 508px;
h5 {
font-family: Azeret Mono;

View File

@ -22,14 +22,14 @@ 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() {
@ -88,7 +88,7 @@ export function StorageRequestCreate() {
mutateAsync({
...rest,
duration: Times.toSeconds(availability, availabilityUnit),
expiry: expiration,
expiry: expiration * 60,
});
} else {
dispatch({

View File

@ -114,22 +114,6 @@
& {
grid-template-columns: 1fr;
}
.storageRequestReview-presets {
flex-direction: column;
}
.storageRequestReview-presets-blocs {
flex-direction: column;
}
.storageRequestReview-alert {
flex-direction: column;
}
.storageRequestReview-expiration {
min-width: 100%;
}
}
@media (min-width: 801px) {
@ -149,114 +133,3 @@
}
}
}
.storageRequestReview-hr {
margin-bottom: 1.5rem;
margin-top: 0rem;
}
.storageRequestReview-numbers {
display: grid;
gap: 16px;
}
.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;
}
.storageRequestReview-presets {
flex-direction: column;
}
.storageRequestReview-presets-blocs {
flex-direction: column;
}
.storageRequestReview-alert {
flex-direction: column;
}
.storageRequestReview-expiration {
min-width: 100%;
}
}
@media (min-width: 801px) {
.storageRequestReview-numbers {
grid-template-columns: 1fr 1fr 1fr;
}
}

View File

@ -314,14 +314,14 @@ export function StorageRequestReview({
<footer>
<CardNumbers
helper="Represents expiry threshold in seconds from when the Request is submitted. When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided. "
helper="Represents expiry threshold in minutes from when the Request is submitted. When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided. "
id="expiration"
unit={"Expiration"}
value={storageRequest.expiration.toString()}
onChange={onExpirationChange}
className="storageRequestReview-expiration"
onValidation={isInvalidNumber}
title="Request expiration in seconds"></CardNumbers>
title="Request expiration in minutes"></CardNumbers>
<Alert
Icon={<FileWarning />}
title="Warning"

View File

@ -76,6 +76,7 @@ export function UserInfo({ onNameChange }: Props) {
onChange={onDisplayNameChange}
label="Preferred name"
id="displayName"
autoComplete="off"
value={displayName}></Input>
</div>
</div>

View File

@ -5,6 +5,15 @@
gap: 16px;
background-color: #252525;
& {
filter: grayscale(30);
transition: filter 0.5s;
}
&:hover {
filter: none;
}
div {
> p {
font-family: Inter;

View File

@ -2,76 +2,16 @@ import {
CodexCreateStorageRequestInput,
CodexData,
CodexMarketplace,
CodexReservation,
SafeValue,
} from "@codex-storage/sdk-js";
import { CodexSdk as Sdk } from "./sdk/codex";
import { PortForwardingUtil as PUtil } from "./hooks/port-forwarding.util";
import { WebStorage } from "./utils/web-storage";
import { GB } from "./utils/constants";
class CodexDataMock extends CodexData {
// override upload(
// file: File,
// onProgress?: (loaded: number, total: number) => void
// ): UploadResponse {
// // const url = CodexSdk.url() + "/api/codex/v1/data";
// // const xhr = new XMLHttpRequest();
// // const promise = new Promise<SafeValue<string>>((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 WebStorage.files.set(safe.data, {
// mimetype: file.type,
// name: file.name,
// uploadedAt: new Date().toJSON(),
// }).then(() => safe);
// }
// return safe;
// }),
// };
// }
}
@ -167,6 +107,34 @@ class CodexMarketplaceMock extends CodexMarketplace {
// ],
// });
// }
// override reservations(): Promise<SafeValue<CodexReservation[]>> {
// 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 = {

View File

@ -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 (
<>
<ScrollRestoration></ScrollRestoration>
<Outlet />
<TanStackRouterDevtools />
</>
);
},

View File

@ -1,4 +1,8 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import {
createFileRoute,
Outlet,
ScrollRestoration,
} from "@tanstack/react-router";
import "./layout.css";
import { Menu } from "../components/Menu/Menu";
import { useState } from "react";
@ -23,6 +27,7 @@ const Layout = () => {
<main>
<AppBar onIconClick={onIconClick} />
<div>
<ScrollRestoration></ScrollRestoration>
<Outlet />
</div>
</main>

View File

@ -4,6 +4,10 @@
flex-wrap: wrap;
gap: 16px;
dialog {
width: 80%;
}
> .card {
flex: 1 1 50%;
}
@ -67,24 +71,6 @@
main {
> div {
position: relative;
.button {
top: 0;
bottom: 0px;
left: 0;
right: 0;
position: absolute;
margin: auto;
border-radius: 100%;
height: 88px;
width: 88px;
background-color: white;
div {
position: relative;
left: 4px;
}
}
}
> .button {
@ -103,7 +89,7 @@
}
footer {
padding: 16px 0;
padding-top: 16px;
b {
display: block;

View File

@ -22,6 +22,7 @@ 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,
@ -121,9 +122,7 @@ 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",
});

View File

@ -3,9 +3,12 @@
flex-wrap: wrap;
gap: 16px;
.card {
> .card {
margin-bottom: 16px;
flex: 1 1 67%;
&:first-child {
flex: 1 1 70%;
}
}
aside {
@ -13,5 +16,11 @@
flex-direction: column;
gap: 16px;
flex: 1 1 auto;
.card:first-child {
display: flex;
flex-direction: column;
/* flex: 1 1 67%; */
}
}
}

View File

@ -2,13 +2,26 @@
.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 50%;
flex: 1 1 62%;
}
}
}
@ -66,6 +79,10 @@
}
@media (min-width: 2000px) {
.column:nth-child(2) {
flex: 1 1 15%;
}
.column {
flex: 1 1 25%;
}

View File

@ -47,7 +47,7 @@ function Dashboard() {
className="card--main"
title="Connected Account"
buttonLabel="Add Wallet"
buttonIcon={PlusIcon}>
buttonIcon={() => <PlusIcon width={20} />}>
<ConnectedAccount></ConnectedAccount>
</Card>

View File

@ -7,8 +7,7 @@ import LogsIcon from "../../assets/icons/logs.svg?react";
const throwOnError = false;
export const Route = createFileRoute("/dashboard/logs")({
component: () => {
const Logs = () => {
const { data } = useDebug(throwOnError);
const { table, ...rest } = data ?? {};
@ -19,8 +18,7 @@ export const Route = createFileRoute("/dashboard/logs")({
<div>
<h5>Log level</h5>
<small>
Manage the type of logs being displayed on your CLI for Codex
Node.
Manage the type of logs being displayed on your CLI for Codex Node.
</small>
<LogLevel></LogLevel>
</div>
@ -40,5 +38,8 @@ export const Route = createFileRoute("/dashboard/logs")({
</div>
</div>
);
},
};
export const Route = createFileRoute("/dashboard/logs")({
component: Logs,
});

View File

@ -16,7 +16,7 @@ const OnBoardingChecks = () => {
const onNextStep = () => {
if (isStepValid) {
navigate({ to: "/onboarding-checks" });
navigate({ to: "/dashboard" });
}
};
@ -36,7 +36,7 @@ const OnBoardingChecks = () => {
</section>
<section className="main">
<h1>
Nice to meet {displayName},<br />
Nice to meet you {displayName},<br />
Lets establish our connection.
</h1>

View File

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