Skip to content

Commit

Permalink
chore(web): Camera block (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
jashanbhullar committed Oct 17, 2023
1 parent c579c63 commit bac4ba8
Show file tree
Hide file tree
Showing 13 changed files with 396 additions and 37 deletions.
28 changes: 24 additions & 4 deletions web/src/beta/components/fields/ListField/index.tsx
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { useCallback, useEffect, useMemo } from "react";

import Button from "@reearth/beta/components/Button";
import DragAndDropList, {
Expand All @@ -22,6 +22,7 @@ export type Props = {
addItem: () => void;
onSelect: (id: string) => void;
selected?: string;
atLeastOneItem?: boolean;
} & Pick<DragAndDropProps, "onItemDrop">;

const ListField: React.FC<Props> = ({
Expand All @@ -33,6 +34,7 @@ const ListField: React.FC<Props> = ({
onItemDrop,
onSelect,
selected,
atLeastOneItem,
}: Props) => {
const t = useT();

Expand All @@ -46,10 +48,20 @@ const ListField: React.FC<Props> = ({
}, []);

const disableRemoveButton = useMemo(() => {
if (!selected) return true;
if (!selected || (atLeastOneItem && items.length === 1)) return true;

return !items.find(({ id }) => id == selected);
}, [items, selected]);
}, [items, selected, atLeastOneItem]);

// if atleastOneItem is true, make sure one item is always selected
useEffect(() => {
if (!atLeastOneItem) return;

const updateSelected = !selected || !items.find(({ id }) => id === selected);
if (updateSelected) {
onSelect(items[0]?.id);
}
}, [selected, items, atLeastOneItem, onSelect]);

return (
<Property name={name} description={description}>
Expand All @@ -61,7 +73,7 @@ const ListField: React.FC<Props> = ({
getId={getId}
renderItem={({ id, value }) => (
<Item onClick={() => onSelect(id)} selected={selected === id}>
<Text size="xFootnote">{value}</Text>
<StyledText size="xFootnote">{value}</StyledText>
</Item>
)}
gap={0}
Expand Down Expand Up @@ -108,6 +120,14 @@ const Item = styled.div<{ selected: boolean }>`
}
`;

const StyledText = styled(Text)`
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
`;

const ButtonGroup = styled.div`
display: flex;
gap: 4px;
Expand Down
174 changes: 174 additions & 0 deletions web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/Editor.tsx
@@ -0,0 +1,174 @@
import { useReactiveVar } from "@apollo/client";
import { debounce } from "lodash-es";
import { useContext, useMemo, useState } from "react";

import Button from "@reearth/beta/components/Button";
import CameraField from "@reearth/beta/components/fields/CameraField";
import ColorField from "@reearth/beta/components/fields/ColorField";
import ListField from "@reearth/beta/components/fields/ListField";
import TextField from "@reearth/beta/components/fields/TextField";
import { Camera } from "@reearth/beta/lib/core/engines";
import { useVisualizer } from "@reearth/beta/lib/core/Visualizer/context";
import { useT } from "@reearth/services/i18n";
import { currentCameraVar } from "@reearth/services/state";
import { styled } from "@reearth/services/theme";

import { BlockContext } from "../common/Wrapper";

type CameraBlock = {
id: string;
title?: string;
color?: string;
bgColor?: string;
cameraPosition?: Camera;
};

export type Props = {
items: CameraBlock[];
onUpdate: (
id: string,
fieldId: keyof CameraBlock,
fieldType: "string" | "camera",
value: string | Camera,
) => void;
onItemRemove: (id: string) => void;
onItemAdd: () => void;
onItemMove: ({ id }: { id: string }, index: number) => void;
inEditor: boolean;
};

const CameraBlockEditor: React.FC<Props> = ({
items,
onUpdate,
onItemRemove,
onItemAdd,
onItemMove,
inEditor,
}) => {
const t = useT();
const context = useContext(BlockContext);
const [selected, setSelected] = useState(items[0]?.id);

const visualizer = useVisualizer();
const currentCamera = useReactiveVar(currentCameraVar);
const handleFlyTo = useMemo(() => visualizer.current?.engine.flyTo, [visualizer]);

const editorProperties = useMemo(() => items.find(i => i.id === selected), [items, selected]);

const handleClick = (itemId: string) => {
if (inEditor) {
setSelected(itemId);
return;
}
const item = items.find(i => i.id === itemId);
if (!item?.cameraPosition) return;
handleFlyTo?.(item.cameraPosition);
};

const debounceOnUpdate = useMemo(() => debounce(onUpdate, 500), [onUpdate]);

const listItems = useMemo(
() => items.map(({ id, title }) => ({ id, value: title ?? "New Camera" })),
[items],
);

return (
<Wrapper>
<ButtonWrapper>
{items.map(({ title, color, bgColor, id }) => {
return (
<StyledButton
key={id}
color={color}
bgColor={bgColor}
icon="cameraButtonStoryBlock"
buttonType="primary"
text={title ?? t("New Camera")}
size="small"
onClick={() => handleClick(id)}
/>
);
})}
</ButtonWrapper>
{context?.editMode && (
<EditorWrapper>
<ListField
name={t("Buttons List")}
items={listItems}
addItem={onItemAdd}
removeItem={onItemRemove}
onItemDrop={onItemMove}
selected={selected}
onSelect={setSelected}
atLeastOneItem
/>
<FieldGroup disabled={!editorProperties}>
<CameraField
name={t("Camera pos")}
value={editorProperties?.cameraPosition}
onSave={value => onUpdate(selected, "cameraPosition", "camera", value as Camera)}
currentCamera={currentCamera}
onFlyTo={handleFlyTo}
/>
<TextField
name={t("Button Title")}
value={editorProperties?.title}
onChange={value => debounceOnUpdate(selected, "title", "string", value)}
/>
<ColorField
name={t("Button Color")}
value={editorProperties?.color}
onChange={value => debounceOnUpdate(selected, "color", "string", value)}
/>
<ColorField
name={t("Button Background Color")}
value={editorProperties?.bgColor}
onChange={value => debounceOnUpdate(selected, "bgColor", "string", value)}
/>
</FieldGroup>
</EditorWrapper>
)}
</Wrapper>
);
};

const Wrapper = styled.div`
width: 100%;
`;

const ButtonWrapper = styled.div`
width: 100%;
display: flex;
gap: 4px;
max-width: 400px;
flex-wrap: wrap;
`;

const StyledButton = styled(Button)<{ color?: string; bgColor?: string }>`
color: ${({ color }) => color};
background-color: ${({ bgColor }) => bgColor};
border-color: ${({ color }) => color};
&:hover {
color: ${({ bgColor }) => bgColor};
background-color: ${({ color }) => color};
}
`;

const EditorWrapper = styled.div`
padding: 12px;
margin: 2px 0;
background: ${({ theme }) => theme.bg[1]};
`;

const FieldGroup = styled.div<{ disabled: boolean }>`
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
opacity: ${({ disabled }) => (disabled ? 0.6 : 1)};
cursor: ${({ disabled }) => (disabled ? "not-allowed" : "inherit")};
pointer-events: ${({ disabled }) => (disabled ? "none" : "inherit")};
`;

export default CameraBlockEditor;
109 changes: 109 additions & 0 deletions web/src/beta/lib/core/StoryPanel/Block/builtin/Camera/index.tsx
@@ -0,0 +1,109 @@
import { useCallback, useEffect, useMemo } from "react";

import { ValueTypes } from "@reearth/beta/utils/value";

import { getFieldValue } from "../../../utils";
import { CommonProps as BlockProps } from "../../types";
import usePropertyValueUpdate from "../common/useActionPropertyApi";
import BlockWrapper from "../common/Wrapper";

import CameraEditor, { Props as EditorProps } from "./Editor";

export type Props = BlockProps;

const CameraBlock: React.FC<Props> = ({ block, isSelected, ...props }) => {
const {
handlePropertyValueUpdate,
handleAddPropertyItem,
handleRemovePropertyItem,
handleMovePropertyItem,
} = usePropertyValueUpdate();

const items = useMemo(
() => getFieldValue(block?.property?.items ?? [], "") as EditorProps["items"],
[block?.property?.items],
);

const handleUpdate = useCallback(
(
itemId: string,
fieldId: string,
fieldType: keyof ValueTypes,
updatedValue: ValueTypes[keyof ValueTypes],
) => {
const schemaGroup = block?.property?.items?.find(
i => i.schemaGroup === "default",
)?.schemaGroup;
if (!block?.property?.id || !itemId || !schemaGroup) return;

handlePropertyValueUpdate(
schemaGroup,
block?.property?.id,
fieldId,
fieldType,
itemId,
)(updatedValue);
},
[block?.property?.id, block?.property?.items, handlePropertyValueUpdate],
);

const handleItemAdd = useCallback(() => {
const schemaGroup = block?.property?.items?.find(i => i.schemaGroup === "default")?.schemaGroup;
if (!block?.property?.id || !schemaGroup) return;
handleAddPropertyItem(block.property.id, schemaGroup);
}, [block?.property?.id, block?.property?.items, handleAddPropertyItem]);

const handleItemRemove = useCallback(
(itemId: string) => {
const schemaGroup = block?.property?.items?.find(
i => i.schemaGroup === "default",
)?.schemaGroup;
if (!block?.property?.id || !itemId || !schemaGroup) return;

handleRemovePropertyItem(block.property.id, schemaGroup, itemId);
},
[block?.property?.id, block?.property?.items, handleRemovePropertyItem],
);

const handleItemMove = useCallback(
({ id }: { id: string }, index: number) => {
const schemaGroup = block?.property?.items?.find(
i => i.schemaGroup === "default",
)?.schemaGroup;
if (!block?.property?.id || !id || !schemaGroup) return;

handleMovePropertyItem(block.property.id, schemaGroup, { id }, index);
},
[block?.property?.id, block?.property?.items, handleMovePropertyItem],
);

// if there's no item add 1 button.
// TODO: Should be added to block creationAPI for generic blocks that require at least 1 item
useEffect(() => {
if (items.length === 0) {
handleItemAdd();
return;
}
}, [items.length, handleItemAdd]);

return (
<BlockWrapper
icon={block?.extensionId}
isSelected={isSelected}
propertyId={block?.property?.id}
propertyItems={block?.property?.items}
settingsEnabled={false}
{...props}>
<CameraEditor
items={items}
onUpdate={handleUpdate}
onItemRemove={handleItemRemove}
onItemAdd={handleItemAdd}
onItemMove={handleItemMove}
inEditor={!!props.isEditable}
/>
</BlockWrapper>
);
};

export default CameraBlock;
Expand Up @@ -4,7 +4,7 @@ import { ValueTypes } from "@reearth/beta/utils/value";

import { getFieldValue } from "../../../utils";
import { CommonProps as BlockProps } from "../../types";
import usePropertyValueUpdate from "../common/usePropertyValueUpdate";
import usePropertyValueUpdate from "../common/useActionPropertyApi";
import BlockWrapper from "../common/Wrapper";

import MdEditor from "./Editor";
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { ValueTypes } from "@reearth/beta/utils/value";

import { getFieldValue } from "../../../utils";
import { CommonProps as BlockProps } from "../../types";
import usePropertyValueUpdate from "../common/usePropertyValueUpdate";
import usePropertyValueUpdate from "../common/useActionPropertyApi";
import BlockWrapper from "../common/Wrapper";

import TextBlockEditor from "./Editor";
Expand Down

0 comments on commit bac4ba8

Please sign in to comment.