Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
488 changes: 50 additions & 438 deletions src/domain/admin/blog/BlogCreatePage.tsx

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/domain/admin/blog/BlogEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
'use client';

import { BlogSchema } from '@/api/blog/blogSchema';
import { useBlogEditForm } from '@/domain/admin/blog/hooks/useBlogEditForm';
import BlogActionButtons from '@/domain/admin/blog/section/BlogActionButtons';
import BlogBasicInfoSection from '@/domain/admin/blog/section/BlogBasicInfoSection';
import BlogProgramRecommendSection from '@/domain/admin/blog/section/BlogProgramRecommendSection';
import BlogPublishDateSection from '@/domain/admin/blog/section/BlogPublishDateSection';
import BlogRecommendSection from '@/domain/admin/blog/section/BlogRecommendSection';
import BlogTagSection from '@/domain/admin/blog/section/BlogTagSection';
import { useBlogEditForm } from '@/domain/admin/blog/hooks/useBlogEditForm';
import EditorApp from '@/domain/admin/lexical/EditorApp';
import { useRouter } from 'next/navigation';

Expand Down
108 changes: 108 additions & 0 deletions src/domain/admin/blog/hooks/useBlogCreateForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { usePostBlogMutation } from '@/api/blog/blog';
import {
BlogContent,
ProgramRecommendItem,
TagDetail,
} from '@/api/blog/blogSchema';
import { useAdminSnackbar } from '@/hooks/useAdminSnackbar';
import dayjs from '@/lib/dayjs';
import { Dayjs } from 'dayjs';
import { ChangeEvent, useState } from 'react';

interface CreateBlog {
title: string;
category: string;
thumbnail: string;
description: string;
ctaLink: string;
ctaText: string;
tagList: TagDetail[];
}

const RECOMMEND_SLOT_COUNT = 4;

const initialBlog: CreateBlog = {
title: '',
category: '',
thumbnail: '',
description: '',
ctaLink: '',
ctaText: '',
tagList: [],
};

const initialContent: BlogContent = {
programRecommend: Array(RECOMMEND_SLOT_COUNT).fill({ id: null }),
blogRecommend: new Array(RECOMMEND_SLOT_COUNT).fill(null),
};

export const useBlogCreateForm = () => {
const { snackbar: setSnackbar } = useAdminSnackbar();
const createBlogMutation = usePostBlogMutation();

const [editingValue, setEditingValue] = useState<CreateBlog>(initialBlog);
const [dateTime, setDateTime] = useState<Dayjs | null>(null);
const [content, setContent] = useState<BlogContent>(initialContent);

const onChangeField = (event: ChangeEvent<HTMLInputElement>) => {
setEditingValue((prev) => ({
...prev,
[event.target.name]: event.target.value,
}));
};

const onChangeCategory = (category: string) => {
setEditingValue((prev) => ({ ...prev, category }));
};

const onChangeThumbnail = (url: string) => {
setEditingValue((prev) => ({ ...prev, thumbnail: url }));
};

const onChangeTagList = (tagList: TagDetail[]) => {
setEditingValue((prev) => ({ ...prev, tagList }));
};

const onChangeProgramRecommend = (items: ProgramRecommendItem[]) => {
setContent((prev) => ({ ...prev, programRecommend: items }));
};

const onChangeBlogRecommend = (items: (number | null)[]) => {
setContent((prev) => ({ ...prev, blogRecommend: items }));
};

const onChangeEditor = (jsonString: string) => {
setContent((prev) => ({ ...prev, lexical: jsonString }));
};

const postBlog = async (isPublish: boolean) => {
const displayDate = isPublish && !dateTime
? dayjs().format('YYYY-MM-DDTHH:mm')
: (dateTime?.format('YYYY-MM-DDTHH:mm') ?? '');

await createBlogMutation.mutateAsync({
...editingValue,
content: JSON.stringify(content),
isDisplayed: isPublish,
tagList: editingValue.tagList.map((tag) => tag.id),
displayDate,
});

setSnackbar('블로그가 생성되었습니다.');
};

return {
editingValue,
dateTime,
content,
onChangeField,
onChangeCategory,
onChangeThumbnail,
onChangeTagList,
onChangeProgramRecommend,
onChangeBlogRecommend,
onChangeEditor,
setDateTime,
postBlog,
};
};
6 changes: 3 additions & 3 deletions src/domain/admin/blog/section/BlogActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ interface BlogActionButtonsProps {
onCancel: () => void;
onSaveTemp: () => void;
onPublish: () => void;
helperText?: string;
}

const BlogActionButtons = ({
onCancel,
onSaveTemp,
onPublish,
helperText = '*임시 저장: 블로그가 숨겨집니다.',
}: BlogActionButtonsProps) => {
return (
<div className="text-right">
Expand All @@ -34,9 +36,7 @@ const BlogActionButtons = ({
발행
</Button>
</div>
<span className="text-0.875 text-neutral-35">
*임시 저장: 블로그가 숨겨집니다.
</span>
<span className="text-0.875 text-neutral-35">{helperText}</span>
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/domain/admin/lexical/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin';
import { MaxLengthPlugin } from './plugins/MaxLengthPlugin';
import MentionsPlugin from './plugins/MentionsPlugin';
import PageBreakPlugin from './plugins/PageBreakPlugin';
import PDFPlugin from './plugins/PDFPlugin';
import PollPlugin from './plugins/PollPlugin';
import SpeechToTextPlugin from './plugins/SpeechToTextPlugin';
import TabFocusPlugin from './plugins/TabFocusPlugin';
Expand Down Expand Up @@ -195,6 +196,7 @@ export default function Editor(): JSX.Element {
<TwitterPlugin />
<YouTubePlugin />
<FigmaPlugin />
<PDFPlugin />
<ClickableLinkPlugin disabled={isEditable} />
<HorizontalRulePlugin />
<EquationsPlugin />
Expand Down
16 changes: 11 additions & 5 deletions src/domain/admin/lexical/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -975,15 +975,20 @@ i.page-break,
text-align: center;
}

.editor-shell .editor-image .image-caption-button {
display: block;
.editor-shell .editor-image .image-action-buttons {
display: flex;
gap: 8px;
position: absolute;
bottom: 20px;
left: 0;
right: 0;
width: 30%;
justify-content: center;
}

.editor-shell .editor-image .image-caption-button,
.editor-shell .editor-image .image-link-button {
display: block;
padding: 10px;
margin: 0 auto;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
Expand All @@ -993,7 +998,8 @@ i.page-break,
user-select: none;
}

.editor-shell .editor-image .image-caption-button:hover {
.editor-shell .editor-image .image-caption-button:hover,
.editor-shell .editor-image .image-link-button:hover {
background-color: rgba(60, 132, 244, 0.5);
}

Expand Down
80 changes: 72 additions & 8 deletions src/domain/admin/lexical/nodes/FigmaNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
*/

import type {
DOMConversionMap,
DOMConversionOutput,
DOMExportOutput,
EditorConfig,
ElementFormatType,
LexicalEditor,
Expand All @@ -22,6 +25,13 @@ import {
} from '@lexical/react/LexicalDecoratorBlockNode';
import * as React from 'react';

export type FigmaUrlType = 'design' | 'file' | 'proto' | 'board' | 'slides' | 'deck';

function getFigmaEmbedUrl(documentID: string, urlType: FigmaUrlType): string {
const embedType = urlType === 'file' ? 'design' : urlType;
return `https://embed.figma.com/${embedType}/${documentID}?embed-host=lexical`;
}

type FigmaComponentProps = Readonly<{
className: Readonly<{
base: string;
Expand All @@ -30,13 +40,15 @@ type FigmaComponentProps = Readonly<{
format: ElementFormatType | null;
nodeKey: NodeKey;
documentID: string;
urlType: FigmaUrlType;
}>;

function FigmaComponent({
className,
format,
nodeKey,
documentID,
urlType,
}: FigmaComponentProps) {
return (
<BlockWithAlignableContents
Expand All @@ -46,8 +58,7 @@ function FigmaComponent({
<iframe
width="560"
height="315"
src={`https://www.figma.com/embed?embed_host=lexical&url=\
https://www.figma.com/file/${documentID}`}
src={getFigmaEmbedUrl(documentID, urlType)}
allowFullScreen={true}
/>
</BlockWithAlignableContents>
Expand All @@ -57,23 +68,40 @@ function FigmaComponent({
export type SerializedFigmaNode = Spread<
{
documentID: string;
urlType?: FigmaUrlType;
},
SerializedDecoratorBlockNode
>;

function $convertFigmaElement(
domNode: HTMLElement,
): null | DOMConversionOutput {
const documentID = domNode.getAttribute('data-lexical-figma');
const urlType = (domNode.getAttribute('data-lexical-figma-type') || 'design') as FigmaUrlType;
if (documentID) {
const node = $createFigmaNode(documentID, urlType);
return {node};
}
return null;
}

export class FigmaNode extends DecoratorBlockNode {
__id: string;
__urlType: FigmaUrlType;

static getType(): string {
return 'figma';
}

static clone(node: FigmaNode): FigmaNode {
return new FigmaNode(node.__id, node.__format, node.__key);
return new FigmaNode(node.__id, node.__urlType, node.__format, node.__key);
}

static importJSON(serializedNode: SerializedFigmaNode): FigmaNode {
const node = $createFigmaNode(serializedNode.documentID);
const node = $createFigmaNode(
serializedNode.documentID,
serializedNode.urlType || 'design',
);
node.setFormat(serializedNode.format);
return node;
}
Expand All @@ -82,14 +110,46 @@ export class FigmaNode extends DecoratorBlockNode {
return {
...super.exportJSON(),
documentID: this.__id,
urlType: this.__urlType,
type: 'figma',
version: 1,
};
}

constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
constructor(
id: string,
urlType: FigmaUrlType = 'design',
format?: ElementFormatType,
key?: NodeKey,
) {
super(format, key);
this.__id = id;
this.__urlType = urlType;
}

exportDOM(): DOMExportOutput {
const element = document.createElement('iframe');
element.setAttribute('data-lexical-figma', this.__id);
element.setAttribute('data-lexical-figma-type', this.__urlType);
element.setAttribute('width', '560');
element.setAttribute('height', '315');
element.setAttribute('src', getFigmaEmbedUrl(this.__id, this.__urlType));
element.setAttribute('allowfullscreen', 'true');
return {element};
}

static importDOM(): DOMConversionMap | null {
return {
iframe: (domNode: HTMLElement) => {
if (!domNode.hasAttribute('data-lexical-figma')) {
return null;
}
return {
conversion: $convertFigmaElement,
priority: 1,
};
},
};
}

updateDOM(): false {
Expand All @@ -104,7 +164,7 @@ export class FigmaNode extends DecoratorBlockNode {
_includeInert?: boolean | undefined,
_includeDirectionless?: false | undefined,
): string {
return `https://www.figma.com/file/${this.__id}`;
return `https://www.figma.com/${this.__urlType}/${this.__id}`;
}

decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
Expand All @@ -119,13 +179,17 @@ export class FigmaNode extends DecoratorBlockNode {
format={this.__format}
nodeKey={this.getKey()}
documentID={this.__id}
urlType={this.__urlType}
/>
);
}
}

export function $createFigmaNode(documentID: string): FigmaNode {
return new FigmaNode(documentID);
export function $createFigmaNode(
documentID: string,
urlType: FigmaUrlType = 'design',
): FigmaNode {
return new FigmaNode(documentID, urlType);
}

export function $isFigmaNode(
Expand Down
Loading