mirror of
https://github.com/codex-storage/codex-marketplace-ui.git
synced 2025-02-24 13:48:14 +00:00
Add folders feature
This commit is contained in:
parent
6c96cbe295
commit
10f6a8b1b3
125
package-lock.json
generated
125
package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"@sentry/react": "^8.31.0",
|
||||
"@tanstack/react-query": "^5.51.15",
|
||||
"@tanstack/react-router": "^1.58.7",
|
||||
"dotted-map": "^2.2.3",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.445.0",
|
||||
"react": "^18.3.1",
|
||||
@ -39,47 +40,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"../storybook": {
|
||||
"name": "@codex-storage/marketplace-ui-components",
|
||||
"version": "0.0.16",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.441.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^2.0.2",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-onboarding": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/react-vite": "^8.2.9",
|
||||
"@storybook/test": "^8.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.6.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"glob": "^9.3.5",
|
||||
"prettier": "^3.3.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"storybook": "^8.2.9",
|
||||
"typescript": "5.5.2",
|
||||
"vite-plugin-dts": "^4.0.3",
|
||||
"vite-plugin-lib-inject-css": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codex-storage/sdk-js": "^0.0.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
@ -383,8 +343,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@codex-storage/marketplace-ui-components": {
|
||||
"resolved": "../storybook",
|
||||
"link": true
|
||||
"version": "0.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@codex-storage/marketplace-ui-components/-/marketplace-ui-components-0.0.16.tgz",
|
||||
"integrity": "sha512-49RQB/ld3EIZrFr/1GL7NB/cEI7TjnHRIBD3LJqZ2/ml+0gj//Mep85yV9Df1owiE2yyFh76bAeC8rUTo9cA/g==",
|
||||
"dependencies": {
|
||||
"lucide-react": "^0.441.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codex-storage/sdk-js": "^0.0.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@codex-storage/marketplace-ui-components/node_modules/lucide-react": {
|
||||
"version": "0.441.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.441.0.tgz",
|
||||
"integrity": "sha512-0vfExYtvSDhkC2lqg0zYVW1Uu9GsI4knuV9GP9by5z0Xhc4Zi5RejTxfz9LsjRmCyWVzHCJvxGKZWcRyvQCWVg==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@codex-storage/sdk-js": {
|
||||
"version": "0.0.7",
|
||||
@ -1595,6 +1575,37 @@
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/boolean-point-in-polygon": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz",
|
||||
"integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "^6.5.0",
|
||||
"@turf/invariant": "^6.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/helpers": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz",
|
||||
"integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@turf/invariant": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz",
|
||||
"integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==",
|
||||
"dependencies": {
|
||||
"@turf/helpers": "^6.5.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/turf"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"dev": true,
|
||||
@ -2190,6 +2201,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotted-map": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/dotted-map/-/dotted-map-2.2.3.tgz",
|
||||
"integrity": "sha512-8hyOOHHLLVCcCisM3yb9hqp+3bJ7TSMcr1SfrUw8Wxp5UMqih35jIvUyagweCooJbz/EH1nC9GGuPysh7+YlAg==",
|
||||
"dependencies": {
|
||||
"@turf/boolean-point-in-polygon": "^6.0.1",
|
||||
"proj4": "^2.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"dev": true,
|
||||
@ -2899,6 +2919,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/mgrs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mgrs/-/mgrs-1.0.0.tgz",
|
||||
"integrity": "sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA=="
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
@ -3119,6 +3144,15 @@
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/proj4": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/proj4/-/proj4-2.12.1.tgz",
|
||||
"integrity": "sha512-vmhP3hmstjXjzFwg8QXJwpoj4n7GVrXk3ZW3DzNK/Ur4cuwXq7ZiMXaWYvLYLQbX8n4MXgbwTr4lthOUZltBpA==",
|
||||
"dependencies": {
|
||||
"mgrs": "1.0.0",
|
||||
"wkt-parser": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
@ -4019,6 +4053,11 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wkt-parser": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/wkt-parser/-/wkt-parser-1.3.3.tgz",
|
||||
"integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw=="
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"dev": true,
|
||||
|
@ -29,6 +29,7 @@
|
||||
"@sentry/react": "^8.31.0",
|
||||
"@tanstack/react-query": "^5.51.15",
|
||||
"@tanstack/react-router": "^1.58.7",
|
||||
"dotted-map": "^2.2.3",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"lucide-react": "^0.445.0",
|
||||
"react": "^18.3.1",
|
||||
|
@ -10,6 +10,7 @@ import { Dates } from "../../utils/dates";
|
||||
import { CidCopyButton } from "./CidCopyButton";
|
||||
import "./FileDetails.css";
|
||||
import { DownloadIcon, X } from "lucide-react";
|
||||
import { CodexSdk } from "../../sdk/codex";
|
||||
|
||||
type Props = {
|
||||
details: CodexDataContent | undefined;
|
||||
@ -18,7 +19,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export function FileDetails({ onClose, details, expanded }: Props) {
|
||||
const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
|
||||
const url = CodexSdk.url() + "/api/codex/v1/data/";
|
||||
|
||||
const Icon = () => <X size={ICON_SIZE} onClick={onClose} />;
|
||||
|
||||
|
@ -69,4 +69,24 @@
|
||||
|
||||
.files-header {
|
||||
margin-bottom: 0.75rem;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.files-headerLeft {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.files-headerRight {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.files-fileBody {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.files-folders {
|
||||
width: 200px;
|
||||
min-width: 0px;
|
||||
}
|
||||
|
@ -1,5 +1,14 @@
|
||||
import { Download, FilesIcon, ReceiptText, Star } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
Download,
|
||||
FilesIcon,
|
||||
Folder,
|
||||
Plus,
|
||||
ReceiptText,
|
||||
Star,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { ChangeEvent, useEffect, useRef, useState } from "react";
|
||||
import { PrettyBytes } from "../../utils/bytes";
|
||||
import { Dates } from "../../utils/dates";
|
||||
import "./Files.css";
|
||||
@ -9,10 +18,17 @@ import {
|
||||
EmptyPlaceholder,
|
||||
WebFileIcon,
|
||||
Tabs,
|
||||
Input,
|
||||
Button,
|
||||
TabProps,
|
||||
} 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";
|
||||
|
||||
type StarIconProps = {
|
||||
isFavorite: boolean;
|
||||
@ -32,11 +48,14 @@ export function Files() {
|
||||
const files = useData();
|
||||
const cid = useRef<string | null>("");
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [favorites, setFavorites] = useState<string[]>([]);
|
||||
const [index, setIndex] = useState(0);
|
||||
const [folder, setFolder] = useState("");
|
||||
const [folders, setFolders] = useState<[string, string[]][]>([]);
|
||||
const [error, setError] = useState("");
|
||||
const [details, setDetails] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
FavoriteStorage.list().then((cids) => setFavorites(cids));
|
||||
WebStorage.folders.list().then((items) => setFolders(items));
|
||||
}, []);
|
||||
|
||||
const onClose = () => {
|
||||
@ -47,59 +66,159 @@ export function Files() {
|
||||
}, SIDE_DURATION);
|
||||
};
|
||||
|
||||
const onTabChange = (i: number) => setIndex(i);
|
||||
const onTabChange = async (i: number) => setIndex(i);
|
||||
|
||||
const onDetails = (id: string) => {
|
||||
cid.current = id;
|
||||
setExpanded(true);
|
||||
};
|
||||
|
||||
const onToggleFavorite = (cid: string) => {
|
||||
if (favorites.includes(cid)) {
|
||||
FavoriteStorage.delete(cid);
|
||||
setFavorites(favorites.filter((c) => c !== cid));
|
||||
// const details = items.find((c) => c.cid === cid.current);
|
||||
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
FavoriteStorage.add(cid);
|
||||
setFavorites([...favorites, cid]);
|
||||
setError("9 alpha characters maximum");
|
||||
}
|
||||
};
|
||||
|
||||
const items = [];
|
||||
const onFolderCreate = () => {
|
||||
WebStorage.folders.create(folder);
|
||||
|
||||
if (index === 1) {
|
||||
items.push(...files.filter((f) => favorites.includes(f.cid)));
|
||||
} else {
|
||||
items.push(...files);
|
||||
}
|
||||
setFolder("");
|
||||
setFolders([...folders, [folder, []]]);
|
||||
};
|
||||
|
||||
const details = items.find((c) => c.cid === cid.current);
|
||||
const onFolderDelete = (val: string) => {
|
||||
WebStorage.folders.delete(val);
|
||||
|
||||
const url = import.meta.env.VITE_CODEX_API_URL + "/api/codex/v1/data/";
|
||||
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: () => (
|
||||
<X
|
||||
size={"1rem"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
onFolderDelete(folder);
|
||||
}}></X>
|
||||
),
|
||||
}));
|
||||
|
||||
const onToggleDetails = (cid: string) => {
|
||||
if (details.includes(cid)) {
|
||||
setDetails(details.filter((val) => val !== cid));
|
||||
} else {
|
||||
setDetails([...details, cid]);
|
||||
}
|
||||
};
|
||||
|
||||
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 url = CodexSdk.url() + "/api/codex/v1/data/";
|
||||
|
||||
return (
|
||||
<div className="files">
|
||||
<div className="files-header">
|
||||
<div className="files-title">Files</div>
|
||||
<Tabs
|
||||
onTabChange={onTabChange}
|
||||
tabIndex={index}
|
||||
tabs={[
|
||||
{
|
||||
label: "All files",
|
||||
Icon: () => <FilesIcon size={"1rem"}></FilesIcon>,
|
||||
},
|
||||
{
|
||||
label: "Favorites",
|
||||
Icon: () => <Star size={"1rem"}></Star>,
|
||||
},
|
||||
]}></Tabs>
|
||||
<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-z]*"
|
||||
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-fileBody">
|
||||
{items.length ? (
|
||||
items.map((c) => (
|
||||
<div className="files-file" key={c.cid}>
|
||||
<div className="files-fileContent">
|
||||
<ChevronDown
|
||||
onClick={() => onToggleDetails(c.cid)}
|
||||
className={classnames(
|
||||
["availabilityTable-chevron"],
|
||||
["availabilityTable-chevron--open", details.includes(c.cid)]
|
||||
)}></ChevronDown>
|
||||
|
||||
<div className="files-fileIcon">
|
||||
<WebFileIcon type={c.manifest.mimetype} />
|
||||
</div>
|
||||
@ -120,12 +239,13 @@ export function Files() {
|
||||
onClick={() => window.open(url + c.cid, "_blank")}
|
||||
Icon={() => <Download size={ICON_SIZE} />}></ButtonIcon>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={() => onToggleFavorite(c.cid)}
|
||||
Icon={() => (
|
||||
<StarIcon isFavorite={favorites.includes(c.cid)} />
|
||||
)}></ButtonIcon>
|
||||
<FolderButton
|
||||
folders={folders.map(([folder, files]) => [
|
||||
folder,
|
||||
files.includes(c.cid),
|
||||
])}
|
||||
onFolderToggle={(folder) => onFolderToggle(c.cid, folder)}
|
||||
/>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
@ -136,6 +256,14 @@ export function Files() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{details.includes(c.cid) && (
|
||||
<>
|
||||
<div>CID : {c.cid}</div>
|
||||
<div>Filename : {c.manifest.filename}</div>
|
||||
<div>Protected: {c.manifest.protected}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
@ -148,7 +276,7 @@ export function Files() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FileDetails onClose={onClose} details={details} expanded={expanded} />
|
||||
{/* <FileDetails onClose={onClose} details={details} expanded={expanded} /> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
38
src/components/Files/FolderButton.css
Normal file
38
src/components/Files/FolderButton.css
Normal file
@ -0,0 +1,38 @@
|
||||
.folderButton {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.folderButton-options {
|
||||
position: absolute;
|
||||
transform: translateY(200px);
|
||||
opacity: 0;
|
||||
transition:
|
||||
transform 0.25s,
|
||||
opacity 0.15s;
|
||||
background-color: var(--codex-background);
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--codex-border-radius);
|
||||
width: 150px;
|
||||
right: -40px;
|
||||
border: 1px solid var(--codex-border-color);
|
||||
}
|
||||
|
||||
.folderButton-options[aria-expanded] {
|
||||
z-index: 12;
|
||||
transform: translateY(0px);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.folderButton-option {
|
||||
padding: 0.75rem;
|
||||
transition: background-color 0.35s;
|
||||
cursor: pointer;
|
||||
border-radius: var(--codex-border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.folderButton-option:hover {
|
||||
background-color: var(--codex-background-light);
|
||||
}
|
53
src/components/Files/FolderButton.tsx
Normal file
53
src/components/Files/FolderButton.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import {
|
||||
Backdrop,
|
||||
ButtonIcon,
|
||||
SimpleText,
|
||||
} from "@codex-storage/marketplace-ui-components";
|
||||
import { CheckCircle, Folder } from "lucide-react";
|
||||
import "./FolderButton.css";
|
||||
import { useState } from "react";
|
||||
import { attributes } from "../../utils/attributes";
|
||||
|
||||
type Props = {
|
||||
folders: [string, boolean][];
|
||||
onFolderToggle: (folder: string) => void;
|
||||
};
|
||||
|
||||
export function FolderButton({ folders, onFolderToggle }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
const onOpen = () => setOpen(true);
|
||||
|
||||
const attr = attributes({ "aria-expanded": open });
|
||||
|
||||
return (
|
||||
<div className="folderButton">
|
||||
<Backdrop open={open} onClose={onClose}></Backdrop>
|
||||
|
||||
<ButtonIcon
|
||||
variant="small"
|
||||
onClick={onOpen}
|
||||
Icon={() => <Folder />}></ButtonIcon>
|
||||
|
||||
<div className="folderButton-options" {...attr}>
|
||||
{folders.map(([folder, isActive]) => (
|
||||
<div
|
||||
key={folder}
|
||||
className="folderButton-option"
|
||||
onClick={() => onFolderToggle(folder)}>
|
||||
<div>{folder}</div>
|
||||
<div>
|
||||
{isActive && (
|
||||
<SimpleText variant="primary">
|
||||
<CheckCircle size={"1rem"}></CheckCircle>
|
||||
</SimpleText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
import { del, get, set } from "idb-keyval";
|
||||
import { createStore, del, entries, get, keys, set } from "idb-keyval";
|
||||
|
||||
const folders = createStore("folders", "folders");
|
||||
|
||||
export const WebStorage = {
|
||||
set(key: string, value: unknown) {
|
||||
@ -12,4 +14,44 @@ export const WebStorage = {
|
||||
delete(key: string) {
|
||||
return del(key);
|
||||
},
|
||||
|
||||
folders: {
|
||||
create(folder: string) {
|
||||
return set(folder, [], folders);
|
||||
},
|
||||
|
||||
async list(): Promise<[string, string[]][]> {
|
||||
const items = await entries<string, string[]>(folders) || []
|
||||
|
||||
if (items.length == 0) {
|
||||
return [["Favorites", []]]
|
||||
}
|
||||
|
||||
if (items[0][0] !== "Favorites") {
|
||||
return [["Favorites", []], ...items]
|
||||
}
|
||||
|
||||
|
||||
return items
|
||||
|
||||
},
|
||||
|
||||
delete(key: string) {
|
||||
return del(key, folders);
|
||||
},
|
||||
|
||||
|
||||
async addFile(folder: string, cid: string) {
|
||||
const files = await get<string[]>(folder, folders) || []
|
||||
|
||||
return set(folder, [...files, cid], folders)
|
||||
},
|
||||
|
||||
async deleteFile(folder: string, cid: string) {
|
||||
const files = await get<string[]>(folder, folders) || []
|
||||
|
||||
return set(folder, files.filter(item => item !== cid), folders)
|
||||
|
||||
},
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user