diff --git a/app/about/page.tsx b/app/about/page.tsx index ffe269ac..2535f3a1 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,12 +1,12 @@ 'use client'; -import { Button } from '@components/ui/button'; +import { DynamicAboutRenderer } from '@components/about/dynamic-about-renderer'; +import { AdminButton } from '@components/admin/admin-button'; +import { LanguageSwitcher } from '@components/ui/language-switcher'; import { PageLoader } from '@components/ui/page-loader'; import { useDynamicTranslations } from '@lib/hooks/use-dynamic-translations'; -import { useTheme } from '@lib/hooks/use-theme'; import { createClient } from '@lib/supabase/client'; -import { cn } from '@lib/utils'; -import { motion } from 'framer-motion'; +import type { AboutTranslationData } from '@lib/types/about-page-components'; import { useEffect, useState } from 'react'; @@ -15,7 +15,6 @@ import { useRouter } from 'next/navigation'; export default function AboutPage() { const router = useRouter(); - const { isDark } = useTheme(); const staticT = useTranslations('pages.about'); const { t: dynamicT, isLoading } = useDynamicTranslations({ sections: ['pages.about'], @@ -33,54 +32,11 @@ export default function AboutPage() { setMounted(true); }, []); - // get colors based on theme - const getColors = () => { - if (isDark) { - return { - titleGradient: 'from-stone-300 to-stone-500', - textColor: 'text-gray-300', - headingColor: 'text-gray-100', - paragraphColor: 'text-gray-400', - cardBg: 'bg-stone-700', - cardBorder: 'border-stone-600', - cardShadow: 'shadow-[0_4px_20px_rgba(0,0,0,0.3)]', - cardHeadingColor: 'text-stone-300', - cardTextColor: 'text-gray-400', - buttonClass: - 'bg-stone-600 hover:bg-stone-500 text-gray-100 cursor-pointer hover:scale-105', - }; - } else { - return { - titleGradient: 'from-stone-700 to-stone-900', - textColor: 'text-stone-700', - headingColor: 'text-stone-800', - paragraphColor: 'text-stone-600', - cardBg: 'bg-stone-100', - cardBorder: 'border-stone-200', - cardShadow: 'shadow-[0_4px_20px_rgba(0,0,0,0.1)]', - cardHeadingColor: 'text-stone-700', - cardTextColor: 'text-stone-600', - buttonClass: - 'bg-stone-800 hover:bg-stone-700 text-gray-100 cursor-pointer hover:scale-105', - }; - } + // Homepage-style colors using Tailwind classes + const colors = { + bgClass: 'bg-stone-100 dark:bg-stone-900', }; - const colors = mounted - ? getColors() - : { - titleGradient: '', - textColor: '', - headingColor: '', - paragraphColor: '', - cardBg: '', - cardBorder: '', - cardShadow: '', - cardHeadingColor: '', - cardTextColor: '', - buttonClass: '', - }; - // handle "start exploring" button click const handleExploreClick = async () => { try { @@ -109,168 +65,47 @@ export default function AboutPage() { return ; } - // Extract value cards data from translations (using static raw method for array data) - const valueCards = staticT.raw('values.items') as Array<{ - title: string; - description: string; - }>; + // Create translation data object for the dynamic renderer + const translationData: AboutTranslationData = { + // Try to get dynamic sections first + sections: dynamicT('sections', 'pages.about') || undefined, + + // Fallback to legacy format for backward compatibility + title: t('title'), + subtitle: t('subtitle'), + mission: { + description: t('mission.description'), + }, + values: { + items: staticT.raw('values.items') as Array<{ + title: string; + description: string; + }>, + }, + buttonText: t('buttonText'), + copyright: { + prefix: t('copyright.prefix'), + linkText: t('copyright.linkText'), + suffix: t('copyright.suffix'), + }, + }; return ( -
-
- {/* title */} - - - {t('title')} - - - {t('subtitle')} - - - - {/* mission */} - -

- {t('mission.title')} -

-

- {t('mission.description')} -

-
- - {/* values */} - -

- {t('values.title')} -

-
- {valueCards.map((value, index) => ( - -

- {value.title} -

-

- {value.description} -

-
- ))} -
-
- - {/* join us */} - - - - - {/* bottom info */} - -

- {t('copyright.prefix', { year: new Date().getFullYear() })} - - {t('copyright.linkText')} - - {t('copyright.suffix')} -

-
+
+ {/* Top-right toolbar: Admin button (left) + Language switcher (right) */} +
+ +
-
+ +
+ +
+ ); } diff --git a/app/admin/content/page.tsx b/app/admin/content/page.tsx index 3a3340fe..825346bc 100644 --- a/app/admin/content/page.tsx +++ b/app/admin/content/page.tsx @@ -4,16 +4,29 @@ import { AboutEditor } from '@components/admin/content/about-editor'; import { AboutPreview } from '@components/admin/content/about-preview'; import { ContentTabs } from '@components/admin/content/content-tabs'; import { EditorSkeleton } from '@components/admin/content/editor-skeleton'; -import { HomeEditor } from '@components/admin/content/home-editor'; -import { HomePreview } from '@components/admin/content/home-preview'; +import { HomePreviewDynamic } from '@components/admin/content/home-preview-dynamic'; import { PreviewToolbar } from '@components/admin/content/preview-toolbar'; import { ResizableSplitPane } from '@components/ui/resizable-split-pane'; import type { SupportedLocale } from '@lib/config/language-config'; import { getCurrentLocaleFromCookie } from '@lib/config/language-config'; import { clearTranslationCache } from '@lib/hooks/use-dynamic-translations'; -import { useTheme } from '@lib/hooks/use-theme'; import { TranslationService } from '@lib/services/admin/content/translation-service'; +import { useAboutEditorStore } from '@lib/stores/about-editor-store'; +import { useHomeEditorStore } from '@lib/stores/home-editor-store'; +import type { + AboutTranslationData, + PageContent, +} from '@lib/types/about-page-components'; +import { + isDynamicFormat, + migrateAboutTranslationData, +} from '@lib/types/about-page-components'; import { cn } from '@lib/utils'; +import type { HomeTranslationData } from '@lib/utils/data-migration'; +import { + isHomeDynamicFormat, + migrateHomeTranslationData, +} from '@lib/utils/data-migration'; import { Eye } from 'lucide-react'; import { toast } from 'sonner'; @@ -22,21 +35,6 @@ import React, { useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useRouter, useSearchParams } from 'next/navigation'; -interface ValueCard { - id: string; - title: string; - description: string; -} - -interface AboutPageConfig { - title: string; - subtitle: string; - mission: string; - valueCards: ValueCard[]; - buttonText: string; - copyrightText: string; -} - interface FeatureCard { title: string; description: string; @@ -56,11 +54,16 @@ interface HomePageConfig { } export default function ContentManagementPage() { - const { isDark } = useTheme(); const searchParams = useSearchParams(); const router = useRouter(); const t = useTranslations('pages.admin.content.page'); + // Editor stores for resetting editor state + const { setPageContent: setAboutPageContent, reset: resetAboutEditor } = + useAboutEditorStore(); + const { setPageContent: setHomePageContent, reset: resetHomeEditor } = + useHomeEditorStore(); + const [activeTab, setActiveTab] = useState<'about' | 'home'>('about'); const [showPreview, setShowPreview] = useState(true); const [previewDevice, setPreviewDevice] = useState< @@ -178,9 +181,61 @@ export default function ContentManagementPage() { const handleReset = () => { if (activeTab === 'about' && originalAboutTranslations) { + // Reset page-level state setAboutTranslations({ ...originalAboutTranslations }); + + // Reset editor state to match the original data + const currentTranslation = originalAboutTranslations[currentLocale] || {}; + let translation = currentTranslation; + + // Ensure it's in dynamic format + if (!isDynamicFormat(translation)) { + translation = migrateAboutTranslationData(translation); + } + + if (translation.sections) { + const content: PageContent = { + sections: translation.sections, + metadata: translation.metadata || { + version: '1.0.0', + lastModified: new Date().toISOString(), + author: 'admin', + }, + }; + + // Reset the editor state completely - this clears undo/redo stacks + resetAboutEditor(); + // Set the page content to original state + setAboutPageContent(content); + } } else if (activeTab === 'home' && originalHomeTranslations) { + // Reset page-level state for home tab setHomeTranslations({ ...originalHomeTranslations }); + + // Reset home editor state to match the original data + const currentTranslation = originalHomeTranslations[currentLocale] || {}; + let translation = currentTranslation; + + // Ensure it's in dynamic format + if (!isHomeDynamicFormat(translation)) { + translation = migrateHomeTranslationData(translation); + } + + if (translation.sections) { + const content: PageContent = { + sections: translation.sections, + metadata: translation.metadata || { + version: '1.0.0', + lastModified: new Date().toISOString(), + author: 'admin', + }, + }; + + // Reset the home editor state completely - this clears undo/redo stacks + resetHomeEditor(); + // Set the page content to original state + setHomePageContent(content); + } } }; @@ -204,57 +259,6 @@ export default function ContentManagementPage() { setShowFullscreenPreview(false); }; - interface AboutTranslationData { - title?: string; - subtitle?: string; - mission?: { description?: string }; - values?: { items?: Array<{ title: string; description: string }> }; - buttonText?: string; - copyright?: { - prefix?: string; - linkText?: string; - suffix?: string; - }; - } - - const transformToAboutPreviewConfig = ( - translations: Record | null, - locale: SupportedLocale - ): AboutPageConfig | null => { - const t = translations?.[locale]; - if (!t) return null; - - return { - title: t.title || '', - subtitle: t.subtitle || '', - mission: t.mission?.description || '', - valueCards: (t.values?.items || []).map( - (item: { title: string; description: string }, index: number) => ({ - id: `value-${index}`, - title: item.title, - description: item.description, - }) - ), - buttonText: t.buttonText || '', - copyrightText: t.copyright - ? `${(t.copyright.prefix || '').replace('{year}', new Date().getFullYear().toString())}${t.copyright.linkText || ''}${t.copyright.suffix || ''}` - : '', - }; - }; - - interface HomeTranslationData { - title?: string; - subtitle?: string; - getStarted?: string; - learnMore?: string; - features?: FeatureCard[]; - copyright?: { - prefix?: string; - linkText?: string; - suffix?: string; - }; - } - const transformToHomePreviewConfig = ( translations: Record | null, locale: SupportedLocale @@ -278,10 +282,6 @@ export default function ContentManagementPage() { }; }; - const aboutPreviewConfig = transformToAboutPreviewConfig( - aboutTranslations, - currentLocale - ); const homePreviewConfig = transformToHomePreviewConfig( homeTranslations, currentLocale @@ -305,12 +305,33 @@ export default function ContentManagementPage() { } if (activeTab === 'home') { - return homeTranslations ? ( - [ + locale, + isHomeDynamicFormat(translation) + ? translation + : migrateHomeTranslationData(translation), + ]) + ) as Record) + : null; + + return convertedHomeTranslations ? ( + { + // Convert back to HomeTranslationData format + const convertedBack = Object.fromEntries( + Object.entries(newTranslations).map(([locale, translation]) => [ + locale, + translation as HomeTranslationData, + ]) + ) as Record; + handleHomeTranslationsChange(convertedBack); + }} onLocaleChange={setCurrentLocale} /> ) : ( @@ -322,9 +343,11 @@ export default function ContentManagementPage() { const renderPreview = () => { if (activeTab === 'about') { - return aboutPreviewConfig ? ( + // Use the most up-to-date translation data that includes real-time edits + const currentTranslation = aboutTranslations?.[currentLocale]; + return currentTranslation ? ( ) : ( @@ -332,8 +355,13 @@ export default function ContentManagementPage() { ); } if (activeTab === 'home') { - return homePreviewConfig ? ( - + // Use the most up-to-date home translation data that includes real-time edits + const currentHomeTranslation = homeTranslations?.[currentLocale]; + return currentHomeTranslation ? ( + ) : (
{t('loadingPreview')}
); @@ -344,7 +372,7 @@ export default function ContentManagementPage() { return (
@@ -375,9 +403,8 @@ export default function ContentManagementPage() { onClick={() => setShowPreview(true)} className={cn( 'flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium shadow-sm transition-colors', - isDark - ? 'border border-stone-700 bg-stone-800 text-stone-300 hover:bg-stone-700' - : 'border border-stone-200 bg-white text-stone-600 hover:bg-stone-100' + 'border border-stone-200 bg-white text-stone-600 hover:bg-stone-100', + 'dark:border-stone-700 dark:bg-stone-800 dark:text-stone-300 dark:hover:bg-stone-700' )} > @@ -407,7 +434,9 @@ export default function ContentManagementPage() { 'bg-white dark:bg-stone-900' )} > -
{renderEditor()}
+
+ {renderEditor()} +
@@ -449,12 +476,8 @@ export default function ContentManagementPage() { className={cn( 'rounded-lg px-6 py-2 text-sm font-medium shadow-sm transition-colors', hasChanges && !isSaving - ? isDark - ? 'bg-stone-100 text-stone-900 hover:bg-white' - : 'bg-stone-900 text-white hover:bg-stone-800' - : isDark - ? 'cursor-not-allowed bg-stone-700 text-stone-400' - : 'cursor-not-allowed bg-stone-300 text-stone-500' + ? 'bg-stone-900 text-white hover:bg-stone-800 dark:bg-stone-100 dark:text-stone-900 dark:hover:bg-white' + : 'cursor-not-allowed bg-stone-300 text-stone-500 dark:bg-stone-700 dark:text-stone-400' )} > {isSaving @@ -476,21 +499,19 @@ export default function ContentManagementPage() { onPreviewToggle={() => setShowPreview(!showPreview)} onFullscreenPreview={handleFullscreenPreview} /> -
+
{renderPreview()}
} /> ) : ( -
-
{renderEditor()}
+
+
{renderEditor()}
@@ -514,9 +535,7 @@ export default function ContentManagementPage() { 'rounded-lg px-4 py-2 text-sm font-medium transition-colors', !hasChanges || isSaving ? 'cursor-not-allowed text-stone-500' - : isDark - ? 'text-stone-300 hover:bg-stone-800' - : 'text-stone-600 hover:bg-stone-100' + : 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-800' )} > {t('saveActions.reset')} @@ -527,12 +546,8 @@ export default function ContentManagementPage() { className={cn( 'rounded-lg px-6 py-2 text-sm font-medium shadow-sm transition-colors', !hasChanges || isSaving - ? isDark - ? 'cursor-not-allowed bg-stone-700 text-stone-400' - : 'cursor-not-allowed bg-stone-300 text-stone-500' - : isDark - ? 'bg-stone-100 text-stone-900 hover:bg-white' - : 'bg-stone-900 text-white hover:bg-stone-800' + ? 'cursor-not-allowed bg-stone-300 text-stone-500 dark:bg-stone-700 dark:text-stone-400' + : 'bg-stone-900 text-white hover:bg-stone-800 dark:bg-stone-100 dark:text-stone-900 dark:hover:bg-white' )} > {isSaving @@ -570,7 +585,7 @@ export default function ContentManagementPage() { > {t('fullscreenPreview')} - {activeTab === 'about' - ? aboutPreviewConfig?.title + ? aboutTranslations?.[currentLocale]?.title || 'About' : homePreviewConfig?.title}
@@ -578,9 +593,8 @@ export default function ContentManagementPage() { onClick={handleCloseFullscreenPreview} className={cn( 'rounded-lg px-4 py-2 text-sm font-medium transition-colors', - isDark - ? 'bg-stone-700 text-stone-300 hover:bg-stone-600' - : 'bg-stone-100 text-stone-700 hover:bg-stone-200' + 'bg-stone-100 text-stone-700 hover:bg-stone-200', + 'dark:bg-stone-700 dark:text-stone-300 dark:hover:bg-stone-600' )} > {t('closePreview')} diff --git a/components/about/dynamic-about-renderer.tsx b/components/about/dynamic-about-renderer.tsx new file mode 100644 index 00000000..5c6beaad --- /dev/null +++ b/components/about/dynamic-about-renderer.tsx @@ -0,0 +1,141 @@ +'use client'; + +import ComponentRenderer from '@components/admin/content/component-renderer'; +import type { + AboutTranslationData, + PageContent, +} from '@lib/types/about-page-components'; +import { + isDynamicFormat, + migrateAboutTranslationData, +} from '@lib/types/about-page-components'; +import { cn } from '@lib/utils'; +import { AnimatePresence, motion } from 'framer-motion'; + +import React from 'react'; + +interface DynamicAboutRendererProps { + /** + * Translation data for the about page (legacy or dynamic format) + */ + translationData: AboutTranslationData; + /** + * Handle button click action + */ + onButtonClick?: (url?: string) => void; +} + +/** + * Dynamic About Page Renderer + * + * Renders about page content using dynamic component structure + * Falls back to legacy structure if dynamic data is not available + */ +export function DynamicAboutRenderer({ + translationData, +}: DynamicAboutRendererProps) { + // Ensure we have dynamic format data + const dynamicData = React.useMemo(() => { + if (!isDynamicFormat(translationData)) { + return migrateAboutTranslationData(translationData); + } + return translationData; + }, [translationData]); + + // Create page content from translation data + const pageContent: PageContent = React.useMemo(() => { + return { + sections: dynamicData.sections || [], + metadata: dynamicData.metadata || { + version: '1.0.0', + lastModified: new Date().toISOString(), + author: 'frontend', + }, + }; + }, [dynamicData]); + + /** + * Render page sections with homepage-style animations and layout + */ + const renderSections = () => { + if (!pageContent.sections || pageContent.sections.length === 0) { + return ( + +

No content to display

+
+ ); + } + + return ( + + {pageContent.sections.map((section, sectionIndex) => ( + +
+ {section.columns.map((column, columnIndex) => ( +
+ {column.map((component, componentIndex) => ( + + + + ))} +
+ ))} +
+
+ ))} +
+ ); + }; + + return ( + + + {renderSections()} + + + ); +} diff --git a/components/admin/content/about-editor.tsx b/components/admin/content/about-editor.tsx index e86b5d45..30a97042 100644 --- a/components/admin/content/about-editor.tsx +++ b/components/admin/content/about-editor.tsx @@ -1,5 +1,7 @@ 'use client'; +import { Badge } from '@components/ui/badge'; +import { Button } from '@components/ui/button'; import { Select, SelectContent, @@ -7,46 +9,42 @@ import { SelectTrigger, SelectValue, } from '@components/ui/select'; +import { Separator } from '@components/ui/separator'; import type { SupportedLocale } from '@lib/config/language-config'; import { getLanguageInfo } from '@lib/config/language-config'; -import { useTheme } from '@lib/hooks/use-theme'; +import { useAboutEditorStore } from '@lib/stores/about-editor-store'; +import { + AboutTranslationData, + ComponentInstance, + PageContent, + isDynamicFormat, + migrateAboutTranslationData, +} from '@lib/types/about-page-components'; import { cn } from '@lib/utils'; -import { Plus, Trash2 } from 'lucide-react'; +import { + useDebouncedCallback, + useThrottledCallback, +} from '@lib/utils/performance'; +import { GripVertical, Plus, Redo2, Trash2, Undo2 } from 'lucide-react'; -import React from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslations } from 'next-intl'; -interface ValueItem { - title: string; - description: string; -} - -interface Copyright { - prefix?: string; - linkText?: string; - suffix?: string; -} - -interface AboutTranslation { - title?: string; - subtitle?: string; - mission?: { - description?: string; - }; - values?: { - items?: ValueItem[]; - }; - buttonText?: string; - copyright?: Copyright; -} +import ComponentPalette from './component-palette'; +import ComponentRenderer from './component-renderer'; +import { ContextMenu } from './context-menu'; +import { Droppable, Sortable, SortableContainer } from './dnd-components'; +import { DndContextWrapper } from './dnd-context'; +import { DragPreviewRenderer } from './drag-preview-renderer'; +import { SectionDragPreview } from './section-drag-preview'; interface AboutEditorProps { - translations: Record; + translations: Record; currentLocale: SupportedLocale; supportedLocales: SupportedLocale[]; onTranslationsChange: ( - newTranslations: Record + newTranslations: Record ) => void; onLocaleChange: (newLocale: SupportedLocale) => void; } @@ -58,359 +56,671 @@ export function AboutEditor({ onTranslationsChange, onLocaleChange, }: AboutEditorProps) { - const { isDark } = useTheme(); const t = useTranslations('pages.admin.content.editor'); - const currentTranslation = translations[currentLocale] || {}; - - const handleFieldChange = (field: string, value: string | ValueItem[]) => { - const newTranslations = JSON.parse(JSON.stringify(translations)) as Record< - SupportedLocale, - AboutTranslation - >; - const fieldParts = field.split('.'); - let current = newTranslations[currentLocale] as any; - - for (let i = 0; i < fieldParts.length - 1; i++) { - if (!current[fieldParts[i]]) { - current[fieldParts[i]] = {}; + + // Context menu state + const [contextMenu, setContextMenu] = React.useState<{ + x: number; + y: number; + componentId: string; + } | null>(null); + + // Inline editing state + const [editingComponent, setEditingComponent] = React.useState<{ + componentId: string; + content: string; + } | null>(null); + + // Zustand store + const { + pageContent, + selectedComponentId, + undoStack, + redoStack, + isDirty, + setPageContent, + setSelectedComponent, + updateComponentProps, + deleteComponent, + deleteSection, + handleDragEnd, + addSection, + undo, + redo, + setCurrentLanguage, + } = useAboutEditorStore(); + + // Get current translation and ensure it's in dynamic format + const currentTranslation = useMemo(() => { + let translation = translations[currentLocale] || {}; + + // If it's not already in dynamic format, migrate it + if (!isDynamicFormat(translation)) { + translation = migrateAboutTranslationData(translation); + } + + return translation; + }, [translations, currentLocale]); + + // Get selected component from page content (currently unused but kept for future use) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const selectedComponent = useMemo(() => { + if (!pageContent || !selectedComponentId) return null; + + for (const section of pageContent.sections) { + for (const column of section.columns) { + const component = column.find(comp => comp.id === selectedComponentId); + if (component) return component; + } + } + return null; + }, [pageContent, selectedComponentId]); + + // Get context menu component (dynamically updated) + const contextMenuComponent = useMemo(() => { + if (!pageContent || !contextMenu?.componentId) return null; + + for (const section of pageContent.sections) { + for (const column of section.columns) { + const component = column.find( + comp => comp.id === contextMenu.componentId + ); + if (component) return component; } - current = current[fieldParts[i]]; } + return null; + }, [pageContent, contextMenu?.componentId]); + + // Initial load and locale change handling + useEffect(() => { + if (currentTranslation.sections) { + const content: PageContent = { + sections: currentTranslation.sections, + metadata: currentTranslation.metadata || { + version: '1.0.0', + lastModified: new Date().toISOString(), + author: 'admin', + }, + }; + setPageContent(content); + } + setCurrentLanguage(currentLocale); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentLocale, setPageContent, setCurrentLanguage]); // Only reload on locale change, not on translation data updates + + // Debounced auto-save function + const debouncedSave = useDebouncedCallback( + useCallback(() => { + if (!pageContent) return; + + const updatedTranslation: AboutTranslationData = { + sections: pageContent.sections, + metadata: { + ...pageContent.metadata, + lastModified: new Date().toISOString(), + }, + }; + + const newTranslations = { + ...translations, + [currentLocale]: updatedTranslation, + }; + + onTranslationsChange(newTranslations); + }, [pageContent, translations, currentLocale, onTranslationsChange]), + 100, // 100ms delay for near real-time sync + [pageContent, translations, currentLocale] + ); - current[fieldParts[fieldParts.length - 1]] = value; - onTranslationsChange(newTranslations); + // Auto-save when pageContent changes + useEffect(() => { + if (pageContent) { + debouncedSave(); + } + }, [pageContent, debouncedSave]); + + // Throttled property change handler for better performance + const throttledPropsChange = useThrottledCallback( + useCallback( + (newProps: Record) => { + if (selectedComponentId) { + updateComponentProps(selectedComponentId, newProps); + debouncedSave(); // Auto-save after prop changes + } + }, + [selectedComponentId, updateComponentProps, debouncedSave] + ), + 100, // 100ms throttle + [selectedComponentId, updateComponentProps] + ); + + // Handle property changes (currently unused but kept for future use) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const handlePropsChange = throttledPropsChange; + + // Handle component deletion + const handleDeleteComponent = (componentId: string) => { + deleteComponent(componentId); + setContextMenu(null); }; - const handleValueCardChange = ( - index: number, - field: 'title' | 'description', - value: string - ) => { - const newItems = [...(currentTranslation.values?.items || [])]; - newItems[index] = { ...newItems[index], [field]: value }; - handleFieldChange('values.items', newItems); + // Handle component selection + const handleComponentClick = (componentId: string) => { + setSelectedComponent(componentId); + // Focus the main container so keyboard events work + const container = document.querySelector('[tabindex="0"]') as HTMLElement; + if (container) { + container.focus(); + } }; - const addValueCard = () => { - const newItems = [ - ...(currentTranslation.values?.items || []), - { title: '', description: '' }, - ]; - handleFieldChange('values.items', newItems); + // Handle right-click context menu + const handleContextMenu = (e: React.MouseEvent, componentId: string) => { + e.preventDefault(); + e.stopPropagation(); + + // Find the component + let targetComponent: ComponentInstance | null = null; + for (const section of pageContent?.sections || []) { + for (const column of section.columns) { + const component = column.find(comp => comp.id === componentId); + if (component) { + targetComponent = component; + break; + } + } + if (targetComponent) break; + } + + if (targetComponent) { + setSelectedComponent(componentId); // Set as selected when right-clicking + setContextMenu({ + x: e.clientX, + y: e.clientY, + componentId: componentId, + }); + // Focus the main container so keyboard events work + const container = document.querySelector('[tabindex="0"]') as HTMLElement; + if (container) { + container.focus(); + } + } }; - const removeValueCard = (index: number) => { - const newItems = (currentTranslation.values?.items || []).filter( - (_: ValueItem, i: number) => i !== index - ); - handleFieldChange('values.items', newItems); + // Handle context menu props change + const handleContextMenuPropsChange = (newProps: Record) => { + if (contextMenu?.componentId) { + updateComponentProps(contextMenu.componentId, newProps); + debouncedSave(); // Auto-save after prop changes + } }; + // Handle keyboard shortcuts + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Delete' && selectedComponentId) { + e.preventDefault(); + handleDeleteComponent(selectedComponentId); + } + if (e.key === 'Escape' && editingComponent) { + e.preventDefault(); + setEditingComponent(null); + } + }; + + // Handle double-click to edit content + const handleDoubleClick = (componentId: string) => { + // Find the component + let targetComponent: ComponentInstance | null = null; + for (const section of pageContent?.sections || []) { + for (const column of section.columns) { + const component = column.find(comp => comp.id === componentId); + if (component) { + targetComponent = component; + break; + } + } + if (targetComponent) break; + } + + if (targetComponent && targetComponent.props.content) { + setEditingComponent({ + componentId, + content: String(targetComponent.props.content || ''), + }); + } + }; + + // Handle saving inline edit + const handleSaveEdit = () => { + if (editingComponent) { + updateComponentProps(editingComponent.componentId, { + content: editingComponent.content, + }); + setEditingComponent(null); + debouncedSave(); + } + }; + + // Handle input change during editing + const handleEditInputChange = (value: string) => { + if (editingComponent) { + setEditingComponent({ + ...editingComponent, + content: value, + }); + } + }; + + // Global context menu handler as fallback - finds component under cursor + const handleGlobalContextMenu = (e: React.MouseEvent) => { + // Only handle if no specific component context menu was triggered + if (contextMenu) return; + + // Find the closest component element + const target = e.target as HTMLElement; + const componentElement = target.closest( + '[data-component-id]' + ) as HTMLElement; + + if (componentElement) { + const componentId = componentElement.getAttribute('data-component-id'); + if (componentId) { + e.preventDefault(); + e.stopPropagation(); + handleContextMenu(e, componentId); + } + } + }; + + if (!pageContent) { + return ( +
+

Loading editor...

+
+ ); + } + return ( -
-
-