ifixkart-admin/app/(admin)/storefront-sections/page.tsx

2678 lines
120 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @page Homepage & Services Layout Section Manager (`app/(admin)/storefront-sections/page.tsx`)
* @purpose Admin CRM console to edit, reorder, upload images, configure nested arrays, and persist dynamic storefront configurations.
*/
'use client';
import { useState, useEffect, useRef } from 'react';
import {
Save,
ChevronDown,
ArrowUp,
ArrowDown,
Eye,
EyeOff,
RefreshCw,
Plus,
Trash2,
Upload,
X,
Calendar,
} from 'lucide-react';
import { toast } from 'sonner';
import { getAccessToken } from '@/services/api/client';
import { CustomSelect } from '@/components/ui/CustomSelect';
import ConfirmDeleteModal from '@/components/ui/ConfirmDeleteModal';
interface HomepageSection {
id: string;
section_key: string;
name: string;
type: string;
page: 'home' | 'services';
is_active: boolean;
display_order: number;
title: string;
subtitle: string;
metadata_json: Record<string, any>;
}
const INITIAL_SECTIONS: HomepageSection[] = [
// Storefront Homepage Sections
{
id: 'sec_hero',
section_key: 'hero_slider',
name: 'Hero Carousel Slider',
type: 'hero',
page: 'home',
is_active: true,
display_order: 1,
title: 'Next-Gen Electronics & OEM Repair Hub',
subtitle: 'Up to 40% Off on Top Smartphones, Laptops & Certified Spare Parts',
metadata_json: {
slides: [
{
id: '1',
badge: 'Limited Time Offer',
title: 'Ultimate Virtual Reality\nExperience Box',
subtitle: 'Experience the Future of Entertainment Today',
image: '',
buttonText: 'Shop Now',
buttonUrl: '/products',
gradient: 'from-blue-600 via-indigo-600 to-purple-650'
},
{
id: '2',
badge: 'Exclusive Launch',
title: 'Next-Gen Performance\nPro Gaming Gear',
subtitle: 'Unleash Your Full Creative Power Everywhere',
image: '',
buttonText: 'Shop Now',
buttonUrl: '/products',
gradient: 'from-slate-900 via-blue-900 to-indigo-950'
}
]
}
},
{
id: 'sec_promo_cards',
section_key: 'promo_cards',
name: 'Promotional Feature Cards',
type: 'promo_cards',
page: 'home',
is_active: true,
display_order: 2,
title: 'Top Category Promos',
subtitle: 'Featured discounts on VR boxes, audio gear, and accessories',
metadata_json: {
cards: [
{
id: 'promo-1',
badge: 'Weekend Discount',
title: 'iPhone Vision',
subtitle: 'Only for this week...',
priceText: 'from $ 239.99',
image: '',
bgColor: 'bg-[#F2EDFD]',
textColor: 'text-[#4A154B]',
badgeBg: 'bg-[#7B2CBF] text-white',
link: '/products'
},
{
id: 'promo-2',
badge: 'Amazing Deal',
title: 'Vibe Your Sound',
subtitle: 'This Week Exclusively...',
priceText: 'from $ 399.99',
image: '',
bgColor: 'bg-[#E3F2FD]',
textColor: 'text-primary-dark',
badgeBg: 'bg-primary text-white',
link: '/products'
},
{
id: 'promo-3',
badge: 'Flat 30% OFF',
title: 'True Harmony',
subtitle: 'Limited Time Only...',
priceText: 'from $ 189.99',
image: '',
bgColor: 'bg-[#FCE4EC]',
textColor: 'text-[#880E4F]',
badgeBg: 'bg-[#EC407A] text-white',
link: '/products'
}
]
}
},
{
id: 'sec_categories',
section_key: 'featured_categories',
name: 'Shop By Categories Grid',
type: 'categories',
page: 'home',
is_active: true,
display_order: 3,
title: 'Shop By Categories',
subtitle: 'Explore high-performance electronics and replacement hardware',
metadata_json: {
categories: [
{ id: 'cat-1', name: 'TV & Speaker', slug: 'tv-speaker', iconName: 'Speaker', image: '', link: '/shop?category=tv-speaker' },
{ id: 'cat-2', name: 'Party Speakers', slug: 'party-speakers', iconName: 'Volume2', image: '', link: '/shop?category=party-speakers' },
{ id: 'cat-3', name: 'Cameras', slug: 'cameras', iconName: 'Camera', image: '', link: '/shop?category=cameras' },
{ id: 'cat-4', name: 'Phones', slug: 'phones', iconName: 'Smartphone', image: '', link: '/shop?category=phones' },
{ id: 'cat-5', name: 'Chargers & Cables', slug: 'chargers-cables', iconName: 'Cable', image: '', link: '/shop?category=chargers-cables' },
{ id: 'cat-6', name: 'Smart Watches', slug: 'smart-watches', iconName: 'Watch', image: '', link: '/shop?category=smart-watches' }
]
}
},
{
id: 'sec_editorial',
section_key: 'editorial_story',
name: 'New Arrivals Products',
type: 'editorial',
page: 'home',
is_active: true,
display_order: 4,
title: 'New Arrivals Products',
subtitle: 'Explore the latest smartphones, accessories, and certified OEM spare parts',
metadata_json: {
items: []
}
},
{
id: 'sec_promo_banners',
section_key: 'promo_banners',
name: 'Dual Promotional Banner Row',
type: 'promo_banners',
page: 'home',
is_active: true,
display_order: 5,
title: 'Featured Banners',
subtitle: 'Spotlight on sound systems and power banks',
metadata_json: {
banners: [
{
title: 'Premium Sound Systems',
subtitle: 'Up to 50% off on premium bluetooth speakers & audio decks',
image: '',
buttonText: 'Browse Audio',
buttonUrl: '/shop?category=tv-speaker'
},
{
title: 'Super-Charged Battery Packs',
subtitle: 'Stay connected with 20000mAh Power Delivery power banks',
image: '',
buttonText: 'Shop Chargers',
buttonUrl: '/shop?category=chargers-cables'
}
]
}
},
{
id: 'sec_deals',
section_key: 'deal_of_the_day',
name: 'Deals Of The Day & Countdown',
type: 'deal',
page: 'home',
is_active: true,
display_order: 6,
title: 'Deals Of The Day',
subtitle: 'Limited-time discounts on certified OEM screens & batteries',
metadata_json: {
countdown_end: '2026-12-31T23:59:59Z',
products: [
{
id: 'deal-1',
title: 'HP LaserJet Mono Single Function Laser Printer',
image: '',
originalPrice: 89,
dealPrice: 85,
link: '/products/hp-laserjet'
},
{
id: 'deal-2',
title: 'Jajot J2 - Feature Phone, Keypad Mobile phone',
image: '',
originalPrice: 2000,
dealPrice: 1500,
link: '/products/jajot-j2'
}
]
}
},
{
id: 'sec_recently',
section_key: 'recently_launched',
name: 'Recently Launched Hardware',
type: 'launched',
page: 'home',
is_active: true,
display_order: 7,
title: 'Recently Launched',
subtitle: 'Latest smartphone accessories & travel gear',
metadata_json: {
items: [
{ id: 'rl-1', title: 'Apple Watch Series 9 GPS + Cellular', price: 669, image: '', link: '/products/apple-watch-s9' },
{ id: 'rl-2', title: 'Ceptics Multi Travel Adapter Plug', price: 300, image: '', link: '/products/ceptics-adapter' },
{ id: 'rl-3', title: "D'Wild USB Type C 100W PD Cable", price: 9, image: '', link: '/products/dwild-cable' }
]
}
},
{
id: 'sec_brands',
section_key: 'official_brands',
name: 'Official Tech Brands Showcase',
type: 'brands',
page: 'home',
is_active: true,
display_order: 8,
title: 'Official Tech Brands',
subtitle: 'Authorized brand partners',
metadata_json: {
brands: [
{ name: 'Apple', logo: 'apple', link: '/shop?brand=apple' },
{ name: 'Samsung', logo: 'samsung', link: '/shop?brand=samsung' },
{ name: 'Sony', logo: 'sony', link: '/shop?brand=sony' },
{ name: 'Dell', logo: 'dell', link: '/shop?brand=dell' },
{ name: 'Bose', logo: 'bose', link: '/shop?brand=bose' }
]
}
},
{
id: 'sec_ultimate',
section_key: 'ultimate_tech',
name: 'Ultimate Tech Gadgets & Banner Grid',
type: 'ultimate_tech',
page: 'home',
is_active: true,
display_order: 9,
title: 'Ultimate Tech Gadgets',
subtitle: 'High-end smart home appliances, wearables, and audio gear',
metadata_json: {
banners: [
{ title: 'Smart Watches', image: '', buttonText: 'Explore Watches', buttonUrl: '/shop?category=smart-watches' },
{ title: 'VR Gaming Headsets', image: '', buttonText: 'VR Gear', buttonUrl: '/shop?category=accessories' }
]
}
},
{
id: 'sec_blog',
section_key: 'tech_blog',
name: 'Tech Blog & Featured Articles',
type: 'blog',
page: 'home',
is_active: true,
display_order: 10,
title: 'From Our Articles',
subtitle: 'Latest tech news, repair guides, and trends',
metadata_json: {
articles: [
{ id: 'b-1', title: 'The Future of Wireless Audio: True Stereo Earbuds', excerpt: 'Explore how audio drivers and high bitrates are redefining our daily listening routines...', date: 'July 10, 2026', author: 'Sarah Connor', readTime: '5 min read', image: '', link: '/blog/wireless-audio' },
{ id: 'b-2', title: 'Top 5 Tech Gadgets Every Student Needs in 2026', excerpt: 'From digital note-taking tablets to portable power delivery adapters...', date: 'July 05, 2026', author: 'James Smith', readTime: '8 min read', image: '', link: '/blog/student-gadgets' }
]
}
},
{
id: 'sec_trust',
section_key: 'trust_badges',
name: 'Service Guarantee & Trust Badges Bar',
type: 'badges',
page: 'home',
is_active: true,
display_order: 11,
title: 'Certified Quality Guarantee',
subtitle: 'Free Shipping, 1-Year Warranty & 24/7 Technical Support',
metadata_json: {
features: [
{ title: '6-Month Spare Parts Warranty', desc: 'Secure protective repair warranties', icon: 'ShieldCheck' },
{ title: 'Skilled Tech Diagnostics', desc: 'Certified technician repairs', icon: 'Wrench' },
{ title: 'Fast Doorstep Repairs', desc: 'In-home diagnostic booking', icon: 'Clock' }
]
}
},
// Header & Navigation Dynamic Sections
{
id: 'sec_announcement_bar',
section_key: 'announcement_bar',
name: 'Announcement Bar Links (Top Bar)',
type: 'announcement_bar',
page: 'home',
is_active: true,
display_order: 0,
title: 'Top Navigation Links',
subtitle: 'Links displayed in the announcement bar above the header',
metadata_json: {
links: [
{ label: 'Track Order', url: '/track-order', icon: 'package' },
{ label: 'About Us', url: '/about' },
{ label: 'Blog', url: '/blog' },
{ label: 'Contact Us', url: '/contact' },
{ label: 'FAQs', url: '/faqs' }
]
}
},
{
id: 'sec_nav_links',
section_key: 'nav_links',
name: 'Main Navigation Links (Navbar)',
type: 'nav_links',
page: 'home',
is_active: true,
display_order: 0,
title: 'Main Navigation Menu',
subtitle: 'Links displayed in the primary navigation bar',
metadata_json: {
links: [
{ label: 'Home', url: '/' },
{ label: 'Shop', url: '/shop', hasDropdown: true },
{ label: 'Service', url: '/repair-services' },
{ label: 'Top Deals', url: '/shop?tag=deals' },
{ label: 'Featured Products', url: '/shop?featured=true' }
]
}
},
// Service E-commerce Booking Page Sections
{
id: 'sec_srv_hero',
section_key: 'service_hero',
name: 'Service Hero Banner',
type: 'service_hero',
page: 'services',
is_active: true,
display_order: 12,
title: 'Mobile phone repair at your doorstep',
subtitle: 'Select your device, get an instant quote, and book skilled technicians to fix your phone screen, battery, or hardware issues at your home or nearest iFixKart store.',
metadata_json: {
badge_tag: 'iFixKart Trusted Repair Partner',
button_text: 'Book Repair Now',
gradient_from: '#00b574',
gradient_to: '#009b62'
}
},
{
id: 'sec_srv_steps',
section_key: 'service_how_it_works',
name: 'Service How It Works (Steps)',
type: 'service_how_it_works',
page: 'services',
is_active: true,
display_order: 13,
title: 'How it works',
subtitle: 'Get your device restored in 3 simple steps',
metadata_json: {
steps: [
{ number: '1', title: 'Check Price', desc: 'Select your phone model and choice of repairs to see your custom instant pricing quote.' },
{ number: '2', title: 'Schedule Service', desc: 'Choose a convenient date and location - your home, office, or our local service workshop.' },
{ number: '3', title: 'Get Device Repaired', desc: 'Our certified technician handles the repair on-site. Test it, pay, and receive your 6-month warranty.' }
]
}
},
{
id: 'sec_srv_why',
section_key: 'service_why_us',
name: 'Service Why Choose Us (Cards)',
type: 'service_why_us',
page: 'services',
is_active: true,
display_order: 14,
title: 'Why Choose iFixKart?',
subtitle: 'Top tier hardware repair guarantees',
metadata_json: {
cards: [
{ title: 'Premium Quality Parts', desc: 'We source only certified top-grade components to guarantee perfect compatibility and longevity.', icon: 'ShieldCheck' },
{ title: 'Instant On-Site Repair', desc: 'No need to leave your phone at a shop for days. Most repairs completed in under 45 minutes.', icon: 'Clock' },
{ title: '6 Months Warranty', desc: 'Every screen and spare part replacement includes our worry-free protection warranty cover.', icon: 'ThumbsUp' }
]
}
},
{
id: 'sec_srv_faq',
section_key: 'service_faq',
name: 'Service FAQ Q&A pairs',
type: 'service_faq',
page: 'services',
is_active: true,
display_order: 15,
title: 'Frequently Asked Questions',
subtitle: 'Got questions? We have answers.',
metadata_json: {
faqs: [
{ question: 'What happens when I place my booking?', answer: 'Our support rep confirms the time slot. A technician is assigned and visits your address to handle the repair on-site.' },
{ question: 'How do I pay for the service?', answer: 'You pay only after the repair is complete and verified. We accept UPI, Cards, Cash, and online payments.' },
{ question: 'Is there a warranty on parts replaced?', answer: 'Yes! All screens and battery replacements include a worry-free 6-month replacement warranty cover.' },
{ question: 'Will my data remain secure?', answer: 'We strictly ensure data security. Repair is conducted in front of you, ensuring zero access to storage components.' }
]
}
}
];
interface SectionImageUploaderProps {
value: string;
onChange: (url: string) => void;
recommendedSize?: string;
}
const SectionImageUploader: React.FC<SectionImageUploaderProps> = ({ value, onChange, recommendedSize }) => {
const [dragActive, setDragActive] = useState(false);
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
await uploadFile(e.dataTransfer.files[0]);
}
};
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
if (e.target.files && e.target.files[0]) {
await uploadFile(e.target.files[0]);
}
};
const uploadFile = async (file: File) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
toast.error('Only JPG, JPEG, PNG, or WebP images are allowed.');
return;
}
const maxSize = 20 * 1024 * 1024;
if (file.size > maxSize) {
toast.error('Image is larger than 20MB.');
return;
}
setUploading(true);
const formData = new FormData();
formData.append('file', file);
const loaderId = toast.loading('Uploading media...');
try {
const token = getAccessToken();
const backendUrl = '';
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${backendUrl}/api/v1/admin/storefront/upload-image`, {
method: 'POST',
headers,
body: formData,
});
if (res.ok) {
const data = await res.json();
onChange(data.image_url);
toast.success('Section image uploaded', { id: loaderId });
} else {
const errData = await res.json().catch(() => ({}));
const msg = errData.detail || 'Image upload failed.';
toast.error(`Upload failed: ${msg}`, { id: loaderId });
}
} catch (err: any) {
toast.error('Network error or CORS issue. Please check console for details.', { id: loaderId });
console.error('File upload error:', err);
} finally {
setUploading(false);
}
};
const handleRemove = (e: React.MouseEvent) => {
e.stopPropagation();
onChange('');
if (fileInputRef.current) fileInputRef.current.value = '';
};
const backendUrl = process.env.NEXT_PUBLIC_API_URL || '';
const displayUrl = value
? value.startsWith('http://') || value.startsWith('https://') || value.startsWith('data:')
? value
: `${backendUrl}${value.startsWith('/') ? value : `/${value}`}`
: '';
return (
<div className="space-y-1" onClick={(e) => e.stopPropagation()}>
<div
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
className={`relative w-full h-32 crm-radius-card border border-dashed flex flex-col items-center justify-center p-3 text-center cursor-pointer transition-colors ${
dragActive
? 'border-primary bg-primary/5'
: 'border-border bg-muted/20 hover:bg-muted/40 hover:border-primary/40'
}`}
>
<input
type="file"
ref={fileInputRef}
onChange={handleChange}
accept="image/*"
className="hidden"
/>
{uploading ? (
<div className="flex flex-col items-center gap-1.5 text-[12px] text-muted-foreground">
<RefreshCw className="w-5 h-5 animate-spin text-primary" />
<span>Uploading...</span>
</div>
) : displayUrl ? (
<div className="relative w-full h-full flex items-center justify-center overflow-hidden crm-radius-icon">
<img src={displayUrl} alt="Preview" className="max-h-full max-w-full object-contain" />
<div className="absolute inset-0 bg-black/40 opacity-0 hover:opacity-100 flex items-center justify-center transition-opacity">
<span className="text-[11px] font-semibold text-white bg-primary px-2 py-1 crm-radius-badge">Change</span>
</div>
<button
type="button"
onClick={handleRemove}
className="absolute top-1 right-1 w-6 h-6 bg-destructive hover:bg-destructive/90 text-white crm-radius-toggle z-10 flex items-center justify-center"
title="Remove image"
>
<X size={12} />
</button>
</div>
) : (
<div className="flex flex-col items-center gap-1 text-muted-foreground">
<Upload size={16} className="text-primary" />
<div className="text-[12px]">
<span className="font-semibold text-primary">Click to upload</span> or drag
</div>
{recommendedSize && (
<div className="text-[11px] text-muted-foreground mt-0.5">
{recommendedSize}
</div>
)}
</div>
)}
</div>
</div>
);
};
interface UppercaseButtonProps {
value: string;
onChange: (val: string) => void;
}
const UppercaseButton: React.FC<UppercaseButtonProps> = ({ value, onChange }) => {
return (
<button
type="button"
onClick={() => onChange((value || '').toUpperCase())}
className="absolute right-2 top-1/2 -translate-y-1/2 h-6 px-1.5 text-[10px] font-semibold uppercase bg-primary/10 text-primary hover:bg-primary hover:text-white crm-radius-badge border border-primary/20 transition-colors z-10 cursor-pointer"
title="Convert to Uppercase"
>
ABC
</button>
);
};
// ─── ProductPicker ────────────────────────────────────────────────────────────
interface ProductPickerProps {
sectionKey: string;
pinnedSlugs: string[];
onUpdate: (slugs: string[]) => void;
label?: string;
}
const ProductPicker: React.FC<ProductPickerProps> = ({ pinnedSlugs, onUpdate, label }) => {
const [query, setQuery] = useState('');
const [results, setResults] = useState<any[]>([]);
const [searching, setSearching] = useState(false);
const [allProducts, setAllProducts] = useState<any[]>([]);
const [loaded, setLoaded] = useState(false);
// Lazy-load full product list on first focus
const loadProducts = async () => {
if (loaded) return;
setSearching(true);
try {
const backendUrl = '';
const res = await fetch(`${backendUrl}/api/v1/catalog/products/all?page=1&limit=200`);
if (res.ok) {
const data = await res.json();
const list = Array.isArray(data) ? data : data?.products || [];
setAllProducts(list);
setResults(list.slice(0, 12));
}
} catch {}
setSearching(false);
setLoaded(true);
};
useEffect(() => {
if (!loaded || allProducts.length === 0) { setResults([]); return; }
const q = query.trim().toLowerCase();
if (!q) { setResults(allProducts.slice(0, 12)); return; }
setResults(allProducts.filter((p: any) =>
(p.name || p.title || '').toLowerCase().includes(q) ||
(p.slug || '').toLowerCase().includes(q)
).slice(0, 20));
}, [query, allProducts, loaded]);
const addProduct = (p: any) => {
const slug = p.slug || p.product_id || p.id || p.name;
if (!slug || pinnedSlugs.includes(slug)) return;
onUpdate([...pinnedSlugs, slug]);
};
const removeProduct = (slug: string) => {
onUpdate(pinnedSlugs.filter((s) => s !== slug));
};
const getDisplayName = (p: any) => p.name || p.title || p.slug || p.product_id || 'Unknown';
const getSlug = (p: any) => p.slug || p.product_id || p.id || p.name;
return (
<div className="space-y-3" onClick={(e) => e.stopPropagation()}>
<h5 className="text-[11px] font-semibold text-muted-foreground uppercase">{label || 'Pinned products'}</h5>
{pinnedSlugs.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{pinnedSlugs.map((slug) => {
const match = allProducts.find((p: any) => getSlug(p) === slug);
const name = match ? getDisplayName(match) : slug;
return (
<span
key={slug}
className="inline-flex items-center gap-1.5 text-[11px] bg-muted text-foreground crm-radius-badge px-2 py-0.5 font-semibold max-w-[220px]"
>
<span className="truncate">{name}</span>
<button
type="button"
onClick={() => removeProduct(slug)}
className="text-muted-foreground hover:text-destructive shrink-0 cursor-pointer"
title="Remove"
>
<X size={10} />
</button>
</span>
);
})}
</div>
)}
<div className="relative">
<input
type="text"
placeholder="Search and add products..."
value={query}
onFocus={loadProducts}
onChange={(e) => setQuery(e.target.value)}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary pr-8"
/>
{searching && (
<RefreshCw size={12} className="absolute right-2 top-1/2 -translate-y-1/2 animate-spin text-primary" />
)}
</div>
{loaded && results.length > 0 && (
<div className="border border-border crm-radius-card bg-card shadow-lg max-h-48 overflow-y-auto divide-y divide-border">
{results.map((p: any) => {
const slug = getSlug(p);
const isPinned = pinnedSlugs.includes(slug);
return (
<button
key={slug}
type="button"
onClick={() => isPinned ? removeProduct(slug) : addProduct(p)}
className={`w-full flex items-center justify-between px-3 py-2 text-left text-[13px] cursor-pointer hover:bg-muted ${
isPinned ? 'text-primary font-semibold' : 'text-foreground'
}`}
>
<span className="truncate max-w-[70%]">{getDisplayName(p)}</span>
<span className="text-[11px] font-medium text-muted-foreground ml-2 shrink-0">
{isPinned ? 'Pinned' : 'Add'}
</span>
</button>
);
})}
</div>
)}
{pinnedSlugs.length === 0 && (
<p className="text-[12px] text-muted-foreground">No products pinned. This section will use catalog products automatically.</p>
)}
</div>
);
};
// ──────────────────────────────────────────────────────────────────────────────
export default function StorefrontSectionsPage() {
const [sections, setSections] = useState<HomepageSection[]>(INITIAL_SECTIONS);
const [activeTab, setActiveTab] = useState<'home' | 'services'>('home');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeEditingId, setActiveEditingId] = useState<string | null>(null);
const [deleteConfirm, setDeleteConfirm] = useState<{
isOpen: boolean;
message: string;
onConfirm: () => void;
}>({
isOpen: false,
message: '',
onConfirm: () => {},
});
const requestDeleteConfirm = (message: string, onConfirm: () => void) => {
setDeleteConfirm({
isOpen: true,
message,
onConfirm,
});
};
const fetchSections = async () => {
setLoading(true);
try {
const token = getAccessToken();
const backendUrl = '';
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${backendUrl}/api/v1/admin/storefront/content/all`, { headers });
if (res.ok) {
const data = await res.json();
if (data && data.length > 0) {
const dbMap = new Map<string, any>();
data.forEach((d: any) => {
const key = d.metadata?.section_key || d.region;
dbMap.set(key, d);
});
const merged = INITIAL_SECTIONS.map((initSec, idx) => {
const dbSec = dbMap.get(initSec.section_key);
if (dbSec) {
return {
id: dbSec.content_id,
section_key: initSec.section_key,
name: initSec.name,
type: dbSec.type,
page: (initSec.page || (initSec.section_key.startsWith('service_') ? 'services' : 'home')) as any,
is_active: typeof dbSec.metadata_json?.is_active === 'boolean' ? dbSec.metadata_json.is_active : true,
display_order: dbSec.display_order || (idx + 1),
title: dbSec.title || initSec.title,
subtitle: dbSec.subtitle || initSec.subtitle,
metadata_json: {
...initSec.metadata_json,
...dbSec.metadata_json,
}
};
}
return {
...initSec,
display_order: idx + 1
};
});
// Sort by display order
merged.sort((a, b) => a.display_order - b.display_order);
setSections(merged);
}
}
} catch {
toast.error('Failed to load live configurations. Using default templates.');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSections();
}, []);
const moveSection = (index: number, direction: 'up' | 'down') => {
const filtered = sections.filter((s) => (s.page || (s.section_key.startsWith('service_') ? 'services' : 'home')) === activeTab);
const targetIndex = direction === 'up' ? index - 1 : index + 1;
if (targetIndex < 0 || targetIndex >= filtered.length) return;
const item1 = filtered[index];
const item2 = filtered[targetIndex];
const updated = sections.map((s) => {
if (s.id === item1.id) return { ...s, display_order: item2.display_order };
if (s.id === item2.id) return { ...s, display_order: item1.display_order };
return s;
});
updated.sort((a, b) => a.display_order - b.display_order);
setSections(updated);
toast.success('Section order updated. Click Save & publish to apply.');
};
const toggleActive = async (id: string) => {
const updated = sections.map((s) => {
if (s.id === id) {
const next = !s.is_active;
toast.success(`${s.name} is now ${next ? 'visible on the storefront' : 'hidden on the storefront'}`);
return { ...s, is_active: next };
}
return s;
});
setSections(updated);
await saveSectionsToBackend(updated);
};
const saveSectionsToBackend = async (sectionsToSave: HomepageSection[]) => {
setSaving(true);
try {
const payload = {
items: sectionsToSave.map((s) => ({
content_id: s.id.startsWith('sec_') ? null : s.id,
page: s.page || (s.section_key.startsWith('service_') ? 'services' : 'home'),
region: s.section_key,
type: s.type,
title: s.title,
subtitle: s.subtitle,
display_order: s.display_order,
metadata_json: {
...s.metadata_json,
section_key: s.section_key,
is_active: s.is_active
}
}))
};
const token = getAccessToken();
const backendUrl = '';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${backendUrl}/api/v1/admin/storefront/content/bulk-save`, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});
if (res.ok) {
toast.success('Layout published to the storefront');
await fetchSections();
} else {
toast.error('Could not save the layout');
}
} catch {
toast.error('Could not reach the server to save the layout');
} finally {
setSaving(false);
}
};
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>, callback: (url: string) => void) => {
if (!e.target.files?.[0]) return;
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
const loaderId = toast.loading('Uploading image...');
try {
const token = getAccessToken();
const backendUrl = '';
const headers: Record<string, string> = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${backendUrl}/api/v1/admin/storefront/upload-image`, {
method: 'POST',
headers,
body: formData,
});
if (res.ok) {
const data = await res.json();
callback(data.image_url);
toast.success('Section image uploaded', { id: loaderId });
} else {
const errData = await res.json().catch(() => ({}));
const msg = errData.detail || 'Image upload failed.';
toast.error(`Upload failed: ${msg}`, { id: loaderId });
}
} catch {
toast.error('Network error uploading image.', { id: loaderId });
}
};
const updateMetadataField = (sectionKey: string, field: string, value: any) => {
setSections((prev) =>
prev.map((s) => {
if (s.section_key === sectionKey) {
return {
...s,
metadata_json: {
...s.metadata_json,
[field]: value
}
};
}
return s;
})
);
};
// Render Section Field Editors dynamically based on layout type
const renderSectionFields = (sec: HomepageSection) => {
const meta = sec.metadata_json || {};
switch (sec.section_key) {
case 'hero_slider': {
const slides = meta.slides || [];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Swiper Slide Cards</h5>
<button
type="button"
onClick={() => {
const newSlides = [
...slides,
{
id: String(Date.now()),
badge: '',
title: '',
subtitle: '',
image: '',
buttonText: '',
buttonUrl: ''
}
];
updateMetadataField(sec.section_key, 'slides', newSlides);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Slide Card
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{slides.map((slide: any, idx: number) => (
<div key={slide.id || idx} className="p-4 bg-card border border-border crm-radius-card space-y-3 relative text-[13px]">
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this slide card?", () => {
const newSlides = slides.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'slides', newSlides);
});
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Slide {idx + 1}</span>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Promo Tag / Badge</label>
<div className="relative">
<input
type="text"
placeholder="e.g. SUPER BUNDLE PROMO"
value={slide.badge || ''}
onChange={(e) => {
const updated = [...slides];
updated[idx].badge = e.target.value;
updateMetadataField(sec.section_key, 'slides', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary pr-10"
/>
<UppercaseButton
value={slide.badge || ''}
onChange={(val) => {
const updated = [...slides];
updated[idx].badge = val;
updateMetadataField(sec.section_key, 'slides', updated);
}}
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Button Text</label>
<input
type="text"
placeholder="e.g. Shop Now"
value={slide.buttonText || ''}
onChange={(e) => {
const updated = [...slides];
updated[idx].buttonText = e.target.value;
updateMetadataField(sec.section_key, 'slides', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Heading Title</label>
<div className="relative">
<textarea
placeholder="e.g. CUSTOMIZE YOUR HERO SLIDE TITLE HERE"
value={slide.title || ''}
onChange={(e) => {
const updated = [...slides];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'slides', updated);
}}
rows={2}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none pr-10"
/>
<UppercaseButton
value={slide.title || ''}
onChange={(val) => {
const updated = [...slides];
updated[idx].title = val;
updateMetadataField(sec.section_key, 'slides', updated);
}}
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Descriptor Subtitle</label>
<input
type="text"
placeholder="e.g. Add descriptive slide context wording..."
value={slide.subtitle || ''}
onChange={(e) => {
const updated = [...slides];
updated[idx].subtitle = e.target.value;
updateMetadataField(sec.section_key, 'slides', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Redirection Link URL</label>
<input
type="text"
placeholder="e.g. /products"
value={slide.buttonUrl || ''}
onChange={(e) => {
const updated = [...slides];
updated[idx].buttonUrl = e.target.value;
updateMetadataField(sec.section_key, 'slides', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Background Image</label>
<SectionImageUploader
value={slide.image || ''}
onChange={(url) => {
const updated = [...slides];
updated[idx].image = url;
updateMetadataField(sec.section_key, 'slides', updated);
}}
recommendedSize="1200 × 768 px"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'promo_cards': {
const cards = meta.cards || [];
// Ensure default cards list if empty
const activeCards = cards.length > 0 ? cards : [
{ id: 'card-0', badge: 'Weekend Discount', title: 'iPhone Vision', subtitle: 'Only for this week...', priceText: 'from $ 239.99', image: '', bgColor: 'bg-slate-800', link: '#' },
{ id: 'card-1', badge: 'Amazing Deal', title: 'Vibe Your Sound', subtitle: 'This Week Exclusively...', priceText: 'from $ 399.99', image: '', bgColor: 'bg-slate-800', link: '#' },
{ id: 'card-2', badge: 'Flat 30% OFF', title: 'True Harmony', subtitle: 'Limited Time Only...', priceText: 'from $ 189.99', image: '', bgColor: 'bg-slate-800', link: '#' }
];
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Feature banners (2–3)</h5>
{activeCards.length < 3 && (
<button
type="button"
onClick={() => {
const updated = [
...activeCards,
{
id: `card-${Date.now()}`,
badge: '',
title: '',
subtitle: '',
priceText: '',
image: '',
bgColor: 'bg-slate-800',
link: '#'
}
];
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Promo Card
</button>
)}
</div>
<div className={`grid grid-cols-1 gap-4 ${
activeCards.length === 2 ? 'md:grid-cols-2' : 'md:grid-cols-3'
}`}>
{activeCards.map((card: any, idx: number) => {
return (
<div key={card.id || idx} className="p-4 bg-card border border-border crm-radius-card space-y-3 relative text-[13px]">
{activeCards.length > 2 && (
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this promotional card? The remaining 2 cards will automatically resize to fill the page layout.", () => {
const updated = activeCards.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'cards', updated);
});
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
title="Delete Card"
>
<Trash2 size={14} />
</button>
)}
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Card {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Badge Promo Text</label>
<div className="relative">
<input
type="text"
placeholder="e.g. WEEKEND SALE"
value={card.badge || ''}
onChange={(e) => {
const updated = [...activeCards];
updated[idx] = { ...card, badge: e.target.value };
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary pr-10"
/>
<UppercaseButton
value={card.badge || ''}
onChange={(val) => {
const updated = [...activeCards];
updated[idx] = { ...card, badge: val };
updateMetadataField(sec.section_key, 'cards', updated);
}}
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Title Heading</label>
<div className="relative">
<input
type="text"
placeholder="e.g. SMARTPHONE REPAIRS"
value={card.title || ''}
onChange={(e) => {
const updated = [...activeCards];
updated[idx] = { ...card, title: e.target.value };
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary pr-10"
/>
<UppercaseButton
value={card.title || ''}
onChange={(val) => {
const updated = [...activeCards];
updated[idx] = { ...card, title: val };
updateMetadataField(sec.section_key, 'cards', updated);
}}
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Descriptor Subtitle</label>
<input
type="text"
placeholder="e.g. High quality OEM screen and battery replacements..."
value={card.subtitle || ''}
onChange={(e) => {
const updated = [...activeCards];
updated[idx] = { ...card, subtitle: e.target.value };
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Price Display Text</label>
<input
type="text"
placeholder="e.g. starting at $99"
value={card.priceText || ''}
onChange={(e) => {
const updated = [...activeCards];
updated[idx] = { ...card, priceText: e.target.value };
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Button Link URL</label>
<input
type="text"
placeholder="e.g. /services"
value={card.link || ''}
onChange={(e) => {
const updated = [...activeCards];
updated[idx] = { ...card, link: e.target.value };
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Device Image Asset</label>
<SectionImageUploader
value={card.image || ''}
onChange={(url) => {
const updated = [...activeCards];
updated[idx] = { ...card, image: url };
updateMetadataField(sec.section_key, 'cards', updated);
}}
recommendedSize="600 × 400 px"
/>
</div>
</div>
);
})}
</div>
</div>
);
}
case 'featured_categories': {
return (
<div className="p-4 bg-muted/20 border border-border crm-radius-card text-[13px] space-y-2">
<h5 className="text-[13px] font-semibold text-foreground">Shop by categories</h5>
<p className="text-muted-foreground">
This section dynamically displays categories pulled directly from your active e-commerce categories registry.
</p>
<div className="text-muted-foreground text-[11px] pt-1">
To add, remove, or modify categories in this list, please manage them under the <strong className="text-foreground">Catalog Management &gt; Categories</strong> menu on the sidebar.
</div>
</div>
);
}
case 'editorial_story': {
const pinnedSlugs = meta.pinned_product_slugs || [];
return (
<div className="space-y-4 text-xs">
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<ProductPicker
sectionKey={sec.section_key}
pinnedSlugs={pinnedSlugs}
onUpdate={(slugs) => updateMetadataField(sec.section_key, 'pinned_product_slugs', slugs)}
label="Pin Specific Products (New Arrivals)"
/>
</div>
<div className="p-3 bg-muted/20 border border-border crm-radius-card text-[13px] text-muted-foreground">
💡 When no products are pinned, this section auto-displays the latest 8 products from your catalog.
</div>
</div>
);
}
case 'promo_banners': {
const banners = meta.banners || [];
return (
<div className="space-y-4 text-xs">
<h5 className="text-[13px] font-semibold text-foreground">Dual Promotional Banner Info</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[0, 1].map((idx) => {
const banner = banners[idx] || { title: '', subtitle: '', image: '', buttonText: 'Explore', buttonUrl: '#' };
return (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Banner Box {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Heading Title</label>
<input
type="text"
value={banner.title || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Sub-heading descriptor</label>
<input
type="text"
value={banner.subtitle || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].subtitle = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Button Label</label>
<input
type="text"
value={banner.buttonText || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].buttonText = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Target URL Link</label>
<input
type="text"
value={banner.buttonUrl || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].buttonUrl = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Background Banner Image</label>
<SectionImageUploader
value={banner.image || ''}
onChange={(url) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].image = url;
updateMetadataField(sec.section_key, 'banners', updated);
}}
recommendedSize="800 × 300 px"
/>
</div>
</div>
);
})}
</div>
</div>
);
}
case 'service_hero': {
return (
<div className="space-y-4 text-xs">
<h5 className="text-[13px] font-semibold text-foreground">Service Page Hero Banner Wording & Aesthetics</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Text Elements</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Badge Top Tag</label>
<input
type="text"
value={meta.badge_tag || ''}
onChange={(e) => updateMetadataField(sec.section_key, 'badge_tag', e.target.value)}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Main Hero Title</label>
<input
type="text"
value={sec.title || ''}
onChange={(e) => {
const val = e.target.value;
setSections((prev) => prev.map((s) => (s.section_key === sec.section_key ? { ...s, title: val } : s)));
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Hero Subtitle</label>
<textarea
value={sec.subtitle || ''}
onChange={(e) => {
const val = e.target.value;
setSections((prev) => prev.map((s) => (s.section_key === sec.section_key ? { ...s, subtitle: val } : s)));
}}
rows={3}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Design & Action Controls</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Button Text</label>
<input
type="text"
value={meta.button_text || ''}
onChange={(e) => updateMetadataField(sec.section_key, 'button_text', e.target.value)}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Featured Image (Optional)</label>
<SectionImageUploader
value={meta.image_url || ''}
onChange={(url) => updateMetadataField(sec.section_key, 'image_url', url)}
recommendedSize="800 × 500 px"
/>
</div>
</div>
</div>
</div>
);
}
case 'service_how_it_works': {
const steps = meta.steps || [];
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Repair Workflow Steps</h5>
<button
type="button"
onClick={() => {
const updated = [
...steps,
{ number: String(steps.length + 1), title: 'Step Title', desc: 'Step detail description...' }
];
updateMetadataField(sec.section_key, 'steps', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Step Card
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{steps.map((step: any, idx: number) => (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
const updated = steps.filter((_: any, i: number) => i !== idx).map((s: any, i: number) => ({ ...s, number: String(i + 1) }));
updateMetadataField(sec.section_key, 'steps', updated);
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Step {step.number}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Step Title</label>
<input
type="text"
value={step.title || ''}
onChange={(e) => {
const updated = [...steps];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'steps', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Detailed Description</label>
<textarea
value={step.desc || ''}
onChange={(e) => {
const updated = [...steps];
updated[idx].desc = e.target.value;
updateMetadataField(sec.section_key, 'steps', updated);
}}
rows={3}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'service_why_us': {
const cards = meta.cards || [];
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Why Choose Us Feature Cards</h5>
<button
type="button"
onClick={() => {
const updated = [
...cards,
{ title: 'New Guarantee', desc: 'Details about quality assurances...', icon: 'ShieldCheck' }
];
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Guarantee Box
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{cards.map((card: any, idx: number) => (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
const updated = cards.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Card {idx + 1}</span>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Card Heading Title</label>
<input
type="text"
value={card.title || ''}
onChange={(e) => {
const updated = [...cards];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'cards', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Lucide Icon Name</label>
<CustomSelect
value={card.icon || 'ShieldCheck'}
onChange={(value) => {
const updated = [...cards];
updated[idx].icon = value;
updateMetadataField(sec.section_key, 'cards', updated);
}}
options={[
{ value: 'ShieldCheck', label: 'ShieldCheck' },
{ value: 'Clock', label: 'Clock' },
{ value: 'ThumbsUp', label: 'ThumbsUp' },
{ value: 'Wrench', label: 'Wrench' },
{ value: 'Award', label: 'Award' },
{ value: 'Heart', label: 'Heart' },
]}
className="w-full"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Descriptor Text</label>
<textarea
value={card.desc || ''}
onChange={(e) => {
const updated = [...cards];
updated[idx].desc = e.target.value;
updateMetadataField(sec.section_key, 'cards', updated);
}}
rows={3}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'service_faq': {
const faqs = meta.faqs || [];
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Frequently Asked Questions List</h5>
<button
type="button"
onClick={() => {
const updated = [
...faqs,
{ question: 'New FAQ Question?', answer: 'Answer details...' }
];
updateMetadataField(sec.section_key, 'faqs', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add FAQ Pair
</button>
</div>
<div className="space-y-3">
{faqs.map((faq: any, idx: number) => (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
const updated = faqs.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'faqs', updated);
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Q&A Pair {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Question Wording</label>
<input
type="text"
value={faq.question || ''}
onChange={(e) => {
const updated = [...faqs];
updated[idx].question = e.target.value;
updateMetadataField(sec.section_key, 'faqs', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Answer Description</label>
<textarea
value={faq.answer || ''}
onChange={(e) => {
const updated = [...faqs];
updated[idx].answer = e.target.value;
updateMetadataField(sec.section_key, 'faqs', updated);
}}
rows={2}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'deal_of_the_day': {
const countdown_end = meta.countdown_end || '2026-12-31T23:59:59Z';
const pinnedDealSlugs = meta.pinned_product_slugs || [];
const dealProducts = meta.products || [];
const isoToDatetimeLocal = (isoStr?: string): string => {
if (!isoStr) return '';
try {
const date = new Date(isoStr);
if (isNaN(date.getTime())) return '';
const pad = (num: number) => String(num).padStart(2, '0');
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
} catch {
return '';
}
};
return (
<div className="space-y-4 text-xs">
{/* Countdown Timer with Calendar Picker */}
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<div className="flex items-center justify-between flex-wrap gap-2">
<span className="text-[11px] font-semibold text-muted-foreground uppercase flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-primary" /> Countdown Timer
</span>
{countdown_end && (
<span className="text-[11px] font-mono text-muted-foreground bg-card border border-border px-2 py-0.5 rounded">
UTC ISO: {countdown_end}
</span>
)}
</div>
<div className="max-w-md">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">
Select Deal End Date & Time
</label>
<input
type="datetime-local"
value={isoToDatetimeLocal(countdown_end)}
onChange={(e) => {
const val = e.target.value;
if (!val) return;
try {
const iso = new Date(val).toISOString();
updateMetadataField(sec.section_key, 'countdown_end', iso);
} catch {
/* ignore */
}
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-sans cursor-pointer [&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-70 hover:[&::-webkit-calendar-picker-indicator]:opacity-100"
/>
</div>
</div>
{/* Product Picker — Pinned from catalog */}
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<ProductPicker
sectionKey={sec.section_key}
pinnedSlugs={pinnedDealSlugs}
onUpdate={(slugs) => updateMetadataField(sec.section_key, 'pinned_product_slugs', slugs)}
label="Pin Deal Products from Catalog"
/>
<p className="text-[10px] text-muted-foreground italic">Pinned catalog products appear as the featured hot deal. If none pinned, a deal product is picked automatically.</p>
</div>
{/* Manual Override Products */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<h5 className="text-[11px] font-semibold text-muted-foreground uppercase">Manual deal products</h5>
<button
type="button"
onClick={() => {
const updated = [
...dealProducts,
{ id: `deal-${Date.now()}`, title: '', image: '', originalPrice: 0, dealPrice: 0, link: '' }
];
updateMetadataField(sec.section_key, 'products', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Deal Item
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{dealProducts.map((p: any, idx: number) => (
<div key={p.id || idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this deal item?", () => {
const updated = dealProducts.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'products', updated);
});
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
title="Delete Item"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Deal Product {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Title / Label</label>
<input
type="text"
placeholder="e.g. HP LaserJet Mono Printer"
value={p.title || ''}
onChange={(e) => {
const updated = [...dealProducts];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'products', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Original Price (₹)</label>
<input
type="number"
placeholder="e.g. 199"
value={p.originalPrice || ''}
onChange={(e) => {
const updated = [...dealProducts];
updated[idx].originalPrice = Number(e.target.value);
updateMetadataField(sec.section_key, 'products', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Deal Price (₹)</label>
<input
type="number"
placeholder="e.g. 149"
value={p.dealPrice || ''}
onChange={(e) => {
const updated = [...dealProducts];
updated[idx].dealPrice = Number(e.target.value);
updateMetadataField(sec.section_key, 'products', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Redirect URL / Link</label>
<input
type="text"
placeholder="e.g. /products/hp-laserjet"
value={p.link || ''}
onChange={(e) => {
const updated = [...dealProducts];
updated[idx].link = e.target.value;
updateMetadataField(sec.section_key, 'products', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Product Image</label>
<SectionImageUploader
value={p.image || ''}
onChange={(url) => {
const updated = [...dealProducts];
updated[idx].image = url;
updateMetadataField(sec.section_key, 'products', updated);
}}
recommendedSize="400 × 400 px"
/>
</div>
</div>
))}
</div>
</div>
</div>
);
}
case 'recently_launched': {
const pinnedRLSlugs = meta.pinned_product_slugs || [];
return (
<div className="space-y-4 text-xs">
{/* Side Banner Customization */}
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Left Feature Banner</span>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Title</label>
<input
type="text"
placeholder="e.g. iPhone Prodigy"
value={meta.banner_title || ''}
onChange={(e) => updateMetadataField(sec.section_key, 'banner_title', e.target.value)}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Subtitle</label>
<input
type="text"
placeholder="e.g. Performance in Motion"
value={meta.banner_subtitle || ''}
onChange={(e) => updateMetadataField(sec.section_key, 'banner_subtitle', e.target.value)}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Link URL</label>
<input
type="text"
placeholder="e.g. /products/iphone"
value={meta.banner_link || ''}
onChange={(e) => updateMetadataField(sec.section_key, 'banner_link', e.target.value)}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-mono"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Background Image</label>
<SectionImageUploader
value={meta.banner_image || ''}
onChange={(url) => updateMetadataField(sec.section_key, 'banner_image', url)}
recommendedSize="400 × 500 px"
/>
</div>
</div>
{/* Product Picker */}
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<ProductPicker
sectionKey={sec.section_key}
pinnedSlugs={pinnedRLSlugs}
onUpdate={(slugs) => updateMetadataField(sec.section_key, 'pinned_product_slugs', slugs)}
label="Pin Products for Carousel (Recently Launched)"
/>
<p className="text-[10px] text-muted-foreground italic">When none are pinned, recently-launched catalog products auto-fill this carousel.</p>
</div>
</div>
);
}
case 'official_brands': {
const brands = meta.brands || [];
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Tech Brands Grid Showcase</h5>
<button
type="button"
onClick={() => {
const updated = [
...brands,
{ name: '', logo: '', link: '' }
];
updateMetadataField(sec.section_key, 'brands', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Brand Logo
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-3">
{brands.map((b: any, idx: number) => (
<div key={idx} className="p-3 bg-muted/20 border border-border crm-radius-card space-y-2 relative flex flex-col justify-between min-h-[220px]">
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this brand?", () => {
const updated = brands.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'brands', updated);
});
}}
className="absolute top-1.5 right-1.5 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
title="Delete Brand"
>
<Trash2 size={12} />
</button>
<div className="w-full">
<SectionImageUploader
value={b.logo || ''}
onChange={(url) => {
const updated = [...brands];
updated[idx].logo = url;
updateMetadataField(sec.section_key, 'brands', updated);
}}
recommendedSize="200 × 80 px"
/>
</div>
<div className="space-y-1">
<input
type="text"
placeholder="Brand Name"
value={b.name || ''}
onChange={(e) => {
const updated = [...brands];
updated[idx].name = e.target.value;
updateMetadataField(sec.section_key, 'brands', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary"
/>
<input
type="text"
placeholder="Redirect URL"
value={b.link || ''}
onChange={(e) => {
const updated = [...brands];
updated[idx].link = e.target.value;
updateMetadataField(sec.section_key, 'brands', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'ultimate_tech': {
const banners = meta.banners || [];
return (
<div className="space-y-4 text-xs">
<h5 className="text-[13px] font-semibold text-foreground">Dual Banner Highlight Grid</h5>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[0, 1].map((idx) => {
const banner = banners[idx] || { title: '', image: '', buttonText: 'Explore', buttonUrl: '#' };
return (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Banner Box {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Banner Heading Title</label>
<input
type="text"
placeholder="e.g. Premium VR Headsets"
value={banner.title || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Button Text</label>
<input
type="text"
placeholder="e.g. Explore"
value={banner.buttonText || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].buttonText = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Redirect URL Link</label>
<input
type="text"
placeholder="e.g. /shop"
value={banner.buttonUrl || ''}
onChange={(e) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].buttonUrl = e.target.value;
updateMetadataField(sec.section_key, 'banners', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-mono"
/>
</div>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Background Highlight Image</label>
<SectionImageUploader
value={banner.image || ''}
onChange={(url) => {
const updated = [...banners];
if (!updated[idx]) updated[idx] = banner;
updated[idx].image = url;
updateMetadataField(sec.section_key, 'banners', updated);
}}
recommendedSize="800 × 400 px"
/>
</div>
</div>
);
})}
</div>
</div>
);
}
case 'tech_blog': {
const articles = meta.articles || [];
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Featured Blog Articles</h5>
<button
type="button"
onClick={() => {
const updated = [
...articles,
{ id: `b-${Date.now()}`, title: '', excerpt: '', date: 'August 2026', author: 'Staff Reporter', readTime: '5 min read', image: '', link: '' }
];
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Article Card
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{articles.map((art: any, idx: number) => (
<div key={art.id || idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this article?", () => {
const updated = articles.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'articles', updated);
});
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
title="Delete Article"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Article Card {idx + 1}</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Article Title</label>
<input
type="text"
placeholder="e.g. Swapping a phone battery safely"
value={art.title || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Short Excerpt</label>
<textarea
placeholder="Enter article synopsis..."
value={art.excerpt || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].excerpt = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
rows={2}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Publish Date</label>
<input
type="text"
placeholder="e.g. July 2026"
value={art.date || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].date = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Author</label>
<input
type="text"
placeholder="e.g. John Smith"
value={art.author || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].author = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Read Time</label>
<input
type="text"
placeholder="e.g. 5 min read"
value={art.readTime || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].readTime = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Article Path</label>
<input
type="text"
placeholder="e.g. /blog/repair-battery"
value={art.link || ''}
onChange={(e) => {
const updated = [...articles];
updated[idx].link = e.target.value;
updateMetadataField(sec.section_key, 'articles', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-mono"
/>
</div>
<div className="space-y-1">
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Featured Image</label>
<SectionImageUploader
value={art.image || ''}
onChange={(url) => {
const updated = [...articles];
updated[idx].image = url;
updateMetadataField(sec.section_key, 'articles', updated);
}}
recommendedSize="600 × 400 px"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'badges':
case 'trust_badges': {
const features = meta.features || [];
return (
<div className="space-y-4 text-xs">
{/* Section Header Customization */}
<div className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3">
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Section Header Text</span>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Section Title</label>
<input
type="text"
placeholder="e.g. Certified Quality Guarantee"
value={sec.title || ''}
onChange={(e) => {
const val = e.target.value;
setSections((prev) => prev.map((s) => (s.section_key === sec.section_key ? { ...s, title: val } : s)));
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Section Subtitle</label>
<input
type="text"
placeholder="e.g. Free Shipping, 1-Year Warranty & 24/7 Technical Support"
value={sec.subtitle || ''}
onChange={(e) => {
const val = e.target.value;
setSections((prev) => prev.map((s) => (s.section_key === sec.section_key ? { ...s, subtitle: val } : s)));
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary"
/>
</div>
</div>
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">Trust Badge Feature Items</h5>
<button
type="button"
onClick={() => {
const updated = [
...features,
{ title: '', desc: '', icon: 'ShieldCheck' }
];
updateMetadataField(sec.section_key, 'features', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Badge Pair
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{features.map((feat: any, idx: number) => (
<div key={idx} className="p-4 bg-muted/20 border border-border crm-radius-card space-y-3 relative">
<button
type="button"
onClick={() => {
requestDeleteConfirm("Are you sure you want to delete this trust badge?", () => {
const updated = features.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'features', updated);
});
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
title="Delete Badge"
>
<Trash2 size={14} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Trust Badge {idx + 1}</span>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Badge Title</label>
<input
type="text"
placeholder="e.g. 6-Month Warranty"
value={feat.title || ''}
onChange={(e) => {
const updated = [...features];
updated[idx].title = e.target.value;
updateMetadataField(sec.section_key, 'features', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-bold"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Icon Style</label>
<CustomSelect
value={feat.icon || 'ShieldCheck'}
onChange={(value) => {
const updated = [...features];
updated[idx].icon = value;
updateMetadataField(sec.section_key, 'features', updated);
}}
options={[
{ value: 'ShieldCheck', label: 'ShieldCheck' },
{ value: 'Clock', label: 'Clock' },
{ value: 'ThumbsUp', label: 'ThumbsUp' },
{ value: 'Wrench', label: 'Wrench' },
{ value: 'Award', label: 'Award' },
{ value: 'Truck', label: 'Truck' },
{ value: 'Heart', label: 'Heart' },
]}
className="w-full"
/>
</div>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Description Text</label>
<textarea
placeholder="e.g. Warranty details here..."
value={feat.desc || ''}
onChange={(e) => {
const updated = [...features];
updated[idx].desc = e.target.value;
updateMetadataField(sec.section_key, 'features', updated);
}}
rows={2}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
</div>
))}
</div>
</div>
);
}
case 'announcement_bar':
case 'nav_links': {
const links = meta.links || [];
const isNav = sec.section_key === 'nav_links';
return (
<div className="space-y-4 text-xs">
<div className="flex justify-between items-center">
<h5 className="text-[13px] font-semibold text-foreground">
{isNav ? 'Main Navigation Links' : 'Top Bar Links'}
</h5>
<button
type="button"
onClick={() => {
const updated = [
...links,
{ label: 'New Link', url: '/', ...(isNav ? { hasDropdown: false } : {}) }
];
updateMetadataField(sec.section_key, 'links', updated);
}}
className="inline-flex items-center gap-1.5 h-8 px-3 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[12px] font-semibold cursor-pointer"
>
<Plus size={12} /> Add Link
</button>
</div>
<div className="space-y-3">
{links.map((link: any, idx: number) => (
<div key={idx} className="p-3 bg-muted/20 border border-border crm-radius-card space-y-2 relative">
<button
type="button"
onClick={() => {
const updated = links.filter((_: any, i: number) => i !== idx);
updateMetadataField(sec.section_key, 'links', updated);
}}
className="absolute top-2 right-2 w-7 h-7 crm-radius-toggle text-muted-foreground hover:text-destructive hover:bg-muted z-10 flex items-center justify-center cursor-pointer"
>
<Trash2 size={12} />
</button>
<span className="text-[11px] font-semibold text-muted-foreground uppercase">Link {idx + 1}</span>
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">Label</label>
<input
type="text"
value={link.label || ''}
onChange={(e) => {
const updated = [...links];
updated[idx] = { ...updated[idx], label: e.target.value };
updateMetadataField(sec.section_key, 'links', updated);
}}
className="w-full px-3 py-2 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary resize-none"
/>
</div>
<div>
<label className="block text-[11px] font-semibold text-muted-foreground uppercase mb-1.5">URL / Path</label>
<input
type="text"
value={link.url || ''}
onChange={(e) => {
const updated = [...links];
updated[idx] = { ...updated[idx], url: e.target.value };
updateMetadataField(sec.section_key, 'links', updated);
}}
className="w-full h-9 px-3 bg-card border border-border crm-radius-control text-[13px] text-foreground focus:outline-none focus:border-primary font-mono"
placeholder="/shop?tag=deals"
/>
</div>
</div>
{isNav && (
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
type="checkbox"
checked={!!link.hasDropdown}
onChange={(e) => {
const updated = [...links];
updated[idx] = { ...updated[idx], hasDropdown: e.target.checked };
updateMetadataField(sec.section_key, 'links', updated);
}}
className="accent-primary"
/>
<span className="text-muted-foreground">Show categories dropdown on hover</span>
</label>
)}
</div>
))}
</div>
</div>
);
}
default:
return (
<div className="py-2 text-[13px] text-muted-foreground">
Customize layout settings directly under the main fields. Use custom metadata parameters where available.
</div>
);
}
};
const visibleSections = sections.filter(
(sec) => (sec.page || (sec.section_key.startsWith('service_') ? 'services' : 'home')) === activeTab
);
const homeCount = sections.filter(
(s) => (s.page || (s.section_key.startsWith('service_') ? 'services' : 'home')) === 'home'
).length;
const servicesCount = sections.filter(
(s) => (s.page || (s.section_key.startsWith('service_') ? 'services' : 'home')) === 'services'
).length;
const dataCardShell =
'crm-radius-section border border-border bg-card shadow-[0_1px_3px_rgba(16,24,40,0.06)] overflow-hidden min-w-0';
return (
<div className="flex flex-col gap-5 min-w-0">
<div className="flex flex-wrap items-start justify-between gap-3 min-w-0">
<div className="min-w-0">
<div className="flex items-center gap-2">
<h1 className="text-lg font-semibold text-foreground">Homepage Layout Sections</h1>
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 crm-radius-toggle bg-primary/10 text-primary text-[11px] font-semibold leading-none">
{visibleSections.length}
</span>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={fetchSections}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:text-foreground hover:bg-muted cursor-pointer transition-colors flex items-center justify-center"
title="Reload"
>
<RefreshCw className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => saveSectionsToBackend(sections)}
disabled={saving}
className="inline-flex items-center gap-2 h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer disabled:opacity-50"
>
{saving ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
{saving ? 'Saving...' : 'Save & publish'}
</button>
</div>
</div>
<div className={dataCardShell}>
<div className="px-5 py-4 border-b border-border flex flex-wrap items-center justify-between gap-3">
<div className="crm-radius-toggle inline-flex items-center gap-1 border border-border bg-card p-1">
<button
type="button"
onClick={() => setActiveTab('home')}
className={`h-8 px-3 crm-radius-toggle text-[13px] font-medium cursor-pointer transition-colors ${
activeTab === 'home' ? 'bg-primary text-white' : 'text-foreground hover:bg-muted'
}`}
>
Homepage ({homeCount})
</button>
<button
type="button"
onClick={() => setActiveTab('services')}
className={`h-8 px-3 crm-radius-toggle text-[13px] font-medium cursor-pointer transition-colors ${
activeTab === 'services' ? 'bg-primary text-white' : 'text-foreground hover:bg-muted'
}`}
>
Repair page ({servicesCount})
</button>
</div>
</div>
{loading ? (
<div className="flex flex-col items-center justify-center py-16 gap-2.5">
<RefreshCw className="w-6 h-6 text-primary animate-spin" />
<span className="text-[13px] text-muted-foreground">Loading layout sections...</span>
</div>
) : visibleSections.length === 0 ? (
<div className="py-12 text-center text-[13px] text-muted-foreground">No sections for this page.</div>
) : (
<div className="divide-y divide-border">
{visibleSections.map((sec, idx) => (
<div
key={sec.section_key}
id={`section-card-${sec.id}`}
className={sec.is_active ? 'bg-card' : 'bg-muted/20'}
>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3 px-5 py-4">
<button
type="button"
onClick={() => setActiveEditingId(activeEditingId === sec.id ? null : sec.id)}
className="flex items-center gap-3 min-w-0 text-left cursor-pointer"
>
<span className="w-9 h-9 crm-radius-icon bg-primary/10 text-primary text-[12px] font-semibold flex items-center justify-center shrink-0">
{idx + 1}
</span>
<div className="min-w-0">
<p className="text-[14px] font-semibold text-foreground truncate">{sec.name}</p>
<p className="text-[12px] text-muted-foreground truncate mt-0.5">{sec.title}</p>
</div>
</button>
<div className="flex items-center gap-2 shrink-0">
<button
type="button"
onClick={() => moveSection(idx, 'up')}
disabled={idx === 0}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:bg-muted cursor-pointer disabled:opacity-40 flex items-center justify-center"
title="Move up"
>
<ArrowUp className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => moveSection(idx, 'down')}
disabled={idx === visibleSections.length - 1}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:bg-muted cursor-pointer disabled:opacity-40 flex items-center justify-center"
title="Move down"
>
<ArrowDown className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => toggleActive(sec.id)}
className={`inline-flex items-center gap-1.5 h-8 px-2.5 crm-radius-badge text-[11px] font-semibold cursor-pointer ${
sec.is_active ? 'bg-success text-white' : 'bg-destructive text-white'
}`}
>
{sec.is_active ? <Eye className="w-3.5 h-3.5" /> : <EyeOff className="w-3.5 h-3.5" />}
{sec.is_active ? 'Visible' : 'Hidden'}
</button>
<button
type="button"
onClick={() => setActiveEditingId(activeEditingId === sec.id ? null : sec.id)}
className="w-8 h-8 crm-radius-control border border-border bg-card text-muted-foreground hover:bg-muted cursor-pointer flex items-center justify-center"
title={activeEditingId === sec.id ? 'Collapse' : 'Edit'}
>
<ChevronDown className={`w-4 h-4 transition-transform ${activeEditingId === sec.id ? 'rotate-180' : ''}`} />
</button>
</div>
</div>
{activeEditingId === sec.id && (
<div className="mx-5 mb-5 border border-border crm-radius-card bg-muted/10 p-4 space-y-4">
<p className="text-[13px] font-semibold text-foreground">Edit {sec.name}</p>
<div>{renderSectionFields(sec)}</div>
<div className="flex justify-end">
<button
type="button"
onClick={() => {
setActiveEditingId(null);
toast.success('Edits saved locally. Click Save & publish to go live.');
}}
className="inline-flex items-center h-9 px-4 crm-radius-control bg-primary hover:bg-primary/90 text-white text-[13px] font-semibold cursor-pointer"
>
Done
</button>
</div>
</div>
)}
</div>
))}
</div>
)}
</div>
<ConfirmDeleteModal
isOpen={deleteConfirm.isOpen}
message={deleteConfirm.message}
onConfirm={deleteConfirm.onConfirm}
onClose={() => setDeleteConfirm(prev => ({ ...prev, isOpen: false }))}
/>
</div>
);
}