/** * CMS Editor Components * Core editing interface components with enhanced functionality */ "use client"; import React, { useState, useCallback, useEffect, useMemo } from "react"; import { useEditor } from "./context"; import { BaseSection, SectionType, EditorToolbarProps, SectionListProps, PreviewPanelProps, PropertyPanelProps, SectionTemplateProps, SectionEditorProps, } from "./types"; import { EnhancedSectionList } from "./drag-drop"; import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd"; import { ChevronDownIcon, ChevronUpIcon, EyeIcon, EyeSlashIcon, ExclamationTriangleIcon, } from "@heroicons/react/24/outline"; import { CogIcon, DocumentDuplicateIcon, TrashIcon, DevicePhoneMobileIcon, DeviceTabletIcon, ComputerDesktopIcon, PlusIcon, PaintBrushIcon, AdjustmentsHorizontalIcon, } from "@heroicons/react/24/outline"; import { useCMSEditor, useEditorSelectors } from "./store"; import { TextField, NumberField, SelectField, ColorField, ToggleField, RangeField, OptimisticForm, ValidationIndicator, } from "./form-components"; import { heroSectionContentSchema, sectionSchema } from "./validation"; // Icon mapping for consistent UI const iconMap: Record = { undo: "β†Ά", redo: "β†·", save: "πŸ’Ύ", desktop: "πŸ–₯️", tablet: "πŸ“±", mobile: "πŸ“±", preview: "πŸ‘οΈ", settings: "βš™οΈ", add: "βž•", hero: "🎯", features: "⭐", cta: "πŸ””", pricing: "πŸ’°", testimonials: "πŸ’¬", contact: "πŸ“ž", gallery: "πŸ–ΌοΈ", text: "πŸ“", custom: "πŸ”§", }; // Enhanced Editor Toolbar with more controls export function EditorToolbar({ className = "" }: { className?: string } = {}) { const { state, config, updateConfig, undo, redo, canUndo, canRedo } = useEditor(); const handlePreviewModeChange = useCallback( (mode: "desktop" | "tablet" | "mobile") => { updateConfig({ previewMode: mode }); }, [updateConfig] ); const handleSave = useCallback(() => { // Implement save functionality console.log("Saving sections...", state.sections); }, [state.sections]); return (
{/* History Controls */}
{/* Save Button */} {/* Status Indicator */}
{state.isLoading && (
μ €μž₯ 쀑...
)} {state.isDirty && !state.isLoading && ( β€’ μ €μž₯λ˜μ§€ μ•Šμ€ 변경사항 )} {!state.isDirty && !state.isLoading && ( β€’ λͺ¨λ“  변경사항 μ €μž₯됨 )}
{/* Preview Mode Controls */}
미리보기:
{(["desktop", "tablet", "mobile"] as const).map((mode) => ( ))}
); } // Updated Section List using Enhanced Drag-and-Drop export function SectionList({ className = "" }: SectionListProps = {}) { return ; } // Enhanced Preview Panel with responsive preview export function PreviewPanel({ className = "" }: { className?: string } = {}) { const { state, config, selectSection } = useEditor(); const getPreviewDimensions = useCallback(() => { switch (config.previewMode) { case "mobile": return { width: "375px", height: "667px" }; case "tablet": return { width: "768px", height: "1024px" }; case "desktop": default: return { width: "100%", height: "100%" }; } }, [config.previewMode]); const dimensions = getPreviewDimensions(); return (
{/* Preview Header */}
미리보기 ({config.previewMode})
{state.sections.length}개 μ„Ήμ…˜
{/* Preview Content */}
{state.sections.length === 0 ? (
πŸ“„

μ„Ήμ…˜μ΄ μ—†μŠ΅λ‹ˆλ‹€

μ™Όμͺ½μ—μ„œ μ„Ήμ…˜μ„ μΆ”κ°€ν•˜μ—¬ μ‹œμž‘ν•˜μ„Έμš”

) : (
{state.sections .sort((a, b) => a.order - b.order) .filter((section) => section.isActive) .map((section) => (
selectSection(section.id)} className={` p-8 cursor-pointer transition-all duration-200 min-h-32 ${ state.selectedSection === section.id ? "bg-blue-50 border-2 border-blue-300" : "hover:bg-gray-50" } `} style={{ backgroundColor: section.styles.background.color || "#ffffff", }} >
{iconMap[section.type] || iconMap.custom}

{section.name}

{section.type} μ„Ήμ…˜

{state.selectedSection === section.id && (
선택됨
)}
{/* Section Content Preview */}
                          {JSON.stringify(section.content, null, 2)}
                        
))}
)}
); } // Enhanced Property Panel with better form controls export function PropertyPanel({ className = "" }: PropertyPanelProps = {}) { const { selectedSectionData } = useEditorSelectors(); const { updateSection } = useCMSEditor(); const [activeTab, setActiveTab] = useState<"content" | "style" | "settings">( "content" ); if (!selectedSectionData) { return (

속성 νŒ¨λ„

μ„Ήμ…˜μ„ μ„ νƒν•˜μ—¬ 속성을 νŽΈμ§‘ν•˜μ„Έμš”

); } const handleSectionUpdate = async (data: Partial) => { // This will be handled by the optimistic form return Promise.resolve(); }; const tabs = [ { id: "content", label: "μ½˜ν…μΈ ", icon: DocumentDuplicateIcon }, { id: "style", label: "μŠ€νƒ€μΌ", icon: PaintBrushIcon }, { id: "settings", label: "μ„€μ •", icon: CogIcon }, ] as const; return (
{/* Header */}

속성 νŽΈμ§‘

{selectedSectionData.name}
{selectedSectionData.type} μ„Ήμ…˜
{/* Tabs */}
{tabs.map((tab) => { const Icon = tab.icon; return ( ); })}
{/* Content */}
{activeTab === "content" && ( )} {activeTab === "style" && ( )} {activeTab === "settings" && ( )}
); } // Content Tab Component const ContentTab: React.FC<{ section: BaseSection; onUpdate: (data: Partial) => Promise; }> = ({ section, onUpdate }) => { const contentSchema = useMemo(() => { switch (section.type) { case "hero": return heroSectionContentSchema; default: return heroSectionContentSchema; // Fallback } }, [section.type]); const defaultValues = useMemo( () => ({ content: section.content, }), [section.content] ); return ( {({ control }) => ( <> {section.type === "hero" && (
)} {/* Other section types */} {section.type !== "hero" && (

{section.type} μ„Ήμ…˜μ˜ μ½˜ν…μΈ  νŽΈμ§‘κΈ°κ°€ κ³§ μ œκ³΅λ©λ‹ˆλ‹€

)} )}
); }; // Style Tab Component const StyleTab: React.FC<{ section: BaseSection; onUpdate: (data: Partial) => Promise; }> = ({ section, onUpdate }) => { const defaultValues = useMemo( () => ({ styles: section.styles, }), [section.styles] ); return ( {({ control }) => ( <> {/* Background */}

λ°°κ²½

{/* Spacing */}

μ—¬λ°±

{/* Typography */}

νƒ€μ΄ν¬κ·Έλž˜ν”Ό

`${value}px`} control={control} />
{/* Borders */}

ν…Œλ‘λ¦¬

`${value}px`} control={control} /> `${value}px`} control={control} />
)}
); }; // Settings Tab Component const SettingsTab: React.FC<{ section: BaseSection; onUpdate: (data: Partial) => Promise; }> = ({ section, onUpdate }) => { const defaultValues = useMemo( () => ({ settings: section.settings, metadata: section.metadata, }), [section.settings, section.metadata] ); return ( {({ control }) => ( <> {/* Visibility Settings */}

ν‘œμ‹œ μ„€μ •

{/* Animation Settings */}

μ• λ‹ˆλ©”μ΄μ…˜

`${value}ms`} control={control} /> `${value}ms`} control={control} />
{/* SEO Settings */}

SEO μ„€μ •

{/* Accessibility */}

μ ‘κ·Όμ„±

{/* Metadata */}

메타데이터

생성일: {new Date(section.metadata.createdAt).toLocaleDateString()}
μˆ˜μ •μΌ: {new Date(section.metadata.updatedAt).toLocaleDateString()}
버전: v{section.metadata.version}
)}
); }; // Section Template Selector Component export function SectionTemplateSelector({ className = "", onSelectTemplate, }: SectionTemplateProps) { const { addSection } = useEditor(); const sectionTemplates = [ { type: "hero" as SectionType, name: "νžˆμ–΄λ‘œ μ„Ήμ…˜", description: "메인 λ°°λ„ˆ μ˜μ—­", icon: iconMap.hero, }, { type: "features" as SectionType, name: "κΈ°λŠ₯ μ„Ήμ…˜", description: "μ£Όμš” κΈ°λŠ₯ μ†Œκ°œ", icon: iconMap.features, }, { type: "cta" as SectionType, name: "CTA μ„Ήμ…˜", description: "행동 μœ λ„ λ²„νŠΌ", icon: iconMap.cta, }, { type: "pricing" as SectionType, name: "가격 μ„Ήμ…˜", description: "μš”κΈˆμ œ ν‘œμ‹œ", icon: iconMap.pricing, }, { type: "testimonials" as SectionType, name: "ν›„κΈ° μ„Ήμ…˜", description: "고객 리뷰", icon: iconMap.testimonials, }, { type: "contact" as SectionType, name: "μ—°λ½μ²˜ μ„Ήμ…˜", description: "문의 정보", icon: iconMap.contact, }, { type: "gallery" as SectionType, name: "가러리 μ„Ήμ…˜", description: "이미지 가러리", icon: iconMap.gallery, }, { type: "text" as SectionType, name: "ν…μŠ€νŠΈ μ„Ήμ…˜", description: "일반 ν…μŠ€νŠΈ", icon: iconMap.text, }, { type: "custom" as SectionType, name: "μ»€μŠ€ν…€ μ„Ήμ…˜", description: "μ‚¬μš©μž μ •μ˜", icon: iconMap.custom, }, ]; const handleTemplateSelect = useCallback( (sectionType: SectionType) => { const template = sectionTemplates.find((t) => t.type === sectionType); if (template) { const newSection = { id: `section-${Date.now()}`, type: sectionType, name: template.name, order: 0, isActive: true, content: {}, styles: { background: { type: "color" as const, color: "#ffffff" }, spacing: { padding: { top: 32, right: 16, bottom: 32, left: 16 }, margin: { top: 0, right: 0, bottom: 0, left: 0 }, }, typography: { fontFamily: "Inter", fontSize: 16, fontWeight: 400, lineHeight: 1.5, letterSpacing: 0, textAlign: "left" as const, textDecoration: "none" as const, textTransform: "none" as const, }, colors: { primary: "#3b82f6", secondary: "#64748b", accent: "#f59e0b", text: "#1f2937", textSecondary: "#6b7280", background: "#ffffff", border: "#e5e7eb", }, borders: { width: 0, style: "none" as const, color: "#e5e7eb", radius: 0, }, effects: { shadow: "none", blur: 0, opacity: 1, transform: "none", transition: "all 0.2s ease", animation: "none", }, responsive: { mobile: {}, tablet: {}, desktop: {}, }, }, settings: { isVisible: true, isLocked: false, animation: { enabled: false, type: "fade" as const, duration: 300, delay: 0, easing: "ease-in-out", trigger: "scroll" as const, }, seo: {}, accessibility: { focusable: true, }, }, metadata: { createdAt: new Date(), updatedAt: new Date(), createdBy: "user", updatedBy: "user", version: 1, }, }; addSection(newSection); onSelectTemplate?.(newSection); } }, [addSection, onSelectTemplate] ); return (

μ„Ήμ…˜ ν…œν”Œλ¦Ώ

{sectionTemplates.map((template) => ( ))}
); } // Main Section Editor Component export function SectionEditor({ className = "", showTemplateSelector = true, }: SectionEditorProps = {}) { const { state } = useEditor(); return (
{state.selectedSection ? ( ) : ( showTemplateSelector && )}
); } // Publishing Status Bar Component export function PublishingStatusBar() { const { isSaving, isPublishing, isPublished, lastSaved, publishedAt, saveError, publishError, isDirty, draftState, saveDraft, publishSections, unpublishSections, getPreviewUrl, } = useCMSEditor(); const handleSaveDraft = async () => { try { await saveDraft(); } catch (error) { console.error("Draft save failed:", error); } }; const handlePublish = async () => { try { await publishSections(); } catch (error) { console.error("Publish failed:", error); } }; const handleUnpublish = async () => { try { await unpublishSections(); } catch (error) { console.error("Unpublish failed:", error); } }; const handlePreview = async () => { try { const url = await getPreviewUrl(); window.open(url, "_blank"); } catch (error) { console.error("Preview failed:", error); } }; return (
{/* Left: Status Info */}
{/* Save Status */}
{isSaving ? ( <>
μ €μž₯ 쀑... ) : isDirty ? ( <>
변경사항 있음 ) : ( <>
{lastSaved ? `μ €μž₯됨 (${formatRelativeTime(lastSaved)})` : "μ €μž₯됨"} )}
{/* Publish Status */}
{isPublishing ? ( <>
λ°œν–‰ 쀑... ) : isPublished ? ( <>
{publishedAt ? `λ°œν–‰λ¨ (${formatRelativeTime(publishedAt)})` : "λ°œν–‰λ¨"} ) : ( <>
λ―Έλ°œν–‰ )}
{/* Draft Info */} {draftState && (
μ΄ˆμ•ˆ v{draftState.version}
)} {/* Error Messages */} {saveError && (
μ €μž₯ 였λ₯˜: {saveError}
)} {publishError && (
λ°œν–‰ 였λ₯˜: {publishError}
)}
{/* Right: Action Buttons */}
{isPublished ? ( ) : ( )}
); } // Network Status Indicator export function NetworkStatusIndicator() { const { isOnline, lastSync, syncError } = useCMSEditor(); return (
{isOnline ? lastSync ? `동기화됨 ${formatRelativeTime(lastSync)}` : "온라인" : "μ˜€ν”„λΌμΈ"} {syncError && ( )}
); } // Auto-save Status Indicator export function AutoSaveIndicator() { const { isSaving, isDirty, lastSaved } = useCMSEditor(); if (isSaving) { return (
μžλ™ μ €μž₯ 쀑...
); } if (isDirty) { return (
μ €μž₯ ν•„μš”
); } return (
{lastSaved ? `μ €μž₯됨 ${formatRelativeTime(lastSaved)}` : "μ €μž₯됨"}
); } // Publishing Actions Menu export function PublishingActionsMenu() { const { isPublished, isPublishing, isDirty, publishSections, unpublishSections, saveDraft, getPreviewUrl, } = useCMSEditor(); const [isOpen, setIsOpen] = React.useState(false); const menuRef = React.useRef(null); React.useEffect(() => { function handleClickOutside(event: MouseEvent) { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setIsOpen(false); } } document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); const handleAction = async (action: string) => { setIsOpen(false); try { switch (action) { case "save-draft": await saveDraft(); break; case "publish": await publishSections(); break; case "unpublish": await unpublishSections(); break; case "preview": const url = await getPreviewUrl(); window.open(url, "_blank"); break; } } catch (error) { console.error(`Action ${action} failed:`, error); } }; return (
{isOpen && (
{isPublished ? ( ) : ( )}
)}
); } // Utility function for relative time formatting function formatRelativeTime(date: Date): string { const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (diffInSeconds < 60) { return "방금 μ „"; } else if (diffInSeconds < 3600) { const minutes = Math.floor(diffInSeconds / 60); return `${minutes}λΆ„ μ „`; } else if (diffInSeconds < 86400) { const hours = Math.floor(diffInSeconds / 3600); return `${hours}μ‹œκ°„ μ „`; } else { const days = Math.floor(diffInSeconds / 86400); return `${days}일 μ „`; } }