- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
101 lines
5.0 KiB
TypeScript
101 lines
5.0 KiB
TypeScript
import React from 'react';
|
|
import { __ } from '@/lib/i18n';
|
|
import { cn } from '@/lib/utils';
|
|
import { FileText, Layout, Loader2, Home } from 'lucide-react';
|
|
|
|
import { PageItem } from '../store/usePageEditorStore';
|
|
|
|
interface PageSidebarProps {
|
|
pages: PageItem[];
|
|
selectedPage: PageItem | null;
|
|
onSelectPage: (page: PageItem) => void;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
export function PageSidebar({ pages, selectedPage, onSelectPage, isLoading }: PageSidebarProps) {
|
|
const structuralPages = pages.filter(p => p.type === 'page');
|
|
const templates = pages.filter(p => p.type === 'template' && p.has_template);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="w-60 border-r bg-white flex items-center justify-center">
|
|
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-60 border-r bg-white flex flex-col">
|
|
<div className="flex-1 overflow-y-auto">
|
|
<div className="p-4">
|
|
{/* Structural Pages */}
|
|
<div className="mb-6">
|
|
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 flex items-center gap-2">
|
|
<FileText className="w-3.5 h-3.5" />
|
|
{__('Structural Pages')}
|
|
</h3>
|
|
<div className="space-y-1">
|
|
{structuralPages.length === 0 ? (
|
|
<p className="text-sm text-gray-400 italic">{__('No pages yet')}</p>
|
|
) : (
|
|
structuralPages.map((page) => (
|
|
<button
|
|
key={`page-${page.id}`}
|
|
onClick={() => onSelectPage(page)}
|
|
className={cn(
|
|
'w-full text-left px-3 py-2 rounded-lg text-sm transition-colors flex items-center justify-between group',
|
|
'hover:bg-gray-100',
|
|
selectedPage?.id === page.id && selectedPage?.type === 'page'
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-gray-700'
|
|
)}
|
|
>
|
|
<span className="truncate">{page.title}</span>
|
|
{(page as any).isSpaLanding && (
|
|
<span title="SPA Landing Page" className="flex-shrink-0 ml-2">
|
|
<Home className="w-3.5 h-3.5 text-green-600" />
|
|
</span>
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Templates */}
|
|
<div>
|
|
<h3 className="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 flex items-center gap-2">
|
|
<Layout className="w-3.5 h-3.5" />
|
|
{__('Templates')}
|
|
</h3>
|
|
<div className="space-y-1">
|
|
{templates.length === 0 ? (
|
|
<p className="text-sm text-gray-400 italic">{__('No templates yet')}</p>
|
|
) : (
|
|
templates.map((template) => (
|
|
<button
|
|
key={`template-${template.cpt}`}
|
|
onClick={() => onSelectPage(template)}
|
|
className={cn(
|
|
'w-full text-left px-3 py-2 rounded-lg text-sm transition-colors',
|
|
'hover:bg-gray-100',
|
|
selectedPage?.cpt === template.cpt && selectedPage?.type === 'template'
|
|
? 'bg-primary/10 text-primary font-medium'
|
|
: 'text-gray-700'
|
|
)}
|
|
>
|
|
<span className="block">{template.title}</span>
|
|
{template.permalink_base && (
|
|
<span className="text-xs text-gray-400">{template.permalink_base}*</span>
|
|
)}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|