diff --git a/.vscode/settings.json b/.vscode/settings.json
index c5b5586..a3b5838 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,10 +2,12 @@
"editor.tabSize": 2,
"editor.insertSpaces": true,
"workbench.editor.customLabels.patterns": {
- "**/route.*": "${dirname}.${extname} (${filename})",
- "**/index.*": "${dirname}.${extname} (${filename})"
+ "{**/index.*,**/route.*,**/layout.*,**/page.*}": "${dirname}.${extname} (${filename})"
},
"cSpell.words": [
+ "nextjs",
+ "tailwindcss",
+ "Topbar",
"zustand"
]
}
diff --git a/app/api/folders/route.ts b/app/api/folders/route.ts
index 62b0fe4..2ebfc72 100644
--- a/app/api/folders/route.ts
+++ b/app/api/folders/route.ts
@@ -7,8 +7,14 @@ export async function POST(request: Request) {
const { name, path } = await request.json();
console.debug("Received data:", { name, path });
+ // Normalization
+ let fmtPath = path.replace(/\\/g, "/"); // Replace backslashes with forward
+ if (!fmtPath.endsWith("/")) {
+ fmtPath += "/"; // Always add trailing slash
+ }
+
const created = await prisma.folder.create({
- data: { name, path },
+ data: { name, path: fmtPath },
});
return NextResponse.json(created);
@@ -25,9 +31,15 @@ export async function PATCH(request: Request) {
try {
const { id, name, path } = await request.json();
+ // Normalization
+ let fmtPath = path.replace(/\\/g, "/"); // Replace backslashes with forward
+ if (!fmtPath.endsWith("/")) {
+ fmtPath += "/"; // Always add trailing slash
+ }
+
const updated = await prisma.folder.update({
where: { id },
- data: { name, path },
+ data: { name, path: fmtPath },
});
return NextResponse.json(updated);
diff --git a/app/api/items/route.ts b/app/api/items/route.ts
index 21c83fc..c8d9cbb 100644
--- a/app/api/items/route.ts
+++ b/app/api/items/route.ts
@@ -6,8 +6,11 @@ export async function POST(request: Request) {
const { path, folderId, name, starred } = await request.json();
console.debug("Received data:", { path, name, starred });
+ // Normalization
+ const fmtPath = path.replace(/\\/g, "/"); // Replace backslashes with forward
+
const item = await prisma.item.create({
- data: { path, folderId, name, starred },
+ data: { path: fmtPath, folderId, name, starred },
});
return NextResponse.json(item);
@@ -19,17 +22,14 @@ export async function POST(request: Request) {
export async function PATCH(request: Request) {
try {
- const {
- id,
- basePathId: folderId,
- path,
- name,
- starred,
- } = await request.json();
+ const { id, folderId, path, name, starred } = await request.json();
+
+ // Normalization
+ const fmtPath = path.replace(/\\/g, "/"); // Replace backslashes with forward
const item = await prisma.item.update({
where: { id },
- data: { path, folderId, name, starred },
+ data: { path: fmtPath, folderId, name, starred },
});
return NextResponse.json(item);
diff --git a/app/components/explorer/imageLoader/index.tsx b/app/components/explorer/imageLoader/index.tsx
index 7ffffad..e6c29f7 100644
--- a/app/components/explorer/imageLoader/index.tsx
+++ b/app/components/explorer/imageLoader/index.tsx
@@ -2,13 +2,17 @@
import { useState, useEffect } from "react";
import { loadImage } from "@/app/lib/imageLoader";
+import { Skeleton } from "@mui/material";
interface Props {
path: string;
+ alt?: string;
+ width?: number;
+ height?: number;
}
export default function ImageLoader(props: Props) {
- const { path } = props;
+ const { path, alt, width, height } = props;
const [imageSrc, setImageSrc] = useState("");
useEffect(() => {
@@ -24,11 +28,13 @@ export default function ImageLoader(props: Props) {
{imageSrc ? (
) : (
-
No image selected
+
)}
);
diff --git a/app/components/explorer/index.tsx b/app/components/explorer/index.tsx
index ddc9932..6b6a0df 100644
--- a/app/components/explorer/index.tsx
+++ b/app/components/explorer/index.tsx
@@ -8,17 +8,17 @@ import { getItem } from "@/app/lib/items";
import { Item } from "@/app/lib/db/types";
import { ImageList, ImageListItem } from "@mui/material";
+const size = 250;
+
export default function Explorer() {
const subscribeSelected = useTagTreeState((s) => s.subscribeSelected);
- const [id, setId] = useState(0);
- const [img, setImg] = useState([]);
+ const [imagePaths, setImagePaths] = useState([]);
useEffect(() => {
subscribeSelected(onSelected);
}, []);
const onSelected = async (id: number) => {
- setId(id);
const tag = await getTag(id, true, true, true);
if (tag === null) return;
if (!tag.items) return;
@@ -33,19 +33,20 @@ export default function Explorer() {
);
items = items.filter((item) => item !== null);
- console.log(items);
-
const paths = items.map((i) => i!.path);
- console.log(paths);
- setImg(paths);
+ setImagePaths(paths);
} catch (err) {}
};
return (
- {img.map((p) => (
-
- ))}
+
+ {imagePaths.map((path, index) => (
+
+
+
+ ))}
+
);
}
diff --git a/app/components/sidebar/index.tsx b/app/components/sidebar/index.tsx
index e282622..1be084f 100644
--- a/app/components/sidebar/index.tsx
+++ b/app/components/sidebar/index.tsx
@@ -5,6 +5,7 @@ import {
Box,
Divider,
Drawer,
+ Link,
List,
ListItem,
ListItemButton,
@@ -21,6 +22,12 @@ import { useTagTreeState } from "@/app/store/tagTree";
const drawerWidth = 240;
const collapsedWidth = 80;
+const NavList = [
+ { label: "Home", url: "/explorer" },
+ { label: "Database", url: "/database" },
+ { label: "Docs", url: "/api/docs" },
+];
+
export default function Sidebar() {
const [open, setOpen] = React.useState(true);
const { tagTreeItems, updateTagTree, updateSelectedTagId } =
@@ -59,12 +66,9 @@ export default function Sidebar() {
- {["Inbox", "Starred", "Send email", "Drafts"].map((text, index) => (
-
-
-
-
-
+ {NavList.map((item, index) => (
+
+ {item.label}
))}
diff --git a/app/components/tag/Button.tsx b/app/components/tag/Button.tsx
deleted file mode 100644
index 94cbde3..0000000
--- a/app/components/tag/Button.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import React from "react";
-import FormDialog from "./FormDialog";
-import { Button } from "@mui/material";
-
-export default function TagButton() {
- const [open, setOpen] = React.useState(false);
-
- const handleClick = () => {
- setOpen(true);
- };
-
- const handleClose = () => {
- setOpen(false);
- };
-
- return (
-
-
-
-
- );
-}
diff --git a/app/database/databaseTable/dbTableHead/index.tsx b/app/database/databaseTable/dbTableHead/index.tsx
new file mode 100644
index 0000000..bdf605a
--- /dev/null
+++ b/app/database/databaseTable/dbTableHead/index.tsx
@@ -0,0 +1,70 @@
+"use client";
+
+import {
+ TableHead,
+ TableRow,
+ TableCell,
+ Checkbox,
+ TableSortLabel,
+} from "@mui/material";
+import { Order, Data, HeadCell } from "@/app/database/databaseTable/types";
+
+interface Props {
+ heads: HeadCell[];
+ numSelected: number;
+ order: Order;
+ orderBy: number;
+ rowCount: number;
+ onRequestSort: (event: React.MouseEvent, property: number) => void;
+ onSelectAllClick: (event: React.ChangeEvent) => void;
+}
+
+export default function DbTableHead(props: Props) {
+ const {
+ heads,
+ onSelectAllClick,
+ order,
+ orderBy,
+ numSelected,
+ rowCount,
+ onRequestSort,
+ } = props;
+ const createSortHandler =
+ (property: number) => (event: React.MouseEvent) => {
+ onRequestSort(event, property);
+ };
+
+ return (
+
+
+
+ 0 && numSelected < rowCount}
+ checked={rowCount > 0 && numSelected === rowCount}
+ onChange={onSelectAllClick}
+ inputProps={{
+ "aria-label": "select all desserts",
+ }}
+ />
+
+ {heads.map((headCell) => (
+
+
+ {headCell.label}
+
+
+ ))}
+
+
+ );
+}
diff --git a/app/database/databaseTable/dbTableToolbar/index.tsx b/app/database/databaseTable/dbTableToolbar/index.tsx
new file mode 100644
index 0000000..4bab4e1
--- /dev/null
+++ b/app/database/databaseTable/dbTableToolbar/index.tsx
@@ -0,0 +1,63 @@
+"use client";
+
+import { alpha, IconButton, Toolbar, Tooltip, Typography } from "@mui/material";
+import DeleteIcon from "@mui/icons-material/Delete";
+import FilterListIcon from "@mui/icons-material/FilterList";
+
+interface Props {
+ numSelected: number;
+ title:string;
+}
+
+export default function DbTableToolbar(props: Props) {
+ const { numSelected ,title } = props;
+
+ return (
+ 0 && {
+ bgcolor: (theme) =>
+ alpha(
+ theme.palette.primary.main,
+ theme.palette.action.activatedOpacity
+ ),
+ }),
+ }}
+ >
+ {numSelected > 0 ? (
+
+ {numSelected} selected
+
+ ) : (
+
+ {title}
+
+ )}
+ {numSelected > 0 ? (
+
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/app/database/databaseTable/index.tsx b/app/database/databaseTable/index.tsx
new file mode 100644
index 0000000..a27a221
--- /dev/null
+++ b/app/database/databaseTable/index.tsx
@@ -0,0 +1,204 @@
+"use client";
+
+import * as React from "react";
+import {
+ Box,
+ Paper,
+ TableContainer,
+ Table,
+ TableBody,
+ TableRow,
+ TableCell,
+ Checkbox,
+ TablePagination,
+} from "@mui/material";
+import { Order, Data, HeadCell } from "@/app/database/databaseTable/types";
+import DbTableHead from "./dbTableHead";
+import DbTableToolbar from "./dbTableToolbar";
+
+function descendingComparator(a: T, b: T, orderBy: keyof T) {
+ if (b[orderBy] < a[orderBy]) {
+ return -1;
+ }
+ if (b[orderBy] > a[orderBy]) {
+ return 1;
+ }
+ return 0;
+}
+
+function getComparator(
+ order: Order,
+ orderBy: keyof T
+): (a: T, b: T) => number {
+ return order === "desc"
+ ? (a, b) => descendingComparator(a, b, orderBy)
+ : (a, b) => -descendingComparator(a, b, orderBy);
+}
+
+interface Props {
+ rows: T[];
+ heads: HeadCell[];
+}
+
+export default function DatabaseTable(
+ props: Props
+) {
+ const { rows, heads } = props;
+
+ const [order, setOrder] = React.useState("asc");
+ const [orderBy, setOrderBy] = React.useState(
+ "name"
+ );
+ const [selected, setSelected] = React.useState([]);
+ const [page, setPage] = React.useState(0);
+ const [dense, setDense] = React.useState(true);
+ const [rowsPerPage, setRowsPerPage] = React.useState(25);
+
+ const handleRequestSort = (
+ event: React.MouseEvent,
+ property: keyof T[number] & string
+ ) => {
+ const isAsc = orderBy === property && order === "asc";
+ setOrder(isAsc ? "desc" : "asc");
+ setOrderBy(property);
+ };
+
+ const handleSelectAllClick = (event: React.ChangeEvent) => {
+ if (event.target.checked) {
+ const newSelected = rows.map((n) => n[0]);
+ setSelected(newSelected);
+ return;
+ }
+ setSelected([]);
+ };
+
+ const handleClick = (event: React.MouseEvent, id: number) => {
+ const selectedIndex = selected.indexOf(id);
+ let newSelected: readonly number[] = [];
+
+ if (selectedIndex === -1) {
+ newSelected = newSelected.concat(selected, id);
+ } else if (selectedIndex === 0) {
+ newSelected = newSelected.concat(selected.slice(1));
+ } else if (selectedIndex === selected.length - 1) {
+ newSelected = newSelected.concat(selected.slice(0, -1));
+ } else if (selectedIndex > 0) {
+ newSelected = newSelected.concat(
+ selected.slice(0, selectedIndex),
+ selected.slice(selectedIndex + 1)
+ );
+ }
+ setSelected(newSelected);
+ };
+
+ const handleChangePage = (event: unknown, newPage: number) => {
+ setPage(newPage);
+ };
+
+ const handleChangeRowsPerPage = (
+ event: React.ChangeEvent
+ ) => {
+ setRowsPerPage(parseInt(event.target.value, 10));
+ setPage(0);
+ };
+
+ const isSelected = (id: number) => selected.indexOf(id) !== -1;
+
+ // Avoid a layout jump when reaching the last page with empty rows.
+ const emptyRows =
+ page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
+
+ const visibleRows = React.useMemo(
+ () =>
+ rows
+ .slice()
+ .sort(getComparator(order, orderBy))
+ .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage),
+ [order, orderBy, page, rows, rowsPerPage]
+ );
+
+ return (
+
+
+
+
+
+
+
+ {visibleRows.map((row, index) => {
+ const isItemSelected = isSelected(row[0]);
+ const labelId = `enhanced-table-checkbox-${index}`;
+
+ return (
+ handleClick(event, row[0])}
+ role="checkbox"
+ aria-checked={isItemSelected}
+ tabIndex={-1}
+ key={row[0]}
+ selected={isItemSelected}
+ sx={{ cursor: "pointer" }}
+ >
+
+
+
+
+ {row[1]}
+
+ {row.slice(2).map((cell, index) => (
+
+ {cell}
+
+ ))}
+
+ );
+ })}
+ {emptyRows > 0 && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/app/database/databaseTable/types.ts b/app/database/databaseTable/types.ts
new file mode 100644
index 0000000..7a315bc
--- /dev/null
+++ b/app/database/databaseTable/types.ts
@@ -0,0 +1,40 @@
+export interface Data {
+ id: number;
+ calories: number;
+ carbs: number;
+ fat: number;
+ name: string;
+ protein: number;
+}
+
+export function createData(
+ id: number,
+ name: string,
+ calories: number,
+ fat: number,
+ carbs: number,
+ protein: number
+): Data {
+ return {
+ id,
+ name,
+ calories,
+ fat,
+ carbs,
+ protein,
+ };
+}
+
+export interface HeadCell {
+ id: number;
+ label: string;
+ align: Align;
+}
+
+export type Align = "right" | "left";
+
+export type Order = "asc" | "desc";
+
+export type TagRow = [number, string, string, string, string, string];
+export type ItemRow = [number, string, string, string, string, string];
+export type FolderRow = [number, string, string, string, string];
diff --git a/app/database/formDialog/folderFormDialog.tsx b/app/database/formDialog/folderFormDialog.tsx
new file mode 100644
index 0000000..37a99df
--- /dev/null
+++ b/app/database/formDialog/folderFormDialog.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Grid,
+ TextField,
+} from "@mui/material";
+import { Folder } from "@/app/lib/db/types";
+import { createFolder } from "@/app/lib/folders";
+
+interface Props {
+ data?: Folder;
+ open: boolean;
+ onClose: () => void;
+}
+
+export default function FolderFormDialog(props: Props) {
+ const { data, open, onClose } = props;
+
+ const handleClose = () => {
+ onClose();
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ const formJson = Object.fromEntries((formData as any).entries());
+ console.debug(formJson);
+
+ const name = formJson.name;
+ const path = formJson.path;
+ const response = await createFolder(name, path);
+
+ handleClose();
+ };
+
+ return (
+
+ );
+}
diff --git a/app/database/formDialog/itemFormDialog.tsx b/app/database/formDialog/itemFormDialog.tsx
new file mode 100644
index 0000000..b9ee01e
--- /dev/null
+++ b/app/database/formDialog/itemFormDialog.tsx
@@ -0,0 +1,176 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Chip,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControlLabel,
+ Grid,
+ MenuItem,
+ OutlinedInput,
+ Select,
+ SelectChangeEvent,
+ Switch,
+ TextField,
+} from "@mui/material";
+import { Item, ItemRelation, Folder, Tag } from "@/app/lib/db/types";
+import { createItem } from "@/app/lib/items";
+import { createFolder } from "@/app/lib/folders";
+import theme from "@/app/lib/theme";
+import React from "react";
+import { createItemRelation } from "@/app/lib/itemRelation";
+
+const SEP = "\t";
+
+const ITEM_HEIGHT = 48;
+const ITEM_PADDING_TOP = 8;
+const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+};
+
+interface Props {
+ data?: Item;
+ folders: Folder[];
+ tags: Tag[];
+ open: boolean;
+ onClose: () => void;
+}
+
+export default function ItemFormDialog(props: Props) {
+ const { data, folders, tags, open, onClose } = props;
+ const [selectedTags, setSelectedTags] = React.useState([]);
+
+ const handleClose = () => {
+ onClose();
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ const formJson = Object.fromEntries((formData as any).entries());
+ console.debug(formJson);
+
+ const folderId = Number(formJson.folder);
+ const path = formJson.path;
+ const name = formJson.name || undefined;
+ const starred = formJson.starred === "on";
+ const response = await createItem(path, folderId, name, starred);
+
+ if (response !== null && selectedTags.length > 0) {
+ for (const tag of selectedTags) {
+ const tagId = Number(tag.split(SEP)[0]);
+ await createItemRelation(tagId, response.id);
+ }
+ }
+
+ handleClose();
+ };
+
+ const handleChange = (event: SelectChangeEvent) => {
+ const {
+ target: { value },
+ } = event;
+ setSelectedTags(
+ // On autofill we get a stringified value.
+ typeof value === "string" ? value.split(",") : value
+ );
+ };
+
+ return (
+
+ );
+}
diff --git a/app/components/tag/FormDialog.tsx b/app/database/formDialog/tagFormDialog.tsx
similarity index 69%
rename from app/components/tag/FormDialog.tsx
rename to app/database/formDialog/tagFormDialog.tsx
index e3ba286..0502287 100644
--- a/app/components/tag/FormDialog.tsx
+++ b/app/database/formDialog/tagFormDialog.tsx
@@ -15,15 +15,17 @@ import {
TextField,
} from "@mui/material";
import { createTag } from "@/app/lib/tags";
+import { createTagRelation } from "@/app/lib/tagRelation";
interface Props {
- open: boolean;
+ existingTags: Tag[];
data?: Tag;
+ open: boolean;
onClose: () => void;
}
-export default function FormDialog(props: Props) {
- const { open, data, onClose } = props;
+export default function TagFormDialog(props: Props) {
+ const { existingTags, data, open, onClose } = props;
const handleClose = () => {
onClose();
@@ -40,7 +42,12 @@ export default function FormDialog(props: Props) {
const starred = formJson.starred === "on";
const textColor = formJson.textColor || undefined;
const backColor = formJson.backColor || undefined;
- await createTag(name, type, starred, textColor, backColor);
+ const response = await createTag(name, type, starred, textColor, backColor);
+
+ const parentId = formJson.parent;
+ if (parentId && response) {
+ await createTagRelation(Number(parentId), response.id);
+ }
handleClose();
};
@@ -58,7 +65,10 @@ export default function FormDialog(props: Props) {
- } label="Star" />
+ }
+ label="Star"
+ />
@@ -75,6 +86,7 @@ export default function FormDialog(props: Props) {
labelId="type-label"
label="Type"
name="type"
+ defaultValue={TagType.Normal}
required
fullWidth
>
@@ -82,6 +94,21 @@ export default function FormDialog(props: Props) {
+
+
+
+ {value === index && {children}}
+
+ );
+}
+
+const tagHeader: HeadCell[] = [
+ {
+ id: 0,
+ label: "Name",
+ align: "right",
+ },
+ {
+ id: 1,
+ label: "Type",
+ align: "right",
+ },
+ {
+ id: 2,
+ label: "Parent",
+ align: "right",
+ },
+ {
+ id: 3,
+ label: "Updated",
+ align: "right",
+ },
+ {
+ id: 4,
+ label: "Created",
+ align: "right",
+ },
+];
+
+const itemHeader: HeadCell[] = [
+ {
+ id: 0,
+ label: "Name",
+ align: "right",
+ },
+ {
+ id: 1,
+ label: "Path",
+ align: "right",
+ },
+ {
+ id: 2,
+ label: "Starred",
+ align: "right",
+ },
+ {
+ id: 3,
+ label: "Updated",
+ align: "right",
+ },
+ {
+ id: 4,
+ label: "Created",
+ align: "right",
+ },
+];
+
+const folderHeader: HeadCell[] = [
+ {
+ id: 0,
+ label: "Name",
+ align: "right",
+ },
+ {
+ id: 1,
+ label: "Path",
+ align: "right",
+ },
+ {
+ id: 2,
+ label: "Updated",
+ align: "right",
+ },
+ {
+ id: 3,
+ label: "Created",
+ align: "right",
+ },
+];
+
+export default function Page() {
+ const [tabIndex, setTabIndex] = React.useState(0);
+
+ const [tagRows, setTagRows] = React.useState([]);
+ const [itemRows, setItemRows] = React.useState([]);
+ const [folderRows, setFolderRows] = React.useState([]);
+
+ const [tags, setTags] = React.useState([]);
+ const [items, setItems] = React.useState- ([]);
+ const [folders, setFolders] = React.useState([]);
+
+ const [tagFormOpen, setTagFormOpen] = React.useState(false);
+ const [itemFormOpen, setItemFormOpen] = React.useState(false);
+ const [folderFormOpen, setFolderFormOpen] = React.useState(false);
+
+ const handleTagFormOpen = () => {
+ setTagFormOpen(true);
+ };
+
+ const handleTagFormClose = () => {
+ setTagFormOpen(false);
+ };
+
+ const handleItemFormOpen = () => {
+ setItemFormOpen(true);
+ };
+
+ const handleItemFormClose = () => {
+ setItemFormOpen(false);
+ };
+
+ const handleFolderFormOpen = () => {
+ setFolderFormOpen(true);
+ };
+
+ const handleFolderFormClose = () => {
+ setFolderFormOpen(false);
+ };
+
+ const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+ setTabIndex(newValue);
+ };
+
+ React.useEffect(() => {
+ listTag(true, true, false).then((data) => {
+ setTags(data);
+
+ const list: TagRow[] = data.map((item) => {
+ const updateDate = item.updatedAt
+ ? new Date(item.updatedAt).toISOString()
+ : "N/A";
+
+ const createDate = item.createdAt
+ ? new Date(item.createdAt).toISOString()
+ : "N/A";
+
+ return [
+ item.id,
+ item.name,
+ item.type,
+ item.parent ? "Has parent" : "No parent",
+ updateDate,
+ createDate,
+ ];
+ });
+ setTagRows(list);
+ });
+
+ listItem().then((data) => {
+ setItems(data);
+
+ const list: ItemRow[] = data.map((item) => {
+ const updateDate = item.updatedAt
+ ? new Date(item.updatedAt).toISOString()
+ : "N/A";
+
+ const createDate = item.createdAt
+ ? new Date(item.createdAt).toISOString()
+ : "N/A";
+
+ return [
+ item.id,
+ item.name || "N/A",
+ item.path,
+ item.starred ? "Starred" : "",
+ updateDate,
+ createDate,
+ ];
+ });
+ setItemRows(list);
+ });
+
+ listFolder(false).then((data) => {
+ setFolders(data);
+
+ const list: FolderRow[] = data.map((item) => {
+ const updateDate = item.updatedAt
+ ? new Date(item.updatedAt).toISOString()
+ : "N/A";
+
+ const createDate = item.createdAt
+ ? new Date(item.createdAt).toISOString()
+ : "N/A";
+
+ return [item.id, item.name, item.path, updateDate, createDate];
+ });
+ setFolderRows(list);
+ });
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/explorer/page.tsx b/app/explorer/page.tsx
new file mode 100644
index 0000000..91a31c9
--- /dev/null
+++ b/app/explorer/page.tsx
@@ -0,0 +1,5 @@
+import Explorer from "@/app/components/explorer";
+
+export default async function Page() {
+ return ;
+}
diff --git a/app/icon.svg b/app/icon.svg
new file mode 100644
index 0000000..595817d
--- /dev/null
+++ b/app/icon.svg
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/app/layout.tsx b/app/layout.tsx
index 93a6f62..a09f8f3 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,15 +2,18 @@ import type { Metadata } from "next";
import { Inter } from "next/font/google";
import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter";
import { ThemeProvider } from "@mui/material/styles";
+import { Box, Toolbar } from "@mui/material";
import CssBaseline from "@mui/material/CssBaseline";
+import Sidebar from "@/app/components/sidebar";
+import Topbar from "@/app/components/topbar";
import theme from "@/app/lib/theme";
// import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Hie",
+ description: "Hierarchical tag-based image explorer",
};
export default function RootLayout({
@@ -24,7 +27,16 @@ export default function RootLayout({
{/* https://mui.com/material-ui/integrations/nextjs/ */}
- {children}
+
+
+
+
+
+
+
+ {children}
+
+