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