From c70376bfcbcc2f33389e5b76ced604c0f4287a20 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 16 Oct 2024 10:12:36 +0200 Subject: [PATCH] Improve the folders featurfe --- package-lock.json | 8 +- package.json | 4 +- .../Availability/AvailabilitySheetCreate.tsx | 6 +- src/components/Files/FileDetails.tsx | 40 ++- src/components/Files/Files.css | 53 +++- src/components/Files/Files.tsx | 238 ++++++++++-------- src/proxy.ts | 5 + src/utils/files.ts | 4 + src/utils/purchases-storage.ts | 6 +- src/utils/web-storage.ts | 2 +- 10 files changed, 244 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0824cf4..6033228 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.5", "license": "MIT", "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.19", + "@codex-storage/marketplace-ui-components": "^0.0.21", "@codex-storage/sdk-js": "^0.0.8", "@sentry/browser": "^8.32.0", "@sentry/react": "^8.31.0", @@ -354,9 +354,9 @@ "dev": true }, "node_modules/@codex-storage/marketplace-ui-components": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.19.tgz", - "integrity": "sha512-b0zrinWNvsi/R95BwPH6pwkEKGyP1fZktWtagbD94N7FJ0W97Tc+TJmrLLcBGx/wal3J6e9DnLHYSl4iMrf11Q==", + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.21.tgz", + "integrity": "sha512-DHy/sGD4pwBG7ysnHZgY/Tl0EGAUVM4tbWeqcJ3jeTPORI+arnoH4gB0zdkNNY1/JuGH84CDqJdwam94WBJqhQ==", "dependencies": { "lucide-react": "^0.441.0" }, diff --git a/package.json b/package.json index 8ecf9b6..f214ca8 100644 --- a/package.json +++ b/package.json @@ -24,14 +24,14 @@ "React" ], "dependencies": { - "@codex-storage/marketplace-ui-components": "^0.0.19", + "@codex-storage/marketplace-ui-components": "^0.0.21", "@codex-storage/sdk-js": "^0.0.8", "@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", "idb-keyval": "^6.2.1", "lucide-react": "^0.445.0", "react": "^18.3.1", diff --git a/src/components/Availability/AvailabilitySheetCreate.tsx b/src/components/Availability/AvailabilitySheetCreate.tsx index 3fc91e7..86dda20 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,7 +140,7 @@ export function AvailabilitySheetCreate({ className={className} /> - + - + ); } diff --git a/src/components/Files/FileDetails.tsx b/src/components/Files/FileDetails.tsx index 6de4a6c..82b2089 100644 --- a/src/components/Files/FileDetails.tsx +++ b/src/components/Files/FileDetails.tsx @@ -11,14 +11,26 @@ import { CidCopyButton } from "./CidCopyButton"; import "./FileDetails.css"; import { DownloadIcon, X } from "lucide-react"; import { CodexSdk } from "../../sdk/codex"; +import { Files } from "../../utils/files"; +import { useEffect, useState } from "react"; +import { PurchaseStorage } from "../../utils/purchases-storage"; type Props = { - details: CodexDataContent | undefined; + details: CodexDataContent | null; onClose: () => void; - expanded: boolean; }; -export function FileDetails({ onClose, details, expanded }: Props) { +export function FileDetails({ onClose, details }: Props) { + const [purchases, setPurchases] = useState(0); + + useEffect(() => { + PurchaseStorage.entries().then((entries) => + setPurchases( + entries.filter((e) => e[1] === details?.cid).reduce((acc) => acc + 1, 0) + ) + ); + }, [details?.cid]); + const url = CodexSdk.url() + "/api/codex/v1/data/"; const Icon = () => ; @@ -26,7 +38,7 @@ export function FileDetails({ onClose, details, expanded }: Props) { const onDownload = () => window.open(url + details?.cid, "_target"); return ( - + <> {details && ( <> @@ -35,6 +47,19 @@ export function FileDetails({ onClose, details, expanded }: Props) { + {Files.isImage(details.manifest.mimetype) && ( +
+ +
+ )} +

CID:

@@ -76,6 +101,13 @@ export function FileDetails({ onClose, details, expanded }: Props) {

+
+

Used:

+

+ {purchases + " purchase(s)"} +

+
+
diff --git a/src/components/Files/Files.css b/src/components/Files/Files.css index 02a9d19..8506c30 100644 --- a/src/components/Files/Files.css +++ b/src/components/Files/Files.css @@ -6,6 +6,16 @@ margin-bottom: 1rem; } +.files-cell-file { + display: flex; + align-items: center; + gap: 1rem; +} + +.files-fileMeta { + word-break: break-all; +} + .files-title { font-weight: bold; font-size: 1.125rem; @@ -40,15 +50,8 @@ height: 2rem; } -.files-fileData { - display: flex; - justify-content: space-between; - align-items: center; - flex-grow: 1; -} - .files-fileActions { - display: flex; + display: inline-flex; align-items: center; border: 1px solid var(--codex-border-color); border-radius: var(--codex-border-radius); @@ -90,3 +93,37 @@ width: 200px; min-width: 0px; } + +.files-filters { + display: flex; + margin: 1rem 0; + gap: 1rem; +} + +.files-filter { + padding: 0.25rem 0.5rem; + background-color: var(--codex-background-light); + border-radius: var(--codex-border-radius); + border: 1px solid var(--codex-border-color); + opacity: 0.5; + display: inline-flex; + align-items: center; + gap: 1rem; + transition: opacity 0.35s; + cursor: pointer; +} + +.files-filter--active { + opacity: 1; +} + +.fileDetails-imageContainer { + display: flex; + justify-content: center; +} + +.fileDetails-image { + max-width: 200px; + max-height: 200px; + margin: auto; +} diff --git a/src/components/Files/Files.tsx b/src/components/Files/Files.tsx index 76281a7..706a832 100644 --- a/src/components/Files/Files.tsx +++ b/src/components/Files/Files.tsx @@ -1,80 +1,67 @@ import { - ChevronDown, + Check, Copy, Download, FilesIcon, Folder, Plus, ReceiptText, - Star, X, } from "lucide-react"; -import { ChangeEvent, 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 { ICON_SIZE } from "../../utils/constants"; import { ButtonIcon, - EmptyPlaceholder, WebFileIcon, Tabs, Input, Button, TabProps, + Table, + Row, + Cell, } 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 { CodexSdk } from "../../sdk/codex.ts"; import { FolderButton } from "./FolderButton.tsx"; import { classnames } from "../../utils/classnames.ts"; +import { CodexDataContent } from "@codex-storage/sdk-js"; +import { Files as F } from "../../utils/files.ts"; -type StarIconProps = { - isFavorite: boolean; -}; - -function StarIcon({ isFavorite }: StarIconProps) { - if (isFavorite) { - return ( - - ); - } - - return ; -} +type SortFn = (a: CodexDataContent, b: CodexDataContent) => number; export function Files() { const files = useData(); - const cid = useRef(""); - const [expanded, setExpanded] = useState(false); const [index, setIndex] = useState(0); const [folder, setFolder] = useState(""); const [folders, setFolders] = useState<[string, string[]][]>([]); const [error, setError] = useState(""); - const [details, setDetails] = useState([]); + const [details, setDetails] = useState(null); + const [sortFn, setSortFn] = useState(null); + const [filters, setFilters] = useState([]); useEffect(() => { WebStorage.folders.list().then((items) => setFolders(items)); }, []); const onClose = () => { - setExpanded(false); - - setTimeout(() => { - cid.current = ""; - }, SIDE_DURATION); + setDetails(null); }; const onTabChange = async (i: number) => setIndex(i); - const onDetails = (id: string) => { - cid.current = id; - setExpanded(true); - }; + const onDetails = (cid: string) => { + const d = files.find((file) => file.cid === cid); - // const details = items.find((c) => c.cid === cid.current); + if (d) { + setDetails(d); + } + }; const onFolderChange = (e: ChangeEvent) => { const val = e.currentTarget.value; @@ -167,13 +154,40 @@ export function Files() { ), })); - // const onToggleDetails = (cid: string) => { - // if (details.includes(cid)) { - // setDetails(details.filter((val) => val !== cid)); - // } else { - // setDetails([...details, cid]); - // } - // }; + const onSortBySize = (state: "" | "asc" | "desc") => { + if (!state) { + setSortFn(null); + return; + } + + setSortFn( + () => (a: CodexDataContent, b: CodexDataContent) => + state === "desc" + ? b.manifest.datasetSize - a.manifest.datasetSize + : a.manifest.datasetSize - b.manifest.datasetSize + ); + }; + + const onSortByDate = (state: "" | "asc" | "desc") => { + if (!state) { + setSortFn(null); + return; + } + + setSortFn( + () => (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() + ); + }; + + const onToggleFilter = (filter: string) => + filters.includes(filter) + ? setFilters(filters.filter((f) => f !== filter)) + : setFilters([...filters, filter]); const onCopy = (cid: string) => navigator.clipboard.writeText(cid); @@ -189,6 +203,75 @@ export function Files() { const url = CodexSdk.url() + "/api/codex/v1/data/"; + const headers = [ + ["file"], + ["size", onSortBySize], + ["date", onSortByDate], + ["actions"], + ]; + + const types = Array.from( + new Set(files.map((file) => F.type(file.manifest.mimetype))) + ); + const filtered = items.filter( + (item) => + filters.length === 0 || filters.includes(F.type(item.manifest.mimetype)) + ); + + const sorted = sortFn ? [...filtered].sort(sortFn) : filtered; + const rows = + sorted.map((c) => ( + +
+ + +
+ {c.manifest.filename} +
+ {c.cid} +
+
+
+ , + {PrettyBytes(c.manifest.datasetSize)}, + {Dates.format(c.manifest.uploadedAt).toString()}, + +
+ window.open(url + c.cid, "_blank")} + Icon={(props) => ( + + )}> + + [ + folder, + files.includes(c.cid), + ])} + onFolderToggle={(folder) => onFolderToggle(c.cid, folder)} + /> + + onCopy(c.cid)} + animation="buzz" + Icon={(props) => ( + + )}> + + onDetails(c.cid)} + Icon={() => }> +
+
, + ]}>
+ )) || []; + return (
@@ -219,68 +302,25 @@ export function Files() { -
- {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={() => }> - - [ - folder, - files.includes(c.cid), - ])} - onFolderToggle={(folder) => onFolderToggle(c.cid, folder)} - /> - - onCopy(c.cid)} - animation="buzz" - Icon={() => }> - - onDetails(c.cid)} - Icon={() => ( - - )}> -
-
-
-
- )) - ) : ( -
- -
- )} +
+ {types.map((type) => ( + onToggleFilter(type)}> + {type} + + ))}
- {/* */} +
+ + + + ); } diff --git a/src/proxy.ts b/src/proxy.ts index 04a8f54..a82ace6 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -22,6 +22,11 @@ class CodexDataMock extends CodexData { abort, result: result.then((safe) => { if (!safe.error) { + FilesStorage.set(safe.data, { + mimetype: file.type, + uploadedAt: new Date().toJSON(), + name: file.name, + }) return WebStorage.set(safe.data, { type: file.type, name: file.name, diff --git a/src/utils/files.ts b/src/utils/files.ts index 7ab1ad7..44dd6ec 100644 --- a/src/utils/files.ts +++ b/src/utils/files.ts @@ -2,6 +2,10 @@ export const Files = { isImage(type: string) { return type.startsWith("image"); }, + type(mimetype: string) { + const [type] = mimetype.split("/") + return type + } }; export type CodexFileMetadata = { diff --git a/src/utils/purchases-storage.ts b/src/utils/purchases-storage.ts index f3a278a..3aafea1 100644 --- a/src/utils/purchases-storage.ts +++ b/src/utils/purchases-storage.ts @@ -1,4 +1,4 @@ -import { createStore, get, set } from "idb-keyval"; +import { createStore, entries, get, set } from "idb-keyval"; const store = createStore("purchases", "purchases"); const storeDates = createStore("purchases", "dates"); @@ -11,6 +11,10 @@ export const PurchaseStorage = { async set(key: string, cid: string) { return set(key, cid, store); }, + + async entries() { + return entries(store); + }, }; export const PurchaseDatesStorage = { diff --git a/src/utils/web-storage.ts b/src/utils/web-storage.ts index bcf5751..56927cb 100644 --- a/src/utils/web-storage.ts +++ b/src/utils/web-storage.ts @@ -1,4 +1,4 @@ -import { createStore, del, entries, get, keys, set } from "idb-keyval"; +import { createStore, del, entries, get, set } from "idb-keyval"; const folders = createStore("folders", "folders");