From b8a4912e1b8f9cd1845e2ee52c409bfd033dcd0c Mon Sep 17 00:00:00 2001 From: Beatrice Mkumbo Date: Tue, 2 Jul 2024 06:17:17 +0300 Subject: [PATCH] chore(web): visualizer dashbord (#1023) Co-authored-by: airslice --- web/src/beta/features/Assets/hooks.ts | 14 +- .../ContentsContainer/Assets/AssetCard.tsx | 81 ++++++++ .../ContentsContainer/Assets/index.tsx | 126 ++++++++++++ .../ContentsContainer/CommonHeader.tsx | 86 ++++++++ .../ContentsContainer/Members/ListItem.tsx | 54 +++++ .../ContentsContainer/Members/index.tsx | 103 ++++++++++ .../Projects/Project/ProjectGridViewItem.tsx | 142 ++++++++++++++ .../Projects/Project/ProjectListViewItem.tsx | 167 ++++++++++++++++ .../Projects/Project/hooks.ts | 84 ++++++++ .../Projects/Project/types.ts | 13 ++ .../Projects/ProjectCreatorModal.tsx | 129 ++++++++++++ .../ContentsContainer/Projects/hooks.ts | 167 ++++++++++++++++ .../ContentsContainer/Projects/index.tsx | 184 ++++++++++++++++++ .../Dashboard/ContentsContainer/index.tsx | 32 +++ .../Dashboard/LeftSidePanel/index.tsx | 98 ++++++++++ .../Dashboard/LeftSidePanel/menuItem.tsx | 47 +++++ .../Dashboard/LeftSidePanel/profile.tsx | 99 ++++++++++ web/src/beta/features/Dashboard/hooks.ts | 75 +++++++ web/src/beta/features/Dashboard/index.tsx | 114 +++++++++++ web/src/beta/features/Dashboard/type.ts | 43 ++++ .../beta/features/Modals/AssetModal/index.tsx | 2 +- .../reearth-ui/components/Button/index.tsx | 12 +- .../components/Icon/Icons/ArrowLeftRight.svg | 6 + .../components/Icon/Icons/Crosshair.svg | 12 +- .../components/Icon/Icons/FolderFilled.svg | 3 + .../reearth-ui/components/Icon/Icons/List.svg | 5 + .../components/Icon/Icons/Question.svg | 5 + .../reearth-ui/components/Icon/Icons/Star.svg | 6 +- .../components/Icon/Icons/StarFilled.svg | 3 + .../reearth-ui/components/Icon/Icons/User.svg | 4 + .../components/Icon/Icons/Users.svg | 6 + .../components/Icon/Icons/UsersFour.svg | 10 + .../lib/reearth-ui/components/Icon/icons.ts | 16 ++ .../components/Loading/index.stories.tsx | 12 ++ .../reearth-ui/components/Loading/index.tsx | 45 +++++ .../components/Modal/index.stories.tsx | 2 +- .../components/ModalPanel/index.stories.tsx | 16 +- .../components/ModalPanel/index.tsx | 8 +- .../components/PopupMenu/index.stories.tsx | 2 +- .../reearth-ui/components/PopupMenu/index.tsx | 80 +++++--- .../reearth-ui/components/Selector/index.tsx | 4 +- .../reearth-ui/components/TextInput/index.tsx | 9 +- .../components/Typography/index.tsx | 5 +- .../beta/lib/reearth-ui/components/index.ts | 1 + web/src/beta/pages/Dashboard/index.tsx | 15 ++ web/src/beta/utils/time.ts | 24 +++ web/src/services/api/projectApi.ts | 65 ++++--- web/src/services/gql/__gen__/gql.ts | 8 +- web/src/services/gql/__gen__/graphql.ts | 20 +- web/src/services/gql/fragments/project.ts | 1 + web/src/services/gql/queries/scene.ts | 3 - web/src/services/i18n/translations/en.yml | 25 ++- web/src/services/i18n/translations/ja.yml | 25 ++- web/src/services/routing/index.tsx | 19 +- .../theme/reearthTheme/darkTheme/index.ts | 3 +- .../theme/reearthTheme/lightTheme/index.ts | 3 +- web/src/services/theme/reearthTheme/types.ts | 1 + 57 files changed, 2221 insertions(+), 123 deletions(-) create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Assets/AssetCard.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Assets/index.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/CommonHeader.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Members/index.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectGridViewItem.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/ProjectListViewItem.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/hooks.ts create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/Project/types.ts create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/ProjectCreatorModal.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/hooks.ts create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/Projects/index.tsx create mode 100644 web/src/beta/features/Dashboard/ContentsContainer/index.tsx create mode 100644 web/src/beta/features/Dashboard/LeftSidePanel/index.tsx create mode 100644 web/src/beta/features/Dashboard/LeftSidePanel/menuItem.tsx create mode 100644 web/src/beta/features/Dashboard/LeftSidePanel/profile.tsx create mode 100644 web/src/beta/features/Dashboard/hooks.ts create mode 100644 web/src/beta/features/Dashboard/index.tsx create mode 100644 web/src/beta/features/Dashboard/type.ts create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/ArrowLeftRight.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/FolderFilled.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/List.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/Question.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/StarFilled.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/User.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/Users.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Icon/Icons/UsersFour.svg create mode 100644 web/src/beta/lib/reearth-ui/components/Loading/index.stories.tsx create mode 100644 web/src/beta/lib/reearth-ui/components/Loading/index.tsx create mode 100644 web/src/beta/pages/Dashboard/index.tsx diff --git a/web/src/beta/features/Assets/hooks.ts b/web/src/beta/features/Assets/hooks.ts index 0876352ccf..df51e0c5aa 100644 --- a/web/src/beta/features/Assets/hooks.ts +++ b/web/src/beta/features/Assets/hooks.ts @@ -70,14 +70,14 @@ export default ({ const openDeleteModal = useCallback(() => setDeleteModalVisible(true), []); const closeDeleteModal = useCallback(() => setDeleteModalVisible(false), []); const assetsWrapperRef = useRef(null); - const sortOptions: { key: string; label: string }[] = useMemo( + const sortOptions: { value: string; label: string }[] = useMemo( () => [ - { key: "date", label: t("Last Uploaded") }, - { key: "date-reverse", label: t("First Uploaded") }, - { key: "name", label: t("A To Z") }, - { key: "name-reverse", label: t("Z To A") }, - { key: "size", label: t("Size Small to Large") }, - { key: "size-reverse", label: t("Size Large to Small") }, + { value: "date", label: t("Last Uploaded") }, + { value: "date-reverse", label: t("First Uploaded") }, + { value: "name", label: t("A To Z") }, + { value: "name-reverse", label: t("Z To A") }, + { value: "size", label: t("Size Small to Large") }, + { value: "size-reverse", label: t("Size Large to Small") }, ], [t], ); diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Assets/AssetCard.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Assets/AssetCard.tsx new file mode 100644 index 0000000000..aea067c6c3 --- /dev/null +++ b/web/src/beta/features/Dashboard/ContentsContainer/Assets/AssetCard.tsx @@ -0,0 +1,81 @@ +import { FC } from "react"; + +import { Asset } from "@reearth/beta/features/Assets/types"; +import { Icon, Typography } from "@reearth/beta/lib/reearth-ui"; +import { styled, useTheme } from "@reearth/services/theme"; + +type AssetCardProps = { + asset: Asset; + icon?: "image" | "file" | "assetNoSupport"; + isSelected?: boolean; + onAssetSelect?: (assets: Asset[]) => void; +}; + +const AssetCard: FC = ({ asset, icon, isSelected, onAssetSelect }) => { + const theme = useTheme(); + + const renderContent = () => { + switch (icon) { + case "image": + return ; + case "file": + case "assetNoSupport": + return ( + + + + ); + default: + return null; + } + }; + + return ( + onAssetSelect?.([asset])} isSelected={isSelected}> + {renderContent()} + + {asset.name} + + + ); +}; + +export default AssetCard; + +const CardWrapper = styled("div")<{ isSelected?: boolean }>(({ theme, isSelected }) => ({ + display: "flex", + flexDirection: "column", + border: `1px solid ${isSelected ? theme.select.main : "transparent"}`, + boxSizing: "border-box", + width: "100%", + height: "160px", + padding: theme.spacing.smallest, + borderRadius: theme.radius.small, +})); + +const AssetImage = styled("div")<{ url?: string }>(({ theme, url }) => ({ + background: url ? `url(${url}) center/cover` : theme.bg[1], + borderRadius: theme.radius.small, + flex: "3", +})); + +const IconWrapper = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + justifyContent: "center", + background: theme.bg[1], + borderRadius: theme.radius.small, + flex: "3", +})); + +const AssetName = styled("div")(({ theme }) => ({ + paddingTop: theme.spacing.small, + textAlign: "center", + wordBreak: "break-word", + flex: "1", + display: "-webkit-box", + WebkitBoxOrient: "vertical", + WebkitLineClamp: 2, + overflow: "hidden", + textOverflow: "ellipsis", +})); diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Assets/index.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Assets/index.tsx new file mode 100644 index 0000000000..05d54ab541 --- /dev/null +++ b/web/src/beta/features/Dashboard/ContentsContainer/Assets/index.tsx @@ -0,0 +1,126 @@ +import { FC, useCallback, useMemo, useState } from "react"; + +import { FILE_FORMATS, IMAGE_FORMATS } from "@reearth/beta/features/Assets/constants"; +import useAssets from "@reearth/beta/features/Assets/hooks"; +import useFileUploaderHook from "@reearth/beta/hooks/useAssetUploader/hooks"; +import { Loading } from "@reearth/beta/lib/reearth-ui"; +import { checkIfFileType } from "@reearth/beta/utils/util"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import CommonHeader from "../CommonHeader"; + +import AssetCard from "./AssetCard"; + +const ASSETS_VIEW_STATE_STORAGE_KEY = `reearth-visualizer-dashboard-asset-view-state`; + +const Assets: FC<{ workspaceId?: string }> = ({ workspaceId }) => { + const t = useT(); + + const [viewState, setViewState] = useState( + localStorage.getItem(ASSETS_VIEW_STATE_STORAGE_KEY) + ? localStorage.getItem(ASSETS_VIEW_STATE_STORAGE_KEY) + : "grid", + ); + const handleViewStateChange = useCallback((newView?: string) => { + if (!newView) return; + localStorage.setItem(ASSETS_VIEW_STATE_STORAGE_KEY, newView); + setViewState(newView); + }, []); + + const { + assets, + assetsWrapperRef, + isAssetsLoading: isLoading, + hasMoreAssets, + sortOptions, + selectedAssets, + handleGetMoreAssets, + onScrollToBottom, + handleSortChange, + selectAsset, + } = useAssets({ workspaceId }); + + const selectedAssetsIds = useMemo(() => selectedAssets.map(a => a.id), [selectedAssets]); + + const { handleFileUpload } = useFileUploaderHook({ + workspaceId: workspaceId, + }); + + return ( + + + !isLoading && hasMoreAssets && onScrollToBottom?.(e, handleGetMoreAssets)}> + + + {assets?.map(asset => ( + + ))} + + {isLoading && } + + + + ); +}; + +export default Assets; + +const Wrapper = styled("div")(() => ({ + display: "flex", + flexDirection: "column", + height: "100%", + boxSizing: "border-box", +})); + +const AssetsWrapper = styled("div")(({ theme }) => ({ + display: "flex", + maxHeight: "calc(100vh - 76px)", + flexDirection: "column", + overflowY: "auto", + padding: `0 ${theme.spacing.largest}px ${theme.spacing.largest}px ${theme.spacing.largest}px`, +})); + +const AssetGridWrapper = styled("div")(({ theme }) => ({ + display: "flex", + flexDirection: "column", + gap: theme.spacing.normal, +})); + +const AssetsRow = styled("div")(({ theme }) => ({ + display: "grid", + gap: theme.spacing.normal, + gridTemplateColumns: "repeat(7, 1fr)", + + "@media (max-width: 1200px)": { + gridTemplateColumns: "repeat(5, 1fr)", + }, + "@media (max-width: 900px)": { + gridTemplateColumns: "repeat(3, 1fr)", + }, + "@media (max-width: 600px)": { + gridTemplateColumns: "repeat(2, 1fr)", + }, +})); diff --git a/web/src/beta/features/Dashboard/ContentsContainer/CommonHeader.tsx b/web/src/beta/features/Dashboard/ContentsContainer/CommonHeader.tsx new file mode 100644 index 0000000000..89ce3a6497 --- /dev/null +++ b/web/src/beta/features/Dashboard/ContentsContainer/CommonHeader.tsx @@ -0,0 +1,86 @@ +import { FC, useCallback } from "react"; + +import { Button, IconName, Selector, Typography } from "@reearth/beta/lib/reearth-ui"; +import { useT } from "@reearth/services/i18n"; +import { styled, useTheme } from "@reearth/services/theme"; + +type HeaderProps = { + viewState: string; + title: string; + icon?: IconName; + options?: { value: string; label?: string }[]; + appearance?: "primary" | "secondary" | "dangerous" | "simple"; + onClick: () => void; + onChangeView?: (v?: string) => void; + onSortChange?: (value?: string) => void; +}; + +const CommonHeader: FC = ({ + title, + icon, + viewState, + options, + appearance, + onClick, + onChangeView, + onSortChange, +}) => { + const theme = useTheme(); + const t = useT(); + + const onChange = useCallback( + (value: string | string[]) => { + onSortChange?.(value as string); + }, + [onSortChange], + ); + + return ( +
+
+ ); +}; + +export default CommonHeader; + +const Header = styled("div")(({ theme }) => ({ + display: "flex", + justifyContent: "space-between", + padding: theme.spacing.largest, +})); + +const Actions = styled("div")(({ theme }) => ({ + display: "flex", + alignItems: "center", + gap: theme.spacing.small, +})); + +const SelectorContainer = styled("div")(() => ({ minWidth: "130px" })); diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx new file mode 100644 index 0000000000..0f19400b88 --- /dev/null +++ b/web/src/beta/features/Dashboard/ContentsContainer/Members/ListItem.tsx @@ -0,0 +1,54 @@ +import { FC } from "react"; + +import { Typography } from "@reearth/beta/lib/reearth-ui"; +import { styled } from "@reearth/services/theme"; + +import { Member } from "../../type"; + +const ListItem: FC<{ member: Member }> = ({ member }) => { + return ( + + + {member.user?.name.charAt(0).toUpperCase()} + + + {member.user?.name} + + + {member.user?.email} + + + + {member.role.charAt(0).toUpperCase() + member.role.slice(1).toLowerCase()} + + + + ); +}; + +export default ListItem; + +const StyledListItem = styled("div")(({ theme }) => ({ + display: "grid", + gridTemplateColumns: "auto 1fr 1fr 1fr", + padding: `${theme.spacing.small}px ${theme.spacing.normal}px`, + alignItems: "center", + background: theme.bg[1], + borderRadius: theme.radius.normal, + gap: theme.spacing.small, +})); + +const Avatar = styled("div")(({ theme }) => ({ + width: "25px", + height: "25px", + borderRadius: "50%", + background: theme.bg[2], + display: "flex", + alignItems: "center", + justifyContent: "center", +})); + +const TypographyWrapper = styled("div")(() => ({ + overflow: "hidden", + textOverflow: "ellipsis", +})); diff --git a/web/src/beta/features/Dashboard/ContentsContainer/Members/index.tsx b/web/src/beta/features/Dashboard/ContentsContainer/Members/index.tsx new file mode 100644 index 0000000000..8342978916 --- /dev/null +++ b/web/src/beta/features/Dashboard/ContentsContainer/Members/index.tsx @@ -0,0 +1,103 @@ +import { FC, useEffect, useState } from "react"; + +import { Button, TextInput, Typography } from "@reearth/beta/lib/reearth-ui"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import { Workspace } from "../../type"; + +import ListItem from "./ListItem"; + +const Members: FC<{ currentWorkspace?: Workspace }> = ({ currentWorkspace }) => { + const t = useT(); + const [searchQuery, setSearchQuery] = useState(""); + const [filteredMembers, setFilteredMembers] = useState(null); + + const handleMemberSearch = () => { + if (!currentWorkspace?.members) { + return; + } + + const filtered = currentWorkspace.members.filter( + member => + member?.user?.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member?.user?.email?.toLowerCase().includes(searchQuery.toLowerCase()), + ); + setFilteredMembers(filtered); + }; + + const handleSearchInputChange = (value: string) => { + setSearchQuery(value); + if (value === "" && currentWorkspace?.members) setFilteredMembers(currentWorkspace.members); + }; + + useEffect(() => { + if (currentWorkspace?.members) { + setFilteredMembers(currentWorkspace.members); + } + }, [currentWorkspace]); + + return ( + + + +