327 lines
8.6 KiB
TypeScript
Raw Normal View History

2024-10-10 10:31:30 +02:00
import {
2024-10-16 10:12:36 +02:00
Check,
2024-10-15 12:20:40 +02:00
Copy,
2024-10-10 10:31:30 +02:00
Download,
FilesIcon,
Folder,
Plus,
ReceiptText,
X,
} from "lucide-react";
2024-10-16 10:12:36 +02:00
import { ChangeEvent, useEffect, useState } from "react";
2024-09-16 12:38:56 +02:00
import { PrettyBytes } from "../../utils/bytes";
import { Dates } from "../../utils/dates";
2024-08-29 18:45:52 +02:00
import "./Files.css";
2024-10-16 10:12:36 +02:00
import { ICON_SIZE } from "../../utils/constants";
2024-08-29 18:45:52 +02:00
import {
ButtonIcon,
WebFileIcon,
Tabs,
2024-10-10 10:31:30 +02:00
Input,
Button,
TabProps,
2024-10-16 10:12:36 +02:00
Table,
Row,
Cell,
2024-08-30 17:26:27 +02:00
} from "@codex-storage/marketplace-ui-components";
2024-08-29 18:45:52 +02:00
import { FileDetails } from "./FileDetails.tsx";
import { useData } from "../../hooks/useData.tsx";
2024-10-10 10:31:30 +02:00
import { WebStorage } from "../../utils/web-storage.ts";
import { CodexSdk } from "../../sdk/codex.ts";
import { FolderButton } from "./FolderButton.tsx";
import { classnames } from "../../utils/classnames.ts";
2024-10-16 10:12:36 +02:00
import { CodexDataContent } from "@codex-storage/sdk-js";
import { Files as F } from "../../utils/files.ts";
2024-08-29 18:45:52 +02:00
2024-10-16 10:12:36 +02:00
type SortFn = (a: CodexDataContent, b: CodexDataContent) => number;
2024-08-29 18:45:52 +02:00
export function Files() {
const files = useData();
const [index, setIndex] = useState(0);
2024-10-10 10:31:30 +02:00
const [folder, setFolder] = useState("");
const [folders, setFolders] = useState<[string, string[]][]>([]);
const [error, setError] = useState("");
2024-10-16 10:12:36 +02:00
const [details, setDetails] = useState<CodexDataContent | null>(null);
const [sortFn, setSortFn] = useState<SortFn | null>(null);
const [filters, setFilters] = useState<string[]>([]);
2024-08-29 18:45:52 +02:00
useEffect(() => {
2024-10-10 10:31:30 +02:00
WebStorage.folders.list().then((items) => setFolders(items));
2024-08-29 18:45:52 +02:00
}, []);
const onClose = () => {
2024-10-16 10:12:36 +02:00
setDetails(null);
2024-08-29 18:45:52 +02:00
};
2024-10-10 10:31:30 +02:00
const onTabChange = async (i: number) => setIndex(i);
2024-08-29 18:45:52 +02:00
2024-10-16 10:12:36 +02:00
const onDetails = (cid: string) => {
const d = files.find((file) => file.cid === cid);
2024-08-29 18:45:52 +02:00
2024-10-16 10:12:36 +02:00
if (d) {
setDetails(d);
}
};
2024-10-10 10:31:30 +02:00
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;
}
2024-10-15 12:20:40 +02:00
if (folders.find(([folder]) => folder === val)) {
setError("This folder already exists");
return;
}
2024-08-29 18:45:52 +02:00
} else {
2024-10-10 10:31:30 +02:00
setError("9 alpha characters maximum");
2024-08-29 18:45:52 +02:00
}
};
2024-10-10 10:31:30 +02:00
const onFolderCreate = () => {
WebStorage.folders.create(folder);
2024-08-29 18:45:52 +02:00
2024-10-10 10:31:30 +02:00
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>,
2024-10-15 12:20:40 +02:00
IconAfter:
folder === "Favorites"
? undefined
: () => (
<X
size={"1rem"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onFolderDelete(folder);
}}></X>
),
2024-10-10 10:31:30 +02:00
}));
2024-10-16 10:12:36 +02:00
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]);
2024-10-15 12:20:40 +02:00
const onCopy = (cid: string) => navigator.clipboard.writeText(cid);
2024-08-29 18:45:52 +02:00
2024-10-10 10:31:30 +02:00
tabs.unshift({
label: "All files",
Icon: () => <FilesIcon size={"1rem"}></FilesIcon>,
});
2024-10-15 12:20:40 +02:00
2024-10-10 10:31:30 +02:00
const items =
index === 0
? files
: files.filter((file) => folders[index - 1][1].includes(file.cid));
2024-08-29 18:45:52 +02:00
2024-10-10 10:31:30 +02:00
const url = CodexSdk.url() + "/api/codex/v1/data/";
2024-08-29 18:45:52 +02:00
2024-10-16 10:12:36 +02:00
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>
)) || [];
2024-08-29 18:45:52 +02:00
return (
<div className="files">
<div className="files-header">
2024-10-10 10:31:30 +02:00
<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}
2024-10-15 12:20:40 +02:00
pattern="[A-Za-z0-9_\-]*"
2024-10-10 10:31:30 +02:00
maxLength={9}
helper={error || "Enter the folder name"}
onChange={onFolderChange}></Input>
</div>
<Button
label="Folder"
Icon={Plus}
disabled={!!error || !folder}
onClick={onFolderCreate}></Button>
</div>
2024-08-29 18:45:52 +02:00
</div>
2024-10-10 10:31:30 +02:00
<Tabs onTabChange={onTabChange} tabIndex={index} tabs={tabs}></Tabs>
2024-10-16 10:12:36 +02:00
<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>
2024-08-29 18:45:52 +02:00
<div className="files-fileBody">
2024-10-16 10:12:36 +02:00
<Table headers={headers as any} rows={rows} />
2024-08-29 18:45:52 +02:00
</div>
2024-10-16 10:12:36 +02:00
<FileDetails onClose={onClose} details={details} />
2024-08-29 18:45:52 +02:00
</div>
);
}