Skip to content

Commit

Permalink
feat: accordion element (#82)
Browse files Browse the repository at this point in the history
* refactor: placeholder style

* feat: add accordion style

* feat: accordion element

* feat: accordion setting

* cleanup

* �feat: OG 이미지 설정 (#79)

* feat: update thumbnail

* feat: apply og image

* feat: add layout thumbnail

* feat: 템플릿의 썸네일이 초기 적용되도록

* feat: 로딩 (#80)

* chore: InView with framer-motion

* feat: add loading layout

* feat: text-effect

* feat: global loading

* feat: delay create invitation

* feat: 저장 중 로딩

* feat: 삭제 토스트 추가

* feat: 저장 버튼 평소 활성화

* �feat: OG 이미지 설정 (#79)

* feat: update thumbnail

* feat: apply og image

* feat: add layout thumbnail

* feat: 템플릿의 썸네일이 초기 적용되도록
  • Loading branch information
bepyan authored Aug 20, 2024
1 parent 810f2aa commit 3e65947
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 6 deletions.
55 changes: 55 additions & 0 deletions src/components/editor/elements/accordion-element.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { ChevronDownIcon } from "lucide-react";
import { useState } from "react";
import ElementWrapper from "~/components/editor/elements/element-wrapper";
import Text from "~/components/editor/elements/text";
import { useEditor } from "~/components/editor/provider";
import type { InferEditorElement } from "~/components/editor/type";

export default function AccordionElement({
element,
}: {
element: InferEditorElement<"accordion">;
}) {
const { editor } = useEditor();
const [isOpen, setIsOpen] = useState(
editor.state.isPreviewMode ? false : true,
);

return (
<ElementWrapper element={element}>
<div
className="overflow-hidden rounded-[20px] bg-muted pb-3"
style={element.content.containerStyle}
>
<button
data-state={isOpen ? "open" : "closed"}
className="flex w-full select-none items-center justify-between p-6 pb-3 text-xl font-semibold [&[data-state=open]>svg]:rotate-180"
style={element.content.triggerStyle}
onClick={() => editor.state.isPreviewMode && setIsOpen(!isOpen)}
>
{element.content.triggerText ?? "제목"}
<ChevronDownIcon className="h-6 w-6 shrink-0 transition-transform duration-200" />
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.2, ease: "easeOut" }}
exit={{ height: 0 }}
style={element.content.contentStyle}
className="overflow-hidden text-sm"
>
<div className="px-6 py-3">
<Text element={element.content.text} />
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</ElementWrapper>
);
}
30 changes: 30 additions & 0 deletions src/components/editor/elements/container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,36 @@ export default function Container({ element }: Props) {
},
});
break;
case "accordion":
dispatch({
type: "ADD_ELEMENT",
payload: {
containerId: id,
elementDetails: {
type: "accordion",
id: nanoid(),
name: "Accordion",
styles: {},
content: {
triggerText: "제목",
triggerStyle: {
color: "#09090B",
},
containerStyle: {
backgroundColor: "#F4F4F5",
},
text: {
type: "text",
id: nanoid(),
name: "Text",
styles: textDefaultStyles,
content: { innerText: "여기에 내용을 입력하세요." },
},
},
},
},
});
break;
}
};

Expand Down
3 changes: 3 additions & 0 deletions src/components/editor/elements/recursive.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AccordionElement from "~/components/editor/elements/accordion-element";
import BlankElement from "~/components/editor/elements/blank-element";
import Container from "~/components/editor/elements/container";
import ImageElement from "~/components/editor/elements/image-element";
Expand Down Expand Up @@ -25,6 +26,8 @@ export default function Recursive({ element }: { element: EditorElement }) {
return <NavigationElement element={element} />;
case "logoBanner":
return <LogoBannerElement element={element} />;
case "accordion":
return <AccordionElement element={element} />;
default:
return null;
}
Expand Down
21 changes: 15 additions & 6 deletions src/components/editor/placeholders.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BoxSelectIcon,
ChevronsDownUpIcon,
ImageIcon,
MapIcon,
NavigationIcon,
Expand All @@ -26,7 +27,7 @@ function Placeholder({ type, children, className }: PlaceholderProps) {
draggable
onDragStart={handleDragStart}
className={cn(
"flex h-14 w-14 cursor-grab items-center justify-center gap-1 rounded-lg bg-muted/70 p-2 active:cursor-grabbing",
"flex h-14 w-14 cursor-grab items-center justify-center gap-1 rounded-lg bg-muted/70 p-2 text-muted-foreground active:cursor-grabbing",
className,
)}
>
Expand Down Expand Up @@ -63,39 +64,47 @@ export function ContainerPlaceholder() {
export function ImagePlaceholder() {
return (
<Placeholder type="image">
<ImageIcon size={40} className="text-muted-foreground" />{" "}
<ImageIcon className="size-8" />
</Placeholder>
);
}

export function NavigationPlaceholder() {
return (
<Placeholder type="navigation">
<NavigationIcon size={40} className="text-muted-foreground" />
<NavigationIcon className="size-8" />
</Placeholder>
);
}

export function KakaoMapPlaceholder() {
return (
<Placeholder type="kakaoMap">
<MapIcon size={40} className="text-muted-foreground" />
<MapIcon className="size-8" />
</Placeholder>
);
}

export function BlankPlaceholder() {
return (
<Placeholder type="blank">
<BoxSelectIcon size={40} className="text-muted-foreground" />
<BoxSelectIcon className="size-8" />
</Placeholder>
);
}

export function LogoBannerPlaceholder() {
return (
<Placeholder type="logoBanner">
<LogoTextIcon className="h-8 text-muted-foreground" />
<LogoTextIcon className="size-8" />
</Placeholder>
);
}

export function AccordionPlaceholder() {
return (
<Placeholder type="accordion">
<ChevronsDownUpIcon className="size-8" />
</Placeholder>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";

import { useEditor } from "~/components/editor/provider";
import type { InferEditorElement } from "~/components/editor/type";
import { EditorInput } from "~/components/editor/ui/input";

export default function AccordionSetting() {
const { editor, dispatch } = useEditor();
const element = editor.state
.selectedElement as InferEditorElement<"accordion">;

const update = (
content: Partial<InferEditorElement<"accordion">["content"]>,
) => {
dispatch({
type: "UPDATE_ELEMENT",
payload: {
elementDetails: {
...element,
content: {
...element.content,
...content,
},
},
},
});
};

return (
<div className="w-full grid-cols-9 gap-1 gap-y-2 border-t p-6">
<div>
<EditorInput
id="triggerText"
defaultValue={element.content.triggerText}
onDebounceChange={(e) => {
if (!e.target.value) {
return;
}

update({
triggerText: e.target.value,
});
}}
componentPrefix={<span className="text-xs">제목</span>}
/>
</div>
<div className="col-span-9">
<EditorInput
id="triggerColor"
defaultValue={element.content.triggerStyle?.color ?? "inherit"}
onDebounceChange={(e) => {
update({
triggerStyle: {
...element.content.triggerStyle,
color: e.target.value,
},
});
}}
componentPrefix={
<div className="flex items-center gap-2">
<span className="text-xs">제목 색상</span>
<div
className="h-3.5 w-3.5 rounded ring-1 ring-border"
style={{
backgroundColor: element.content.triggerStyle?.color,
}}
/>
</div>
}
/>
</div>
<div className="col-span-9">
<EditorInput
id="containerBackgroundColor"
defaultValue={
element.content.containerStyle?.backgroundColor ?? "transparent"
}
onDebounceChange={(e) => {
update({
containerStyle: {
...element.content.containerStyle,
backgroundColor: e.target.value,
},
});
}}
componentPrefix={
<div className="flex items-center gap-2">
<span className="text-xs">배경 색상</span>
<div
className="h-3.5 w-3.5 rounded ring-1 ring-border"
style={{
backgroundColor:
element.content.containerStyle?.backgroundColor,
}}
/>
</div>
}
/>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { useEditor } from "~/components/editor/provider";
import AccordionSetting from "~/components/editor/sidebar/sidebar-element-settings-tab/accordion-setting";
import BackgroundSetting from "~/components/editor/sidebar/sidebar-element-settings-tab/background-setting";
import BorderSetting from "~/components/editor/sidebar/sidebar-element-settings-tab/border-setting";
import ImageSetting from "~/components/editor/sidebar/sidebar-element-settings-tab/image-setting";
Expand Down Expand Up @@ -71,6 +72,12 @@ export default function SidebarElementSettingsTab(props: Props) {
<LogoBannerSetting />
</>
)}

{selectedElement.type === "accordion" && (
<>
<AccordionSetting />
</>
)}
</div>
</div>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/editor/sidebar/sidebar-elements-tab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react";
import {
AccordionPlaceholder,
BlankPlaceholder,
ContainerPlaceholder,
ImagePlaceholder,
Expand Down Expand Up @@ -79,6 +80,12 @@ export default function SidebarElementsTab(props: Props) {
Component: <LogoBannerPlaceholder />,
group: "elements",
},
{
id: "accordion",
label: "Accordion",
Component: <AccordionPlaceholder />,
group: "elements",
},
];

return (
Expand Down
7 changes: 7 additions & 0 deletions src/components/editor/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ type EditorElementContentMap = {
blank: {};
empty: {};
logoBanner: {};
accordion: {
triggerText?: string;
triggerStyle?: React.CSSProperties;
containerStyle?: React.CSSProperties;
contentStyle?: React.CSSProperties;
text: InferEditorElement<"text">;
};
};

export type EditorElementType = keyof EditorElementContentMap;
Expand Down

0 comments on commit 3e65947

Please sign in to comment.