diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 08f81bb..ed7f7de 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -8,9 +8,9 @@ on: workflow_dispatch: env: - codex_version: v0.1.8 - circuit_version: v0.1.8 - marketplace_address: "0x5Bd66fA15Eb0E546cd26808248867a572cFF5706" + codex_version: v0.1.9 + circuit_version: v0.1.9 + marketplace_address: "0xAB03b6a58C5262f530D54146DA2a552B1C0F7648" eth_provider: "https://rpc.testnet.codex.storage" VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }} VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }} diff --git a/e2e/availabilities.spec.ts b/e2e/availabilities.spec.ts index c64a67d..ff43c45 100644 --- a/e2e/availabilities.spec.ts +++ b/e2e/availabilities.spec.ts @@ -1,11 +1,16 @@ import test, { expect } from "@playwright/test"; +import { Bytes } from "../src/utils/bytes" +import { GB } from "../src/utils/constants" test('create an availability', async ({ page }) => { await page.goto('/dashboard/availabilities'); await page.waitForTimeout(500); await page.locator('.availability-edit button').first().click(); await page.getByLabel('Total size').click(); - await page.getByLabel('Total size').fill('0.50'); + + const value = (Math.random() * 0.5) + 0.1; + + await page.getByLabel('Total size').fill(value.toFixed(1)); await page.getByLabel('Duration').click(); await page.getByLabel('Duration').fill('30'); await page.getByLabel('Min price').click(); @@ -20,7 +25,7 @@ test('create an availability', async ({ page }) => { await page.getByRole('button', { name: 'Next' }).click(); await expect(page.getByText('Success', { exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); - await expect(page.getByText('512.0 MB allocated for the').first()).toBeVisible(); + await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible(); }) test('availability navigation buttons', async ({ page }) => { @@ -54,3 +59,63 @@ test('availability navigation buttons', async ({ page }) => { await page.getByRole('button', { name: 'Finish' }).click(); await expect(page.locator('.modal--open')).not.toBeVisible(); }) + +test('create an availability with changing the duration to months', async ({ page }) => { + await page.goto('/dashboard/availabilities'); + await page.waitForTimeout(500); + await page.locator('.availability-edit button').first().click(); + await page.getByLabel('Total size').click(); + + await page.getByLabel('Total size').fill("0.1"); + await page.getByLabel('Duration').click(); + await page.getByLabel('Duration').fill("3"); + await page.getByRole('combobox').nth(1).selectOption('months'); + + await page.getByLabel('Min price').click(); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Max collateral').click(); + await page.getByLabel('Max collateral').fill('30'); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Nickname').click(); + await page.getByLabel('Nickname').fill('test'); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Confirm your new sale')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.getByText("3 months").first()).toBeVisible(); +}) + + +test('create an availability after checking max size and invalid input', async ({ page }) => { + await page.goto('/dashboard/availabilities'); + await page.waitForTimeout(500); + await page.locator('.availability-edit button').first().click(); + await page.getByLabel('Total size').click(); + + + await page.getByLabel('Total size').fill("9999"); + await expect(page.getByLabel('Total size')).toHaveAttribute("aria-invalid"); + + await page.getByText("Use max size").click() + await expect(page.getByLabel('Total size')).not.toHaveAttribute("aria-invalid"); + + const value = (Math.random() * 0.5); + await page.getByLabel('Total size').fill(value.toFixed(1)); + + await page.getByLabel('Duration').click(); + await page.getByLabel('Duration').fill('30'); + await page.getByLabel('Min price').click(); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Max collateral').click(); + await page.getByLabel('Max collateral').fill('30'); + await page.getByLabel('Min price').fill('5'); + await page.getByLabel('Nickname').click(); + await page.getByLabel('Nickname').fill('test'); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Confirm your new sale')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Success', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.getByText(Bytes.pretty(parseFloat(value.toFixed(1)) * GB)).first()).toBeVisible(); +}) \ No newline at end of file diff --git a/e2e/storage-requests.spec.ts b/e2e/storage-requests.spec.ts index 23be51f..a04263d 100644 --- a/e2e/storage-requests.spec.ts +++ b/e2e/storage-requests.spec.ts @@ -76,3 +76,58 @@ test('remove the CID when the file is deleted', async ({ page }) => { await page.locator('.button-icon--small').nth(1).click(); await expect(page.locator('#cid')).toBeEmpty() }) + +test('create a storage request by using decimal values', async ({ page }) => { + await page.goto('/dashboard'); + await page.locator('a').filter({ hasText: 'Purchases' }).click(); + await page.getByRole('button', { name: 'Storage Request' }).click(); + + await page.locator('div').getByTestId("upload").setInputFiles([ + path.join(__dirname, "assets", 'chatgpt.jpg'), + ]); + await expect(page.locator('#cid')).not.toBeEmpty() + await expect(page.getByText('Success, the CID has been')).toBeVisible(); + await page.getByRole('button', { name: 'Next' }).click(); + + await page.getByLabel("Full period of the contract").fill("10") + await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled"); + + await page.getByLabel("Full period of the contract").fill("1") + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); + + await page.getByLabel("Full period of the contract").fill("0") + await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled"); + + const value = (Math.random() * 7); + await page.getByLabel("Full period of the contract").fill(value.toFixed(1)) + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); + + await page.getByRole('button', { name: 'Next' }).click(); + await expect(page.getByText('Your request is being processed.')).toBeVisible(); + await page.getByRole('button', { name: 'Finish' }).click(); + await expect(page.getByText('No data.')).not.toBeVisible(); + await expect(page.getByText(value.toFixed(1) + " days").first()).toBeVisible(); +}) + +// test('create a storage request by using months', async ({ page }) => { +// await page.goto('/dashboard'); +// await page.locator('a').filter({ hasText: 'Purchases' }).click(); +// await page.getByRole('button', { name: 'Storage Request' }).click(); + +// await page.locator('div').getByTestId("upload").setInputFiles([ +// path.join(__dirname, "assets", 'chatgpt.jpg'), +// ]); +// await expect(page.locator('#cid')).not.toBeEmpty() +// await expect(page.getByText('Success, the CID has been')).toBeVisible(); +// await page.getByRole('button', { name: 'Next' }).click(); + +// await page.getByLabel("Full period of the contract").fill("3") +// await page.getByRole('combobox').selectOption('months'); +// await expect(page.getByLabel("Full period of the contract")).toHaveValue("3") + +// await page.getByRole('button', { name: 'Next' }).click(); +// await expect(page.getByText('Your request is being processed.')).toBeVisible(); +// await page.getByRole('button', { name: 'Finish' }).click(); +// await expect(page.getByText('No data.')).not.toBeVisible(); +// await expect(page.getByText("3 months").first()).toBeVisible(); +// }) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e96f80e..13046f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.13", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.50", + "@codex-storage/marketplace-ui-components": "^0.0.51", "@codex-storage/sdk-js": "^0.0.16", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -395,9 +395,9 @@ } }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.50", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.50.tgz", - "integrity": "sha512-pijYw8Wd/ns64Q7iSLQ444U5Q7Ql0U88OuxBycuxIOVp3obITJk6SBgdZpxHIj5kWShsUvXU/nJl0V0oKt9ieQ==", + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.51.tgz", + "integrity": "sha512-KPPFlcpx3a83WCBSLbRONrF/yr4J/ctyTfFPxMaRSMTRD1LtfIE0uPy3QxtHs6tigOts2h4DEz6Kn2ynHdfKPg==", "engines": { "node": ">=18" }, diff --git a/package.json b/package.json index 4415bb3..4da9373 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.50", + "@codex-storage/marketplace-ui-components": "^0.0.51", "@codex-storage/sdk-js": "^0.0.16", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", diff --git a/public/icons/aud-flag.svg b/public/icons/aud-flag.svg new file mode 100644 index 0000000..e7917dc --- /dev/null +++ b/public/icons/aud-flag.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/icons/btc-flag.svg b/public/icons/btc-flag.svg new file mode 100644 index 0000000..30f600e --- /dev/null +++ b/public/icons/btc-flag.svg @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/public/icons/cad-flag.svg b/public/icons/cad-flag.svg new file mode 100644 index 0000000..6b1ea17 --- /dev/null +++ b/public/icons/cad-flag.svg @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/public/icons/cny-flag.svg b/public/icons/cny-flag.svg new file mode 100644 index 0000000..053f46c --- /dev/null +++ b/public/icons/cny-flag.svg @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/public/icons/eth-flag.svg b/public/icons/eth-flag.svg new file mode 100644 index 0000000..3ad1c3e --- /dev/null +++ b/public/icons/eth-flag.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/src/assets/icons/dots.svg b/src/assets/icons/dots.svg new file mode 100644 index 0000000..d04ff91 --- /dev/null +++ b/src/assets/icons/dots.svg @@ -0,0 +1,14 @@ + + + + + diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 9030404..6d60f0a 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -28,7 +28,7 @@ type Props = { }; const icons: Record = { - dashboard: , + dashboard: , peers: , settings: , files: , @@ -74,16 +74,16 @@ export function AppBar({ onIconClick, onExpanded }: Props) { const title = location.pathname.split("/")[2] || location.pathname.split("/")[1]; - const networkIconColor = online ? "#3EE089" : "var(-codex-color-error)"; + const networkIconColor = online ? "#3EE089" : "var(--codex-color-error)"; const nodesIconColor = codex.enabled === false - ? "var(-codex-color-error)" + ? "var(--codex-color-error)" : persistence.enabled ? "#3EE089" : "var(--codex-input-color-warning)"; const icon = isMobile ? ( - onExpanded(true)}> + onExpanded(true)} width={30}> ) : ( icons[title] ); diff --git a/src/components/AppBar/appBar.css b/src/components/AppBar/appBar.css index b7201bd..6e8aa59 100644 --- a/src/components/AppBar/appBar.css +++ b/src/components/AppBar/appBar.css @@ -45,6 +45,12 @@ sans-serif; letter-spacing: -0.006em; color: rgba(150, 150, 150, 0.8); + + @media (max-width: 800px) { + & { + display: none; + } + } } > div { @@ -88,7 +94,7 @@ } @media (max-width: 800px) { - aside { + .wallet-login { display: none; } } diff --git a/src/components/Availability/AvailabilitiesTable.tsx b/src/components/Availability/AvailabilitiesTable.tsx index 776f57a..531e050 100644 --- a/src/components/Availability/AvailabilitiesTable.tsx +++ b/src/components/Availability/AvailabilitiesTable.tsx @@ -4,7 +4,7 @@ import { Table, TabSortState, } from "@codex-storage/marketplace-ui-components"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import { AvailabilityActionsCell } from "./AvailabilityActionsCell"; import { CodexAvailability, CodexNodeSpace } from "@codex-storage/sdk-js/async"; import { Times } from "../../utils/times"; @@ -86,7 +86,7 @@ export function AvailabilitiesTable({ availabilities, space }: Props) { )} , , - {PrettyBytes(a.totalSize)}, + {Bytes.pretty(a.totalSize)}, {Times.pretty(a.duration)}, {a.minPrice.toString()}, {a.maxCollateral.toString()}, diff --git a/src/components/Availability/AvailabilityDiskRow.tsx b/src/components/Availability/AvailabilityDiskRow.tsx index 9ee444d..e5f4aca 100644 --- a/src/components/Availability/AvailabilityDiskRow.tsx +++ b/src/components/Availability/AvailabilityDiskRow.tsx @@ -1,5 +1,5 @@ import { Cell, Row } from "@codex-storage/marketplace-ui-components"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import { classnames } from "../../utils/classnames"; import HardriveIcon from "../../assets/icons/hardrive.svg?react"; @@ -19,7 +19,7 @@ export function AvailabilityDiskRow({ bytes }: Props) {
Node - {PrettyBytes(bytes)} allocated for the node + {Bytes.pretty(bytes)} allocated for the node
, diff --git a/src/components/Availability/AvailabilityEdit.tsx b/src/components/Availability/AvailabilityEdit.tsx index 1389663..46acddd 100644 --- a/src/components/Availability/AvailabilityEdit.tsx +++ b/src/components/Availability/AvailabilityEdit.tsx @@ -10,7 +10,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { AvailabilityConfirm } from "./AvailabilityConfirmation"; import { WebStorage } from "../../utils/web-storage"; import { AvailabilityState } from "./types"; -import { GB, STEPPER_DURATION } from "../../utils/constants"; +import { STEPPER_DURATION } from "../../utils/constants"; import { useAvailabilityMutation } from "./useAvailabilityMutation"; import { AvailabilitySuccess } from "./AvailabilitySuccess"; import { AvailabilityError } from "./AvailabilityError"; @@ -28,8 +28,8 @@ type Props = { const CONFIRM_STATE = 2; const defaultAvailabilityData: AvailabilityState = { - totalSize: 0.5 * GB, - duration: Times.unitValue("days"), + totalSize: 0.5, + duration: 1, minPrice: 0, maxCollateral: 0, totalSizeUnit: "gb", @@ -52,7 +52,7 @@ export function AvailabilityEdit({ useEffect(() => { Promise.all([ WebStorage.get("availability-step"), - WebStorage.get("availability"), + WebStorage.get("availability-1"), ]).then(([s, a]) => { if (s) { dispatch({ @@ -62,31 +62,11 @@ export function AvailabilityEdit({ } if (a) { - setAvailability(a); + setAvailability({ ...defaultAvailabilityData, ...a }); } }); }, [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); - // }; - - // document.addEventListener( - // "codexavailabilityid", - // onAvailabilityIdChange, - // false - // ); - - // return () => - // document.removeEventListener( - // "codexavailabilityid", - // onAvailabilityIdChange - // ); - // }, []); - const components = [ AvailabilityForm, AvailabilityConfirm, @@ -157,7 +137,7 @@ export function AvailabilityEdit({ editAvailabilityValue.current = a.totalSize; WebStorage.set("availability-step", 0); - WebStorage.set("availability", a); + WebStorage.set("availability-1", a); const unit = Times.unit(a.duration); diff --git a/src/components/Availability/AvailabilityForm.tsx b/src/components/Availability/AvailabilityForm.tsx index 315dcf1..fcf42a3 100644 --- a/src/components/Availability/AvailabilityForm.tsx +++ b/src/components/Availability/AvailabilityForm.tsx @@ -11,7 +11,6 @@ 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, @@ -39,27 +38,24 @@ export function AvailabilityForm({ const element = e.currentTarget; onAvailabilityChange({ - totalSize: 0, totalSizeUnit: element.value as "tb" | "gb", }); }; const onDurationChange = async (e: ChangeEvent) => { const element = e.currentTarget; - const unitValue = Times.unitValue(availability.durationUnit); onAvailabilityChange({ - duration: parseInt(element.value) * unitValue, + duration: parseInt(element.value), }); }; const onDurationUnitChange = async (e: ChangeEvent) => { const element = e.currentTarget; const unit = element.value as "hours" | "days" | "months"; - const unitValue = Times.unitValue(unit); onAvailabilityChange({ - duration: unitValue, + duration: availability.duration, durationUnit: unit, }); }; @@ -67,10 +63,9 @@ export function AvailabilityForm({ const onAvailablityChange = async (e: ChangeEvent) => { const element = e.currentTarget; const v = element.value; - const unit = AvailabilityUtils.unitValue(availability.totalSizeUnit); onAvailabilityChange({ - totalSize: parseFloat(v) * unit, + totalSize: parseFloat(v), }); }; @@ -87,7 +82,12 @@ export function AvailabilityForm({ const available = AvailabilityUtils.maxValue(space); onAvailabilityChange({ - totalSize: available, + totalSize: + Math.floor( + ((available - 1) / + AvailabilityUtils.unitValue(availability.totalSizeUnit)) * + 10 + ) / 10, }); }; @@ -96,20 +96,17 @@ export function AvailabilityForm({ available += editAvailabilityValue; } - const isValid = - availability.totalSize > 0 && available >= availability.totalSize; + const totalSizeInBytes = + availability.totalSize * + AvailabilityUtils.unitValue(availability.totalSizeUnit); + + const isValid = totalSizeInBytes > 0 && available >= totalSizeInBytes; const helper = isValid ? "Total size of sale's storage in bytes" : "The total size cannot exceed the space available."; - const value = AvailabilityUtils.toUnit( - availability.totalSize, - availability.totalSizeUnit - ).toFixed(2); - - const unitValue = Times.unitValue(availability.durationUnit); - const duration = availability.duration / unitValue; + const duration = availability.duration; return (
@@ -143,13 +140,12 @@ export function AvailabilityForm({ name="totalSize" type="number" label="Total size" - min={0.01} isInvalid={!isValid} max={available.toFixed(2)} onChange={onAvailablityChange} onGroupChange={onTotalSizeUnitChange} - step={"0.01"} - value={value} + value={availability.totalSize.toString()} + min={"0"} group={[ ["gb", "GB"], // ["tb", "TB"], diff --git a/src/components/Availability/AvailabilityIdCell.tsx b/src/components/Availability/AvailabilityIdCell.tsx index aec2721..f2c1478 100644 --- a/src/components/Availability/AvailabilityIdCell.tsx +++ b/src/components/Availability/AvailabilityIdCell.tsx @@ -1,6 +1,6 @@ import { Strings } from "../../utils/strings"; import { Cell } from "@codex-storage/marketplace-ui-components"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import { AvailabilityWithSlots } from "./types"; import AvailbilityIcon from "../../assets/icons/availability.svg?react"; @@ -18,7 +18,7 @@ export function AvailabilityIdCell({ value }: Props) { {value.name || Strings.shortId(value.id)}
- {PrettyBytes(value.totalSize)} allocated for the availability + {Bytes.pretty(value.totalSize)} allocated for the availability
diff --git a/src/components/Availability/SlotRow.tsx b/src/components/Availability/SlotRow.tsx index 7cc2793..c78b450 100644 --- a/src/components/Availability/SlotRow.tsx +++ b/src/components/Availability/SlotRow.tsx @@ -1,5 +1,5 @@ import { Cell, Row } from "@codex-storage/marketplace-ui-components"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import "./SlotRow.css"; import { classnames } from "../../utils/classnames"; import { attributes } from "../../utils/attributes"; @@ -47,7 +47,7 @@ export function SlotRow({ bytes, active, id }: Props) {
Slot {id} - {PrettyBytes(bytes)} allocated for the slot + {Bytes.pretty(bytes)} allocated for the slot
, diff --git a/src/components/Availability/Sunburst.tsx b/src/components/Availability/Sunburst.tsx index c0950c5..847162c 100644 --- a/src/components/Availability/Sunburst.tsx +++ b/src/components/Availability/Sunburst.tsx @@ -1,7 +1,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { Times } from "../../utils/times"; import { Strings } from "../../utils/strings"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import { useEffect, useRef, useState } from "react"; import { CallbackDataParams, ECBasicOption } from "echarts/types/dist/shared"; import * as echarts from "echarts/core"; @@ -36,6 +36,16 @@ export function Sunburst({ availabilities, space }: Props) { } }, [chart, div]); + useEffect(() => { + const refresh = () => chart.current?.resize(); + + window.addEventListener("resize", refresh); + + return () => { + window.removeEventListener("resize", refresh); + }; + }, []); + const data = availabilities.map((a, index) => { return { name: Strings.shortId(a.id), @@ -65,7 +75,7 @@ export function Sunburst({ availabilities, space }: Props) { a.minPrice + "
" + "Size " + - PrettyBytes(a.totalSize) + Bytes.pretty(a.totalSize) ); }, }, @@ -87,7 +97,7 @@ export function Sunburst({ availabilities, space }: Props) { params.marker + "Slot " + slot.id + - PrettyBytes(parseFloat(slot.size)) + Bytes.pretty(parseFloat(slot.size)) ); }, }, @@ -121,7 +131,7 @@ export function Sunburst({ availabilities, space }: Props) { return ( params.marker + " Space remaining " + - PrettyBytes( + Bytes.pretty( space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes @@ -170,6 +180,8 @@ export function Sunburst({ availabilities, space }: Props) { }; chart.current.setOption(option); + chart.current?.resize(); + // chart.current.off("click"); // chart.current.on("click", function (params) { // // console.info(params.componentIndex); @@ -196,7 +208,6 @@ export function Sunburst({ availabilities, space }: Props) { ref={div} className="sunburst" style={{ - width: size, height: size, }}> ); diff --git a/src/components/Availability/availability.utils.test.ts b/src/components/Availability/availability.utils.test.ts index 645ed75..2e654ff 100644 --- a/src/components/Availability/availability.utils.test.ts +++ b/src/components/Availability/availability.utils.test.ts @@ -173,16 +173,18 @@ describe("files", () => { quotaUsedBytes: GB, totalBlocks: 0 } - assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB); + assert.deepEqual(AvailabilityUtils.maxValue(space), 5 * GB - 1); }) it("checks the availability max value", async () => { const availability = { - totalSize: GB + totalSizeUnit: "gb", + totalSize: 1 } as AvailabilityState + assert.deepEqual(AvailabilityUtils.isValid(availability, GB * 2), true); assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: -1 }, GB), false); - assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: 2 * GB }, GB), false); + assert.deepEqual(AvailabilityUtils.isValid({ ...availability, totalSize: GB }, 2 * GB), false); }) it("toggles item in array", async () => { diff --git a/src/components/Availability/availability.utils.ts b/src/components/Availability/availability.utils.ts index 464c600..50bf72f 100644 --- a/src/components/Availability/availability.utils.ts +++ b/src/components/Availability/availability.utils.ts @@ -39,7 +39,8 @@ export const AvailabilityUtils = { return bytes / this.unitValue(unit || "gb") }, maxValue(space: CodexNodeSpace) { - return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes + // Remove 1 byte to allow to create an availability with the max space possible + return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes - 1 }, unitValue(unit: "gb" | "tb") { return unit === "tb" ? TB : GB @@ -47,7 +48,7 @@ export const AvailabilityUtils = { isValid: ( availability: AvailabilityState, max: number - ) => availability.totalSize > 0 && availability.totalSize <= max + ) => availability.totalSize > 0 && availability.totalSize * AvailabilityUtils.unitValue(availability.totalSizeUnit) <= max , toggle: (arr: Array, value: T) => arr.includes(value) ? arr.filter(i => i !== value) : [...arr, value], @@ -88,5 +89,5 @@ export const AvailabilityUtils = { "#D2493C22", "#D2493C11", "#D2493C00", - ] + ], } \ No newline at end of file diff --git a/src/components/Availability/useAvailabilityMutation.ts b/src/components/Availability/useAvailabilityMutation.ts index 64a8838..b79b2aa 100644 --- a/src/components/Availability/useAvailabilityMutation.ts +++ b/src/components/Availability/useAvailabilityMutation.ts @@ -9,6 +9,8 @@ import { } from "@codex-storage/marketplace-ui-components"; import { CodexSdk } from "../../sdk/codex"; import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js"; +import { Times } from "../../utils/times"; +import { AvailabilityUtils } from "./availability.utils"; export function useAvailabilityMutation( @@ -42,15 +44,15 @@ export function useAvailabilityMutation( return fn({ ...input, - duration, - totalSize: Math.trunc(totalSize), + duration: Times.value(durationUnit) * duration, + totalSize: Math.trunc(totalSize * AvailabilityUtils.unitValue(totalSizeUnit)), }); }, onSuccess: (res, body) => { queryClient.invalidateQueries({ queryKey: ["availabilities"] }); queryClient.invalidateQueries({ queryKey: ["space"] }); - WebStorage.delete("availability"); + WebStorage.delete("availability-1"); WebStorage.delete("availability-step"); if (typeof res === "object" && body.name) { diff --git a/src/components/Card/Card.css b/src/components/Card/Card.css index 4584204..e3e4201 100644 --- a/src/components/Card/Card.css +++ b/src/components/Card/Card.css @@ -4,6 +4,13 @@ padding: 16px; background-color: rgb(35, 35, 35); + @media (max-width: 800px) { + &, + td:nth-child(2) { + padding: 8px; + } + } + > header { display: flex; align-items: center; diff --git a/src/components/ConnectedAccount/WalletCard.css b/src/components/ConnectedAccount/WalletCard.css index b4b6b28..f8f4b57 100644 --- a/src/components/ConnectedAccount/WalletCard.css +++ b/src/components/ConnectedAccount/WalletCard.css @@ -147,18 +147,38 @@ box-shadow: 0 0 0 3px rgba(150, 150, 150, 0.2); } - &:has(option[value="USD"]:checked) { - background-image: url(/icons/us-flag.svg); + &:has(option:checked) { background-position: 10px; background-repeat: no-repeat; background-size: 16px; } + &:has(option[value="USD"]:checked) { + background-image: url(/icons/us-flag.svg); + } + &:has(option[value="EUR"]:checked) { background-image: url(/icons/euro-flag.svg); - background-position: 10px; - background-repeat: no-repeat; - background-size: 16px; + } + + &:has(option[value="AUD"]:checked) { + background-image: url(/icons/aud-flag.svg); + } + + &:has(option[value="CAD"]:checked) { + background-image: url(/icons/cad-flag.svg); + } + + &:has(option[value="CNY"]:checked) { + background-image: url(/icons/cny-flag.svg); + } + + &:has(option[value="BTC"]:checked) { + background-image: url(/icons/btc-flag.svg); + } + + &:has(option[value="ETH"]:checked) { + background-image: url(/icons/eth-flag.svg); } option { @@ -205,6 +225,5 @@ .lines { height: 200; - transform: scale(1.25); } } diff --git a/src/components/ConnectedAccount/WalletCard.tsx b/src/components/ConnectedAccount/WalletCard.tsx index e88983e..0ffdf59 100644 --- a/src/components/ConnectedAccount/WalletCard.tsx +++ b/src/components/ConnectedAccount/WalletCard.tsx @@ -33,24 +33,52 @@ export function WalletCard({ tab }: Props) { } }, [chart, div]); + useEffect(() => { + const refresh = () => chart.current?.resize(); + + window.addEventListener("resize", refresh); + + return () => { + window.removeEventListener("resize", refresh); + }; + }, []); + const onCurrencyChange = async (e: ChangeEvent) => { const value = e.currentTarget.value; setCurrency(value); if (value === "USD") { setTokenValue(1540); - } else { + } else if (["BTC", "ETH"].includes(value) === false) { const json = await fetch( "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.json" ).then((res) => res.json()); - setTokenValue(defaultTokenValue * json.usd.eur); + setTokenValue(defaultTokenValue * json.usd[value.toLocaleLowerCase()]); + } else { + const json = await fetch( + "https://api.coinbase.com/v2/prices/" + + value.toLocaleLowerCase() + + "-USD/spot.json" + ).then((res) => res.json()); + setTokenValue(defaultTokenValue / json.data.amount); } }; if (chart.current) { + const today = new Date(); + const startOfWeek = today.getDate() - today.getDay() + 1; + const startDates = []; + + today.setDate(startOfWeek); + + for (let i = 0; i < 5; i++) { + startDates.push(today.toISOString().split("T")[0]); + today.setDate(today.getDate() + 7); + } + const data = { daily: ["MON", "TUE", "WED", "THU", "WED", "SAT", "SUN"], - weekly: ["1", "2", "3", "4", "5", "6"], + weekly: startDates, monthly: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN"], }[tab]; @@ -72,6 +100,14 @@ export function WalletCard({ tab }: Props) { type: "value", show: false, }, + grid: [ + { + left: 0, + right: 0, + top: 50, + bottom: 50, + }, + ], series: [ { data: [220, 932, 401, 934, 1290, 1330, 1450].slice(0, data.length), @@ -89,7 +125,7 @@ export function WalletCard({ tab }: Props) { }, }; - chart.current.setOption(option); + chart.current.setOption(option, true); } return ( @@ -115,7 +151,10 @@ export function WalletCard({ tab }: Props) {
{/* */} {/* */} -
+
@@ -124,14 +163,20 @@ export function WalletCard({ tab }: Props) { TOKEN
- {tokenValue.toFixed(2)} {currency} + {tokenValue.toFixed(["BTC", "ETH"].includes(currency) ? 8 : 2)}{" "} + {currency} + 5%
diff --git a/src/components/Files/FileActions.css b/src/components/Files/FileActions.css index 4afa681..6ec1267 100644 --- a/src/components/Files/FileActions.css +++ b/src/components/Files/FileActions.css @@ -10,6 +10,37 @@ padding: 10px; } + ul { + transition: bottom 0.35s; + position: fixed; + bottom: -500px; + left: 0; + right: 0; + background-color: rgba(47, 47, 47, 1); + border-top: 1px solid rgba(150, 150, 150, 0.2); + border-top-left-radius: 12px; + border-top-right-radius: 12px; + + padding: 16px; + + &[aria-expanded] { + bottom: 0; + z-index: 11; + } + + li { + display: flex; + align-items: center; + gap: 16px; + } + + li + li { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(150, 150, 150, 0.2); + } + } + @media (max-width: 800px) { .folder-button { display: none; diff --git a/src/components/Files/FileActions.tsx b/src/components/Files/FileActions.tsx index bfb3f10..1e6eae0 100644 --- a/src/components/Files/FileActions.tsx +++ b/src/components/Files/FileActions.tsx @@ -1,10 +1,19 @@ -import { ButtonIcon, Cell } from "@codex-storage/marketplace-ui-components"; +import { + Backdrop, + ButtonIcon, + Cell, +} from "@codex-storage/marketplace-ui-components"; import { FolderButton } from "./FolderButton"; import { CodexDataContent } from "@codex-storage/sdk-js"; import { CodexSdk } from "../../sdk/codex"; import "./FileActions.css"; import DownloadIcon from "../../assets/icons/download-file.svg?react"; import InfoFileIcon from "../../assets/icons/info-file.svg?react"; +import DotsIcon from "../../assets/icons/dots.svg?react"; +import { useIsMobile } from "../../hooks/useMobile"; +import { useState } from "react"; +import { attributes } from "../../utils/attributes"; +import CopyIcon from "../../assets/icons/copy.svg?react"; type Props = { content: CodexDataContent; @@ -19,7 +28,53 @@ export function FileActions({ onFolderToggle, onDetails, }: Props) { + const isMobile = useIsMobile(); const url = CodexSdk.url() + "/api/codex/v1/data/"; + const [isExpanded, setIsExpanded] = useState(false); + + const onClose = () => setIsExpanded(false); + + const onOpen = () => setIsExpanded(true); + + const onCopy = (cid: string) => { + setIsExpanded(false); + navigator.clipboard.writeText(cid); + }; + + if (isMobile) { + return ( + + <> + ( + + )}> +
    +
  • { + window.open(url + content.cid, "_blank"); + setIsExpanded(false); + }}> + Download +
  • +
  • { + onDetails(content.cid); + setIsExpanded(false); + }}> + Details +
  • +
  • onCopy(content.cid)}> + Copy +
  • +
+ + +
+ ); + } return ( diff --git a/src/components/Files/FileCell.css b/src/components/Files/FileCell.css index c2c2d35..6fbb294 100644 --- a/src/components/Files/FileCell.css +++ b/src/components/Files/FileCell.css @@ -17,6 +17,12 @@ height: 40px; background-color: rgba(20, 20, 20, 0.6); border: 1px solid rgba(150, 150, 150, 0.2); + + @media (max-width: 800px) { + & { + display: none; + } + } } } } diff --git a/src/components/Files/FileCell.tsx b/src/components/Files/FileCell.tsx index 987ec94..b29d3a4 100644 --- a/src/components/Files/FileCell.tsx +++ b/src/components/Files/FileCell.tsx @@ -14,11 +14,27 @@ type Props = { }; export function FileCell({ content }: Props) { - const [toast, setToast] = useState({ time: 0, message: "" }); + const [toast, setToast] = useState({ + time: 0, + message: "", + variant: "success" as "success" | "error", + }); const onCopy = (cid: string) => { - navigator.clipboard.writeText(cid); - setToast({ message: "CID copied to the clipboard.", time: Date.now() }); + if (navigator.clipboard) { + navigator.clipboard.writeText(cid); + setToast({ + message: "CID copied to the clipboard.", + time: Date.now(), + variant: "success" as "success", + }); + } else { + setToast({ + message: "Sorry the CID cannot be copied to the clipboard.", + time: Date.now(), + variant: "error" as "error", + }); + } }; return ( @@ -44,7 +60,11 @@ export function FileCell({ content }: Props) { )}> - + ); diff --git a/src/components/Files/FileDetails.css b/src/components/Files/FileDetails.css index 4a8bae5..6db82d7 100644 --- a/src/components/Files/FileDetails.css +++ b/src/components/Files/FileDetails.css @@ -21,17 +21,6 @@ span { flex-grow: 1; } - - .button-icon { - background-color: rgb(47, 47, 47); - border: 1px solid rgba(150, 150, 150, 0.2); - - svg { - position: relative; - left: -3px; - top: -1px; - } - } } .preview { diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx index 04cea98..ef20734 100644 --- a/src/components/Files/FileDetails.tsx +++ b/src/components/Files/FileDetails.tsx @@ -5,7 +5,7 @@ import { WebFileIcon, } from "@codex-storage/marketplace-ui-components"; import { CodexDataContent, CodexPurchase } from "@codex-storage/sdk-js"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import { CidCopyButton } from "./CidCopyButton"; import "./FileDetails.css"; import { CodexSdk } from "../../sdk/codex"; @@ -136,7 +136,7 @@ export function FileDetails({ onClose, details }: Props) {
  • Size:

    -

    {PrettyBytes(details.manifest.datasetSize)}

    +

    {Bytes.pretty(details.manifest.datasetSize)}

  • diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 83d0e85..172e727 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -1,5 +1,5 @@ import { ChangeEvent, useEffect, useState } from "react"; -import { PrettyBytes } from "../../utils/bytes"; +import { Bytes } from "../../utils/bytes"; import "./Files.css"; import { Tabs, @@ -164,7 +164,7 @@ export function Files({ limit }: Props) { , - {PrettyBytes(c.manifest.datasetSize)}, + {Bytes.pretty(c.manifest.datasetSize)}, {FilesUtils.formatDate(c.manifest.uploadedAt).toString()} , diff --git a/src/components/Files/files.utils.test.ts b/src/components/Files/files.utils.test.ts index 4b8fd9d..7e43cc6 100644 --- a/src/components/Files/files.utils.test.ts +++ b/src/components/Files/files.utils.test.ts @@ -294,8 +294,8 @@ describe("files", () => { }); it("formats date", async () => { + const utcDate = new Date(Date.UTC(2024, 10, 20, 11, 36)); - assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, 11:36"); - + assert.equal(FilesUtils.formatDate(1732102577), "20 Nov 2024, " + utcDate.getHours() + ":" + utcDate.getMinutes()); }) }) \ No newline at end of file diff --git a/src/components/LogLevel/LogLevel.css b/src/components/LogLevel/LogLevel.css index b225a22..42bca48 100644 --- a/src/components/LogLevel/LogLevel.css +++ b/src/components/LogLevel/LogLevel.css @@ -7,6 +7,12 @@ > div:first-child { position: relative; + @media (max-width: 800px) { + & { + flex: 1; + } + } + svg { position: absolute; top: 11px; @@ -22,5 +28,11 @@ select { border-color: rgba(150, 150, 150, 1); padding-left: 40px; + + @media (max-width: 800px) { + & { + width: 100%; + } + } } } diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 0f5f423..e4abd90 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -16,6 +16,7 @@ import PurchaseIcon from "../../assets/icons/purchase.svg?react"; import HostIcon from "../../assets/icons/host.svg?react"; import LogsIcon from "../../assets/icons/logs.svg?react"; import SettingsIcon from "../../assets/icons/settings.svg?react"; +import CloseIcon from "../../assets/icons/close.svg?react"; import HelpIcon from "../../assets/icons/help.svg?react"; import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react"; import { NavLink } from "react-router-dom"; @@ -65,6 +66,8 @@ export function Menu({ isExpanded, onExpanded }: Props) { } }; + const Icon = isMobile ? CloseIcon : ExpandIcon; + return ( <>