feat: Page Editor Phase 2 - Admin UI
- 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
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user