diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index bbde8c4..a6e9332 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -112,16 +112,6 @@ jobs: - name: Build run: npm run build - # - uses: actions/cache@v4 - # id: playwright-browsers-cache - # with: - # path: ~/.cache/ms-playwright - # key: ${{ runner.os }}-node-${{ hashFiles('**/playwright.config.ts') }} - - # - if: steps.playwright-browsers-cache.outputs.cache-hit != 'true' - # name: Install Playwright Browsers - # run: npx playwright install --with-deps - - name: Install Playwright Browsers run: npx playwright install --with-deps diff --git a/.github/workflows/unit-testing.yml b/.github/workflows/unit-testing.yml new file mode 100644 index 0000000..2ff3a3a --- /dev/null +++ b/.github/workflows/unit-testing.yml @@ -0,0 +1,39 @@ +name: Unit Tests + +on: + push: + branches: + - "**" + pull_request: + branches: + - "**" + workflow_dispatch: + +env: + VITE_CODEX_API_URL: ${{ secrets.VITE_CODEX_API_URL }} + VITE_GEO_IP_URL: ${{ secrets.VITE_GEO_IP_URL }} +jobs: + tests: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: sudo apt update + + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + + - uses: actions/cache@v4 + id: npm-cache + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm run test:unit diff --git a/e2e/availabilities.spec.ts b/e2e/availabilities.spec.ts index ce823d2..6306b8a 100644 --- a/e2e/availabilities.spec.ts +++ b/e2e/availabilities.spec.ts @@ -1,9 +1,9 @@ import test, { expect } from "@playwright/test"; -test('creates an availability', async ({ page }) => { - await page.goto('/dashboard'); - await page.getByRole('link', { name: 'Sales' }).click(); - await page.getByRole('button').first().click(); +test('create an availability', async ({ page }) => { + await page.goto('/dashboard/availabilities'); + await page.waitForTimeout(500); + await page.locator('.availabilities-create').first().click(); await page.getByLabel('Total size').click(); await page.getByLabel('Total size').fill('0.50'); await page.getByLabel('Duration').click(); @@ -17,7 +17,7 @@ test('creates an availability', async ({ page }) => { await page.getByLabel('Nickname').fill('test'); await page.getByRole('button', { name: 'Next' }).click(); await expect(page.getByText('Confirm your new sale')).toBeVisible(); - await page.locator('small').filter({ hasText: /^512\.0 MB$/ }).click(); + await expect(page.getByText('512.0 MB').first()).toBeVisible(); await page.getByRole('button', { name: 'Next' }).click(); await expect(page.getByText('Success', { exact: true })).toBeVisible(); await page.getByRole('button', { name: 'Finish' }).click(); @@ -26,31 +26,32 @@ test('creates an availability', async ({ page }) => { test('availability navigation buttons', async ({ page }) => { await page.goto('/dashboard/availabilities'); - await page.getByRole('button').first().click(); + await page.waitForTimeout(500); + await page.locator('.availabilities-create').first().click(); await expect(page.locator('.stepper-number-done')).not.toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--active')).toBeVisible() + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); await page.getByLabel('Total size').click(); await page.getByLabel('Total size').fill('19'); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled"); + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled"); await page.getByLabel('Total size').click(); await page.getByLabel('Total size').fill('0.5'); await page.getByRole('button', { name: 'Next' }).click(); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-number-done')).toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--done')).toBeVisible() + await expect(page.locator('.step--active')).toBeVisible() await page.getByRole('button', { name: 'Back' }).click(); - await expect(page.locator('.stepper-number-done')).not.toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.step--done')).not.toBeVisible() + await expect(page.locator('.step--active')).toBeVisible() await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole('button', { name: 'Next' }).click(); - await expect(page.locator('.stepper-number-done')).toHaveCount(2) - await expect(page.locator('.stepper-number-active')).toBeVisible() - await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--done')).toHaveCount(2) + await expect(page.locator('.step--active')).toBeVisible() + await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); await page.getByRole('button', { name: 'Finish' }).click(); await expect(page.locator('.modal--open')).not.toBeVisible(); }) diff --git a/e2e/download.spec.ts b/e2e/download.spec.ts index ae7899e..4a625b0 100644 --- a/e2e/download.spec.ts +++ b/e2e/download.spec.ts @@ -1,32 +1,31 @@ -import { test, expect } from '@playwright/test'; -import { readFileSync } from 'fs'; -import path, { dirname } from 'path'; -import { fileURLToPath } from 'url'; +// import { test, expect } from '@playwright/test'; +// import path, { dirname } from 'path'; +// import { fileURLToPath } from 'url'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +// const __filename = fileURLToPath(import.meta.url); +// const __dirname = dirname(__filename); -test('download a file', async ({ page, browserName }) => { - // https://github.com/microsoft/playwright/issues/13037 - test.skip(browserName.toLowerCase() !== 'chromium', - `Test only for chromium!`); +// test('download a file', async ({ page, browserName }) => { +// // https://github.com/microsoft/playwright/issues/13037 +// test.skip(browserName.toLowerCase() !== 'chromium', +// `Test only for chromium!`); - await page.goto('/dashboard'); - await page.locator('div').getByTestId("upload").setInputFiles([ - path.join(__dirname, "assets", 'chatgpt.jpg'), - ]); - await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); - await page.locator('.files-fileActions > button:nth-child(3)').first().click(); - await page.getByRole('button', { name: 'Copy CID' }).click(); - const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); - const cid = await handle.jsonValue() - await page.locator('.sheets-container > .backdrop').click(); - await page.getByPlaceholder('CID').click(); - await page.getByPlaceholder('CID').fill(cid); - const page1Promise = page.waitForEvent('popup'); - const downloadPromise = page.waitForEvent('download'); - await page.locator('div').filter({ hasText: /^Download a fileDownload$/ }).getByRole('button').click(); - const page1 = await page1Promise; - const download = await downloadPromise; - expect(await download.failure()).toBeNull() -}); \ No newline at end of file +// await page.goto('/dashboard'); +// await page.locator('div').getByTestId("upload").setInputFiles([ +// path.join(__dirname, "assets", 'chatgpt.jpg'), +// ]); +// await page.context().grantPermissions(["clipboard-read", "clipboard-write"]); +// await page.locator('.files-fileActions > button:nth-child(3)').first().click(); +// await page.getByRole('button', { name: 'Copy CID' }).click(); +// const handle = await page.evaluateHandle(() => navigator.clipboard.readText()); +// const cid = await handle.jsonValue() +// await page.locator('.sheets > .backdrop').click(); +// await page.locator('.download-input input').click(); +// await page.locator('.download-input input').fill(cid); +// // const page1Promise = page.waitForEvent('popup'); +// const downloadPromise = page.waitForEvent('download'); +// await page.locator('div').filter({ hasText: /^Download a fileDownload$/ }).getByRole('button').click(); +// // const page1 = await page1Promise; +// const download = await downloadPromise; +// expect(await download.failure()).toBeNull() +// }); \ No newline at end of file diff --git a/e2e/folders.spec.ts b/e2e/folders.spec.ts new file mode 100644 index 0000000..dbb5e73 --- /dev/null +++ b/e2e/folders.spec.ts @@ -0,0 +1,20 @@ +import test, { expect } from "@playwright/test"; + +test('create a folder', async ({ page }) => { + await page.goto('/dashboard'); + await page.locator('#folder').click(); + await page.locator('#folder').fill('abc'); + await expect(page.getByText('Enter the folder name')).toBeVisible(); + await page.locator('#folder').fill('abc '); + await expect(page.getByText('9 alpha characters maximum')).toBeVisible(); + await page.locator('#folder').fill('abc !'); + await expect(page.getByText('9 alpha characters maximum')).toBeVisible(); + await page.locator('#folder').fill('abc )'); + await expect(page.getByText('9 alpha characters maximum')).toBeVisible(); + await page.locator('#folder').fill('Favorites )'); + await expect(page.getByText('This folder already exists')).toBeVisible(); + await page.locator('#folder').fill('abc-_'); + await expect(page.getByText('Enter the folder name')).toBeVisible(); + await page.getByRole('button', { name: 'Folder' }).click(); + await expect(page.locator('span').filter({ hasText: 'abc-_' })).toBeVisible(); +}) \ No newline at end of file diff --git a/e2e/onboarding.spec.ts b/e2e/onboarding.spec.ts new file mode 100644 index 0000000..a8d9e71 --- /dev/null +++ b/e2e/onboarding.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test'; + +test('onboarding steps', async ({ page }) => { + await page.context().setOffline(false) + await page.goto('/'); + await expect(page.getByText("Codex is a durable, decentralised data storage protocol, created so the world community can preserve its most important knowledge without risk of censorship.")).toBeVisible() + await page.locator('.navigation').click(); + await expect(page.locator('.navigation')).toHaveAttribute("aria-disabled"); + await page.getByLabel('Preferred name').fill('Arnaud'); + await expect(page.locator('.navigation')).not.toHaveAttribute("aria-disabled"); + await page.locator('.navigation').click(); + + // Network + await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).toBeVisible() + + // Port forwarding + await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-error")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(2).getByTestId("icon-success")).toBeVisible() + + // Codex node + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).toBeVisible() + + // Marketplace + await expect(page.locator(".health-checks ul li").nth(4).getByTestId("icon-error")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(4).getByTestId("icon-success")).toBeVisible() + + await page.context().setOffline(true) + + // Network + await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-error")).toBeVisible() + await expect(page.locator(".health-checks ul li").nth(1).getByTestId("icon-success")).not.toBeVisible() + + await page.context().setOffline(false) +}); + +// await expect(page.locator('#root')).toContainText('Network connected'); +// await page.locator('a').nth(2).click(); +// await page.context().setOffline(true) +// await expect(page.locator('#root')).toContainText('Network disconnected'); \ No newline at end of file diff --git a/e2e/settings.spec.ts b/e2e/settings.spec.ts index f1f16a9..6d35e47 100644 --- a/e2e/settings.spec.ts +++ b/e2e/settings.spec.ts @@ -1,25 +1,30 @@ import test, { expect } from "@playwright/test"; -test('update the log level', async ({ page }) => { - await page.goto('/dashboard'); - await page.getByRole('link', { name: 'Settings' }).click(); - await page.getByLabel('Log level').selectOption('TRACE'); - await page.getByRole('main').locator('div').filter({ hasText: 'Log' }).getByRole('button').click(); - await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible(); -}) +// test('update the log level', async ({ page }) => { +// await page.goto('/dashboard'); +// await page.getByRole('link', { name: 'Settings' }).click(); +// await page.getByLabel('Log level').selectOption('TRACE'); +// await page.getByRole('main').locator('div').filter({ hasText: 'Log' }).getByRole('button').click(); +// await expect(page.locator('span').filter({ hasText: 'success ! The log level has' }).locator('b')).toBeVisible(); +// }) test('update the URL with wrong URL applies', async ({ page }) => { await page.goto('/dashboard'); - await page.getByRole('link', { name: 'Settings' }).click(); - await page.getByLabel('Codex client node URL').click(); - await page.getByLabel('Codex client node URL').fill('hello'); - await expect.soft(page.getByText("The URL is not valid")).toBeVisible() - await expect.soft(page.locator(".settings-url-button")).toBeDisabled() - await page.getByLabel('Codex client node URL').fill('http://127.0.0.1:8079'); - await expect.soft(page.getByText("The URL is not valid")).not.toBeVisible() - await expect.soft(page.locator(".settings-url-button")).not.toBeDisabled() - await page.getByRole('button', { name: 'Save changes' }).nth(1).click(); - await expect.soft(page.getByText("Cannot retrieve the data")).toBeVisible() - await page.getByLabel('Codex client node URL').fill('http://127.0.0.1:8080'); - await page.getByRole('button', { name: 'Save changes' }).nth(1).click(); + await page.locator('a').filter({ hasText: 'Settings' }).click(); + await page.getByLabel('Address').click(); + await page.getByLabel('Address').fill('hello'); + await expect(page.getByLabel('Address')).toHaveAttribute("aria-invalid") + await expect(page.locator(".refresh svg")).toHaveAttribute("aria-disabled") + await page.getByLabel('Address').fill('http://127.0.0.1:8079'); + await expect(page.getByLabel('Address')).not.toHaveAttribute("aria-invalid") + await expect(page.locator(".refresh svg")).not.toHaveAttribute("aria-disabled") + await expect(page.getByLabel('Address')).toHaveValue("http://127.0.0.1") + await expect(page.getByLabel('Port')).toHaveValue("8079") + await page.locator(".refresh").click() + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).toBeVisible() + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).not.toBeVisible() + await page.getByLabel('Address').fill('http://127.0.0.1:8080'); + await page.locator(".refresh").click() + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-error")).not.toBeVisible() + await expect(page.locator(".health-checks ul li").nth(3).getByTestId("icon-success")).toBeVisible() }) \ No newline at end of file diff --git a/e2e/storage-requests.spec.ts b/e2e/storage-requests.spec.ts index 3db89ab..3dd6edf 100644 --- a/e2e/storage-requests.spec.ts +++ b/e2e/storage-requests.spec.ts @@ -5,14 +5,15 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -test('creates a storage request', async ({ page }) => { +test('create a storage request', async ({ page }) => { await page.goto('/dashboard'); - await page.getByRole('link', { name: 'Purchases' }).click(); + 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')).toHaveValue("zDvZRwzkvwapyNeL4mzw5gBsZvyn7x8F8Y9n4RYSC7ETBssDYpGe") + 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.getByRole('button', { name: 'Next' }).click(); @@ -27,51 +28,51 @@ test('select a uploaded cid when creating a storage request', async ({ page }) = await page.locator('div').getByTestId("upload").setInputFiles([ path.join(__dirname, "assets", 'chatgpt.jpg'), ]); - await page.getByRole('link', { name: 'Purchases' }).click(); + await page.locator('a').filter({ hasText: 'Purchases' }).click(); await page.getByRole('button', { name: 'Storage Request' }).click(); await page.getByPlaceholder('Select or type your CID').click(); - await page.locator('.dropdown-option').nth(1).click(); + await page.locator('.dropdown ul li').nth(1).click(); await expect(page.getByText('button[disabled]')).not.toBeVisible(); }) test('storage request navigation buttons', async ({ page }) => { await page.goto('/dashboard/purchases'); await page.getByRole('button', { name: 'Storage Request' }).click(); - await expect(page.locator('.stepper-number-done')).not.toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() - await expect(page.locator('.stepper-buttons .button--primary')).toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--done')).not.toBeVisible() + await expect(page.locator('.step--active')).toBeVisible() + await expect(page.locator('footer .button--primary')).toHaveAttribute("disabled"); + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); await page.locator('div').getByTestId("upload").setInputFiles([ path.join(__dirname, "assets", 'chatgpt.jpg'), ]); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); await page.getByRole('button', { name: 'Next' }).click(); - await expect(page.locator('.stepper-buttons .button--outline')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); - await expect(page.locator('.stepper-number-done')).toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('footer .button--outline').first()).not.toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--done')).toBeVisible() + await expect(page.locator('.step--active')).toBeVisible() await page.getByRole('button', { name: 'Back' }).click(); - await expect(page.locator('.stepper-number-done')).not.toBeVisible() - await expect(page.locator('.stepper-number-active')).toBeVisible() + await expect(page.locator('.step--done')).not.toBeVisible() + await expect(page.locator('.step--active')).toBeVisible() await page.getByRole('button', { name: 'Next' }).click(); await page.getByRole('button', { name: 'Next' }).click(); - await expect(page.locator('.stepper-number-done')).toHaveCount(2) - await expect(page.locator('.stepper-number-active')).toBeVisible() - await expect(page.locator('.stepper-buttons .button--outline')).toHaveAttribute("disabled"); - await expect(page.locator('.stepper-buttons .button--primary')).not.toHaveAttribute("disabled"); + await expect(page.locator('.step--done')).toHaveCount(2) + await expect(page.locator('.step--active')).toBeVisible() + await expect(page.locator('footer .button--outline').first()).toHaveAttribute("disabled"); + await expect(page.locator('footer .button--primary')).not.toHaveAttribute("disabled"); await page.getByRole('button', { name: 'Finish' }).click(); await expect(page.locator('.modal--open')).not.toBeVisible(); }) test('remove the CID when the file is deleted', async ({ page }) => { await page.goto('/dashboard'); - await page.getByRole('link', { name: 'Purchases' }).click(); + 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')).toHaveValue("zDvZRwzkvwapyNeL4mzw5gBsZvyn7x8F8Y9n4RYSC7ETBssDYpGe") - await page.locator('.uploadFile-infoRight .buttonIcon--small').click(); - await expect(page.locator('#cid')).toHaveValue("") + await expect(page.locator('#cid')).not.toBeEmpty() + await page.locator('.button-icon--small').click(); + await expect(page.locator('#cid')).toBeEmpty() }) diff --git a/e2e/upload.spec.ts b/e2e/upload.spec.ts index ebbf05e..c8b9582 100644 --- a/e2e/upload.spec.ts +++ b/e2e/upload.spec.ts @@ -26,8 +26,8 @@ test('multiple files upload', async ({ page }) => { await expect(page.getByText('File uploaded successfully').nth(1)).toBeVisible(); - await page.locator('.uploadFile-infoRight > .buttonIcon').first().click(); - await page.locator('.uploadFile-infoRight > .buttonIcon').click(); + await page.locator('.upload-file .button-icon--small').first().click(); + await page.locator('.upload-file .button-icon--small').click(); await expect(page.getByText('File uploaded successfully').first()).not.toBeVisible(); await expect(page.getByText('File uploaded successfully').nth(1)).not.toBeVisible(); @@ -42,7 +42,7 @@ test('drag and drop file', async ({ page }) => { const dataTransfer = await page.evaluateHandle((data) => { const dt = new DataTransfer(); // Convert the buffer to a hex array - const file = new File([data.toString('hex')], 'chat.jpg', { type: 'image/jpg' }); + const file = new File([data.toString('hex')], 'chat.jpg', { type: 'image/jpeg' }); dt.items.add(file); return dt; }, buffer); @@ -63,7 +63,7 @@ test('drag and drop file', async ({ page }) => { // mimeType: 'text/plain' // }); -// await page.locator('.uploadFile-infoRight > .buttonIcon--small').click(); +// await page.locator('.uploadFile-infoRight > .button-icon--small').click(); // await expect(page.getByText('The upload has been cancelled')).toBeVisible(); // }); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b83d1c7..1f35814 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,14 +9,15 @@ "version": "0.0.7", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.24", - "@codex-storage/sdk-js": "^0.0.8", + "@codex-storage/marketplace-ui-components": "0.0.42", + "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", "@tanstack/react-router": "^1.58.7", "dotted-map": "^2.2.3", "echarts": "^5.5.1", + "emoji-picker-react": "^4.12.0", "idb-keyval": "^6.2.1", "lucide-react": "^0.445.0", "react": "^18.3.1", @@ -24,7 +25,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.0", - "@tanstack/router-devtools": "^1.58.7", + "@svgr/plugin-svgo": "^8.1.0", "@tanstack/router-plugin": "^1.58.4", "@types/node": "^22.7.5", "@types/react": "^18.3.8", @@ -36,15 +37,62 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.12", + "postcss": "^8.4.47", + "postcss-nesting": "^13.0.1", "prettier": "^3.3.3", - "sass-embedded": "^1.79.4", "typescript": "5.5.4", - "vite": "^5.4.7" + "vite": "^5.4.7", + "vite-plugin-svgr": "^4.3.0", + "vitest": "^2.1.4" }, "engines": { "node": ">=18" } }, + "../storybook": { + "name": "@codex-storage/marketplace-ui-components", + "version": "0.0.36", + "extraneous": true, + "license": "MIT", + "dependencies": { + "lucide-react": "^0.453.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^2.0.2", + "@codex-storage/sdk-js": "^0.0.15", + "@storybook/addon-essentials": "^8.2.9", + "@storybook/addon-interactions": "^8.2.9", + "@storybook/addon-links": "^8.2.9", + "@storybook/addon-onboarding": "^8.2.9", + "@storybook/blocks": "^8.2.9", + "@storybook/react": "^8.2.9", + "@storybook/react-vite": "^8.2.9", + "@storybook/test": "^8.2.9", + "@typescript-eslint/eslint-plugin": "^8.6.0", + "@typescript-eslint/parser": "^8.0.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "glob": "^9.3.5", + "prettier": "^3.3.3", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "storybook": "^8.2.9", + "typescript": "5.5.2", + "vite-plugin-dts": "^4.0.3", + "vite-plugin-lib-inject-css": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@codex-storage/sdk-js": ">=0.0.14", + "postcss-nesting": "^13.0.1", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "dev": true, @@ -371,36 +419,39 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.1.0.tgz", "integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.24.tgz", - "integrity": "sha512-7YVpy70zC1rHxpUjFOt+gkhj1Rt9wG1Ls4hNtUzR4lFrICvCC8m4EuUg37FlgjTt2H9eLpVt1b090Wyz+CKeng==", + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.42.tgz", + "integrity": "sha512-JRs7v5rsxNnH3T30UV+DHuJNr25kJ1a1EAqgthA/0okDrcr9IlOVEvl7XrsNBGRDDbJC5MDJkWo9VD2Jh3gAgQ==", "dependencies": { - "lucide-react": "^0.441.0" + "lucide-react": "^0.453.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@codex-storage/sdk-js": ">=0.0.7", + "@codex-storage/sdk-js": ">=0.0.14", + "postcss-nesting": "^13.0.1", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "node_modules/@codex-storage/marketplace-ui-components/node_modules/lucide-react": { - "version": "0.441.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz", - "integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==", + "version": "0.453.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.453.0.tgz", + "integrity": "sha512-kL+RGZCcJi9BvJtzg2kshO192Ddy9hv3ij+cPrVPWSRzgCWCVazoQJxOjAwgK53NomL07HB7GPHW120FimjNhQ==", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "node_modules/@codex-storage/sdk-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.8.tgz", - "integrity": "sha512-NSHwDpWmRVHlCJHUDVr7FZ0HBxpyoFNXHTjPqOPeQDaGlN+5Yzf/9aBU+lmYVdvIk68BQIzlScMIisRf8IYw9A==", + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.0.15.tgz", + "integrity": "sha512-asL59uhHNI2zPLEcygh6HnZEdQJebSjU2JXut6xZxAI87VVkWfN9Y1BxlyxgsF+wJF10DB0tnv3cPEI3wb2huQ==", "dependencies": { "valibot": "^0.32.0" }, @@ -408,6 +459,48 @@ "node": ">=20" } }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", + "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -634,6 +727,46 @@ "node": ">=18" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", @@ -962,6 +1095,240 @@ "node": ">=14.18" } }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, "node_modules/@tanstack/history": { "version": "1.61.1", "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.61.1.tgz", @@ -1043,28 +1410,6 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, - "node_modules/@tanstack/router-devtools": { - "version": "1.58.7", - "resolved": "https://registry.npmjs.org/@tanstack/router-devtools/-/router-devtools-1.58.7.tgz", - "integrity": "sha512-bZL3VDmS63gOW+RKSXRQ7uagATP1k8sM+ucHrcLy98hcVxzYRVwIwVgqTZY2KtUSXgFwb4LXClAdZdiJM9i+gw==", - "dev": true, - "dependencies": { - "clsx": "^2.1.1", - "goober": "^2.1.14" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "@tanstack/react-router": "^1.58.7", - "react": ">=18", - "react-dom": ">=18" - } - }, "node_modules/@tanstack/router-generator": { "version": "1.74.2", "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.74.2.tgz", @@ -1623,6 +1968,15 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@turf/boolean-point-in-polygon": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", @@ -1947,6 +2301,112 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@vitest/expect": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.4.tgz", + "integrity": "sha512-DOETT0Oh1avie/D/o2sgMHGrzYUFFo3zqESB2Hn70z6QB1HrS2IQ9z5DfyTqU8sg4Bpu13zZe9V4+UTNQlUeQA==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.4.tgz", + "integrity": "sha512-Ky/O1Lc0QBbutJdW0rqLeFNbuLEyS+mIPiNdlVlp2/yhJ0SbyYqObS5IHdhferJud8MbbwMnexg4jordE5cCoQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.4.tgz", + "integrity": "sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.4.tgz", + "integrity": "sha512-sKRautINI9XICAMl2bjxQM8VfCMTB0EbsBc/EDFA57V6UQevEKY/TOPOF5nzcvCALltiLfXWbq4MaAwWx/YxIA==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.4", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.4.tgz", + "integrity": "sha512-3Kab14fn/5QZRog5BPj6Rs8dc4B+mim27XaKWFWHWA87R56AKjHTGcBFKpvZKDzC4u5Wd0w/qKsUIio3KzWW4Q==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.4", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.4.tgz", + "integrity": "sha512-4JOxa+UAizJgpZfaCPKK2smq9d8mmjZVPMt2kOsg/R8QkoRzydHH1qHxIYNvr1zlEaFj4SXiaaJWxq/LPLKaLg==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.4.tgz", + "integrity": "sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.4", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.12.1", "dev": true, @@ -2018,6 +2478,15 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/babel-dead-code-elimination": { "version": "1.0.6", "dev": true, @@ -2045,6 +2514,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2101,7 +2576,18 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "dev": true + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } }, "node_modules/callsites": { "version": "3.1.0", @@ -2111,6 +2597,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001668", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", @@ -2131,6 +2629,22 @@ } ] }, + "node_modules/chai": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", + "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -2145,6 +2659,15 @@ "node": ">=4" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "3.6.0", "dev": true, @@ -2182,14 +2705,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/clsx": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2209,7 +2724,18 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.5.2.tgz", "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", - "dev": true + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -2221,6 +2747,32 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -2234,6 +2786,91 @@ "node": ">= 8" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, "node_modules/csstype": { "version": "3.1.3", "dev": true, @@ -2255,11 +2892,29 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/deep-is": { "version": "0.1.4", "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "dev": true, @@ -2271,6 +2926,71 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -2312,6 +3032,41 @@ "integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==", "dev": true }, + "node_modules/emoji-picker-react": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz", + "integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2596,6 +3351,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "dev": true, @@ -2604,6 +3368,15 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -2680,6 +3453,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==" + }, "node_modules/flat-cache": { "version": "3.2.0", "dev": true, @@ -2783,14 +3561,6 @@ "node": ">=4" } }, - "node_modules/goober": { - "version": "2.1.14", - "dev": true, - "license": "MIT", - "peerDependencies": { - "csstype": "^3.0.10" - } - }, "node_modules/graphemer": { "version": "1.4.0", "dev": true, @@ -2828,7 +3598,9 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/import-fresh": { "version": "3.3.0", @@ -2867,6 +3639,12 @@ "dev": true, "license": "ISC" }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-binary-path": { "version": "2.1.0", "dev": true, @@ -2950,6 +3728,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "dev": true, @@ -2991,6 +3775,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "dev": true, @@ -3020,6 +3810,21 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", + "dev": true + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3037,6 +3842,21 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3086,7 +3906,6 @@ }, "node_modules/nanoid": { "version": "3.3.7", - "dev": true, "funding": [ { "type": "github", @@ -3106,6 +3925,16 @@ "dev": true, "license": "MIT" }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3120,6 +3949,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -3183,6 +4024,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -3207,9 +4066,32 @@ "node": ">=8" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.0", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3254,8 +4136,9 @@ } }, "node_modules/postcss": { - "version": "8.4.45", - "dev": true, + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -3270,16 +4153,53 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-nesting": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", + "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -3497,6 +4417,8 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -3506,6 +4428,8 @@ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.80.3.tgz", "integrity": "sha512-aTxTl4ToSAWg7ILFgAe+kMenj+zNlwHmHK/ZNPrOM8+HTef1Q6zuxolptYLijmHdZHKSMOkWYHgo5MMN6+GIyg==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", @@ -3556,6 +4480,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3572,6 +4497,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3588,6 +4514,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3604,6 +4531,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3620,6 +4548,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3636,6 +4565,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3652,6 +4582,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3668,6 +4599,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3684,6 +4616,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3700,6 +4633,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3716,6 +4650,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3732,6 +4667,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3748,6 +4684,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3764,6 +4701,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3780,6 +4718,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3796,6 +4735,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3812,6 +4752,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3828,6 +4769,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3844,6 +4786,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3860,6 +4803,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -3869,6 +4813,8 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -3878,6 +4824,8 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -3923,14 +4871,41 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map-js": { "version": "1.2.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, "node_modules/strip-ansi": { "version": "6.0.1", "dev": true, @@ -3965,6 +4940,37 @@ "node": ">=4" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, "node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -3978,6 +4984,45 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "dev": true, @@ -4122,6 +5167,11 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/valibot": { "version": "0.32.0", "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.32.0.tgz", @@ -4131,7 +5181,9 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", - "dev": true + "dev": true, + "optional": true, + "peer": true }, "node_modules/vite": { "version": "5.4.9", @@ -4192,6 +5244,41 @@ } } }, + "node_modules/vite-node": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.4.tgz", + "integrity": "sha512-kqa9v+oi4HwkG6g8ufRnb5AeplcRw8jUF6/7/Qz1qRQOXHImG8YnLbB+LLszENwFnoBl9xIf9nVdCFzNd7GQEg==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-svgr": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", + "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.3", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=2.6.0" + } + }, "node_modules/vite/node_modules/@esbuild/linux-x64": { "version": "0.21.5", "cpu": [ @@ -4610,6 +5697,71 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/vitest": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.4.tgz", + "integrity": "sha512-eDjxbVAJw1UJJCHr5xr/xM86Zx+YxIEXGAR+bmnEID7z9qWfoxpHw0zdobz+TQAFOLT+nEXz3+gx6nUJ7RgmlQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.4", + "@vitest/mocker": "2.1.4", + "@vitest/pretty-format": "^2.1.4", + "@vitest/runner": "2.1.4", + "@vitest/snapshot": "2.1.4", + "@vitest/spy": "2.1.4", + "@vitest/utils": "2.1.4", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.4", + "@vitest/ui": "2.1.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "dev": true, @@ -4629,6 +5781,22 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wkt-parser": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz", diff --git a/package.json b/package.json index 12161c7..8204889 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview --host 127.0.0.1 --port 5173", "format": "prettier --write ./src", - "test": "npx playwright test" + "test": "npx playwright test", + "test:unit": "vitest run" }, "keywords": [ "Codex", @@ -24,14 +25,15 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.24", - "@codex-storage/sdk-js": "^0.0.8", + "@codex-storage/marketplace-ui-components": "0.0.42", + "@codex-storage/sdk-js": "^0.0.15", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", "@tanstack/react-query": "^5.51.15", "@tanstack/react-router": "^1.58.7", - "echarts": "^5.5.1", "dotted-map": "^2.2.3", + "echarts": "^5.5.1", + "emoji-picker-react": "^4.12.0", "idb-keyval": "^6.2.1", "lucide-react": "^0.445.0", "react": "^18.3.1", @@ -39,7 +41,7 @@ }, "devDependencies": { "@playwright/test": "^1.48.0", - "@tanstack/router-devtools": "^1.58.7", + "@svgr/plugin-svgo": "^8.1.0", "@tanstack/router-plugin": "^1.58.4", "@types/node": "^22.7.5", "@types/react": "^18.3.8", @@ -51,10 +53,13 @@ "eslint": "^8.57.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-refresh": "^0.4.12", + "postcss": "^8.4.47", + "postcss-nesting": "^13.0.1", "prettier": "^3.3.3", - "sass-embedded": "^1.79.4", "typescript": "5.5.4", - "vite": "^5.4.7" + "vite": "^5.4.7", + "vite-plugin-svgr": "^4.3.0", + "vitest": "^2.1.4" }, "engines": { "node": ">=18" diff --git a/postcss.config.json b/postcss.config.json new file mode 100644 index 0000000..65ccabb --- /dev/null +++ b/postcss.config.json @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + 'postcss-nesting': { /* plugin options */ }, + }, + } + \ No newline at end of file diff --git a/public/fonts/AzeretMono-VariableFont.ttf b/public/fonts/AzeretMono-VariableFont.ttf new file mode 100644 index 0000000..4e508e8 Binary files /dev/null and b/public/fonts/AzeretMono-VariableFont.ttf differ diff --git a/public/fonts/Inter-VariableFont.ttf b/public/fonts/Inter-VariableFont.ttf new file mode 100644 index 0000000..e31b51e Binary files /dev/null and b/public/fonts/Inter-VariableFont.ttf differ diff --git a/public/icons/circle-error.svg b/public/icons/circle-error.svg new file mode 100644 index 0000000..90de158 --- /dev/null +++ b/public/icons/circle-error.svg @@ -0,0 +1,12 @@ + + + + + diff --git a/public/icons/onboarding.png b/public/icons/onboarding.png new file mode 100644 index 0000000..835c6a1 Binary files /dev/null and b/public/icons/onboarding.png differ diff --git a/public/icons/select-arrow.svg b/public/icons/select-arrow.svg new file mode 100644 index 0000000..d68c733 --- /dev/null +++ b/public/icons/select-arrow.svg @@ -0,0 +1,10 @@ + + + diff --git a/public/icons/us-flag.svg b/public/icons/us-flag.svg new file mode 100644 index 0000000..9a4d69f --- /dev/null +++ b/public/icons/us-flag.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + diff --git a/public/img/assistance.png b/public/img/assistance.png new file mode 100644 index 0000000..306c513 Binary files /dev/null and b/public/img/assistance.png differ diff --git a/public/img/assistance.webp b/public/img/assistance.webp new file mode 100644 index 0000000..9269984 Binary files /dev/null and b/public/img/assistance.webp differ diff --git a/public/img/assistance@2x.png b/public/img/assistance@2x.png new file mode 100644 index 0000000..0ac7623 Binary files /dev/null and b/public/img/assistance@2x.png differ diff --git a/public/img/assistance@2x.webp b/public/img/assistance@2x.webp new file mode 100644 index 0000000..950ca89 Binary files /dev/null and b/public/img/assistance@2x.webp differ diff --git a/public/img/assistance@3x.png b/public/img/assistance@3x.png new file mode 100644 index 0000000..cc205bf Binary files /dev/null and b/public/img/assistance@3x.png differ diff --git a/public/img/assistance@3x.webp b/public/img/assistance@3x.webp new file mode 100644 index 0000000..9ed2dce Binary files /dev/null and b/public/img/assistance@3x.webp differ diff --git a/public/img/onboarding.png b/public/img/onboarding.png new file mode 100644 index 0000000..28c0587 Binary files /dev/null and b/public/img/onboarding.png differ diff --git a/public/img/onboarding.webp b/public/img/onboarding.webp new file mode 100644 index 0000000..d93661e Binary files /dev/null and b/public/img/onboarding.webp differ diff --git a/public/img/onboarding@2x.png b/public/img/onboarding@2x.png new file mode 100644 index 0000000..caf731d Binary files /dev/null and b/public/img/onboarding@2x.png differ diff --git a/public/img/onboarding@2x.webp b/public/img/onboarding@2x.webp new file mode 100644 index 0000000..af91ea6 Binary files /dev/null and b/public/img/onboarding@2x.webp differ diff --git a/public/img/onboarding@3x.png b/public/img/onboarding@3x.png new file mode 100644 index 0000000..6a8fc22 Binary files /dev/null and b/public/img/onboarding@3x.png differ diff --git a/public/img/onboarding@3x.webp b/public/img/onboarding@3x.webp new file mode 100644 index 0000000..b5a6a7c Binary files /dev/null and b/public/img/onboarding@3x.webp differ diff --git a/public/img/wallet-login.png b/public/img/wallet-login.png new file mode 100644 index 0000000..143673e Binary files /dev/null and b/public/img/wallet-login.png differ diff --git a/public/img/wallet.png b/public/img/wallet.png new file mode 100644 index 0000000..eb269a4 Binary files /dev/null and b/public/img/wallet.png differ diff --git a/public/img/wallet.webp b/public/img/wallet.webp new file mode 100644 index 0000000..90fd5e4 Binary files /dev/null and b/public/img/wallet.webp differ diff --git a/public/img/welcome.png b/public/img/welcome.png new file mode 100644 index 0000000..58429ff Binary files /dev/null and b/public/img/welcome.png differ diff --git a/public/img/welcome.webp b/public/img/welcome.webp new file mode 100644 index 0000000..54cd028 Binary files /dev/null and b/public/img/welcome.webp differ diff --git a/public/img/welcome@2x.png b/public/img/welcome@2x.png new file mode 100644 index 0000000..b3d6a3d Binary files /dev/null and b/public/img/welcome@2x.png differ diff --git a/public/img/welcome@2x.webp b/public/img/welcome@2x.webp new file mode 100644 index 0000000..b9c637e Binary files /dev/null and b/public/img/welcome@2x.webp differ diff --git a/public/img/welcome@3x.png b/public/img/welcome@3x.png new file mode 100644 index 0000000..12bbe5d Binary files /dev/null and b/public/img/welcome@3x.png differ diff --git a/public/img/welcome@3x.webp b/public/img/welcome@3x.webp new file mode 100644 index 0000000..8d8db3a Binary files /dev/null and b/public/img/welcome@3x.webp differ diff --git a/src/assets/icons/all.svg b/src/assets/icons/all.svg new file mode 100644 index 0000000..9239ff7 --- /dev/null +++ b/src/assets/icons/all.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/alpha.svg b/src/assets/icons/alpha.svg new file mode 100644 index 0000000..f7362bd --- /dev/null +++ b/src/assets/icons/alpha.svg @@ -0,0 +1,7 @@ + + + + diff --git a/src/assets/icons/alphatext.svg b/src/assets/icons/alphatext.svg new file mode 100644 index 0000000..adcf1ee --- /dev/null +++ b/src/assets/icons/alphatext.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/assets/icons/analytics.svg b/src/assets/icons/analytics.svg new file mode 100644 index 0000000..acee199 --- /dev/null +++ b/src/assets/icons/analytics.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/archive.svg b/src/assets/icons/archive.svg new file mode 100644 index 0000000..c40495b --- /dev/null +++ b/src/assets/icons/archive.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/arrow-circle.svg b/src/assets/icons/arrow-circle.svg new file mode 100644 index 0000000..793e324 --- /dev/null +++ b/src/assets/icons/arrow-circle.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/arrow-left.svg b/src/assets/icons/arrow-left.svg new file mode 100644 index 0000000..a1e034b --- /dev/null +++ b/src/assets/icons/arrow-left.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/arrow-right.svg b/src/assets/icons/arrow-right.svg new file mode 100644 index 0000000..2cd522d --- /dev/null +++ b/src/assets/icons/arrow-right.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/availability.svg b/src/assets/icons/availability.svg new file mode 100644 index 0000000..371458e --- /dev/null +++ b/src/assets/icons/availability.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/chart.svg b/src/assets/icons/chart.svg new file mode 100644 index 0000000..7811c77 --- /dev/null +++ b/src/assets/icons/chart.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/chevron.svg b/src/assets/icons/chevron.svg new file mode 100644 index 0000000..ca83bd9 --- /dev/null +++ b/src/assets/icons/chevron.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/choose-cid.svg b/src/assets/icons/choose-cid.svg new file mode 100644 index 0000000..1bc2a0f --- /dev/null +++ b/src/assets/icons/choose-cid.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/close.svg b/src/assets/icons/close.svg new file mode 100644 index 0000000..c3c854f --- /dev/null +++ b/src/assets/icons/close.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/commitment.svg b/src/assets/icons/commitment.svg new file mode 100644 index 0000000..481c959 --- /dev/null +++ b/src/assets/icons/commitment.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/copy.svg b/src/assets/icons/copy.svg new file mode 100644 index 0000000..46abc70 --- /dev/null +++ b/src/assets/icons/copy.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/dashboard.svg b/src/assets/icons/dashboard.svg new file mode 100644 index 0000000..e09f608 --- /dev/null +++ b/src/assets/icons/dashboard.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/device.svg b/src/assets/icons/device.svg new file mode 100644 index 0000000..af27b03 --- /dev/null +++ b/src/assets/icons/device.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/disclaimer.svg b/src/assets/icons/disclaimer.svg new file mode 100644 index 0000000..d16dd9d --- /dev/null +++ b/src/assets/icons/disclaimer.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/discord.svg b/src/assets/icons/discord.svg new file mode 100644 index 0000000..8b26b63 --- /dev/null +++ b/src/assets/icons/discord.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/document.svg b/src/assets/icons/document.svg new file mode 100644 index 0000000..b2c7c12 --- /dev/null +++ b/src/assets/icons/document.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/download-file.svg b/src/assets/icons/download-file.svg new file mode 100644 index 0000000..abf0247 --- /dev/null +++ b/src/assets/icons/download-file.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 0000000..34f0576 --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/durability.svg b/src/assets/icons/durability.svg new file mode 100644 index 0000000..068f115 --- /dev/null +++ b/src/assets/icons/durability.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/edit.svg b/src/assets/icons/edit.svg new file mode 100644 index 0000000..ad389bd --- /dev/null +++ b/src/assets/icons/edit.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/error-circle.svg b/src/assets/icons/error-circle.svg new file mode 100644 index 0000000..a8e775f --- /dev/null +++ b/src/assets/icons/error-circle.svg @@ -0,0 +1,9 @@ + + + diff --git a/src/assets/icons/expand.svg b/src/assets/icons/expand.svg new file mode 100644 index 0000000..3444cde --- /dev/null +++ b/src/assets/icons/expand.svg @@ -0,0 +1,16 @@ + + + + diff --git a/src/assets/icons/favorite.svg b/src/assets/icons/favorite.svg new file mode 100644 index 0000000..9303307 --- /dev/null +++ b/src/assets/icons/favorite.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/fetch.svg b/src/assets/icons/fetch.svg new file mode 100644 index 0000000..42d7e8b --- /dev/null +++ b/src/assets/icons/fetch.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/file-details.svg b/src/assets/icons/file-details.svg new file mode 100644 index 0000000..93b20d0 --- /dev/null +++ b/src/assets/icons/file-details.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/files-outline.svg b/src/assets/icons/files-outline.svg new file mode 100644 index 0000000..5ebfd5e --- /dev/null +++ b/src/assets/icons/files-outline.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/files.svg b/src/assets/icons/files.svg new file mode 100644 index 0000000..e683889 --- /dev/null +++ b/src/assets/icons/files.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/flash.svg b/src/assets/icons/flash.svg new file mode 100644 index 0000000..0fa8538 --- /dev/null +++ b/src/assets/icons/flash.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/folder.svg b/src/assets/icons/folder.svg new file mode 100644 index 0000000..3f1b795 --- /dev/null +++ b/src/assets/icons/folder.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/hardrive.svg b/src/assets/icons/hardrive.svg new file mode 100644 index 0000000..29e449a --- /dev/null +++ b/src/assets/icons/hardrive.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/help.svg b/src/assets/icons/help.svg new file mode 100644 index 0000000..6b3f736 --- /dev/null +++ b/src/assets/icons/help.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/home.svg b/src/assets/icons/home.svg new file mode 100644 index 0000000..faf32bd --- /dev/null +++ b/src/assets/icons/home.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/host.svg b/src/assets/icons/host.svg new file mode 100644 index 0000000..eadb08c --- /dev/null +++ b/src/assets/icons/host.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/image.svg b/src/assets/icons/image.svg new file mode 100644 index 0000000..dc74d58 --- /dev/null +++ b/src/assets/icons/image.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/info-file.svg b/src/assets/icons/info-file.svg new file mode 100644 index 0000000..fca4e37 --- /dev/null +++ b/src/assets/icons/info-file.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/info.svg b/src/assets/icons/info.svg new file mode 100644 index 0000000..014ef66 --- /dev/null +++ b/src/assets/icons/info.svg @@ -0,0 +1,13 @@ + + + diff --git a/src/assets/icons/lines.svg b/src/assets/icons/lines.svg new file mode 100644 index 0000000..168afa6 --- /dev/null +++ b/src/assets/icons/lines.svg @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/src/assets/icons/logo.svg b/src/assets/icons/logo.svg new file mode 100644 index 0000000..ca1d049 --- /dev/null +++ b/src/assets/icons/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/src/assets/icons/logotype.svg b/src/assets/icons/logotype.svg new file mode 100644 index 0000000..32a4c54 --- /dev/null +++ b/src/assets/icons/logotype.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/src/assets/icons/logs.svg b/src/assets/icons/logs.svg new file mode 100644 index 0000000..1610ebc --- /dev/null +++ b/src/assets/icons/logs.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/nodes.svg b/src/assets/icons/nodes.svg new file mode 100644 index 0000000..162bbb6 --- /dev/null +++ b/src/assets/icons/nodes.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/peers.svg b/src/assets/icons/peers.svg new file mode 100644 index 0000000..1dba5b9 --- /dev/null +++ b/src/assets/icons/peers.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/plus-circle.svg b/src/assets/icons/plus-circle.svg new file mode 100644 index 0000000..108a6e2 --- /dev/null +++ b/src/assets/icons/plus-circle.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 0000000..a4c885d --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/preset.svg b/src/assets/icons/preset.svg new file mode 100644 index 0000000..805a299 --- /dev/null +++ b/src/assets/icons/preset.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/assets/icons/purchase-history-outline.svg b/src/assets/icons/purchase-history-outline.svg new file mode 100644 index 0000000..162bf2e --- /dev/null +++ b/src/assets/icons/purchase-history-outline.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/purchase.svg b/src/assets/icons/purchase.svg new file mode 100644 index 0000000..73621d0 --- /dev/null +++ b/src/assets/icons/purchase.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/purchases-state-pending.svg b/src/assets/icons/purchases-state-pending.svg new file mode 100644 index 0000000..b868e75 --- /dev/null +++ b/src/assets/icons/purchases-state-pending.svg @@ -0,0 +1,11 @@ + + + diff --git a/src/assets/icons/refresh.svg b/src/assets/icons/refresh.svg new file mode 100644 index 0000000..935bc1b --- /dev/null +++ b/src/assets/icons/refresh.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/request-duration.svg b/src/assets/icons/request-duration.svg new file mode 100644 index 0000000..d174d46 --- /dev/null +++ b/src/assets/icons/request-duration.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/save.svg b/src/assets/icons/save.svg new file mode 100644 index 0000000..8ca6e5a --- /dev/null +++ b/src/assets/icons/save.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/settings.svg b/src/assets/icons/settings.svg new file mode 100644 index 0000000..0c16c76 --- /dev/null +++ b/src/assets/icons/settings.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/slot.svg b/src/assets/icons/slot.svg new file mode 100644 index 0000000..9482663 --- /dev/null +++ b/src/assets/icons/slot.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/success-circle.svg b/src/assets/icons/success-circle.svg new file mode 100644 index 0000000..6aea5db --- /dev/null +++ b/src/assets/icons/success-circle.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/upload.svg b/src/assets/icons/upload.svg new file mode 100644 index 0000000..8972e14 --- /dev/null +++ b/src/assets/icons/upload.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/video.svg b/src/assets/icons/video.svg new file mode 100644 index 0000000..2afd68b --- /dev/null +++ b/src/assets/icons/video.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/wallet.svg b/src/assets/icons/wallet.svg new file mode 100644 index 0000000..85bb7f2 --- /dev/null +++ b/src/assets/icons/wallet.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/icons/warning-circle.svg b/src/assets/icons/warning-circle.svg new file mode 100644 index 0000000..839a501 --- /dev/null +++ b/src/assets/icons/warning-circle.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/assets/icons/warning.svg b/src/assets/icons/warning.svg new file mode 100644 index 0000000..523a66c --- /dev/null +++ b/src/assets/icons/warning.svg @@ -0,0 +1,10 @@ + + + diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx new file mode 100644 index 0000000..b9a1540 --- /dev/null +++ b/src/components/AppBar/AppBar.tsx @@ -0,0 +1,111 @@ +import "./appBar.css"; +import { classnames } from "../../utils/classnames"; +import { useNetwork } from "../../network/useNetwork"; +import { useQueryClient } from "@tanstack/react-query"; +import { ReactElement, useEffect } from "react"; +import { useCodexConnection } from "../../hooks/useCodexConnection"; +import { usePersistence } from "../../hooks/usePersistence"; +import { useLocation, useNavigate } from "@tanstack/react-router"; +import DashboardIcon from "../../assets/icons/dashboard.svg?react"; +import PeersIcon from "../../assets/icons/peers.svg?react"; +import NodesIcon from "../../assets/icons/nodes.svg?react"; +import FilesIcon from "../../assets/icons/files.svg?react"; +import LogsIcon from "../../assets/icons/logs.svg?react"; +import HostIcon from "../../assets/icons/host.svg?react"; +import SettingsIcon from "../../assets/icons/settings.svg?react"; +import WalletIcon from "../../assets/icons/wallet.svg?react"; +import NetworkFlashIcon from "../../assets/icons/flash.svg?react"; +import PurchasesIcon from "../../assets/icons/purchase.svg?react"; +import HelpIcon from "../../assets/icons/help.svg?react"; +import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react"; +import { WalletConnect } from "../WalletLogin/WalletLogin"; + +type Props = { + onIconClick: () => void; +}; + +const icons: Record = { + dashboard: , + peers: , + settings: , + files: , + logs: , + availabilities: , + wallet: , + purchases: , + help: , + disclaimer: , +}; + +const descriptions: Record = { + dashboard: "Get Overview of your Codex Vault.", + peers: "Monitor your Codex peer connections.", + settings: "Manage your Codex Vault.", + files: "Manage your files in your local vault.", + logs: "Manage your logs and debug console.", + availabilities: "Manage your host data.", + wallet: "Manage your Codex wallet.", + purchases: "Manage your storage requests.", + help: "Quick help resources.", + disclaimer: "Important information.", +}; + +export function AppBar({ onIconClick }: Props) { + const online = useNetwork(); + const queryClient = useQueryClient(); + const codex = useCodexConnection(); + const persistence = usePersistence(codex.enabled); + const location = useLocation(); + const navigate = useNavigate({ from: location.pathname }); + + useEffect(() => { + queryClient.invalidateQueries({ + type: "active", + refetchType: "all", + }); + }, [queryClient, codex.enabled]); + + const offline = !online || !codex.enabled; + + const onNodeClick = () => navigate({ to: "/dashboard/settings" }); + + const title = + location.pathname.split("/")[2] || location.pathname.split("/")[1]; + const networkIconColor = online + ? "#3EE089" + : "var(--codex-input-color-error)"; + const nodesIconColor = codex.enabled + ? "#3EE089" + : "var(--codex-input-color-error)"; + + return ( + <> +
+
+ {icons[title]} + +
+

{title}

+

{descriptions[title]}

+
+
+ +
+ + ); +} diff --git a/src/components/AppBar/appBar.css b/src/components/AppBar/appBar.css new file mode 100644 index 0000000..084a830 --- /dev/null +++ b/src/components/AppBar/appBar.css @@ -0,0 +1,95 @@ +.app-bar { + height: 80px; + justify-content: space-between; + border-bottom: 1px solid var(--codex-border-color); + display: flex; + padding: 16px; + border-bottom: 1px solid #96969633; + box-sizing: border-box; + background-color: #1c1c1c; + border-right: 12px solid transparent; + position: sticky; + top: 0; + z-index: 2; + + @media (min-width: 1000px) { + & { + padding: 20px 48px; + } + } + + &:not(.app-bar--offline):not(.app-bar--no-persistence) { + border-right-color: #6ccc93; + } + + &.app-bar--offline { + border-right-color: var(--codex-input-color-error); + } + + &.app-bar--no-persistence { + border-right-color: rgb(var(--codex-color-warning)); + } + + h1 { + font: + 500 18px/24px "Inter", + sans-serif; + letter-spacing: -0.015em; + color: white; + text-transform: capitalize; + } + + h2 { + font: + 400 14px/20px "Inter", + sans-serif; + letter-spacing: -0.006em; + color: #969696cc; + } + + > div { + span { + background: #141414; + height: 48px; + width: 48px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #353639; + border-radius: 50%; + color: #969696; + } + } + + aside { + svg { + padding: 10px; + background-color: #141414; + border-radius: var(--codex-border-radius); + } + + span { + font: + 500 14px/20px "Inter", + sans-serif; + letter-spacing: -0.006em; + color: #8d8d8d; + + @media (max-width: 999px) { + & { + display: none; + } + } + } + + > div:last-child { + cursor: pointer; + } + } +} + +/* @media (min-width: 1000px) { + .appBar-burger { + display: none; + } +} */ diff --git a/src/components/Availability/AvailabilitiesTable.css b/src/components/Availability/AvailabilitiesTable.css deleted file mode 100644 index 23b14f0..0000000 --- a/src/components/Availability/AvailabilitiesTable.css +++ /dev/null @@ -1,8 +0,0 @@ -.availabilityTable-chevron { - cursor: pointer; - transition: transform 0.35s; -} - -.availabilityTable-chevron--open { - transform: rotate(180deg); -} diff --git a/src/components/Availability/AvailabilitiesTable.tsx b/src/components/Availability/AvailabilitiesTable.tsx index 78c156d..b2c71d8 100644 --- a/src/components/Availability/AvailabilitiesTable.tsx +++ b/src/components/Availability/AvailabilitiesTable.tsx @@ -1,4 +1,9 @@ -import { Cell, Row, Table } from "@codex-storage/marketplace-ui-components"; +import { + Cell, + Row, + Table, + TabSortState, +} from "@codex-storage/marketplace-ui-components"; import { PrettyBytes } from "../../utils/bytes"; import { AvailabilityActionsCell } from "./AvailabilityActionsCell"; import { CodexAvailability, CodexNodeSpace } from "@codex-storage/sdk-js/async"; @@ -6,13 +11,13 @@ import { Times } from "../../utils/times"; import { Fragment, useState } from "react"; import { AvailabilityReservations } from "./AvailabilityReservations"; import { AvailabilityIdCell } from "./AvailabilityIdCell"; -import { ChevronDown } from "lucide-react"; -import "./AvailabilitiesTable.css"; import { Arrays } from "../../utils/arrays"; -import { AvailabilitySlotRow } from "./AvailabilitySlotRow"; -import { classnames } from "../../utils/classnames"; +import { SlotRow } from "./SlotRow"; import { AvailabilityWithSlots } from "./types"; import { AvailabilityDiskRow } from "./AvailabilityDiskRow"; +import { attributes } from "../../utils/attributes"; +import ChevronIcon from "../../assets/icons/chevron.svg?react"; +import { AvailabilityUtils } from "./availability.utils"; type Props = { // onEdit: () => void; @@ -20,25 +25,47 @@ type Props = { availabilities: AvailabilityWithSlots[]; }; +type SortFn = (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => number; + export function AvailabilitiesTable({ availabilities, space }: Props) { const [availability, setAvailability] = useState( null ); const [details, setDetails] = useState([]); - - const headers = [ - "", - "id", - "total size", - "duration", - "min price", - "max collateral", - "actions", - ]; + const [sortFn, setSortFn] = useState(() => + AvailabilityUtils.sortById("desc") + ); const onReservationsClose = () => setAvailability(null); - const rows = availabilities.map((a, index) => { + const onSortById = (state: TabSortState) => + setSortFn(() => AvailabilityUtils.sortById(state)); + + const onSortBySize = (state: TabSortState) => + setSortFn(() => AvailabilityUtils.sortBySize(state)); + + const onSortByDuration = (state: TabSortState) => + setSortFn(() => AvailabilityUtils.sortByDuration(state)); + + const onSortByPrice = (state: TabSortState) => + setSortFn(() => AvailabilityUtils.sortByPrice(state)); + + const onSortByCollateral = (state: TabSortState) => + setSortFn(() => AvailabilityUtils.sortByCollateral(state)); + + const headers = [ + [""], + ["id", onSortById], + ["total size", onSortBySize], + ["duration", onSortByDuration], + ["min price", onSortByPrice], + ["max collateral", onSortByCollateral], + ["actions"], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const sorted = sortFn ? [...availabilities].sort(sortFn) : availabilities; + + const rows = sorted.map((a) => { const showDetails = details.includes(a.id); const onShowDetails = () => setDetails(Arrays.toggle(details, a.id)); @@ -47,20 +74,18 @@ export function AvailabilitiesTable({ availabilities, space }: Props) { return ( {hasSlots ? ( - + ) : ( - + "" )} , - , + , {PrettyBytes(a.totalSize)}, {Times.pretty(a.duration)}, {a.minPrice.toString()}, @@ -69,11 +94,11 @@ export function AvailabilitiesTable({ availabilities, space }: Props) { ]}> {a.slots.map((slot) => ( - + id={slot.id}> ))} ); @@ -85,7 +110,7 @@ export function AvailabilitiesTable({ availabilities, space }: Props) { return ( <> - +
void; }; -/* eslint-disable @typescript-eslint/no-unused-vars */ -export function AvailabilityActionsCell(_: Props) { - // const onEditClick = async () => { - // const unit = availability.totalSize >= 1_000_000_000 ? "gb" : "mb"; - // const totalSize = - // unit === "gb" - // ? availability.totalSize / 1_000_000_000 - // : availability.totalSize / 1_000_000; - - // await WebStorage.set("availability-step-1", { - // ...availability, - // totalSize, - // totalSizeUnit: unit, - // }); - - // onEdit(); - // }; +export function AvailabilityActionsCell({ availability }: Props) { + const onEditClick = async () => { + document.dispatchEvent( + new CustomEvent("codexavailabilityedit", { detail: availability }) + ); + }; return (
- - - +
); diff --git a/src/components/Availability/AvailabilityConfirm.css b/src/components/Availability/AvailabilityConfirm.css index 84b0a38..923fefa 100644 --- a/src/components/Availability/AvailabilityConfirm.css +++ b/src/components/Availability/AvailabilityConfirm.css @@ -1,3 +1,10 @@ +.availability-confirm { + header { + margin-top: 16px; + margin-bottom: 16px; + } +} + .availabilitConfirm-bottom { margin-top: 1.5rem; display: flex; diff --git a/src/components/Availability/AvailabilityConfirmation.tsx b/src/components/Availability/AvailabilityConfirmation.tsx index 977552a..3e8ed62 100644 --- a/src/components/Availability/AvailabilityConfirmation.tsx +++ b/src/components/Availability/AvailabilityConfirmation.tsx @@ -1,9 +1,10 @@ -import "./AvailabilityForm.css"; +import "./AvailabilityConfirm.css"; import { AvailabilityComponentProps } from "./types"; import "./AvailabilityConfirm.css"; import { Info } from "lucide-react"; -import { AvailabilitySpaceAllocation } from "./AvailabilitySpaceAllocation"; import { useEffect } from "react"; +import { SpaceAllocation } from "@codex-storage/marketplace-ui-components"; +import NodesIcon from "../../assets/icons/nodes.svg?react"; export function AvailabilityConfirm({ dispatch, @@ -18,9 +19,40 @@ export function AvailabilityConfirm({ }); }, [dispatch]); + const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space; + const isUpdating = !!availability.id; + const allocated = isUpdating + ? quotaReservedBytes - availability.totalSize + quotaUsedBytes + : quotaReservedBytes + quotaUsedBytes; + const remaining = + availability.totalSize > quotaMaxBytes - allocated + ? quotaMaxBytes - allocated + : quotaMaxBytes - allocated - availability.totalSize; + return ( - <> - +
+
+ +
Disk
+
+
@@ -36,6 +68,6 @@ export function AvailabilityConfirm({

- +
); } diff --git a/src/components/Availability/AvailabilityDiskRow.css b/src/components/Availability/AvailabilityDiskRow.css deleted file mode 100644 index d27469c..0000000 --- a/src/components/Availability/AvailabilityDiskRow.css +++ /dev/null @@ -1,10 +0,0 @@ -.availabilityDiskRow { - border-bottom: 5px solid var(--codex-border-color); - background-color: var(--codex-background-light); -} - -.availabilityDiskRow-cell-content { - display: flex; - align-items: center; - gap: 1rem; -} diff --git a/src/components/Availability/AvailabilityDiskRow.tsx b/src/components/Availability/AvailabilityDiskRow.tsx index 01e7732..9ee444d 100644 --- a/src/components/Availability/AvailabilityDiskRow.tsx +++ b/src/components/Availability/AvailabilityDiskRow.tsx @@ -1,11 +1,7 @@ -import { - Cell, - Row, - SimpleText, -} from "@codex-storage/marketplace-ui-components"; +import { Cell, Row } from "@codex-storage/marketplace-ui-components"; import { PrettyBytes } from "../../utils/bytes"; -import "./AvailabilityDiskRow.css"; import { classnames } from "../../utils/classnames"; +import HardriveIcon from "../../assets/icons/hardrive.svg?react"; type Props = { bytes: number; @@ -14,61 +10,19 @@ type Props = { export function AvailabilityDiskRow({ bytes }: Props) { return ( + , - -
- + +
+
-
- Node -
- - {PrettyBytes(bytes)} allocated for the node - + Node + {PrettyBytes(bytes)} allocated for the node
, ]}> ); } - -const HardDrive = () => ( - - - - - - - - - -); diff --git a/src/components/Availability/AvailabilityEdit.css b/src/components/Availability/AvailabilityEdit.css index 6cb96ca..085867d 100644 --- a/src/components/Availability/AvailabilityEdit.css +++ b/src/components/Availability/AvailabilityEdit.css @@ -1,5 +1,48 @@ -@media (min-width: 801px) { - .availabilityCreate .stepper-body { - width: 600px; +.availability-edit { + > .button { + top: 0; + bottom: 0px; + left: 0; + right: 0; + position: absolute; + margin: auto; + border-radius: 100%; + height: 88px; + width: 88px; + background-color: white; + + div { + position: relative; + left: 4px; + width: 40px; + height: 40px; + color: black; + } + } + + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + } + + svg { + color: #969696; + } + + header { + display: flex; + gap: 8px; + } + + input { + background-color: transparent; + } + + .space-allocation { + margin-bottom: 16px; } } diff --git a/src/components/Availability/AvailabilityEdit.tsx b/src/components/Availability/AvailabilityEdit.tsx index 5c8a8ed..7ef63a0 100644 --- a/src/components/Availability/AvailabilityEdit.tsx +++ b/src/components/Availability/AvailabilityEdit.tsx @@ -4,18 +4,20 @@ import { Button, Modal, } from "@codex-storage/marketplace-ui-components"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { AvailabilityForm } from "./AvailabilityForm"; -import { Pencil, Plus } from "lucide-react"; import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { AvailabilityConfirm } from "./AvailabilityConfirmation"; import { WebStorage } from "../../utils/web-storage"; import { AvailabilityState } from "./types"; -import { STEPPER_DURATION } from "../../utils/constants"; +import { GB, STEPPER_DURATION } from "../../utils/constants"; import { useAvailabilityMutation } from "./useAvailabilityMutation"; import { AvailabilitySuccess } from "./AvailabilitySuccess"; import { AvailabilityError } from "./AvailabilityError"; import "./AvailabilityEdit.css"; +import PlusIcon from "../../assets/icons/plus.svg?react"; +import HostIcon from "../../assets/icons/host.svg?react"; +import { Times } from "../../utils/times"; type Props = { space: CodexNodeSpace; @@ -26,8 +28,8 @@ type Props = { const CONFIRM_STATE = 2; const defaultAvailabilityData: AvailabilityState = { - totalSize: 1, - duration: 1, + totalSize: 0.5 * GB, + duration: Times.unitValue("days"), minPrice: 0, maxCollateral: 0, totalSizeUnit: "gb", @@ -45,7 +47,7 @@ export function AvailabilityEdit({ ); const { state, dispatch } = useStepperReducer(); const { mutateAsync, error } = useAvailabilityMutation(dispatch, state); - const [availabilityId, setAvailabilityId] = useState(null); + const editAvailabilityValue = useRef(0); useEffect(() => { Promise.all([ @@ -66,24 +68,24 @@ export function AvailabilityEdit({ }, [dispatch]); // We use a custom event to not re render the sunburst component - useEffect(() => { - const onAvailabilityIdChange = (e: Event) => { - const custom = e as CustomEvent; - setAvailabilityId(custom.detail); - }; + // useEffect(() => { + // const onAvailabilityIdChange = (e: Event) => { + // const custom = e as CustomEvent; + // setAvailabilityId(custom.detail); + // }; - document.addEventListener( - "codexavailabilityid", - onAvailabilityIdChange, - false - ); + // document.addEventListener( + // "codexavailabilityid", + // onAvailabilityIdChange, + // false + // ); - return () => - document.removeEventListener( - "codexavailabilityid", - onAvailabilityIdChange - ); - }, []); + // return () => + // document.removeEventListener( + // "codexavailabilityid", + // onAvailabilityIdChange + // ); + // }, []); const components = [ AvailabilityForm, @@ -110,7 +112,8 @@ export function AvailabilityEdit({ WebStorage.set("availability-step", step); if (step == CONFIRM_STATE) { - mutateAsync(availability); + const { slots, name, ...rest } = availability as any; + mutateAsync(rest); } else { dispatch({ step, @@ -122,18 +125,12 @@ export function AvailabilityEdit({ const onAvailabilityChange = (data: Partial) => { const val = { ...availability, ...data }; - WebStorage.set("availability", val); - setAvailability(val); }; - const onOpen = () => { - if (availability.id) { - WebStorage.set("availability-step", 0); - WebStorage.set("availability", defaultAvailabilityData); - - setAvailability(defaultAvailabilityData); - } + const onOpen = useCallback(() => { + setAvailability(defaultAvailabilityData); + editAvailabilityValue.current = 0; dispatch({ type: "open", @@ -143,7 +140,48 @@ export function AvailabilityEdit({ step: 0, type: "next", }); - }; + }, [editAvailabilityValue, dispatch]); + + useEffect(() => { + document.addEventListener("codexavailabilitycreate", onOpen, false); + + return () => + document.removeEventListener("codexavailabilitycreate", onOpen); + }, [onOpen]); + + const onEdit = useCallback( + (event: Event) => { + const e = event as CustomEvent; + const a = e.detail; + + editAvailabilityValue.current = a.totalSize; + WebStorage.set("availability-step", 0); + WebStorage.set("availability", a); + + const unit = Times.unit(a.duration); + + setAvailability({ + ...a, + durationUnit: unit as "hours" | "days" | "months", + }); + + dispatch({ + type: "open", + }); + + dispatch({ + step: 0, + type: "next", + }); + }, + [editAvailabilityValue, dispatch] + ); + + useEffect(() => { + document.addEventListener("codexavailabilityedit", onEdit, false); + + return () => document.removeEventListener("codexavailabilityedit", onEdit); + }, [onEdit, dispatch]); const onClose = () => dispatch({ type: "close" }); @@ -153,34 +191,40 @@ export function AvailabilityEdit({ return ( <> -
); } diff --git a/src/components/Availability/AvailabilityForm.css b/src/components/Availability/AvailabilityForm.css index c67b73e..262e8c1 100644 --- a/src/components/Availability/AvailabilityForm.css +++ b/src/components/Availability/AvailabilityForm.css @@ -1,31 +1,65 @@ -.availabilityForm-itemInput { - width: 100%; -} +.availability-form { + input { + width: 100%; + font-family: Inter; + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: -0.015em; + width: 100%; + } -.availabilityForm-item { - margin-bottom: 1rem; -} + option { + background-color: #232323; + } -.availabilityForm-item--error .input, -.availabilityForm-item--error .inputGroup-helper, -.availabilityForm-item--error .inputGroup-select { - color: rgb(var(--codex-color-error)); - border-color: rgb(var(--codex-color-error)); -} + header { + margin-top: 16px; + margin-bottom: 16px; + } -.availabilityForm-row { - display: flex; - gap: 0.5rem; -} + .row { + margin-bottom: 16px; + } -.availabilityForm-itemInput-maxSize { - color: var(--codex-color-primary); - padding-right: 0.5rem; - font-size: 0.85rem; - cursor: pointer; - transition: 0.35s opacity; -} + .group { + position: relative; + flex: 1; + margin-top: 16px; -.availabilityForm-itemInput-maxSize:hover { - opacity: 0.7; + &[aria-invalid] { + .input-group > div > div > div:nth-child(2) { + border-color: var(--codex-input-color-error); + } + + label, + svg, + select { + color: var(--codex-input-color-error); + } + } + } + + .tooltip { + position: absolute; + right: 0px; + top: 0px; + } + + .input-group p { + max-width: inherit; + position: relative; + + a { + cursor: pointer; + color: var(--codex-color-primary); + top: 4px; + right: 4px; + position: absolute; + } + } + + select { + min-width: min-content; + } } diff --git a/src/components/Availability/AvailabilityForm.tsx b/src/components/Availability/AvailabilityForm.tsx index 6e6a799..6f4c847 100644 --- a/src/components/Availability/AvailabilityForm.tsx +++ b/src/components/Availability/AvailabilityForm.tsx @@ -1,37 +1,39 @@ -import { Input, InputGroup } from "@codex-storage/marketplace-ui-components"; -import { ChangeEvent, useEffect, useState } from "react"; +import { + Input, + InputGroup, + SpaceAllocation, + Tooltip, +} from "@codex-storage/marketplace-ui-components"; +import { ChangeEvent, useEffect } from "react"; import "./AvailabilityForm.css"; import { AvailabilityComponentProps } from "./types"; -import { classnames } from "../../utils/classnames"; -import { AvailabilitySpaceAllocation } from "./AvailabilitySpaceAllocation"; -import { - availabilityMax, - availabilityUnit, - isAvailabilityValid, -} from "./availability.domain"; +import NodesIcon from "../../assets/icons/nodes.svg?react"; +import InfoIcon from "../../assets/icons/info.svg?react"; +import { attributes } from "../../utils/attributes"; +import { AvailabilityUtils } from "./availability.utils"; +import { Times } from "../../utils/times"; export function AvailabilityForm({ dispatch, onAvailabilityChange, availability, space, + editAvailabilityValue, }: AvailabilityComponentProps) { - const [availabilityValue, setAvailabilityValue] = useState( - ( - availability.totalSize / availabilityUnit(availability.totalSizeUnit) - ).toFixed(2) - ); - useEffect(() => { - const max = availabilityMax(space); - const isValid = isAvailabilityValid(availability, max); + let max = AvailabilityUtils.maxValue(space); + if (availability.id && editAvailabilityValue) { + max += editAvailabilityValue; + } + + const isValid = AvailabilityUtils.isValid(availability, max); dispatch({ type: "toggle-buttons", isNextEnable: isValid, isBackEnable: true, }); - }, [dispatch, space, availability]); + }, [dispatch, space, availability, editAvailabilityValue]); const onTotalSizeUnitChange = async (e: ChangeEvent) => { const element = e.currentTarget; @@ -42,24 +44,33 @@ export function AvailabilityForm({ }); }; - const onDurationUnitChange = async (e: ChangeEvent) => { + const onDurationChange = async (e: ChangeEvent) => { const element = e.currentTarget; + const unitValue = Times.unitValue(availability.durationUnit); onAvailabilityChange({ - duration: 1, - durationUnit: element.value as "hours" | "days" | "months", + duration: parseInt(element.value) * unitValue, + }); + }; + + const onDurationUnitChange = async (e: ChangeEvent) => { + const element = e.currentTarget; + const unit = element.value as "hours" | "days" | "months"; + const unitValue = Times.unitValue(unit); + + onAvailabilityChange({ + duration: unitValue, + durationUnit: unit, }); }; const onAvailablityChange = async (e: ChangeEvent) => { const element = e.currentTarget; const v = element.value; - const unit = availabilityUnit(availability.totalSizeUnit); - - setAvailabilityValue(v); + const unit = AvailabilityUtils.unitValue(availability.totalSizeUnit); onAvailabilityChange({ - [element.name]: parseFloat(v) * unit, + totalSize: parseFloat(v) * unit, }); }; @@ -72,128 +83,162 @@ export function AvailabilityForm({ }); }; - // const domain = new AvailabilityDomain(space, availability); - const onMaxSize = () => { - const available = - space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes; - - const unit = availabilityUnit(availability.totalSizeUnit); - - setAvailabilityValue((available / unit).toFixed(2)); + const available = AvailabilityUtils.maxValue(space); onAvailabilityChange({ totalSize: available, }); }; - const available = - space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes; - const isValid = available >= availability.totalSize; - const unit = availabilityUnit(availability.totalSizeUnit); - const max = available / unit; + let available = AvailabilityUtils.maxValue(space); + if (availability.id && editAvailabilityValue) { + available += editAvailabilityValue; + } + + const isValid = + availability.totalSize > 0 && available >= availability.totalSize; + const helper = isValid ? "Total size of sale's storage in bytes" : "The total size cannot exceed the space available."; + const value = AvailabilityUtils.toUnit( + availability.totalSize, + availability.totalSizeUnit + ).toFixed(2); + + const unitValue = Times.unitValue(availability.durationUnit); + const duration = availability.duration / unitValue; + return ( - <> - +
+
+ +
Disk
+
+ - - Use max size - - } - /> - -
- -
- -
-
- +
+ Use max size} /> + + +
-
- + + + +
-
+
+
+
+ + + + +
+ +
+ + + + +
+
+
+ +
+ + +
- +
); } diff --git a/src/components/Availability/AvailabilityIdCell.css b/src/components/Availability/AvailabilityIdCell.css deleted file mode 100644 index 263df35..0000000 --- a/src/components/Availability/AvailabilityIdCell.css +++ /dev/null @@ -1,5 +0,0 @@ -.availabilityIdCell { - display: flex; - align-items: center; - gap: 1rem; -} diff --git a/src/components/Availability/AvailabilityIdCell.tsx b/src/components/Availability/AvailabilityIdCell.tsx index 5d46917..aec2721 100644 --- a/src/components/Availability/AvailabilityIdCell.tsx +++ b/src/components/Availability/AvailabilityIdCell.tsx @@ -1,62 +1,31 @@ -import "./AvailabilityIdCell.css"; import { Strings } from "../../utils/strings"; -import { Cell, SimpleText } from "@codex-storage/marketplace-ui-components"; +import { Cell } from "@codex-storage/marketplace-ui-components"; import { PrettyBytes } from "../../utils/bytes"; -import { availabilityColors } from "./availability.colors"; import { AvailabilityWithSlots } from "./types"; +import AvailbilityIcon from "../../assets/icons/availability.svg?react"; type Props = { value: AvailabilityWithSlots; - index: number; }; -export function AvailabilityIdCell({ value, index }: Props) { +export function AvailabilityIdCell({ value }: Props) { return ( -
- +
+
{value.name || Strings.shortId(value.id)}
- + {PrettyBytes(value.totalSize)} allocated for the availability - -
- {/*
- - {a.id} - -
*/} - - Max collateral {value.maxCollateral} | Min price {value.minPrice} - -
+
+
+ + Max collateral {value.maxCollateral} | Min price {value.minPrice} +
); } - -const Folder = ({ color }: { color: string }) => ( - - - - - -); diff --git a/src/components/Availability/AvailabilitySheetCreate.tsx b/src/components/Availability/AvailabilitySheetCreate.tsx index 3fc91e7..fb7ca19 100644 --- a/src/components/Availability/AvailabilitySheetCreate.tsx +++ b/src/components/Availability/AvailabilitySheetCreate.tsx @@ -2,7 +2,7 @@ import { Stepper, useStepperReducer, Button, - Sheets, + Modal, } from "@codex-storage/marketplace-ui-components"; import { useEffect, useRef, useState } from "react"; import { AvailabilityForm } from "./AvailabilityForm"; @@ -140,9 +140,8 @@ export function AvailabilitySheetCreate({ className={className} /> - + - + ); } diff --git a/src/components/Availability/AvailabilitySlotRow.css b/src/components/Availability/AvailabilitySlotRow.css deleted file mode 100644 index 5dd2217..0000000 --- a/src/components/Availability/AvailabilitySlotRow.css +++ /dev/null @@ -1,35 +0,0 @@ -.availabilitySlotRow { - border-bottom: none; -} - -.availabilitySlotRow--active { - border-bottom: 1px solid var(--codex-border-color); -} - -.availabilitySlotRow-cell { - padding: 0; -} - -.availabilitySlotRow--inactive { - display: none; -} - -.availabilitySlotRow-cell-content { - display: flex; - align-items: center; - gap: 1rem; - height: 0; - overflow: hidden; - transition: height 0.35s; - will-change: height; - padding-left: 3rem; - padding-right: 1rem; -} - -.availabilitySlotRow--active .availabilitySlotRow-cell-content { - height: 65px; -} - -.availabilitySlotRow--closing .availabilitySlotRow-cell-content { - height: 0; -} diff --git a/src/components/Availability/AvailabilitySlotRow.tsx b/src/components/Availability/AvailabilitySlotRow.tsx deleted file mode 100644 index 137a655..0000000 --- a/src/components/Availability/AvailabilitySlotRow.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { - Cell, - Row, - SimpleText, -} from "@codex-storage/marketplace-ui-components"; -import { PrettyBytes } from "../../utils/bytes"; -import "./AvailabilitySlotRow.css"; -import { classnames } from "../../utils/classnames"; -import { useEffect, useState } from "react"; - -type Props = { - bytes: number; - id: string; - active: boolean; -}; - -export function AvailabilitySlotRow({ bytes, active, id }: Props) { - const [className, setClassName] = useState("availabilitySlotRow--inactive"); - - useEffect(() => { - if (active) { - setClassName("availabilitySlotRow--opening"); - - setTimeout(() => { - setClassName("availabilitySlotRow--active"); - }, 15); - } else { - setClassName("availabilitySlotRow--closing"); - - setTimeout(() => { - setClassName("availabilitySlotRow--inactive"); - }, 350); - } - }, [active]); - - return ( - - - , - -
- -
-
- Slot {id} -
- - {PrettyBytes(bytes)} allocated for the slot - -
-
-
, - ]}>
- ); -} - -const SlotIcon = () => ( - - - - - -); diff --git a/src/components/Availability/AvailabilitySpaceAllocation.tsx b/src/components/Availability/AvailabilitySpaceAllocation.tsx index 08f8438..b3495fe 100644 --- a/src/components/Availability/AvailabilitySpaceAllocation.tsx +++ b/src/components/Availability/AvailabilitySpaceAllocation.tsx @@ -2,7 +2,7 @@ import { CodexNodeSpace } from "@codex-storage/sdk-js"; import { AvailabilityState } from "./types"; import { SpaceAllocation } from "@codex-storage/marketplace-ui-components"; import "./AvailabilitySpaceAllocation.css"; -import { nodeSpaceAllocationColors } from "../NodeSpaceAllocation/nodeSpaceAllocation.domain"; +import { nodeSpaceAllocationColors } from "../NodeSpace/nodeSpace.domain"; type Props = { space: CodexNodeSpace; diff --git a/src/components/Availability/AvailabilitySunburst.css b/src/components/Availability/AvailabilitySunburst.css deleted file mode 100644 index 86c8e96..0000000 --- a/src/components/Availability/AvailabilitySunburst.css +++ /dev/null @@ -1,5 +0,0 @@ -.activity-sunburst { - height: 600px; - width: 600px; - margin: auto; -} diff --git a/src/components/Availability/SlotRow.css b/src/components/Availability/SlotRow.css new file mode 100644 index 0000000..32f5611 --- /dev/null +++ b/src/components/Availability/SlotRow.css @@ -0,0 +1,13 @@ +.slot-row { + transition: + visibility 0.35s, + max-height 0.35s; + + td:nth-child(2) { + padding-left: 48px; + } + + &.slot-row--inactive { + visibility: collapse; + } +} diff --git a/src/components/Availability/SlotRow.tsx b/src/components/Availability/SlotRow.tsx new file mode 100644 index 0000000..7cc2793 --- /dev/null +++ b/src/components/Availability/SlotRow.tsx @@ -0,0 +1,56 @@ +import { Cell, Row } from "@codex-storage/marketplace-ui-components"; +import { PrettyBytes } from "../../utils/bytes"; +import "./SlotRow.css"; +import { classnames } from "../../utils/classnames"; +import { attributes } from "../../utils/attributes"; +import SlotIcon from "../../assets/icons/slot.svg?react"; + +type Props = { + bytes: number; + id: string; + active: boolean; +}; + +export function SlotRow({ bytes, active, id }: Props) { + // const [className, setClassName] = useState("slot-row--inactive"); + + // useEffect(() => { + // if (active) { + // setClassName("slot-row--opening"); + + // setTimeout(() => { + // setClassName("slot-row--active"); + // }, 15); + // } else { + // setClassName("slot-row--closing"); + + // setTimeout(() => { + // setClassName("slot-row--inactive"); + // }, 350); + // } + // }, [active]); + + return ( + + + , + +
+ +
+ Slot {id} + {PrettyBytes(bytes)} allocated for the slot +
+
+
, + ]}>
+ ); +} diff --git a/src/components/Availability/Sunburst.css b/src/components/Availability/Sunburst.css new file mode 100644 index 0000000..80d6ecd --- /dev/null +++ b/src/components/Availability/Sunburst.css @@ -0,0 +1,5 @@ +.sunburst { + height: 350px; + width: 350px; + margin: auto; +} diff --git a/src/components/Availability/AvailabilitySunburst.tsx b/src/components/Availability/Sunburst.tsx similarity index 76% rename from src/components/Availability/AvailabilitySunburst.tsx rename to src/components/Availability/Sunburst.tsx index 5179d4a..31608fd 100644 --- a/src/components/Availability/AvailabilitySunburst.tsx +++ b/src/components/Availability/Sunburst.tsx @@ -5,23 +5,25 @@ import { PrettyBytes } from "../../utils/bytes"; import { useEffect, useRef, useState } from "react"; import { CallbackDataParams, ECBasicOption } from "echarts/types/dist/shared"; import * as echarts from "echarts"; -import { availabilityColors } from "./availability.colors"; +import { availabilityColors, slotColors } from "./availability.colors"; import { AvailabilityWithSlots } from "./types"; -import "./AvailabilitySunburst.css"; +import "./Sunburst.css"; type Props = { availabilities: AvailabilityWithSlots[]; space: CodexNodeSpace; }; -export function AvailabilitySunburst({ availabilities, space }: Props) { +export function Sunburst({ availabilities, space }: Props) { const div = useRef(null); const chart = useRef(null); const [, setRefresher] = useState(Date.now()); useEffect(() => { if (div.current && !chart.current) { - chart.current = echarts.init(div.current); + chart.current = echarts.init(div.current, null, { + renderer: "svg", + }); setRefresher(Date.now()); } }, [chart, div]); @@ -32,7 +34,7 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { value: a.totalSize, itemStyle: { color: availabilityColors[index], - borderColor: "var(--codex-background)", + borderColor: "transparent", }, tooltip: { backgroundColor: "#333", @@ -64,8 +66,8 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { value: parseFloat(slot.size), children: [], itemStyle: { - color: availabilityColors[index], - borderColor: "var(--codex-background)", + color: slotColors[index], + borderColor: "transparent", }, tooltip: { backgroundColor: "#333", @@ -98,8 +100,8 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { space.quotaUsedBytes, children: [], itemStyle: { - color: "#ccc", - borderColor: "var(--codex-background)", + color: "#2F2F2F", + borderColor: "transparent", }, tooltip: { backgroundColor: "#333", @@ -120,12 +122,12 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { }, }, ], + radius: [60, "90%"], itemStyle: { - // borderRadius: 7, borderWidth: 1, }, label: { - show: true, + show: false, }, levels: [ {}, @@ -139,10 +141,7 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { { r0: "75%", r: "85%", - itemStyle: { - shadowBlur: 80, - shadowColor: "#ccc", - }, + itemStyle: {}, label: { position: "outside", textShadowBlur: 5, @@ -163,23 +162,23 @@ export function AvailabilitySunburst({ availabilities, space }: Props) { if (chart.current) { chart.current.setOption(option); - chart.current.off("click"); - chart.current.on("click", function (params) { - // console.info(params.componentIndex); - // console.info(params.dataIndex); + // chart.current.off("click"); + // chart.current.on("click", function (params) { + // // console.info(params.componentIndex); + // // console.info(params.dataIndex); - const index = params.dataIndex; + // const index = params.dataIndex; - const detail = - params.dataIndex === 0 ? null : availabilities[index - 1].id; + // const detail = + // params.dataIndex === 0 ? null : availabilities[index - 1].id; - document.dispatchEvent( - new CustomEvent("codexavailabilityid", { - detail, - }) - ); - }); + // document.dispatchEvent( + // new CustomEvent("codexavailabilityid", { + // detail, + // }) + // ); + // }); } - return
; + return
; } diff --git a/src/components/Availability/availability.colors.ts b/src/components/Availability/availability.colors.ts index 478b5f9..be484b2 100644 --- a/src/components/Availability/availability.colors.ts +++ b/src/components/Availability/availability.colors.ts @@ -1,33 +1,19 @@ export const availabilityColors = [ - "#004d00", // Very Dark Green - "#1B5E20", // Dark Green - "#2E7D32", // Medium Dark Green - "#388E3C", // Medium Green - "#43A047", // Bright Forest Green - "#4CAF50", // Green - "#5CB85C", // Medium Green - "#66BB6A", // Light Green - "#76FF03", // Bright Green - "#A5D6A7", // Soft Green - "#007A33", // Darker Green - "#009639", // Vivid Green - "#3B8A3B", // Medium Olive Green - "#4E9F3D", // Olive Green - "#5CBA3D", // Olive Drab - "#6BBE45", // Light Olive Green - "#7ED957", // Bright Olive - "#8BC34A", // Light Olive - "#A4D65E", // Olive Green - "#B2DFDB", // Soft Mint Green - "#C8E6C9", // Pale Green - "#AEEA00", // Lime Green - "#B9FBC0", // Soft Mint - "#C5E1A5", // Soft Light Green - "#DCE775", // Light Lime - "#A4D65E", // Olive Green - "#4CAF50", // Green - "#66BB6A", // Light Green - "#007A33", // Darker Green - "#009639", // Vivid Green - "#3B8A3B", // Medium Olive Green + "#34A0FFFF", + "#34A0FFEE", + "#34A0FFDD", + "#34A0FFCC", + "#34A0FFBB", + "#34A0FFAA", + "#34A0FF99", +]; + +export const slotColors = [ + "#D2493CFF", + "#D2493CEE", + "#D2493CDD", + "#D2493CCC", + "#D2493CBB", + "#D2493CAA", + "#D2493C99", ]; \ No newline at end of file diff --git a/src/components/Availability/availability.domain.ts b/src/components/Availability/availability.domain.ts index 9bf5d0c..199d867 100644 --- a/src/components/Availability/availability.domain.ts +++ b/src/components/Availability/availability.domain.ts @@ -38,12 +38,6 @@ export class AvailabilityDomain { } } -export const availabilityUnit = (unit: "gb" | "tb") => - unit === "gb" ? GB : TB; - -export const availabilityMax = (space: CodexNodeSpace) => - space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes; - export const isAvailabilityValid = ( availability: AvailabilityState, max: number diff --git a/src/components/Availability/availability.utils.test.ts b/src/components/Availability/availability.utils.test.ts new file mode 100644 index 0000000..e883fbe --- /dev/null +++ b/src/components/Availability/availability.utils.test.ts @@ -0,0 +1,159 @@ +import { assert, describe, it } from "vitest"; +import { AvailabilityUtils } from "./availability.utils"; + +describe("files", () => { + it("sorts by id", async () => { + const a = { + id: "a", + totalSize: 0, + duration: 0, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + const b = { + id: "b", + totalSize: 0, + duration: 0, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + + const items = [a, b,] + + const descSorted = items.slice().sort(AvailabilityUtils.sortById("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(AvailabilityUtils.sortById("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by size", async () => { + const a = { + id: "", + totalSize: 1, + duration: 0, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + const b = { + id: "", + totalSize: 2, + duration: 0, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + + const items = [a, b,] + + const descSorted = items.slice().sort(AvailabilityUtils.sortBySize("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(AvailabilityUtils.sortBySize("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by duration", async () => { + const a = { + id: "", + totalSize: 0, + duration: 1, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + const b = { + id: "", + totalSize: 0, + duration: 2, + minPrice: 0, + maxCollateral: 0, + name: "", + slots: [] + } + + const items = [a, b,] + + const descSorted = items.slice().sort(AvailabilityUtils.sortByDuration("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(AvailabilityUtils.sortByDuration("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by price", async () => { + const a = { + id: "", + totalSize: 0, + duration: 0, + minPrice: 1, + maxCollateral: 0, + name: "", + slots: [] + } + const b = { + id: "", + totalSize: 0, + duration: 0, + minPrice: 2, + maxCollateral: 0, + name: "", + slots: [] + } + + const items = [a, b,] + + const descSorted = items.slice().sort(AvailabilityUtils.sortByPrice("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(AvailabilityUtils.sortByPrice("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by collateral", async () => { + const a = { + id: "", + totalSize: 0, + duration: 0, + minPrice: 0, + maxCollateral: 1, + name: "", + slots: [] + } + const b = { + id: "", + totalSize: 0, + duration: 0, + minPrice: 0, + maxCollateral: 2, + name: "", + slots: [] + } + + const items = [a, b,] + + const descSorted = items.slice().sort(AvailabilityUtils.sortByCollateral("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(AvailabilityUtils.sortByCollateral("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); +}) \ No newline at end of file diff --git a/src/components/Availability/availability.utils.ts b/src/components/Availability/availability.utils.ts new file mode 100644 index 0000000..28c5ae7 --- /dev/null +++ b/src/components/Availability/availability.utils.ts @@ -0,0 +1,52 @@ +import { TabSortState } from "@codex-storage/marketplace-ui-components" +import { AvailabilityState, AvailabilityWithSlots } from "./types" +import { GB, TB } from "../../utils/constants"; +import { CodexNodeSpace } from "@codex-storage/sdk-js"; + +export const AvailabilityUtils = { + sortById: (state: TabSortState) => + (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => { + + return state === "desc" + ? b.id + .toLocaleLowerCase() + .localeCompare(a.id.toLocaleLowerCase()) + : a.id + .toLocaleLowerCase() + .localeCompare(b.id.toLocaleLowerCase()) + }, + sortBySize: (state: TabSortState) => + (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc" + ? b.totalSize - a.totalSize + : a.totalSize - b.totalSize + , + sortByDuration: (state: TabSortState) => + (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc" + ? b.duration - a.duration + : a.duration - b.duration + , + sortByPrice: (state: TabSortState) => + (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc" + ? b.minPrice - a.minPrice + : a.minPrice - b.minPrice + , + sortByCollateral: (state: TabSortState) => + (a: AvailabilityWithSlots, b: AvailabilityWithSlots) => state === "desc" + ? b.maxCollateral - a.maxCollateral + : a.maxCollateral - b.maxCollateral + , + toUnit(bytes: number, unit: "gb" | "tb") { + return bytes / this.unitValue(unit || "gb") + }, + maxValue(space: CodexNodeSpace) { + return space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes + }, + unitValue(unit: "gb" | "tb") { + return unit === "tb" ? TB : GB + }, + isValid: ( + availability: AvailabilityState, + max: number + ) => availability.totalSize > 0 && availability.totalSize <= max + +} \ No newline at end of file diff --git a/src/components/Availability/types.tsx b/src/components/Availability/types.tsx index d5744f2..8aa7d8b 100644 --- a/src/components/Availability/types.tsx +++ b/src/components/Availability/types.tsx @@ -27,6 +27,7 @@ export type AvailabilityComponentProps = { onAvailabilityChange: (data: Partial) => void; availability: AvailabilityState; error: Error | null; + editAvailabilityValue?: number; }; export type AvailabilityWithSlots = CodexAvailability & { diff --git a/src/components/Availability/useAvailabilityMutation.ts b/src/components/Availability/useAvailabilityMutation.ts index 9727de6..64a8838 100644 --- a/src/components/Availability/useAvailabilityMutation.ts +++ b/src/components/Availability/useAvailabilityMutation.ts @@ -7,9 +7,7 @@ import { StepperAction, StepperState, } from "@codex-storage/marketplace-ui-components"; -import { Times } from "../../utils/times"; import { CodexSdk } from "../../sdk/codex"; -import { AvailabilityStorage } from "../../utils/availabilities-storage"; import { CodexAvailabilityCreateResponse } from "@codex-storage/sdk-js"; @@ -26,12 +24,10 @@ export function useAvailabilityMutation( totalSize, totalSizeUnit, duration, - durationUnit = "days", + durationUnit, name, ...input }: AvailabilityState) => { - const time = Times.toSeconds(duration, durationUnit); - const fn: ( input: Omit ) => Promise<"" | CodexAvailabilityCreateResponse> = input.id @@ -46,7 +42,7 @@ export function useAvailabilityMutation( return fn({ ...input, - duration: time, + duration, totalSize: Math.trunc(totalSize), }); }, @@ -58,7 +54,7 @@ export function useAvailabilityMutation( WebStorage.delete("availability-step"); if (typeof res === "object" && body.name) { - AvailabilityStorage.add(res.id, body.name) + WebStorage.availabilities.add(res.id, body.name) } setError(null); diff --git a/src/components/BackgroundImage/BackgroundImage.css b/src/components/BackgroundImage/BackgroundImage.css new file mode 100644 index 0000000..83ea6eb --- /dev/null +++ b/src/components/BackgroundImage/BackgroundImage.css @@ -0,0 +1,15 @@ +.background-img { + position: fixed; + right: -40px; + max-height: 90%; + width: auto; + /* z-index: -1; */ + transition: opacity 0.35s; + opacity: 0.3; + + @media (min-width: 1580px) { + & { + opacity: 1; + } + } +} diff --git a/src/components/BackgroundImage/BackgroundImage.tsx b/src/components/BackgroundImage/BackgroundImage.tsx new file mode 100644 index 0000000..cb4f1ac --- /dev/null +++ b/src/components/BackgroundImage/BackgroundImage.tsx @@ -0,0 +1,25 @@ +import "./BackgroundImage.css"; + +export function BackgroundImage() { + return ( + + + + Background Image + + ); +} diff --git a/src/components/Card/Card.css b/src/components/Card/Card.css new file mode 100644 index 0000000..543f0d2 --- /dev/null +++ b/src/components/Card/Card.css @@ -0,0 +1,32 @@ +.card { + border: 1px solid #96969633; + border-radius: 16px; + padding: 16px; + background-color: #232323; + + > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + + svg { + color: #969696; + } + + > div { + display: flex; + align-items: center; + gap: 12px; + } + + h5 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + } + } +} diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx new file mode 100644 index 0000000..4a9403a --- /dev/null +++ b/src/components/Card/Card.tsx @@ -0,0 +1,53 @@ +import { ComponentType, ReactElement, ReactNode } from "react"; +import "./Card.css"; +import { Button } from "@codex-storage/marketplace-ui-components"; +import { ErrorBoundary } from "@sentry/react"; +import { ErrorPlaceholder } from "../ErrorPlaceholder/ErrorPlaceholder"; + +type Props = { + className?: string; + icon: ReactNode; + buttonLabel?: string; + buttonIcon?: ComponentType; + buttonAction?: () => void; + title?: string; + children: ReactElement; +}; + +export function Card({ + icon, + buttonAction, + buttonLabel, + title, + children, + buttonIcon, + className = "", +}: Props) { + return ( +
+
+
+ {icon} +
{title}
+
+ {buttonLabel && ( + + )} +
+ ( + + )}> + {children} + +
+ ); +} diff --git a/src/components/CardNumbers/CardNumbers.css b/src/components/CardNumbers/CardNumbers.css index 3230fde..25b52be 100644 --- a/src/components/CardNumbers/CardNumbers.css +++ b/src/components/CardNumbers/CardNumbers.css @@ -1,87 +1,62 @@ -.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; - top: 0px; - /* --codex-button-icon-background: var(--codex-color-primary); */ - justify-content: space-between; - align-items: center; -} + label { + color: var(--codex-card-number-label-color); + } -.cardNumber-dataContainer .buttonIcon { - position: relative; - top: 3px; -} + input { + font-family: Inter; + font-size: 24px; + font-weight: 500; + line-height: 32px; + letter-spacing: -0.015em; + width: 100%; + background-color: transparent; + } -.cardNumber-tooltip { - color: var(--codex-color-disabled); - display: flex; -} + .tooltip { + position: absolute; + right: 0px; + top: 0px; + } -.cardNumber-info { - display: flex; - align-items: center; - gap: 0.5rem; -} + svg { + color: var(--codex-card-number-unit-color); + } -.cardNumber .input { - min-width: 0; - width: 65px; - height: 2.5rem; -} + /* 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 .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: 48px; + } } diff --git a/src/components/CardNumbers/CardNumbers.tsx b/src/components/CardNumbers/CardNumbers.tsx index a9e7750..03aa7f7 100644 --- a/src/components/CardNumbers/CardNumbers.tsx +++ b/src/components/CardNumbers/CardNumbers.tsx @@ -1,113 +1,36 @@ -import { - ButtonIcon, - SimpleText, -} 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(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) => { - 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) => { + const text = e.currentTarget.value; + onChange(e.currentTarget.value); const msg = onValidation?.(text); @@ -119,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 - ? () => - : isDirty - ? () => - : () => ; - return ( -
-
+ + + + + + {unit} + + {/*
<> @@ -165,13 +84,8 @@ export function CardNumbers({ {error ? ( {error} ) : ( - - {helper} - - )} + {helper} + )} */}
); } diff --git a/src/components/ConnectedAccount/ConnectedAccount.css b/src/components/ConnectedAccount/ConnectedAccount.css new file mode 100644 index 0000000..55f6c46 --- /dev/null +++ b/src/components/ConnectedAccount/ConnectedAccount.css @@ -0,0 +1,93 @@ +.connected-account { + border-radius: 8px; + display: flex; + flex-direction: column; + min-width: 550px; + + main { + flex: 1; + } + + > footer { + ul { + display: flex; + list-style-type: none; + margin-top: 16px; + border: 1px solid #96969633; + background-color: #14141499; + height: 24px; + border-radius: 6px; + + li { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + font-family: Inter; + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-align: left; + color: #969696; + + &[aria-selected] { + background: #2f2f2f; + color: white; + } + } + } + + button { + background-color: #161616; + border: 1px solid #96969633; + height: 24px; + width: 24px; + border-radius: 6px; + display: inline-flex; + justify-content: center; + align-items: center; + + svg { + position: relative; + left: -2px; + } + } + + > div { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 16px; + } + + h6 { + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #969696; + } + + var { + font-family: Inter; + font-size: 18px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.015em; + text-align: left; + color: white; + font-style: normal; + } + + small { + font-family: Inter; + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-align: left; + color: #7d7d7d; + } + } +} diff --git a/src/components/ConnectedAccount/ConnectedAccount.tsx b/src/components/ConnectedAccount/ConnectedAccount.tsx new file mode 100644 index 0000000..37ecf33 --- /dev/null +++ b/src/components/ConnectedAccount/ConnectedAccount.tsx @@ -0,0 +1,38 @@ +import "./ConnectedAccount.css"; +import { WalletCard } from "./WalletCard"; +import { ProgressCircle } from "./ProgressCircle"; +import ArrowRightIcon from "../../assets/icons/arrow-right.svg?react"; + +export function ConnectedAccount() { + return ( +
+
+ +
+
+
    +
  • Daily
  • +
  • Weekly
  • +
  • Monthly
  • +
+
+
+ +
+
Income
+ $15.00 / week +
+ +
+
Spend
+ $5.00 / week +
+
+ +
+
+
+ ); +} diff --git a/src/components/ConnectedAccount/ProgressCircle.css b/src/components/ConnectedAccount/ProgressCircle.css new file mode 100644 index 0000000..6f73ad1 --- /dev/null +++ b/src/components/ConnectedAccount/ProgressCircle.css @@ -0,0 +1,23 @@ +.progress-circle { + border-radius: 50%; + width: 48px; + height: 48px; + display: flex; + background: #2f2f2f; + + background-image: + -webkit-linear-gradient(90deg, #2f2f2f 50%, transparent 50%), + -webkit-linear-gradient(270deg, var(--codex-color-primary) 50%, #2f2f2f 50%); + background-image: linear-gradient(90deg, #2f2f2f 50%, transparent 50%), + linear-gradient(270deg, var(--codex-color-primary) 50%, #2f2f2f 50%); + + div { + border-radius: 50%; + width: 36px; + height: 36px; + margin: auto; + background: #232323; + text-align: center; + box-sizing: border-box; + } +} diff --git a/src/components/ConnectedAccount/ProgressCircle.tsx b/src/components/ConnectedAccount/ProgressCircle.tsx new file mode 100644 index 0000000..1137d1b --- /dev/null +++ b/src/components/ConnectedAccount/ProgressCircle.tsx @@ -0,0 +1,13 @@ +import "./ProgressCircle.css"; + +type Props = { + value: number; +}; + +export function ProgressCircle(_: Props) { + return ( +
+
+
+ ); +} diff --git a/src/components/ConnectedAccount/WalletCard.css b/src/components/ConnectedAccount/WalletCard.css new file mode 100644 index 0000000..4971622 --- /dev/null +++ b/src/components/ConnectedAccount/WalletCard.css @@ -0,0 +1,190 @@ +.wallet-card { + background-image: -webkit-image-set( + url(/img/wallet.webp), + url(/img/wallet.png) + ); + background-image: image-set( + url(/img/wallet.webp) type("image/webp"), + url(/img/wallet.png) type("image/png") + ); + + background-repeat: no-repeat; + background-size: cover; + border: 1px solid #96969633; + border-radius: 16px; + padding: 16px; + box-sizing: border-box; + position: relative; + display: flex; + flex-direction: column; + + &::before { + content: " "; + width: 42%; + height: 50%; + position: absolute; + bottom: -1px; + right: 0; + background: transparent; + backdrop-filter: blur(3px); + } + + header { + button { + background-color: #161616; + border: 1px solid #96969633; + height: 24px; + width: 24px; + cursor: pointer; + transition: box-shadow 0.35s; + display: inline-flex; + justify-content: center; + align-items: center; + + &:hover { + box-shadow: 0 0 0 3px var(--codex-border-color); + } + + &:first-child { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; + } + + &:nth-child(2) { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; + } + + svg { + position: relative; + left: -2px; + } + } + } + + h6 { + font-family: Inter; + font-size: 12px; + font-weight: 700; + line-height: 14.52px; + letter-spacing: 0.01em; + text-align: left; + text-transform: uppercase; + } + + span { + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #ffffffb2; + } + + var { + font-family: Inter; + font-weight: 500; + line-height: 40px; + letter-spacing: -0.005em; + color: var(--text-strong-950, #ffffff); + display: block; + font-style: normal; + } + + main var { + font-size: 32px; + } + + svg + svg { + position: absolute; + left: 0; + top: 10px; + } + + footer { + display: flex; + justify-content: space-between; + align-items: center; + position: relative; + + &::after { + content: " "; + background-image: url(/icons/select-arrow.svg); + background-repeat: no-repeat; + position: absolute; + width: 16px; + height: 16px; + right: 0; + top: 22px; + } + + var { + font-size: 20px; + line-height: 25px; + } + + select { + background-color: #161616; + border-radius: 8px; + border: 1px solid #96969633; + padding: 6px 6px 6px 44px; + outline: none; + -moz-appearance: none; /* Firefox */ + -webkit-appearance: none; /* Safari and Chrome */ + appearance: none; + position: relative; + transition: box-shadow 0.35s; + + &:hover { + box-shadow: 0 0 0 3px var(--codex-border-color); + } + + &:has(option[value="US"]:checked) { + background-image: url(/icons/us-flag.svg); + background-position: 10px; + background-repeat: no-repeat; + background-size: 16px; + } + + option { + border-radius: 32px; + } + } + + div { + position: relative; + + .row { + gap: 8px; + } + } + + small { + color: #3ee089; + height: 20px; + width: 42px; + border-radius: 16px; + background-color: #1fc16b29; + display: flex; + align-items: center; + justify-content: center; + } + } + + section:first-child { + margin-top: 12px; + } + + section:nth-child(2) { + margin-top: 16px; + margin-bottom: 10px; + position: relative; + + .wallet-lines { + position: absolute; + left: 0; + top: 10px; + } + } +} diff --git a/src/components/ConnectedAccount/WalletCard.tsx b/src/components/ConnectedAccount/WalletCard.tsx new file mode 100644 index 0000000..5f6ec8c --- /dev/null +++ b/src/components/ConnectedAccount/WalletCard.tsx @@ -0,0 +1,48 @@ +import "./WalletCard.css"; +import ArrowRightIcon from "../../assets/icons/arrow-right.svg?react"; +import ArrowLeftIcon from "../../assets/icons/arrow-left.svg?react"; +import WalletChart from "../../assets/icons/chart.svg?react"; +import WalletLines from "../../assets/icons/lines.svg?react"; + +export function WalletCard() { + return ( +
+
+
Wallet
+
+ + +
+
+ +
+
+ TOKEN + 123,223 CDX +
+ +
+ + +
+
+ +
+
+ TOKEN +
+ $1,540 USD + + 5% +
+
+ +
+
+ ); +} diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.css b/src/components/CustomStateCellRender/CustomStateCellRender.css deleted file mode 100644 index 596fb39..0000000 --- a/src/components/CustomStateCellRender/CustomStateCellRender.css +++ /dev/null @@ -1,34 +0,0 @@ -.cell-state--custom { - display: inline-flex; -} - -.cell-stateIcon { - position: relative; - top: 2px; - margin-right: 0.5rem; -} - -.cell-state { - border-radius: var(--codex-border-radius); - padding: 0.5rem; -} - -.cell-state--error { - background-color: rgba(var(--codex-color-error), 0.2); - color: rgb(var(--codex-color-error)); -} - -.cell-state--success { - background-color: rgba(var(--codex-color-success), 0.2); - color: rgb(var(--codex-color-success)); -} - -.cell-state--warning { - background-color: rgba(var(--codex-color-warning), 0.2); - color: rgb(var(--codex-color-warning)); -} - -.cell-state--loading { - background-color: rgba(var(--codex-color-grey), 0.2); - color: rgb(var(--codex-color-grey)); -} diff --git a/src/components/CustomStateCellRender/CustomStateCellRender.tsx b/src/components/CustomStateCellRender/CustomStateCellRender.tsx index 0add909..b7a095e 100644 --- a/src/components/CustomStateCellRender/CustomStateCellRender.tsx +++ b/src/components/CustomStateCellRender/CustomStateCellRender.tsx @@ -1,20 +1,21 @@ -import { CheckCircle, CircleDashed, ShieldAlert } from "lucide-react"; -import "./CustomStateCellRender.css"; -import { Cell, Tooltip } from "@codex-storage/marketplace-ui-components"; +import { Cell } from "@codex-storage/marketplace-ui-components"; +import PurchaseStateIcon from "../../assets/icons/purchases-state-pending.svg?react"; +import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react"; +import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react"; type Props = { state: string; message: string | undefined; }; -export const CustomStateCellRender = ({ state, message }: Props) => { +export const CustomStateCellRender = ({ state }: Props) => { const icons = { - pending: CircleDashed, - submitted: CircleDashed, - started: CircleDashed, - finished: CheckCircle, - cancelled: ShieldAlert, - errored: ShieldAlert, + pending: PurchaseStateIcon, + submitted: PurchaseStateIcon, + started: PurchaseStateIcon, + finished: SuccessCircleIcon, + cancelled: ErrorCircleIcon, + errored: ErrorCircleIcon, }; const states = { @@ -26,24 +27,19 @@ export const CustomStateCellRender = ({ state, message }: Props) => { finished: "success", }; - const Icon = icons[state as keyof typeof icons] || CircleDashed; + const Icon = icons[state as keyof typeof icons] || PurchaseStateIcon; return ( -

- {message ? ( +

+ {/* {message ? ( - + ) : ( - - )} - - {state} + + )} */} +

); diff --git a/src/components/Debug/Debug.tsx b/src/components/Debug/Debug.tsx index 339724b..9036a8b 100644 --- a/src/components/Debug/Debug.tsx +++ b/src/components/Debug/Debug.tsx @@ -1,32 +1,10 @@ -import { useQuery } from "@tanstack/react-query"; -import { CodexSdk } from "../../sdk/codex"; -import { Promises } from "../../utils/promises"; import { Spinner } from "@codex-storage/marketplace-ui-components"; +import { useDebug } from "../../hooks/useDebug"; + +const throwOnError = true; export function Debug() { - const { data, isPending } = useQuery({ - queryFn: () => - CodexSdk.debug() - .info() - .then((s) => Promises.rejectOnError(s)), - - queryKey: ["debug"], - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, - - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, - - // Throw the error to the error boundary - throwOnError: true, - }); + const { data, isPending } = useDebug(throwOnError); if (isPending) { return ( diff --git a/src/components/Download/Download.css b/src/components/Download/Download.css index 7209c51..f3fed7b 100644 --- a/src/components/Download/Download.css +++ b/src/components/Download/Download.css @@ -1,13 +1,13 @@ .download { - display: flex; - align-items: center; - gap: 0.5rem; -} + .input { + flex: 1; -.download-inputContainer { - flex: 1; -} + input { + width: 100%; + } + } -.download-input { - width: 100%; + .button { + width: 105px; + } } diff --git a/src/components/Download/Download.tsx b/src/components/Download/Download.tsx index e6b19af..26f1675 100644 --- a/src/components/Download/Download.tsx +++ b/src/components/Download/Download.tsx @@ -14,15 +14,15 @@ export function Download() { setCid(e.currentTarget.value); return ( -
-
- -
- -
+
+ + +
); } diff --git a/src/components/ErrorIcon/ErrorIcon.tsx b/src/components/ErrorIcon/ErrorIcon.tsx index 8982be3..1f41214 100644 --- a/src/components/ErrorIcon/ErrorIcon.tsx +++ b/src/components/ErrorIcon/ErrorIcon.tsx @@ -1,10 +1,9 @@ import { CircleX } from "lucide-react"; -import { SimpleText } from "@codex-storage/marketplace-ui-components"; export function ErrorIcon() { return ( - + - + ); } diff --git a/src/components/FileCellRender/FileCell.tsx b/src/components/FileCellRender/FileCell.tsx index 814507a..9095046 100644 --- a/src/components/FileCellRender/FileCell.tsx +++ b/src/components/FileCellRender/FileCell.tsx @@ -5,43 +5,63 @@ import { WebFileIcon, } from "@codex-storage/marketplace-ui-components"; import "./FileCell.css"; -import { FileMetadata, FilesStorage } from "../../utils/file-storage"; -import { PurchaseStorage } from "../../utils/purchases-storage"; +import { WebStorage } from "../../utils/web-storage"; +import { CodexDataContent } from "@codex-storage/sdk-js"; + +type FileMetadata = { + mimetype: string | null; + uploadedAt: number; + filename: string | null; +}; type Props = { requestId: string; purchaseCid: string; index: number; + data: CodexDataContent[]; + onMetadata?: (requestId: string, metadata: FileMetadata) => void; }; -export function FileCell({ requestId, purchaseCid }: Props) { +export function FileCell({ requestId, purchaseCid, data, onMetadata }: Props) { const [cid, setCid] = useState(purchaseCid); const [metadata, setMetadata] = useState({ - name: "N/A.jpg", - mimetype: "N/A", - uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(), + filename: "-", + mimetype: "-", + uploadedAt: 0, }); useEffect(() => { - PurchaseStorage.get(requestId).then((cid) => { + WebStorage.purchases.get(requestId).then((cid) => { if (cid) { setCid(cid); - FilesStorage.get(cid).then((data) => { - if (data) { - setMetadata(data); - } - }); + const content = data.find((m) => m.cid === cid); + if (content) { + const { + filename = "-", + mimetype = "application/octet-stream", + uploadedAt = 0, + } = content.manifest; + setMetadata({ + filename, + mimetype, + uploadedAt, + }); + onMetadata?.(requestId, { + filename, + mimetype, + uploadedAt, + }); + } } }); - }, [requestId]); + }, [requestId, data, onMetadata]); - let name = metadata.name.slice(0, 10); + let filename = metadata.filename || "-"; - if (metadata.name.length > 10) { - // const [filename, ext] = metadata.name.split("."); - // name = filename.slice(0, 10) + "..." + ext; - name += "..."; + if (filename.length > 10) { + const [name, ext] = filename.split("."); + filename = name.slice(0, 10) + "..." + ext; } // const cidTruncated = cid.slice(0, 5) + ".".repeat(5) + cid.slice(-5); @@ -50,10 +70,10 @@ export function FileCell({ requestId, purchaseCid }: Props) { return (
- +
- {name} + {filename} diff --git a/src/components/Files/CidCopyButton.tsx b/src/components/Files/CidCopyButton.tsx index 12d61ab..213d299 100644 --- a/src/components/Files/CidCopyButton.tsx +++ b/src/components/Files/CidCopyButton.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; -import { COPY_DURATION, ICON_SIZE } from "../../utils/constants"; -import { Copy } from "lucide-react"; +import { COPY_DURATION } from "../../utils/constants"; import { Button } from "@codex-storage/marketplace-ui-components"; +import CopyIcon from "../../assets/icons/copy.svg?react"; type CopyButtonProps = { cid: string; @@ -27,13 +27,11 @@ export function CidCopyButton({ cid }: CopyButtonProps) { const label = copied ? "Copied !" : "Copy CID"; - const Icon = () => ; - return ( + Icon={() => }> ); } diff --git a/src/components/Files/FileActions.css b/src/components/Files/FileActions.css new file mode 100644 index 0000000..242ffac --- /dev/null +++ b/src/components/Files/FileActions.css @@ -0,0 +1,19 @@ +.file-actions { + > div { + display: inline-flex; + align-items: center; + border: 1px solid var(--codex-border-color); + border-radius: var(--codex-border-radius); + padding: 0.5rem; + background-color: #14141499; + gap: 8px; + padding: 10px; + + .button-icon { + width: 40px; + height: 40px; + background-color: #2f2f2f; + border: 1px solid #96969633; + } + } +} diff --git a/src/components/Files/FileActions.tsx b/src/components/Files/FileActions.tsx new file mode 100644 index 0000000..18151dd --- /dev/null +++ b/src/components/Files/FileActions.tsx @@ -0,0 +1,47 @@ +import { ButtonIcon, Cell } from "@codex-storage/marketplace-ui-components"; +import { FolderButton } from "./FolderButton"; +import { CodexDataContent } from "@codex-storage/sdk-js"; +import { CodexSdk } from "../../sdk/codex"; +import "./FileActions.css"; +import DownloadIcon from "../../assets/icons/download-file.svg?react"; +import InfoFileIcon from "../../assets/icons/info-file.svg?react"; + +type Props = { + content: CodexDataContent; + folders: [string, string[]][]; + onFolderToggle: (cid: string, folder: string) => void; + onDetails: (cid: string) => void; +}; + +export function FileActions({ + content, + folders, + onFolderToggle, + onDetails, +}: Props) { + const url = CodexSdk.url() + "/api/codex/v1/data/"; + + return ( + +
+ window.open(url + content.cid, "_blank")} + Icon={DownloadIcon}> + + [ + folder, + files.includes(content.cid), + ])} + onFolderToggle={(folder) => onFolderToggle(content.cid, folder)} + /> + + onDetails(content.cid)} + Icon={InfoFileIcon}> +
+
+ ); +} diff --git a/src/components/Files/FileCell.css b/src/components/Files/FileCell.css new file mode 100644 index 0000000..b0a4d65 --- /dev/null +++ b/src/components/Files/FileCell.css @@ -0,0 +1,22 @@ +.file-cell { + small { + word-break: break-all; + } + + > div { + display: flex; + gap: 12px; + align-items: center; + + > div { + flex: 1; + } + + .button-icon { + width: 40px; + height: 40px; + background-color: #14141499; + border: 1px solid #96969633; + } + } +} diff --git a/src/components/Files/FileCell.tsx b/src/components/Files/FileCell.tsx new file mode 100644 index 0000000..987ec94 --- /dev/null +++ b/src/components/Files/FileCell.tsx @@ -0,0 +1,51 @@ +import { + ButtonIcon, + Cell, + Toast, + WebFileIcon, +} from "@codex-storage/marketplace-ui-components"; +import { CodexDataContent } from "@codex-storage/sdk-js"; +import { useState } from "react"; +import "./FileCell.css"; +import CopyIcon from "../../assets/icons/copy.svg?react"; + +type Props = { + content: CodexDataContent; +}; + +export function FileCell({ content }: Props) { + const [toast, setToast] = useState({ time: 0, message: "" }); + + const onCopy = (cid: string) => { + navigator.clipboard.writeText(cid); + setToast({ message: "CID copied to the clipboard.", time: Date.now() }); + }; + + return ( + <> + +
+ + +
+

+ {content.manifest.filename} +

+

+ {content.cid} +

+
+ onCopy(content.cid)} + animation="buzz" + Icon={(props) => ( + + )}> +
+ + +
+ + ); +} diff --git a/src/components/Files/FileDetails.css b/src/components/Files/FileDetails.css index a38bb60..790551b 100644 --- a/src/components/Files/FileDetails.css +++ b/src/components/Files/FileDetails.css @@ -1,33 +1,155 @@ -.fileDetails-header { - padding: 0.75rem 1.5rem; - border-bottom: 1px solid var(--codex-border-color); - display: flex; - align-items: center; -} +.file-details { + background-color: #232323; + border-left: 1px solid #96969633; + border-top-left-radius: 16px; + border-bottom-left-radius: 16px; + padding: 16px; + height: 100%; -.fileDetails-headerTitle { - flex-grow: 1; -} + header { + display: flex; + align-items: center; + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; -.fileDetails-body { - padding: 0; -} + span { + flex-grow: 1; + } -.fileDetails-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - display: grid; - padding: 0.75rem 1.5rem; - border-bottom: 1px solid var(--codex-border-color); -} + .button-icon { + background-color: #2f2f2f; + border: 1px solid #96969633; -.fileDetails-gridColumn { - grid-column: span 2 / span 2; - color: var(--codex-text-contrast); -} + svg { + position: relative; + left: -3px; + top: -1px; + } + } + } -.fileDetails-actions { - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 0.75rem 1.5rem; + .preview { + background-color: #14141499; + border: 1px solid #69696933; + height: 150px; + margin: auto; + border-radius: 10px; + margin-bottom: 16px; + + img { + max-width: 100%; + max-height: 100%; + margin: auto; + display: flex; + } + + figure { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; + margin: 0; + + font-family: Inter; + font-size: 16px; + font-weight: 400; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + color: #ffffff33; + + p { + margin-top: 8px; + } + } + } + + ul { + li { + grid-template-columns: repeat(4, minmax(0, 1fr)); + display: grid; + padding: 8px 0; + align-items: center; + + p:first-child { + font-family: Inter; + font-size: 14px; + font-weight: 700; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + } + + p:nth-child(2) { + grid-column: span 2 / span 2; + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #ffffffcc; + } + + &:last-child p:nth-child(2) { + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + + color: #6beca1; + } + } + } + + .buttons { + padding: 16px 0; + border-bottom: 1px solid #96969633; + display: flex; + gap: 16px; + + button { + flex: 1; + } + } + + .purchases { + padding-bottom: 16px; + + header { + margin-top: 16px; + display: block; + border-bottom: 1px solid #96969633; + padding-bottom: 16px; + + > span { + display: flex; + gap: 16px; + } + } + + thead tr th { + border-top: 1px solid #96969633; + border-bottom: 1px solid #96969633; + + &:first-child { + border-left: 1px solid #96969633; + } + + &:last-child { + border-right: 1px solid #96969633; + } + } + + .truncateCell { + width: 100px; + } + } } diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx index 98e1915..4ad1817 100644 --- a/src/components/Files/FileDetails.tsx +++ b/src/components/Files/FileDetails.tsx @@ -2,89 +2,157 @@ import { ButtonIcon, Button, Sheets, + WebFileIcon, } from "@codex-storage/marketplace-ui-components"; -import { CodexDataContent } from "@codex-storage/sdk-js"; +import { CodexDataContent, CodexPurchase } from "@codex-storage/sdk-js"; import { PrettyBytes } from "../../utils/bytes"; -import { ICON_SIZE } from "../../utils/constants"; import { Dates } from "../../utils/dates"; import { CidCopyButton } from "./CidCopyButton"; import "./FileDetails.css"; -import { DownloadIcon, X } from "lucide-react"; +import { CodexSdk } from "../../sdk/codex"; +import { FilesUtils } from "./files.utils"; +import DownloadIcon from "../../assets/icons/download-file.svg?react"; +import FileDetailsIcon from "../../assets/icons/file-details.svg?react"; +import CloseIcon from "../../assets/icons/close.svg?react"; +import { useQuery } from "@tanstack/react-query"; +import { Promises } from "../../utils/promises"; +import { PurchaseHistory } from "./PurchaseHistory"; +import { WebStorage } from "../../utils/web-storage"; type Props = { - details: CodexDataContent | undefined; + details: CodexDataContent | null; onClose: () => void; - expanded: boolean; }; -export function FileDetails({ onClose, details, expanded }: Props) { - const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/"; +export function FileDetails({ onClose, details }: Props) { + const { data: purchases = [] } = useQuery({ + queryFn: () => + CodexSdk.marketplace() + .purchases() + .then(async (res) => { + if (res.error) { + return res; + } - const Icon = () => ; + const all: CodexPurchase[] = []; + for (const p of res.data) { + const cid = await WebStorage.purchases.get(p.requestId); + if (cid == details?.cid) { + all.push(p); + } + } + + return { + error: false as any, + data: all, + }; + }) + .then((s) => Promises.rejectOnError(s)), + queryKey: ["purchases"], + + enabled: !!details, + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + initialData: [], + + // Throw the error to the error boundary + throwOnError: true, + }); + + const url = CodexSdk.url() + "/api/codex/v1/data/"; const onDownload = () => window.open(url + details?.cid, "_target"); return ( - + <> {details && ( - <> -
- File details - +
+
+ + File details + +
+ +
+ {FilesUtils.isImage(details.manifest.mimetype) ? ( + + ) : ( +
+ +

File Preview not available.

+
+ )}
-
-
-

CID:

-

{details.cid}

-
+
    +
  • +

    CID:

    +

    {details.cid}

    +
  • -
    -

    File name:

    -

    - {details.manifest.filename} +

  • +

    File name:

    +

    {details.manifest.filename}

    +
  • + +
  • +

    Date:

    +

    {Dates.format(details.manifest.uploadedAt).toString()}

    +
  • + +
  • +

    Mimetype:

    +

    {details.manifest.mimetype}

    +
  • + +
  • +

    Size:

    +

    {PrettyBytes(details.manifest.datasetSize)}

    +
  • + +
  • +

    Protected:

    +

    {details.manifest.protected ? "Yes" : "No"}

    +
  • + +
  • +

    Used:

    +

    + {purchases.length} purchase(s)

    -
  • + +
-
-

Date:

-

- {Dates.format(details.manifest.uploadedAt).toString()} -

-
+
+ -
-

Mimetype:

-

- {details.manifest.mimetype} -

-
- -
-

Size:

-

- {PrettyBytes(details.manifest.datasetSize)} -

-
- -
-

Protected:

-

- {details.manifest.protected ? "Yes" : "No"} -

-
- -
- - - -
+
- + + +
)} diff --git a/src/components/Files/FileFilters.css b/src/components/Files/FileFilters.css new file mode 100644 index 0000000..7606270 --- /dev/null +++ b/src/components/Files/FileFilters.css @@ -0,0 +1,35 @@ +.filter { + padding: 4px 8px; + gap: 8px; + border-radius: 6px; + border: 1px solid #96969633; + background-color: #2f2f2f; + font-family: Inter; + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-align: left; + color: #969696; + text-transform: capitalize; + display: inline-flex; + align-items: center; + cursor: pointer; + transition: box-shadow 0.35s; + + &:hover { + box-shadow: 0 0 0 3px var(--codex-border-color); + } + + svg { + color: #969696; + } + + &.filter--active { + border-color: var(--codex-color-primary); + color: var(--codex-color-primary); + + svg { + color: var(--codex-color-primary); + } + } +} diff --git a/src/components/Files/FileFilters.tsx b/src/components/Files/FileFilters.tsx new file mode 100644 index 0000000..963786a --- /dev/null +++ b/src/components/Files/FileFilters.tsx @@ -0,0 +1,85 @@ +import { CodexDataContent } from "@codex-storage/sdk-js"; +import { FilesUtils } from "./files.utils"; +import { classnames } from "../../utils/classnames"; +import "./FileFilters.css"; +import ImageIcon from "../../assets/icons/image.svg?react"; +import VideoIcon from "../../assets/icons/video.svg?react"; +import ArchiveIcon from "../../assets/icons/archive.svg?react"; +import DocumentIcon from "../../assets/icons/document.svg?react"; + +type Props = { + files: CodexDataContent[]; + onFilterToggle: (filter: string) => void; + selected: string[]; +}; + +function getIcon(type: string) { + switch (type) { + case "image": { + return ; + } + + case "archive": { + return ; + } + + case "video": { + return ; + } + + default: { + return ; + } + } +} + +function getType(mimetype: string) { + if (FilesUtils.isArchive(mimetype)) { + return "archive"; + } + + if (FilesUtils.isImage(mimetype)) { + return "image"; + } + + if (FilesUtils.isVideo(mimetype)) { + return "video"; + } + + return "document"; +} + +export function FilterFilters({ selected, files, onFilterToggle }: Props) { + const filters = Array.from( + new Set( + files + .filter((f) => f.manifest.mimetype !== "") + .map((file) => getType(file.manifest.mimetype || "")) + ) + ); + + const html = filters.map((type) => { + const count = files.reduce( + (acc, file) => + getType(file.manifest.mimetype || "") === type ? acc + 1 : acc, + 0 + ); + + return ( + onFilterToggle(type)}> + {getIcon(type)} + + {type + "s"} ({count}) + + + ); + }); + + return
{html}
; +} diff --git a/src/components/Files/Files.css b/src/components/Files/Files.css index e5427f7..a8bd06a 100644 --- a/src/components/Files/Files.css +++ b/src/components/Files/Files.css @@ -1,72 +1,27 @@ .files { - border-radius: var(--codex-border-radius); - border: 1px solid var(--codex-border-color); - background-color: var(--codex-background-secondary); - padding: 1rem 1.5rem; - margin-bottom: 1rem; -} + section { + display: inline-flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: 16px; -.files-title { - font-weight: bold; - font-size: 1.125rem; - line-height: 1.75rem; -} + .tabs { + flex-basis: 75%; + } + } -.files-file:not(:last-child) { - padding-bottom: 0.75rem; -} + .filters { + margin-top: 16px; + gap: 16px; + display: flex; + } -.files-fileContent { - display: flex; - gap: 0.75rem; -} + .button { + width: 105px; + } -.files-file:not(:last-child) .files-fileContent { - border-bottom: 1px solid var(--codex-border-color); -} - -.files-file:not(:last-child) .files-fileContent { - padding-bottom: 0.75rem; -} - -.files-fileIcon { - padding: 0.5rem; - border: 1px solid var(--codex-border-color); - border-radius: var(--codex-border-radius); - display: flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; -} - -.files-fileData { - display: flex; - justify-content: space-between; - align-items: center; - flex-grow: 1; -} - -.files-fileActions { - display: flex; - align-items: center; - border: 1px solid var(--codex-border-color); - border-radius: var(--codex-border-radius); - padding: 0.5rem; - gap: 0.75rem; -} - -.files-fileStar { - transition: - fill 0.35s, - stroke 0.35s; -} - -.files-fileFavorite { - fill: yellow; - stroke: yellow; -} - -.files-header { - margin-bottom: 0.75rem; + table thead tr th { + background-color: #14141499; + } } diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 65d4d34..fe25afd 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -1,154 +1,226 @@ -import { Download, FilesIcon, ReceiptText, Star } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; +import { ChangeEvent, useEffect, useState } from "react"; import { PrettyBytes } from "../../utils/bytes"; import { Dates } from "../../utils/dates"; import "./Files.css"; -import { ICON_SIZE, SIDE_DURATION } from "../../utils/constants"; import { - ButtonIcon, - EmptyPlaceholder, - WebFileIcon, Tabs, + Input, + Button, + TabProps, + Table, + Row, + Cell, + TabSortState, } from "@codex-storage/marketplace-ui-components"; import { FileDetails } from "./FileDetails.tsx"; -import { FavoriteStorage } from "../../utils/favorite-storage.tsx"; import { useData } from "../../hooks/useData.tsx"; +import { WebStorage } from "../../utils/web-storage.ts"; +import { classnames } from "../../utils/classnames.ts"; +import { CodexDataContent } from "@codex-storage/sdk-js"; +import { FilesUtils } from "./files.utils.ts"; +import { FilterFilters } from "./FileFilters.tsx"; +import { FileCell } from "./FileCell.tsx"; +import { FileActions } from "./FileActions.tsx"; +import PlusIcon from "../../assets/icons/plus.svg?react"; +import AllFilesIcon from "../../assets/icons/all.svg?react"; +import FavoriteIcon from "../../assets/icons/favorite.svg?react"; -type StarIconProps = { - isFavorite: boolean; +type SortFn = (a: CodexDataContent, b: CodexDataContent) => number; + +type Props = { + limit?: number; }; -function StarIcon({ isFavorite }: StarIconProps) { - if (isFavorite) { - return ( - - ); - } - - return ; -} - -export function Files() { +export function Files({ limit }: Props) { const files = useData(); - const cid = useRef(""); - const [expanded, setExpanded] = useState(false); - const [favorites, setFavorites] = useState([]); const [index, setIndex] = useState(0); + const [folder, setFolder] = useState(""); + const [folders, setFolders] = useState<[string, string[]][]>([]); + const [error, setError] = useState(""); + const [details, setDetails] = useState(null); + const [sortFn, setSortFn] = useState(() => + FilesUtils.sortByDate("desc") + ); + const [selectedFilters, setSelectedFilters] = useState([]); useEffect(() => { - FavoriteStorage.list().then((cids) => setFavorites(cids)); + WebStorage.folders.list().then((items) => setFolders(items)); }, []); - const onClose = () => { - setExpanded(false); + const onClose = () => setDetails(null); - setTimeout(() => { - cid.current = ""; - }, SIDE_DURATION); - }; + const onTabChange = async (i: number) => setIndex(i); - const onTabChange = (i: number) => setIndex(i); + const onDetails = (cid: string) => { + const d = files.find((file) => file.cid === cid); - const onDetails = (id: string) => { - cid.current = id; - setExpanded(true); - }; - - const onToggleFavorite = (cid: string) => { - if (favorites.includes(cid)) { - FavoriteStorage.delete(cid); - setFavorites(favorites.filter((c) => c !== cid)); - } else { - FavoriteStorage.add(cid); - setFavorites([...favorites, cid]); + if (d) { + setDetails(d); } }; - const items = []; + const onFolderChange = (e: ChangeEvent) => { + const val = e.currentTarget.value; + setFolder(val); + setError(""); - if (index === 1) { - items.push(...files.filter((f) => favorites.includes(f.cid))); - } else { - items.push(...files); + if (!val) { + return; + } + + if (e.currentTarget.checkValidity()) { + if (folders.length >= 5) { + setError("5 folders limit reached"); + return; + } + + if (FilesUtils.exists(folders, val)) { + setError("This folder already exists"); + return; + } + } else { + setError("9 alpha characters maximum"); + } + }; + + const onFolderCreate = () => { + WebStorage.folders.create(folder); + + setFolder(""); + setFolders([...folders, [folder, []]]); + }; + + // const onFolderDelete = (val: string) => { + // WebStorage.folders.delete(val); + + // const currentIndex = folders.findIndex(([name]) => name === val); + + // if (currentIndex + 1 == index) { + // setIndex(index - 1); + // } + + // setFolders(folders.filter(([name]) => name !== val)); + // }; + + const onFolderToggle = (cid: string, folder: string) => { + const current = folders.find(([name]) => name === folder); + + if (!current) { + return; + } + + const [, files] = current; + + if (files.includes(cid)) { + WebStorage.folders.deleteFile(folder, cid); + setFolders(FilesUtils.removeCidFromFolder(folders, folder, cid)); + } else { + WebStorage.folders.addFile(folder, cid); + setFolders(FilesUtils.addCidToFolder(folders, folder, cid)); + } + }; + + const tabs: TabProps[] = folders.map(([folder], index) => ({ + label: folder, + Icon: () => (index === 0 ? : null), + // IconAfter: + // folder === "Favorites" + // ? undefined + // : () => ( + // { + // e.preventDefault(); + // e.stopPropagation(); + + // onFolderDelete(folder); + // }}> + // ), + })); + + const onSortByFilename = (state: TabSortState) => + setSortFn(() => FilesUtils.sortByName(state)); + + const onSortBySize = (state: TabSortState) => + setSortFn(() => FilesUtils.sortBySize(state)); + + const onSortByDate = (state: TabSortState) => + setSortFn(() => FilesUtils.sortByDate(state)); + + const onToggleFilter = (filter: string) => + setSelectedFilters(FilesUtils.toggleFilters(selectedFilters, filter)); + + const headers = [ + ["file", onSortByFilename], + ["size", onSortBySize], + ["date", onSortByDate], + ["actions"], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const items = FilesUtils.listInFolder(files, folders, index); + const filtered = FilesUtils.applyFilters(items, selectedFilters); + const sorted = sortFn ? [...filtered].sort(sortFn) : filtered; + let rows = + sorted.map((c) => ( + , + {PrettyBytes(c.manifest.datasetSize)}, + {Dates.format(c.manifest.uploadedAt).toString()}, + , + ]}> + )) || []; + + if (limit) { + rows = rows.slice(0, limit); } - const details = items.find((c) => c.cid === cid.current); - - const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/"; + tabs.unshift({ + label: "All", + Icon: () => , + }); return ( -
-
-
Files
- , - }, - { - label: "Favorites", - Icon: () => , - }, - ]}> -
+
+
+ +
+ -
- {items.length ? ( - items.map((c) => ( -
-
-
- -
-
-
- {c.manifest.filename} -
- - {PrettyBytes(c.manifest.datasetSize)} -{" "} - {Dates.format(c.manifest.uploadedAt).toString()} - ... - {c.cid.slice(-5)} - -
-
-
- window.open(url + c.cid, "_blank")} - Icon={() => }> + +
+
- onToggleFavorite(c.cid)} - Icon={() => ( - - )}> + - onDetails(c.cid)} - Icon={() => ( - - )}> -
-
-
-
- )) - ) : ( -
- -
- )} -
+
- - + + ); } diff --git a/src/components/Files/FolderButton.css b/src/components/Files/FolderButton.css new file mode 100644 index 0000000..e26bf49 --- /dev/null +++ b/src/components/Files/FolderButton.css @@ -0,0 +1,41 @@ +.folder-button { + > div { + position: absolute; + transform: translate(-110px, -75px); + opacity: 0; + transition: + transform 0.25s, + opacity 0.15s; + background-color: var(--codex-background); + padding: 0.5rem; + border-radius: var(--codex-border-radius); + width: 150px; + right: -40px; + border: 1px solid var(--codex-border-color); + z-index: -1; + + &[aria-expanded] { + z-index: 12; + transform: translate(-110px, -200px); + opacity: 1; + } + + > div { + padding: 0.75rem; + transition: background-color 0.35s; + cursor: pointer; + border-radius: var(--codex-border-radius); + display: flex; + align-items: center; + justify-content: space-between; + + &:hover { + background-color: var(--codex-background-light); + } + } + + svg { + color: var(--codex-color-primary); + } + } +} diff --git a/src/components/Files/FolderButton.tsx b/src/components/Files/FolderButton.tsx new file mode 100644 index 0000000..dbbc2aa --- /dev/null +++ b/src/components/Files/FolderButton.tsx @@ -0,0 +1,53 @@ +import { Backdrop, ButtonIcon } from "@codex-storage/marketplace-ui-components"; +import { CheckCircle } from "lucide-react"; +import "./FolderButton.css"; +import { useState } from "react"; +import { attributes } from "../../utils/attributes"; +import { classnames } from "../../utils/classnames"; +import FolderIcon from "../../assets/icons/folder.svg?react"; + +type Props = { + folders: [string, boolean][]; + onFolderToggle: (folder: string) => void; +}; + +export function FolderButton({ folders, onFolderToggle }: Props) { + const [open, setOpen] = useState(false); + + const onClose = () => setOpen(false); + + const onOpen = () => setOpen(true); + + const attr = attributes({ "aria-expanded": open }); + + const doesFolderContainFile = folders.reduce( + (prev, [, isActive]) => isActive || prev, + false + ); + + return ( + <> +
+ + +
+ {folders.map(([folder, isActive]) => ( +
onFolderToggle(folder)}> + {folder} + {isActive && } +
+ ))} +
+
+ + + ); +} diff --git a/src/components/Files/PurchaseHistory.tsx b/src/components/Files/PurchaseHistory.tsx new file mode 100644 index 0000000..1c805c0 --- /dev/null +++ b/src/components/Files/PurchaseHistory.tsx @@ -0,0 +1,67 @@ +import { + Row, + Cell, + Table, + TabSortState, +} from "@codex-storage/marketplace-ui-components"; +import { Times } from "../../utils/times"; +import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender"; +import { TruncateCell } from "../TruncateCell/TruncateCell"; +import { CodexPurchase } from "@codex-storage/sdk-js"; +import PurchaseHistoryIcon from "../../assets/icons/purchase-history-outline.svg?react"; +import { useState } from "react"; +import { PurchaseUtils } from "../Purchase/purchase.utils"; + +type Props = { + purchases: CodexPurchase[]; +}; + +type SortFn = (a: CodexPurchase, b: CodexPurchase) => number; + +export function PurchaseHistory({ purchases }: Props) { + const [sortFn, setSortFn] = useState(() => + PurchaseUtils.sortById("desc") + ); + + const onSortById = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortById(state)); + + const headers = [ + ["request id", onSortById], + ["duration"], + ["expiry"], + ["status"], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const sorted = sortFn ? [...purchases].sort(sortFn) : purchases; + + const rows = sorted.map((p) => { + const duration = parseInt(p.request.ask.duration, 10); + + return ( + , + {Times.pretty(duration)}, + {p.request.expiry}, + , + ]}> + ); + }); + + if (purchases.length > 0) { + return ( +
+
+ + Purchase history + +
+ +
+ + ); + } + + return ""; +} diff --git a/src/components/Files/files.utils.test.ts b/src/components/Files/files.utils.test.ts new file mode 100644 index 0000000..355d92e --- /dev/null +++ b/src/components/Files/files.utils.test.ts @@ -0,0 +1,295 @@ +import { assert, describe, it } from "vitest"; +import { FilesUtils } from "./files.utils"; + +describe("files", () => { + it("sorts by name", async () => { + const a = { + cid: "", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + } + const b = { + cid: "", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "b", + mimetype: null, + uploadedAt: 0 + } + } + + const items = [a, b,] + + const descSorted = items.slice().sort(FilesUtils.sortByName("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(FilesUtils.sortByName("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by size", async () => { + const a = { + cid: "", manifest: { + datasetSize: 1000, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + } + const b = { + cid: "", manifest: { + datasetSize: 2000, + blockSize: 0, + protected: false, + treeCid: "", + filename: "b", + mimetype: null, + uploadedAt: 0 + } + } + + const items = [a, b,] + + const descSorted = items.slice().sort(FilesUtils.sortBySize("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by date", async () => { + const now = new Date() + + const a = { + cid: "", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: now.getTime() + } + } + + now.setDate(now.getDate() - 1) + + const b = { + cid: "", manifest: { + datasetSize: 2000, + blockSize: 0, + protected: false, + treeCid: "", + filename: "b", + mimetype: null, + uploadedAt: now.getTime() + } + } + + const items = [a, b,] + + const descSorted = items.slice().sort(FilesUtils.sortBySize("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(FilesUtils.sortBySize("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("returns true when a file is an image", async () => { + assert.deepEqual(FilesUtils.isImage("image/jpg"), true); + assert.deepEqual(FilesUtils.isImage("video/mp4"), false); + assert.deepEqual(FilesUtils.isImage(null), false); + }); + + it("returns true when a file is a video", async () => { + assert.deepEqual(FilesUtils.isVideo("video/mp4"), true); + assert.deepEqual(FilesUtils.isVideo("image/jpg"), false); + assert.deepEqual(FilesUtils.isImage(null), false); + }); + + it("returns true when a file is an archive", async () => { + assert.deepEqual(FilesUtils.isArchive("application/zip"), true); + assert.deepEqual(FilesUtils.isArchive("video/mp4"), false); + assert.deepEqual(FilesUtils.isArchive(null), false); + }); + + it("gets the type of a file", async () => { + assert.deepEqual(FilesUtils.type("application/zip"), "archive"); + }); + + it("fallbacks to document when the mimetype is not known", async () => { + assert.deepEqual(FilesUtils.type("application/octet-stream"), "document"); + }); + + it("removes a cid from a folder", async () => { + const folders = [["favorites", ["123", "456"]]] satisfies [string, string[]][] + const folder = "favorites" + const cid = "456" + + assert.deepEqual(FilesUtils.removeCidFromFolder(folders, folder, cid), [["favorites", ["123"]]]); + }); + + it("adds a cid from to a folder", async () => { + const folders = [["favorites", ["123"]]] satisfies [string, string[]][] + const folder = "favorites" + const cid = "456" + + assert.deepEqual(FilesUtils.addCidToFolder(folders, folder, cid), [["favorites", ["123", cid]]]); + }); + + it("returns true when the folder exists", async () => { + const folders = [["favorites", []]] satisfies [string, string[]][] + + assert.deepEqual(FilesUtils.exists(folders, "favorites"), true); + }); + + it("toggles filter", async () => { + const filters = FilesUtils.toggleFilters(["images"], "archives") + + assert.deepEqual(filters, ["images", "archives"]); + assert.deepEqual(FilesUtils.toggleFilters(filters, "archives"), ["images"]); + }); + + it("list all files when the first item is selected", async () => { + const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][] + const files = [ + { + cid: "123", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + }, + { + cid: "456", + manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + } + ] + + + assert.deepEqual(FilesUtils.listInFolder(files, folders, 0), files); + }); + + it("list all files in favorites", async () => { + const folders = [["favorites", ["123"]], ["hello", ["456"]]] satisfies [string, string[]][] + const files = [ + { + cid: "123", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + }, + { + cid: "456", + manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + } + ] + + + assert.deepEqual(FilesUtils.listInFolder(files, folders, 1), [files[0]]); + }); + + it("returns all files when no filter is selected", async () => { + const files = [ + { + cid: "123", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + }, + { + cid: "456", + manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: null, + uploadedAt: 0 + } + } + ] + + + assert.deepEqual(FilesUtils.applyFilters(files, []), files); + }); + + it("returns apply filter by mimetype", async () => { + const files = [ + { + cid: "123", manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: "image/jpg", + uploadedAt: 0 + } + }, + { + cid: "456", + manifest: { + datasetSize: 0, + blockSize: 0, + protected: false, + treeCid: "", + filename: "a", + mimetype: "application/zip", + uploadedAt: 0 + } + } + ] + + + assert.deepEqual(FilesUtils.applyFilters(files, ["archive"]), [files[1]]); + }); +}) \ No newline at end of file diff --git a/src/components/Files/files.utils.ts b/src/components/Files/files.utils.ts new file mode 100644 index 0000000..3cffb64 --- /dev/null +++ b/src/components/Files/files.utils.ts @@ -0,0 +1,98 @@ +import { TabSortState } from "@codex-storage/marketplace-ui-components"; +import { CodexDataContent } from "@codex-storage/sdk-js"; + +const archiveMimetypes = [ + "application/zip", + "application/x-rar-compressed", + "application/x-tar", + "application/gzip", + "application/x-7z-compressed", + "application/gzip", // for .tar.gz + "application/x-bzip2", + "application/x-xz", +]; + +export const FilesUtils = { + isImage(type: string | null) { + return !!type && type.startsWith("image"); + }, + isVideo(type: string | null) { + return !!type && type.startsWith("video"); + }, + isArchive(mimetype: string | null) { + return !!mimetype && archiveMimetypes.includes(mimetype) + }, + type(mimetype: string | null) { + if (FilesUtils.isArchive(mimetype)) { + return "archive" + } + + if (FilesUtils.isVideo(mimetype)) { + return "video" + } + + if (FilesUtils.isImage(mimetype)) { + return "image" + } + + return "document" + }, + sortByName: (state: TabSortState) => + (a: CodexDataContent, b: CodexDataContent) => { + const { manifest: { filename: afilename } } = a + const { manifest: { filename: bfilename } } = b + + return state === "desc" + ? (bfilename || "") + .toLocaleLowerCase() + .localeCompare((afilename || "").toLocaleLowerCase()) + : (afilename || "") + .toLocaleLowerCase() + .localeCompare((bfilename || "").toLocaleLowerCase()) + }, + sortBySize: (state: TabSortState) => + (a: CodexDataContent, b: CodexDataContent) => state === "desc" + ? b.manifest.datasetSize - a.manifest.datasetSize + : a.manifest.datasetSize - b.manifest.datasetSize + , + sortByDate: (state: TabSortState) => + (a: CodexDataContent, b: CodexDataContent) => state === "desc" + ? new Date(b.manifest.uploadedAt).getTime() - + new Date(a.manifest.uploadedAt).getTime() + : new Date(a.manifest.uploadedAt).getTime() - + new Date(b.manifest.uploadedAt).getTime() + , + removeCidFromFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] { + return folders.map(([name, files]) => + name === folder + ? [name, files.filter((id) => id !== cid)] + : [name, files] + ) + }, + addCidToFolder(folders: [string, string[]][], folder: string, cid: string): [string, string[]][] { + return folders.map(([name, files]) => + name === folder ? [name, [...files, cid]] : [name, files] + ) + }, + exists(folders: [string, string[]][], name: string) { + return !!folders.find(([folder]) => folder === name) + }, + toggleFilters: (filters: string[], filter: string) => filters.includes(filter) + ? filters.filter((f) => f !== filter) + : [...filters, filter], + listInFolder(files: CodexDataContent[], folders: [string, string[]][], index: number) { + return index === 0 + ? files + : files.filter((file) => folders[index - 1][1].includes(file.cid)); + }, + applyFilters(files: CodexDataContent[], filters: string[]) { + return files.filter( + (file) => filters.length === 0 || filters.includes(this.type(file.manifest.mimetype)) + ) + } +}; + +export type CodexFileMetadata = { + type: string; + name: string; +}; diff --git a/src/components/HealthChecks/HealthChecks.css b/src/components/HealthChecks/HealthChecks.css new file mode 100644 index 0000000..6ff3ceb --- /dev/null +++ b/src/components/HealthChecks/HealthChecks.css @@ -0,0 +1,124 @@ +.health-checks { + .address { + display: flex; + position: relative; + gap: 16px; + flex-direction: column; + align-items: flex-start; + + @media (min-width: 1000px) { + & { + flex-direction: row; + align-items: center; + } + } + + > div { + position: relative; + + @media (max-width: 999px) { + &:not(.refresh) { + width: 100%; + } + } + } + + svg { + position: absolute; + top: 68px; + bottom: 0; + right: 18px; + } + + .refresh { + position: relative; + cursor: pointer; + cursor: pointer; + + @media (max-width: 999px) { + & { + top: 0px; + left: 0; + transform: scale(1.5); + right: 0; + margin: auto; + } + } + + @media (min-width: 1000px) { + & { + top: 18px; + } + } + + svg { + position: initial; + } + + &.address--fetching .refresh svg { + animation: rotate 2s linear infinite; + } + } + + input[type="number"] { + width: 150px; + } + + input[type="number"]::-webkit-outer-spin-button, + input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + + input[type="number"] { + -moz-appearance: textfield; + } + + @media (max-width: 999px) { + input[type="number"], + input { + width: 100%; + } + } + } + + p { + font-family: Azeret Mono; + font-size: 12px; + font-weight: 400; + line-height: 14px; + color: #828282; + padding-left: 1.25rem; + margin-top: 1.75rem; + margin-bottom: 3rem; + } + + ul { + margin-bottom: 32px; + + li { + display: flex; + align-items: center; + padding: 16px 0; + gap: 16px; + border-top: 1px solid #96969633; + border-bottom: 1px solid #96969633; + + &:first-child { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + } + + span { + display: flex; + align-items: center; + width: 20px; + height: 20px; + justify-content: center; + } + } + } +} diff --git a/src/components/HealthChecks/HealthChecks.tsx b/src/components/HealthChecks/HealthChecks.tsx new file mode 100644 index 0000000..7efb698 --- /dev/null +++ b/src/components/HealthChecks/HealthChecks.tsx @@ -0,0 +1,205 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useEffect, useState } from "react"; +import { useDebug } from "../../hooks/useDebug"; +import { usePersistence } from "../../hooks/usePersistence"; +import { usePortForwarding } from "../../hooks/usePortForwarding"; +import { Input, Spinner } from "@codex-storage/marketplace-ui-components"; +import { classnames } from "../../utils/classnames"; +import "./HealthChecks.css"; +import { CodexSdk } from "../../sdk/codex"; +import { HealthCheckUtil } from "./health-check.utils"; +import { PortForwardingUtil } from "../../hooks/port-forwarding.util"; +import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react"; +import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react"; +import DeviceIcon from "../../assets/icons/device.svg?react"; +import RefreshIcon from "../../assets/icons/refresh.svg?react"; +import WarningIcon from "../../assets/icons/warning-circle.svg?react"; + +type Props = { + online: boolean; + onStepValid: (valid: boolean) => void; +}; + +const throwOnError = false; +const defaultPort = 8070; + +export function HealthChecks({ online, onStepValid }: Props) { + const codex = useDebug(throwOnError); + const portForwarding = usePortForwarding(codex.data); + const persistence = usePersistence(codex.isSuccess); + const [isAddressInvalid, setIsAddressInvalid] = useState(false); + const [isPortInvalid, setIsPortInvalid] = useState(false); + const [address, setAddress] = useState( + HealthCheckUtil.removePort(CodexSdk.url()) + ); + const [port, setPort] = useState(HealthCheckUtil.getPort(CodexSdk.url())); + const queryClient = useQueryClient(); + + useEffect( + () => { + if (codex.isSuccess) { + persistence.refetch(); + portForwarding.refetch().then(({ data }) => { + onStepValid(data?.reachable || false); + }); + } else { + onStepValid(false); + } + }, + // We really do not want to add persistence and portForwarding as + // dependencies because it will cause a re-render every time. + // eslint-disable-next-line react-hooks/exhaustive-deps + [persistence.refetch, onStepValid, portForwarding.refetch, codex.isSuccess] + ); + + const onAddressChange = (e: React.FormEvent) => { + const element = e.currentTarget; + const value = e.currentTarget.value; + + setIsAddressInvalid(!element.checkValidity()); + + const address = HealthCheckUtil.removePort(value); + setAddress(address); + + if (HealthCheckUtil.containsPort(value)) { + const p = HealthCheckUtil.getPort(value); + setPort(p); + } + }; + + const onPortChange = (e: React.FormEvent) => { + const element = e.currentTarget; + const value = element.value; + + setIsPortInvalid(!element.checkValidity()); + setPort(parseInt(value, 10)); + }; + + const onSave = () => { + const url = address + ":" + port; + + if (HealthCheckUtil.isUrlInvalid(url)) { + return; + } + + CodexSdk.updateURL(url) + .then(() => queryClient.invalidateQueries()) + .then(() => codex.refetch()); + }; + + let forwardingPortValue = defaultPort; + + if (codex.isSuccess && codex.data) { + const port = PortForwardingUtil.getTcpPort(codex.data); + if (!port.error) { + forwardingPortValue = port.data; + } + } + + return ( +
+
+
+ + {isAddressInvalid ? ( + + ) : ( + + )} +
+ +
+ + +
+ +
+ +
+
+ +

+

  • + Port forwarding should be {forwardingPortValue} for TCP and 8090 by + default for UDP. +
  • +

    + +
      +
    • + + + + Health Check +
    • +
    • + + {online ? ( + + ) : ( + + )} + + Internet connection +
    • +
    • + + {portForwarding.isFetching ? ( + + ) : portForwarding.enabled ? ( + + ) : ( + + )} + + Port forwarding +
    • +
    • + + {codex.isFetching ? ( + + ) : codex.isSuccess ? ( + + ) : ( + + )} + + Codex connection +
    • +
    • + + {persistence.isFetching ? ( + + ) : persistence.enabled ? ( + + ) : ( + + )} + + Marketplace +
    • +
    +
    + ); +} diff --git a/src/components/HealthChecks/health-check.utils.test.ts b/src/components/HealthChecks/health-check.utils.test.ts new file mode 100644 index 0000000..4565603 --- /dev/null +++ b/src/components/HealthChecks/health-check.utils.test.ts @@ -0,0 +1,148 @@ +import { assert, describe, it } from "vitest"; +import { HealthCheckUtil } from "./health-check.utils"; + +describe("health check", () => { + it("remove the port from an url", async () => { + assert.deepEqual(HealthCheckUtil.removePort("http://localhost:8080"), "http://localhost"); + }); + + it("get the port from an url", async () => { + assert.deepEqual(HealthCheckUtil.getPort("http://localhost:8080"), 8080); + }); + + it("get the default port when the url does not contain the port", async () => { + assert.deepEqual(HealthCheckUtil.getPort("http://localhost"), 80); + }); + + it("returns true when the url contains a port", async () => { + assert.deepEqual(HealthCheckUtil.containsPort("http://localhost:8080"), true); + }); + + it("returns false when the url does not contain a port", async () => { + assert.deepEqual(HealthCheckUtil.containsPort("http://localhost"), false); + }); + + + it("returns true when the url is invalid", async () => { + assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://"), true); + }); + + it("returns false when the url is valid", async () => { + assert.deepEqual(HealthCheckUtil.isUrlInvalid("http://localhost:8080"), false); + }); + + it("returns the tcp port", async () => { + const debug = { + "id": "a", + "addrs": [ + "/ip4/127.0.0.1/tcp/8070" + ], + "repo": "", + "spr": "", + "announceAddresses": [ + "/ip4/127.0.0.1/tcp/8070" + ], + "table": { + "localNode": { + "nodeId": "", + "peerId": "", + "record": "", + "address": "0.0.0.0:8090", + "seen": false + }, + "nodes": [] + }, + "codex": { + "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + "revision": "2fb7031e" + } + } + assert.deepEqual(HealthCheckUtil.getTcpPort(debug), { error: false, data: 8070 }); + }); + + it("returns an error when the addr is empty", async () => { + const debug = { + "id": "a", + "addrs": [ + ], + "repo": "", + "spr": "", + "announceAddresses": [ + "/ip4/127.0.0.1/tcp/8070" + ], + "table": { + "localNode": { + "nodeId": "", + "peerId": "", + "record": "", + "address": "0.0.0.0:8090", + "seen": false + }, + "nodes": [] + }, + "codex": { + "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + "revision": "2fb7031e" + } + } + assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true); + }); + + it("returns an error when the addr is misformated", async () => { + const debug = { + "id": "a", + "addrs": [ + "/ip4/127.0.0.1/tcp/hello" + ], + "repo": "", + "spr": "", + "announceAddresses": [ + "/ip4/127.0.0.1/tcp/8070" + ], + "table": { + "localNode": { + "nodeId": "", + "peerId": "", + "record": "", + "address": "0.0.0.0:8090", + "seen": false + }, + "nodes": [] + }, + "codex": { + "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + "revision": "2fb7031e" + } + } + assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true); + }); + + it("returns an error when the port is misformated", async () => { + const debug = { + "id": "a", + "addrs": [ + "hello" + ], + "repo": "", + "spr": "", + "announceAddresses": [ + "/ip4/127.0.0.1/tcp/8070" + ], + "table": { + "localNode": { + "nodeId": "", + "peerId": "", + "record": "", + "address": "0.0.0.0:8090", + "seen": false + }, + "nodes": [] + }, + "codex": { + "version": "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7", + "revision": "2fb7031e" + } + } + assert.deepEqual(HealthCheckUtil.getTcpPort(debug).error, true); + }); +}) \ No newline at end of file diff --git a/src/components/HealthChecks/health-check.utils.ts b/src/components/HealthChecks/health-check.utils.ts new file mode 100644 index 0000000..65250a4 --- /dev/null +++ b/src/components/HealthChecks/health-check.utils.ts @@ -0,0 +1,51 @@ +import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js" + +export const HealthCheckUtil = { + removePort(url: string) { + const parts = url.split(":") + return parts[0] + ":" + parts[1] + }, + + /* + * Extract the port from a protocol + ip + port string + */ + getPort(url: string) { + return parseInt(url.split(":")[2] || "80", 10) + }, + + containsPort(url: string) { + return url.split(":").length > 2 + }, + + isUrlInvalid(url: string) { + try { + new URL(url) + return false + // We do not need to manage the error because we want to check + // if the URL is valid or not only. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + return true + } + }, + + getTcpPort(info: CodexDebugInfo): SafeValue { + if (info.addrs.length === 0) { + return { error: true, data: new CodexError("Not existing address") } + } + + const parts = info.addrs[0].split("/") + + if (parts.length < 2) { + return { error: true, data: new CodexError("Address misformated") } + } + + const port = parseInt(parts[parts.length - 1], 10) + + if (isNaN(port)) { + return { error: true, data: new CodexError("Port misformated") } + } + + return { error: false, data: port } + } +} \ No newline at end of file diff --git a/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx b/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx deleted file mode 100644 index f06a392..0000000 --- a/src/components/HttpNetworkIndicator/HttpNetworkIndicator.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { NetworkIndicator } from "@codex-storage/marketplace-ui-components"; -import { useNetwork } from "../../network/useNetwork"; - -export function HttpNetworkIndicator() { - const online = useNetwork(); - - const text = online ? "Online" : "Offline"; - - return ; -} diff --git a/src/components/LogLevel/LogLevel.css b/src/components/LogLevel/LogLevel.css index a544411..74ac045 100644 --- a/src/components/LogLevel/LogLevel.css +++ b/src/components/LogLevel/LogLevel.css @@ -1,7 +1,26 @@ -.logLevel { +.log-level { flex-grow: 1; -} + display: flex; + gap: 16px; + margin-top: 14px; -.logLevel-select { - margin-bottom: 0.75rem; + > div:first-child { + position: relative; + + svg { + position: absolute; + top: 11px; + left: 16px; + color: #969696; + } + } + + .select { + background-position: url(/icons/logs.svg); + } + + select { + border-color: #969696; + padding-left: 40px; + } } diff --git a/src/components/LogLevel/LogLevel.tsx b/src/components/LogLevel/LogLevel.tsx index c2f3978..eb740a7 100644 --- a/src/components/LogLevel/LogLevel.tsx +++ b/src/components/LogLevel/LogLevel.tsx @@ -9,6 +9,8 @@ import { Toast, } from "@codex-storage/marketplace-ui-components"; import { Promises } from "../../utils/promises"; +import LogsIcon from "../../assets/icons/logs.svg?react"; +import SaveIcon from "../../assets/icons/save.svg?react"; export function LogLevel() { const queryClient = useQueryClient(); @@ -37,7 +39,7 @@ export function LogLevel() { const [toast, setToast] = useState({ time: 0, message: "", - variant: "success" as "success" | "error" | "default", + variant: "success" as "success" | "error", }); function onChange(e: React.FormEvent) { @@ -52,34 +54,40 @@ export function LogLevel() { }; const levels = [ - ["DEBUG", "DEBUG"], - ["TRACE", "TRACE"], - ["INFO", "INFO"], - ["NOTICE", "NOTICE"], - ["WARN", "WARN"], - ["ERROR", "ERROR"], - ["FATAL", "FATAL"], + ["DEBUG", "Debug"], + ["TRACE", "Trace"], + ["INFO", "Info"], + ["NOTICE", "Notice"], + ["WARN", "Warn"], + ["ERROR", "Error"], + ["FATAL", "Fatal"], ] satisfies [string, string][]; return ( - <> - +
    +
    + + +
    + - +
    ); } diff --git a/src/components/ManifestFetch/ManifestFetch.css b/src/components/ManifestFetch/ManifestFetch.css new file mode 100644 index 0000000..8c03b3c --- /dev/null +++ b/src/components/ManifestFetch/ManifestFetch.css @@ -0,0 +1,16 @@ +.manifest-fetch { + display: flex; + gap: 16px; + + .input { + flex: 1; + + input { + width: 100%; + } + } + + .button { + width: 105px; + } +} diff --git a/src/components/ManifestFetch/ManifestFetch.tsx b/src/components/ManifestFetch/ManifestFetch.tsx new file mode 100644 index 0000000..1250ff4 --- /dev/null +++ b/src/components/ManifestFetch/ManifestFetch.tsx @@ -0,0 +1,57 @@ +import { Button, Input } from "@codex-storage/marketplace-ui-components"; +import "./ManifestFetch.css"; +import { ChangeEvent, useState } from "react"; +import { CodexSdk } from "../../sdk/codex"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Promises } from "../../utils/promises"; + +export function ManifestFetch() { + const [cid, setCid] = useState(""); + const queryClient = useQueryClient(); + + const { refetch } = useQuery({ + queryFn: () => { + return CodexSdk.data() + .fetchManifest(cid) + .then((s) => { + if (s.error === false) { + setCid(""); + queryClient.invalidateQueries({ queryKey: ["cids"] }); + } + return Promises.rejectOnError(s); + }); + }, + queryKey: ["manifest"], + + // Disable the fetch to make it available on refetch only + enabled: false, + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + refetchOnWindowFocus: false, + }); + + const onDownload = () => refetch(); + + const onCidChange = (e: ChangeEvent) => + setCid(e.currentTarget.value); + + return ( +
    + + +
    + ); +} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx new file mode 100644 index 0000000..c921dbd --- /dev/null +++ b/src/components/Menu/Menu.tsx @@ -0,0 +1,171 @@ +import { attributes } from "../../utils/attributes"; +import "./menu.css"; +import { ComponentType, useState } from "react"; +import { classnames } from "../../utils/classnames"; +import { Link } from "@tanstack/react-router"; +import HomeIcon from "../../assets/icons/home.svg?react"; +import ExpandIcon from "../../assets/icons/expand.svg?react"; +import WalletIcon from "../../assets/icons/wallet.svg?react"; +import NodesIcon from "../../assets/icons/wallet.svg?react"; +import FilesIcon from "../../assets/icons/files.svg?react"; +import AnalyticsIcon from "../../assets/icons/analytics.svg?react"; +import Logo from "../../assets/icons/logo.svg?react"; +import Logotype from "../../assets/icons/logotype.svg?react"; +import DeviceIcon from "../../assets/icons/device.svg?react"; +import PeersIcon from "../../assets/icons/peers.svg?react"; +import PurchaseIcon from "../../assets/icons/purchase.svg?react"; +import HostIcon from "../../assets/icons/host.svg?react"; +import LogsIcon from "../../assets/icons/logs.svg?react"; +import SettingsIcon from "../../assets/icons/settings.svg?react"; +import HelpIcon from "../../assets/icons/help.svg?react"; +import DisclaimerIcon from "../../assets/icons/disclaimer.svg?react"; + +export type MenuItemComponentProps = { + onClick: () => void; +}; + +export type MenuItem = + | { + type: "separator"; + } + | { + type: "space"; + } + | { + type: "item"; + Component: ComponentType; + }; + +export function Menu() { + const [isExpanded, setIsExpanded] = useState(true); + + const onLogoClick = () => { + if (isExpanded === false) { + setIsExpanded(true); + } + }; + + const onExpandMenu = () => setIsExpanded(!isExpanded); + + return ( + <> + + + ); +} diff --git a/src/components/Menu/menu.css b/src/components/Menu/menu.css new file mode 100644 index 0000000..459116b --- /dev/null +++ b/src/components/Menu/menu.css @@ -0,0 +1,255 @@ +.menu { + display: flex; + flex-direction: column; + background-color: #1c1c1c; + border-radius: var(--codex-border-radius); + transition: left 0.25s; + position: sticky; + z-index: 10; + view-transition-name: main-menu; + height: 100%; + top: 0; + transition: + width 0.5s, + font-size 0.5s, + left 0.05s; + min-width: 0; + width: 272px; + min-width: 80px; + + @media (max-width: 1199px) { + & { + width: 80px; + .items { + a { + width: 26px; + gap: 0; + display: flex; + justify-content: center; + + span + span { + font-size: 0; + display: none; + } + } + } + } + } + + @media (min-width: 1200px) { + &[aria-expanded] a[data-title]:hover::after { + content: attr(data-title); + background-color: #2f2f2f; + color: #fff; + padding: 8px; + border-radius: 4px; + font-size: 12px; + line-height: 14px; + display: block; + white-space: nowrap; + position: absolute; + right: 1rem; + overflow: visible; + } + } + + &:not([aria-expanded]) { + width: 80px; + + .items { + a { + width: 26px; + gap: 0; + display: flex; + justify-content: center; + + span + span { + font-size: 0; + display: none; + } + } + } + } + + > div { + display: flex; + flex-direction: column; + padding: 12px; + position: sticky; + top: 0; + height: calc(100vh - 24px); + overflow: auto; + } + + header { + padding: 13px; + display: flex; + align-items: center; + gap: 1.5rem; + background-color: #060606; + border-radius: 8px; + + > svg:first-child { + min-width: 30px; + } + + @media (min-width: 1200px) { + > svg:first-child { + cursor: pointer; + } + } + + div { + flex: 1; + text-align: right; + transition: opacity 0.35s; + display: inline-block; + overflow: hidden; + min-width: 0; + + svg { + cursor: pointer; + } + + &:hover { + animation-name: example; + animation-duration: 2.5s; + animation-iteration-count: infinite; + } + } + } + + .items { + padding-top: 1.5rem; + display: flex; + flex-direction: column; + position: relative; + height: 100%; + margin-bottom: 2.5rem; + gap: 0.5rem; + border-top: 1px solid #96969633; + + &::before { + height: 20px; + width: 8px; + background-color: var(--codex-color-primary); + position: absolute; + content: " "; + transition: + top 1s, + bottom 1s; + border-radius: 4px; + left: -16px; + } + + &:has(.active:nth-child(1))::before { + top: 30px; + } + + &:has(.active:nth-child(2))::before { + top: 72px; + } + + &:has(.active:nth-child(3))::before { + top: 115px; + } + + &:has(.active:nth-child(4))::before { + top: 158px; + } + + &:has(.active:nth-child(5))::before { + top: 201px; + } + + &:has(.active:nth-child(6))::before { + top: 244px; + } + + &:has(.active:nth-child(8))::before { + top: 339px; + } + + &:has(.active:nth-child(9))::before { + top: 382px; + } + + &:has(.active:nth-child(11))::before { + top: 475px; + } + + &:has(.active:nth-child(12))::before { + top: 513px; + } + + &:has(.active:nth-child(14))::before { + top: calc(100% - 113px); + } + + &:has(.active:nth-child(15))::before { + top: calc(100% - 70px); + } + + &:has(.active:nth-child(16))::before { + top: calc(100% - 27px); + } + + &:not(:first-child) { + margin-top: 0.5rem; + } + + hr { + margin-top: 1.5rem; + margin-bottom: 1.5rem; + border: 0.1px solid #96969633; + width: 100%; + } + + section { + flex: 1; + } + + a { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 8px 10px; + margin-bottom: 0; + text-decoration: none; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: -0.006em; + color: #969696; + border-radius: 8px; + transition: background-color 0.35s; + position: relative; + margin-left: 6px; + + &:hover:not([aria-disabled="true"]), + &.active { + background-color: var(--codex-highlight-color); + color: #c7c7c7; + } + + span:first-child { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + transition: color 1s; + } + + span + span { + display: inline-block; + overflow: hidden; + min-width: 0; + } + + &.active span:first-child { + color: var(--codex-color-primary); + } + } + } +} diff --git a/src/components/NodeIndicator/NodeIndicator.tsx b/src/components/NodeIndicator/NodeIndicator.tsx deleted file mode 100644 index 422a9e3..0000000 --- a/src/components/NodeIndicator/NodeIndicator.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import { CodexSdk } from "../../sdk/codex"; -import { - NetworkIndicator, - Toast, -} from "@codex-storage/marketplace-ui-components"; -import { Promises } from "../../utils/promises"; - -const report = false; - -export function NodeIndicator() { - const queryClient = useQueryClient(); - const [toast] = useState({ - time: 0, - message: "", - }); - - const { data, isError } = useQuery({ - queryKey: ["spr"], - queryFn: async () => { - return CodexSdk.node() - .spr() - .then((data) => Promises.rejectOnError(data, report)); - }, - refetchInterval: 5000, - - // No need to retry because we defined a refetch interval - retry: false, - - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, - - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, - - // Cache is not useful for the spr endpoint - gcTime: 0, - }); - - const isCodexOnline = !isError && !!data; - - useEffect(() => { - queryClient.invalidateQueries({ - type: "active", - refetchType: "all", - }); - - // Dispatch an event in order to reset Sentry error boundary - document.dispatchEvent(new CustomEvent("codexinvalidatequeries")); - }, [queryClient, isCodexOnline]); - - return ( - <> - - - - ); -} diff --git a/src/components/NodeSpace/NodeSpace.css b/src/components/NodeSpace/NodeSpace.css new file mode 100644 index 0000000..8c7c82d --- /dev/null +++ b/src/components/NodeSpace/NodeSpace.css @@ -0,0 +1,13 @@ +.node-space { + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + padding-top: 16px; + margin-bottom: 16px; + border-top: 1px solid #96969633; + } +} diff --git a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx b/src/components/NodeSpace/NodeSpace.tsx similarity index 63% rename from src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx rename to src/components/NodeSpace/NodeSpace.tsx index e5cca39..0c65a20 100644 --- a/src/components/NodeSpaceAllocation/NodeSpaceAllocation.tsx +++ b/src/components/NodeSpace/NodeSpace.tsx @@ -3,7 +3,7 @@ import Loader from "../../assets/loader.svg"; import { CodexSdk } from "../../sdk/codex"; import { SpaceAllocation } from "@codex-storage/marketplace-ui-components"; import { Promises } from "../../utils/promises"; -import { nodeSpaceAllocationColors } from "./nodeSpaceAllocation.domain"; +import "./NodeSpace.css"; const defaultSpace = { quotaMaxBytes: 0, @@ -12,7 +12,7 @@ const defaultSpace = { totalBlocks: 0, }; -export function NodeSpaceAllocation() { +export function NodeSpace() { const { data: space, isPending } = useQuery({ queryFn: () => CodexSdk.data() @@ -41,24 +41,27 @@ export function NodeSpaceAllocation() { const { quotaMaxBytes, quotaReservedBytes, quotaUsedBytes } = space; return ( - +
    +
    Disk
    + + +
    ); } diff --git a/src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts b/src/components/NodeSpace/nodeSpace.domain.ts similarity index 100% rename from src/components/NodeSpaceAllocation/nodeSpaceAllocation.domain.ts rename to src/components/NodeSpace/nodeSpace.domain.ts diff --git a/src/components/OnBoarding/OnBoardingLayout.css b/src/components/OnBoarding/OnBoardingLayout.css new file mode 100644 index 0000000..6a1c2cc --- /dev/null +++ b/src/components/OnBoarding/OnBoardingLayout.css @@ -0,0 +1,240 @@ +.onboarding { + width: 100%; + padding: 16px; + display: flex; + + @media (min-width: 1000px) { + & { + padding: 3rem 6rem; + } + } + + > section { + display: flex; + flex-direction: column; + justify-content: space-between; + z-index: 1; + } + + @media (min-width: 1000px) { + > section:first-child { + max-width: 500px; + } + } + + section { + flex: 1; + } + + &.onboarding--second .alpha { + flex: 0.3; + } + + &.onboarding--third .alpha { + flex: 0.5; + } + + section > *:first-child { + flex: 0.5; + } + + h1 { + font-family: Inter; + font-size: 36px; + font-weight: 300; + line-height: 43.57px; + letter-spacing: 0.01em; + + b { + font-weight: 400; + } + + b + b { + font-weight: 900; + text-transform: uppercase; + } + } + + footer { + display: flex; + align-items: center; + justify-content: space-between; + flex: 0; + + ul { + display: flex; + gap: 8px; + + li { + cursor: pointer; + width: 12px; + height: 12px; + background-color: white; + display: inline-block; + border-radius: 50%; + transition: opacity 0.35s; + + &:hover { + animation-name: pulse; + animation-duration: 2.5s; + animation-iteration-count: infinite; + } + + &:not([aria-selected]) { + opacity: 0.4; + } + + &[aria-selected] { + box-shadow: 0px 0px 12px 0px #fff; + opacity: 1; + } + } + } + } + + .alpha { + > div { + margin-top: 4px; + display: block; + } + + b { + font-weight: 500; + opacity: 0.6; + display: block; + } + + a { + text-decoration: underline; + font-family: Azeret Mono; + font-size: 12px; + font-weight: 400; + line-height: 14px; + margin-top: 16px; + cursor: pointer; + display: inline-flex; + align-items: center; + color: var(--codex-color-error-hexa); + + &:hover { + animation-name: example; + animation-duration: 2.5s; + animation-iteration-count: infinite; + } + } + + p { + margin-top: 1rem; + font-family: Azeret Mono; + font-size: 14px; + font-weight: 400; + line-height: 16.34px; + display: inline-block; + color: var(--codex-color-primary); + } + } + + .main { + p { + font-family: Azeret Mono; + font-size: 14px; + font-weight: 400; + line-height: 16.34px; + max-width: 532px; + margin-top: 20px; + color: var(--codex-input-label-color); + } + + label { + margin-top: 1rem; + } + + .health-checks { + .address { + .refresh { + top: 22px; + } + } + } + } + + .get-started { + a { + font-size: 24px; + font-weight: 600; + line-height: 29.05px; + letter-spacing: 0.01em; + margin-top: 32px; + font-family: Inter; + color: #7bfbaf; + gap: 4px; + text-decoration: none; + border-bottom: 2px solid #7bfbaf; + cursor: pointer; + display: inline-flex; + align-items: center; + } + } + + .modal { + max-width: 600px; + margin: auto; + + h1 { + margin-top: 0; + margin-bottom: 3rem; + } + + p { + line-height: 1.5rem; + } + } + + .navigation { + cursor: pointer; + position: absolute; + right: 16px; + bottom: 16px; + border-bottom: none; + text-decoration: none; + z-index: 1; + + right: 6rem; + bottom: 16px; + + &:hover { + animation-name: example; + animation-duration: 2.5s; + animation-iteration-count: infinite; + } + + &[aria-disabled="true"] { + cursor: not-allowed; + } + } +} + +@keyframes rotate { + from { + transform: rotate(0deg); /* Start at 0 degrees */ + } + to { + transform: rotate(360deg); /* End at 360 degrees */ + } +} + +@keyframes pulse { + 0% { + opacity: 0.4; + } + 30% { + opacity: 0.8; + } + 100% { + opacity: 0.4; + } +} + +#sentry-feedback { + right: 128px; +} diff --git a/src/components/OnBoarding/OnBoardingLayout.tsx b/src/components/OnBoarding/OnBoardingLayout.tsx new file mode 100644 index 0000000..9f752a1 --- /dev/null +++ b/src/components/OnBoarding/OnBoardingLayout.tsx @@ -0,0 +1,50 @@ +import { ReactElement } from "react"; +import { classnames } from "../../utils/classnames"; +import Logotype from "../../assets/icons/logotype.svg?react"; +import { attributes } from "../../utils/attributes"; +import "./OnBoardingLayout.css"; +import { BackgroundImage } from "../BackgroundImage/BackgroundImage"; +import { useNavigate } from "@tanstack/react-router"; + +type Props = { + children: ReactElement<{ onStepValid: (isValid: boolean) => void }>; + step: number; + defaultIsStepValid: boolean; +}; + +export function OnBoardingLayout({ children, step }: Props) { + const navigate = useNavigate({ from: window.location.pathname }); + + return ( +
    +
    +
    + +
    + + {children} + +
    +
      +
    • navigate({ to: "/" })}>
    • +
    • navigate({ to: "/onboarding-name" })}>
    • +
    • navigate({ to: "/onboarding-checks" })}>
    • +
    +
    +
    + +
    + ); +} diff --git a/src/components/Peers/PeerCountryCell.css b/src/components/Peers/PeerCountryCell.css index ac159a5..b2489b3 100644 --- a/src/components/Peers/PeerCountryCell.css +++ b/src/components/Peers/PeerCountryCell.css @@ -1,5 +1,11 @@ -.peerCountry { +.peer-country { display: flex; align-items: center; gap: 1rem; + + span:first-child { + background-color: #141414; + border-radius: 50%; + padding: 12px; + } } diff --git a/src/components/Peers/PeerCountryCell.tsx b/src/components/Peers/PeerCountryCell.tsx index 0737655..e1a2873 100644 --- a/src/components/Peers/PeerCountryCell.tsx +++ b/src/components/Peers/PeerCountryCell.tsx @@ -1,69 +1,23 @@ import { Cell } from "@codex-storage/marketplace-ui-components"; -import { PeerPin } from "./types"; -import { useQuery } from "@tanstack/react-query"; import "./PeerCountryCell.css"; -import { useEffect } from "react"; +import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils"; export type Props = { - address: string; - onPinAdd: (pin: PeerPin) => void; + node: PeerNode; + geo: PeerGeo | undefined; }; -const getFlagEmoji = (countryCode: string) => { - const codePoints = countryCode - .toUpperCase() - .split("") - .map((char) => 127397 + char.charCodeAt(0)); - return String.fromCodePoint(...codePoints); -}; - -export function PeerCountryCell({ address, onPinAdd }: Props) { - const { data } = useQuery({ - queryFn: () => { - const [ip] = address.split(":"); - - return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then( - (res) => res.json() - ); - }, - refetchOnMount: true, - - queryKey: [address], - - // Enable only when the address exists - enabled: !!address, - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // We can cache the data at Infinity because the relation between - // country and ip is fixed - staleTime: Infinity, - - // Don't expect something new when coming back to the UI - refetchOnWindowFocus: false, - }); - - useEffect(() => { - if (data) { - onPinAdd({ - lat: data.latitude, - lng: data.longitude, - }); - } - }, [data]); - +export function PeerCountryCell({ geo }: Props) { return ( -
    - {data ? ( +
    + {geo ? ( <> - {!!data && getFlagEmoji(data.country_iso)} - {data?.country} + {!!geo && PeerUtils.geCountryEmoji(geo.country_iso)} + {geo?.country} ) : ( - {address} + )}
    diff --git a/src/components/Peers/Peers.css b/src/components/Peers/Peers.css new file mode 100644 index 0000000..18669ad --- /dev/null +++ b/src/components/Peers/Peers.css @@ -0,0 +1,184 @@ +.peers { + max-width: 1320px; + margin-left: auto; + margin-right: auto; + padding-bottom: 32px; + + > div { + max-width: 1320px; + } + + > div:first-child { + width: calc(100% - 16px); + border: 1px solid #96969633; + padding: 16px; + border-radius: 16px; + position: relative; + + @media (min-width: 1000px) { + & { + width: calc(100% - 128px - 16px); + padding: 16px 16px 16px 128px; + height: 600px; + } + } + + ul { + display: none; + + @media (min-width: 1000px) { + & { + list-style-type: none; + width: 71px; + position: absolute; + left: 16px; + top: 16px; + display: inline-block; + } + } + + li { + border-bottom: 1px solid #969696cc; + padding: 16px 0; + text-align: right; + } + + li:first-child { + font-size: 20px; + font-weight: 500; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #7b7b7b; + } + + li:not(:first-child) { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + position: relative; + } + + li:not(:first-child)::before { + content: " "; + border: 4px solid var(--codex-color-primary); + border-radius: 50%; + height: 8px; + width: 8px; + display: inline-block; + position: absolute; + left: 0; + top: 20px; + } + + li:nth-child(3)::before { + border-width: 5px; + height: 11px; + width: 11px; + top: 18px; + } + + li:nth-child(4)::before { + border-width: 6px; + height: 12px; + width: 12px; + top: 16px; + } + } + + .connections { + background-color: #232323; + border: 1px solid #96969633; + border-radius: 16px; + max-width: 280px; + padding: 16px; + transform: scale(0.7); + width: 280px; + + @media (max-width: 999px) { + & { + position: relative; + bottom: -32px; + left: -32px; + } + } + + @media (min-width: 1000px) { + & { + transform: scale(1); + } + } + + @media (min-width: 1000px) { + & { + position: absolute; + bottom: 16px; + left: 16px; + width: 280px; + } + } + + header { + display: flex; + align-items: center; + gap: 8px; + color: #969696; + padding-bottom: 16px; + border-bottom: 1px solid #96969633; + + span { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + color: white; + } + } + + footer { + border-top: 1px solid #96969633; + padding-top: 16px; + } + } + } + + > div:nth-child(2) { + margin-top: 32px; + width: 100%; + } + + table { + td:last-child { + div { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 8px; + + &.status--active { + color: #1daf61; + background-color: #6fcb9433; + } + + &.status--inactive { + color: #fb3748; + background-color: #fb374833; + } + } + } + } + + .peers-chart { + transform: scale(0.5); + + @media (min-width: 1000px) { + & { + transform: scale(0.73); + } + } + } +} diff --git a/src/components/Peers/Peers.tsx b/src/components/Peers/Peers.tsx new file mode 100644 index 0000000..93faf98 --- /dev/null +++ b/src/components/Peers/Peers.tsx @@ -0,0 +1,106 @@ +import { + TabSortState, + Row, + Cell, + Table, +} from "@codex-storage/marketplace-ui-components"; +import { useCallback, useState } from "react"; +import { PeerCountryCell } from "./PeerCountryCell"; +import "./Peers.css"; +import { PeerGeo, PeerNode, PeerSortFn, PeerUtils } from "./peers.utils"; +import { PeersMap } from "./PeersMap"; +import { useDebug } from "../../hooks/useDebug"; +import { PeersQuality } from "./PeersQuality"; +import { PeersChart } from "./PeersChart"; +import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react"; +import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react"; +import PeersIcon from "../../assets/icons/peers.svg?react"; + +const throwOnError = true; + +export const Peers = () => { + const { data } = useDebug(throwOnError); + const [ips, setIps] = useState>({}); + + const onPinAdd = useCallback((node: PeerNode, geo: PeerGeo) => { + const [ip = ""] = node.address.split(":"); + setIps((ips) => ({ ...ips, [ip]: geo })); + }, []); + + const [sortFn, setSortFn] = useState(() => + PeerUtils.sortByBoolean("desc") + ); + + const onSortByCountry = (state: TabSortState) => + setSortFn(() => PeerUtils.sortByCountry(state, ips)); + + const onSortActive = (state: TabSortState) => + setSortFn(() => PeerUtils.sortByBoolean(state)); + + const headers = [ + ["Country", onSortByCountry], + ["PeerId"], + ["Active", onSortActive], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const nodes = data?.table.nodes || []; + const sorted = sortFn ? nodes.slice().sort(sortFn) : nodes; + + const rows = sorted.map((node) => { + const [ip = ""] = node.address.split(":"); + const geo = ips[ip]; + + return ( + , + {node.peerId}, + + {node.seen ? ( +
    + Active +
    + ) : ( +
    + Inactive +
    + )} +
    , + ]}>
    + ); + }); + + const actives = PeerUtils.countActives(sorted); + const degrees = PeerUtils.calculareDegrees(sorted); + const good = PeerUtils.isGoodQuality(actives); + + return ( +
    +
    + +
    +
      +
    • Legend
    • +
    • 1-3
    • +
    • 3-5
    • +
    • 5 +
    • +
    +
    +
    + + Connections +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + ); +}; diff --git a/src/components/Peers/PeersCard.css b/src/components/Peers/PeersCard.css new file mode 100644 index 0000000..c5007ed --- /dev/null +++ b/src/components/Peers/PeersCard.css @@ -0,0 +1,23 @@ +.peers-card { + position: relative; + + .peers-map { + border-top: 1px solid #96969633; + border-bottom: 1px solid #96969633; + width: 100%; + + svg { + width: 100%; + } + } + + .peers-chart { + position: absolute; + left: 0; + right: 0; + } + + footer { + padding-top: 16px; + } +} diff --git a/src/components/Peers/PeersCard.tsx b/src/components/Peers/PeersCard.tsx new file mode 100644 index 0000000..cd6f2d7 --- /dev/null +++ b/src/components/Peers/PeersCard.tsx @@ -0,0 +1,29 @@ +import { PeersMap } from "./PeersMap"; +import "./PeersCard.css"; +import { useDebug } from "../../hooks/useDebug"; +import { PeerUtils } from "./peers.utils"; +import { PeersChart } from "./PeersChart"; +import { PeersQuality } from "./PeersQuality"; + +const throwOnError = true; + +export function PeersCard() { + const { data } = useDebug(throwOnError); + + const nodes = data?.table.nodes ?? []; + const actives = PeerUtils.countActives(nodes); + const degrees = PeerUtils.calculareDegrees(nodes); + const good = PeerUtils.isGoodQuality(actives); + + return ( +
    +
    + + +
    +
    + +
    +
    + ); +} diff --git a/src/components/Peers/PeersChart.css b/src/components/Peers/PeersChart.css new file mode 100644 index 0000000..e01d60f --- /dev/null +++ b/src/components/Peers/PeersChart.css @@ -0,0 +1,64 @@ +.peers-chart { + position: relative; + width: 350px; + height: 175px; + overflow: hidden; + margin: auto; + left: -32px; + + @media (min-width: 1000px) { + & { + transform: scale(0.73); + } + } + + *, + &::before { + box-sizing: border-box; + } + + &::before, + &::after { + position: absolute; + } + + &::before { + content: ""; + width: inherit; + height: inherit; + border: 45px solid #323232; + border-bottom: none; + border-top-left-radius: 175px; + border-top-right-radius: 175px; + } + + div { + position: absolute; + top: 100%; + width: inherit; + height: inherit; + border: 45px solid #323232; + border-top: none; + border-bottom-left-radius: 175px; + border-bottom-right-radius: 175px; + transform-origin: 50% 0; + } + + div:nth-child(1) { + border-color: var(--codex-color-primary); + transform: rotate(calc(var(--codex-peers-degrees) * 1deg)); + background-color: transparent; + } + + span { + font-family: Inter; + font-size: 38.67px; + font-weight: 500; + line-height: 48.34px; + letter-spacing: -0.005em; + text-align: center; + position: absolute; + bottom: 0; + left: calc(50% - 24px); + } +} diff --git a/src/components/Peers/PeersChart.tsx b/src/components/Peers/PeersChart.tsx new file mode 100644 index 0000000..3406f87 --- /dev/null +++ b/src/components/Peers/PeersChart.tsx @@ -0,0 +1,23 @@ +import "./PeersChart.css"; + +type Props = { + actives: number; + degrees: number; +}; + +type CustomCSSProperties = React.CSSProperties & { + "--codex-peers-degrees": number; +}; + +export function PeersChart({ actives, degrees }: Props) { + const style: CustomCSSProperties = { + "--codex-peers-degrees": degrees, + }; + + return ( +
    +
    + {actives} +
    + ); +} diff --git a/src/components/Peers/PeersMap.tsx b/src/components/Peers/PeersMap.tsx new file mode 100644 index 0000000..d8efdd7 --- /dev/null +++ b/src/components/Peers/PeersMap.tsx @@ -0,0 +1,92 @@ +import { getMapJSON } from "dotted-map"; +import DottedMap from "dotted-map/without-countries"; +import { PeerGeo, PeerNode, PeerUtils } from "./peers.utils"; +import { useCallback, useState } from "react"; +import { PeersPin } from "./PeersPin"; + +// This function accepts the same arguments as DottedMap in the example above. +const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" }); + +type Props = { + nodes: PeerNode[]; + onPinAdd?: (node: PeerNode, geo: PeerGeo) => void; +}; + +export function PeersMap({ nodes, onPinAdd }: Props) { + // It’s safe to re-create the map at each render, because of the + // pre-computation it’s super fast ⚡️ + const map = new DottedMap({ map: JSON.parse(mapJsonString) }); + + const [pins, setPins] = useState<[PeerNode & PeerGeo, number][]>([]); + + const onInternalPinAdd = useCallback( + (node: PeerNode, geo: PeerGeo) => { + setPins((val) => PeerUtils.incPin(val, { ...node, ...geo })); + onPinAdd?.(node, geo); + }, + [setPins, onPinAdd] + ); + + pins.map(([pin, quantity]) => { + let radius = 0.65; + + if (quantity > 3) { + radius = 0.85; + } + + if (quantity > 5) { + radius = 0.95; + } + + map.addPin({ + lat: pin.latitude, + lng: pin.longitude, + svgOptions: { color: "#d6ff79", radius }, + }); + }); + + const svgMap = map + .getSVG({ + radius: 0.32, + color: "#969696", + shape: "circle", + backgroundColor: "#141414", + }) + .replace( + "", + // Include the style into the svg tag for permance reason. + // An alternative would be to generated the full svg instead + // of the image but it's costful. + ` + ` + ); + + return ( + <> + {nodes.map((node, index) => ( + + ))} + + + ); +} diff --git a/src/components/Peers/PeersPin.ts b/src/components/Peers/PeersPin.ts new file mode 100644 index 0000000..7144651 --- /dev/null +++ b/src/components/Peers/PeersPin.ts @@ -0,0 +1,43 @@ +import { useQuery } from "@tanstack/react-query"; +import { PeerGeo, PeerNode } from "./peers.utils"; +import { useEffect } from "react"; + +type Props = { + node: PeerNode + onLoad: (node: PeerNode, geo: PeerGeo) => void +} + +export function PeersPin({ node, onLoad }: Props) { + const { data } = useQuery({ + queryFn: () => { + const [ip] = node.address.split(":"); + + return fetch(import.meta.env.VITE_GEO_IP_URL + "/json?ip=" + ip).then( + (res) => res.json() + ); + }, + queryKey: ["peers", node.address], + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // We can cache the data at Infinity because the relation between + // country and ip is fixed + staleTime: Infinity, + + // Don't expect something new when coming back to the UI + refetchOnWindowFocus: false, + + refetchOnMount: false, + }); + + useEffect(() => { + if (data) { + onLoad(node, data) + } + }, [data, onLoad, node]) + + + return "" +} \ No newline at end of file diff --git a/src/components/Peers/PeersQuality.css b/src/components/Peers/PeersQuality.css new file mode 100644 index 0000000..712f541 --- /dev/null +++ b/src/components/Peers/PeersQuality.css @@ -0,0 +1,10 @@ +.peers-quality { + display: flex; + align-items: center; + gap: 8px; + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; +} diff --git a/src/components/Peers/PeersQuality.tsx b/src/components/Peers/PeersQuality.tsx new file mode 100644 index 0000000..5fb8fa4 --- /dev/null +++ b/src/components/Peers/PeersQuality.tsx @@ -0,0 +1,25 @@ +import "./PeersQuality.css"; +import SuccessCircleIcon from "../../assets/icons/success-circle.svg?react"; +import ErrorCircleIcon from "../../assets/icons/error-circle.svg?react"; + +type Props = { + good: boolean; +}; + +export function PeersQuality({ good }: Props) { + if (good) { + return ( +
    + + Peer connections in good standing. +
    + ); + } + + return ( +
    + + No peer connection active. +
    + ); +} diff --git a/src/components/Peers/peers.utils.test.ts b/src/components/Peers/peers.utils.test.ts new file mode 100644 index 0000000..74cc315 --- /dev/null +++ b/src/components/Peers/peers.utils.test.ts @@ -0,0 +1,74 @@ +import { assert, describe, it } from "vitest"; +import { PeerGeo, PeerUtils } from "./peers.utils"; + +describe("peers", () => { + it("sorts by boolean", async () => { + const a = { nodeId: "a", peerId: "", record: "", address: "", seen: false } + const b = { nodeId: "a", peerId: "", record: "", address: "", seen: true } + const items = [a, b,] + + const descSorted = items.slice().sort(PeerUtils.sortByBoolean("desc")) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(PeerUtils.sortByBoolean("asc")) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("sorts by country", async () => { + const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false } + const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true } + + const table = { + "127.0.0.1": { + country: "United States" + } as PeerGeo, + "127.0.0.2": { + country: "France" + } as PeerGeo, + } + + const items = [a, b,] + + const descSorted = items.slice().sort(PeerUtils.sortByCountry("desc", table)) + + assert.deepEqual(descSorted, [b, a]); + + const ascSorted = items.slice().sort(PeerUtils.sortByCountry("asc", table)) + + assert.deepEqual(ascSorted, [a, b]); + }); + + it("adds a new pin", async () => { + const latLng = { latitude: 0, longitude: 0 } as any + const values = PeerUtils.incPin([], latLng) + + assert.deepEqual(values, [[latLng, 1]]); + }); + + it("increments an existing pin", async () => { + const latLng = { lat: 0, lng: 0 } as any + const values = PeerUtils.incPin([[latLng, 1]], latLng) + + assert.deepEqual(values, [[latLng, 2]]); + }); + + it("count active peers nodes", async () => { + const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false } + const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true } + + assert.equal(PeerUtils.countActives([a, b]), 1) + }); + + it("calculates active peers nodes degrees", async () => { + const a = { nodeId: "a", peerId: "", record: "", address: "127.0.0.1", seen: false } + const b = { nodeId: "a", peerId: "", record: "", address: "127.0.0.2", seen: true } + + assert.equal(PeerUtils.calculareDegrees([a, b]), 90) + }); + + it("returns the country flag", async () => { + assert.equal(PeerUtils.geCountryEmoji("FR"), "🇫🇷") + }); +}) \ No newline at end of file diff --git a/src/components/Peers/peers.utils.ts b/src/components/Peers/peers.utils.ts new file mode 100644 index 0000000..bfd6c05 --- /dev/null +++ b/src/components/Peers/peers.utils.ts @@ -0,0 +1,70 @@ +import { TabSortState } from "@codex-storage/marketplace-ui-components"; + +export type PeerNode = { + nodeId: string; + peerId: string; + record: string; + address: string; + seen: boolean; +}; + +export type PeerGeo = { + latitude: number + longitude: number + country: string + country_iso: string +} + +export type PeerSortFn = (a: PeerNode, b: PeerNode) => number; + +export const PeerUtils = { + sortByBoolean: (state: TabSortState) => (a: PeerNode, b: PeerNode) => { + const order = state === "desc" ? 1 : -1; + return a?.seen === b?.seen ? 0 : b?.seen ? order : -order; + }, + + sortByCountry: (state: TabSortState, ipTable: Record) => + (a: PeerNode, b: PeerNode) => { + const [ipA = ""] = a.address.split(":") + const [ipB = ""] = b.address.split(":") + const countryA = ipTable[ipA].country || ""; + const countryB = ipTable[ipB].country || ""; + + return state === "desc" + ? countryA.localeCompare(countryB) + : countryB.localeCompare(countryA); + }, + + /** + * Increments the number of pin for a location + */ + incPin(val: [PeerNode & PeerGeo, number][], pin: PeerNode & PeerGeo): [PeerNode & PeerGeo, number][] { + const [, quantity = 0] = + val.find(([p]) => p.latitude === pin.latitude && p.longitude == pin.longitude) || []; + const rest = val.filter(([p]) => p.latitude !== pin.latitude || p.longitude !== pin.longitude) + return [...rest, [pin, quantity + 1]]; + }, + + countActives: (peers: PeerNode[]) => + peers.reduce((acc, cur) => acc + (cur.seen ? 1 : 0), 0) || 0, + + calculareDegrees: (peers: PeerNode[]) => { + const actives = PeerUtils.countActives(peers); + const total = peers.length || 1; + + return (actives / total) * 180 + }, + + isGoodQuality(actives: number) { + return actives > 0 + }, + + geCountryEmoji: (countryCode: string) => { + const codePoints = countryCode + .toUpperCase() + .split("") + .map((char) => 127397 + char.charCodeAt(0)); + return String.fromCodePoint(...codePoints); + } + +} diff --git a/src/components/Peers/types.ts b/src/components/Peers/types.ts deleted file mode 100644 index a3e1bea..0000000 --- a/src/components/Peers/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type PeerPin = { - lat: number; - lng: number; -}; \ No newline at end of file diff --git a/src/components/Purchase/PurchasesTable.tsx b/src/components/Purchase/PurchasesTable.tsx new file mode 100644 index 0000000..e8553a1 --- /dev/null +++ b/src/components/Purchase/PurchasesTable.tsx @@ -0,0 +1,128 @@ +import { + Cell, + Row, + Spinner, + Table, + TabSortState, +} from "@codex-storage/marketplace-ui-components"; +import { Times } from "../../utils/times"; +import { useState } from "react"; +import { FileCell } from "../../components/FileCellRender/FileCell"; +import { useData } from "../../hooks/useData"; +import { useQuery } from "@tanstack/react-query"; +import { CodexSdk } from "../../sdk/codex"; +import { Promises } from "../../utils/promises"; +import { CodexPurchase } from "@codex-storage/sdk-js"; +import { TruncateCell } from "../TruncateCell/TruncateCell"; +import { CustomStateCellRender } from "../CustomStateCellRender/CustomStateCellRender"; +import { PurchaseUtils } from "./purchase.utils"; + +type SortFn = (a: CodexPurchase, b: CodexPurchase) => number; + +export function PurchasesTable() { + const content = useData(); + const { data, isPending } = useQuery({ + queryFn: () => + CodexSdk.marketplace() + .purchases() + .then((s) => Promises.rejectOnError(s)), + queryKey: ["purchases"], + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + initialData: [], + + // Throw the error to the error boundary + throwOnError: true, + }); + + // const onMetadata = ( + // requestId: string, + // { uploadedAt }: { uploadedAt: number } + // ) => { + // setMetadata((m) => ({ ...m, [requestId]: uploadedAt })); + // setSortFn(() => + // PurchaseUtils.sortByUploadedAt("desc", { + // ...metadata, + // [requestId]: uploadedAt, + // }) + // ); + // }; + + const [sortFn, setSortFn] = useState(() => + PurchaseUtils.sortByDuration("desc") + ); + + const onSortByDuration = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByDuration(state)); + + const onSortByReward = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByReward(state)); + + const onSortByState = (state: TabSortState) => + setSortFn(() => PurchaseUtils.sortByState(state)); + + // const onSortByUploadedAt = (state: TabSortState) => + // setSortFn(() => PurchaseUtils.sortByUploadedAt(state, metadata)); + + const headers = [ + ["file"], + ["request id"], + ["duration", onSortByDuration], + ["slots"], + ["reward", onSortByReward], + ["proof probability"], + ["state", onSortByState], + ] satisfies [string, ((state: TabSortState) => void)?][]; + + const sorted = sortFn ? [...data].sort(sortFn) : data; + + const rows = sorted.map((p, index) => { + const r = p.request; + const ask = p.request.ask; + const duration = parseInt(p.request.ask.duration, 10); + const pf = parseInt(p.request.ask.proofProbability, 10); + + return ( + , + , + {Times.pretty(duration)}, + {ask.slots.toString()}, + {ask.reward + " CDX"}, + {pf.toString()}, + , + ]}> + ); + }); + + if (isPending) { + return ( +
    + +
    + ); + } + + return ( + <> +
    + + ); +} diff --git a/src/components/Purchase/purchase.utils.ts b/src/components/Purchase/purchase.utils.ts new file mode 100644 index 0000000..3005d8b --- /dev/null +++ b/src/components/Purchase/purchase.utils.ts @@ -0,0 +1,42 @@ +import { TabSortState } from "@codex-storage/marketplace-ui-components" +import { CodexPurchase } from "@codex-storage/sdk-js" + +export const PurchaseUtils = { + sortById: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => { + + return state === "desc" + ? b.requestId + .toLocaleLowerCase() + .localeCompare(a.requestId.toLocaleLowerCase()) + : a.requestId + .toLocaleLowerCase() + .localeCompare(b.requestId.toLocaleLowerCase()) + }, + sortByState: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? b.state + .toLocaleLowerCase() + .localeCompare(a.state.toLocaleLowerCase()) + : a.state + .toLocaleLowerCase() + .localeCompare(b.state.toLocaleLowerCase()) + , + sortByDuration: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? Number(b.request.ask.duration) - Number(a.request.ask.duration) + : Number(a.request.ask.duration) - Number(b.request.ask.duration) + , + sortByReward: (state: TabSortState) => + (a: CodexPurchase, b: CodexPurchase) => state === "desc" + ? Number(b.request.ask.reward) - Number(a.request.ask.reward) + : Number(a.request.ask.reward) - Number(b.request.ask.reward) + , + sortByUploadedAt: (state: TabSortState, table: Record) => + (a: CodexPurchase, b: CodexPurchase) => { + return state === "desc" + ? (table[b.requestId] || 0) - (table[a.requestId] || 0) + : (table[a.requestId] || 0) - (table[b.requestId] || 0) + } + , +} \ No newline at end of file diff --git a/src/components/RequireAssitance/AssistanceImage.css b/src/components/RequireAssitance/AssistanceImage.css new file mode 100644 index 0000000..ad742d4 --- /dev/null +++ b/src/components/RequireAssitance/AssistanceImage.css @@ -0,0 +1,7 @@ +.assistance-img { + position: absolute; + right: 0px; + top: 0px; + width: auto; + border-top-right-radius: 16px; +} diff --git a/src/components/RequireAssitance/AssistanceImage.tsx b/src/components/RequireAssitance/AssistanceImage.tsx new file mode 100644 index 0000000..2fca9ca --- /dev/null +++ b/src/components/RequireAssitance/AssistanceImage.tsx @@ -0,0 +1,26 @@ +import { classnames } from "../../utils/classnames"; +import "./AssistanceImage.css"; + +export function AssistanceImage() { + return ( + + + + assistance Image + + ); +} diff --git a/src/components/RequireAssitance/RequireAssitance.css b/src/components/RequireAssitance/RequireAssitance.css new file mode 100644 index 0000000..db48d0e --- /dev/null +++ b/src/components/RequireAssitance/RequireAssitance.css @@ -0,0 +1,37 @@ +.require-assistance { + background-color: #0a1410; + box-sizing: border-box; + padding: 16px; + border-radius: 16px; + border-left: 4px solid #6ccc93; + height: 144px; + display: flex; + flex-direction: column; + justify-content: space-around; + width: 500px; + flex: 1 1 auto; + cursor: pointer; + text-decoration: none; + position: relative; + max-width: 508px; + + h5 { + font-family: Azeret Mono; + font-size: 10px; + font-weight: 400; + line-height: 20px; + text-align: left; + color: #6ccc93; + } + + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 600; + line-height: 19.36px; + letter-spacing: 0.01em; + text-align: left; + color: #7bfbaf; + margin-bottom: 4px; + } +} diff --git a/src/components/RequireAssitance/RequireAssitance.tsx b/src/components/RequireAssitance/RequireAssitance.tsx new file mode 100644 index 0000000..ae74d4a --- /dev/null +++ b/src/components/RequireAssitance/RequireAssitance.tsx @@ -0,0 +1,23 @@ +import "./RequireAssitance.css"; +import DiscordIcon from "../../assets/icons/discord.svg?react"; +import { AssistanceImage } from "./AssistanceImage"; + +export function RequireAssitance() { + return ( + +
    Require Assistance?
    + + + +
    +
    Join Codex Discord
    + Get direct access to the Core Team. +
    + + +
    + ); +} diff --git a/src/components/StorageRequestSetup/StorageRequestAvailability.css b/src/components/StorageRequestSetup/StorageRequestAvailability.css index 3ce11a4..8768c3d 100644 --- a/src/components/StorageRequestSetup/StorageRequestAvailability.css +++ b/src/components/StorageRequestSetup/StorageRequestAvailability.css @@ -3,21 +3,3 @@ display: flex; align-items: center; } - -.storageRequestFileChooser-dropdown-success { - animation-duration: 3s; - animation-name: cid-selected; - border-radius: var(--codex-border-radius); -} - -@keyframes cid-selected { - 0% { - box-shadow: 0 0 0 0px var(--codex-color-primary-variant); - } - 50% { - box-shadow: 0 0 0 3px var(--codex-color-primary-variant); - } - 100% { - box-shadow: 0 0 0 0px var(--codex-color-primary-variant); - } -} diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.css b/src/components/StorageRequestSetup/StorageRequestCreate.css index 09e57a1..2b746c1 100644 --- a/src/components/StorageRequestSetup/StorageRequestCreate.css +++ b/src/components/StorageRequestSetup/StorageRequestCreate.css @@ -1,5 +1,35 @@ -@media (min-width: 801px) { - .storageRequestCreate { - min-width: 700px; +.storage-request { + .modal dialog { + width: 80%; + max-width: 100% !important; + } + + header { + display: flex; + align-items: flex-start; + gap: 8px; + margin-bottom: 16px; + + small { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: #969696; + } + } + + h6 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + } + + .upload-file { + margin-bottom: 16px; } } diff --git a/src/components/StorageRequestSetup/StorageRequestCreate.tsx b/src/components/StorageRequestSetup/StorageRequestCreate.tsx index a352b32..8d63361 100644 --- a/src/components/StorageRequestSetup/StorageRequestCreate.tsx +++ b/src/components/StorageRequestSetup/StorageRequestCreate.tsx @@ -16,26 +16,27 @@ import { useStorageRequestMutation } from "./useStorageRequestMutation"; import { Plus } from "lucide-react"; import "./StorageRequestCreate.css"; import { StorageRequestError } from "./StorageRequestError"; +import PurchaseIcon from "../../assets/icons/purchase.svg?react"; const CONFIRM_STATE = 2; const defaultStorageRequest: StorageRequest = { cid: "", - availabilityUnit: "days", + availabilityUnit: "months", availability: 1, tolerance: 1, proofProbability: 1, nodes: 3, reward: 10, collateral: 10, - expiration: 300, + expiration: 5, }; export function StorageRequestCreate() { const [storageRequest, setStorageRequest] = useState( defaultStorageRequest ); - const steps = useRef(["File", "Criteria", "Success"]); + const steps = useRef(["Select File", "Select Request Criteria", "Success"]); const { state, dispatch } = useStepperReducer(); const { mutateAsync, error } = useStorageRequestMutation(dispatch, state); @@ -87,7 +88,7 @@ export function StorageRequestCreate() { mutateAsync({ ...rest, duration: Times.toSeconds(availability, availabilityUnit), - expiry: expiration, + expiry: expiration * 60, }); } else { dispatch({ @@ -117,15 +118,21 @@ export function StorageRequestCreate() { const nextLabel = state.step === steps.current.length - 1 ? "Finish" : "Next"; return ( - <> +
    ); } diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.css b/src/components/StorageRequestSetup/StorageRequestFileChooser.css index dfd0d5d..54746ff 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.css +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.css @@ -1,3 +1,35 @@ +.file-chooser { + .input { + width: 100%; + } + + hr { + flex: 1; + border: 1px solid #96969633; + margin-top: 24px; + margin-bottom: 24px; + + + span { + font-family: Inter; + font-size: 11px; + font-weight: 500; + line-height: 12px; + letter-spacing: 0.02em; + text-align: left; + color: #696969; + } + } + + .upload { + margin-top: 16px; + margin-bottom: 16px; + } + + input { + width: 100%; + } +} + .storageRequestFileChooser-hr { margin: 1.5rem 0; } @@ -5,18 +37,3 @@ .storageRequestFileChooser-input { width: 100%; } - -.storageRequestFileChooser-separator { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 0; -} - -.storageRequestFileChooser-or { - font-size: 1.25rem; -} - -.storageRequestFileChooser-dropdown .dropdown-input { - width: 100%; -} diff --git a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx index e5f7321..dae27c7 100644 --- a/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx +++ b/src/components/StorageRequestSetup/StorageRequestFileChooser.tsx @@ -1,7 +1,6 @@ import { CodexSdk } from "../../sdk/codex"; import "./StorageRequestFileChooser.css"; import { ChangeEvent, useEffect } from "react"; -import { classnames } from "../../utils/classnames"; import { Dropdown, DropdownOption, @@ -11,6 +10,8 @@ import { import { useData } from "../../hooks/useData"; import { StorageRequestComponentProps } from "./types"; import { useQueryClient } from "@tanstack/react-query"; +import ChooseCidIcon from "../../assets/icons/choose-cid.svg?react"; +import UploadIcon from "../../assets/icons/upload.svg?react"; export function StorageRequestFileChooser({ storageRequest, @@ -48,56 +49,49 @@ export function StorageRequestFileChooser({ const options = files.map((f) => { return { - Icon: () => , - title: f.manifest.filename, + Icon: () => , + title: f.manifest.filename || "", subtitle: f.cid, }; }) || []; return ( - <> - Choose a CID - - +
    +
    + +
    Choose a CID
    +
    -
    -
    - OR -
    +
    +
    + OR +
    - -
    - Upload a file -
    - - The CID will be automatically copied after your upload. - -
    +
    + +
    Upload
    +
    - +
    ); } diff --git a/src/components/StorageRequestSetup/StorageRequestReview.css b/src/components/StorageRequestSetup/StorageRequestReview.css index 33ea913..24b8a96 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.css +++ b/src/components/StorageRequestSetup/StorageRequestReview.css @@ -1,110 +1,135 @@ -.storageRequestReview-hr { - margin-bottom: 1.5rem; - margin-top: 0rem; -} +.request-review { + > header { + border-bottom: 1px solid #96969633; + padding-bottom: 16px; -.storageRequestReview-numbers { - display: grid; - gap: 0.5rem; -} - -.storageRequestReview-range { - margin: 0.5rem 0 1rem 0; - font-size: 0.85rem; -} - -.storageRequestReview-alert .alert-message { - font-size: 0.9rem; -} - -.storageRequestReview-range--disabled .range { - opacity: 0.5; -} - -.storageRequestReview-presets { - display: flex; - padding: 0 0.5rem 2rem 0.5rem; - gap: 1rem; -} - -.storageRequestReview-presets-blocs { - display: flex; - flex: 1; - gap: 0.5rem; -} - -.storageRequestReview-presets-bloc { - flex: 1; - border-radius: var(--codex-border-radius); - background-color: rgb(56 56 56); - align-items: center; - padding: 1rem; - align-items: center; - justify-content: center; - text-align: center; - display: flex; - flex-direction: column; - gap: 0.5rem; - transition: opacity 0.35s; - cursor: pointer; - border: 1px solid transparent; -} - -.storageRequest-price { - display: flex; - justify-content: center; -} - -.storageRequestReview-presets-title { - display: flex; - flex-direction: column; - justify-content: center; -} - -.storageRequestReview-presets-bloc:not( - .storageRequestReview-presets--selected - ):hover { - border: 1px solid var(--codex-border-color); -} - -.storageRequestReview-presets--selected { - border: 1px solid var(--codex-color-primary); -} - -.storageRequestReview-alert { - display: flex; - gap: 1rem; - align-items: flex-start; -} - -.storageRequestReview-expiration { - min-width: 33%; -} - -@media (max-width: 800px) { - .storageRequestReview-numbers { - grid-template-columns: 1fr; + div { + line-height: 8px; + } } - .storageRequestReview-presets { - flex-direction: column; + .presets { + display: flex; + gap: 16px; + margin-bottom: 16px; + + > div { + height: 74px; + position: relative; + } + + > div:first-child { + display: flex; + justify-content: center; + align-items: center; + gap: 8px; + + span { + display: block; + font-family: Inter; + font-size: 10px; + font-weight: 400; + line-height: 12.1px; + letter-spacing: 0.01em; + text-transform: uppercase; + } + + small { + font-family: Inter; + font-size: 14px; + font-weight: 500; + line-height: 18px; + letter-spacing: -0.011em; + color: #969696cc; + } + } + + > div:nth-child(n + 2) { + --codex-preset-border-color: #494949; + --codex-preset-color: #969696; + border: 1px solid var(--codex-preset-border-color); + border-radius: 12px; + padding: 16px; + flex: 1; + box-sizing: border-box; + overflow: hidden; + display: flex; + align-items: flex-end; + cursor: pointer; + transition: 0.35s box-shadow; + + &:hover { + box-shadow: 0 0 0 2px var(--codex-preset-border-color); + } + + &[aria-selected] { + --codex-preset-border-color: #6fcb94; + --codex-preset-color: #6fcb94; + } + + svg { + position: absolute; + right: 0; + top: 0; + color: var(--codex-preset-color); + } + + span { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: var(--codex-preset-color); + + + span { + background: #6fcb9433; + padding: 2px 8px; + font-family: Inter; + font-size: 11px; + font-weight: 500; + line-height: 12px; + letter-spacing: 0.02em; + color: #6fcb94; + border-radius: 16px; + margin-left: 4px; + } + } + } } - .storageRequestReview-presets-blocs { - flex-direction: column; + .row { + border-top: 1px solid #96969633; + margin-top: 16px; + margin-bottom: 16px; + padding-top: 16px; + gap: 8px; } - .storageRequestReview-alert { - flex-direction: column; + .grid { + display: grid; + gap: 16px; + + @media (max-width: 800px) { + & { + grid-template-columns: 1fr; + } + } + + @media (min-width: 801px) { + & { + grid-template-columns: 1fr 1fr 1fr; + } + } } - .storageRequestReview-expiration { - min-width: 100%; - } -} - -@media (min-width: 801px) { - .storageRequestReview-numbers { - grid-template-columns: 1fr 1fr 1fr; + footer { + display: flex; + gap: 16px; + margin-bottom: 16px; + + > * { + flex: 1; + } } } diff --git a/src/components/StorageRequestSetup/StorageRequestReview.tsx b/src/components/StorageRequestSetup/StorageRequestReview.tsx index ab1ae8f..2c18314 100644 --- a/src/components/StorageRequestSetup/StorageRequestReview.tsx +++ b/src/components/StorageRequestSetup/StorageRequestReview.tsx @@ -3,12 +3,14 @@ import "./StorageRequestReview.css"; import { Alert } from "@codex-storage/marketplace-ui-components"; import { CardNumbers } from "../CardNumbers/CardNumbers"; import { FileWarning } from "lucide-react"; -import { classnames } from "../../utils/classnames"; -import { - AvailabilityUnit, - StorageRequest, - StorageRequestComponentProps, -} from "./types"; +import { StorageRequest, StorageRequestComponentProps } from "./types"; +import DurabilityIcon from "../../assets/icons/durability.svg?react"; +import AlphaIcon from "../../assets/icons/alpha.svg?react"; +import PresetIcon from "../../assets/icons/preset.svg?react"; +import CommitmentIcon from "../../assets/icons/commitment.svg?react"; +import RequestDurationIcon from "../../assets/icons/request-duration.svg?react"; +import { attributes } from "../../utils/attributes"; +import { Strings } from "../../utils/strings"; type Durability = { nodes: number; @@ -32,14 +34,14 @@ const findDurabilityIndex = (d: Durability) => { return durabilities.findIndex((d) => JSON.stringify(d) === s); }; -const units = ["days", "minutes", "hours", "days", "months", "years"]; +// const units = ["days", "minutes", "hours", "days", "months", "years"]; export function StorageRequestReview({ dispatch, onStorageRequestChange, storageRequest, }: StorageRequestComponentProps) { - const [durability, setDurability] = useState(1); + const [durability, setDurability] = useState(2); const isInvalidConstrainst = useCallback( (nodes: number, tolerance: number) => { @@ -67,10 +69,12 @@ export function StorageRequestReview({ const onUpdateDurability = (data: Partial) => { onStorageRequestChange(data); + const merge = { ...storageRequest, ...data }; + const index = findDurabilityIndex({ - nodes: storageRequest.nodes, - tolerance: storageRequest.tolerance, - proofProbability: storageRequest.proofProbability, + nodes: merge.nodes, + tolerance: merge.tolerance, + proofProbability: merge.proofProbability, }); setDurability(index + 1); @@ -124,7 +128,7 @@ export function StorageRequestReview({ }; const isInvalidAvailability = (data: string) => { - const [value, unit = "days"] = data.split(" "); + const [value] = data.split(" "); const error = isInvalidNumber(value); @@ -136,9 +140,9 @@ export function StorageRequestReview({ // unit += "s"; // } - if (!units.includes(unit)) { - return "Invalid unit must one of: minutes, hours, days, months, years"; - } + // if (!units.includes(unit)) { + // return "Invalid unit must one of: minutes, hours, days, months, years"; + // } return ""; }; @@ -156,7 +160,7 @@ export function StorageRequestReview({ onUpdateDurability({ proofProbability: Number(value) }); const onAvailabilityChange = (value: string) => { - const [availability, availabilityUnit = "days"] = value.split(" "); + const [availability] = value.split(" "); // if (!availabilityUnit.endsWith("s")) { // availabilityUnit += "s"; @@ -164,7 +168,7 @@ export function StorageRequestReview({ onStorageRequestChange({ availability: Number(availability), - availabilityUnit: availabilityUnit as AvailabilityUnit, + availabilityUnit: "months", }); }; @@ -189,157 +193,148 @@ export function StorageRequestReview({ // return data.availabilityUnit; // }; - const availability = `${storageRequest.availability} ${storageRequest.availabilityUnit}`; + const availability = storageRequest.availability; return ( -
    - Durability -
    - - - -
    - -
    -
    - Define your durability profile -

    +

    +
    + +
    +
    Define your Durability Profile
    + Select the appropriate level of data storage reliability ensuring your information is protected and accessible. -

    +
    -
    -
    onDurabilityChange(0)} - className={classnames( - ["storageRequestReview-presets-bloc"], - [ - "storageRequestReview-presets--selected", - durability <= 0 || durability > 3, - ] - )}> -
    - +
    +
    +
    +
    + +
    + Durability + Suggested Defaults
    -

    Custom

    onDurabilityChange(1)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 1] - )}> -
    - -
    -

    Low

    + {...attributes({ + "aria-selected": durability <= 0 || durability > 3, + })} + onClick={() => onDurabilityChange(0)}> + Custom +
    onDurabilityChange(2)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 2] - )}> -
    - -
    -

    Medium

    + {...attributes({ + "aria-selected": durability == 1, + })} + onClick={() => onDurabilityChange(1)}> + Low +
    onDurabilityChange(3)} - className={classnames( - ["storageRequestReview-presets-bloc"], - ["storageRequestReview-presets--selected", durability === 3] - )}> -
    - -
    -

    High

    + {...attributes({ + "aria-selected": durability == 2, + })} + onClick={() => onDurabilityChange(2)}> + Medium + Recommanded + +
    +
    onDurabilityChange(3)}> + High +
    -
    - {/* */} +
    + + + +
    - Commitment +
    + +
    Commitment
    +
    -
    - - - -
    - {/* */} -
    +
    + + + +
    -
    +
    + +
    Request Duration
    +
    -
    - - } - title="Warning" - variant="warning" - className="storageRequestReview-alert"> - If no suitable hosts are found for the CID {storageRequest.cid}{" "} - matching your storage requirements, you will incur a charge a small - amount of tokens. - -
    +
    + + } + title="Warning" + variant="warning" + className="storageRequestReview-alert"> + 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. + +
    +
    ); } diff --git a/src/components/SuccessIcon/SuccessIcon.tsx b/src/components/SuccessIcon/SuccessIcon.tsx index e53eab6..ea06811 100644 --- a/src/components/SuccessIcon/SuccessIcon.tsx +++ b/src/components/SuccessIcon/SuccessIcon.tsx @@ -1,10 +1,9 @@ -import { SimpleText } from "@codex-storage/marketplace-ui-components"; import { CircleCheck } from "lucide-react"; export function SuccessIcon() { return ( - + - + ); } diff --git a/src/components/UploadCard/UploadCard.tsx b/src/components/UploadCard/UploadCard.tsx new file mode 100644 index 0000000..a437007 --- /dev/null +++ b/src/components/UploadCard/UploadCard.tsx @@ -0,0 +1,23 @@ +import { Upload } from "@codex-storage/marketplace-ui-components"; +import { CodexSdk } from "../../sdk/codex"; +import { useQueryClient } from "@tanstack/react-query"; +import UploadIcon from "../../assets/icons/upload.svg?react"; + +export function UploadCard() { + const queryClient = useQueryClient(); + + const onSuccess = () => { + queryClient.invalidateQueries({ queryKey: ["cids"] }); + }; + + return ( +
    + } + /> +
    + ); +} diff --git a/src/components/UserInfo/UserInfo.css b/src/components/UserInfo/UserInfo.css new file mode 100644 index 0000000..94238e5 --- /dev/null +++ b/src/components/UserInfo/UserInfo.css @@ -0,0 +1,47 @@ +.user-info { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 16px; + + @media (min-width: 1000px) { + & { + flex-direction: row; + gap: 32px; + align-items: center; + } + } + + .emoji { + position: relative; + + aside { + position: absolute; + top: -140px; + left: 0px; + z-index: 1; + + @media (min-width: 1000px) { + & { + left: 116px; + } + } + } + + .input input { + width: 64px; + text-align: center; + cursor: pointer; + } + } + + .input input { + width: 100%; + + @media (min-width: 1000px) { + & { + width: inherit; + } + } + } +} diff --git a/src/components/UserInfo/UserInfo.tsx b/src/components/UserInfo/UserInfo.tsx new file mode 100644 index 0000000..6580da7 --- /dev/null +++ b/src/components/UserInfo/UserInfo.tsx @@ -0,0 +1,84 @@ +import { ChangeEvent, useState } from "react"; +import "./UserInfo.css"; +import { Input } from "@codex-storage/marketplace-ui-components"; +import EmojiPicker, { + EmojiClickData, + EmojiStyle, + Theme, +} from "emoji-picker-react"; +import { WebStorage } from "../../utils/web-storage"; + +type Props = { + onNameChange?: (value: string) => void; +}; + +export function UserInfo({ onNameChange }: Props) { + const [displayName, setDisplayName] = useState( + WebStorage.onBoarding.getDisplayName() + ); + const [emoji, setEmoji] = useState(WebStorage.onBoarding.getEmoji()); + const [areEmojiVisible, setAreEmojiVisible] = useState(false); + + const onDisplayNameChange = (e: ChangeEvent) => { + const value = e.currentTarget.value; + WebStorage.onBoarding.setDisplayName(value); + setDisplayName(value); + onNameChange?.(value); + }; + + const onDisplayEmoji = () => setAreEmojiVisible(!areEmojiVisible); + + const onEmojiClick = (emojiData: EmojiClickData) => { + setEmoji(emojiData.emoji); + WebStorage.onBoarding.setEmoji(emojiData.emoji); + setAreEmojiVisible(false); + }; + + return ( +
    +
    + {areEmojiVisible && ( + + )} +
    + +
    +
    + +
    + +
    +
    + ); +} diff --git a/src/components/Versions/Versions.css b/src/components/Versions/Versions.css new file mode 100644 index 0000000..4e43756 --- /dev/null +++ b/src/components/Versions/Versions.css @@ -0,0 +1,30 @@ +.versions { + display: flex; + gap: 32px; + align-items: flex-start; + + > div { + width: 50px; + text-align: right; + + p { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + color: #99a0ae; + } + + small { + font-family: Inter; + font-size: 10px; + font-weight: 400; + line-height: 12.1px; + letter-spacing: 0.01em; + color: white; + text-transform: uppercase; + white-space: nowrap; + } + } +} diff --git a/src/components/Versions/Versions.tsx b/src/components/Versions/Versions.tsx new file mode 100644 index 0000000..23e0357 --- /dev/null +++ b/src/components/Versions/Versions.tsx @@ -0,0 +1,28 @@ +import { useDebug } from "../../hooks/useDebug"; +import "./Versions.css"; +import { VersionsUtil } from "./versions.utils"; +import AlphaIcon from "../../assets/icons/alpha.svg?react"; +import AlphaText from "../../assets/icons/alphatext.svg?react"; + +const throwOnError = false; + +export function Versions() { + const debug = useDebug(throwOnError); + + const version = VersionsUtil.clientVersion(debug.data?.codex.version); + + return ( +
    + +
    +

    Client

    + VER. {version} +
    +
    +

    Vault

    + VER. {VersionsUtil.codexVersion()} + +
    +
    + ); +} diff --git a/src/components/Versions/versions.utils.test.ts b/src/components/Versions/versions.utils.test.ts new file mode 100644 index 0000000..56a5115 --- /dev/null +++ b/src/components/Versions/versions.utils.test.ts @@ -0,0 +1,9 @@ +import { assert, describe, it } from "vitest"; +import { VersionsUtil } from "./versions.utils"; + +describe("versions", () => { + it("gets the last client version", async () => { + const version = "v0.1.0\nv0.1.1\nv0.1.2\nv0.1.3\nv0.1.4\nv0.1.5\nv0.1.6\nv0.1.7" + assert.equal(VersionsUtil.clientVersion(version), "v0.1.7") + }) +}) \ No newline at end of file diff --git a/src/components/Versions/versions.utils.ts b/src/components/Versions/versions.utils.ts new file mode 100644 index 0000000..151ecc3 --- /dev/null +++ b/src/components/Versions/versions.utils.ts @@ -0,0 +1,8 @@ +export const VersionsUtil = { + codexVersion: () => import.meta.env.PACKAGE_VERSION, + + clientVersion: (version: string | undefined) => { + const parts = version?.split("\n") || [""]; + return parts[parts.length - 1]; + } +} \ No newline at end of file diff --git a/src/components/WalletLogin/WalletLogin.css b/src/components/WalletLogin/WalletLogin.css new file mode 100644 index 0000000..4ac3e13 --- /dev/null +++ b/src/components/WalletLogin/WalletLogin.css @@ -0,0 +1,64 @@ +.wallet-login { + padding: 10px; + display: flex; + align-items: center; + gap: 16px; + background-color: #252525; + + & { + filter: grayscale(30); + transition: filter 0.5s; + } + + &:hover { + filter: none; + } + + div { + > p { + font-family: Inter; + font-size: 8px; + font-weight: 700; + text-transform: uppercase; + color: #6e6e6e; + display: block; + font-size: 10px; + } + } + + var { + font-family: Inter; + font-size: 12px; + font-weight: 700; + color: #ffffff99; + font-style: normal; + font-size: 16px; + } + + footer { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; + + p { + font-family: Inter; + font-size: 10px; + font-weight: 700; + line-height: 10px; + text-align: left; + color: #3ee089; + font-style: normal; + } + + a { + font-family: Inter; + font-size: 10px; + font-weight: 700; + line-height: 10px; + color: #6e6e6e; + cursor: pointer; + } + } +} diff --git a/src/components/WalletLogin/WalletLogin.tsx b/src/components/WalletLogin/WalletLogin.tsx new file mode 100644 index 0000000..d622cef --- /dev/null +++ b/src/components/WalletLogin/WalletLogin.tsx @@ -0,0 +1,20 @@ +import { Strings } from "../../utils/strings"; +import "./WalletLogin.css"; + +export function WalletConnect() { + return ( +
    + +
    +

    Mainnet

    + + {Strings.shortId("0x5B3D1D5D5C5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D")} + + +
    +
    + ); +} diff --git a/src/components/Welcome/Welcome.css b/src/components/Welcome/Welcome.css deleted file mode 100644 index 3530439..0000000 --- a/src/components/Welcome/Welcome.css +++ /dev/null @@ -1,35 +0,0 @@ -.welcome { - border-radius: var(--codex-border-radius); - border: 1px solid var(--codex-border-color); - background-color: var(--codex-background-secondary); - padding: 1rem 1.5rem; - display: flex; - align-items: flex-start; - flex-direction: column; - flex: 1; -} - -.welcome-disclaimer { - margin: 1rem 0; -} - -.welcome-title { - font-weight: bold; - font-size: 1.125rem; - line-height: 1.75rem; - margin-bottom: 0.75rem; -} - -.welcome-body { - flex: 1; -} - -.welcome-link { - display: flex; - align-items: center; - color: var(--codex-color-primary); -} - -.welcome-link:hover { - text-decoration: underline; -} diff --git a/src/components/Welcome/Welcome.tsx b/src/components/Welcome/Welcome.tsx deleted file mode 100644 index 0c31579..0000000 --- a/src/components/Welcome/Welcome.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SimpleText } from "@codex-storage/marketplace-ui-components"; -import "./Welcome.css"; -import { Link } from "@tanstack/react-router"; -import { ChevronRight } from "lucide-react"; - -export function Welcome() { - return ( -
    -

    Welcome to Codex Marketplace

    -
    - - Begin your journey with Codex by uploading new files for testing. - Experience the power of our decentralized data storage platform and - explore its features. Your feedback is invaluable as we continue to - improve! - -
    - - - Explore more content - -
    - ); -} diff --git a/src/components/Welcome/WelcomeCard.css b/src/components/Welcome/WelcomeCard.css new file mode 100644 index 0000000..baa2061 --- /dev/null +++ b/src/components/Welcome/WelcomeCard.css @@ -0,0 +1,72 @@ +.welcome-card { + display: flex; + flex-direction: column; + flex: 1 1 50%; + min-width: 420px; + + .card { + position: relative; + overflow: hidden; + } + + > div { + padding: 16px; + background-color: #141414; + flex: 1; + display: flex; + flex-direction: column; + } + + main { + max-width: 400px; + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; + + h6 { + font-family: Inter; + font-size: 20px; + font-weight: 400; + line-height: 24.2px; + color: var(--codex-color-primary); + } + + p { + margin-top: 16px; + margin-bottom: 32px; + font-family: Azeret Mono; + font-size: 12px; + font-weight: 400; + line-height: 14px; + text-align: left; + color: #7f948d; + } + + div { + display: flex; + align-items: center; + justify-content: space-between; + } + + a { + font-family: Inter; + font-size: 16px; + font-weight: 600; + line-height: 19.36px; + letter-spacing: 0.01em; + text-align: left; + color: #7bfbaf; + display: flex; + align-items: center; + + &:nth-child(2) { + gap: 12px; + } + } + } + + footer { + margin-top: 32px; + } +} diff --git a/src/components/Welcome/WelcomeCard.tsx b/src/components/Welcome/WelcomeCard.tsx new file mode 100644 index 0000000..77f6357 --- /dev/null +++ b/src/components/Welcome/WelcomeCard.tsx @@ -0,0 +1,67 @@ +import "./WelcomeCard.css"; +import { Link } from "@tanstack/react-router"; +import { ArrowRight } from "lucide-react"; +import { Alert } from "@codex-storage/marketplace-ui-components"; +import { useEffect, useRef, useState } from "react"; +import { classnames } from "../../utils/classnames"; +import Logotype from "../../assets/icons/logotype.svg?react"; +import Logo from "../../assets/icons/logo.svg?react"; +import DiscordIcon from "../../assets/icons/discord.svg?react"; +import WarningIcon from "../../assets/icons/warning.svg?react"; +import { WelcomeImage } from "./WelcomeImage"; + +export function WelcomeCard() { + const ref = useRef(null); + const [clientWidth, setClientWidth] = useState(0); + + useEffect(() => { + const onResize = () => { + setClientWidth(ref.current?.clientWidth || 0); + }; + + window.addEventListener("resize", onResize); + + return () => { + window.removeEventListener("resize", onResize); + }; + }, [setClientWidth]); + + return ( +
    +
    +
    +
    + + +
    +
    +
    + +
    + Begin your journey with Codex by uploading new files for testing. +
    +

    + Experience the power of our decentralized data storage platform and + explore its features. Your feedback is invaluable as we continue to + improve! +

    +
    + + Learn more + + + + Join Codex Discord + +
    +
    +
    + }> + The website and the content herein is not intended for public use + and is for informational and demonstration purposes only. + +
    +
    +
    + ); +} diff --git a/src/components/Welcome/WelcomeImage.css b/src/components/Welcome/WelcomeImage.css new file mode 100644 index 0000000..61c036b --- /dev/null +++ b/src/components/Welcome/WelcomeImage.css @@ -0,0 +1,14 @@ +.welcome-img { + position: absolute; + right: 0px; + top: 0px; + width: auto; + border-top-right-radius: 16px; + transition: + right 0.35s, + right 0.35s; + + &.welcome-img--tiny { + right: -180px; + } +} diff --git a/src/components/Welcome/WelcomeImage.tsx b/src/components/Welcome/WelcomeImage.tsx new file mode 100644 index 0000000..cfac8fb --- /dev/null +++ b/src/components/Welcome/WelcomeImage.tsx @@ -0,0 +1,30 @@ +import { classnames } from "../../utils/classnames"; +import "./WelcomeImage.css"; + +type Props = { + tiny: boolean; +}; + +export function WelcomeImage({ tiny }: Props) { + return ( + + + + Welcome Image + + ); +} diff --git a/src/hooks/port-forwarding.util.ts b/src/hooks/port-forwarding.util.ts new file mode 100644 index 0000000..6833ca9 --- /dev/null +++ b/src/hooks/port-forwarding.util.ts @@ -0,0 +1,27 @@ +import { CodexDebugInfo, SafeValue, CodexError } from "@codex-storage/sdk-js" + +export const PortForwardingUtil = { + check: (port: number) => fetch(import.meta.env.VITE_GEO_IP_URL + "/port/" + port) + .then((res) => res.json()), + + getTcpPort(info: CodexDebugInfo): SafeValue { + if (info.addrs.length === 0) { + return { error: true, data: new CodexError("Not existing address") } + } + + const parts = info.addrs[0].split("/") + + if (parts.length < 2) { + return { error: true, data: new CodexError("Address misformated") } + } + + const port = parseInt(parts[parts.length - 1], 10) + + if (isNaN(port)) { + return { error: true, data: new CodexError("Port misformated") } + } + + return { error: false, data: port } + } + +} \ No newline at end of file diff --git a/src/hooks/useCodexConnection.tsx b/src/hooks/useCodexConnection.tsx new file mode 100644 index 0000000..57fb340 --- /dev/null +++ b/src/hooks/useCodexConnection.tsx @@ -0,0 +1,35 @@ +import { useQuery } from "@tanstack/react-query"; +import { CodexSdk } from "../sdk/codex"; +import { Promises } from "../utils/promises"; + +const report = false; + +export function useCodexConnection() { + const { data, isError, isFetching, refetch } = useQuery({ + queryKey: ["spr"], + queryFn: async () => { + return CodexSdk.node() + .spr() + .then((data) => Promises.rejectOnError(data, report)); + }, + refetchInterval: 5000, + + // No need to retry because we defined a refetch interval + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + // Cache is not useful for the spr endpoint + gcTime: 0, + + throwOnError: false, + }); + + return { enabled: !isError && !!data, isFetching, refetch }; +} diff --git a/src/hooks/useDebug.ts b/src/hooks/useDebug.ts new file mode 100644 index 0000000..909335f --- /dev/null +++ b/src/hooks/useDebug.ts @@ -0,0 +1,31 @@ +import { useQuery } from "@tanstack/react-query"; +import { CodexSdk } from "../sdk/codex"; +import { Promises } from "../utils/promises"; + +export function useDebug(throwOnError: boolean) { + const { data, isError, isPending, refetch, isSuccess, isFetching } = useQuery({ + queryFn: () => + CodexSdk.debug() + .info() + .then((s) => Promises.rejectOnError(s)), + + queryKey: ["debug"], + + // No need to retry because if the connection to the node + // is back again, all the queries will be invalidated. + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + // Throw the error to the error boundary + throwOnError, + }); + + return { data, isPending, isError, isSuccess, refetch, isFetching }; +} diff --git a/src/hooks/usePersistence.tsx b/src/hooks/usePersistence.tsx new file mode 100644 index 0000000..b66d68d --- /dev/null +++ b/src/hooks/usePersistence.tsx @@ -0,0 +1,36 @@ +import { useQuery } from "@tanstack/react-query"; +import { CodexSdk } from "../sdk/codex"; +import { Promises } from "../utils/promises"; + +const report = false; + +export function usePersistence(isCodexOnline: boolean) { + const { data, isError, isFetching, refetch } = useQuery({ + queryKey: [], + queryFn: async () => { + return CodexSdk.marketplace() + .purchases() + .then((data) => Promises.rejectOnError(data, report)); + }, + + refetchInterval: 5000, + + // Enable only when the use has an internet connection + enabled: !!isCodexOnline, + + // No need to retry because we defined a refetch interval + retry: false, + + // The client node should be local, so display the cache value while + // making a background request looks good. + staleTime: 0, + + // Refreshing when focus returns can be useful if a user comes back + // to the UI after performing an operation in the terminal. + refetchOnWindowFocus: true, + + throwOnError: false, + }); + + return { enabled: !isError && !!data, isFetching, refetch }; +} diff --git a/src/hooks/usePortForwarding.tsx b/src/hooks/usePortForwarding.tsx new file mode 100644 index 0000000..f77beb1 --- /dev/null +++ b/src/hooks/usePortForwarding.tsx @@ -0,0 +1,42 @@ +import { useQuery } from "@tanstack/react-query"; +import { Errors } from "../utils/errors"; +import { CodexDebugInfo } from "@codex-storage/sdk-js"; +import { PortForwardingUtil } from "./port-forwarding.util"; + +type PortForwardingResponse = { reachable: boolean }; + +export function usePortForwarding(info: CodexDebugInfo | undefined) { + const { data, isFetching, refetch } = useQuery({ + queryFn: (): Promise => { + const port = PortForwardingUtil.getTcpPort(info!); + if (port.error) { + Errors.report(port); + return Promise.resolve({ reachable: false }); + } else { + return PortForwardingUtil.check(port.data).catch((e) => + Errors.report(e) + ); + } + }, + queryKey: ["port-forwarding"], + + initialData: { reachable: false }, + + // Enable only when the use has an internet connection + enabled: !!info, + + // No need to retry because we provide a retry button + retry: false, + + // The data should not be cached + staleTime: 0, + + // The user may try to change the port forwarding and go back + // to the tab + refetchOnWindowFocus: true, + + throwOnError: false, + }); + + return { enabled: data.reachable, isFetching, refetch }; +} diff --git a/src/index.css b/src/index.css index 82c61e2..a144b1c 100644 --- a/src/index.css +++ b/src/index.css @@ -2,31 +2,51 @@ @import url(./assets/css/indicator.css); @import url(./assets/css/text.css); +@font-face { + font-family: Inter; + font-weight: 300 800; + src: url(/fonts/Inter-VariableFont.ttf); +} + +@font-face { + font-family: "Azeret Mono"; + font-weight: 400 800; + src: url(/fonts/AzeretMono-VariableFont.ttf); +} + :root { - --codex-background: rgb(23 23 23); - --codex-color: #e1e4d9; + --codex-background: #1c1c1c; + --codex-color: white; --codex-color-contrast: #f8f8f8; - --codex-color-error: 239, 68, 68; + --codex-color-error: 204, 108, 108; + --codex-color-error-hexa: #cc6c6c; --codex-color-warning: 234, 179, 8; --codex-color-success: 20, 184, 166; --codex-color-blue: 30, 64, 175; --codex-color-grey: 170, 170, 170; - --codex-color-primary: #c1f0a4; + --codex-color-primary: #6fcb94; --codex-color-primary-rgb: 193, 240, 164; --codex-color-primary-variant: #c1f0a4cc; --codex-color-on-primary: #333; --codex-color-disabled: #717171; --codex-color-light: rgb(150 150 150); - --codex-border-color: rgb(82 82 82); + --codex-border-color: #96969633; + --codex-input-border-color: #494949; --codex-background-secondary: rgb(38 38 38); + --codex-highlight-color: #2f2f2f; --codex-background-light: rgb(64 64 64); --codex-background-backdrop: rgba(70, 70, 70, 0.75); --codex-border-radius: 0.5rem; - --codex-font-size: 0.875rem; + --codex-font-size: 14px; --codex-font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji; + --codex-input-label-color: #7b7b7b; + --codex-input-border-color: #494949; + --codex-input-background: #232323; + --codex-input-color-error: #fb3748; + --codex-row-gap: 16px; -webkit-tap-highlight-color: transparent; -webkit-text-size-adjust: 100%; @@ -39,7 +59,22 @@ font-size: var(--codex-font-size); color-scheme: dark; color: var(--codex-color); - background-color: var(--codex-background); + background: #000000; /* Fallback color */ + background: -webkit-linear-gradient( + 246.02deg, + #000000 30.36%, + #222222 91.05% + ); /* For Safari and older Chrome */ + background: -moz-linear-gradient( + 246.02deg, + #000000 30.36%, + #222222 91.05% + ); /* For older Firefox */ + background: linear-gradient( + 246.02deg, + #000000 30.36%, + #222222 91.05% + ); /* Standard syntax */ } ::selection { @@ -95,23 +130,22 @@ ul { padding: 0; } -main { +body > main { flex: 1; display: flex; flex-direction: column; max-width: 100%; } -hr { - border: 0.1px solid var(--codex-border-color); - width: 100%; -} - ul { margin: 0; padding: 0; } +dialog { + padding: 0; +} + input, button, textarea, @@ -124,9 +158,9 @@ pre { word-break: break-word; } -a { - text-decoration: inherit; - color: var(--codex-color); +a[aria-disabled] { + opacity: 0.6; + cursor: not-allowed; } .root { @@ -135,6 +169,52 @@ a { max-width: 100%; } -.page { - max-width: 100%; +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-transition-delay: 9999s; + transition-delay: 9999s; +} + +.row { + display: flex; + align-items: center; +} + +.gap { + gap: var(--codex-row-gap); +} + +.card { + border: 1px solid #96969633; + border-radius: 16px; + padding: 16px; + background-color: #232323; + + > header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + + svg { + color: #969696; + } + + > div { + display: flex; + align-items: center; + gap: 12px; + } + + h5 { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + } + } } diff --git a/src/main.tsx b/src/main.tsx index 3e88972..0d335a0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -25,6 +25,7 @@ if (import.meta.env.PROD && !import.meta.env.CI) { Sentry.feedbackIntegration({ // Additional SDK configuration goes in here, for example: colorScheme: "dark", + triggerLabel: "", }), ], // Tracing @@ -40,6 +41,7 @@ if (import.meta.env.PROD && !import.meta.env.CI) { // Create a new router instance const router = createRouter({ routeTree, + defaultPreload: "viewport", defaultNotFoundComponent: () => { return ( void - ): UploadResponse { - // const url = CodexSdk.url() + "/api/codex/v1/data"; - // const xhr = new XMLHttpRequest(); - - // const promise = new Promise>((resolve) => { - // xhr.upload.onprogress = (evt) => { - // if (evt.lengthComputable) { - // onProgress?.(evt.loaded, evt.total); - // } - // }; - - // xhr.open("POST", url, true); - // xhr.setRequestHeader("Content-Disposition", "attachment; filename=\"" + file.name + "\"") - // xhr.send(file); - - // xhr.onload = function () { - // if (xhr.status != 200) { - // resolve({ - // error: true, - // data: new CodexError(xhr.responseText, { - // code: xhr.status, - // }), - // }); - // } else { - // resolve({ error: false, data: xhr.response }); - // } - // }; - - // xhr.onerror = function () { - // resolve({ - // error: true, - // data: new CodexError("Something went wrong during the file upload."), - // }); - // }; - // }); - - // return { - // result: promise, - // abort: () => { - // xhr.abort(); - // }, - // }; - const { result, abort } = super.upload(file, onProgress); - - return { - abort, - result: result.then((safe) => { - if (!safe.error) { - return FilesStorage.set(safe.data, { - mimetype: file.type, - name: file.name, - uploadedAt: new Date().toJSON(), - }).then(() => safe); - } - - return safe; - }), - }; - } - - override async cids(): Promise> { - const res = await super.cids(); - - if (res.error) { - return res; - } - - const metadata = await FilesStorage.list(); - - const content = res.data.content.map((content, index) => { - if (content.manifest.filename) { - return content; - } - - const value = metadata.find(([cid]) => content.cid === cid); - - if (!value) { - return { - cid: content.cid, - manifest: { - ...content.manifest, - mimetype: "N/A", - uploadedAt: new Date(0, 0, 0, 0, 0, 0).toJSON(), - filename: "N/A" + index, - }, - }; - } - - const { - mimetype = "", - name = "", - uploadedAt = new Date(0, 0, 0, 0, 0, 0).toJSON(), - } = value[1]; - - return { - cid: content.cid, - manifest: { - ...content.manifest, - mimetype, - filename: name, - uploadedAt: uploadedAt, - }, - }; - }); - - return { error: false, data: { content } }; - } } @@ -156,7 +44,7 @@ class CodexMarketplaceMock extends CodexMarketplace { return res } - await PurchaseStorage.set("0x" + res.data, input.cid) + await WebStorage.purchases.set("0x" + res.data, input.cid) // await PurchaseDatesStorage.set(res.data, new Date().toJSON()) @@ -217,6 +105,34 @@ class CodexMarketplaceMock extends CodexMarketplace { // ], // }); // } + + // override reservations(): Promise> { + // return Promise.resolve({ + // error: false, + // data: [ + // { + // id: "0x123456789", + // availabilityId: "0x12345678910", + // requestId: "0x1234567891011", + // size: GB * 0.5 + "", + // slotIndex: "2", + // }, + // { + // id: "0x987654321", + // availabilityId: "0x9876543210", + // requestId: "0x98765432100", + // /** + // * Size in bytes + // */ + // size: GB * 0.25 + "", + // /** + // * Slot Index as hexadecimal string + // */ + // slotIndex: "1", + // }, + // ], + // }); + // } } export const CodexSdk = { @@ -224,3 +140,15 @@ export const CodexSdk = { marketplace: () => new CodexMarketplaceMock(CodexSdk.url()), data: () => new CodexDataMock(CodexSdk.url()), }; + + +export const PortForwardingUtil = { + ...PUtil, + check: (port: number) => { + if (import.meta.env.CI) { + return Promise.resolve({ reachable: true }) + } + + return PUtil.check(port) + } +} \ No newline at end of file diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 3441411..d5991c6 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -11,77 +11,145 @@ // Import Routes import { Route as rootRoute } from './routes/__root' +import { Route as OnboardingNameImport } from './routes/onboarding-name' +import { Route as OnboardingChecksImport } from './routes/onboarding-checks' import { Route as DashboardImport } from './routes/dashboard' import { Route as IndexImport } from './routes/index' import { Route as DashboardIndexImport } from './routes/dashboard/index' +import { Route as DashboardWalletImport } from './routes/dashboard/wallet' import { Route as DashboardSettingsImport } from './routes/dashboard/settings' import { Route as DashboardRequestsImport } from './routes/dashboard/requests' import { Route as DashboardPurchasesImport } from './routes/dashboard/purchases' import { Route as DashboardPeersImport } from './routes/dashboard/peers' +import { Route as DashboardNodesImport } from './routes/dashboard/nodes' +import { Route as DashboardLogsImport } from './routes/dashboard/logs' import { Route as DashboardHelpImport } from './routes/dashboard/help' +import { Route as DashboardFilesImport } from './routes/dashboard/files' import { Route as DashboardFavoritesImport } from './routes/dashboard/favorites' import { Route as DashboardDisclaimerImport } from './routes/dashboard/disclaimer' +import { Route as DashboardDeviceImport } from './routes/dashboard/device' import { Route as DashboardAvailabilitiesImport } from './routes/dashboard/availabilities' +import { Route as DashboardAnalyticsImport } from './routes/dashboard/analytics' import { Route as DashboardAboutImport } from './routes/dashboard/about' // Create/Update Routes +const OnboardingNameRoute = OnboardingNameImport.update({ + id: '/onboarding-name', + path: '/onboarding-name', + getParentRoute: () => rootRoute, +} as any) + +const OnboardingChecksRoute = OnboardingChecksImport.update({ + id: '/onboarding-checks', + path: '/onboarding-checks', + getParentRoute: () => rootRoute, +} as any) + const DashboardRoute = DashboardImport.update({ + id: '/dashboard', path: '/dashboard', getParentRoute: () => rootRoute, } as any) const IndexRoute = IndexImport.update({ + id: '/', path: '/', getParentRoute: () => rootRoute, } as any) const DashboardIndexRoute = DashboardIndexImport.update({ + id: '/', path: '/', getParentRoute: () => DashboardRoute, } as any) +const DashboardWalletRoute = DashboardWalletImport.update({ + id: '/wallet', + path: '/wallet', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardSettingsRoute = DashboardSettingsImport.update({ + id: '/settings', path: '/settings', getParentRoute: () => DashboardRoute, } as any) const DashboardRequestsRoute = DashboardRequestsImport.update({ + id: '/requests', path: '/requests', getParentRoute: () => DashboardRoute, } as any) const DashboardPurchasesRoute = DashboardPurchasesImport.update({ + id: '/purchases', path: '/purchases', getParentRoute: () => DashboardRoute, } as any) const DashboardPeersRoute = DashboardPeersImport.update({ + id: '/peers', path: '/peers', getParentRoute: () => DashboardRoute, } as any) +const DashboardNodesRoute = DashboardNodesImport.update({ + id: '/nodes', + path: '/nodes', + getParentRoute: () => DashboardRoute, +} as any) + +const DashboardLogsRoute = DashboardLogsImport.update({ + id: '/logs', + path: '/logs', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardHelpRoute = DashboardHelpImport.update({ + id: '/help', path: '/help', getParentRoute: () => DashboardRoute, } as any) +const DashboardFilesRoute = DashboardFilesImport.update({ + id: '/files', + path: '/files', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardFavoritesRoute = DashboardFavoritesImport.update({ + id: '/favorites', path: '/favorites', getParentRoute: () => DashboardRoute, } as any) const DashboardDisclaimerRoute = DashboardDisclaimerImport.update({ + id: '/disclaimer', path: '/disclaimer', getParentRoute: () => DashboardRoute, } as any) +const DashboardDeviceRoute = DashboardDeviceImport.update({ + id: '/device', + path: '/device', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardAvailabilitiesRoute = DashboardAvailabilitiesImport.update({ + id: '/availabilities', path: '/availabilities', getParentRoute: () => DashboardRoute, } as any) +const DashboardAnalyticsRoute = DashboardAnalyticsImport.update({ + id: '/analytics', + path: '/analytics', + getParentRoute: () => DashboardRoute, +} as any) + const DashboardAboutRoute = DashboardAboutImport.update({ + id: '/about', path: '/about', getParentRoute: () => DashboardRoute, } as any) @@ -104,6 +172,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardImport parentRoute: typeof rootRoute } + '/onboarding-checks': { + id: '/onboarding-checks' + path: '/onboarding-checks' + fullPath: '/onboarding-checks' + preLoaderRoute: typeof OnboardingChecksImport + parentRoute: typeof rootRoute + } + '/onboarding-name': { + id: '/onboarding-name' + path: '/onboarding-name' + fullPath: '/onboarding-name' + preLoaderRoute: typeof OnboardingNameImport + parentRoute: typeof rootRoute + } '/dashboard/about': { id: '/dashboard/about' path: '/about' @@ -111,6 +193,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardAboutImport parentRoute: typeof DashboardImport } + '/dashboard/analytics': { + id: '/dashboard/analytics' + path: '/analytics' + fullPath: '/dashboard/analytics' + preLoaderRoute: typeof DashboardAnalyticsImport + parentRoute: typeof DashboardImport + } '/dashboard/availabilities': { id: '/dashboard/availabilities' path: '/availabilities' @@ -118,6 +207,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardAvailabilitiesImport parentRoute: typeof DashboardImport } + '/dashboard/device': { + id: '/dashboard/device' + path: '/device' + fullPath: '/dashboard/device' + preLoaderRoute: typeof DashboardDeviceImport + parentRoute: typeof DashboardImport + } '/dashboard/disclaimer': { id: '/dashboard/disclaimer' path: '/disclaimer' @@ -132,6 +228,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardFavoritesImport parentRoute: typeof DashboardImport } + '/dashboard/files': { + id: '/dashboard/files' + path: '/files' + fullPath: '/dashboard/files' + preLoaderRoute: typeof DashboardFilesImport + parentRoute: typeof DashboardImport + } '/dashboard/help': { id: '/dashboard/help' path: '/help' @@ -139,6 +242,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardHelpImport parentRoute: typeof DashboardImport } + '/dashboard/logs': { + id: '/dashboard/logs' + path: '/logs' + fullPath: '/dashboard/logs' + preLoaderRoute: typeof DashboardLogsImport + parentRoute: typeof DashboardImport + } + '/dashboard/nodes': { + id: '/dashboard/nodes' + path: '/nodes' + fullPath: '/dashboard/nodes' + preLoaderRoute: typeof DashboardNodesImport + parentRoute: typeof DashboardImport + } '/dashboard/peers': { id: '/dashboard/peers' path: '/peers' @@ -167,6 +284,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DashboardSettingsImport parentRoute: typeof DashboardImport } + '/dashboard/wallet': { + id: '/dashboard/wallet' + path: '/wallet' + fullPath: '/dashboard/wallet' + preLoaderRoute: typeof DashboardWalletImport + parentRoute: typeof DashboardImport + } '/dashboard/': { id: '/dashboard/' path: '/' @@ -181,27 +305,39 @@ declare module '@tanstack/react-router' { interface DashboardRouteChildren { DashboardAboutRoute: typeof DashboardAboutRoute + DashboardAnalyticsRoute: typeof DashboardAnalyticsRoute DashboardAvailabilitiesRoute: typeof DashboardAvailabilitiesRoute + DashboardDeviceRoute: typeof DashboardDeviceRoute DashboardDisclaimerRoute: typeof DashboardDisclaimerRoute DashboardFavoritesRoute: typeof DashboardFavoritesRoute + DashboardFilesRoute: typeof DashboardFilesRoute DashboardHelpRoute: typeof DashboardHelpRoute + DashboardLogsRoute: typeof DashboardLogsRoute + DashboardNodesRoute: typeof DashboardNodesRoute DashboardPeersRoute: typeof DashboardPeersRoute DashboardPurchasesRoute: typeof DashboardPurchasesRoute DashboardRequestsRoute: typeof DashboardRequestsRoute DashboardSettingsRoute: typeof DashboardSettingsRoute + DashboardWalletRoute: typeof DashboardWalletRoute DashboardIndexRoute: typeof DashboardIndexRoute } const DashboardRouteChildren: DashboardRouteChildren = { DashboardAboutRoute: DashboardAboutRoute, + DashboardAnalyticsRoute: DashboardAnalyticsRoute, DashboardAvailabilitiesRoute: DashboardAvailabilitiesRoute, + DashboardDeviceRoute: DashboardDeviceRoute, DashboardDisclaimerRoute: DashboardDisclaimerRoute, DashboardFavoritesRoute: DashboardFavoritesRoute, + DashboardFilesRoute: DashboardFilesRoute, DashboardHelpRoute: DashboardHelpRoute, + DashboardLogsRoute: DashboardLogsRoute, + DashboardNodesRoute: DashboardNodesRoute, DashboardPeersRoute: DashboardPeersRoute, DashboardPurchasesRoute: DashboardPurchasesRoute, DashboardRequestsRoute: DashboardRequestsRoute, DashboardSettingsRoute: DashboardSettingsRoute, + DashboardWalletRoute: DashboardWalletRoute, DashboardIndexRoute: DashboardIndexRoute, } @@ -212,29 +348,45 @@ const DashboardRouteWithChildren = DashboardRoute._addFileChildren( export interface FileRoutesByFullPath { '/': typeof IndexRoute '/dashboard': typeof DashboardRouteWithChildren + '/onboarding-checks': typeof OnboardingChecksRoute + '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute + '/dashboard/analytics': typeof DashboardAnalyticsRoute '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute + '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute + '/dashboard/files': typeof DashboardFilesRoute '/dashboard/help': typeof DashboardHelpRoute + '/dashboard/logs': typeof DashboardLogsRoute + '/dashboard/nodes': typeof DashboardNodesRoute '/dashboard/peers': typeof DashboardPeersRoute '/dashboard/purchases': typeof DashboardPurchasesRoute '/dashboard/requests': typeof DashboardRequestsRoute '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/wallet': typeof DashboardWalletRoute '/dashboard/': typeof DashboardIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/onboarding-checks': typeof OnboardingChecksRoute + '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute + '/dashboard/analytics': typeof DashboardAnalyticsRoute '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute + '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute + '/dashboard/files': typeof DashboardFilesRoute '/dashboard/help': typeof DashboardHelpRoute + '/dashboard/logs': typeof DashboardLogsRoute + '/dashboard/nodes': typeof DashboardNodesRoute '/dashboard/peers': typeof DashboardPeersRoute '/dashboard/purchases': typeof DashboardPurchasesRoute '/dashboard/requests': typeof DashboardRequestsRoute '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/wallet': typeof DashboardWalletRoute '/dashboard': typeof DashboardIndexRoute } @@ -242,15 +394,23 @@ export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute '/dashboard': typeof DashboardRouteWithChildren + '/onboarding-checks': typeof OnboardingChecksRoute + '/onboarding-name': typeof OnboardingNameRoute '/dashboard/about': typeof DashboardAboutRoute + '/dashboard/analytics': typeof DashboardAnalyticsRoute '/dashboard/availabilities': typeof DashboardAvailabilitiesRoute + '/dashboard/device': typeof DashboardDeviceRoute '/dashboard/disclaimer': typeof DashboardDisclaimerRoute '/dashboard/favorites': typeof DashboardFavoritesRoute + '/dashboard/files': typeof DashboardFilesRoute '/dashboard/help': typeof DashboardHelpRoute + '/dashboard/logs': typeof DashboardLogsRoute + '/dashboard/nodes': typeof DashboardNodesRoute '/dashboard/peers': typeof DashboardPeersRoute '/dashboard/purchases': typeof DashboardPurchasesRoute '/dashboard/requests': typeof DashboardRequestsRoute '/dashboard/settings': typeof DashboardSettingsRoute + '/dashboard/wallet': typeof DashboardWalletRoute '/dashboard/': typeof DashboardIndexRoute } @@ -259,42 +419,66 @@ export interface FileRouteTypes { fullPaths: | '/' | '/dashboard' + | '/onboarding-checks' + | '/onboarding-name' | '/dashboard/about' + | '/dashboard/analytics' | '/dashboard/availabilities' + | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' + | '/dashboard/files' | '/dashboard/help' + | '/dashboard/logs' + | '/dashboard/nodes' | '/dashboard/peers' | '/dashboard/purchases' | '/dashboard/requests' | '/dashboard/settings' + | '/dashboard/wallet' | '/dashboard/' fileRoutesByTo: FileRoutesByTo to: | '/' + | '/onboarding-checks' + | '/onboarding-name' | '/dashboard/about' + | '/dashboard/analytics' | '/dashboard/availabilities' + | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' + | '/dashboard/files' | '/dashboard/help' + | '/dashboard/logs' + | '/dashboard/nodes' | '/dashboard/peers' | '/dashboard/purchases' | '/dashboard/requests' | '/dashboard/settings' + | '/dashboard/wallet' | '/dashboard' id: | '__root__' | '/' | '/dashboard' + | '/onboarding-checks' + | '/onboarding-name' | '/dashboard/about' + | '/dashboard/analytics' | '/dashboard/availabilities' + | '/dashboard/device' | '/dashboard/disclaimer' | '/dashboard/favorites' + | '/dashboard/files' | '/dashboard/help' + | '/dashboard/logs' + | '/dashboard/nodes' | '/dashboard/peers' | '/dashboard/purchases' | '/dashboard/requests' | '/dashboard/settings' + | '/dashboard/wallet' | '/dashboard/' fileRoutesById: FileRoutesById } @@ -302,11 +486,15 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute DashboardRoute: typeof DashboardRouteWithChildren + OnboardingChecksRoute: typeof OnboardingChecksRoute + OnboardingNameRoute: typeof OnboardingNameRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, DashboardRoute: DashboardRouteWithChildren, + OnboardingChecksRoute: OnboardingChecksRoute, + OnboardingNameRoute: OnboardingNameRoute, } export const routeTree = rootRoute @@ -322,7 +510,9 @@ export const routeTree = rootRoute "filePath": "__root.tsx", "children": [ "/", - "/dashboard" + "/dashboard", + "/onboarding-checks", + "/onboarding-name" ] }, "/": { @@ -332,25 +522,45 @@ export const routeTree = rootRoute "filePath": "dashboard.tsx", "children": [ "/dashboard/about", + "/dashboard/analytics", "/dashboard/availabilities", + "/dashboard/device", "/dashboard/disclaimer", "/dashboard/favorites", + "/dashboard/files", "/dashboard/help", + "/dashboard/logs", + "/dashboard/nodes", "/dashboard/peers", "/dashboard/purchases", "/dashboard/requests", "/dashboard/settings", + "/dashboard/wallet", "/dashboard/" ] }, + "/onboarding-checks": { + "filePath": "onboarding-checks.tsx" + }, + "/onboarding-name": { + "filePath": "onboarding-name.tsx" + }, "/dashboard/about": { "filePath": "dashboard/about.tsx", "parent": "/dashboard" }, + "/dashboard/analytics": { + "filePath": "dashboard/analytics.tsx", + "parent": "/dashboard" + }, "/dashboard/availabilities": { "filePath": "dashboard/availabilities.tsx", "parent": "/dashboard" }, + "/dashboard/device": { + "filePath": "dashboard/device.tsx", + "parent": "/dashboard" + }, "/dashboard/disclaimer": { "filePath": "dashboard/disclaimer.tsx", "parent": "/dashboard" @@ -359,10 +569,22 @@ export const routeTree = rootRoute "filePath": "dashboard/favorites.tsx", "parent": "/dashboard" }, + "/dashboard/files": { + "filePath": "dashboard/files.tsx", + "parent": "/dashboard" + }, "/dashboard/help": { "filePath": "dashboard/help.tsx", "parent": "/dashboard" }, + "/dashboard/logs": { + "filePath": "dashboard/logs.tsx", + "parent": "/dashboard" + }, + "/dashboard/nodes": { + "filePath": "dashboard/nodes.tsx", + "parent": "/dashboard" + }, "/dashboard/peers": { "filePath": "dashboard/peers.tsx", "parent": "/dashboard" @@ -379,6 +601,10 @@ export const routeTree = rootRoute "filePath": "dashboard/settings.tsx", "parent": "/dashboard" }, + "/dashboard/wallet": { + "filePath": "dashboard/wallet.tsx", + "parent": "/dashboard" + }, "/dashboard/": { "filePath": "dashboard/index.tsx", "parent": "/dashboard" diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index c9f3b8a..9e9864f 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,23 +1,15 @@ -import { createRootRoute, Outlet } from "@tanstack/react-router"; -import React from "react"; - -const TanStackRouterDevtools = import.meta.env.PROD - ? () => null // Render nothing in production - : React.lazy(() => - // Lazy load in development - import("@tanstack/router-devtools").then((res) => ({ - default: res.TanStackRouterDevtools, - // For Embedded Mode - // default: res.TanStackRouterDevtoolsPanel - })) - ); +import { + createRootRoute, + Outlet, + ScrollRestoration, +} from "@tanstack/react-router"; export const Route = createRootRoute({ component: () => { return ( <> + - ); }, diff --git a/src/routes/dashboard.css b/src/routes/dashboard.css deleted file mode 100644 index b43ee4a..0000000 --- a/src/routes/dashboard.css +++ /dev/null @@ -1,25 +0,0 @@ -.dashboard { - padding: 1.5rem; - display: grid; - grid-template-columns: 1fr; - gap: 0.75rem; -} - -.dashboard-download { - margin-top: 1rem; -} - -.dashboard-welcome { - display: flex; - flex-direction: column; -} - -.dashboard-alert { - margin-bottom: 0; -} - -@media (min-width: 1000px) { - .dashboard { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index fea649d..8c47007 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -1,120 +1,39 @@ -import { createFileRoute, Link, Outlet } from "@tanstack/react-router"; -import "./dashboard.css"; import { - MenuItem, - MenuItemComponentProps, - Page, -} from "@codex-storage/marketplace-ui-components"; -import { - Home, - ShoppingBag, - Server, - Settings, - HelpCircle, - TriangleAlert, - Earth, -} from "lucide-react"; -import { ICON_SIZE } from "../utils/constants"; -import { NodeIndicator } from "../components/NodeIndicator/NodeIndicator"; -import { HttpNetworkIndicator } from "../components/HttpNetworkIndicator/HttpNetworkIndicator"; + createFileRoute, + Outlet, + ScrollRestoration, +} from "@tanstack/react-router"; +import "./layout.css"; +import { Menu } from "../components/Menu/Menu"; +import { useState } from "react"; +import { AppBar } from "../components/AppBar/AppBar"; +import { Backdrop } from "@codex-storage/marketplace-ui-components"; const Layout = () => { - const Right = ( - <> - - - - ); + const [hasMobileMenu, setHasMobileMenu] = useState(false); - const items = [ - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Dashboard - - ), - }, - { - type: "separator", - }, - { - type: "menu-title", - title: "rent", - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Purchases - - ), - }, - { - type: "separator", - }, - { - type: "menu-title", - title: "host", - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Sales - - ), - }, - { - type: "separator", - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - Help - - ), - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Settings - - ), - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Peers - - ), - }, - { - type: "menu-item", - Component: (p: MenuItemComponentProps) => ( - - - Disclaimer - - ), - }, - ] satisfies MenuItem[]; + const onIconClick = () => { + if (window.innerWidth <= 999) { + setHasMobileMenu(true); + } + }; + + const onClose = () => setHasMobileMenu(false); return ( - } - items={items} - Right={Right} - version={import.meta.env.PACKAGE_VERSION} - /> +
    + + +
    + +
    + + +
    +
    + + +
    ); }; diff --git a/src/routes/dashboard/about.tsx b/src/routes/dashboard/about.tsx index ca4faa3..b18c86d 100644 --- a/src/routes/dashboard/about.tsx +++ b/src/routes/dashboard/about.tsx @@ -33,7 +33,7 @@ const About = () => {
    - +
    {c.manifest.filename} diff --git a/src/routes/dashboard/analytics.tsx b/src/routes/dashboard/analytics.tsx new file mode 100644 index 0000000..ab7c726 --- /dev/null +++ b/src/routes/dashboard/analytics.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/dashboard/analytics")({ + component: () =>
    Hello /dashboard/analytics!
    , +}); diff --git a/src/routes/dashboard/availabilities.css b/src/routes/dashboard/availabilities.css index e65ce40..5c5f636 100644 --- a/src/routes/dashboard/availabilities.css +++ b/src/routes/dashboard/availabilities.css @@ -1,5 +1,117 @@ .availabilities { height: 100%; + display: flex; + flex-wrap: wrap; + gap: 16px; + + dialog { + width: 80%; + } + + > .card { + flex: 1 1 50%; + } + + .table { + table thead tr th { + background-color: #14141499; + } + + table tbody tr.availabilty-row { + td { + background-color: #292929; + padding: 6px 12px; + + &:first-child { + cursor: pointer; + transition: transform 0.35s; + + & svg { + transition: transform 0.35s; + } + + & svg[aria-expanded] { + transform: rotate(-90deg); + } + } + } + } + + td { + b { + font-family: Inter; + font-size: 16px; + font-weight: 700; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + display: block; + } + + small { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: #ffffffcc; + } + } + } + + aside { + display: flex; + width: 400px; + flex: 1 1 30%; + + .card { + flex: 1; + } + + main { + > div { + position: relative; + } + + > .button { + width: 100%; + gap: 4px; + } + } + + .node-space { + border-bottom: 1px solid #96969633; + padding-bottom: 16px; + + h6 { + border-top: none; + } + } + + footer { + padding-top: 16px; + + b { + display: block; + font-family: Inter; + font-size: 18px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.015em; + text-align: left; + } + + small { + font-family: Inter; + font-size: 14px; + font-weight: 400; + line-height: 20px; + letter-spacing: -0.006em; + text-align: left; + color: #969696cc; + } + } + } } .availabilities-actions { @@ -14,24 +126,8 @@ display: block; } -.availabilities-create { - position: absolute; - margin: auto; - border-radius: 100%; - height: 6rem; - width: 6rem; -} - -.availabilities-create .button-label { - display: none; -} - .availabilities-header { position: relative; - display: flex; - place-items: center; - justify-content: center; - margin-bottom: 2rem; } .availabilities-content { diff --git a/src/routes/dashboard/availabilities.tsx b/src/routes/dashboard/availabilities.tsx index fe0a012..dc22f94 100644 --- a/src/routes/dashboard/availabilities.tsx +++ b/src/routes/dashboard/availabilities.tsx @@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router"; import { ErrorBoundary } from "@sentry/react"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; import { + Button, SpaceAllocationItem, Spinner, } from "@codex-storage/marketplace-ui-components"; @@ -13,11 +14,15 @@ import { AvailabilitiesTable } from "../../components/Availability/Availabilitie import { AvailabilityEdit } from "../../components/Availability/AvailabilityEdit"; import { Strings } from "../../utils/strings"; import { PrettyBytes } from "../../utils/bytes"; -import { AvailabilitySunburst } from "../../components/Availability/AvailabilitySunburst"; +import { Sunburst } from "../../components/Availability/Sunburst"; import { Errors } from "../../utils/errors"; import { availabilityColors } from "../../components/Availability/availability.colors"; -import { AvailabilityStorage } from "../../utils/availabilities-storage"; import { AvailabilityWithSlots } from "../../components/Availability/types"; +import { WebStorage } from "../../utils/web-storage"; +import { NodeSpace } from "../../components/NodeSpace/NodeSpace"; +import PlusIcon from "../../assets/icons/plus-circle.svg?react"; +import UploadIcon from "../../assets/icons/upload.svg?react"; +import { AvailabilityUtils } from "../../components/Availability/availability.utils"; const defaultSpace = { quotaMaxBytes: 0, @@ -51,7 +56,7 @@ export function Availabilities() { return { ...a, slots: res.data }; }) .then((data) => - AvailabilityStorage.get(data.id).then((n) => ({ + WebStorage.availabilities.get(data.id).then((n) => ({ ...data, name: n || "", })) @@ -117,43 +122,63 @@ export function Availabilities() { allocation.push({ title: "Space remaining", - // TODO move this to domain - size: - space.quotaMaxBytes - space.quotaReservedBytes - space.quotaUsedBytes, + size: AvailabilityUtils.maxValue(space), color: "transparent", }); - return ( -
    -
    - {isPending ? ( -
    - -
    - ) : ( - <> -
    - - - -
    - -
    - -
    - - )} + if (isPending) { + return ( +
    +
    + ); + } + + const onOpenAvailabilities = () => + document.dispatchEvent(new CustomEvent("codexavailabilitycreate", {})); + + return ( +
    +
    + +
    +
    ); } diff --git a/src/routes/dashboard/device.tsx b/src/routes/dashboard/device.tsx new file mode 100644 index 0000000..049ff50 --- /dev/null +++ b/src/routes/dashboard/device.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/dashboard/device")({ + component: () =>
    Hello /dashboard/device!
    , +}); diff --git a/src/routes/dashboard/favorites.tsx b/src/routes/dashboard/favorites.tsx index 7b9dd9c..c96d39f 100644 --- a/src/routes/dashboard/favorites.tsx +++ b/src/routes/dashboard/favorites.tsx @@ -2,19 +2,16 @@ import { createFileRoute } from "@tanstack/react-router"; import { Files } from "../../components/Files/Files"; import { ErrorBoundary } from "@sentry/react"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; -import { Card } from "@codex-storage/marketplace-ui-components"; export const Route = createFileRoute("/dashboard/favorites")({ component: () => ( <> ( - - - + )}>
    diff --git a/src/routes/dashboard/files.css b/src/routes/dashboard/files.css new file mode 100644 index 0000000..ea89917 --- /dev/null +++ b/src/routes/dashboard/files.css @@ -0,0 +1,26 @@ +.files-page { + display: flex; + flex-wrap: wrap; + gap: 16px; + + > .card { + margin-bottom: 16px; + + &:first-child { + flex: 1 1 70%; + } + } + + aside { + display: flex; + flex-direction: column; + gap: 16px; + flex: 1 1 auto; + + .card:first-child { + display: flex; + flex-direction: column; + /* flex: 1 1 67%; */ + } + } +} diff --git a/src/routes/dashboard/files.tsx b/src/routes/dashboard/files.tsx new file mode 100644 index 0000000..39092ba --- /dev/null +++ b/src/routes/dashboard/files.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Files } from "../../components/Files/Files"; +import "./files.css"; +import { UploadCard } from "../../components/UploadCard/UploadCard"; +import { Download } from "../../components/Download/Download"; +import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch"; +import UploadIcon from "../../assets/icons/upload.svg?react"; +import DownloadIcon from "../../assets/icons/download.svg?react"; +import FetchIcon from "../../assets/icons/fetch.svg?react"; +import { Card } from "../../components/Card/Card"; +import FilesIconOutline from "../../assets/icons/files-outline.svg?react"; + +export const Route = createFileRoute("/dashboard/files")({ + component: () => ( +
    + } + title="Files"> + + + + +
    + ), +}); diff --git a/src/routes/dashboard/index.css b/src/routes/dashboard/index.css new file mode 100644 index 0000000..af9341a --- /dev/null +++ b/src/routes/dashboard/index.css @@ -0,0 +1,96 @@ +.dashboard { + .card--main { + flex: 1 1 60%; + + &:first-child { + filter: grayscale(30); + transition: filter 0.5s; + + &:hover { + filter: none; + } + } + + @media (min-width: 2000px) { + &:nth-child(n + 1) { + flex: 1 1 34%; + } + + &:first-child { + flex: 1 1 20%; + } + + &.card--main--files { + flex: 1 1 62%; + } + } + } + + header { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + + h3 { + font-family: Inter; + font-size: 12px; + font-weight: 700; + line-height: 14.52px; + letter-spacing: 0.01em; + color: #969696cc; + text-transform: uppercase; + } + + h4 { + font-family: Inter; + font-size: 32px; + font-weight: 400; + line-height: 38.73px; + letter-spacing: 0.01em; + color: white; + } + + .emoji { + border-radius: 50%; + width: 52px; + height: 52px; + background-color: #4a9a73; + display: flex; + align-items: center; + justify-content: center; + font-size: 26px; + } + } + + > main { + display: flex; + flex-wrap: wrap; + gap: 16px; + .column { + min-width: 350px; + display: flex; + flex-direction: column; + gap: 16px; + flex: 1 1 30%; + + .card { + flex: 1; + } + } + + @media (min-width: 2000px) { + .column:nth-child(2) { + flex: 1 1 15%; + } + + .column { + flex: 1 1 25%; + } + } + + .files { + flex: 1; + flex-basis: 66%; + } + } +} diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index 931438f..8a90cb1 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1,91 +1,94 @@ import { createFileRoute } from "@tanstack/react-router"; import { Files } from "../../components/Files/Files.tsx"; -import { Alert, Card, Upload } from "@codex-storage/marketplace-ui-components"; -import { CodexSdk } from "../../sdk/codex"; -import { Welcome } from "../../components/Welcome/Welcome.tsx"; -import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder.tsx"; -import { ErrorBoundary } from "@sentry/react"; -import { useQueryClient } from "@tanstack/react-query"; +import { WelcomeCard } from "../../components/Welcome/WelcomeCard.tsx"; import { Download } from "../../components/Download/Download.tsx"; +import "./index.css"; +import { Versions } from "../../components/Versions/Versions.tsx"; +import { WebStorage } from "../../utils/web-storage.ts"; +import { ConnectedAccount } from "../../components/ConnectedAccount/ConnectedAccount.tsx"; +import { NodeSpace } from "../../components/NodeSpace/NodeSpace.tsx"; +import { UploadCard } from "../../components/UploadCard/UploadCard.tsx"; +import { PeersCard } from "../../components/Peers/PeersCard.tsx"; +import { Card } from "../../components/Card/Card.tsx"; +import NodesIcon from "../../assets/icons/nodes.svg?react"; +import WalletIcon from "../../assets/icons/wallet.svg?react"; +import PlusIcon from "../../assets/icons/plus.svg?react"; +import PeersIcon from "../../assets/icons/peers.svg?react"; +import UploadIcon from "../../assets/icons/upload.svg?react"; +import DownloadIcon from "../../assets/icons/download.svg?react"; +import FetchIcon from "../../assets/icons/fetch.svg?react"; +import { ManifestFetch } from "../../components/ManifestFetch/ManifestFetch.tsx"; +import FilesIconOutline from "../../assets/icons/files-outline.svg?react"; export const Route = createFileRoute("/dashboard/")({ - component: About, + component: Dashboard, }); -function About() { - const queryClient = useQueryClient(); +function Dashboard() { + const username = WebStorage.onBoarding.getDisplayName(); - const onSuccess = () => { - queryClient.invalidateQueries({ queryKey: ["cids"] }); - }; + const emoji = WebStorage.onBoarding.getEmoji(); return ( - <> -
    -
    - ( - - )}> - - - - +
    +
    +
    +
    {emoji}
    +
    +

    Welcome back,

    +

    {username}

    +
    +
    + +
    +
    + } + className="card--main" + title="Connected Account" + buttonLabel="Add Wallet" + buttonIcon={() => }> + + - ( - - )}> - - - - +
    + } + title="Storage" + buttonLabel="Details"> + + + } + title="Peers" + buttonLabel="Details"> + +
    - ( - - )}> -
    - + - - The website and the content herein is not intended for public use - and is for informational and demonstration purposes only. - -
    -
    -
    +
    + } title="Upload"> + + -
    - ( - - - - )}> - - -
    - + } title="Download"> + + + + } title="Fetch manifest"> + + +
    + + } + className="card--main card--main--files" + title="Files"> + + + +
    ); } diff --git a/src/routes/dashboard/logs.css b/src/routes/dashboard/logs.css new file mode 100644 index 0000000..75107a5 --- /dev/null +++ b/src/routes/dashboard/logs.css @@ -0,0 +1,48 @@ +.logs-card { + display: flex; + justify-content: space-between; + border: 1px solid #96969633; + border-radius: 16px; + margin-bottom: 16px; + + > div:first-child { + padding: 16px; + } + + h5 { + font-family: Inter; + font-size: 18px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.015em; + text-align: left; + } + + small { + font-family: Inter; + font-size: 12px; + font-weight: 400; + line-height: 16px; + text-align: left; + color: #969696cc; + } + + .button { + width: 187px; + gap: 8px; + } +} + +.logs { + .node { + pre { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + text-align: left; + color: #757575; + } + } +} diff --git a/src/routes/dashboard/logs.tsx b/src/routes/dashboard/logs.tsx new file mode 100644 index 0000000..ec5e5f6 --- /dev/null +++ b/src/routes/dashboard/logs.tsx @@ -0,0 +1,45 @@ +import { createFileRoute } from "@tanstack/react-router"; +import "./logs.css"; +import { RequireAssitance } from "../../components/RequireAssitance/RequireAssitance"; +import { LogLevel } from "../../components/LogLevel/LogLevel"; +import { useDebug } from "../../hooks/useDebug"; +import LogsIcon from "../../assets/icons/logs.svg?react"; + +const throwOnError = false; + +const Logs = () => { + const { data } = useDebug(throwOnError); + + const { table, ...rest } = data ?? {}; + + return ( +
    +
    +
    +
    Log level
    + + Manage the type of logs being displayed on your CLI for Codex Node. + + +
    + +
    + +
    +
    +
    + +
    Node
    +
    +
    +
    +
    {JSON.stringify(rest, null, 2)}
    +
    +
    +
    + ); +}; + +export const Route = createFileRoute("/dashboard/logs")({ + component: Logs, +}); diff --git a/src/routes/dashboard/nodes.tsx b/src/routes/dashboard/nodes.tsx new file mode 100644 index 0000000..c8d1738 --- /dev/null +++ b/src/routes/dashboard/nodes.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from '@tanstack/react-router' + +export const Route = createFileRoute('/dashboard/nodes')({ + component: () =>
    Hello /nodes!
    , +}) diff --git a/src/routes/dashboard/peers.css b/src/routes/dashboard/peers.css deleted file mode 100644 index 7f308ee..0000000 --- a/src/routes/dashboard/peers.css +++ /dev/null @@ -1,41 +0,0 @@ -.peers-map { - max-width: 1000px; - width: 100%; -} - -.peers-table { - margin-top: 1rem; - width: calc(100% - 4rem); - max-width: calc(1000px - 4rem); -} - -.peers { - display: flex; - flex-direction: column; - align-items: center; - padding-bottom: 4rem; - padding-left: 2rem; - padding-right: 2rem; -} - -.peers circle[fill="#d6ff79"] { - /* fill: yellow; */ - animation: dash 3s linear infinite; - stroke: white; - stroke-width: 0.6px; - stroke-dasharray: 0.3; -} - -@keyframes dash { - from { - stroke-dashoffset: 2; - } - to { - stroke-dashoffset: 0; - } -} -@keyframes circleAn { - to { - /* stroke-dashoffset: 100px; */ - } -} diff --git a/src/routes/dashboard/peers.tsx b/src/routes/dashboard/peers.tsx index 491e867..a9c8827 100644 --- a/src/routes/dashboard/peers.tsx +++ b/src/routes/dashboard/peers.tsx @@ -1,107 +1,7 @@ -import { Cell, Row, Table } from "@codex-storage/marketplace-ui-components"; -import { createFileRoute } from "@tanstack/react-router"; -import { getMapJSON } from "dotted-map"; -import DottedMap from "dotted-map/without-countries"; -import { Promises } from "../../utils/promises"; -import { useQuery } from "@tanstack/react-query"; -import { PeerCountryCell } from "../../components/Peers/PeerCountryCell"; -import { useCallback, useState } from "react"; -import { PeerPin } from "../../components/Peers/types"; -import "./peers.css"; -import { CodexSdk } from "../../sdk/codex"; import { ErrorBoundary } from "@sentry/react"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; - -// This function accepts the same arguments as DottedMap in the example above. -const mapJsonString = getMapJSON({ height: 60, grid: "diagonal" }); - -const Peers = () => { - const [pins, setPins] = useState<[PeerPin, number][]>([]); - const { data } = useQuery({ - queryFn: () => - CodexSdk.debug() - .info() - .then((s) => Promises.rejectOnError(s)), - queryKey: ["debug"], - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, - - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, - - // Throw the error to the error boundary - throwOnError: true, - }); - - const onPinAdd = useCallback((pin: PeerPin) => { - setPins((val) => { - const [, quantity = 0] = - val.find(([p]) => p.lat === pin.lat && p.lng == pin.lng) || []; - return [...val, [pin, quantity + 1]]; - }); - }, []); - - // It’s safe to re-create the map at each render, because of the - // pre-computation it’s super fast ⚡️ - const map = new DottedMap({ map: JSON.parse(mapJsonString) }); - - pins.map(([pin, quantity]) => - map.addPin({ - lat: pin.lat, - lng: pin.lng, - svgOptions: { color: "#d6ff79", radius: 0.8 * quantity }, - }) - ); - - const svgMap = map.getSVG({ - radius: 0.42, - color: "#423B38", - shape: "circle", - backgroundColor: "#020300", - }); - - const headers = ["Country", "PeerId", "Active"]; - - const rows = - (data?.table?.nodes || []).map((node) => ( - , - {node.peerId}, - - {node.seen ? ( -
    - ) : ( -
    - )} -
    , - ]}>
    - )) || []; - - return ( -
    - {/* */} - -
    - -
    - - ); -}; +import { createFileRoute } from "@tanstack/react-router"; +import { Peers } from "../../components/Peers/Peers"; export const Route = createFileRoute("/dashboard/peers")({ component: () => ( diff --git a/src/routes/dashboard/purchases.css b/src/routes/dashboard/purchases.css index df90d3a..afb1ae9 100644 --- a/src/routes/dashboard/purchases.css +++ b/src/routes/dashboard/purchases.css @@ -1,12 +1,16 @@ -.purchases-modal { - margin: auto; -} +.purchases { + > div:first-child { + padding: 1rem 0; + display: flex; + align-items: center; + justify-content: flex-end; + } -.purchases-actions { - padding: 1rem; - display: flex; - align-items: center; - justify-content: flex-end; + .table { + table thead tr th { + background-color: #14141499; + } + } } .purchases-loader { diff --git a/src/routes/dashboard/purchases.tsx b/src/routes/dashboard/purchases.tsx index 04eb9eb..dbf0039 100644 --- a/src/routes/dashboard/purchases.tsx +++ b/src/routes/dashboard/purchases.tsx @@ -1,97 +1,20 @@ -import { useQuery } from "@tanstack/react-query"; import { createFileRoute } from "@tanstack/react-router"; -import { CodexSdk } from "../../sdk/codex"; -import { - Cell, - Row, - Spinner, - Table, -} from "@codex-storage/marketplace-ui-components"; import { StorageRequestCreate } from "../../components/StorageRequestSetup/StorageRequestCreate"; import "./purchases.css"; -import { FileCell } from "../../components/FileCellRender/FileCell"; -import { CustomStateCellRender } from "../../components/CustomStateCellRender/CustomStateCellRender"; -import { Promises } from "../../utils/promises"; -import { TruncateCell } from "../../components/TruncateCell/TruncateCell"; -import { Times } from "../../utils/times"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; import { ErrorBoundary } from "@sentry/react"; +import { PurchasesTable } from "../../components/Purchase/PurchasesTable"; const Purchases = () => { - const { data, isPending } = useQuery({ - queryFn: () => - CodexSdk.marketplace() - .purchases() - .then((s) => Promises.rejectOnError(s)), - queryKey: ["purchases"], - - // No need to retry because if the connection to the node - // is back again, all the queries will be invalidated. - retry: false, - - // The client node should be local, so display the cache value while - // making a background request looks good. - staleTime: 0, - - // Refreshing when focus returns can be useful if a user comes back - // to the UI after performing an operation in the terminal. - refetchOnWindowFocus: true, - - initialData: [], - - // Throw the error to the error boundary - throwOnError: true, - }); - - if (isPending) { - return ( -
    - -
    - ); - } - - const headers = [ - "file", - "request id", - "duration", - "slots", - "reward", - "proof probability", - "state", - ]; - - const rows = data.map((p, index) => { - const r = p.request; - const ask = p.request.ask; - const duration = parseInt(p.request.ask.duration, 10); - const pf = parseInt(p.request.ask.proofProbability, 10); - - return ( - , - , - {Times.pretty(duration)}, - {ask.slots.toString()}, - {ask.reward + " CDX"}, - {pf.toString()}, - , - ]}> - ); - }); - return ( -
    -
    +
    +
    -
    +
    + +
    ); }; diff --git a/src/routes/dashboard/settings.css b/src/routes/dashboard/settings.css index fd82edf..c75ccc5 100644 --- a/src/routes/dashboard/settings.css +++ b/src/routes/dashboard/settings.css @@ -1,11 +1,47 @@ .settings { - border-radius: var(--codex-border-radius); - border: 1px solid var(--codex-border-color); - background-color: var(--codex-background-secondary); - padding: 1rem 1.5rem; - margin: 1rem 1.5rem; -} + header { + display: flex; + align-items: center; + justify-content: space-between; + } + main { + margin-top: 16px; + padding: 16px; + max-width: 800px; + z-index: 1; + position: relative; + + h3 { + font-family: Inter; + font-size: 18px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.015em; + margin-bottom: 16px; + } + + .user-info { + padding-bottom: 16px; + margin-bottom: 16px; + border-bottom: 1px solid #96969633; + } + } + + .background-img { + top: 200px; + right: -400px; + } + + .refresh { + top: 17px; + } + + .address svg { + top: 55px; + } +} +/* .settings-title { font-weight: bold; font-size: 1.125rem; @@ -19,4 +55,4 @@ .settings-debug-loader { margin: auto; -} +} */ diff --git a/src/routes/dashboard/settings.tsx b/src/routes/dashboard/settings.tsx index e46deee..182fe10 100644 --- a/src/routes/dashboard/settings.tsx +++ b/src/routes/dashboard/settings.tsx @@ -1,16 +1,26 @@ import { createFileRoute } from "@tanstack/react-router"; import "./settings.css"; -import { LogLevel } from "../../components/LogLevel/LogLevel"; -import { Debug } from "../../components/Debug/Debug"; -import { CodexUrlSettings } from "../../components/CodexUrllSettings/CodexUrlSettings"; import { ErrorBoundary } from "@sentry/react"; import { ErrorPlaceholder } from "../../components/ErrorPlaceholder/ErrorPlaceholder"; -import { useEffect } from "react"; +import { UserInfo } from "../../components/UserInfo/UserInfo"; +import { HealthChecks } from "../../components/HealthChecks/HealthChecks"; +import Logotype from "../../assets/icons/logotype.svg?react"; +import Logo from "../../assets/icons/logo.svg?react"; +import { Versions } from "../../components/Versions/Versions"; +import { BackgroundImage } from "../../components/BackgroundImage/BackgroundImage"; export const Route = createFileRoute("/dashboard/settings")({ component: () => ( - <> -
    +
    +
    +
    + + +
    + +
    +
    +

    Personalization

    ( )}> - + -
    -
    +

    Connection

    + + ( + + )}> + {}} /> + + + + + + {/*
    ( -
    - - {/*
    - - -
    - -
    - - -
    */} - +
    */} +
    ), }); diff --git a/src/routes/dashboard/wallet.tsx b/src/routes/dashboard/wallet.tsx new file mode 100644 index 0000000..eee5841 --- /dev/null +++ b/src/routes/dashboard/wallet.tsx @@ -0,0 +1,5 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/dashboard/wallet")({ + component: () =>
    Hello /dashboard/wallet!
    , +}); diff --git a/src/routes/index.tsx b/src/routes/index.tsx index c59bca3..7f65845 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,20 +1,98 @@ -import { createFileRoute, Link, redirect } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { Modal } from "@codex-storage/marketplace-ui-components"; +import { ArrowRight } from "lucide-react"; +import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout"; +import AlphaIcon from "../assets/icons/alpha.svg?react"; +import AlphaText from "../assets/icons/alphatext.svg?react"; +import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react"; export const Route = createFileRoute("/")({ component: Index, beforeLoad: async () => { - throw redirect({ - to: "/dashboard", - }); + // throw redirect({ + // to: "/dashboard", + // }); }, }); function Index() { - return ( -
    -

    Welcome Home!

    + const [modal, setModal] = useState(false); + const navigate = useNavigate({ from: "/" }); - Go to dashboard -
    + const onLegalDisclaimerOpen = () => setModal(true); + + const onLegalDisclaimerClose = () => setModal(false); + + const onNextStep = () => navigate({ to: "/onboarding-name" }); + + return ( + <> + + <> +
    + +
    + + {import.meta.env.PACKAGE_VERSION} + Legal Disclaimer +
    +
    +
    +

    + Hello, +
    Welcome to Codex Vault +

    +

    + Codex is a durable, decentralised data storage protocol, created + so the world community can preserve its most important knowledge + without risk of censorship. +

    +
    +
    + + Let’s get started + + + +

    Disclaimer

    + +

    + The website and the content herein is not intended for public + use and is for informational and demonstration purposes only. +

    + +
    + +

    + The website and any associated functionalities are provided on + an “as is” basis without any guarantees, warranties, or + representations of any kind, either express or implied. The + website and any associated functionalities may not reflect the + final version of the project and is subject to changes, updates, + or removal at any time and without notice. +

    + +
    + +

    + By accessing and using this website, you agree that we, Logos + Collective Association and its affiliates, will not be liable + for any direct, indirect, incidental, or consequential damages + arising from the use of, or inability to use, this website. Any + data, content, or interactions on this site are non-binding and + should not be considered final or actionable. Your use of this + website is at your sole risk. +

    +
    +
    + + + + +
    + ); } diff --git a/src/routes/layout.css b/src/routes/layout.css new file mode 100644 index 0000000..16ec77c --- /dev/null +++ b/src/routes/layout.css @@ -0,0 +1,65 @@ +.layout { + display: flex; + flex: 1; + max-width: 100%; + + > main { + flex: 1; + background-color: #141414; + + > div { + padding: 16px; + + @media (min-width: 1000px) { + padding: 24px 48px; + } + } + } +} + +.dashboard-download { + margin-top: 1rem; +} + +.dashboard-fetch { + margin-top: 1rem; +} + +.dashboard-welcome { + display: flex; + flex-direction: column; +} + +.dashboard-alert { + margin-bottom: 0; +} + +.dashboard-welcome-versions { + display: flex; + gap: 32px; +} + +.dashboard-welcome-versionContainer { + width: 50px; + text-align: right; +} + +.dashboard-welcome-versionTitle { + font-family: Inter; + font-size: 16px; + font-weight: 500; + line-height: 24px; + letter-spacing: -0.011em; + color: #99a0ae; +} + +.dashboard-welcome-versionValue { + font-family: Inter; + font-size: 10px; + font-weight: 400; + line-height: 12.1px; + letter-spacing: 0.01em; + color: white; + text-transform: uppercase; + white-space: nowrap; +} diff --git a/src/routes/onboarding-checks.tsx b/src/routes/onboarding-checks.tsx new file mode 100644 index 0000000..bf9ea25 --- /dev/null +++ b/src/routes/onboarding-checks.tsx @@ -0,0 +1,61 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { attributes } from "../utils/attributes"; +import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react"; +import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout"; +import { HealthChecks } from "../components/HealthChecks/HealthChecks"; +import { useNetwork } from "../network/useNetwork"; +import { WebStorage } from "../utils/web-storage"; +import AlphaIcon from "../assets/icons/alpha.svg?react"; + +const OnBoardingChecks = () => { + const online = useNetwork(); + const displayName = WebStorage.onBoarding.getDisplayName(); + const [isStepValid, setIsStepValid] = useState(false); + const navigate = useNavigate({ from: "/onboarding-checks" }); + + const onNextStep = () => { + if (isStepValid) { + navigate({ to: "/dashboard" }); + } + }; + + const onStepValid = (valid: boolean) => setIsStepValid(valid); + + return ( + + <> +
    +
    + +
    +

    + Connection /
    + Device Health Check +

    +
    +
    +

    + Nice to meet you {displayName},
    + Let’s establish our connection. +

    + + +
    + + + + + +
    + ); +}; + +export const Route = createFileRoute("/onboarding-checks")({ + component: OnBoardingChecks, +}); diff --git a/src/routes/onboarding-name.tsx b/src/routes/onboarding-name.tsx new file mode 100644 index 0000000..983c3df --- /dev/null +++ b/src/routes/onboarding-name.tsx @@ -0,0 +1,55 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { OnBoardingLayout } from "../components/OnBoarding/OnBoardingLayout"; +import { attributes } from "../utils/attributes"; +import ArrowRightCircle from "../assets/icons/arrow-circle.svg?react"; +import { UserInfo } from "../components/UserInfo/UserInfo"; +import { WebStorage } from "../utils/web-storage"; +import AlphaIcon from "../assets/icons/alpha.svg?react"; + +const OnBoardingName = () => { + const [isStepValid, setIsStepValid] = useState( + !!WebStorage.onBoarding.getDisplayName() + ); + const navigate = useNavigate({ from: "/onboarding-name" }); + + const onNameChange = (value: string) => setIsStepValid(!!value); + + const onNextStep = () => { + if (isStepValid) { + navigate({ to: "/onboarding-checks" }); + } + }; + + return ( + + <> +
    +
    + +
    +

    Personalization

    +
    +
    +

    + Let’s get you setup.
    + What do you want to be called? +

    + + +
    + + + + + +
    + ); +}; + +export const Route = createFileRoute("/onboarding-name")({ + component: OnBoardingName, +}); diff --git a/src/utils/availabilities-storage.ts b/src/utils/availabilities-storage.ts deleted file mode 100644 index f28bd02..0000000 --- a/src/utils/availabilities-storage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createStore, del, get, set } from "idb-keyval"; - -const store = createStore("availabilities", "availabilities"); - -export const AvailabilityStorage = { - get(key: string) { - return get(key, store); - }, - - delete(key: string) { - return del(key, store); - }, - - async add(key: string, value: string) { - return set(key, value, store); - }, -}; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index d9a5c72..ccbe7d7 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -13,3 +13,5 @@ export const EXPLORER_URL = "https://explorer.testnet.codex.storage/tx"; export const GB = 1_073_741_824; export const TB = 1_099_511_627_776; + +export const MOBILE_MAX_WIDTH = 999 \ No newline at end of file diff --git a/src/utils/dates.ts b/src/utils/dates.ts index 434c910..d34def9 100644 --- a/src/utils/dates.ts +++ b/src/utils/dates.ts @@ -1,12 +1,12 @@ export const Dates = { - format(date: string | Date) { + format(date: number) { if (!date) { - return "N/A"; + return "-"; } return new Intl.DateTimeFormat("en-GB", { dateStyle: "medium", timeStyle: "short", - }).format(new Date(date)); + }).format(new Date(date * 1000)); }, }; diff --git a/src/utils/favorite-storage.tsx b/src/utils/favorite-storage.tsx deleted file mode 100644 index a589b03..0000000 --- a/src/utils/favorite-storage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { createStore, del, keys, set } from "idb-keyval"; - -const store = createStore("favorites", "favorites"); - -export const FavoriteStorage = { - list() { - return keys(store); - }, - - delete(key: string) { - return del(key, store); - }, - - async add(key: string) { - return set(key, "1", store); - }, -}; diff --git a/src/utils/file-storage.ts b/src/utils/file-storage.ts deleted file mode 100644 index 7e9088f..0000000 --- a/src/utils/file-storage.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createStore, entries, get, set } from "idb-keyval"; - -const store = createStore("files", "files"); - -export type FileMetadata = { - mimetype: string; - uploadedAt: string; - name: string; -}; - -export const FilesStorage = { - list() { - return entries(store); - }, - - async get(cid: string) { - return get(cid, store); - }, - - async set(cid: string, metadata: FileMetadata) { - return set(cid, metadata, store); - }, -}; diff --git a/src/utils/files.ts b/src/utils/files.ts deleted file mode 100644 index 7ab1ad7..0000000 --- a/src/utils/files.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const Files = { - isImage(type: string) { - return type.startsWith("image"); - }, -}; - -export type CodexFileMetadata = { - type: string; - name: string; -}; diff --git a/src/utils/purchases-storage.ts b/src/utils/purchases-storage.ts deleted file mode 100644 index f3a278a..0000000 --- a/src/utils/purchases-storage.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createStore, get, set } from "idb-keyval"; - -const store = createStore("purchases", "purchases"); -const storeDates = createStore("purchases", "dates"); - -export const PurchaseStorage = { - async get(key: string) { - return get(key, store); - }, - - async set(key: string, cid: string) { - return set(key, cid, store); - }, -}; - -export const PurchaseDatesStorage = { - async get(key: string) { - return get(key, storeDates); - }, - - async set(key: string, date: string) { - return set(key, date, storeDates); - }, -}; diff --git a/src/utils/strings.ts b/src/utils/strings.ts index 82030f9..95c4ea1 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -1,3 +1,8 @@ export const Strings = { shortId: (id: string) => id.slice(0, 5) + "..." + id.slice(-5), + + + + }; + diff --git a/src/utils/times.ts b/src/utils/times.ts index a2c5730..1ff5c4d 100644 --- a/src/utils/times.ts +++ b/src/utils/times.ts @@ -61,4 +61,33 @@ export const Times = { return plural(value, "seconds"); }, + + unit(value: number) { + let seconds = 30 * 24 * 60 * 60; + + if (value >= seconds) { + return "months"; + } + + seconds /= 30; + if (value >= seconds) { + return "days" + } + + return "hours" + }, + + unitValue(unit: "hours" | "days" | "months") { + switch (unit) { + case "months": { + return 30 * 24 * 60 * 60 + } + case "days": { + return 24 * 60 * 60 + } + default: { + return 60 * 60 + } + } + } }; diff --git a/src/utils/web-storage.ts b/src/utils/web-storage.ts index 9f1c8a3..4136c5b 100644 --- a/src/utils/web-storage.ts +++ b/src/utils/web-storage.ts @@ -1,4 +1,5 @@ -import { del, get, set } from "idb-keyval"; +import { createStore, del, entries, get, set } from "idb-keyval"; + export const WebStorage = { set(key: string, value: unknown) { @@ -12,4 +13,113 @@ export const WebStorage = { delete(key: string) { return del(key); }, + + onBoarding: { + getStep() { + return parseInt(localStorage.getItem("onboarding-step") || "0", 10) + }, + + setStep(step: number) { + localStorage.setItem("onboarding-step", step.toString()) + }, + + setDisplayName(displayName: string) { + localStorage.setItem("display-name", displayName) + }, + + getDisplayName() { + return localStorage.getItem("display-name") || "" + }, + + setEmoji(emoji: string) { + localStorage.setItem("emoji", emoji) + }, + + getEmoji() { + return localStorage.getItem("emoji") || "🤖" + }, + }, + + folders: { + store: createStore("folders", "folders"), + + create(folder: string) { + return set(folder, [], this.store); + }, + + async list(): Promise<[string, string[]][]> { + const items = await entries(this.store) || [] + + if (items.length == 0) { + return [["Favorites", []]] + } + + if (items[0][0] !== "Favorites") { + return [["Favorites", []], ...items] + } + + + return items + }, + delete(key: string) { + return del(key, this.store); + }, + async addFile(folder: string, cid: string) { + const files = await get(folder, this.store) || [] + + return set(folder, [...files, cid], this.store) + }, + + async deleteFile(folder: string, cid: string) { + const files = await get(folder, this.store) || [] + + return set(folder, files.filter(item => item !== cid), this.store) + + }, + }, + + availabilities: { + store: createStore("availabilities", "availabilities"), + + get(key: string) { + return get(key, this.store); + }, + + delete(key: string) { + return del(key, this.store); + }, + + async add(key: string, value: string) { + return set(key, value, this.store); + }, + }, + + purchases: { + store: createStore("purchases", "purchases"), + + + async get(key: string) { + return get(key, this.store); + }, + + async set(key: string, cid: string) { + return set(key, cid, this.store); + }, + + async entries() { + return entries(this.store); + }, + + dates: { + store: createStore("purchases", "dates"), + + async get(key: string) { + return get(key, this.store); + }, + + async set(key: string, date: string) { + return set(key, date, this.store); + }, + } + } }; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index bb5e7a0..d2f2e02 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,8 +1,10 @@ /// +/// interface ImportMetaEnv { VITE_CODEX_API_URL: string; VITE_GEO_IP_URL: string; + VITE_DISCORD_LINK: string; } interface ImportMeta { diff --git a/vite.config.ts b/vite.config.ts index 105be43..b4a2237 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,16 +2,33 @@ import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import packageJson from "./package.json"; +import svgr from "vite-plugin-svgr"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [TanStackRouterVite(), react()], + plugins: [TanStackRouterVite(), react(), svgr({ + svgrOptions: { + plugins: ["@svgr/plugin-svgo", "@svgr/plugin-jsx"], + svgoConfig: { + floatPrecision: 2, + }, + }, + // ... + })], define: { "import.meta.env.PACKAGE_VERSION": JSON.stringify(packageJson.version), }, build: { sourcemap: true, rollupOptions: { + output: { + manualChunks: { + "@sentry/react": ["@sentry/react"], + "emoji-picker-react": ["emoji-picker-react"], + "dotted-map": ["dotted-map"], + "echarts": ["echarts"], + } + }, onwarn(warning, defaultHandler) { if (warning.code === "SOURCEMAP_ERROR") { return; @@ -20,11 +37,13 @@ export default defineConfig({ defaultHandler(warning); }, }, + }, resolve: { alias: { "../sdk/codex": "../proxy", "../../sdk/codex": "../../proxy", + "./port-forwarding.util": "../proxy", }, }, }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..d73ab47 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +/// +import { defineConfig } from 'vite' + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"] + }, +})