diff --git a/apps/web/app/[lng]/(dashboard)/projects/page.tsx b/apps/web/app/[lng]/(dashboard)/projects/page.tsx
index bf89fdd2..6df5bb0a 100644
--- a/apps/web/app/[lng]/(dashboard)/projects/page.tsx
+++ b/apps/web/app/[lng]/(dashboard)/projects/page.tsx
@@ -4,10 +4,13 @@ import {
Box,
Button,
Container,
+ Divider,
+ Grid,
IconButton,
InputBase,
Paper,
Typography,
+ useTheme,
} from "@mui/material";
import { ICON_NAME, Icon } from "@p4b/ui/components/Icon";
import GridViewIcon from "@mui/icons-material/GridView";
@@ -15,6 +18,7 @@ import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted";
import { useState } from "react";
import { useProjects } from "@/lib/api/projects";
import TileGrid from "@/components/dashboard/common/TileGrid";
+import FoldersTreeView from "@/components/dashboard/common/FoldersTreeView";
const Projects = () => {
const {
@@ -28,10 +32,12 @@ const Projects = () => {
const newView = view === "list" ? "grid" : "list";
setView(newView);
};
+ const theme = useTheme();
return (
{/* {Header} */}
+
{
New project
- {/* Search bar */}
-
-
-
-
-
-
-
-
-
- {view === "list" ? : }
-
-
+
+
+
+
+ {/* Search bar */}
+
+
+
+
+
+
+
-
+
+ {view === "list" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/apps/web/components/@mui/ThemeRegistry.tsx b/apps/web/components/@mui/ThemeRegistry.tsx
index 3ec7b9bb..4902b2b3 100644
--- a/apps/web/components/@mui/ThemeRegistry.tsx
+++ b/apps/web/components/@mui/ThemeRegistry.tsx
@@ -11,9 +11,9 @@ export default function ThemeRegistry({
}: {
children: React.ReactNode;
}) {
- let theme = "dark";
+ let theme = "light";
if (typeof window !== "undefined") {
- theme = localStorage.getItem("theme") || "dark";
+ theme = localStorage.getItem("theme") || "light";
}
const [mode, setMode] = React.useState<"light" | "dark">(
diff --git a/apps/web/components/common/TreeViewItem.tsx b/apps/web/components/common/TreeViewItem.tsx
new file mode 100644
index 00000000..0b35f2d0
--- /dev/null
+++ b/apps/web/components/common/TreeViewItem.tsx
@@ -0,0 +1,156 @@
+import * as React from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import clsx from "clsx";
+
+import type {
+ TreeItemContentProps,
+ TreeItemProps,
+} from "@mui/x-tree-view/TreeItem";
+import {
+ TreeItem,
+ treeItemClasses,
+ useTreeItem,
+} from "@mui/x-tree-view/TreeItem";
+import { useTheme } from "@mui/material";
+
+type StyledTreeItemProps = TreeItemProps & {
+ labelIcon: JSX.Element;
+ labelInfo?: string;
+ labelText: string;
+ actionElement?: JSX.Element;
+};
+
+const CustomContent = React.forwardRef(function CustomContent(
+ props: TreeItemContentProps,
+ ref,
+) {
+ const {
+ classes,
+ className,
+ label,
+ nodeId,
+ icon: iconProp,
+ expansionIcon,
+ displayIcon,
+ } = props;
+
+ const {
+ disabled,
+ expanded,
+ selected,
+ focused,
+ handleExpansion,
+ handleSelection,
+ preventSelection,
+ } = useTreeItem(nodeId);
+
+ const icon = iconProp || expansionIcon || displayIcon;
+
+ const handleMouseDown = (
+ event: React.MouseEvent,
+ ) => {
+ preventSelection(event);
+ };
+
+ const handleExpansionClick = (
+ event: React.MouseEvent,
+ ) => {
+ handleExpansion(event);
+ };
+
+ const handleSelectionClick = (
+ event: React.MouseEvent,
+ ) => {
+ handleSelection(event);
+ };
+
+ return (
+ }
+ >
+
+ {icon}
+
+
+ {label}
+
+
+ );
+});
+
+const StyledTreeItem = React.forwardRef(function StyledTreeItem(
+ props: StyledTreeItemProps,
+ ref: React.Ref,
+) {
+ const { labelIcon, labelInfo, labelText, actionElement, ...other } = props;
+ const theme = useTheme();
+ return (
+
+
+ {labelIcon}
+
+
+ {labelText}
+
+
+ {labelInfo}
+
+ {actionElement}
+ >
+ }
+ {...other}
+ ref={ref}
+ />
+ );
+});
+
+export default StyledTreeItem;
diff --git a/apps/web/components/dashboard/common/FoldersTreeView.tsx b/apps/web/components/dashboard/common/FoldersTreeView.tsx
new file mode 100644
index 00000000..0c467b85
--- /dev/null
+++ b/apps/web/components/dashboard/common/FoldersTreeView.tsx
@@ -0,0 +1,98 @@
+import { TreeView } from "@mui/x-tree-view/TreeView";
+import TreeViewItem from "@/components/common/TreeViewItem";
+import { ICON_NAME, Icon } from "@p4b/ui/components/Icon";
+import { IconButton, Tooltip } from "@mui/material";
+import GroupIcon from "@mui/icons-material/Group";
+import { useFolders } from "@/lib/api/folders";
+import { useState } from "react";
+
+export default function FoldersTreeView() {
+ const [selected, setSelected] = useState(["0"]);
+ const { folders } = useFolders({});
+
+ const handleNodeSelect = (_event, nodeIds: string[]) => {
+ setSelected(nodeIds);
+ };
+
+ return (
+
+ }
+ defaultExpandIcon={
+
+ }
+ defaultEndIcon={}
+ sx={{ height: "100%", flexGrow: 1, overflowY: "auto" }}
+ selected={selected}
+ onNodeSelect={handleNodeSelect}
+ >
+
+ }
+ actionElement={
+
+ {
+ event.stopPropagation();
+ }}
+ >
+
+
+
+ }
+ >
+ {folders?.items?.map((folder) => (
+
+ }
+ actionElement={
+ {
+ event.stopPropagation();
+ }}
+ >
+
+
+ }
+ />
+ ))}
+
+ {/* TEAMS SECTION */}
+ } />
+ {/* ORGANIZATION SECTION */}
+ }
+ />
+
+ );
+}
diff --git a/apps/web/components/dashboard/common/TileCard.tsx b/apps/web/components/dashboard/common/TileCard.tsx
index c5651627..7e2f3edf 100644
--- a/apps/web/components/dashboard/common/TileCard.tsx
+++ b/apps/web/components/dashboard/common/TileCard.tsx
@@ -217,12 +217,7 @@ const TileCard = (props: TileCard) => {
const updatedAtText = (
<>
{updatedAt && (
-
+
Last updated:{" "}
{formatDistance(new Date(updatedAt), new Date(), {
@@ -237,12 +232,7 @@ const TileCard = (props: TileCard) => {
const createdAtText = (
<>
{createdAt && (
-
+
Created:{" "}
{formatDistance(new Date(createdAt), new Date(), {
@@ -301,11 +291,11 @@ const TileCard = (props: TileCard) => {
display: "flex",
flexDirection: cardType === "grid" ? "column" : "row",
...(cardType === "list" && {
- boxShadow: 0,
p: 2,
borderTop: `1px solid ${theme.palette.divider}`,
alignItems: "center",
borderRadius: 0,
+ boxShadow: 0,
}),
"&:hover": {
cursor: "pointer",
diff --git a/apps/web/components/dashboard/common/TileGrid.tsx b/apps/web/components/dashboard/common/TileGrid.tsx
index 0c7b85df..01220433 100644
--- a/apps/web/components/dashboard/common/TileGrid.tsx
+++ b/apps/web/components/dashboard/common/TileGrid.tsx
@@ -24,7 +24,13 @@ const TileGrid = (props: TileGridProps) => {
};
return (
-
+
{(isLoading ? Array.from(new Array(4)) : items ?? []).map(
(item: Project, index: number) => (
diff --git a/apps/web/components/header/Toolbar.tsx b/apps/web/components/header/Toolbar.tsx
index 3a009038..2e9deef0 100644
--- a/apps/web/components/header/Toolbar.tsx
+++ b/apps/web/components/header/Toolbar.tsx
@@ -10,6 +10,7 @@ import {
useTheme,
IconButton,
Divider,
+ Link,
} from "@mui/material";
import { Icon, ICON_NAME } from "@p4b/ui/components/Icon";
@@ -24,14 +25,28 @@ export type MapToolbarProps = {
};
export function Toolbar(props: MapToolbarProps) {
- const { LeftToolbarChild, RightToolbarChild, height, showHambugerMenu, onMenuIconClick } = props;
+ const {
+ LeftToolbarChild,
+ RightToolbarChild,
+ height,
+ showHambugerMenu,
+ onMenuIconClick,
+ } = props;
const theme = useTheme();
return (
- theme.zIndex.drawer + 2 }}>
+ theme.zIndex.drawer + 2,
+ borderBottom: "1px solid rgba(58, 53, 65, 0.12)",
+ }}
+ >
- {showHambugerMenu && (
+ {showHambugerMenu && (
<>
@@ -40,10 +55,29 @@ export function Toolbar(props: MapToolbarProps) {
>
)}
-
-
+
+
+
+
+
+
+
{
+ const { data, isLoading, error, mutate, isValidating } =
+ useSWR([`${FOLDERS_API_BASE_URL}`, queryParams], fetcher);
+ return {
+ folders: data,
+ isLoading: isLoading,
+ isError: error,
+ mutate,
+ isValidating,
+ };
+};
+
+export const deleteFolders = async (id: string) => {
+ try {
+ await fetch(`${FOLDERS_API_BASE_URL}/${id}`, {
+ method: "DELETE",
+ });
+ } catch (error) {
+ console.error(error);
+ throw Error(`deleteFolder: unable to delete folder with id ${id}`);
+ }
+};
diff --git a/apps/web/lib/validations/folder.ts b/apps/web/lib/validations/folder.ts
new file mode 100644
index 00000000..e869f286
--- /dev/null
+++ b/apps/web/lib/validations/folder.ts
@@ -0,0 +1,15 @@
+import { responseSchema } from "@/lib/validations/response";
+import * as z from "zod";
+
+export const folderSchema = z.object({
+ name: z.string(),
+ id: z.string().uuid(),
+ user_id: z.string().uuid()
+});
+
+
+
+export const folderResponseSchema = responseSchema(folderSchema);
+
+export type Folder = z.infer;
+export type FolderPaginated = z.infer;
diff --git a/apps/web/lib/validations/layer.ts b/apps/web/lib/validations/layer.ts
index 48fb5f88..add39174 100644
--- a/apps/web/lib/validations/layer.ts
+++ b/apps/web/lib/validations/layer.ts
@@ -19,8 +19,8 @@ const layerSchema = z.object({
thumbnail_url: z.string(),
data_source: z.string(),
data_reference_year: z.number(),
- id: z.string(),
- user_id: z.string(),
+ id: z.string().uuid(),
+ user_id: z.string().uuid(),
type: layerType,
size: z.number().optional(),
style: z.object({}).optional(),
diff --git a/apps/web/package.json b/apps/web/package.json
index c22ba942..60dbed95 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -26,12 +26,14 @@
"@mui/material": "latest",
"@mui/private-theming": "^5.14.5",
"@mui/utils": "^5.13.7",
+ "@mui/x-tree-view": "6.0.0-alpha.3",
"@p4b/types": "workspace:*",
"@p4b/ui": "workspace:*",
"@reduxjs/toolkit": "^1.9.5",
"@types/mapbox-gl": "^2.7.11",
"accept-language": "^3.0.18",
"axios": "^1.4.0",
+ "clsx": "^2.0.0",
"color": "^4.2.3",
"date-fns": "^2.30.0",
"dayjs": "^1.11.9",
diff --git a/packages/ui/components/Icon.tsx b/packages/ui/components/Icon.tsx
index e1f37be3..aed47178 100644
--- a/packages/ui/components/Icon.tsx
+++ b/packages/ui/components/Icon.tsx
@@ -55,6 +55,7 @@ import {
faPen,
faFloppyDisk,
faDatabase,
+ faFolderPlus,
} from "@fortawesome/free-solid-svg-icons";
import {
faGoogle,
@@ -91,6 +92,7 @@ export enum ICON_NAME {
CLOSE = "close",
HOUSE = "house",
FOLDER = "folder",
+ FOLDER_NEW = "folder-new",
SETTINGS = "settings",
CIRCLECHECK = "circleCheck",
CIRCLEINFO = "circleInfo",
@@ -130,7 +132,6 @@ export enum ICON_NAME {
XCLOSE = "xclose",
EDITPEN = "editpen",
SAVE = "save",
-=======
DATABASE = "database",
// Brand icons
GOOGLE = "google",
@@ -163,6 +164,7 @@ const nameToIcon: { [k in ICON_NAME]: IconDefinition } = {
[ICON_NAME.CLOSE]: faClose,
[ICON_NAME.HOUSE]: faHouse,
[ICON_NAME.FOLDER]: faFolder,
+ [ICON_NAME.FOLDER_NEW]: faFolderPlus,
[ICON_NAME.SETTINGS]: faGears,
[ICON_NAME.CIRCLECHECK]: faCircleCheck,
[ICON_NAME.CIRCLEINFO]: faCircleExclamation,
@@ -239,7 +241,7 @@ library.add(...Object.values(nameToIcon));
export function Icon({
iconName,
...rest
-}: SvgIconProps & { iconName: ICON_NAME }) {
+}: SvgIconProps & { iconName: ICON_NAME }): JSX.Element {
if (!(iconName in nameToIcon)) {
throw new Error(`Invalid icon name: ${iconName}`);
}
diff --git a/packages/ui/theme/overrides/paper.ts b/packages/ui/theme/overrides/paper.ts
index 3bf8ffb8..562123b0 100644
--- a/packages/ui/theme/overrides/paper.ts
+++ b/packages/ui/theme/overrides/paper.ts
@@ -1,10 +1,12 @@
const MuiPaper = () => {
return {
- styleOverrides: {
- root: {
- backgroundImage: 'none'
- }
- }
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ backgroundImage: "unset",
+ },
+ },
+ },
};
};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f8695d38..74fc2fc9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -167,7 +167,7 @@ importers:
version: 2.2.0(react@18.2.0)
tss-react:
specifier: latest
- version: 4.9.1(@emotion/react@11.11.1)(react@18.2.0)
+ version: 4.9.2(@emotion/react@11.11.1)(@mui/material@5.14.10)(react@18.2.0)
uuid:
specifier: ^9.0.0
version: 9.0.0
@@ -332,6 +332,9 @@ importers:
'@mui/utils':
specifier: ^5.13.7
version: 5.13.7(react@18.2.0)
+ '@mui/x-tree-view':
+ specifier: 6.0.0-alpha.3
+ version: 6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.10)(@mui/system@5.14.10)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
'@p4b/types':
specifier: workspace:*
version: link:../../packages/types
@@ -350,6 +353,9 @@ importers:
axios:
specifier: ^1.4.0
version: 1.4.0
+ clsx:
+ specifier: ^2.0.0
+ version: 2.0.0
color:
specifier: ^4.2.3
version: 4.2.3
@@ -427,7 +433,7 @@ importers:
version: 1.6.4
tss-react:
specifier: latest
- version: 4.9.1(@emotion/react@11.11.1)(react@18.2.0)
+ version: 4.9.2(@emotion/react@11.11.1)(@mui/material@5.14.10)(react@18.2.0)
uuid:
specifier: ^9.0.0
version: 9.0.0
@@ -4352,6 +4358,34 @@ packages:
- '@types/react'
dev: false
+ /@mui/x-tree-view@6.0.0-alpha.3(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@mui/material@5.14.10)(@mui/system@5.14.10)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-j6vPD3e4Y4dd8v19bnDkPBXdAD8Qo5pbSEAwB3XYh60tprphBU+YVjCfUo4ZZkYEqhGBen6gKsJhFz98H2v2kg==}
+ engines: {node: '>=14.0.0'}
+ peerDependencies:
+ '@emotion/react': ^11.9.0
+ '@emotion/styled': ^11.8.1
+ '@mui/material': ^5.8.6
+ '@mui/system': ^5.8.0
+ react: ^17.0.0 || ^18.0.0
+ react-dom: ^17.0.0 || ^18.0.0
+ dependencies:
+ '@babel/runtime': 7.22.15
+ '@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0)
+ '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.18)(react@18.2.0)
+ '@mui/base': 5.0.0-beta.16(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@mui/system': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react@18.2.0)
+ '@mui/utils': 5.14.10(@types/react@18.2.18)(react@18.2.0)
+ '@types/react-transition-group': 4.4.6
+ clsx: 2.0.0
+ prop-types: 15.8.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ dev: false
+
/@ndelangen/get-tarball@3.0.9:
resolution: {integrity: sha512-9JKTEik4vq+yGosHYhZ1tiH/3WpUS0Nh0kej4Agndhox8pAdWhEx5knFVRcb/ya9knCRCs1rPxNrSXTDdfVqpA==}
dependencies:
@@ -19064,20 +19098,24 @@ packages:
react: 18.2.0
dev: false
- /tss-react@4.9.1(@emotion/react@11.11.1)(react@18.2.0):
- resolution: {integrity: sha512-pSvjM9FJpPUTqEPdiPPMnkLBdZLBTZ3pzxfOut5NjY1/wWHlTKl+oK2yvKJ6jATMcJZcw55vHuPgynWLFZKSOQ==}
+ /tss-react@4.9.2(@emotion/react@11.11.1)(@mui/material@5.14.10)(react@18.2.0):
+ resolution: {integrity: sha512-0qOuDpar3q3N59Jsl50oDd+Zu3wfXv2rdf4VlPzvuekH6mkAgUVobZV3j69NPH0nm3Vv5xDRACjVUqVWmaNW0g==}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/server': ^11.4.0
+ '@mui/material': ^5.0.0
react: ^16.8.0 || ^17.0.2 || ^18.0.0
peerDependenciesMeta:
'@emotion/server':
optional: true
+ '@mui/material':
+ optional: true
dependencies:
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1(@types/react@18.2.18)(react@18.2.0)
'@emotion/serialize': 1.1.2
'@emotion/utils': 1.2.1
+ '@mui/material': 5.14.10(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false