2024-11-01 20:23:47 +01:00

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