From d422944d95f620cdb431252f2710cd9e875a3407 Mon Sep 17 00:00:00 2001 From: lby Date: Wed, 30 Aug 2023 14:21:55 +0800 Subject: [PATCH] chore(web): beta project settings pages (#645) --- .../components/Accordion/AccordionItem.tsx | 76 +++++ .../components/Accordion/index.stories.tsx | 36 +++ .../beta/components/Accordion/index.test.tsx | 42 +++ web/src/beta/components/Accordion/index.tsx | 47 +++ web/src/beta/components/Button/index.tsx | 10 +- .../components/Collapse/index.stories.tsx | 26 ++ web/src/beta/components/Collapse/index.tsx | 59 ++++ web/src/beta/components/Icon/Icons/bin.svg | 3 + .../components/Icon/Icons/checkCircle.svg | 3 + .../beta/components/Icon/Icons/install.svg | 3 + .../components/Icon/Icons/marketplace.svg | 11 + .../Icon/Icons/publicGitHubRepo.svg | 11 + web/src/beta/components/Icon/Icons/scene.svg | 4 + web/src/beta/components/Icon/Icons/search.svg | 4 + .../components/Icon/Icons/uploadSimple.svg | 5 + .../components/Icon/Icons/uploadZipPlugin.svg | 9 + web/src/beta/components/Icon/icons.ts | 22 ++ web/src/beta/components/Loading/index.tsx | 2 +- web/src/beta/components/Modal/index.tsx | 6 +- .../components/fields/TextInput/index.tsx | 7 +- .../features/Navbar/LeftSection/index.tsx | 7 + .../Navbar/Menus/ProjectMenu/index.tsx | 20 +- web/src/beta/features/Navbar/hooks.ts | 37 ++- web/src/beta/features/Navbar/index.tsx | 8 +- .../ProjectSettings/MenuList/index.tsx | 54 ++++ .../beta/features/ProjectSettings/hooks.ts | 177 +++++++++++ .../beta/features/ProjectSettings/index.tsx | 169 ++++++++++ .../AssetSettings/AssetCard/index.stories.tsx | 27 ++ .../AssetSettings/AssetCard/index.tsx | 95 ++++++ .../AssetSettings/AssetContainer/hooks.ts | 132 ++++++++ .../AssetSettings/AssetContainer/index.tsx | 257 +++++++++++++++ .../AssetSettings/AssetDeleteModal/index.tsx | 32 ++ .../innerPages/AssetSettings/hooks.ts | 118 +++++++ .../innerPages/AssetSettings/index.tsx | 60 ++++ .../innerPages/GeneralSettings/index.tsx | 292 ++++++++++++++++++ .../PluginAccordionItem/deleteModal.tsx | 55 ++++ .../PluginAccordionItem/itemBody.tsx | 26 ++ .../PluginAccordionItem/itemHeader.tsx | 107 +++++++ .../PluginAccordion/index.stories.tsx | 34 ++ .../PluginSettings/PluginAccordion/index.tsx | 54 ++++ .../MarketplacePublish/index.tsx | 63 ++++ .../PluginInstall/PluginInstallCardButton.tsx | 41 +++ .../PluginInstall/PrivateRepo/index.tsx | 11 + .../PluginInstall/PublicRepo/hooks.test.tsx | 16 + .../PluginInstall/PublicRepo/hooks.ts | 44 +++ .../PluginInstall/PublicRepo/index.tsx | 81 +++++ .../PluginInstall/ZipUpload/index.tsx | 33 ++ .../PluginSettings/PluginInstall/index.tsx | 94 ++++++ .../innerPages/PluginSettings/hooks.ts | 98 ++++++ .../innerPages/PluginSettings/index.tsx | 153 +++++++++ .../PublicSettings/PublicSettingsDetail.tsx | 140 +++++++++ .../innerPages/PublicSettings/index.tsx | 114 +++++++ .../StorySettings/StorySettingsDetail.tsx | 45 +++ .../innerPages/StorySettings/index.tsx | 66 ++++ .../ProjectSettings/innerPages/common.tsx | 53 ++++ web/src/beta/pages/Page.tsx | 5 +- .../beta/pages/ProjectSettingsPage/index.tsx | 31 ++ web/src/beta/utils/infinite-scroll.ts | 23 ++ .../molecules/Dashboard/Project.tsx | 7 +- .../organisms/Settings/ProjectList/hooks.ts | 6 +- web/src/services/api/assetsApi.ts | 94 ++++++ web/src/services/api/index.ts | 1 + web/src/services/api/pluginsApi.ts | 225 +++++++++++--- web/src/services/api/projectApi.ts | 122 ++++++++ web/src/services/api/storytellingApi/index.ts | 23 +- web/src/services/config/extensions.ts | 1 + .../services/gql/__gen__/fragmentMatcher.json | 4 + web/src/services/gql/__gen__/gql.ts | 43 ++- web/src/services/gql/__gen__/graphql.ts | 227 +++++++++++++- web/src/services/gql/fragments/story.ts | 9 + web/src/services/gql/queries/asset.ts | 57 ++++ web/src/services/gql/queries/plugin.ts | 49 +++ web/src/services/gql/queries/scene.ts | 2 +- web/src/services/i18n/translations/en.yml | 167 +++++----- web/src/services/i18n/translations/ja.yml | 135 ++++---- web/src/services/routing/index.tsx | 8 +- .../theme/reearthTheme/common/fonts.ts | 35 +++ .../theme/reearthTheme/common/index.ts | 3 + .../theme/reearthTheme/common/spacing.ts | 15 + 79 files changed, 4263 insertions(+), 228 deletions(-) create mode 100644 web/src/beta/components/Accordion/AccordionItem.tsx create mode 100644 web/src/beta/components/Accordion/index.stories.tsx create mode 100644 web/src/beta/components/Accordion/index.test.tsx create mode 100644 web/src/beta/components/Accordion/index.tsx create mode 100644 web/src/beta/components/Collapse/index.stories.tsx create mode 100644 web/src/beta/components/Collapse/index.tsx create mode 100644 web/src/beta/components/Icon/Icons/bin.svg create mode 100644 web/src/beta/components/Icon/Icons/checkCircle.svg create mode 100644 web/src/beta/components/Icon/Icons/install.svg create mode 100644 web/src/beta/components/Icon/Icons/marketplace.svg create mode 100644 web/src/beta/components/Icon/Icons/publicGitHubRepo.svg create mode 100644 web/src/beta/components/Icon/Icons/scene.svg create mode 100644 web/src/beta/components/Icon/Icons/search.svg create mode 100644 web/src/beta/components/Icon/Icons/uploadSimple.svg create mode 100644 web/src/beta/components/Icon/Icons/uploadZipPlugin.svg create mode 100644 web/src/beta/features/ProjectSettings/MenuList/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/hooks.ts create mode 100644 web/src/beta/features/ProjectSettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.stories.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/hooks.ts create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetDeleteModal/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/hooks.ts create mode 100644 web/src/beta/features/ProjectSettings/innerPages/AssetSettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/GeneralSettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginAccordion/PluginAccordionItem/deleteModal.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginAccordion/PluginAccordionItem/itemBody.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginAccordion/PluginAccordionItem/itemHeader.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginAccordion/index.stories.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginAccordion/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/MarketplacePublish/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/PluginInstallCardButton.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/PrivateRepo/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/PublicRepo/hooks.test.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/PublicRepo/hooks.ts create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/PublicRepo/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/ZipUpload/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/PluginInstall/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/hooks.ts create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PluginSettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PublicSettings/PublicSettingsDetail.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/PublicSettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/StorySettings/StorySettingsDetail.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/StorySettings/index.tsx create mode 100644 web/src/beta/features/ProjectSettings/innerPages/common.tsx create mode 100644 web/src/beta/pages/ProjectSettingsPage/index.tsx create mode 100644 web/src/beta/utils/infinite-scroll.ts create mode 100644 web/src/services/api/assetsApi.ts create mode 100644 web/src/services/gql/queries/asset.ts create mode 100644 web/src/services/gql/queries/plugin.ts create mode 100644 web/src/services/theme/reearthTheme/common/spacing.ts diff --git a/web/src/beta/components/Accordion/AccordionItem.tsx b/web/src/beta/components/Accordion/AccordionItem.tsx new file mode 100644 index 000000000..fc84252ed --- /dev/null +++ b/web/src/beta/components/Accordion/AccordionItem.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { + AccordionItem as AccordionItemComponent, + AccordionItemButton, + AccordionItemHeading, + AccordionItemPanel, + AccordionItemState, +} from "react-accessible-accordion"; + +import { styled, useTheme } from "@reearth/services/theme"; + +import Icon from "../Icon"; + +export type Props = { + className?: string; + id: string; + heading?: React.ReactNode; + content?: React.ReactNode; + bg?: string; +}; + +const AccordionItem: React.FC = ({ className, id, heading, content, bg }) => { + const theme = useTheme(); + return ( + + + + + + + {({ expanded }) => ( + <> + + {heading} + + )} + + + + + + {content} + + + + ); +}; + +const Wrapper = styled.div<{ bg?: string }>` + margin: ${({ theme }) => theme.metrics["2xl"]}px 0; + background-color: ${({ bg }) => bg}; + border-radius: ${({ theme }) => theme.metrics["l"]}px; +`; + +const AccordionItemStateWrapper = styled.div` + display: flex; + align-items: center; + padding: ${({ theme }) => theme.metrics["xl"]}px; +`; + +const StyledIcon = styled(Icon)<{ open: boolean }>` + transition: transform 0.15s ease; + transform: ${({ open }) => open && "translateY(10%) rotate(90deg)"}; + margin-right: 24px; +`; + +const StyledAccordionItemButton = styled(AccordionItemButton)` + outline: none; + cursor: pointer; +`; +export default AccordionItem; diff --git a/web/src/beta/components/Accordion/index.stories.tsx b/web/src/beta/components/Accordion/index.stories.tsx new file mode 100644 index 000000000..7dfbbf6ad --- /dev/null +++ b/web/src/beta/components/Accordion/index.stories.tsx @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import Accordion from "."; + +const meta: Meta = { + component: Accordion, +}; + +export default meta; + +type Story = StoryObj; + +const SampleHeading =
heading
; + +const SampleContent = ( +
hoge
+); + +export const Default: Story = { + render: () => ( + + ), +}; diff --git a/web/src/beta/components/Accordion/index.test.tsx b/web/src/beta/components/Accordion/index.test.tsx new file mode 100644 index 000000000..f3436a8f3 --- /dev/null +++ b/web/src/beta/components/Accordion/index.test.tsx @@ -0,0 +1,42 @@ +import { expect, test } from "vitest"; + +import { fireEvent, render, screen } from "@reearth/test/utils"; + +import Accordion, { AccordionItemType } from "./index"; + +const sampleContents: AccordionItemType[] = [ + { + id: "1", + heading:
This is heading1
, + content:
This is content1
, + }, + { + id: "2", + heading:
This is heading2
, + content:
This is content2
, + }, +]; + +test("should be rendered", () => { + render(); +}); + +test("should display items header", () => { + render(); + expect(screen.getByTestId("atoms-accordion")).toBeInTheDocument(); + expect(screen.getByText(/heading1/)).toBeInTheDocument(); + expect(screen.getByText(/heading2/)).toBeInTheDocument(); +}); + +test("should display items content", () => { + render(); + expect(screen.getByText(/content1/)).toBeInTheDocument(); + expect(screen.getByText(/content2/)).toBeInTheDocument(); +}); + +test("should open when header button is clicked", () => { + render(); + expect(screen.getAllByTestId("atoms-accordion-item-content")[0]).not.toBeVisible(); + fireEvent.click(screen.getAllByTestId("atoms-accordion-item-header")[0]); + expect(screen.getAllByTestId("atoms-accordion-item-content")[0]).toBeVisible(); +}); diff --git a/web/src/beta/components/Accordion/index.tsx b/web/src/beta/components/Accordion/index.tsx new file mode 100644 index 000000000..0595e2787 --- /dev/null +++ b/web/src/beta/components/Accordion/index.tsx @@ -0,0 +1,47 @@ +import { Accordion as AccordionComponent } from "react-accessible-accordion"; + +import AccordionItem from "./AccordionItem"; + +export type Props = { + className?: string; + items?: AccordionItemType[]; + allowZeroExpanded?: boolean; + allowMultipleExpanded?: boolean; + itemBgColor?: string; +}; + +export type AccordionItemType = { + id: string; + heading?: React.ReactNode; + content?: React.ReactNode; +}; + +const Accordion: React.FC = ({ + className, + items, + allowMultipleExpanded, + allowZeroExpanded = true, + itemBgColor, +}) => { + return ( + + {items?.map(i => { + return ( + + ); + })} + + ); +}; + +export default Accordion; diff --git a/web/src/beta/components/Button/index.tsx b/web/src/beta/components/Button/index.tsx index d69178906..4ad59d051 100644 --- a/web/src/beta/components/Button/index.tsx +++ b/web/src/beta/components/Button/index.tsx @@ -19,6 +19,8 @@ export interface Props { margin?: string; onClick?: (e: React.MouseEvent) => void; + onMouseEnter?: () => void; + onMouseLeave?: () => void; } const Button: React.FC = ({ @@ -32,6 +34,8 @@ const Button: React.FC = ({ iconPosition = icon ? "left" : undefined, margin, onClick, + onMouseEnter, + onMouseLeave, }) => { const hasText = useMemo(() => { return !!text || !!children; @@ -57,7 +61,9 @@ const Button: React.FC = ({ text={hasText} disabled={disabled} margin={margin} - onClick={onClick}> + onClick={onClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave}> {iconPosition === "left" && WrappedIcon} {size === "medium" ? ( @@ -120,7 +126,7 @@ const StyledButton = styled.button` size === "medium" ? `${metricsSizes["s"]}px ${metricsSizes["l"]}px` : `${metricsSizes["xs"]}px ${metricsSizes["s"]}px`}; - margin: ${({ margin }) => margin || `${metricsSizes["m"]}px`}; + margin: ${({ margin }) => margin}; user-select: none; cursor: ${({ disabled }) => (disabled ? "not-allowed" : "pointer")}; justify-content: center; diff --git a/web/src/beta/components/Collapse/index.stories.tsx b/web/src/beta/components/Collapse/index.stories.tsx new file mode 100644 index 000000000..b1bb4ab54 --- /dev/null +++ b/web/src/beta/components/Collapse/index.stories.tsx @@ -0,0 +1,26 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import Collapse from "."; + +const meta: Meta = { + component: Collapse, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: "Title", + children:

Item

, + }, +}; + +export const AlwaysOpen: Story = { + args: { + title: "Title", + alwaysOpen: true, + children:

Item

, + }, +}; diff --git a/web/src/beta/components/Collapse/index.tsx b/web/src/beta/components/Collapse/index.tsx new file mode 100644 index 000000000..7b960cefb --- /dev/null +++ b/web/src/beta/components/Collapse/index.tsx @@ -0,0 +1,59 @@ +import { useState, ReactNode, useCallback } from "react"; + +import { styled, useTheme } from "@reearth/services/theme"; + +import Icon from "../Icon"; +import Text from "../Text"; + +const Collapse: React.FC<{ + title?: string; + alwaysOpen?: boolean; + children?: ReactNode; +}> = ({ title, alwaysOpen, children }) => { + const theme = useTheme(); + const [opened, setOpened] = useState(true); + const handleOpen = useCallback(() => { + if (!alwaysOpen) { + setOpened(!opened); + } + }, [alwaysOpen, opened]); + + return ( + + {title && ( +
+ + {title} + + {!alwaysOpen && ( + + )} +
+ )} + {opened && children && {children}} +
+ ); +}; + +const Field = styled.div` + background: ${({ theme }) => theme.bg[1]}; +`; + +const Header = styled.div<{ clickable?: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + cursor: ${({ clickable }) => (clickable ? "pointer" : "cursor")}; + padding: ${({ theme }) => theme.spacing.normal}px; +`; + +const ArrowIcon = styled(Icon)<{ opened: boolean }>` + transform: rotate(${props => (props.opened ? 90 : 180)}deg); + transition: all 0.2s; +`; + +const Content = styled.div` + padding: ${({ theme }) => `${theme.spacing.largest}px ${theme.spacing.super}px`}; +`; + +export default Collapse; diff --git a/web/src/beta/components/Icon/Icons/bin.svg b/web/src/beta/components/Icon/Icons/bin.svg new file mode 100644 index 000000000..3de70047c --- /dev/null +++ b/web/src/beta/components/Icon/Icons/bin.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/checkCircle.svg b/web/src/beta/components/Icon/Icons/checkCircle.svg new file mode 100644 index 000000000..8df7730dd --- /dev/null +++ b/web/src/beta/components/Icon/Icons/checkCircle.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/install.svg b/web/src/beta/components/Icon/Icons/install.svg new file mode 100644 index 000000000..d74011759 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/install.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/src/beta/components/Icon/Icons/marketplace.svg b/web/src/beta/components/Icon/Icons/marketplace.svg new file mode 100644 index 000000000..97bf15614 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/marketplace.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/beta/components/Icon/Icons/publicGitHubRepo.svg b/web/src/beta/components/Icon/Icons/publicGitHubRepo.svg new file mode 100644 index 000000000..7013d5aaa --- /dev/null +++ b/web/src/beta/components/Icon/Icons/publicGitHubRepo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/web/src/beta/components/Icon/Icons/scene.svg b/web/src/beta/components/Icon/Icons/scene.svg new file mode 100644 index 000000000..a48a30eb3 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/scene.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/src/beta/components/Icon/Icons/search.svg b/web/src/beta/components/Icon/Icons/search.svg new file mode 100644 index 000000000..4b4ae1f5f --- /dev/null +++ b/web/src/beta/components/Icon/Icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/src/beta/components/Icon/Icons/uploadSimple.svg b/web/src/beta/components/Icon/Icons/uploadSimple.svg new file mode 100644 index 000000000..adaa5b288 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/uploadSimple.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/src/beta/components/Icon/Icons/uploadZipPlugin.svg b/web/src/beta/components/Icon/Icons/uploadZipPlugin.svg new file mode 100644 index 000000000..32c47f6c7 --- /dev/null +++ b/web/src/beta/components/Icon/Icons/uploadZipPlugin.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/web/src/beta/components/Icon/icons.ts b/web/src/beta/components/Icon/icons.ts index 5db166a7a..0b1b747ee 100644 --- a/web/src/beta/components/Icon/icons.ts +++ b/web/src/beta/components/Icon/icons.ts @@ -34,6 +34,13 @@ import Plus from "./Icons/plus.svg"; import Minus from "./Icons/minus.svg"; import Alert from "./Icons/alert.svg"; import DndHandle from "./Icons/dndHandle.svg"; +import Bin from "./Icons/bin.svg"; +import Install from "./Icons/install.svg"; +import UploadSimple from "./Icons/uploadSimple.svg"; +import Search from "./Icons/search.svg"; + +// MSIC +import CheckCircle from "./Icons/checkCircle.svg"; // Dataset import File from "./Icons/fileIcon.svg"; @@ -46,6 +53,7 @@ import Ellipse from "./Icons/ellipse.svg"; // Dashboard import Dashboard from "./Icons/dashboard.svg"; +import Scene from "./Icons/scene.svg"; import Logout from "./Icons/logout.svg"; // Workspaces @@ -92,6 +100,11 @@ import Plugin from "./Icons/plugin.svg"; import Logo from "./Icons/reearthLogo.svg"; import LogoColorful from "./Icons/reearthLogoColorful.svg"; +// Plug-ins +import UploadZipPlugin from "./Icons/uploadZipPlugin.svg"; +import PublicGitHubRepo from "./Icons/publicGitHubRepo.svg"; +import Marketplace from "./Icons/marketplace.svg"; + export default { file: File, dl: InfoTable, @@ -162,4 +175,13 @@ export default { compassFocus: CompassFocus, house: House, plugin: Plugin, + scene: Scene, + uploadZipPlugin: UploadZipPlugin, + publicGitHubRepo: PublicGitHubRepo, + marketplace: Marketplace, + bin: Bin, + install: Install, + uploadSimple: UploadSimple, + search: Search, + checkCircle: CheckCircle, }; diff --git a/web/src/beta/components/Loading/index.tsx b/web/src/beta/components/Loading/index.tsx index d07bd1dc7..a15e25ccc 100644 --- a/web/src/beta/components/Loading/index.tsx +++ b/web/src/beta/components/Loading/index.tsx @@ -27,7 +27,7 @@ const Loading: React.FC = ({ const theme = useTheme(); const loading = ( - + ); return portal ? {loading} : loading; diff --git a/web/src/beta/components/Modal/index.tsx b/web/src/beta/components/Modal/index.tsx index e2cb6a7a1..38d7fe32a 100644 --- a/web/src/beta/components/Modal/index.tsx +++ b/web/src/beta/components/Modal/index.tsx @@ -118,9 +118,9 @@ const ContentWrapper = styled.div` const Content = styled.div` display: flex; - padding: 24px; + padding: ${({ theme }) => theme.spacing.super}px; flex-direction: column; - gap: 20px; + gap: ${({ theme }) => theme.spacing.largest}px; align-self: stretch; `; @@ -130,6 +130,8 @@ const ButtonWrapper = styled.div` align-self: stretch; justify-content: flex-end; border-top: 1px solid ${({ theme }) => theme.bg[3]}; + gap: ${({ theme }) => theme.spacing.normal}px; + padding: ${({ theme }) => theme.spacing.normal}px; `; export default Modal; diff --git a/web/src/beta/components/fields/TextInput/index.tsx b/web/src/beta/components/fields/TextInput/index.tsx index e2ea582f1..215043d2f 100644 --- a/web/src/beta/components/fields/TextInput/index.tsx +++ b/web/src/beta/components/fields/TextInput/index.tsx @@ -8,10 +8,11 @@ type Props = { name?: string; description?: string; value?: string; + timeout?: number; onChange?: (text: string) => void; }; -const TextInput: React.FC = ({ name, description, value, onChange }) => { +const TextInput: React.FC = ({ name, description, value, timeout = 1000, onChange }) => { const [currentValue, setCurrentValue] = useState(value ?? ""); const timeoutRef = useRef(); @@ -29,9 +30,9 @@ const TextInput: React.FC = ({ name, description, value, onChange }) => { timeoutRef.current = setTimeout(() => { if (newValue === undefined) return; onChange?.(newValue); - }, 1000); + }, timeout); }, - [onChange], + [onChange, timeout], ); const handleBlur = useCallback(() => { diff --git a/web/src/beta/features/Navbar/LeftSection/index.tsx b/web/src/beta/features/Navbar/LeftSection/index.tsx index cf33cc6fd..8fdbf9e52 100644 --- a/web/src/beta/features/Navbar/LeftSection/index.tsx +++ b/web/src/beta/features/Navbar/LeftSection/index.tsx @@ -17,6 +17,8 @@ type Props = { personalWorkspace: boolean; workspaces?: Workspace[]; modalShown: boolean; + sceneId?: string; + page: "editor" | "settings"; onSignOut: () => void; onWorkspaceCreate?: (data: { name: string }) => Promise; onWorkspaceChange?: (workspaceId: string) => void; @@ -31,6 +33,8 @@ const LeftSection: React.FC = ({ personalWorkspace, workspaces, modalShown, + sceneId, + page, onSignOut, onWorkspaceCreate, onWorkspaceChange, @@ -42,6 +46,9 @@ const LeftSection: React.FC = ({ {!dashboard && } + + {page === "settings" && } + = ({ currentProject, workspaceId }) => { const menuItems: MenuItem[] = [ { text: t("Project settings"), - linkTo: `/settings/projects/${currentProject.id}`, + linkTo: `/settings/project/${currentProject.id}`, }, { - text: t("Datasets"), - linkTo: `/settings/projects/${currentProject.id}/dataset`, + text: t("Story"), + linkTo: `/settings/project/${currentProject.id}/story`, }, { - text: t("Plugins"), - linkTo: `/settings/projects/${currentProject.id}/plugins`, + text: t("Public"), + linkTo: `/settings/project/${currentProject.id}/public`, }, { - text: t("Plugins"), - linkTo: `/settings/projects/${currentProject.id}/plugins`, + text: t("Workspace assets"), + linkTo: `/settings/project/${currentProject.id}/asset`, + }, + { + text: t("Plugin"), + linkTo: `/settings/project/${currentProject.id}/plugins`, }, { breakpoint: true }, { text: t("Manage projects"), - linkTo: `/settings/workspaces/${workspaceId}/projects`, + linkTo: `/settings/project/${workspaceId}/projects`, }, ]; diff --git a/web/src/beta/features/Navbar/hooks.ts b/web/src/beta/features/Navbar/hooks.ts index bc31542e4..4a1e3533d 100644 --- a/web/src/beta/features/Navbar/hooks.ts +++ b/web/src/beta/features/Navbar/hooks.ts @@ -3,14 +3,14 @@ import { useNavigate } from "react-router-dom"; import { useWorkspaceFetcher, useMeFetcher, useProjectFetcher } from "@reearth/services/api"; import { useAuth } from "@reearth/services/auth"; -import { Workspace, useProject, useWorkspace } from "@reearth/services/state"; +import { Workspace, useWorkspace } from "@reearth/services/state"; +import { ProjectType } from "@reearth/types"; export default ({ projectId, workspaceId }: { projectId?: string; workspaceId?: string }) => { const navigate = useNavigate(); const { logout: handleLogout } = useAuth(); const [currentWorkspace, setCurrentWorkspace] = useWorkspace(); // todo: remove when we don't rely on jotai anymore - const [currentProject, setProject] = useProject(); // todo: remove when we don't rely on jotai anymore const [workspaceModalVisible, setWorkspaceModalVisible] = useState(false); @@ -37,20 +37,25 @@ export default ({ projectId, workspaceId }: { projectId?: string; workspaceId?: const handleWorkspaceModalOpen = useCallback(() => setWorkspaceModalVisible(true), []); const handleWorkspaceModalClose = useCallback(() => setWorkspaceModalVisible(false), []); - useEffect(() => { - setProject(p => - p?.id !== project?.id - ? project - ? { - id: project.id, - name: project.name, - sceneId: project.scene?.id, - projectType: project.coreSupport ? "beta" : "classic", - } - : undefined - : p, - ); - }, [project, setProject]); + const currentProject: + | { + id: string; + name: string; + sceneId?: string; + projectType: ProjectType; + } + | undefined = useMemo( + () => + project + ? { + id: project.id, + name: project.name, + sceneId: project.scene?.id, + projectType: project.coreSupport ? "beta" : "classic", + } + : undefined, + [project], + ); const handleWorkspaceChange = useCallback( (id: string) => { diff --git a/web/src/beta/features/Navbar/index.tsx b/web/src/beta/features/Navbar/index.tsx index e8396f428..171ba5ed4 100644 --- a/web/src/beta/features/Navbar/index.tsx +++ b/web/src/beta/features/Navbar/index.tsx @@ -9,7 +9,8 @@ type Props = { projectId?: string; workspaceId?: string; isDashboard?: boolean; - currentTab: Tab; + currentTab?: Tab; + page?: "editor" | "settings"; }; export const Tabs = ["map", "story", "widgets", "publish"] as const; @@ -25,6 +26,7 @@ const Navbar: React.FC = ({ workspaceId, currentTab = "map", isDashboard = false, + page = "editor", }) => { const { currentProject, @@ -43,7 +45,7 @@ const Navbar: React.FC = ({ const { rightSide } = useRightSide({ currentTab, sceneId, - page: "editor", + page, }); return ( @@ -56,6 +58,8 @@ const Navbar: React.FC = ({ personalWorkspace={isPersonal} modalShown={workspaceModalVisible} workspaces={workspaces} + sceneId={sceneId} + page={page} onWorkspaceChange={handleWorkspaceChange} onWorkspaceCreate={handleWorkspaceCreate} onSignOut={handleLogout} diff --git a/web/src/beta/features/ProjectSettings/MenuList/index.tsx b/web/src/beta/features/ProjectSettings/MenuList/index.tsx new file mode 100644 index 000000000..8c143cf43 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/MenuList/index.tsx @@ -0,0 +1,54 @@ +import { Link } from "react-router-dom"; + +import Text from "@reearth/beta/components/Text"; +import { styled, useTheme } from "@reearth/services/theme"; + +export const MenuItem: React.FC<{ + text?: string; + linkTo?: string; + onClick?: () => void; + color?: string; + active?: boolean; +}> = ({ text, linkTo, onClick, color, active }) => { + const theme = useTheme(); + const content = ( + + + {text} + + + ); + + return !linkTo ? content : {content}; +}; + +export const MenuList = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + margin: 0; + padding: 12px 0; + width: 200px; +`; + +const MenuItemWrapper = styled.div<{ active?: boolean }>` + display: flex; + box-sizing: border-box; + width: 100%; + padding: 8px 16px; + cursor: pointer; + background-color: ${({ active, theme }) => (active ? theme.select.main : "")}; + transition: all 0.3s ease; + &:hover { + background-color: ${({ active, theme }) => (active ? theme.select.main : theme.bg[2])}; + } +`; + +const StyledLinkButton = styled(Link)` + text-decoration: none; + width: 100%; + + :hover { + text-decoration: none; + } +`; diff --git a/web/src/beta/features/ProjectSettings/hooks.ts b/web/src/beta/features/ProjectSettings/hooks.ts new file mode 100644 index 000000000..1183f58e0 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/hooks.ts @@ -0,0 +1,177 @@ +import { useCallback, useMemo, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; + +import { useProjectFetcher, useSceneFetcher } from "@reearth/services/api"; +import useStorytellingAPI from "@reearth/services/api/storytellingApi"; +import { useAuth } from "@reearth/services/auth"; +import { config } from "@reearth/services/config"; + +import { GeneralSettingsType } from "./innerPages/GeneralSettings"; +import { + PublicBasicAuthSettingsType, + PublicSettingsType, + PublicAliasSettingsType, +} from "./innerPages/PublicSettings"; +import { StorySettingsType } from "./innerPages/StorySettings"; + +import { projectSettingsTab } from "."; + +type Props = { + projectId: string; + tab?: projectSettingsTab; + subId?: string; +}; + +export default ({ projectId, tab, subId }: Props) => { + const navigate = useNavigate(); + + const { + useProjectQuery, + useUpdateProject, + useArchiveProject, + useDeleteProject, + useUpdateProjectBasicAuth, + useUpdateProjectAlias, + } = useProjectFetcher(); + const { useSceneQuery } = useSceneFetcher(); + + const { project } = useProjectQuery(projectId); + + const { scene } = useSceneQuery({ sceneId: project?.scene?.id }); + + const workspaceId = scene?.teamId; + + const handleUpdateProject = useCallback( + async (settings: GeneralSettingsType & PublicSettingsType) => { + await useUpdateProject({ projectId, ...settings }); + }, + [projectId, useUpdateProject], + ); + + const handleArchiveProject = useCallback( + async (archived: boolean) => { + const { status } = await useArchiveProject({ projectId, archived }); + if (status === "success") { + navigate(`/settings/workspaces/${workspaceId}/projects`); + } + }, + [workspaceId, projectId, useArchiveProject, navigate], + ); + + const handleDeleteProject = useCallback(async () => { + const { status } = await useDeleteProject({ projectId }); + if (status === "success") { + navigate(`/settings/workspaces/${workspaceId}/projects`); + } + }, [workspaceId, projectId, useDeleteProject, navigate]); + + const handleUpdateProjectBasicAuth = useCallback( + async (settings: PublicBasicAuthSettingsType) => { + if (!projectId) return; + await useUpdateProjectBasicAuth({ projectId, ...settings }); + }, + [projectId, useUpdateProjectBasicAuth], + ); + + const handleUpdateProjectAlias = useCallback( + async (settings: PublicAliasSettingsType) => { + if (!projectId) return; + await useUpdateProjectAlias({ projectId, ...settings }); + }, + [projectId, useUpdateProjectAlias], + ); + + const stories = useMemo(() => scene?.stories ?? [], [scene?.stories]); + const currentStory = useMemo( + () => + tab === "story" + ? stories.find(s => s.id === subId) ?? stories[0] + : tab === "public" + ? stories.find(s => s.id === subId) + : undefined, + [tab, subId, stories], + ); + + const { useUpdateStory } = useStorytellingAPI(); + const handleUpdateStory = useCallback( + async (settings: PublicSettingsType & StorySettingsType) => { + if (!scene?.id || !currentStory?.id) return; + await useUpdateStory({ storyId: currentStory.id, sceneId: scene.id, ...settings }); + }, + [useUpdateStory, currentStory?.id, scene?.id], + ); + + const handleUpdateStoryBasicAuth = useCallback( + async (settings: PublicBasicAuthSettingsType) => { + if (!scene?.id || !currentStory?.id) return; + await useUpdateStory({ storyId: currentStory.id, sceneId: scene.id, ...settings }); + }, + [useUpdateStory, currentStory?.id, scene?.id], + ); + const handleUpdateStoryAlias = useCallback( + async (settings: PublicAliasSettingsType) => { + if (!scene?.id || !currentStory?.id) return; + await useUpdateStory({ storyId: currentStory.id, sceneId: scene.id, ...settings }); + }, + [useUpdateStory, currentStory?.id, scene?.id], + ); + + const { getAccessToken } = useAuth(); + const [accessToken, setAccessToken] = useState(); + + useEffect(() => { + getAccessToken().then(token => { + setAccessToken(token); + }); + }, [getAccessToken]); + + const extensions = useMemo( + () => ({ + library: config()?.extensions?.pluginLibrary, + installed: config()?.extensions?.pluginInstalled, + }), + [], + ); + + // Redirection for classic projects + useEffect(() => { + if (!project) return; + if (!project.coreSupport) { + switch (tab) { + case "general": + navigate(`/settings/projects/${projectId}`); + break; + case "public": + navigate(`/settings/projects/${projectId}/public`); + break; + case "asset": + navigate(`/settings/workspaces/${workspaceId}/asset`); + break; + case "plugins": + navigate(`/settings/projects/${projectId}/plugins`); + break; + default: + navigate(`/settings/projects/${projectId}`); + } + } + }, [project, projectId, tab, workspaceId, navigate]); + + return { + sceneId: scene?.id, + workspaceId, + project, + plugins: scene?.plugins, + stories, + currentStory, + accessToken, + extensions, + handleUpdateProject, + handleArchiveProject, + handleDeleteProject, + handleUpdateProjectBasicAuth, + handleUpdateProjectAlias, + handleUpdateStory, + handleUpdateStoryBasicAuth, + handleUpdateStoryAlias, + }; +}; diff --git a/web/src/beta/features/ProjectSettings/index.tsx b/web/src/beta/features/ProjectSettings/index.tsx new file mode 100644 index 000000000..4e3b1fd62 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/index.tsx @@ -0,0 +1,169 @@ +import { useMemo } from "react"; + +import Text from "@reearth/beta/components/Text"; +import Navbar from "@reearth/beta/features/Navbar"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import useHooks from "./hooks"; +import AssetSettings from "./innerPages/AssetSettings"; +import GeneralSettings from "./innerPages/GeneralSettings"; +import PluginSettings from "./innerPages/PluginSettings"; +import PublicSettings from "./innerPages/PublicSettings"; +import StorySettings from "./innerPages/StorySettings"; +import { MenuList, MenuItem } from "./MenuList"; + +export const projectSettingTabs = [ + { id: "general", text: "General" }, + { id: "story", text: "Story" }, + { id: "public", text: "Public" }, + { id: "asset", text: "Workspace Assets" }, + { id: "plugins", text: "Plugin" }, +] as const; + +export type projectSettingsTab = (typeof projectSettingTabs)[number]["id"]; + +export function isProjectSettingTab(tab: string): tab is projectSettingsTab { + return projectSettingTabs.map(f => f.id).includes(tab as never); +} + +type Props = { + projectId: string; + tab?: projectSettingsTab; + subId?: string; +}; + +const ProjectSettings: React.FC = ({ projectId, tab, subId }) => { + const t = useT(); + const { + sceneId, + workspaceId, + project, + plugins, + stories, + currentStory, + accessToken, + extensions, + handleUpdateProject, + handleArchiveProject, + handleDeleteProject, + handleUpdateProjectBasicAuth, + handleUpdateProjectAlias, + handleUpdateStory, + handleUpdateStoryBasicAuth, + handleUpdateStoryAlias, + } = useHooks({ + projectId, + tab, + subId, + }); + + const tabs = useMemo( + () => + projectSettingTabs.map(tab => ({ + id: tab.id, + text: t(tab.text), + linkTo: `/settings/project/${projectId}/${tab.id === "general" ? "" : tab.id}`, + })), + [projectId, t], + ); + + return ( + + + + {t("Project Settings")} + + + + + {tabs.map(t => ( + + ))} + + + + {tab === "general" && project && ( + + )} + {tab === "story" && currentStory && ( + + )} + {tab === "public" && project && ( + + )} + {tab === "plugins" && ( + + )} + {tab === "asset" && workspaceId && } + + + + ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + color: ${({ theme }) => theme.content.main}; + background-color: ${({ theme }) => theme.bg[0]}; +`; + +const SecondaryNav = styled.div` + color: ${({ theme }) => theme.content.main}; + background-color: ${({ theme }) => theme.bg[1]}; + border-bottom: 1px solid ${({ theme }) => theme.outline.weak}; +`; + +const Title = styled(Text)` + padding: 12px; +`; + +const MainSection = styled.div` + flex: 1; + overflow: auto; +`; + +const Menu = styled.div` + position: fixed; + height: 100%; + background-color: ${({ theme }) => theme.bg[1]}; +`; + +const Content = styled.div` + display: flex; + justify-content: center; + margin-left: 200px; + padding: 20px; +`; + +export default ProjectSettings; diff --git a/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.stories.tsx b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.stories.tsx new file mode 100644 index 000000000..ec5008160 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.stories.tsx @@ -0,0 +1,27 @@ +import { Meta, StoryObj } from "@storybook/react"; + +import AssetCard from "."; + +const meta: Meta = { + component: AssetCard, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + selected: false, + url: `/sample.svg`, + name: "hoge", + }, +}; + +export const Selected: Story = { + args: { + url: `/sample.svg`, + name: "hoge", + selected: true, + }, +}; diff --git a/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.tsx b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.tsx new file mode 100644 index 000000000..a19a06e87 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetCard/index.tsx @@ -0,0 +1,95 @@ +import React from "react"; + +import Icon from "@reearth/beta/components/Icon"; +import Text from "@reearth/beta/components/Text"; +import { styled } from "@reearth/services/theme"; + +export type Props = { + className?: string; + name: string; + url?: string; + icon?: string; + iconSize?: string; + checked?: boolean; + selected?: boolean; + onSelect?: (selected: boolean) => void; +}; + +const AssetCard: React.FC = ({ + className, + name, + url, + icon, + iconSize, + checked, + selected, + onSelect, +}) => { + return ( + onSelect?.(!selected)}> + + {!icon ? : } + + + {name} + {checked && } + + + ); +}; + +const Wrapper = styled.div<{ selected?: boolean }>` + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.bg[1]}; + box-shadow: 2px 2px 2px 0 rgba(0, 0, 0, 0.25); + border: 2px solid ${({ selected, theme }) => (selected ? `${theme.select.main}` : "transparent")}; + padding: ${({ theme }) => theme.spacing.small}px; + width: 100%; + max-width: 148px; + height: 100%; + position: relative; + cursor: pointer; + color: ${({ theme }) => theme.content.main}; + box-sizing: border-box; + + &:hover { + background: ${({ theme }) => theme.bg[2]}; + } +`; + +const ImgWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + height: 75px; +`; + +const InfoWrapper = styled.div` + display: flex; +`; + +const PreviewImage = styled.div<{ url?: string }>` + width: 100%; + height: 100%; + background-image: ${props => `url(${props.url})`}; + background-size: cover; + background-position: center; +`; + +const FileName = styled(Text)` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-top: ${({ theme }) => theme.spacing.small}px; + color: inherit; +`; + +const StyledIcon = styled(Icon)` + position: absolute; + bottom: 7px; + right: 7px; + color: ${({ theme }) => theme.select.main}; +`; + +export default AssetCard; diff --git a/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/hooks.ts b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/hooks.ts new file mode 100644 index 000000000..112d42047 --- /dev/null +++ b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/hooks.ts @@ -0,0 +1,132 @@ +import { useState, useCallback, useMemo, useRef, useEffect } from "react"; +import useFileInput from "use-file-input"; + +import { autoFillPage, onScrollToBottom } from "@reearth/beta/utils/infinite-scroll"; +import { useT } from "@reearth/services/i18n"; + +export type SortType = "date" | "name" | "size"; + +export const fileFormats = ".kml,.czml,.topojson,.geojson,.json,.gltf,.glb"; + +export const imageFormats = ".jpg,.jpeg,.png,.gif,.svg,.tiff,.webp"; + +export type Asset = { + id: string; + teamId: string; + name: string; + size: number; + url: string; + contentType: string; +}; + +export default ({ + selectedAssets, + sort, + searchTerm, + isLoading, + hasMoreAssets, + onGetMore, + onCreateAssets, + onAssetUrlSelect, + onRemove, + onSortChange, + onSearch, +}: { + selectedAssets?: Asset[]; + sort?: { type?: SortType | null; reverse?: boolean }; + searchTerm?: string; + isLoading?: boolean; + hasMoreAssets?: boolean; + onGetMore?: () => void; + onCreateAssets?: (files: FileList) => void; + onAssetUrlSelect?: (asset?: string) => void; + onRemove?: (assetIds: string[]) => void; + onSortChange?: (type?: string, reverse?: boolean) => void; + onSearch?: (term?: string | undefined) => void; +}) => { + const t = useT(); + const [deleteModalVisible, setDeleteModalVisible] = useState(false); + + const sortOptions: { key: SortType; label: string }[] = useMemo( + () => [ + { key: "date", label: t("Date") }, + { key: "size", label: t("File size") }, + { key: "name", label: t("Alphabetical") }, + ], + [t], + ); + + const iconChoice = + sort?.type === "name" + ? sort?.reverse + ? "filterNameReverse" + : "filterName" + : sort?.type === "size" + ? sort?.reverse + ? "filterSizeReverse" + : "filterSize" + : sort?.reverse + ? "filterTimeReverse" + : "filterTime"; + + const handleFileSelect = useFileInput(files => onCreateAssets?.(files), { + accept: imageFormats + "," + fileFormats, + multiple: true, + }); + + const handleUploadToAsset = useCallback(() => { + handleFileSelect(); + }, [handleFileSelect]); + + const handleRemove = useCallback(() => { + if (selectedAssets?.length) { + onRemove?.(selectedAssets.map(a => a.id)); + onAssetUrlSelect?.(); + setDeleteModalVisible(false); + } + }, [onRemove, onAssetUrlSelect, selectedAssets]); + + const handleReverse = useCallback(() => { + onSortChange?.(undefined, !sort?.reverse); + }, [onSortChange, sort?.reverse]); + + const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm ?? ""); + const handleSearchInputChange = useCallback( + (e: React.ChangeEvent) => { + setLocalSearchTerm(e.currentTarget.value); + }, + [setLocalSearchTerm], + ); + const handleSearch = useCallback(() => { + if (!localSearchTerm || localSearchTerm.length < 1) { + onSearch?.(undefined); + } else { + onSearch?.(localSearchTerm); + } + }, [onSearch, localSearchTerm]); + + const openDeleteModal = useCallback(() => setDeleteModalVisible(true), []); + const closeDeleteModal = useCallback(() => setDeleteModalVisible(false), []); + + const wrapperRef = useRef(null); + + useEffect(() => { + if (wrapperRef.current && !isLoading && hasMoreAssets) autoFillPage(wrapperRef, onGetMore); + }, [hasMoreAssets, isLoading, onGetMore]); + + return { + iconChoice, + deleteModalVisible, + sortOptions, + localSearchTerm, + wrapperRef, + handleSearchInputChange, + handleUploadToAsset, + handleReverse, + handleSearch, + openDeleteModal, + closeDeleteModal, + handleRemove, + onScrollToBottom, + }; +}; diff --git a/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/index.tsx b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/index.tsx new file mode 100644 index 000000000..d9120ddfd --- /dev/null +++ b/web/src/beta/features/ProjectSettings/innerPages/AssetSettings/AssetContainer/index.tsx @@ -0,0 +1,257 @@ +import Button from "@reearth/beta/components/Button"; +import Loading from "@reearth/beta/components/Loading"; +import Text from "@reearth/beta/components/Text"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import AssetCard from "../AssetCard"; +import AssetDeleteModal from "../AssetDeleteModal"; + +import useHooks, { Asset as AssetType, SortType, fileFormats, imageFormats } from "./hooks"; + +export type Asset = AssetType; + +export type AssetSortType = SortType; + +export type Props = { + workspaceId?: string; + allowDeletion?: boolean; + className?: string; + assets?: Asset[]; + selectedAssets?: Asset[]; + isLoading?: boolean; + hasMoreAssets?: boolean; + sort?: { type?: AssetSortType | null; reverse?: boolean }; + searchTerm?: string; + onCreateAssets?: (files: FileList) => void; + onRemove?: (assetIds: string[]) => void; + onGetMore?: () => void; + onAssetUrlSelect?: (asset?: string) => void; + onSelect?: (asset?: Asset) => void; + onSortChange?: (type?: string, reverse?: boolean) => void; + onSearch?: (term?: string) => void; +}; + +const AssetContainer: React.FC = ({ + assets, + selectedAssets, + hasMoreAssets, + isLoading, + sort, + searchTerm, + onCreateAssets, + onRemove, + onGetMore, + onAssetUrlSelect, + onSelect, + onSortChange, + onSearch, +}) => { + const t = useT(); + const { + deleteModalVisible, + + localSearchTerm, + wrapperRef, + onScrollToBottom, + handleSearchInputChange, + handleUploadToAsset, + // iconChoice, + // sortOptions, + // handleReverse, + handleSearch, + openDeleteModal, + closeDeleteModal, + handleRemove, + } = useHooks({ + sort, + selectedAssets, + searchTerm, + isLoading, + hasMoreAssets, + onGetMore, + onSortChange, + onCreateAssets, + onAssetUrlSelect, + onRemove, + onSearch, + }); + + return ( + + + + + +