Improve the folders featurfe

This commit is contained in:
Arnaud 2024-10-16 10:12:36 +02:00
parent c119a5cbf7
commit c70376bfcb
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
10 changed files with 244 additions and 122 deletions

8
package-lock.json generated
View File

@ -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"
},

View File

@ -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",

View File

@ -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}
/>
<Sheets open={state.open} onClose={onClose}>
<Modal open={state.open} onClose={onClose}>
<Stepper
className="availabilityCreate"
titles={steps.current}
@ -159,7 +159,7 @@ export function AvailabilitySheetCreate({
error={error}
/>
</Stepper>
</Sheets>
</Modal>
</>
);
}

View File

@ -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 = () => <X size={ICON_SIZE} onClick={onClose} />;
@ -26,7 +38,7 @@ export function FileDetails({ onClose, details, expanded }: Props) {
const onDownload = () => window.open(url + details?.cid, "_target");
return (
<Sheets open={expanded} onClose={onClose}>
<Sheets open={!!details} onClose={onClose}>
<>
{details && (
<>
@ -35,6 +47,19 @@ export function FileDetails({ onClose, details, expanded }: Props) {
<ButtonIcon variant="small" Icon={Icon}></ButtonIcon>
</div>
{Files.isImage(details.manifest.mimetype) && (
<div className="fileDetails-imageContainer">
<img
className="fileDetails-image"
src={
import.meta.env.VITE_CODEX_API_URL +
"/api/codex/v1/data/" +
details.cid
}
/>
</div>
)}
<div className="fileDetails-body">
<div className="fileDetails-grid">
<p className="text-secondary">CID:</p>
@ -76,6 +101,13 @@ export function FileDetails({ onClose, details, expanded }: Props) {
</p>
</div>
<div className="fileDetails-grid">
<p className="text-secondary">Used:</p>
<p className="fileDetails-gridColumn">
{purchases + " purchase(s)"}
</p>
</div>
<div className="fileDetails-actions">
<CidCopyButton cid={details.cid} />

View File

@ -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;
}

View File

@ -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 (
<Star size={ICON_SIZE} className="files-fileFavorite files-fileStar" />
);
}
return <Star size={ICON_SIZE} className="files-star" />;
}
type SortFn = (a: CodexDataContent, b: CodexDataContent) => number;
export function Files() {
const files = useData();
const cid = useRef<string | null>("");
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<string[]>([]);
const [details, setDetails] = useState<CodexDataContent | null>(null);
const [sortFn, setSortFn] = useState<SortFn | null>(null);
const [filters, setFilters] = useState<string[]>([]);
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<HTMLInputElement>) => {
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) => (
<Row
cells={[
<Cell>
<div className="files-cell-file">
<WebFileIcon type={c.manifest.mimetype} />
<div>
<b>{c.manifest.filename}</b>
<div>
<small className="files-fileMeta">{c.cid}</small>
</div>
</div>
</div>
</Cell>,
<Cell>{PrettyBytes(c.manifest.datasetSize)}</Cell>,
<Cell>{Dates.format(c.manifest.uploadedAt).toString()}</Cell>,
<Cell>
<div className="files-fileActions">
<ButtonIcon
variant="small"
animation="bounce"
onClick={() => window.open(url + c.cid, "_blank")}
Icon={(props) => (
<Download size={ICON_SIZE} {...props} />
)}></ButtonIcon>
<FolderButton
folders={folders.map(([folder, files]) => [
folder,
files.includes(c.cid),
])}
onFolderToggle={(folder) => onFolderToggle(c.cid, folder)}
/>
<ButtonIcon
variant="small"
onClick={() => onCopy(c.cid)}
animation="buzz"
Icon={(props) => (
<Copy size={ICON_SIZE} {...props} />
)}></ButtonIcon>
<ButtonIcon
variant="small"
onClick={() => onDetails(c.cid)}
Icon={() => <ReceiptText size={ICON_SIZE} />}></ButtonIcon>
</div>
</Cell>,
]}></Row>
)) || [];
return (
<div className="files">
<div className="files-header">
@ -219,68 +302,25 @@ export function Files() {
<Tabs onTabChange={onTabChange} tabIndex={index} tabs={tabs}></Tabs>
<div className="files-fileBody">
{items.length ? (
items.map((c) => (
<div className="files-file" key={c.cid}>
<div className="files-fileContent">
<div className="files-fileIcon">
<WebFileIcon type={c.manifest.mimetype} />
</div>
<div className="files-fileData">
<div>
<b>{c.manifest.filename}</b>
<div>
<small className="files-fileMeta">
{PrettyBytes(c.manifest.datasetSize)} -{" "}
{Dates.format(c.manifest.uploadedAt).toString()} - ...
{c.cid.slice(-5)}
</small>
</div>
</div>
<div className="files-fileActions">
<ButtonIcon
variant="small"
animation="bounce"
onClick={() => window.open(url + c.cid, "_blank")}
Icon={() => <Download size={ICON_SIZE} />}></ButtonIcon>
<FolderButton
folders={folders.map(([folder, files]) => [
folder,
files.includes(c.cid),
])}
onFolderToggle={(folder) => onFolderToggle(c.cid, folder)}
/>
<ButtonIcon
variant="small"
onClick={() => onCopy(c.cid)}
animation="buzz"
Icon={() => <Copy size={ICON_SIZE} />}></ButtonIcon>
<ButtonIcon
variant="small"
onClick={() => onDetails(c.cid)}
Icon={() => (
<ReceiptText size={ICON_SIZE} />
)}></ButtonIcon>
</div>
</div>
</div>
</div>
))
) : (
<div className="files-placeholder">
<EmptyPlaceholder
title="Nothing to show"
message="No data here yet. Start to upload files to see data here."
/>
</div>
)}
<div className="files-filters">
{types.map((type) => (
<span
key={type}
className={classnames(
["files-filter"],
["files-filter--active", filters.includes(type)]
)}
onClick={() => onToggleFilter(type)}>
<span>{type}</span> <Check size={"1rem"}></Check>
</span>
))}
</div>
{/* <FileDetails onClose={onClose} details={details} expanded={expanded} /> */}
<div className="files-fileBody">
<Table headers={headers as any} rows={rows} />
</div>
<FileDetails onClose={onClose} details={details} />
</div>
);
}

View File

@ -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,

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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");