mirror of
https://github.com/codex-storage/codex-marketplace-ui.git
synced 2025-02-23 13:18:37 +00:00
UI implementation
This commit is contained in:
parent
d6398fdb02
commit
47915d7432
8
package-lock.json
generated
8
package-lock.json
generated
@ -9,7 +9,7 @@
|
||||
"version": "0.0.7",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.40",
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.41",
|
||||
"@codex-storage/sdk-js": "^0.0.15",
|
||||
"@sentry/browser": "^8.32.0",
|
||||
"@sentry/react": "^8.31.0",
|
||||
@ -425,9 +425,9 @@
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@codex-storage/marketplace-ui-components": {
|
||||
"version": "0.0.40",
|
||||
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.40.tgz",
|
||||
"integrity": "sha512-ttyf7Yxk1QFSmdISVXNjGNZ0i4PlNkU9RCZxjU8cbPLMIfAoMBaKsptNzZxxifsBrNLLqthZujOTOdILsQHGIg==",
|
||||
"version": "0.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.41.tgz",
|
||||
"integrity": "sha512-zAbiN7yzpCDpGgGfqbJTutDjsxIKCj3QZ0wWuk3sk74nqhv16LGjvYTL6r2E35LG69Fp+yOGk3wC6t+zSmYcYw==",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.453.0"
|
||||
},
|
||||
|
@ -25,7 +25,7 @@
|
||||
"React"
|
||||
],
|
||||
"dependencies": {
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.40",
|
||||
"@codex-storage/marketplace-ui-components": "^0.0.41",
|
||||
"@codex-storage/sdk-js": "^0.0.15",
|
||||
"@sentry/browser": "^8.32.0",
|
||||
"@sentry/react": "^8.31.0",
|
||||
|
10
src/assets/icons/choose-cid.svg
Normal file
10
src/assets/icons/choose-cid.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M5.25 16.5H18.75V7.5H5.25V16.5ZM3.75 6.75C3.75 6.33579 4.08579 6 4.5 6H19.5C19.9142 6 20.25 6.33579 20.25 6.75V17.25C20.25 17.6642 19.9142 18 19.5 18H4.5C4.08579 18 3.75 17.6642 3.75 17.25V6.75ZM9.75 10.5C9.75 10.0858 9.41421 9.75 9 9.75C8.58579 9.75 8.25 10.0858 8.25 10.5C8.25 10.9142 8.58579 11.25 9 11.25C9.41421 11.25 9.75 10.9142 9.75 10.5ZM11.25 10.5C11.25 11.7427 10.2426 12.75 9 12.75C7.75736 12.75 6.75 11.7427 6.75 10.5C6.75 9.25736 7.75736 8.25 9 8.25C10.2426 8.25 11.25 9.25736 11.25 10.5ZM9.00135 15C8.27627 15 7.62105 15.293 7.1452 15.7688L6.08454 14.7082C6.8302 13.9625 7.86247 13.5 9.00135 13.5C10.1402 13.5 11.1725 13.9625 11.9182 14.7082L10.8575 15.7688C10.3817 15.293 9.72643 15 9.00135 15ZM12.75 9.75V14.25H14.25V9.75H12.75ZM15.75 9.75V14.25H17.25V9.75H15.75Z"
|
||||
fill="#969696" />
|
||||
</svg>
|
After Width: | Height: | Size: 932 B |
10
src/assets/icons/commitment.svg
Normal file
10
src/assets/icons/commitment.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M18.75 9V12H17.25V9.75H13.5V6H6.75V18H11.25V19.5H5.99505C5.58371 19.5 5.25 19.167 5.25 18.7561V5.24385C5.25 4.84148 5.58653 4.5 6.00166 4.5H14.2476L18.75 9ZM13.3393 14.4952C13.3685 13.9498 13.7893 13.5062 14.3325 13.4485L14.9855 13.3792C15.0631 13.3709 15.1361 13.3384 15.1942 13.2863L15.6826 12.8474C16.0889 12.4823 16.7002 12.4664 17.1251 12.8096L17.6359 13.2224C17.6965 13.2713 17.7712 13.3 17.8491 13.3042L18.5048 13.3393C19.0502 13.3685 19.4938 13.7893 19.5515 14.3325L19.6208 14.9855C19.6291 15.0631 19.6616 15.1361 19.7137 15.1942L20.1526 15.6826C20.5177 16.0889 20.5336 16.7002 20.1904 17.1251L19.7776 17.6359C19.7287 17.6965 19.7 17.7712 19.6958 17.8491L19.6607 18.5048C19.6315 19.0502 19.2106 19.4938 18.6675 19.5515L18.0145 19.6208C17.9369 19.6291 17.8639 19.6616 17.8058 19.7137L17.3174 20.1526C16.9111 20.5177 16.2998 20.5336 15.8749 20.1904L15.3641 19.7776C15.3035 19.7287 15.2288 19.7 15.1509 19.6958L14.4952 19.6607C13.9498 19.6315 13.5062 19.2106 13.4485 18.6675L13.3792 18.0145C13.3709 17.9369 13.3384 17.8639 13.2863 17.8058L12.8474 17.3174C12.4823 16.9111 12.4664 16.2997 12.8096 15.8749L13.2224 15.3641C13.2713 15.3035 13.3 15.2288 13.3042 15.1509L13.3393 14.4952ZM18.7727 15.7727L17.9773 14.9773L16.125 16.8295L15.0227 15.7273L14.2273 16.5227L16.125 18.4205L18.7727 15.7727Z"
|
||||
fill="#969696" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
10
src/assets/icons/durability.svg
Normal file
10
src/assets/icons/durability.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12.3726 5.6999H20.1C20.3387 5.6999 20.5676 5.79472 20.7364 5.96351C20.9052 6.13229 21 6.36121 21 6.5999V19.1999C21 19.4386 20.9052 19.6675 20.7364 19.8363C20.5676 20.0051 20.3387 20.0999 20.1 20.0999H3.9C3.66131 20.0999 3.43239 20.0051 3.2636 19.8363C3.09482 19.6675 3 19.4386 3 19.1999V4.7999C3 4.56121 3.09482 4.33229 3.2636 4.16351C3.43239 3.99472 3.66131 3.8999 3.9 3.8999H10.5726L12.3726 5.6999ZM4.8 5.6999V18.2999H19.2V7.4999H11.6274L9.8274 5.6999H4.8ZM8.4 9.2999H15.6V13.7135C15.6 14.5163 15.1986 15.2651 14.5317 15.7106L12 17.3981L9.4683 15.7106C9.13962 15.4914 8.87015 15.1944 8.68379 14.846C8.49743 14.4976 8.39995 14.1086 8.4 13.7135V9.2999ZM10.2 13.7135C10.2 13.9142 10.2999 14.1014 10.4673 14.213L12 15.2345L13.5327 14.213C13.6149 14.1582 13.6824 14.0839 13.729 13.9968C13.7756 13.9096 13.8 13.8123 13.8 13.7135V11.0999H10.2V13.7135Z"
|
||||
fill="#969696" />
|
||||
</svg>
|
After Width: | Height: | Size: 999 B |
13
src/assets/icons/info.svg
Normal file
13
src/assets/icons/info.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C7.85786 4.5 4.5 7.85786 4.5 12C4.5 16.1421 7.85786 19.5 12 19.5ZM13.339 15.8503L13.4588 15.3607C13.3968 15.3898 13.2968 15.4231 13.1598 15.461C13.0224 15.4988 12.8987 15.5181 12.79 15.5181C12.5584 15.5181 12.3954 15.4802 12.3008 15.4039C12.2068 15.3276 12.16 15.1841 12.16 14.9739C12.16 14.8906 12.1741 14.7665 12.2037 14.604C12.2323 14.4405 12.2653 14.2951 12.3019 14.168L12.749 12.5851C12.7928 12.4398 12.8229 12.2801 12.839 12.1058C12.8555 11.9319 12.8632 11.8102 12.8632 11.7412C12.8632 11.4074 12.7462 11.1365 12.5121 10.9275C12.278 10.7187 11.9447 10.6143 11.5128 10.6143C11.2724 10.6143 11.0183 10.6571 10.7493 10.7424C10.4804 10.8275 10.1992 10.9301 9.90505 11.0498L9.78498 11.5398C9.87263 11.5074 9.97703 11.4725 10.0992 11.4364C10.2208 11.4005 10.3401 11.3819 10.4562 11.3819C10.6932 11.3819 10.8528 11.4223 10.9364 11.5019C11.0201 11.5817 11.0621 11.7236 11.0621 11.9266C11.0621 12.0388 11.0488 12.1635 11.0213 12.299C10.9941 12.4354 10.9601 12.5796 10.9202 12.7318L10.4711 14.321C10.4312 14.488 10.402 14.6374 10.3836 14.7701C10.3654 14.9026 10.3567 15.0327 10.3567 15.1591C10.3567 15.4858 10.4774 15.755 10.7187 15.9675C10.96 16.1792 11.2983 16.2857 11.7332 16.2857C12.0165 16.2857 12.2651 16.2487 12.479 16.1742C12.6927 16.1 12.9797 15.9921 13.339 15.8503ZM13.2594 9.42024C13.4682 9.22658 13.5722 8.99105 13.5722 8.71526C13.5722 8.44009 13.4684 8.20409 13.2594 8.00797C13.051 7.81239 12.7999 7.71429 12.5063 7.71429C12.2117 7.71429 11.9596 7.81216 11.7493 8.00797C11.5389 8.20409 11.4336 8.44001 11.4336 8.71526C11.4336 8.99105 11.5389 9.2265 11.7493 9.42024C11.96 9.6146 12.2117 9.71186 12.5063 9.71186C12.8 9.71186 13.051 9.6146 13.2594 9.42024Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.4" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
11
src/assets/icons/preset.svg
Normal file
11
src/assets/icons/preset.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg
|
||||
width="61"
|
||||
height="74"
|
||||
viewBox="0 0 61 74"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M37 -7L0 15.2V59.46L37 81.66L74 59.46V15.2L37 -7ZM68.12 16.33L47.83 28.5L39 23.2V-1.14L68.12 16.33ZM46 40.4L40.88 37.33L46 34.26V40.4ZM28 34.26L33.12 37.33L28 40.4V34.26ZM39 33.8V27.86L43.95 30.83L39 33.8ZM35 33.8L30.05 30.83L35 27.86V33.8ZM35 40.86V46.8L30.05 43.83L35 40.86ZM39 40.86L43.95 43.83L39 46.8V40.86ZM35 -1.14V23.2L26.17 28.5L5.88 16.33L35 -1.14ZM4 19.86L24 31.86V42.8L4 54.8V19.86ZM5.88 58.33L26.17 46.16L35 51.46V75.8L5.88 58.33ZM39 75.8V51.46L47.83 46.16L68.12 58.33L39 75.8ZM70 54.8L50 42.8V31.86L70 19.86V54.8Z"
|
||||
fill="currentColor"
|
||||
fill-opacity="0.1" />
|
||||
</svg>
|
After Width: | Height: | Size: 707 B |
10
src/assets/icons/request-duration.svg
Normal file
10
src/assets/icons/request-duration.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M17.0562 6.5713L18.3639 5.2636L19.6365 6.5362L18.3288 7.8439C19.621 9.46135 20.2448 11.5122 20.0722 13.5752C19.8995 15.6382 18.9435 17.5569 17.4004 18.937C15.8574 20.3172 13.8444 21.0542 11.775 20.9966C9.70555 20.939 7.73671 20.0912 6.27283 18.6273C4.80895 17.1634 3.96114 15.1946 3.90353 13.1251C3.84592 11.0557 4.58288 9.04271 5.96306 7.49966C7.34323 5.9566 9.26185 5.00057 11.3249 4.82792C13.3879 4.65527 15.4388 5.2791 17.0562 6.5713ZM12 19.2001C12.8273 19.2001 13.6466 19.0371 14.4109 18.7205C15.1753 18.4039 15.8698 17.9399 16.4548 17.3549C17.0398 16.7699 17.5038 16.0754 17.8204 15.311C18.1371 14.5467 18.3 13.7274 18.3 12.9001C18.3 12.0728 18.1371 11.2535 17.8204 10.4892C17.5038 9.72484 17.0398 9.03033 16.4548 8.44532C15.8698 7.86032 15.1753 7.39626 14.4109 7.07966C13.6466 6.76305 12.8273 6.6001 12 6.6001C10.3291 6.6001 8.72671 7.26385 7.54523 8.44532C6.36375 9.6268 5.7 11.2292 5.7 12.9001C5.7 14.571 6.36375 16.1734 7.54523 17.3549C8.72671 18.5363 10.3291 19.2001 12 19.2001ZM11.1 8.4001H12.9V13.8001H11.1V8.4001ZM8.4 2.1001H15.6V3.9001H8.4V2.1001Z"
|
||||
fill="#969696" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -10,7 +10,7 @@
|
||||
border-right: 12px solid transparent;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
& {
|
||||
|
@ -1,87 +1,72 @@
|
||||
.cardNumber {
|
||||
border-radius: var(--codex-border-radius);
|
||||
border: 1px solid var(--codex-border-color);
|
||||
font-family: var(--codex-font-family);
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: rgb(56 56 56);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card-number {
|
||||
--codex-card-number-label-color: #7b7b7b;
|
||||
--codex-card-number-unit-color: #969696;
|
||||
|
||||
.cardNumber-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
&[aria-invalid] {
|
||||
--codex-card-number-label-color: var(--codex-input-color-error);
|
||||
--codex-card-number-unit-color: var(--codex-input-color-error);
|
||||
}
|
||||
|
||||
.cardNumber--error {
|
||||
border-color: rgb(var(--codex-color-error));
|
||||
}
|
||||
|
||||
.cardNumber-errorText,
|
||||
.cardNumber-helperText {
|
||||
height: 2rem;
|
||||
display: inline-block;
|
||||
margin-top: 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.cardNumber-errorText {
|
||||
color: rgb(var(--codex-color-error));
|
||||
}
|
||||
|
||||
.cardNumber-title {
|
||||
display: inline-block;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cardNumber-data {
|
||||
font-size: 2rem;
|
||||
color: var(--codex-color-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.cardNumber-data:focus-visible {
|
||||
outline: 1px solid var(--codex-border-color);
|
||||
outline-offset: 0.25rem;
|
||||
}
|
||||
|
||||
.cardNumber-dataContainer {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
label {
|
||||
color: var(--codex-card-number-label-color);
|
||||
}
|
||||
|
||||
input {
|
||||
font-family: Inter;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
line-height: 32px;
|
||||
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 {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
/* --codex-button-icon-background: var(--codex-color-primary); */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cardNumber-dataContainer .button-icon {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
svg {
|
||||
color: var(--codex-card-number-unit-color);
|
||||
}
|
||||
|
||||
.cardNumber-tooltip {
|
||||
color: var(--codex-color-disabled);
|
||||
display: flex;
|
||||
}
|
||||
/* svg::after {
|
||||
content: attr(data-title);
|
||||
background-color: #2f2f2f;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
overflow: visible;
|
||||
} */
|
||||
|
||||
.cardNumber-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cardNumber .input {
|
||||
min-width: 0;
|
||||
width: 65px;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.cardNumber .inputGroup-select {
|
||||
height: 2.5rem;
|
||||
padding: 0.25rem 1rem;
|
||||
span {
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
color: var(--codex-card-number-unit-color);
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
right: 16px;
|
||||
}
|
||||
}
|
||||
|
@ -1,110 +1,36 @@
|
||||
import { ButtonIcon } from "@codex-storage/marketplace-ui-components";
|
||||
import { Input, Tooltip } from "@codex-storage/marketplace-ui-components";
|
||||
import "./CardNumbers.css";
|
||||
import { Check, CircleX, Pencil } from "lucide-react";
|
||||
import { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import InfoIcon from "../../assets/icons/info.svg?react";
|
||||
import { attributes } from "../../utils/attributes";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
data: string;
|
||||
unit: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onValidation?: (value: string) => string;
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* If true, the caret will be set at the end of the input
|
||||
* Default is true
|
||||
*/
|
||||
repositionCaret?: boolean;
|
||||
|
||||
title: string;
|
||||
id: string;
|
||||
helper: string;
|
||||
};
|
||||
|
||||
export function CardNumbers({
|
||||
title,
|
||||
data,
|
||||
id,
|
||||
unit,
|
||||
value,
|
||||
onValidation,
|
||||
onChange,
|
||||
helper,
|
||||
title,
|
||||
className = "",
|
||||
repositionCaret = true,
|
||||
helper,
|
||||
}: Props) {
|
||||
const [isDirty, setIsDirty] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const ref = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
const replaceCaret = useCallback(
|
||||
(el: HTMLElement) => {
|
||||
if (!repositionCaret) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Place the caret at the end of the element
|
||||
const target = document.createTextNode("");
|
||||
el.appendChild(target);
|
||||
// do not move caret if element was not focused
|
||||
const isTargetFocused = document.activeElement === el;
|
||||
if (target !== null && target.nodeValue !== null && isTargetFocused) {
|
||||
const sel = window.getSelection();
|
||||
if (sel !== null) {
|
||||
const range = document.createRange();
|
||||
range.setStart(target, target.nodeValue.length);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
if (el instanceof HTMLElement) el.focus();
|
||||
}
|
||||
},
|
||||
[repositionCaret]
|
||||
);
|
||||
|
||||
const updateText = useCallback(
|
||||
(text: string | null) => {
|
||||
const current = ref.current;
|
||||
|
||||
if (current && text) {
|
||||
current.textContent = text;
|
||||
replaceCaret(current);
|
||||
}
|
||||
},
|
||||
[replaceCaret, ref]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateText(data);
|
||||
setIsDirty(false);
|
||||
}, [data, updateText]);
|
||||
|
||||
const onEditingClick = () => {
|
||||
const current = ref.current;
|
||||
|
||||
if (isDirty) {
|
||||
onChange?.(current?.textContent || "");
|
||||
} else if (current) {
|
||||
current.focus();
|
||||
replaceCaret(current);
|
||||
}
|
||||
};
|
||||
|
||||
const onInput = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.currentTarget.textContent;
|
||||
|
||||
setIsDirty(text !== data);
|
||||
|
||||
if (!text) {
|
||||
setError("A value is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (text?.length > 10) {
|
||||
e.currentTarget.textContent = text.slice(0, 10);
|
||||
replaceCaret(e.currentTarget);
|
||||
setError("The value is too long");
|
||||
return;
|
||||
}
|
||||
|
||||
updateText(text);
|
||||
const onInternalChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.currentTarget.value;
|
||||
onChange(e.currentTarget.value);
|
||||
|
||||
const msg = onValidation?.(text);
|
||||
|
||||
@ -116,28 +42,24 @@ export function CardNumbers({
|
||||
setError("");
|
||||
};
|
||||
|
||||
const onBlur = () => {
|
||||
if (error === "") {
|
||||
if (isDirty) {
|
||||
onChange?.(ref.current?.textContent || "");
|
||||
}
|
||||
} else {
|
||||
updateText(data);
|
||||
}
|
||||
|
||||
setIsDirty(false);
|
||||
setError("");
|
||||
};
|
||||
|
||||
const Icon = error
|
||||
? () => <CircleX size={"1rem"} />
|
||||
: isDirty
|
||||
? () => <Check size={"1rem"} />
|
||||
: () => <Pencil size={"1rem"} />;
|
||||
|
||||
return (
|
||||
<div className={classnames(["cardNumber-container"], [className])}>
|
||||
<div
|
||||
className={classnames(["card-number cardNumber-container"], [className])}
|
||||
{...attributes({ "aria-invalid": !!error })}>
|
||||
<Input
|
||||
id={id}
|
||||
label={title}
|
||||
value={value}
|
||||
type="number"
|
||||
isInvalid={!!error}
|
||||
onChange={onInternalChange}></Input>
|
||||
|
||||
<Tooltip message={error || helper}>
|
||||
<InfoIcon></InfoIcon>
|
||||
</Tooltip>
|
||||
<span>{unit}</span>
|
||||
|
||||
{/* <div
|
||||
className={classnames(["cardNumber"], ["cardNumber--error", !!error])}>
|
||||
<div className="cardNumber-dataContainer">
|
||||
<>
|
||||
@ -163,7 +85,7 @@ export function CardNumbers({
|
||||
<small className="cardNumber-errorText">{error}</small>
|
||||
) : (
|
||||
<small className="cardNumber-helperText">{helper}</small>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -19,9 +19,10 @@ type Props = {
|
||||
purchaseCid: string;
|
||||
index: number;
|
||||
data: CodexDataContent[];
|
||||
onMetadata?: (requestId: string, metadata: FileMetadata) => void;
|
||||
};
|
||||
|
||||
export function FileCell({ requestId, purchaseCid, data }: Props) {
|
||||
export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) {
|
||||
const [cid, setCid] = useState(purchaseCid);
|
||||
const [metadata, setMetadata] = useState<FileMetadata>({
|
||||
filename: "-",
|
||||
@ -46,6 +47,11 @@ export function FileCell({ requestId, purchaseCid, data }: Props) {
|
||||
mimetype,
|
||||
uploadedAt,
|
||||
});
|
||||
onMetadata?.(requestId, {
|
||||
filename,
|
||||
mimetype,
|
||||
uploadedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -205,6 +205,7 @@ export function Files({ limit }: Props) {
|
||||
label="Folder"
|
||||
Icon={PlusIcon}
|
||||
variant="outline"
|
||||
size="small"
|
||||
disabled={!!error || !folder}
|
||||
onClick={onFolderCreate}></Button>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@ import { TruncateCell } from "../TruncateCell/TruncateCell";
|
||||
import { CodexPurchase } from "@codex-storage/sdk-js";
|
||||
import PurchaseHistoryIcon from "../../assets/icons/purchase-history-outline.svg?react";
|
||||
import { useState } from "react";
|
||||
import { PurchaseUtils } from "../StorageRequestSetup/purchase.util";
|
||||
import { PurchaseUtils } from "../Purchase/purchase.utils";
|
||||
|
||||
type Props = {
|
||||
purchases: CodexPurchase[];
|
||||
|
134
src/components/Purchase/PurchasesTable.tsx
Normal file
134
src/components/Purchase/PurchasesTable.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import {
|
||||
Cell,
|
||||
Row,
|
||||
Spinner,
|
||||
Table,
|
||||
TabSortState,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { Times } from "../../utils/times";
|
||||
import { useState } from "react";
|
||||
import { FileCell } from "../../components/FileCellRender/FileCell";
|
||||
import { useData } from "../../hooks/useData";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { CodexPurchase } from "@codex-storage/sdk-js";
|
||||
import { TruncateCell } from "../TruncateCell/TruncateCell";
|
||||
import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender";
|
||||
import { PurchaseUtils } from "./purchase.utils";
|
||||
|
||||
type Props = {};
|
||||
|
||||
type SortFn = (a: CodexPurchase, b: CodexPurchase) => number;
|
||||
|
||||
export function PurchasesTable({}: Props) {
|
||||
const [metadata, setMetadata] = useState<{ [key: string]: number }>({});
|
||||
const content = useData();
|
||||
const { data, isPending } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.marketplace()
|
||||
.purchases()
|
||||
.then((s) => Promises.rejectOnError(s)),
|
||||
queryKey: ["purchases"],
|
||||
|
||||
// No need to retry because if the connection to the node
|
||||
// is back again, all the queries will be invalidated.
|
||||
retry: false,
|
||||
|
||||
// The client node should be local, so display the cache value while
|
||||
// making a background request looks good.
|
||||
staleTime: 0,
|
||||
|
||||
// Refreshing when focus returns can be useful if a user comes back
|
||||
// to the UI after performing an operation in the terminal.
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
initialData: [],
|
||||
|
||||
// Throw the error to the error boundary
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
const onMetadata = (
|
||||
requestId: string,
|
||||
{ uploadedAt }: { uploadedAt: number }
|
||||
) => {
|
||||
setMetadata((m) => ({ ...m, [requestId]: uploadedAt }));
|
||||
setSortFn(() =>
|
||||
PurchaseUtils.sortByUploadedAt("desc", {
|
||||
...metadata,
|
||||
[requestId]: uploadedAt,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const [sortFn, setSortFn] = useState<SortFn>(() =>
|
||||
PurchaseUtils.sortByUploadedAt("desc", metadata)
|
||||
);
|
||||
|
||||
const onSortByDuration = (state: TabSortState) =>
|
||||
setSortFn(() => PurchaseUtils.sortByDuration(state));
|
||||
|
||||
const onSortByReward = (state: TabSortState) =>
|
||||
setSortFn(() => PurchaseUtils.sortByReward(state));
|
||||
|
||||
const onSortByState = (state: TabSortState) =>
|
||||
setSortFn(() => PurchaseUtils.sortByState(state));
|
||||
|
||||
const onSortByUploadedAt = (state: TabSortState) =>
|
||||
setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata));
|
||||
|
||||
const headers = [
|
||||
["file", onSortByUploadedAt],
|
||||
["request id"],
|
||||
["duration", onSortByDuration],
|
||||
["slots"],
|
||||
["reward", onSortByReward],
|
||||
["proof probability"],
|
||||
["state", onSortByState],
|
||||
] satisfies [string, ((state: TabSortState) => void)?][];
|
||||
|
||||
const sorted = sortFn ? [...data].sort(sortFn) : data;
|
||||
|
||||
const rows = sorted.map((p, index) => {
|
||||
const r = p.request;
|
||||
const ask = p.request.ask;
|
||||
const duration = parseInt(p.request.ask.duration, 10);
|
||||
const pf = parseInt(p.request.ask.proofProbability, 10);
|
||||
|
||||
return (
|
||||
<Row
|
||||
cells={[
|
||||
<FileCell
|
||||
requestId={r.id}
|
||||
purchaseCid={r.content.cid}
|
||||
index={index}
|
||||
data={content}
|
||||
onMetadata={onMetadata}
|
||||
/>,
|
||||
<TruncateCell value={r.id} />,
|
||||
<Cell>{Times.pretty(duration)}</Cell>,
|
||||
<Cell>{ask.slots.toString()}</Cell>,
|
||||
<Cell>{ask.reward + " CDX"}</Cell>,
|
||||
<Cell>{pf.toString()}</Cell>,
|
||||
<CustomStateCellRender state={p.state} message={p.error} />,
|
||||
]}></Row>
|
||||
);
|
||||
});
|
||||
|
||||
console.info(metadata);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="purchases-loader">
|
||||
<Spinner width="3rem" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table headers={headers} rows={rows} defaultSortIndex={0} />
|
||||
</>
|
||||
);
|
||||
}
|
43
src/components/Purchase/purchase.utils.ts
Normal file
43
src/components/Purchase/purchase.utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { TabSortState } from "@codex-storage/marketplace-ui-components"
|
||||
import { CodexPurchase } from "@codex-storage/sdk-js"
|
||||
|
||||
export const PurchaseUtils = {
|
||||
sortById: (state: TabSortState) =>
|
||||
(a: CodexPurchase, b: CodexPurchase) => {
|
||||
|
||||
return state === "desc"
|
||||
? b.requestId
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(a.requestId.toLocaleLowerCase())
|
||||
: a.requestId
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(b.requestId.toLocaleLowerCase())
|
||||
},
|
||||
sortByState: (state: TabSortState) =>
|
||||
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
|
||||
? b.state
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(a.state.toLocaleLowerCase())
|
||||
: a.state
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(b.state.toLocaleLowerCase())
|
||||
,
|
||||
sortByDuration: (state: TabSortState) =>
|
||||
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
|
||||
? Number(b.request.ask.duration) - Number(a.request.ask.duration)
|
||||
: Number(a.request.ask.duration) - Number(b.request.ask.duration)
|
||||
,
|
||||
sortByReward: (state: TabSortState) =>
|
||||
(a: CodexPurchase, b: CodexPurchase) => state === "desc"
|
||||
? Number(b.request.ask.reward) - Number(a.request.ask.reward)
|
||||
: Number(a.request.ask.reward) - Number(b.request.ask.reward)
|
||||
,
|
||||
sortByUploadedAt: (state: TabSortState, table: Record<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)
|
||||
}
|
||||
,
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import { useStorageRequestMutation } from "./useStorageRequestMutation";
|
||||
import { Plus } from "lucide-react";
|
||||
import "./StorageRequestCreate.css";
|
||||
import { StorageRequestError } from "./StorageRequestError";
|
||||
import PurchaseIcon from "../../assets/icons/purchase.svg?react";
|
||||
|
||||
const CONFIRM_STATE = 2;
|
||||
|
||||
@ -35,7 +36,7 @@ export function StorageRequestCreate() {
|
||||
const [storageRequest, setStorageRequest] = useState<StorageRequest>(
|
||||
defaultStorageRequest
|
||||
);
|
||||
const steps = useRef(["File", "Criteria", "Success"]);
|
||||
const steps = useRef(["Select File", "Select Request Criteria", "Success"]);
|
||||
const { state, dispatch } = useStepperReducer();
|
||||
const { mutateAsync, error } = useStorageRequestMutation(dispatch, state);
|
||||
|
||||
@ -117,15 +118,21 @@ export function StorageRequestCreate() {
|
||||
const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="storage-request">
|
||||
<Button
|
||||
label="Storage Request"
|
||||
Icon={Plus}
|
||||
onClick={onOpen}
|
||||
variant="primary"
|
||||
variant="outline"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Modal open={state.open} onClose={onClose} displayCloseButton={false}>
|
||||
<Modal
|
||||
title="Storage request"
|
||||
Icon={PurchaseIcon}
|
||||
open={state.open}
|
||||
onClose={onClose}
|
||||
displayCloseButton={false}>
|
||||
<Stepper
|
||||
titles={steps.current}
|
||||
state={state}
|
||||
@ -133,7 +140,6 @@ export function StorageRequestCreate() {
|
||||
duration={STEPPER_DURATION}
|
||||
onNextStep={onNextStep}
|
||||
backLabel={backLabel}
|
||||
className="storageRequestCreate"
|
||||
nextLabel={nextLabel}>
|
||||
<Body
|
||||
dispatch={dispatch}
|
||||
@ -144,6 +150,6 @@ export function StorageRequestCreate() {
|
||||
/>
|
||||
</Stepper>
|
||||
</Modal>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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%;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import "./StorageRequestFileChooser.css";
|
||||
import { ChangeEvent, useEffect } from "react";
|
||||
import { classnames } from "../../utils/classnames";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
@ -11,6 +10,8 @@ import {
|
||||
import { useData } from "../../hooks/useData";
|
||||
import { StorageRequestComponentProps } from "./types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import ChooseCidIcon from "../../assets/icons/choose-cid.svg?react";
|
||||
import UploadIcon from "../../assets/icons/upload.svg?react";
|
||||
|
||||
export function StorageRequestFileChooser({
|
||||
storageRequest,
|
||||
@ -55,49 +56,42 @@ export function StorageRequestFileChooser({
|
||||
}) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="storageRequest-title">Choose a CID</span>
|
||||
|
||||
<label className="label" htmlFor="cid">
|
||||
CID
|
||||
</label>
|
||||
<div className="file-chooser">
|
||||
<header>
|
||||
<ChooseCidIcon></ChooseCidIcon>
|
||||
<h6>Choose a CID</h6>
|
||||
</header>
|
||||
|
||||
<Dropdown
|
||||
label=""
|
||||
id="cid"
|
||||
placeholder="Select or type your CID"
|
||||
placeholder="CID"
|
||||
onChange={onChange}
|
||||
value={storageRequest.cid}
|
||||
options={options}
|
||||
onSelected={onSelected}
|
||||
className={classnames(
|
||||
["storageRequestFileChooser-dropdown"],
|
||||
["storageRequestFileChooser-dropdown-success", !!storageRequest.cid]
|
||||
)}
|
||||
size="medium"
|
||||
/>
|
||||
|
||||
<div className="storageRequestFileChooser-separator">
|
||||
<hr className="storageRequestFileChooser-hr" />
|
||||
<span className="storageRequestFileChooser-or">OR</span>
|
||||
<hr className="storageRequestFileChooser-hr" />
|
||||
<div className="row gap">
|
||||
<hr />
|
||||
<span>OR</span>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<span className="storageRequest-title">
|
||||
<div>
|
||||
<span>Upload a file</span>
|
||||
<div className="row gap">
|
||||
<UploadIcon width={24} color="#969696"></UploadIcon>
|
||||
<h6>Upload</h6>
|
||||
</div>
|
||||
<span className="input-helper-text text-secondary">
|
||||
The CID will be automatically copied after your upload.
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<Upload
|
||||
onSuccess={onSuccess}
|
||||
editable={false}
|
||||
multiple={false}
|
||||
onDeleteItem={onDelete}
|
||||
codexData={CodexSdk.data()}
|
||||
successMessage={"Success, the CID has been copied to the field on top."}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,155 @@
|
||||
.request-review {
|
||||
> header {
|
||||
border-bottom: 1px solid #96969633;
|
||||
padding-bottom: 16px;
|
||||
|
||||
div {
|
||||
line-height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
> div {
|
||||
height: 74px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> div:first-child {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
line-height: 12.1px;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
small {
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.011em;
|
||||
color: #969696cc;
|
||||
}
|
||||
}
|
||||
|
||||
> div:nth-child(n + 2) {
|
||||
--codex-preset-border-color: #494949;
|
||||
--codex-preset-color: #969696;
|
||||
border: 1px solid var(--codex-preset-border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
cursor: pointer;
|
||||
transition: 0.35s box-shadow;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 2px var(--codex-preset-border-color);
|
||||
}
|
||||
|
||||
&[aria-selected] {
|
||||
--codex-preset-border-color: #6fcb94;
|
||||
--codex-preset-color: #6fcb94;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
color: var(--codex-preset-color);
|
||||
}
|
||||
|
||||
span {
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
text-align: left;
|
||||
color: var(--codex-preset-color);
|
||||
|
||||
+ span {
|
||||
background: #6fcb9433;
|
||||
padding: 2px 8px;
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
line-height: 12px;
|
||||
letter-spacing: 0.02em;
|
||||
color: #6fcb94;
|
||||
border-radius: 16px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.row {
|
||||
border-top: 1px solid #96969633;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding-top: 16px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
|
||||
@media (max-width: 800px) {
|
||||
& {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.storageRequestReview-presets {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.storageRequestReview-presets-blocs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.storageRequestReview-alert {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.storageRequestReview-expiration {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 801px) {
|
||||
& {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.storageRequestReview-hr {
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 0rem;
|
||||
@ -5,7 +157,7 @@
|
||||
|
||||
.storageRequestReview-numbers {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.storageRequestReview-range {
|
||||
|
@ -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<number>(1);
|
||||
const [durability, setDurability] = useState<number>(2);
|
||||
|
||||
const isInvalidConstrainst = useCallback(
|
||||
(nodes: number, tolerance: number) => {
|
||||
@ -124,7 +126,7 @@ export function StorageRequestReview({
|
||||
};
|
||||
|
||||
const isInvalidAvailability = (data: string) => {
|
||||
const [value, unit = "days"] = data.split(" ");
|
||||
const [value] = data.split(" ");
|
||||
|
||||
const error = isInvalidNumber(value);
|
||||
|
||||
@ -136,9 +138,9 @@ export function StorageRequestReview({
|
||||
// unit += "s";
|
||||
// }
|
||||
|
||||
if (!units.includes(unit)) {
|
||||
return "Invalid unit must one of: minutes, hours, days, months, years";
|
||||
}
|
||||
// if (!units.includes(unit)) {
|
||||
// return "Invalid unit must one of: minutes, hours, days, months, years";
|
||||
// }
|
||||
|
||||
return "";
|
||||
};
|
||||
@ -156,7 +158,7 @@ export function StorageRequestReview({
|
||||
onUpdateDurability({ proofProbability: Number(value) });
|
||||
|
||||
const onAvailabilityChange = (value: string) => {
|
||||
const [availability, availabilityUnit = "days"] = value.split(" ");
|
||||
const [availability] = value.split(" ");
|
||||
|
||||
// if (!availabilityUnit.endsWith("s")) {
|
||||
// availabilityUnit += "s";
|
||||
@ -164,7 +166,7 @@ export function StorageRequestReview({
|
||||
|
||||
onStorageRequestChange({
|
||||
availability: Number(availability),
|
||||
availabilityUnit: availabilityUnit as AvailabilityUnit,
|
||||
availabilityUnit: "months",
|
||||
});
|
||||
};
|
||||
|
||||
@ -189,157 +191,148 @@ export function StorageRequestReview({
|
||||
// return data.availabilityUnit;
|
||||
// };
|
||||
|
||||
const availability = `${storageRequest.availability} ${storageRequest.availabilityUnit}`;
|
||||
const availability = storageRequest.availability;
|
||||
|
||||
return (
|
||||
<div className="request-review">
|
||||
<header>
|
||||
<DurabilityIcon></DurabilityIcon>
|
||||
<div>
|
||||
<span className="storageRequest-title">Durability</span>
|
||||
<div className="storageRequestReview-numbers">
|
||||
<CardNumbers
|
||||
title={"Nodes"}
|
||||
data={storageRequest.nodes.toString()}
|
||||
onChange={onNodesChange}
|
||||
onValidation={isInvalidNodes}
|
||||
helper="Number of storage nodes"></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Tolerance"}
|
||||
data={storageRequest.tolerance.toString()}
|
||||
onChange={onToleranceChange}
|
||||
onValidation={isInvalidTolerance}
|
||||
helper="Failure node tolerated"></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Proof probability"}
|
||||
data={storageRequest.proofProbability.toString()}
|
||||
onChange={onProofProbabilityChange}
|
||||
helper="Proof request frequency"></CardNumbers>
|
||||
</div>
|
||||
|
||||
<div className="storageRequestReview-presets">
|
||||
<div className="storageRequestReview-presets-title">
|
||||
<b>Define your durability profile</b>
|
||||
<p>
|
||||
<h6>Define your Durability Profile</h6>
|
||||
<small>
|
||||
Select the appropriate level of data storage reliability ensuring
|
||||
your information is protected and accessible.
|
||||
</p>
|
||||
</small>
|
||||
</div>
|
||||
<div className="storageRequestReview-presets-blocs">
|
||||
<div
|
||||
onClick={() => onDurabilityChange(0)}
|
||||
className={classnames(
|
||||
["storageRequestReview-presets-bloc"],
|
||||
[
|
||||
"storageRequestReview-presets--selected",
|
||||
durability <= 0 || durability > 3,
|
||||
]
|
||||
)}>
|
||||
<div className="storageRequestReview-presets-blocIcon">
|
||||
<img src="/shape-1.png" width={48} />
|
||||
</header>
|
||||
<main>
|
||||
<div className="presets">
|
||||
<div>
|
||||
<AlphaIcon width={20} color="#6FCB94"></AlphaIcon>
|
||||
<div>
|
||||
<span>Durability</span>
|
||||
<small>Suggested Defaults</small>
|
||||
</div>
|
||||
<p>Custom</p>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDurabilityChange(1)}
|
||||
className={classnames(
|
||||
["storageRequestReview-presets-bloc"],
|
||||
["storageRequestReview-presets--selected", durability === 1]
|
||||
)}>
|
||||
<div className="storageRequestReview-presets-blocIcon">
|
||||
<img src="/shape-2.png" width={48} />
|
||||
</div>
|
||||
<p>Low</p>
|
||||
{...attributes({
|
||||
"aria-selected": durability <= 0 || durability > 3,
|
||||
})}
|
||||
onClick={() => onDurabilityChange(0)}>
|
||||
<span>Custom</span>
|
||||
<PresetIcon></PresetIcon>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDurabilityChange(2)}
|
||||
className={classnames(
|
||||
["storageRequestReview-presets-bloc"],
|
||||
["storageRequestReview-presets--selected", durability === 2]
|
||||
)}>
|
||||
<div className="storageRequestReview-presets-blocIcon">
|
||||
<img src="/shape-3.png" width={48} />
|
||||
</div>
|
||||
<p>Medium</p>
|
||||
{...attributes({
|
||||
"aria-selected": durability == 1,
|
||||
})}
|
||||
onClick={() => onDurabilityChange(1)}>
|
||||
<span>Low</span>
|
||||
<PresetIcon></PresetIcon>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDurabilityChange(3)}
|
||||
className={classnames(
|
||||
["storageRequestReview-presets-bloc"],
|
||||
["storageRequestReview-presets--selected", durability === 3]
|
||||
)}>
|
||||
<div className="storageRequestReview-presets-blocIcon">
|
||||
<img src="/shape-4.png" width={48} />
|
||||
</div>
|
||||
<p>High</p>
|
||||
{...attributes({
|
||||
"aria-selected": durability == 2,
|
||||
})}
|
||||
onClick={() => onDurabilityChange(2)}>
|
||||
<span>Medium</span>
|
||||
<span>Recommanded</span>
|
||||
<PresetIcon></PresetIcon>
|
||||
</div>
|
||||
<div
|
||||
{...attributes({
|
||||
"aria-selected": durability == 3,
|
||||
})}
|
||||
onClick={() => onDurabilityChange(3)}>
|
||||
<span>High</span>
|
||||
<PresetIcon></PresetIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <Range
|
||||
onChange={onDurabilityRangeChange}
|
||||
className={classnames(
|
||||
["storageRequestReview-range"],
|
||||
["storageRequestReview-range--disabled", durability === 0]
|
||||
)}
|
||||
labels={["Weak", "Low", "Medium", "High", "Confident"]}
|
||||
max={5}
|
||||
label=""
|
||||
value={durability}
|
||||
/> */}
|
||||
|
||||
<span className="storageRequest-title">Commitment</span>
|
||||
|
||||
<div className="storageRequestReview-numbers">
|
||||
<div className="grid">
|
||||
<CardNumbers
|
||||
title={"Contract duration"}
|
||||
data={availability}
|
||||
helper="Minimal number of nodes the content should be stored on."
|
||||
id="nodes"
|
||||
unit={"Nodes"}
|
||||
value={storageRequest.nodes.toString()}
|
||||
onChange={onNodesChange}
|
||||
onValidation={isInvalidNodes}
|
||||
title="Number of storage nodes"></CardNumbers>
|
||||
<CardNumbers
|
||||
id="tolerance"
|
||||
unit={"Tolerance Multiplier"}
|
||||
value={storageRequest.tolerance.toString()}
|
||||
onChange={onToleranceChange}
|
||||
onValidation={isInvalidTolerance}
|
||||
title="Failure Node Tolerated"
|
||||
helper="Additional number of nodes on top of the nodes property that can be lost before pronouncing the content lost."></CardNumbers>
|
||||
<CardNumbers
|
||||
helper="How often storage proofs are required."
|
||||
id="proof-request"
|
||||
unit={"Frequency Level"}
|
||||
value={storageRequest.proofProbability.toString()}
|
||||
onChange={onProofProbabilityChange}
|
||||
title="Proof Request Frequency"></CardNumbers>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<CommitmentIcon></CommitmentIcon>
|
||||
<h6>Commitment</h6>
|
||||
</div>
|
||||
|
||||
<div className="grid">
|
||||
<CardNumbers
|
||||
helper="The duration of the request in months"
|
||||
id="duration"
|
||||
title={"Full period of the contract"}
|
||||
value={availability.toString()}
|
||||
onChange={onAvailabilityChange}
|
||||
onValidation={isInvalidAvailability}
|
||||
repositionCaret={false}
|
||||
helper="Full period of the contract"></CardNumbers>
|
||||
unit="Contract duration"></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Collateral"}
|
||||
data={storageRequest.collateral.toString()}
|
||||
helper="Represents how much collateral is asked from hosts that wants to fill a slots"
|
||||
id="collateral"
|
||||
unit={"Collateral"}
|
||||
value={storageRequest.collateral.toString()}
|
||||
onChange={onCollateralChange}
|
||||
onValidation={isInvalidNumber}
|
||||
helper="Reward tokens for hosts"></CardNumbers>
|
||||
title="Reward tokens for hosts"></CardNumbers>
|
||||
<CardNumbers
|
||||
title={"Reward"}
|
||||
data={storageRequest.reward.toString()}
|
||||
helper="The maximum amount of tokens paid per second per slot to hosts the client is willing to pay."
|
||||
id="reward"
|
||||
unit={"Reward"}
|
||||
value={storageRequest.reward.toString()}
|
||||
onChange={onRewardChange}
|
||||
onValidation={isInvalidNumber}
|
||||
helper="Penality tokens"></CardNumbers>
|
||||
<div className="storageRequest-price"></div>
|
||||
{/* <Range
|
||||
className={classnames(
|
||||
["storageRequestReview-range"],
|
||||
["storageRequestReview-range--disabled", price === 0]
|
||||
)}
|
||||
labels={["Low", "Average", "Attractive"]}
|
||||
max={100}
|
||||
label=""
|
||||
onChange={onPriceRangeChange}
|
||||
/> */}
|
||||
title="Penality tokens"></CardNumbers>
|
||||
</div>
|
||||
|
||||
<hr className="storageRequestReview-hr" />
|
||||
<div className="row">
|
||||
<RequestDurationIcon></RequestDurationIcon>
|
||||
<h6>Request Duration</h6>
|
||||
</div>
|
||||
|
||||
<div className="storageRequestReview-alert">
|
||||
<footer>
|
||||
<CardNumbers
|
||||
title={"Expiration"}
|
||||
data={storageRequest.expiration.toString()}
|
||||
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. "
|
||||
id="expiration"
|
||||
unit={"Expiration"}
|
||||
value={storageRequest.expiration.toString()}
|
||||
onChange={onExpirationChange}
|
||||
className="storageRequestReview-expiration"
|
||||
onValidation={isInvalidNumber}
|
||||
helper="Request expiration in seconds"></CardNumbers>
|
||||
title="Request expiration in seconds"></CardNumbers>
|
||||
<Alert
|
||||
Icon={<FileWarning />}
|
||||
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.
|
||||
If no suitable hosts are found for the CID{" "}
|
||||
{Strings.shortId(storageRequest.cid)} matching your storage
|
||||
requirements, you will incur a charge a small amount of tokens.
|
||||
</Alert>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { TabSortState } from "@codex-storage/marketplace-ui-components"
|
||||
import { CodexPurchase } from "@codex-storage/sdk-js"
|
||||
|
||||
export const PurchaseUtils = {
|
||||
sortById: (state: TabSortState) =>
|
||||
(a: CodexPurchase, b: CodexPurchase) => {
|
||||
|
||||
return state === "desc"
|
||||
? b.requestId
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(a.requestId.toLocaleLowerCase())
|
||||
: a.requestId
|
||||
.toLocaleLowerCase()
|
||||
.localeCompare(b.requestId.toLocaleLowerCase())
|
||||
},
|
||||
}
|
@ -142,6 +142,10 @@ ul {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
|
@ -70,7 +70,7 @@
|
||||
|
||||
.button {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
bottom: 0px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
|
@ -1,12 +1,16 @@
|
||||
.purchases-modal {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.purchases-actions {
|
||||
padding: 1rem;
|
||||
.purchases {
|
||||
> div:first-child {
|
||||
padding: 1rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.table {
|
||||
table thead tr th {
|
||||
background-color: #14141499;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.purchases-loader {
|
||||
|
@ -1,100 +1,20 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
import {
|
||||
Cell,
|
||||
Row,
|
||||
Spinner,
|
||||
Table,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate";
|
||||
import "./purchases.css";
|
||||
import { FileCell } from "../../components/FileCellRender/FileCell";
|
||||
import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender";
|
||||
import { Promises } from "../../utils/promises";
|
||||
import { TruncateCell } from "../../components/TruncateCell/TruncateCell";
|
||||
import { Times } from "../../utils/times";
|
||||
import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
import { useData } from "../../hooks/useData";
|
||||
import { PurchasesTable } from "../../components/Purchase/PurchasesTable";
|
||||
|
||||
const Purchases = () => {
|
||||
const content = useData();
|
||||
const { data, isPending } = useQuery({
|
||||
queryFn: () =>
|
||||
CodexSdk.marketplace()
|
||||
.purchases()
|
||||
.then((s) => Promises.rejectOnError(s)),
|
||||
queryKey: ["purchases"],
|
||||
|
||||
// No need to retry because if the connection to the node
|
||||
// is back again, all the queries will be invalidated.
|
||||
retry: false,
|
||||
|
||||
// The client node should be local, so display the cache value while
|
||||
// making a background request looks good.
|
||||
staleTime: 0,
|
||||
|
||||
// Refreshing when focus returns can be useful if a user comes back
|
||||
// to the UI after performing an operation in the terminal.
|
||||
refetchOnWindowFocus: true,
|
||||
|
||||
initialData: [],
|
||||
|
||||
// Throw the error to the error boundary
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="purchases-loader">
|
||||
<Spinner width="3rem" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Row
|
||||
cells={[
|
||||
<FileCell
|
||||
requestId={r.id}
|
||||
purchaseCid={r.content.cid}
|
||||
index={index}
|
||||
data={content}
|
||||
/>,
|
||||
<TruncateCell value={r.id} />,
|
||||
<Cell>{Times.pretty(duration)}</Cell>,
|
||||
<Cell>{ask.slots.toString()}</Cell>,
|
||||
<Cell>{ask.reward + " CDX"}</Cell>,
|
||||
<Cell>{pf.toString()}</Cell>,
|
||||
<CustomStateCellRender state={p.state} message={p.error} />,
|
||||
]}></Row>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="purchases-actions">
|
||||
<div className="purchases">
|
||||
<div>
|
||||
<StorageRequestCreate />
|
||||
</div>
|
||||
|
||||
<Table headers={headers} rows={rows} />
|
||||
<div className="card">
|
||||
<PurchasesTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user