mirror of
https://github.com/logos-storage/logos-storage-marketplace-ui.git
synced 2026-08-07 10:53:23 +00:00
289 lines
7.7 KiB
TypeScript
289 lines
7.7 KiB
TypeScript
import { FilesIcon, Folder, Plus, X } from "lucide-react";
|
|
import { ChangeEvent, useEffect, useState } from "react";
|
|
import { PrettyBytes } from "../../utils/bytes";
|
|
import { Dates } from "../../utils/dates";
|
|
import "./Files.css";
|
|
import {
|
|
Tabs,
|
|
Input,
|
|
Button,
|
|
TabProps,
|
|
Table,
|
|
Row,
|
|
Cell,
|
|
TabSortState,
|
|
} from "@codex-storage/marketplace-ui-components";
|
|
import { FileDetails } from "./FileDetails.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 { Files as F } from "../../utils/files.ts";
|
|
import { FilterFilters } from "./FileFilters.tsx";
|
|
import { FileCell } from "./FileCell.tsx";
|
|
import { FileActions } from "./FileActions.tsx";
|
|
|
|
type SortFn = (a: CodexDataContent, b: CodexDataContent) => number;
|
|
|
|
export function Files() {
|
|
const files = useData();
|
|
const [index, setIndex] = useState(0);
|
|
const [folder, setFolder] = useState("");
|
|
const [folders, setFolders] = useState<[string, string[]][]>([]);
|
|
const [error, setError] = useState("");
|
|
const [details, setDetails] = useState<CodexDataContent | null>(null);
|
|
const [sortFn, setSortFn] = useState<SortFn | null>(null);
|
|
const [selectedFilters, setSelectedFilters] = useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
WebStorage.folders.list().then((items) => setFolders(items));
|
|
}, []);
|
|
|
|
const onClose = () => setDetails(null);
|
|
|
|
const onTabChange = async (i: number) => setIndex(i);
|
|
|
|
const onDetails = (cid: string) => {
|
|
const d = files.find((file) => file.cid === cid);
|
|
|
|
if (d) {
|
|
setDetails(d);
|
|
}
|
|
};
|
|
|
|
const onFolderChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
const val = e.currentTarget.value;
|
|
setFolder(val);
|
|
setError("");
|
|
|
|
if (!val) {
|
|
return;
|
|
}
|
|
|
|
if (e.currentTarget.checkValidity()) {
|
|
if (folders.length >= 5) {
|
|
setError("5 folders limit reached");
|
|
return;
|
|
}
|
|
|
|
if (folders.find(([folder]) => folder === 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(
|
|
folders.map(([name, files]) =>
|
|
name === folder
|
|
? [name, files.filter((id) => id !== cid)]
|
|
: [name, files]
|
|
)
|
|
);
|
|
} else {
|
|
WebStorage.folders.addFile(folder, cid);
|
|
|
|
setFolders(
|
|
folders.map(([name, files]) =>
|
|
name === folder ? [name, [...files, cid]] : [name, files]
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
const tabs: TabProps[] = folders.map(([folder]) => ({
|
|
label: folder,
|
|
Icon: () => <Folder size={"1rem"}></Folder>,
|
|
IconAfter:
|
|
folder === "Favorites"
|
|
? undefined
|
|
: () => (
|
|
<X
|
|
size={"1rem"}
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
onFolderDelete(folder);
|
|
}}></X>
|
|
),
|
|
}));
|
|
|
|
const onSortByFilename = (state: TabSortState) => {
|
|
if (!state) {
|
|
setSortFn(null);
|
|
return;
|
|
}
|
|
|
|
setSortFn(
|
|
() =>
|
|
(
|
|
{ manifest: { filename: afilename } }: CodexDataContent,
|
|
{ manifest: { filename: bfilename } }: CodexDataContent
|
|
) =>
|
|
state === "desc"
|
|
? (bfilename || "")
|
|
.toLocaleLowerCase()
|
|
.localeCompare((afilename || "").toLocaleLowerCase())
|
|
: (afilename || "")
|
|
.toLocaleLowerCase()
|
|
.localeCompare((bfilename || "").toLocaleLowerCase())
|
|
);
|
|
};
|
|
|
|
const onSortBySize = (state: TabSortState) => {
|
|
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: TabSortState) => {
|
|
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) =>
|
|
selectedFilters.includes(filter)
|
|
? setSelectedFilters(selectedFilters.filter((f) => f !== filter))
|
|
: setSelectedFilters([...selectedFilters, filter]);
|
|
|
|
tabs.unshift({
|
|
label: "All files",
|
|
Icon: () => <FilesIcon size={"1rem"}></FilesIcon>,
|
|
});
|
|
|
|
const items =
|
|
index === 0
|
|
? files
|
|
: files.filter((file) => folders[index - 1][1].includes(file.cid));
|
|
|
|
const headers = [
|
|
["file", onSortByFilename],
|
|
["size", onSortBySize],
|
|
["date", onSortByDate],
|
|
["actions"],
|
|
] satisfies [string, ((state: TabSortState) => void)?][];
|
|
|
|
const filtered = items.filter(
|
|
(item) =>
|
|
selectedFilters.length === 0 ||
|
|
selectedFilters.includes(F.type(item.manifest.mimetype)) ||
|
|
(selectedFilters.includes("archive") &&
|
|
F.isArchive(item.manifest.mimetype))
|
|
);
|
|
|
|
const sorted = sortFn ? [...filtered].sort(sortFn) : filtered;
|
|
const rows =
|
|
sorted.map((c) => (
|
|
<Row
|
|
cells={[
|
|
<FileCell content={c}></FileCell>,
|
|
<Cell>{PrettyBytes(c.manifest.datasetSize)}</Cell>,
|
|
<Cell>{Dates.format(c.manifest.uploadedAt).toString()}</Cell>,
|
|
<FileActions
|
|
content={c}
|
|
folders={folders}
|
|
onDetails={onDetails}
|
|
onFolderToggle={onFolderToggle}></FileActions>,
|
|
]}></Row>
|
|
)) || [];
|
|
|
|
return (
|
|
<div className="files">
|
|
<div className="files-header">
|
|
<div className="files-headerLeft">
|
|
<div className="files-title">Files</div>
|
|
</div>
|
|
<div className="files-headerRight">
|
|
<div>
|
|
<Input
|
|
id="folder"
|
|
inputClassName={classnames(["files-folders"])}
|
|
isInvalid={folder !== "" && !!error}
|
|
value={folder}
|
|
required={true}
|
|
pattern="[A-Za-z0-9_\-]*"
|
|
maxLength={9}
|
|
helper={error || "Enter the folder name"}
|
|
onChange={onFolderChange}></Input>
|
|
</div>
|
|
|
|
<Button
|
|
label="Folder"
|
|
Icon={Plus}
|
|
disabled={!!error || !folder}
|
|
onClick={onFolderCreate}></Button>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs onTabChange={onTabChange} tabIndex={index} tabs={tabs}></Tabs>
|
|
|
|
<div className="files-filters">
|
|
<FilterFilters
|
|
files={files}
|
|
onFilterToggle={onToggleFilter}
|
|
selected={selectedFilters}
|
|
/>
|
|
</div>
|
|
|
|
<div className="files-fileBody">
|
|
<Table headers={headers} rows={rows} defaultSortIndex={2} />
|
|
</div>
|
|
|
|
<FileDetails onClose={onClose} details={details} />
|
|
</div>
|
|
);
|
|
}
|