+ {/* Header */}
+
- {t('common.editLanguage')}
-
-
+ )}
+
);
}
diff --git a/components/admin/content/about-preview.tsx b/components/admin/content/about-preview.tsx
index 932ec01d..cc5bdf00 100644
--- a/components/admin/content/about-preview.tsx
+++ b/components/admin/content/about-preview.tsx
@@ -1,67 +1,69 @@
'use client';
-import { useTheme } from '@lib/hooks/use-theme';
+import {
+ AboutTranslationData,
+ PageContent,
+ isDynamicFormat,
+ migrateAboutTranslationData,
+} from '@lib/types/about-page-components';
import { cn } from '@lib/utils';
+import { AnimatePresence, motion } from 'framer-motion';
import React from 'react';
-export interface ValueCard {
- id: string;
- title: string;
- description: string;
-}
-
-export interface AboutPageConfig {
- title: string;
- subtitle: string;
- mission: string;
- valueCards: ValueCard[];
- buttonText: string;
- copyrightText: string;
-}
+import ComponentRenderer from './component-renderer';
interface AboutPreviewProps {
- config: AboutPageConfig;
+ /**
+ * Translation data for the about page
+ * Can be either legacy or dynamic format
+ */
+ translation: AboutTranslationData;
+ /**
+ * Preview device type for responsive preview
+ */
previewDevice: 'desktop' | 'tablet' | 'mobile';
}
-export function AboutPreview({ config, previewDevice }: AboutPreviewProps) {
- const { isDark } = useTheme();
-
- 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',
- };
+/**
+ * About Page Preview Component
+ *
+ * Displays a preview of the about page with homepage-style visual effects
+ * Unified styling with stone color system, animations, and shadows
+ */
+export function AboutPreview({
+ translation,
+ previewDevice,
+}: AboutPreviewProps) {
+ // Ensure translation is in dynamic format
+ const dynamicTranslation = React.useMemo(() => {
+ if (!isDynamicFormat(translation)) {
+ return migrateAboutTranslationData(translation);
}
+ return translation;
+ }, [translation]);
+
+ // Create page content from translation
+ const pageContent: PageContent = React.useMemo(() => {
+ return {
+ sections: dynamicTranslation.sections || [],
+ metadata: dynamicTranslation.metadata || {
+ version: '1.0.0',
+ lastModified: new Date().toISOString(),
+ author: 'preview',
+ },
+ };
+ }, [dynamicTranslation]);
+
+ // Homepage-style colors using Tailwind classes that respond to dark mode
+ const colors = {
+ bgClass: 'bg-stone-100 dark:bg-stone-900',
+ textColor: 'text-stone-700 dark:text-gray-300',
};
- const colors = getColors();
-
+ /**
+ * Get device-specific container styles with homepage styling
+ */
const getDeviceStyles = () => {
switch (previewDevice) {
case 'mobile':
@@ -70,8 +72,7 @@ export function AboutPreview({ config, previewDevice }: AboutPreviewProps) {
screen:
'w-[375px] h-[667px] bg-white rounded-[1.75rem] overflow-hidden relative',
content: 'h-full overflow-y-auto',
- mainClass: 'min-h-full w-full py-4 px-4',
- innerContainer: 'w-full',
+ mainClass: 'relative w-full px-4 py-12 sm:px-6 lg:px-8',
};
case 'tablet':
return {
@@ -79,8 +80,7 @@ export function AboutPreview({ config, previewDevice }: AboutPreviewProps) {
screen:
'w-[768px] h-[1024px] bg-white rounded-lg overflow-hidden relative',
content: 'h-full overflow-y-auto',
- mainClass: 'min-h-full w-full py-6 px-6',
- innerContainer: 'max-w-2xl mx-auto',
+ mainClass: 'relative w-full px-4 py-12 sm:px-6 lg:px-8',
};
case 'desktop':
default:
@@ -88,206 +88,162 @@ export function AboutPreview({ config, previewDevice }: AboutPreviewProps) {
container: 'w-full h-full',
screen: 'w-full h-full overflow-hidden relative',
content: 'h-full overflow-y-auto',
- mainClass:
- 'min-h-screen w-full py-6 px-4 sm:px-6 lg:px-8 overflow-x-hidden',
- innerContainer: 'max-w-5xl mx-auto',
+ mainClass: 'relative w-full px-4 py-12 sm:px-6 lg:px-8',
};
}
};
const deviceStyles = getDeviceStyles();
- const getResponsiveClasses = () => {
- switch (previewDevice) {
- case 'mobile':
- return {
- title: 'text-3xl font-bold mb-4 leading-tight py-1',
- subtitle: 'text-base font-light',
- missionTitle: 'text-xl font-bold mb-4',
- missionContent: 'text-sm',
- valuesTitle: 'text-xl font-bold mb-4',
- cardTitle: 'text-base font-semibold mb-2',
- cardContent: 'text-sm',
- button: 'px-6 py-2 text-sm font-medium rounded-lg',
- copyright: 'text-xs',
- spacing: {
- section: 'mb-6',
- missionPadding: 'p-4',
- cardPadding: 'p-4',
- cardGap: 'gap-4',
- },
- };
- case 'tablet':
- return {
- title: 'text-4xl md:text-4xl font-bold mb-4 leading-tight py-1',
- subtitle: 'text-lg font-light',
- missionTitle: 'text-2xl font-bold mb-4',
- missionContent: 'text-base',
- valuesTitle: 'text-2xl font-bold mb-4',
- cardTitle: 'text-lg font-semibold mb-2',
- cardContent: 'text-sm',
- button: 'px-6 py-2.5 text-base font-medium rounded-lg',
- copyright: 'text-sm',
- spacing: {
- section: 'mb-8',
- missionPadding: 'p-6',
- cardPadding: 'p-5',
- cardGap: 'gap-5',
- },
- };
- case 'desktop':
- default:
- return {
- title: 'text-4xl md:text-5xl font-bold mb-6 leading-tight py-2',
- subtitle: 'text-xl font-light',
- missionTitle: 'text-2xl font-bold mb-6',
- missionContent: 'text-lg',
- valuesTitle: 'text-2xl font-bold mb-6',
- cardTitle: 'text-lg font-semibold mb-2',
- cardContent: '',
- button: 'px-8 py-3 text-base font-medium rounded-lg',
- copyright: 'text-sm',
- spacing: {
- section: 'mb-10',
- missionPadding: '',
- cardPadding: 'p-6',
- cardGap: 'gap-6',
- },
- };
+ /**
+ * Render page sections with homepage-style animations and layout
+ */
+ const renderSections = () => {
+ if (!pageContent.sections || pageContent.sections.length === 0) {
+ return (
+
+ No content to preview
+
+ );
}
- };
- const responsive = getResponsiveClasses();
+ return (
+
+ {pageContent.sections.map((section, sectionIndex) => (
+
+
+ {section.columns.map((column, columnIndex) => (
+
+ {column.map((component, componentIndex) => (
+
+
+
+ ))}
+
+ ))}
+
+
+ ))}
+
+ );
+ };
return (
-
+
-
-
-
-
- {config.title}
-
-
- {config.subtitle}
-
-
-
-
-
-
-
- {config.valueCards.map(value => (
-
-
- {value.title}
-
-
- {value.description}
-
-
- ))}
-
-
-
-
-
-
-
-
-
{config.copyrightText}
-
-
-
+
+ {renderSections()}
+
);
}
+
+/**
+ * Legacy interface for backward compatibility
+ * This is for existing components that still use the old preview format
+ */
+export interface ValueCard {
+ id: string;
+ title: string;
+ description: string;
+}
+
+export interface AboutPageConfig {
+ title: string;
+ subtitle: string;
+ mission: string;
+ valueCards: ValueCard[];
+ buttonText: string;
+ copyrightText: string;
+}
+
+/**
+ * Legacy preview component for backward compatibility
+ * @deprecated Use AboutPreview with dynamic translation data instead
+ */
+export function LegacyAboutPreview({
+ config,
+ previewDevice,
+}: {
+ config: AboutPageConfig;
+ previewDevice: 'desktop' | 'tablet' | 'mobile';
+}) {
+ // Convert legacy config to new format
+ const legacyTranslation: AboutTranslationData = {
+ title: config.title,
+ subtitle: config.subtitle,
+ mission: { description: config.mission },
+ values: {
+ items: config.valueCards.map(card => ({
+ title: card.title,
+ description: card.description,
+ })),
+ },
+ buttonText: config.buttonText,
+ copyright: {
+ prefix: config.copyrightText,
+ linkText: '',
+ suffix: '',
+ },
+ };
+
+ return (
+
+ );
+}
diff --git a/components/admin/content/component-palette.tsx b/components/admin/content/component-palette.tsx
new file mode 100644
index 00000000..4ca70535
--- /dev/null
+++ b/components/admin/content/component-palette.tsx
@@ -0,0 +1,246 @@
+'use client';
+
+import { ComponentType } from '@lib/types/about-page-components';
+import { cn } from '@lib/utils';
+import {
+ AlignLeft,
+ Grid3x3,
+ Image as ImageIcon,
+ Minus,
+ MousePointer,
+ Palette,
+ Type,
+} from 'lucide-react';
+
+import React from 'react';
+
+import { Draggable, Droppable } from './dnd-components';
+
+/**
+ * Available component definitions for the palette
+ */
+interface ComponentDefinition {
+ type: ComponentType;
+ name: string;
+ icon: React.ReactNode;
+ description: string;
+ defaultProps: Record
;
+ category: 'basic' | 'content' | 'media' | 'layout';
+}
+
+const availableComponents: ComponentDefinition[] = [
+ {
+ type: 'heading',
+ name: 'Heading',
+ icon: ,
+ description: 'Add headings (H1-H6)',
+ defaultProps: {
+ content: 'New Heading',
+ level: 2,
+ textAlign: 'left',
+ },
+ category: 'basic',
+ },
+ {
+ type: 'paragraph',
+ name: 'Paragraph',
+ icon: ,
+ description: 'Add paragraph text',
+ defaultProps: {
+ content: 'New paragraph text.',
+ textAlign: 'left',
+ },
+ category: 'basic',
+ },
+ {
+ type: 'button',
+ name: 'Button',
+ icon: ,
+ description: 'Interactive button',
+ defaultProps: {
+ text: 'Click me',
+ variant: 'solid',
+ action: 'link',
+ url: '#',
+ },
+ category: 'content',
+ },
+ {
+ type: 'cards',
+ name: 'Cards',
+ icon: ,
+ description: 'Grid or list of cards',
+ defaultProps: {
+ layout: 'grid',
+ items: [
+ { title: 'Card 1', description: 'Description for card 1' },
+ { title: 'Card 2', description: 'Description for card 2' },
+ ],
+ },
+ category: 'content',
+ },
+ {
+ type: 'image',
+ name: 'Image',
+ icon: ,
+ description: 'Add images with captions',
+ defaultProps: {
+ src: 'https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?w=400&h=200&fit=crop',
+ alt: 'Placeholder Image',
+ caption: '',
+ alignment: 'center',
+ width: 'auto',
+ height: 'auto',
+ },
+ category: 'media',
+ },
+ {
+ type: 'divider',
+ name: 'Divider',
+ icon: ,
+ description: 'Horizontal divider line',
+ defaultProps: {
+ style: 'solid',
+ thickness: 'thin',
+ color: 'gray',
+ },
+ category: 'layout',
+ },
+];
+
+/**
+ * Component categories for organization
+ */
+const categories = [
+ { id: 'basic' as const, name: 'Basic', icon: },
+ {
+ id: 'content' as const,
+ name: 'Content',
+ icon: ,
+ },
+ {
+ id: 'media' as const,
+ name: 'Media',
+ icon: ,
+ },
+ {
+ id: 'layout' as const,
+ name: 'Layout',
+ icon: ,
+ },
+];
+
+interface ComponentPaletteProps {
+ className?: string;
+}
+
+/**
+ * Component Palette
+ *
+ * Displays available components that can be dragged to the editor
+ */
+const ComponentPalette: React.FC = ({ className }) => {
+ return (
+
+
+
+
+ {categories.map(category => {
+ const categoryComponents = availableComponents.filter(
+ comp => comp.category === category.id
+ );
+
+ if (categoryComponents.length === 0) return null;
+
+ return (
+
+
+ {category.icon}
+ {category.name}
+
+
+ {categoryComponents.map(comp => {
+ const componentPreview = (
+
+
+ {comp.icon}
+
+
+
+ {comp.name}
+
+
+ {comp.description}
+
+
+
+ );
+
+ return (
+
+ {componentPreview}
+
+ );
+ })}
+
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default ComponentPalette;
+export { availableComponents };
diff --git a/components/admin/content/component-renderer.tsx b/components/admin/content/component-renderer.tsx
new file mode 100644
index 00000000..e2668962
--- /dev/null
+++ b/components/admin/content/component-renderer.tsx
@@ -0,0 +1,344 @@
+'use client';
+
+import {
+ CardsProps,
+ ComponentInstance,
+ DividerProps,
+ ButtonProps as DynamicButtonProps,
+ HeadingProps,
+ ImageProps,
+ ParagraphProps,
+ SectionCommonProps,
+ getResolvedComponentProps,
+} from '@lib/types/about-page-components';
+import { cn } from '@lib/utils';
+import { processTextPlaceholders } from '@lib/utils/text-processing';
+
+import React from 'react';
+
+/**
+ * Heading component renderer
+ *
+ * Renders heading elements with homepage-style gradients and styling
+ */
+const Heading: React.FC = ({ content, level, textAlign }) => {
+ const textAlignClasses = {
+ left: 'text-left',
+ center: 'text-center',
+ right: 'text-right',
+ };
+
+ const alignmentClass = textAlignClasses[textAlign];
+
+ // Define font sizes for different heading levels with responsive design
+ const sizeClasses = {
+ 1: 'text-5xl md:text-6xl',
+ 2: 'text-3xl sm:text-4xl md:text-5xl',
+ 3: 'text-2xl sm:text-3xl md:text-4xl',
+ 4: 'text-xl sm:text-2xl md:text-3xl',
+ 5: 'text-lg sm:text-xl md:text-2xl',
+ 6: 'text-base sm:text-lg md:text-xl',
+ };
+
+ // Apply gradient for large headings (H1, H2)
+ const shouldUseGradient = level <= 2;
+
+ if (shouldUseGradient) {
+ return React.createElement(
+ `h${level}`,
+ {
+ className: cn(
+ 'bg-gradient-to-r from-stone-700 to-stone-900 dark:from-stone-300 dark:to-stone-500',
+ 'mb-6 bg-clip-text py-3 leading-normal font-bold text-transparent',
+ sizeClasses[level],
+ alignmentClass
+ ),
+ },
+ content
+ );
+ }
+
+ return React.createElement(
+ `h${level}`,
+ {
+ className: cn(
+ 'mb-4 font-bold text-stone-900 sm:mb-6 dark:text-stone-100',
+ sizeClasses[level],
+ alignmentClass
+ ),
+ },
+ content
+ );
+};
+
+/**
+ * Paragraph component renderer
+ *
+ * Renders paragraph text with stone color system styling
+ */
+const Paragraph: React.FC = ({ content, textAlign }) => {
+ const textAlignClasses = {
+ left: 'text-left',
+ center: 'text-center',
+ right: 'text-right',
+ };
+
+ // Process text placeholders like {year}
+ const processedContent = processTextPlaceholders(content);
+
+ return (
+
+ {processedContent}
+
+ );
+};
+
+/**
+ * Cards component renderer
+ *
+ * Renders a grid or list of cards with homepage-inspired shadows and stone color system
+ */
+const Cards: React.FC<
+ CardsProps & { previewDevice?: 'desktop' | 'tablet' | 'mobile' }
+> = ({ items, layout, previewDevice = 'desktop' }) => {
+ // Determine grid layout based on layout prop and preview device
+ const getGridClass = () => {
+ if (layout !== 'grid') return 'grid-cols-1';
+
+ // For mobile preview, always use single column
+ if (previewDevice === 'mobile') return 'grid-cols-1';
+
+ // For desktop and tablet, use responsive two-column layout
+ return 'md:grid-cols-2';
+ };
+
+ const gridClass = getGridClass();
+
+ return (
+
+ {items.map((item, index) => (
+
+
+
+ #{index + 1}
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+ ))}
+
+ );
+};
+
+/**
+ * Button component renderer
+ *
+ * Renders interactive buttons with stone color system variants
+ * Supports dual button layout with homepage-style spacing
+ */
+const DynamicButton: React.FC = ({
+ text,
+ variant,
+ action,
+ url,
+ secondaryButton,
+}) => {
+ const handleClick = (
+ e: React.MouseEvent,
+ buttonAction?: string,
+ buttonUrl?: string
+ ) => {
+ if (buttonAction === 'link' && buttonUrl === '#') {
+ e.preventDefault();
+ // In preview mode, prevent default action
+ }
+ };
+
+ const buttonStyles = {
+ solid: cn(
+ 'inline-flex items-center justify-center rounded-lg px-8 py-3 text-base font-medium transition-all duration-200',
+ 'min-w-[8rem] bg-stone-900 text-white hover:scale-105 hover:bg-stone-800 focus:ring-2 focus:ring-stone-500 focus:ring-offset-2 focus:outline-none',
+ 'dark:bg-stone-100 dark:text-stone-900 dark:hover:bg-stone-200 dark:focus:ring-stone-400'
+ ),
+ outline: cn(
+ 'inline-flex items-center justify-center rounded-lg px-8 py-3 text-base font-medium transition-all duration-200',
+ 'min-w-[8rem] border border-stone-300 bg-transparent text-stone-900 hover:scale-105 hover:bg-stone-50 focus:ring-2 focus:ring-stone-500 focus:ring-offset-2 focus:outline-none',
+ 'dark:border-stone-600 dark:text-stone-100 dark:hover:bg-stone-800 dark:focus:ring-stone-400'
+ ),
+ };
+
+ return (
+
+ );
+};
+
+/**
+ * Image component renderer
+ *
+ * Renders images with captions and alignment
+ */
+const Image: React.FC = ({
+ src,
+ alt,
+ caption,
+ alignment,
+ width,
+ height,
+}) => {
+ const alignmentClasses = {
+ left: 'text-left',
+ center: 'text-center',
+ right: 'text-right',
+ };
+
+ const imageStyle: React.CSSProperties = {
+ width: typeof width === 'number' ? `${width}px` : width,
+ height: typeof height === 'number' ? `${height}px` : height,
+ maxWidth: '100%',
+ };
+
+ return (
+
+

+ {caption && (
+
{caption}
+ )}
+
+ );
+};
+
+/**
+ * Divider component renderer
+ *
+ * Renders horizontal dividers with different styles
+ */
+const Divider: React.FC = ({ style, thickness, color }) => {
+ let borderStyle = 'border-solid';
+ if (style === 'dashed') borderStyle = 'border-dashed';
+ if (style === 'dotted') borderStyle = 'border-dotted';
+
+ let borderThickness = 'border-t';
+ if (thickness === 'medium') borderThickness = 'border-t-2';
+ if (thickness === 'thick') borderThickness = 'border-t-4';
+
+ const borderColor = color ? `border-${color}-300` : 'border-border';
+
+ return (
+
+ );
+};
+
+/**
+ * Component type to React component mapping
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const componentMap: Record> = {
+ heading: Heading,
+ paragraph: Paragraph,
+ cards: Cards,
+ button: DynamicButton,
+ image: Image,
+ divider: Divider,
+};
+
+/**
+ * Main component renderer
+ *
+ * Dynamically renders components based on their type
+ */
+interface ComponentRendererProps {
+ component: ComponentInstance;
+ className?: string;
+ sectionCommonProps?: SectionCommonProps;
+ previewDevice?: 'desktop' | 'tablet' | 'mobile';
+}
+
+const ComponentRenderer: React.FC = ({
+ component,
+ className,
+ sectionCommonProps,
+ previewDevice = 'desktop',
+}) => {
+ const Component = componentMap[component.type];
+
+ if (!Component) {
+ // Fallback for unregistered components
+ return (
+
+
+ Component type "{component.type}" not found.
+
+
Please check the component registration.
+
+ );
+ }
+
+ // Final properties after merge (including inheritance logic)
+ const resolvedProps = getResolvedComponentProps(
+ component,
+ sectionCommonProps
+ );
+
+ // Add previewDevice to props if component supports it
+ const componentProps =
+ component.type === 'cards'
+ ? { ...resolvedProps, previewDevice }
+ : resolvedProps;
+
+ return (
+
+
+
+ );
+};
+
+export default ComponentRenderer;
diff --git a/components/admin/content/content-tabs.tsx b/components/admin/content/content-tabs.tsx
index 8c111c33..aac67158 100644
--- a/components/admin/content/content-tabs.tsx
+++ b/components/admin/content/content-tabs.tsx
@@ -1,6 +1,5 @@
'use client';
-import { useTheme } from '@lib/hooks/use-theme';
import { cn } from '@lib/utils';
import { useTranslations } from 'next-intl';
@@ -11,7 +10,6 @@ interface ContentTabsProps {
}
export function ContentTabs({ activeTab, onTabChange }: ContentTabsProps) {
- const { isDark } = useTheme();
const t = useTranslations('pages.admin.content.tabs');
const tabs = [
@@ -23,7 +21,7 @@ export function ContentTabs({ activeTab, onTabChange }: ContentTabsProps) {
{tabs.map(tab => (
@@ -33,12 +31,8 @@ export function ContentTabs({ activeTab, onTabChange }: ContentTabsProps) {
className={cn(
'rounded-md px-4 py-1.5 text-sm font-medium transition-colors',
activeTab === tab.id
- ? isDark
- ? 'bg-stone-500 text-white shadow-sm'
- : 'bg-white text-stone-900 shadow-sm'
- : isDark
- ? 'text-stone-300 hover:bg-stone-600'
- : 'text-stone-600 hover:bg-stone-100'
+ ? 'bg-white text-stone-900 shadow-sm dark:bg-stone-500 dark:text-white'
+ : 'text-stone-600 hover:bg-stone-100 dark:text-stone-300 dark:hover:bg-stone-600'
)}
>
{tab.label}
diff --git a/components/admin/content/context-menu.tsx b/components/admin/content/context-menu.tsx
new file mode 100644
index 00000000..b6d92140
--- /dev/null
+++ b/components/admin/content/context-menu.tsx
@@ -0,0 +1,534 @@
+'use client';
+
+import { Input } from '@components/ui/input';
+import { Label } from '@components/ui/label';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from '@components/ui/select';
+import { Textarea } from '@components/ui/textarea';
+import { ComponentInstance } from '@lib/types/about-page-components';
+import { cn } from '@lib/utils';
+import { Plus, Trash2, X } from 'lucide-react';
+
+import React, { useEffect, useRef, useState } from 'react';
+
+interface ContextMenuProps {
+ x: number;
+ y: number;
+ component: ComponentInstance | null;
+ onPropsChange: (newProps: Record
) => void;
+ onDelete: (componentId: string) => void;
+ onClose: () => void;
+}
+
+/**
+ * Context Menu Component
+ *
+ * Right-click context menu that directly shows property editor
+ */
+export const ContextMenu: React.FC = ({
+ x,
+ y,
+ component,
+ onPropsChange,
+ onDelete,
+ onClose,
+}) => {
+ const modalRef = useRef(null);
+ const [position, setPosition] = useState({ x, y });
+
+ useEffect(() => {
+ if (modalRef.current) {
+ const modal = modalRef.current;
+ const rect = modal.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ let newX = x;
+ let newY = y;
+
+ // Adjust position if modal would go outside viewport
+ if (x + rect.width > viewportWidth) {
+ newX = viewportWidth - rect.width - 20;
+ }
+ if (y + rect.height > viewportHeight) {
+ newY = viewportHeight - rect.height - 20;
+ }
+ if (newX < 20) newX = 20;
+ if (newY < 20) newY = 20;
+
+ setPosition({ x: newX, y: newY });
+ }
+ }, [x, y]);
+
+ // Handle ESC key press to close the context menu
+ useEffect(() => {
+ const handleEscapeKey = (event: KeyboardEvent) => {
+ if (event.key === 'Escape') {
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleEscapeKey);
+ return () => {
+ document.removeEventListener('keydown', handleEscapeKey);
+ };
+ }, [onClose]);
+
+ const handleInputChange = (name: string, value: unknown) => {
+ if (!component) return;
+ onPropsChange({ ...component.props, [name]: value });
+ };
+
+ const handleSecondaryButtonChange = (key: string, value: unknown) => {
+ if (!component) return;
+ const currentSecondaryButton =
+ (component.props.secondaryButton as Record) || {};
+ const updatedSecondaryButton = { ...currentSecondaryButton, [key]: value };
+ onPropsChange({
+ ...component.props,
+ secondaryButton: updatedSecondaryButton,
+ });
+ };
+
+ const handleAddSecondaryButton = () => {
+ if (!component) return;
+ const defaultSecondaryButton = {
+ text: 'Secondary Button',
+ variant: 'outline',
+ action: 'link',
+ url: '#',
+ };
+ onPropsChange({
+ ...component.props,
+ secondaryButton: defaultSecondaryButton,
+ });
+ };
+
+ const handleRemoveSecondaryButton = () => {
+ if (!component) return;
+ const newProps = { ...component.props };
+ delete newProps.secondaryButton;
+ onPropsChange(newProps);
+ };
+
+ const handleItemsChange = (newItems: Array>) => {
+ handleInputChange('items', newItems);
+ };
+
+ const handleItemChange = (index: number, key: string, value: unknown) => {
+ if (!component) return;
+ const items = component.props.items as Array>;
+ const newItems = [...items];
+ newItems[index] = { ...newItems[index], [key]: value };
+ handleItemsChange(newItems);
+ };
+
+ const handleAddItem = () => {
+ if (!component) return;
+ const items =
+ (component.props.items as Array>) || [];
+ const newItem = {
+ title: 'New Item',
+ description: 'Add description here',
+ };
+ handleItemsChange([...items, newItem]);
+ };
+
+ const handleRemoveItem = (index: number) => {
+ if (!component) return;
+ const items = component.props.items as Array>;
+ const newItems = items.filter((_, i) => i !== index);
+ handleItemsChange(newItems);
+ };
+
+ const handleDelete = () => {
+ if (component) {
+ onDelete(component.id);
+ onClose();
+ }
+ };
+
+ if (!component) return null;
+
+ const renderPropertyField = (key: string, value: unknown) => {
+ const fieldId = `prop-${key}`;
+
+ // Array properties (cards items)
+ if (key === 'items' && component.type === 'cards') {
+ const items = value as Array>;
+ return (
+
+
+
+ {items.map((item, index) => (
+
+
+
+ Item {index + 1}
+
+
+
+
+ {Object.entries(item).map(([itemKey, itemValue]) => (
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+ );
+ }
+
+ // Secondary button for button component
+ if (key === 'secondaryButton' && component.type === 'button') {
+ const secondaryButton = value as Record | undefined;
+
+ if (!secondaryButton) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {/* Text field */}
+
+
+
+ handleSecondaryButtonChange('text', e.target.value)
+ }
+ placeholder="Enter button text"
+ className="h-8 text-sm"
+ />
+
+
+ {/* Variant field */}
+
+
+
+ handleSecondaryButtonChange('variant', newValue)
+ }
+ >
+
+
+
+
+ Solid
+ Outline
+
+
+
+
+ {/* Action field */}
+
+
+
+ handleSecondaryButtonChange('action', newValue)
+ }
+ >
+
+
+
+
+ Link
+ Submit
+ External
+
+
+
+
+ {/* URL field */}
+
+
+
+ handleSecondaryButtonChange('url', e.target.value)
+ }
+ placeholder="Enter URL"
+ className="h-8 text-sm"
+ />
+
+
+
+ );
+ }
+
+ // Select fields
+ const selectFields: Record<
+ string,
+ Array<{ value: string; label: string }>
+ > = {
+ layout: [
+ { value: 'grid', label: 'Grid' },
+ { value: 'list', label: 'List' },
+ ],
+ thickness: [
+ { value: 'thin', label: 'Thin' },
+ { value: 'medium', label: 'Medium' },
+ { value: 'thick', label: 'Thick' },
+ ],
+ style: [
+ { value: 'solid', label: 'Solid' },
+ { value: 'dashed', label: 'Dashed' },
+ { value: 'dotted', label: 'Dotted' },
+ ],
+ alignment: [
+ { value: 'left', label: 'Left' },
+ { value: 'center', label: 'Center' },
+ { value: 'right', label: 'Right' },
+ ],
+ textAlign: [
+ { value: 'left', label: 'Left' },
+ { value: 'center', label: 'Center' },
+ { value: 'right', label: 'Right' },
+ ],
+ action: [
+ { value: 'link', label: 'Link' },
+ { value: 'submit', label: 'Submit' },
+ { value: 'external', label: 'External' },
+ ],
+ variant: [
+ { value: 'solid', label: 'Solid' },
+ { value: 'outline', label: 'Outline' },
+ ],
+ level: [
+ { value: '1', label: 'H1' },
+ { value: '2', label: 'H2' },
+ { value: '3', label: 'H3' },
+ { value: '4', label: 'H4' },
+ { value: '5', label: 'H5' },
+ { value: '6', label: 'H6' },
+ ],
+ };
+
+ if (selectFields[key]) {
+ return (
+
+
+
+ handleInputChange(
+ key,
+ key === 'level' ? Number(newValue) : newValue
+ )
+ }
+ >
+
+
+
+
+ {selectFields[key].map(option => (
+
+ {option.label}
+
+ ))}
+
+
+
+ );
+ }
+
+ // Multi-line text fields
+ if (
+ key === 'content' &&
+ (component.type === 'paragraph' || component.type === 'heading')
+ ) {
+ return (
+
+
+
+ );
+ }
+
+ // Number fields
+ if (typeof value === 'number') {
+ return (
+
+
+ handleInputChange(key, Number(e.target.value) || 0)}
+ placeholder={`Enter ${key}`}
+ className="h-8 text-sm"
+ />
+
+ );
+ }
+
+ // String fields
+ if (typeof value === 'string') {
+ return (
+
+
+ handleInputChange(key, e.target.value)}
+ placeholder={`Enter ${key}`}
+ className="h-8 text-sm"
+ />
+
+ );
+ }
+
+ return null;
+ };
+
+ return (
+ <>
+ {/* Backdrop */}
+
+
+ {/* Property Editor Modal */}
+
+
+
+
+ {component.type} Properties
+
+
+
+
+
+
+
+
+ {Object.entries(component.props).map(([key, value]) =>
+ renderPropertyField(key, value)
+ )}
+ {/* Always show secondary button option for button components */}
+ {component.type === 'button' &&
+ !component.props.secondaryButton &&
+ renderPropertyField('secondaryButton', undefined)}
+
+
+ >
+ );
+};
diff --git a/components/admin/content/dnd-components.tsx b/components/admin/content/dnd-components.tsx
new file mode 100644
index 00000000..1fedb44a
--- /dev/null
+++ b/components/admin/content/dnd-components.tsx
@@ -0,0 +1,319 @@
+'use client';
+
+import { useDraggable, useDroppable } from '@dnd-kit/core';
+import {
+ SortableContext,
+ SortingStrategy,
+ arrayMove,
+ useSortable,
+ verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import { CSS } from '@dnd-kit/utilities';
+import { cn } from '@lib/utils';
+
+import React from 'react';
+
+import { useDndState } from './dnd-context';
+
+// Re-export arrayMove for convenience
+export { arrayMove };
+
+// Droppable component interface
+interface DroppableProps {
+ id: string;
+ children: React.ReactNode | ((isOver: boolean) => React.ReactNode);
+ className?: string;
+ disabled?: boolean;
+}
+
+/**
+ * DndKit Droppable Component
+ *
+ * Replaces react-beautiful-dnd's Droppable
+ */
+export function Droppable({
+ id,
+ children,
+ className,
+ disabled = false,
+}: DroppableProps) {
+ const { isOver, setNodeRef } = useDroppable({
+ id,
+ disabled,
+ });
+
+ // Check if this is a section drop zone that should have special hover effects
+ const isSectionDropZone = id.includes('section-drop');
+
+ // Apply drag-over styles that mimic hover effects for section drop zones
+ const dragOverStyles =
+ isSectionDropZone && isOver
+ ? {
+ height: '4rem', // equivalent to h-16
+ borderColor: 'rgb(168 162 158)', // stone-400
+ backgroundColor: 'rgb(245 245 244)', // stone-100 light mode
+ }
+ : {};
+
+ return (
+
+ {typeof children === 'function'
+ ? (children as (isOver: boolean) => React.ReactNode)(isOver)
+ : children}
+
+ );
+}
+
+// Draggable component interface
+interface DraggableProps {
+ id: string;
+ children: React.ReactNode;
+ className?: string;
+ disabled?: boolean;
+ preview?: React.ReactNode;
+}
+
+/**
+ * DndKit Draggable Component
+ *
+ * Replaces react-beautiful-dnd's Draggable
+ */
+export function Draggable({
+ id,
+ children,
+ className,
+ disabled = false,
+ preview,
+}: DraggableProps) {
+ const { attributes, listeners, setNodeRef, transform, isDragging } =
+ useDraggable({
+ id,
+ disabled,
+ data: {
+ type: id.startsWith('palette-') ? 'palette-item' : 'component',
+ preview,
+ },
+ });
+
+ const style = {
+ transform: CSS.Translate.toString(transform),
+ };
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Sortable component for within containers
+interface SortableProps {
+ id: string;
+ children: React.ReactNode;
+ className?: string;
+ disabled?: boolean;
+ preview?: React.ReactNode;
+ onClick?: () => void;
+ onDoubleClick?: () => void;
+ onContextMenu?: (e: React.MouseEvent) => void;
+}
+
+/**
+ * DndKit Sortable Component
+ *
+ * For sortable items within containers
+ */
+export function Sortable({
+ id,
+ children,
+ className,
+ disabled = false,
+ preview,
+ onClick,
+ onDoubleClick,
+ onContextMenu,
+}: SortableProps) {
+ const { isDraggingFromPalette } = useDndState();
+
+ // Normal sortable behavior - always call hooks at the top level
+ const shouldDisable = disabled;
+
+ const {
+ attributes,
+ listeners,
+ setNodeRef,
+ transform,
+ transition,
+ isDragging,
+ isOver,
+ } = useSortable({
+ id,
+ disabled: shouldDisable,
+ data: {
+ type: 'component',
+ preview,
+ },
+ });
+
+ // Filter out context menu interfering events and handle them separately
+ // IMPORTANT: This hook must be called before any conditional returns!
+ const filteredListeners = React.useMemo(() => {
+ if (!listeners) return {};
+
+ // Create a copy without onContextMenu to prevent conflicts
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { onContextMenu: _, ...otherListeners } = listeners as Record<
+ string,
+ unknown
+ >;
+ return otherListeners;
+ }, [listeners]);
+
+ // Conditional rendering after all hooks are called
+ if (isDraggingFromPalette) {
+ console.log('🚫 SORTABLE RENDERED AS PLAIN DIV:', {
+ componentId: id,
+ isDraggingFromPalette,
+ reason: 'palette dragging - avoiding drop conflicts',
+ });
+
+ return (
+
+ {children}
+
+ );
+ }
+
+ // Log component dragging state
+ if (isDragging) {
+ console.log('🔥 COMPONENT BEING DRAGGED:', {
+ componentId: id,
+ isDragging,
+ transform,
+ isDraggingFromPalette,
+ });
+ }
+
+ // Log if somehow this component is still receiving hover events
+ if (isDraggingFromPalette && isOver) {
+ console.log('⚠️ SORTABLE STILL HOVERING (Should not happen!):', {
+ componentId: id,
+ isOver,
+ shouldDisable,
+ isDraggingFromPalette,
+ });
+ }
+
+ const style = {
+ transform: CSS.Transform.toString(transform),
+ transition,
+ };
+
+ return (
+ {
+ // Call the provided context menu handler if available
+ if (onContextMenu) {
+ onContextMenu(e);
+ }
+ // Don't prevent default or stop propagation to ensure proper handling
+ }}
+ data-component-id={id}
+ >
+ {children}
+
+ );
+}
+
+// Sortable Container
+interface SortableContainerProps {
+ id: string;
+ items: string[];
+ children: React.ReactNode;
+ className?: string;
+ strategy?: SortingStrategy;
+}
+
+/**
+ * DndKit Sortable Container
+ *
+ * Provides sortable context for child components
+ */
+export function SortableContainer({
+ id,
+ items,
+ children,
+ className,
+ strategy = verticalListSortingStrategy,
+}: SortableContainerProps) {
+ const { isDraggingFromPalette } = useDndState();
+
+ const { isOver, setNodeRef } = useDroppable({
+ id,
+ data: {
+ type: 'container',
+ accepts: ['component', 'palette-item'],
+ },
+ });
+
+ // Log hover events when dragging from palette
+ if (isDraggingFromPalette && isOver) {
+ console.log('📦 CONTAINER HOVER:', {
+ containerId: id,
+ isOver,
+ isDraggingFromPalette,
+ itemCount: items.length,
+ });
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/components/admin/content/dnd-context.tsx b/components/admin/content/dnd-context.tsx
new file mode 100644
index 00000000..1b08236e
--- /dev/null
+++ b/components/admin/content/dnd-context.tsx
@@ -0,0 +1,143 @@
+'use client';
+
+import {
+ DndContext,
+ DragEndEvent,
+ DragOverlay,
+ DragStartEvent,
+ PointerSensor,
+ useSensor,
+ useSensors,
+} from '@dnd-kit/core';
+import { createPortal } from 'react-dom';
+
+import React, { createContext, useContext, useState } from 'react';
+
+interface DndContextWrapperProps {
+ children: React.ReactNode;
+ onDragEnd: (event: DragEndEvent) => boolean; // Return true if drop was successful
+}
+
+interface DndStateContextValue {
+ isDraggingFromPalette: boolean;
+}
+
+const DndStateContext = createContext({
+ isDraggingFromPalette: false,
+});
+
+export const useDndState = () => useContext(DndStateContext);
+
+/**
+ * DndKit Context Wrapper
+ *
+ * Provides drag and drop context using @dnd-kit/core
+ * Replaces react-beautiful-dnd's DragDropContext
+ */
+export function DndContextWrapper({
+ children,
+ onDragEnd,
+}: DndContextWrapperProps) {
+ const [activeItem, setActiveItem] = useState<{
+ id: string;
+ content: React.ReactNode;
+ } | null>(null);
+
+ const [isDraggingFromPalette, setIsDraggingFromPalette] = useState(false);
+ const [dropWasSuccessful, setDropWasSuccessful] = useState(false);
+ const [isAnimatingOut, setIsAnimatingOut] = useState(false);
+
+ const sensors = useSensors(
+ useSensor(PointerSensor, {
+ activationConstraint: {
+ distance: 5,
+ },
+ })
+ );
+
+ const handleDragStart = (event: DragStartEvent) => {
+ const { active } = event;
+ const activeId = String(active.id);
+
+ // Check if dragging from palette
+ const isFromPalette = activeId.startsWith('palette-');
+ setIsDraggingFromPalette(isFromPalette);
+
+ console.log('🚀 DND START:', {
+ activeId,
+ isFromPalette,
+ isDraggingFromPalette: isFromPalette,
+ activeData: active.data.current,
+ });
+
+ setActiveItem({
+ id: activeId,
+ content: active.data.current?.preview || activeId,
+ });
+ };
+
+ const handleDragEnd = (event: DragEndEvent) => {
+ console.log('🏁 DND END:', {
+ activeId: event.active.id,
+ overId: event.over?.id,
+ overData: event.over?.data.current,
+ isDraggingFromPalette,
+ });
+
+ // Call the store's drag end handler and get success status
+ const wasSuccessful = onDragEnd(event);
+ setDropWasSuccessful(wasSuccessful);
+
+ if (wasSuccessful && activeItem) {
+ // For successful drops, animate out in place
+ setIsAnimatingOut(true);
+
+ // Clear the item after animation completes
+ setTimeout(() => {
+ setActiveItem(null);
+ setIsDraggingFromPalette(false);
+ setDropWasSuccessful(false);
+ setIsAnimatingOut(false);
+ }, 200); // 200ms fade out animation
+ } else {
+ // For failed drops, clear immediately (allows default return animation)
+ setActiveItem(null);
+ setIsDraggingFromPalette(false);
+ setDropWasSuccessful(false);
+ setIsAnimatingOut(false);
+ }
+ };
+
+ return (
+
+
+ {children}
+ {typeof document !== 'undefined' &&
+ createPortal(
+
+ {activeItem ? (
+
+ {activeItem.content}
+
+ ) : null}
+ ,
+ document.body
+ )}
+
+
+ );
+}
diff --git a/components/admin/content/drag-preview-renderer.tsx b/components/admin/content/drag-preview-renderer.tsx
new file mode 100644
index 00000000..02f10ea8
--- /dev/null
+++ b/components/admin/content/drag-preview-renderer.tsx
@@ -0,0 +1,141 @@
+'use client';
+
+import type { ComponentInstance } from '@lib/types/about-page-components';
+import { cn } from '@lib/utils';
+import {
+ AlignLeft,
+ Grid3x3,
+ Image as ImageIcon,
+ Minus,
+ MousePointer,
+ Type,
+} from 'lucide-react';
+
+import React from 'react';
+
+interface DragPreviewRendererProps {
+ component: ComponentInstance;
+ className?: string;
+}
+
+/**
+ * Simplified drag preview renderer for components
+ *
+ * Displays a compact preview that shows the component type and key information
+ * without taking up too much screen space during drag operations
+ */
+export function DragPreviewRenderer({
+ component,
+ className,
+}: DragPreviewRendererProps) {
+ const getComponentIcon = (type: string) => {
+ switch (type) {
+ case 'heading':
+ return ;
+ case 'paragraph':
+ return ;
+ case 'button':
+ return ;
+ case 'cards':
+ return ;
+ case 'image':
+ return ;
+ case 'divider':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ const getPreviewText = (component: ComponentInstance): string => {
+ const { type, props } = component;
+
+ switch (type) {
+ case 'heading':
+ return `Heading: "${String(props.content || 'New Heading').slice(0, 30)}${String(props.content || '').length > 30 ? '...' : ''}"`;
+
+ case 'paragraph':
+ return `Paragraph: "${String(props.content || 'New paragraph').slice(0, 35)}${String(props.content || '').length > 35 ? '...' : ''}"`;
+
+ case 'button':
+ return `Button: "${String(props.text || 'Click me')}"`;
+
+ case 'cards':
+ const items = props.items as Array<{ title?: string }> | undefined;
+ const itemCount = items?.length || 0;
+ return `Cards: ${itemCount} item${itemCount !== 1 ? 's' : ''}`;
+
+ case 'image':
+ const alt = String(props.alt || 'Image');
+ return `Image: ${alt}`;
+
+ case 'divider':
+ return `Divider`;
+
+ default:
+ return `Component: ${type}`;
+ }
+ };
+
+ const getComponentTypeName = (type: string): string => {
+ switch (type) {
+ case 'heading':
+ return 'Heading';
+ case 'paragraph':
+ return 'Paragraph';
+ case 'button':
+ return 'Button';
+ case 'cards':
+ return 'Cards';
+ case 'image':
+ return 'Image';
+ case 'divider':
+ return 'Divider';
+ default:
+ return 'Component';
+ }
+ };
+
+ return (
+
+ {/* Component Icon */}
+
+ {getComponentIcon(component.type)}
+
+
+ {/* Component Info */}
+
+
+ {getComponentTypeName(component.type)}
+
+
+ {getPreviewText(component)}
+
+
+
+ );
+}
diff --git a/components/admin/content/home-editor.tsx b/components/admin/content/home-editor.tsx
index ef9ed84e..943feaa5 100644
--- a/components/admin/content/home-editor.tsx
+++ b/components/admin/content/home-editor.tsx
@@ -9,7 +9,6 @@ import {
} from '@components/ui/select';
import type { SupportedLocale } from '@lib/config/language-config';
import { getLanguageInfo } from '@lib/config/language-config';
-import { useTheme } from '@lib/hooks/use-theme';
import { cn } from '@lib/utils';
import { Plus, Trash2 } from 'lucide-react';
@@ -52,7 +51,6 @@ export function HomeEditor({
onTranslationsChange,
onLocaleChange,
}: HomeEditorProps) {
- const { isDark } = useTheme();
const t = useTranslations('pages.admin.content.editor');
const currentTranslation = translations[currentLocale] || {};
@@ -62,13 +60,13 @@ export function HomeEditor({
HomeTranslation
>;
const fieldParts = field.split('.');
- let current = newTranslations[currentLocale] as any;
+ let current = newTranslations[currentLocale] as Record;
for (let i = 0; i < fieldParts.length - 1; i++) {
if (!current[fieldParts[i]]) {
current[fieldParts[i]] = {};
}
- current = current[fieldParts[i]];
+ current = current[fieldParts[i]] as Record;
}
current[fieldParts[fieldParts.length - 1]] = value;
@@ -106,7 +104,7 @@ export function HomeEditor({
@@ -217,7 +205,7 @@ export function HomeEditor({
{t('home.features')}
@@ -226,9 +214,7 @@ export function HomeEditor({
onClick={addFeatureCard}
className={cn(
'rounded p-1 transition-colors',
- isDark
- ? 'bg-stone-700 text-stone-300 hover:bg-stone-600'
- : 'bg-stone-100 text-stone-600 hover:bg-stone-200'
+ 'bg-stone-100 text-stone-600 hover:bg-stone-200 dark:bg-stone-700 dark:text-stone-300 dark:hover:bg-stone-600'
)}
>
@@ -242,14 +228,14 @@ export function HomeEditor({
key={index}
className={cn(
'rounded-lg border p-4',
- isDark ? 'border-stone-600' : 'border-stone-200'
+ 'border-stone-200 dark:border-stone-600'
)}
>
{t('common.cardNumber', { number: index + 1 })}
@@ -258,7 +244,7 @@ export function HomeEditor({
onClick={() => removeFeatureCard(index)}
className={cn(
'rounded p-1 text-red-500 transition-colors',
- isDark ? 'hover:bg-red-900/50' : 'hover:bg-red-100'
+ 'hover:bg-red-100 dark:hover:bg-red-900/50'
)}
>
@@ -275,9 +261,7 @@ export function HomeEditor({
}
className={cn(
'w-full rounded-md border px-3 py-1.5 text-sm',
- isDark
- ? 'border-stone-500 bg-stone-600 text-stone-100'
- : 'border-stone-300 bg-white text-stone-900'
+ 'border-stone-300 bg-white text-stone-900 dark:border-stone-500 dark:bg-stone-600 dark:text-stone-100'
)}
/>
@@ -310,7 +292,7 @@ export function HomeEditor({
{t('home.getStarted')}
@@ -321,9 +303,7 @@ export function HomeEditor({
onChange={e => handleFieldChange('getStarted', e.target.value)}
className={cn(
'w-full rounded-lg border px-3 py-2 text-sm',
- isDark
- ? 'border-stone-600 bg-stone-700 text-stone-100'
- : 'border-stone-300 bg-white text-stone-900'
+ 'border-stone-300 bg-white text-stone-900 dark:border-stone-600 dark:bg-stone-700 dark:text-stone-100'
)}
/>
@@ -331,7 +311,7 @@ export function HomeEditor({
{t('home.learnMore')}
@@ -342,9 +322,7 @@ export function HomeEditor({
onChange={e => handleFieldChange('learnMore', e.target.value)}
className={cn(
'w-full rounded-lg border px-3 py-2 text-sm',
- isDark
- ? 'border-stone-600 bg-stone-700 text-stone-100'
- : 'border-stone-300 bg-white text-stone-900'
+ 'border-stone-300 bg-white text-stone-900 dark:border-stone-600 dark:bg-stone-700 dark:text-stone-100'
)}
/>
@@ -354,7 +332,7 @@ export function HomeEditor({