diff --git a/e2e/storage-requests.spec.ts b/e2e/storage-requests.spec.ts index 23be51f..55300e8 100644 --- a/e2e/storage-requests.spec.ts +++ b/e2e/storage-requests.spec.ts @@ -76,3 +76,49 @@ 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(); + + const value = (Math.random() * 10); + await page.getByLabel("Full period of the contract").fill(value.toFixed(1)) + + 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("1") + await page.getByLabel("Full period of the contract").fill("2") + + 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("2 months").first()).toBeVisible(); +}) \ No newline at end of file diff --git a/src/components/Availability/AvailabilityEdit.tsx b/src/components/Availability/AvailabilityEdit.tsx index 1389663..c6780b1 100644 --- a/src/components/Availability/AvailabilityEdit.tsx +++ b/src/components/Availability/AvailabilityEdit.tsx @@ -29,7 +29,7 @@ const CONFIRM_STATE = 2; const defaultAvailabilityData: AvailabilityState = { totalSize: 0.5 * GB, - duration: Times.unitValue("days"), + duration: Times.value("days"), minPrice: 0, maxCollateral: 0, totalSizeUnit: "gb", @@ -62,7 +62,7 @@ export function AvailabilityEdit({ } if (a) { - setAvailability(a); + setAvailability({ ...defaultAvailabilityData, ...a }); } }); }, [dispatch]); diff --git a/src/components/Availability/AvailabilityForm.tsx b/src/components/Availability/AvailabilityForm.tsx index 315dcf1..f8b1a7b 100644 --- a/src/components/Availability/AvailabilityForm.tsx +++ b/src/components/Availability/AvailabilityForm.tsx @@ -46,7 +46,7 @@ export function AvailabilityForm({ const onDurationChange = async (e: ChangeEvent) => { const element = e.currentTarget; - const unitValue = Times.unitValue(availability.durationUnit); + const unitValue = Times.value(availability.durationUnit); onAvailabilityChange({ duration: parseInt(element.value) * unitValue, @@ -56,7 +56,7 @@ export function AvailabilityForm({ const onDurationUnitChange = async (e: ChangeEvent) => { const element = e.currentTarget; const unit = element.value as "hours" | "days" | "months"; - const unitValue = Times.unitValue(unit); + const unitValue = Times.value(unit); onAvailabilityChange({ duration: unitValue, @@ -108,7 +108,7 @@ export function AvailabilityForm({ availability.totalSizeUnit ).toFixed(2); - const unitValue = Times.unitValue(availability.durationUnit); + const unitValue = Times.value(availability.durationUnit); const duration = availability.duration / unitValue; return ( diff --git a/src/components/Availability/availability.utils.ts b/src/components/Availability/availability.utils.ts index 464c600..e73b1ee 100644 --- a/src/components/Availability/availability.utils.ts +++ b/src/components/Availability/availability.utils.ts @@ -88,5 +88,5 @@ export const AvailabilityUtils = { "#D2493C22", "#D2493C11", "#D2493C00", - ] + ], } \ No newline at end of file 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/StorageRequestSetup/Commitment.css b/src/components/StorageRequestSetup/Commitment.css new file mode 100644 index 0000000..75837d4 --- /dev/null +++ b/src/components/StorageRequestSetup/Commitment.css @@ -0,0 +1,7 @@ +.commitment { + --codex-input-group-background-color: transparent; + + span { + right: 155px; + } +} diff --git a/src/components/StorageRequestSetup/Commitment.tsx b/src/components/StorageRequestSetup/Commitment.tsx new file mode 100644 index 0000000..f6c2f4a --- /dev/null +++ b/src/components/StorageRequestSetup/Commitment.tsx @@ -0,0 +1,76 @@ +import { InputGroup, Tooltip } from "@codex-storage/marketplace-ui-components"; +import "../CardNumbers/CardNumbers.css"; +import "./Commitment.css"; + +import { ChangeEvent, useState } from "react"; +import { classnames } from "../../utils/classnames"; +import InfoIcon from "../../assets/icons/info.svg?react"; +import { attributes } from "../../utils/attributes"; +import { Times } from "../../utils/times"; + +type Props = { + value: string; + onChange: (value: string) => void; + onValidation?: (value: string) => string; +}; + +export function Commitment({ value, onValidation, onChange }: Props) { + const [error, setError] = useState(""); + + const unitValue = Times.unit(parseFloat(value)); + const val = parseFloat(value) / Times.value(unitValue); + + const onValueChange = (e: ChangeEvent) => { + console.info("e.currentTarget.value", e.currentTarget.value); + onValueOrUnitChange( + (parseFloat(e.currentTarget.value) * Times.value(unitValue)).toFixed(1) + ); + }; + const onUnitChange = (e: ChangeEvent) => { + onValueOrUnitChange( + Times.value(e.currentTarget.value as "days" | "months").toFixed(1) + ); + }; + + const onValueOrUnitChange = (val: string) => { + onChange(val); + + const msg = onValidation?.(val); + + if (msg) { + setError(msg); + return; + } + + setError(""); + }; + + console.info(val); + + return ( +
+ + + + + + {"Contract duration"} +
+ ); +} diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.tsx b/src/components/StorageRequestSetup/StorageRequestCreate.tsx index 972f00c..9378f54 100644 --- a/src/components/StorageRequestSetup/StorageRequestCreate.tsx +++ b/src/components/StorageRequestSetup/StorageRequestCreate.tsx @@ -22,8 +22,7 @@ const CONFIRM_STATE = 2; const defaultStorageRequest: StorageRequest = { cid: "", - availabilityUnit: "months", - availability: 1, + availability: Times.value("days"), tolerance: 1, proofProbability: 1, nodes: 3, @@ -43,7 +42,7 @@ export function StorageRequestCreate() { useEffect(() => { Promise.all([ WebStorage.get("storage-request-step"), - WebStorage.get("storage-request"), + WebStorage.get("storage-request-2"), ]).then(([s, data]) => { if (s) { dispatch({ @@ -83,11 +82,10 @@ export function StorageRequestCreate() { WebStorage.set("storage-request-step", step); if (step == CONFIRM_STATE) { - const { availabilityUnit, availability, expiration, ...rest } = - storageRequest; + const { availability, expiration, ...rest } = storageRequest; mutateAsync({ ...rest, - duration: Times.toSeconds(availability, availabilityUnit), + duration: availability, expiry: expiration * 60, }); } else { @@ -101,7 +99,7 @@ export function StorageRequestCreate() { const onStorageRequestChange = (data: Partial) => { const val = { ...storageRequest, ...data }; - WebStorage.set("storage-request", val); + WebStorage.set("storage-request-2", val); setStorageRequest(val); }; diff --git a/src/components/StorageRequestSetup/StorageRequestReview.tsx b/src/components/StorageRequestSetup/StorageRequestReview.tsx index 639a0f7..90e64be 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.tsx +++ b/src/components/StorageRequestSetup/StorageRequestReview.tsx @@ -14,6 +14,7 @@ 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"; +import { Commitment } from "./Commitment"; type Durability = { nodes: number; @@ -139,14 +140,6 @@ export function StorageRequestReview({ return error; } - // if (!unit.endsWith("s")) { - // unit += "s"; - // } - - // if (!units.includes(unit)) { - // return "Invalid unit must one of: minutes, hours, days, months, years"; - // } - return ""; }; @@ -165,13 +158,8 @@ export function StorageRequestReview({ const onAvailabilityChange = (value: string) => { const [availability] = value.split(" "); - // if (!availabilityUnit.endsWith("s")) { - // availabilityUnit += "s"; - // } - onStorageRequestChange({ availability: Number(availability), - availabilityUnit: "months", }); }; @@ -184,18 +172,6 @@ export function StorageRequestReview({ const onCollateralChange = (value: string) => onStorageRequestChange({ collateral: Number(value) }); - // const pluralizeUnit = () => { - // if (data.availability > 1 && !data.availabilityUnit.endsWith("s")) { - // return data.availability + " " +data.availabilityUnit + "s"; - // } - - // if (data.availability <= 1 && data.availabilityUnit.endsWith("s")) { - // return data.availabilityUnit.slice(0, -1); - // } - - // return data.availabilityUnit; - // }; - const availability = storageRequest.availability; return ( @@ -286,14 +262,10 @@ export function StorageRequestReview({
- + onValidation={isInvalidAvailability}> { + it("display the bytes", async () => { + assert.equal(Bytes.pretty(0), "0 B"); + assert.equal(Bytes.pretty(512), "512.0 B"); + assert.equal(Bytes.pretty(1025), "1.0 KB"); + assert.equal(Bytes.pretty(GB), "1.0 GB"); + }); +}) \ No newline at end of file diff --git a/src/utils/bytes.ts b/src/utils/bytes.ts index 9bc5a22..a3171fe 100644 --- a/src/utils/bytes.ts +++ b/src/utils/bytes.ts @@ -1,14 +1,14 @@ export const Bytes = { pretty(bytes: number) { - const sizes = ["bytes", "KB", "MB", "GB", "TB"]; + const sizes = ["B", "KB", "MB", "GB", "TB"]; if (bytes == 0) { - return "0 b"; + return "0 B"; } const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)).toString()); if (i == 0) { - return bytes + " " + sizes[i]; + return bytes.toFixed(1) + " " + sizes[i]; } return (bytes / Math.pow(1024, i)).toFixed(1) + " " + sizes[i]; diff --git a/src/utils/times.test.ts b/src/utils/times.test.ts new file mode 100644 index 0000000..e2cb3e2 --- /dev/null +++ b/src/utils/times.test.ts @@ -0,0 +1,28 @@ +import { assert, describe, it } from "vitest"; +import { Times } from "./times"; + +describe("times", () => { + it("display the times", async () => { + assert.equal(Times.pretty(0), "0 second"); + assert.equal(Times.pretty(2), "2 seconds"); + assert.equal(Times.pretty(60), "1 minute"); + assert.equal(Times.pretty(90), "1.5 minutes"); + assert.equal(Times.pretty(3600), "1 hour"); + assert.equal(Times.pretty(3600 * 2), "2 hours"); + assert.equal(Times.pretty(3600 * 24), "1 day"); + assert.equal(Times.pretty(3600 * 36), "1.5 days"); + assert.equal(Times.pretty(3600 * 24 * 30), "1 month"); + }); + + it("guess the time unit", async () => { + assert.equal(Times.unit(0), "hours"); + assert.equal(Times.unit(3600 * 24), "days"); + assert.equal(Times.unit(3600 * 24 * 30), "months"); + }) + + it("get the seconds for a time unit given", async () => { + assert.equal(Times.value("hours"), 3600); + assert.equal(Times.value("days"), 3600 * 24); + assert.equal(Times.value("months"), 3600 * 24 * 30); + }) +}) \ No newline at end of file diff --git a/src/utils/times.ts b/src/utils/times.ts index 1ff5c4d..055513b 100644 --- a/src/utils/times.ts +++ b/src/utils/times.ts @@ -6,31 +6,33 @@ export type TimesUnit = | "hours" | "seconds"; -const plural = (value: number, unit: TimesUnit) => - value > 1 ? value + ` ${unit}` : value + ` ${unit.slice(0, -1)}`; +const plural = (value: number, unit: TimesUnit) => { + const val = Number.isInteger(value) ? value : value.toFixed(1) + return value > 1 ? val + ` ${unit}` : val + ` ${unit.slice(0, -1)}`; +} export const Times = { toSeconds(value: number, unit: TimesUnit) { - let seconds = value; + let val = value; /* eslint-disable no-fallthrough */ switch (unit) { // @ts-expect-error - We don't want to break case "years": - seconds *= 365; + val *= 365; // @ts-expect-error - We don't want to break case "months": - seconds *= 30; + val *= 30; // @ts-expect-error - We don't want to break case "days": - seconds *= 24; + val *= 24; // @ts-expect-error - We don't want to break case "hours": - seconds *= 60; + val *= 60; case "minutes": - seconds *= 60; + val *= 60; } - return seconds; + return val; }, pretty(value: number) { @@ -62,7 +64,7 @@ export const Times = { return plural(value, "seconds"); }, - unit(value: number) { + unit(value: number): "months" | "days" | "hours" { let seconds = 30 * 24 * 60 * 60; if (value >= seconds) { @@ -77,7 +79,7 @@ export const Times = { return "hours" }, - unitValue(unit: "hours" | "days" | "months") { + value(unit: "hours" | "days" | "months") { switch (unit) { case "months": { return 30 * 24 * 60 * 60 @@ -90,4 +92,5 @@ export const Times = { } } } + };