- Add AppearancePages component with 3-column layout - Add PageSidebar for listing structural pages and CPT templates - Add SectionEditor with add/delete/reorder functionality - Add PageSettings with layout/color scheme and static/dynamic toggle - Add CreatePageModal for creating new structural pages - Add route at /appearance/pages in admin App.tsx - Build admin-spa successfully
190 lines
7.9 KiB
TypeScript
190 lines
7.9 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { __ } from '@/lib/i18n';
|
|
import { cn } from '@/lib/utils';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/ui/card';
|
|
import {
|
|
Plus, ChevronUp, ChevronDown, Trash2, GripVertical,
|
|
LayoutTemplate, Type, Image, Grid3x3, Megaphone, MessageSquare,
|
|
Loader2
|
|
} from 'lucide-react';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
|
|
interface Section {
|
|
id: string;
|
|
type: string;
|
|
layoutVariant?: string;
|
|
colorScheme?: string;
|
|
props: Record<string, any>;
|
|
}
|
|
|
|
interface SectionEditorProps {
|
|
sections: Section[];
|
|
selectedSection: Section | null;
|
|
onSelectSection: (section: Section | null) => void;
|
|
onAddSection: (type: string) => void;
|
|
onDeleteSection: (id: string) => void;
|
|
onMoveSection: (id: string, direction: 'up' | 'down') => void;
|
|
isTemplate: boolean;
|
|
cpt?: string;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
const SECTION_TYPES = [
|
|
{ type: 'hero', label: 'Hero', icon: LayoutTemplate },
|
|
{ type: 'content', label: 'Content', icon: Type },
|
|
{ type: 'image-text', label: 'Image + Text', icon: Image },
|
|
{ type: 'feature-grid', label: 'Feature Grid', icon: Grid3x3 },
|
|
{ type: 'cta-banner', label: 'CTA Banner', icon: Megaphone },
|
|
{ type: 'contact-form', label: 'Contact Form', icon: MessageSquare },
|
|
];
|
|
|
|
export function SectionEditor({
|
|
sections,
|
|
selectedSection,
|
|
onSelectSection,
|
|
onAddSection,
|
|
onDeleteSection,
|
|
onMoveSection,
|
|
isTemplate,
|
|
cpt,
|
|
isLoading,
|
|
}: SectionEditorProps) {
|
|
if (isLoading) {
|
|
return (
|
|
<div className="h-full flex items-center justify-center">
|
|
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Section Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold">{__('Sections')}</h2>
|
|
{isTemplate && (
|
|
<span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded">
|
|
{__('Template: ')} {cpt}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sections List */}
|
|
<div className="space-y-3">
|
|
{sections.map((section, index) => {
|
|
const sectionType = SECTION_TYPES.find(s => s.type === section.type);
|
|
const Icon = sectionType?.icon || LayoutTemplate;
|
|
const hasDynamic = Object.values(section.props).some(
|
|
p => typeof p === 'object' && p?.type === 'dynamic'
|
|
);
|
|
|
|
return (
|
|
<Card
|
|
key={section.id}
|
|
className={cn(
|
|
'p-4 cursor-pointer transition-all',
|
|
'hover:shadow-md',
|
|
selectedSection?.id === section.id
|
|
? 'ring-2 ring-primary shadow-md'
|
|
: ''
|
|
)}
|
|
onClick={() => onSelectSection(section)}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<GripVertical className="w-4 h-4 text-gray-400 cursor-grab" />
|
|
|
|
<div className="flex-1 flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
|
|
<Icon className="w-5 h-5 text-gray-600" />
|
|
</div>
|
|
<div>
|
|
<div className="font-medium text-sm">
|
|
{sectionType?.label || section.type}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{section.layoutVariant || 'default'}
|
|
{hasDynamic && (
|
|
<span className="ml-2 text-primary">◆ {__('Dynamic')}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1" onClick={e => e.stopPropagation()}>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => onMoveSection(section.id, 'up')}
|
|
disabled={index === 0}
|
|
>
|
|
<ChevronUp className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => onMoveSection(section.id, 'down')}
|
|
disabled={index === sections.length - 1}
|
|
>
|
|
<ChevronDown className="w-4 h-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-8 w-8 text-red-500 hover:text-red-600 hover:bg-red-50"
|
|
onClick={() => {
|
|
if (confirm(__('Delete this section?'))) {
|
|
onDeleteSection(section.id);
|
|
}
|
|
}}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
);
|
|
})}
|
|
|
|
{sections.length === 0 && (
|
|
<div className="text-center py-12 text-gray-400">
|
|
<LayoutTemplate className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
|
<p>{__('No sections yet. Add your first section.')}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Add Section Button */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" className="w-full">
|
|
<Plus className="w-4 h-4 mr-2" />
|
|
{__('Add Section')}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent className="w-56">
|
|
{SECTION_TYPES.map((sectionType) => {
|
|
const Icon = sectionType.icon;
|
|
return (
|
|
<DropdownMenuItem
|
|
key={sectionType.type}
|
|
onClick={() => onAddSection(sectionType.type)}
|
|
>
|
|
<Icon className="w-4 h-4 mr-2" />
|
|
{sectionType.label}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
}
|