**WORKFLOW SKILL** — Create a new EcoPro storefront template with ALL required functionality: delivery system, order submission, cart, checkout form, wilaya dropdown, contentEditable text editing, RTL layout, color/settings wiring, mobile responsiveness, product display, success screen. USE FOR: adding a new storefront template, creating a store theme, building a template component. DO NOT USE FOR: editing existing templates, fixing bugs in templates, template editor changes.
Every storefront template is a single React component that must implement:
useStoreDeliveryPricesPOST /api/orders/createdir="rtl")Missing ANY of these = broken template.
Path: client/pages/storefront/templates/{id}/{Name}Template.tsx
{id} = lowercase template ID (e.g. elegance){Name} = PascalCase name (e.g. Elegance)import React, { useState, useMemo } from 'react';
import { TemplateProps } from '../types';
import { useStoreDeliveryPrices } from '@/hooks/useStoreDeliveryPrices';
Also import icons from lucide-react as needed (e.g. ShoppingBag, Check, X, Phone, MapPin, Truck).
export default function {Name}Template({
settings,
products,
canManage,
storeSlug,
}: TemplateProps) {
All four props are mandatory to destructure and use.
Global keys MUST take precedence over template-specific keys:
const accentColor = settings?.template_accent_color || settings?.{id}_accent_color || '#DEFAULT';
const bgColor = settings?.template_bg_color || settings?.{id}_bg_color || '#DEFAULT';
const primaryColor = settings?.primary_color || '#DEFAULT';
WRONG: settings?.{id}_accent_color || settings?.template_accent_color — this breaks the editor color pickers.
const heroTitle = settings?.template_hero_heading || 'Default Arabic Title';
const heroSubtitle = settings?.template_hero_subtitle || 'Default subtitle';
const buttonText = settings?.template_button_text || 'اطلب الآن';
const mainProduct = useMemo(() => {
const mainId = settings?.dzp_main_product_id;
return mainId
? products?.find((p: any) => String(p.id) === String(mainId))
: products?.[0];
}, [products, settings?.dzp_main_product_id]);
const currency = settings?.currency_code || 'د.ج';
{settings?.store_logo ? (
<img src={settings.store_logo} alt="" className="w-8 h-8 rounded-full object-cover" />
) : (
<div className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold"
style={{ backgroundColor: accentColor }}>
{(settings?.store_name || 'م').charAt(0)}
</div>
)}
const { wilayas } = useStoreDeliveryPrices(storeSlug);
const [selectedWilayaId, setSelectedWilayaId] = useState<number | null>(null);
const selectedWilaya = wilayas.find(w => w.id === selectedWilayaId);
const deliveryFee = selectedWilaya?.homePrice ?? 0;
<select
value={selectedWilayaId ?? ''}
onChange={(e) => setSelectedWilayaId(Number(e.target.value) || null)}
className="w-full px-4 py-2 border rounded-lg"
>
<option value="">اختر الولاية</option>
{wilayas.map(w => (
<option key={w.id} value={w.id}>{w.labelAR}</option>
))}
</select>
Use a simple cart array with quantity tracking:
const [cart, setCart] = useState<{ id: number; title: string; price: number; image: string; qty: number }[]>([]);
const addToCart = (product: any) => {
setCart(prev => {
const existing = prev.find(item => item.id === product.id);
if (existing) return prev.map(item =>
item.id === product.id ? { ...item, qty: item.qty + 1 } : item
);
return [...prev, {
id: product.id,
title: product.title,
price: product.price,
image: product.images?.[0] || '/placeholder.png',
qty: 1
}];
});
};
const removeFromCart = (productId: number) => {
setCart(prev => prev.filter(item => item.id !== productId));
};
const subtotal = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
const total = subtotal + deliveryFee;
const [customerName, setCustomerName] = useState('');
const [customerPhone, setCustomerPhone] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [orderSuccess, setOrderSuccess] = useState(false);
const handleOrder = async () => {
if (!customerName || !customerPhone || !selectedWilayaId || cart.length === 0) {
alert('يرجى ملء جميع الحقول');
return;
}
try {
setIsSubmitting(true);
for (const item of cart) {
const res = await fetch('/api/orders/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
store_slug: storeSlug,
product_id: item.id,
quantity: item.qty,
total_price: item.price * item.qty,
delivery_fee: deliveryFee,
delivery_type: 'desk',
customer_name: customerName,
customer_phone: customerPhone,
customer_address: selectedWilaya?.labelAR || '',
}),
});
if (!res.ok) {
const data = await res.json();
alert(data.error || 'خطأ في الطلب');
return;
}
}
setOrderSuccess(true);
} catch (err) {
alert('خطأ في الطلب');
} finally {
setIsSubmitting(false);
}
};
| Field | Source |
|---|---|
store_slug | storeSlug prop |
product_id | item.id from cart |
quantity | item.qty |
total_price | item.price * item.qty |
delivery_fee | From useStoreDeliveryPrices |
delivery_type | 'desk' or 'home' |
customer_name | Form input |
customer_phone | Form input |
customer_address | selectedWilaya?.labelAR |
Must show BEFORE the main template return (early return pattern):
if (orderSuccess) {
return (
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: bgColor }} dir="rtl">
<div className="text-center p-8 max-w-md">
<div className="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4"
style={{ backgroundColor: accentColor + '20' }}>
<Check size={32} style={{ color: accentColor }} />
</div>
<h2 className="text-2xl font-bold mb-2">تم تأكيد طلبك!</h2>
<p className="text-gray-500 mb-6">سنتواصل معك قريباً</p>
<div className="text-right bg-gray-50 rounded-xl p-4 mb-4 space-y-2">
{cart.map(item => (
<div key={item.id} className="flex justify-between">
<span>{item.title} × {item.qty}</span>
<span>{item.price * item.qty} {currency}</span>
</div>
))}
<div className="border-t pt-2 flex justify-between font-bold">
<span>المجموع</span>
<span style={{ color: accentColor }}>{total} {currency}</span>
</div>
</div>
<button onClick={() => { setOrderSuccess(false); setCart([]); }}
className="px-6 py-2 rounded-lg text-white"
style={{ backgroundColor: accentColor }}>
تسوق مرة أخرى
</button>
</div>
</div>
);
}
For the template editor to allow inline text editing:
const handleTextEdit = (key: string) => (e: React.FocusEvent<HTMLElement>) => {
const text = e.currentTarget.textContent || '';
if (typeof window !== 'undefined' && window.parent !== window) {
window.parent.postMessage({ type: 'TEMPLATE_UPDATE_SETTING', key, value: text }, '*');
}
};
<h1
contentEditable={canManage}
suppressContentEditableWarning
onBlur={handleTextEdit('template_hero_heading')}
>
{heroTitle}
</h1>
Apply to ALL user-facing text: hero heading, subtitle, button text, section titles.
The root div MUST have dir="rtl":
return (
<div className="min-h-screen" style={{ backgroundColor: bgColor }} dir="rtl">
{/* All content */}
</div>
);
Use Arabic text for all default/fallback strings:
'اطلب الآن''الاسم الكامل', 'رقم الهاتف', 'اختر الولاية'// Enable text editing only in editor mode
<h2 contentEditable={canManage} suppressContentEditableWarning ...>
// Show placeholder when no products in editor
{canManage && products.length === 0 && (
<div className="py-20 text-center opacity-50">
<p>أضف منتجات من لوحة التحكم</p>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map(p => (...))}
</div>
{showCheckout && (
<div className="fixed inset-0 z-[60] flex items-end sm:items-center justify-center bg-black/40">
<div className="w-full sm:max-w-xl rounded-t-[2rem] sm:rounded-[2rem] bg-white p-6 max-h-[90vh] overflow-y-auto">
{/* Checkout form */}
</div>
</div>
)}
<div className="max-w-md mx-auto px-4">
<img
src={product?.images?.[0] || '/placeholder.png'}
alt={product?.title}
className="w-full h-full object-cover"
/>
const bannerUrl = settings?.banner_url || '/placeholder.png';
<div className="bg-black/40 backdrop-blur-md border border-white/10 rounded-[2rem] p-6">
rounded-[2rem]rounded-2xl or rounded-xlrounded-lg<div style={{ backgroundColor: bgColor, color: accentColor }}>
<button style={{ backgroundColor: accentColor }} className="text-white rounded-xl px-6 py-3">
useEffect(() => {
if (!document.getElementById('cairo-font')) {
const link = document.createElement('link');
link.id = 'cairo-font';
link.href = 'https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700;800&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
}, []);
client/pages/storefront/templates/index.tsxFour changes required:
1. Add import:
import {Name}Template from './{id}/{Name}Template';
2. Add to validIds array in normalizeTemplateId():
const validIds = ['dzshop', 'dzpremium', ..., '{id}'];
3. Add case to RenderStorefront() switch:
case '{id}': return <{Name}Template {...sanitizedProps} />;
4. Add to named exports:
export { ..., {Name}Template };
client/pages/GoldTemplateEditor.tsx1. Add to TEMPLATE_PREVIEWS array:
{
id: '{id}',
name: '{Display Name}',
description: 'Short description of the template style',
image: '/templates/{id}.png',
category: 'modern' | 'classic' | 'minimal' | 'luxury' | 'bold',
isNew: true,
}
2. Add to READY_TEMPLATE_IDS set:
const READY_TEMPLATE_IDS = new Set([..., '{id}']);
Every template MUST have a checkout form with these fields:
<input
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
placeholder="الاسم الكامل"
className="w-full px-4 py-3 border rounded-xl"
/>
<input
value={customerPhone}
onChange={(e) => setCustomerPhone(e.target.value)}
placeholder="رقم الهاتف"
className="w-full px-4 py-3 border rounded-xl"
type="tel"
/>
<select
value={selectedWilayaId ?? ''}
onChange={(e) => setSelectedWilayaId(Number(e.target.value) || null)}
className="w-full px-4 py-3 border rounded-xl"
>
<option value="">اختر الولاية</option>
{wilayas.map(w => <option key={w.id} value={w.id}>{w.labelAR}</option>)}
</select>
<div className="space-y-2">
<div className="flex justify-between">
<span>المنتجات</span><span>{subtotal} {currency}</span>
</div>
<div className="flex justify-between">
<span>التوصيل</span><span>{deliveryFee} {currency}</span>
</div>
<div className="flex justify-between font-bold text-lg border-t pt-2">
<span>المجموع</span><span style={{ color: accentColor }}>{total} {currency}</span>
</div>
</div>
<button
onClick={handleOrder}
disabled={isSubmitting || cart.length === 0}
className="w-full py-3 rounded-xl text-white font-bold disabled:opacity-50"
style={{ backgroundColor: accentColor }}
>
{isSubmitting ? 'جاري المعالجة...' : buttonText}
</button>
Before considering the template done, verify ALL of these:
client/pages/storefront/templates/{id}/{Name}Template.tsxTemplateProps from ../typesuseStoreDeliveryPrices from @/hooks/useStoreDeliveryPrices{ settings, products, canManage, storeSlug }dir="rtl" and style={{ backgroundColor: bgColor }}template_accent_color BEFORE template-specific keytemplate_bg_color BEFORE template-specific keycurrency_code with fallback 'د.ج'dzp_main_product_id with fallback to products?.[0]template_hero_heading, template_hero_subtitle, template_button_textuseStoreDeliveryPrices(storeSlug) hook calledPOST /api/orders/create with ALL required fieldscontentEditable={canManage} on text elementshandleTextEdit sends postMessage to parentcanManage && products.length === 0 placeholderindex.tsx (import, validIds, switch, export)TEMPLATE_PREVIEWS in GoldTemplateEditor.tsxREADY_TEMPLATE_IDS in GoldTemplateEditor.tsx