<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The Wardrobe Boutique</title>
<!-- React & Babel Standalone -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Inter:wght@300;400;500;600;700&display=swap');
:root {
--font-serif: 'Playfair Display', serif;
--font-sans: 'Inter', sans-serif;
}
body {
font-family: var(--font-sans);
}
.font-serif {
font-family: var(--font-serif);
}
</style>
</head>
<body class="bg-white text-zinc-900">
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useMemo } = React;
/**
* FUNGSI PEMFORMATAN IMEJ
* Menukarkan ID Google Drive kepada format lh3.googleusercontent.com
*/
const formatImageUrl = (url) => {
if (!url) return 'https://placehold.co/600x900?text=Tiada+Imej';
// Jika ia adalah ID atau URL Drive, tukar kepada format LH3
const driveIdMatch = url.match(/(?:\/d\/|id=)([\w-]+)/);
if (driveIdMatch && driveIdMatch[1]) {
return `https://lh3.googleusercontent.com/d/${driveIdMatch[1]}`;
}
return url;
};
// Komponen Ikon Ringkas
const Icon = ({ name, size = 20, className = "" }) => {
const icons = {
shoppingBag: (
<>
<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/>
<path d="M3 6h18"/>
<path d="M16 10a4 4 0 0 1-8 0"/>
</>
),
user: (
<>
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</>
),
search: (
<>
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.3-4.3"/>
</>
),
arrowLeft: (
<>
<path d="m12 19-7-7 7-7"/>
<path d="M19 12H5"/>
</>
),
x: (
<>
<path d="M18 6 6 18"/>
<path d="m6 6 12 12"/>
</>
),
chevronRight: <path d="m9 18 6-6-6-6"/>,
checkCircle: (
<>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</>
),
package: (
<>
<path d="M16.5 9.4 7.5 4.21"/>
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.29 7 12 12 20.71 7"/>
<line x1="12" y1="22" x2="12" y2="12"/>
</>
),
loader: (
<>
<path d="M12 2v4"/>
<path d="m16.2 7.8 2.9-2.9"/>
<path d="M18 12h4"/>
<path d="m16.2 16.2 2.9 2.9"/>
<path d="M12 18v4"/>
<path d="m4.9 19.1 2.9-2.9"/>
<path d="M2 12h4"/>
<path d="m4.9 4.9 2.9 2.9"/>
</>
)
};
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
{icons[name]}
</svg>
);
};
function App() {
const [view, setView] = useState('home');
const [products, setProducts] = useState([]);
const [cart, setCart] = useState([]);
const [selectedProduct, setSelectedProduct] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Mengambil data menggunakan google.script.run
useEffect(() => {
const fetchData = () => {
if (typeof google !== 'undefined' && google.script && google.script.run) {
google.script.run
.withSuccessHandler((data) => {
// Proses data dari server-side getProducts()
setProducts(data);
setIsLoading(false);
})
.withFailureHandler((err) => {
console.error(err);
setError("Gagal memuatkan data daripada Skrip Google.");
setIsLoading(false);
})
.getProducts(); // Pastikan fungsi ini wujud dalam Code.gs
} else {
// Fallback untuk tujuan pembangunan/preview
setError("Sila jalankan aplikasi ini dalam persekitaran Google Apps Script.");
setIsLoading(false);
}
};
fetchData();
}, []);
const navigate = (to, data = null) => {
if (data) setSelectedProduct(data);
setView(to);
window.scrollTo(0, 0);
};
const addToCart = (product, config) => {
setCart([...cart, { ...product, ...config, cartId: Math.random().toString(36).substr(2, 9) }]);
navigate('cart');
};
if (isLoading) return (
<div className="h-screen flex flex-col items-center justify-center text-zinc-400">
<Icon name="loader" className="animate-spin mb-4" size={32} />
<p className="text-[10px] uppercase tracking-widest font-bold text-zinc-500">Menyediakan Butik...</p>
</div>
);
if (error) return (
<div className="h-screen flex flex-col items-center justify-center text-center px-6">
<p className="text-red-500 mb-4 font-serif">{error}</p>
<button onClick={() => window.location.reload()} className="text-[10px] uppercase font-bold border-b border-black">Cuba Lagi</button>
</div>
);
return (
<div className="min-h-screen">
{/* Navigasi Utama */}
<nav className="sticky top-0 z-50 bg-white/95 backdrop-blur-sm border-b border-zinc-100 px-6 py-4 flex items-center justify-between">
<div className="flex items-center space-x-8">
<button onClick={() => navigate('shop')} className="hidden md:block text-[10px] uppercase tracking-[0.2em] font-bold hover:text-amber-600 transition-colors">Koleksi</button>
<button onClick={() => navigate('home')} className="hidden md:block text-[10px] uppercase tracking-[0.2em] font-bold hover:text-amber-600 transition-colors">Jurnal</button>
</div>
<button onClick={() => navigate('home')} className="text-2xl font-serif tracking-tighter uppercase">THE WARDROBE</button>
<div className="flex items-center space-x-6">
<button onClick={() => navigate('account')} className="hover:text-amber-600"><Icon name="user" /></button>
<button onClick={() => navigate('cart')} className="relative hover:text-amber-600">
<Icon name="shoppingBag" />
{cart.length > 0 && <span className="absolute -top-1 -right-1 bg-zinc-900 text-white text-[8px] w-3.5 h-3.5 rounded-full flex items-center justify-center font-bold">{cart.length}</span>}
</button>
</div>
</nav>
<main>
{view === 'home' && <HomeView products={products} onNavigate={navigate} />}
{view === 'shop' && <ShopView products={products} onNavigate={navigate} />}
{view === 'product' && <ProductDetailView product={selectedProduct} onAddToCart={addToCart} onBack={() => navigate('shop')} />}
{view === 'cart' && <CartView cart={cart} onRemove={(id) => setCart(cart.filter(i => i.cartId !== id))} onCheckout={() => navigate('checkout')} />}
{view === 'checkout' && <CheckoutView total={cart.reduce((a, b) => a + b.price, 0)} onOrder={() => {setCart([]); setView('success');}} onBack={() => navigate('cart')} />}
{view === 'success' && <SuccessView onNavigate={navigate} />}
{view === 'account' && <AccountView />}
</main>
{/* Pengaki Halaman */}
<footer className="bg-zinc-50 border-t border-zinc-100 py-20 px-8 mt-20">
<div className="max-w-7xl auto grid grid-cols-1 md:grid-cols-4 gap-12 mx-auto">
<div>
<h3 className="font-serif text-xl mb-6">THE WARDROBE</h3>
<p className="text-sm text-zinc-500 leading-relaxed italic">Keanggunan abadi dalam setiap jahitan.</p>
</div>
<div>
<h4 className="text-[10px] uppercase tracking-widest font-bold mb-6 text-zinc-400">Maklumat</h4>
<ul className="text-sm text-zinc-600 space-y-3">
<li>Jejak Pesanan</li>
<li>Panduan Saiz</li>
<li>Mengenai Kami</li>
</ul>
</div>
<div className="md:col-span-2">
<h4 className="text-[10px] uppercase tracking-widest font-bold mb-6 text-zinc-400">Hab Skrip Google</h4>
<p className="text-xs text-zinc-400">Laman web ini dihubungkan secara terus melalui Google Apps Script.</p>
</div>
</div>
</footer>
</div>
);
}
// Paparan Utama
function HomeView({ products, onNavigate }) {
const featured = products.filter(p => String(p.isnew).toUpperCase() === 'TRUE').slice(0, 4);
return (
<div className="animate-in fade-in duration-700">
<div className="relative h-[80vh] bg-zinc-100 overflow-hidden">
<img src="https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&q=80&w=2000" className="w-full h-full object-cover opacity-90" alt="Imej Utama" />
<div className="absolute inset-0 flex flex-col items-center justify-center text-center px-4">
<span className="uppercase tracking-[0.3em] text-[10px] mb-6 font-bold">Koleksi Musim Bunga 2024</span>
<h1 className="text-5xl md:text-7xl font-serif mb-10">Kemurnian Sutera</h1>
<button onClick={() => onNavigate('shop')} className="bg-zinc-900 text-white px-10 py-4 text-[10px] uppercase tracking-[0.2em] font-bold hover:bg-amber-600 transition-all">Terokai Koleksi</button>
</div>
</div>
<div className="max-w-7xl mx-auto px-6 py-24">
<h2 className="text-3xl font-serif mb-12 text-center">Ketibaan Baharu</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{featured.map(p => <ProductCard key={p.id} product={p} onClick={() => onNavigate('product', p)} />)}
</div>
</div>
</div>
);
}
function ShopView({ products, onNavigate }) {
return (
<div className="max-w-7xl mx-auto px-6 py-20 animate-in slide-in-from-bottom-4">
<h1 className="text-4xl font-serif mb-16 text-center">Katalog Lengkap</h1>
<div className="grid grid-cols-1 md:grid-cols-4 gap-x-8 gap-y-16">
{products.map(p => <ProductCard key={p.id} product={p} onClick={() => onNavigate('product', p)} />)}
</div>
</div>
);
}
function ProductCard({ product, onClick }) {
const imgUrl = formatImageUrl(product.images && product.images.length > 0 ? product.images[0] : null);
return (
<div onClick={onClick} className="group cursor-pointer">
<div className="aspect-[2/3] overflow-hidden bg-zinc-50 mb-4 relative">
<img src={imgUrl} className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105" alt={product.name} />
{String(product.isnew).toUpperCase() === 'TRUE' && <span className="absolute top-4 left-4 bg-white text-zinc-900 px-3 py-1 text-[8px] uppercase font-bold tracking-widest">Baharu</span>}
</div>
<h3 className="text-[10px] font-bold uppercase tracking-widest mb-1">{product.name}</h3>
<p className="font-serif text-zinc-500">RM {product.price}</p>
</div>
);
}
function ProductDetailView({ product, onAddToCart, onBack }) {
if (!product) return null;
const [size, setSize] = useState(product.sizes?.[0] || "");
const [color, setColor] = useState(product.colors?.[0] || "");
const imgUrl = formatImageUrl(product.images && product.images.length > 0 ? product.images[0] : null);
return (
<div className="max-w-7xl mx-auto px-6 py-12">
<button onClick={onBack} className="flex items-center text-[10px] uppercase font-bold mb-12 hover:text-amber-600">
<Icon name="arrowLeft" size={14} className="mr-2" /> Kembali ke Katalog
</button>
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
<div className="aspect-[4/5] bg-zinc-50 overflow-hidden">
<img src={imgUrl} className="w-full h-full object-cover" alt={product.name} />
</div>
<div>
<span className="text-amber-700 text-[10px] uppercase font-bold tracking-widest">{product.category}</span>
<h1 className="text-4xl font-serif mt-2 mb-6">{product.name}</h1>
<p className="text-2xl font-serif mb-10 text-zinc-900">RM {product.price}</p>
<div className="space-y-8 mb-12 border-t border-zinc-100 pt-8">
{product.colors?.length > 0 && (
<div>
<p className="text-[10px] uppercase font-bold text-zinc-400 mb-4 tracking-widest">Pilih Warna</p>
<div className="flex flex-wrap gap-2">
{product.colors.map(c => (
<button key={c} onClick={() => setColor(c)} className={`px-4 py-2 border text-[10px] uppercase font-bold ${color === c ? 'bg-zinc-900 text-white' : 'hover:border-zinc-900'}`}>{c}</button>
))}
</div>
</div>
)}
{product.sizes?.length > 0 && (
<div>
<p className="text-[10px] uppercase font-bold text-zinc-400 mb-4 tracking-widest">Pilih Saiz</p>
<div className="flex flex-wrap gap-2">
{product.sizes.map(s => (
<button key={s} onClick={() => setSize(s)} className={`w-10 h-10 border flex items-center justify-center text-[10px] font-bold ${size === s ? 'bg-zinc-900 text-white' : 'hover:border-zinc-900'}`}>{s}</button>
))}
</div>
</div>
)}
</div>
<button onClick={() => onAddToCart(product, { size, color })} className="w-full bg-zinc-900 text-white py-5 text-[10px] uppercase font-bold tracking-widest hover:bg-amber-600 transition-colors">Tambah ke Beg</button>
<div className="mt-12 text-sm text-zinc-500 leading-relaxed whitespace-pre-line">{product.description}</div>
</div>
</div>
</div>
);
}
function CartView({ cart, onRemove, onCheckout }) {
const total = cart.reduce((acc, i) => acc + i.price, 0);
return (
<div className="max-w-xl mx-auto px-6 py-20">
<h1 className="text-3xl font-serif mb-12 text-center">Beg Belanja</h1>
{cart.length === 0 ? <p className="text-center italic text-zinc-400 font-serif">Beg belanja anda kosong.</p> : (
<div className="space-y-8">
{cart.map(item => (
<div key={item.cartId} className="flex gap-6 pb-8 border-b border-zinc-50">
<img src={formatImageUrl(item.images?.[0])} className="w-20 h-28 object-cover bg-zinc-100" alt={item.name} />
<div className="flex-1">
<div className="flex justify-between items-start">
<h3 className="font-serif">{item.name}</h3>
<button onClick={() => onRemove(item.cartId)} className="text-zinc-400 hover:text-black"><Icon name="x" size={14} /></button>
</div>
<p className="text-[10px] text-zinc-400 uppercase mt-1">{item.size} / {item.color}</p>
<p className="mt-4 font-serif text-amber-800">RM {item.price}</p>
</div>
</div>
))}
<div className="pt-8 flex justify-between items-center text-xl font-serif border-t border-zinc-100">
<span className="text-sm font-sans uppercase font-bold tracking-widest text-zinc-400">Jumlah Keseluruhan</span>
<span>RM {total}</span>
</div>
<button onClick={onCheckout} className="w-full bg-zinc-900 text-white py-4 text-[10px] uppercase font-bold tracking-widest hover:bg-zinc-800 transition-colors">Daftar Keluar</button>
</div>
)}
</div>
);
}
function CheckoutView({ total, onOrder, onBack }) {
return (
<div className="max-w-4xl mx-auto px-6 py-20">
<div className="grid grid-cols-1 md:grid-cols-2 gap-16">
<div className="space-y-8">
<h2 className="text-2xl font-serif text-zinc-900">Alamat Penghantaran</h2>
<div className="space-y-4">
<input placeholder="Nama Penuh" className="w-full p-4 border border-zinc-100 bg-zinc-50 outline-none text-sm focus:border-black transition-all" />
<input placeholder="Emel" className="w-full p-4 border border-zinc-100 bg-zinc-50 outline-none text-sm focus:border-black transition-all" />
<input placeholder="Alamat Lengkap" className="w-full p-4 border border-zinc-100 bg-zinc-50 outline-none text-sm focus:border-black transition-all" />
</div>
<button onClick={onOrder} className="w-full bg-zinc-900 text-white py-4 text-[10px] uppercase font-bold tracking-widest hover:bg-amber-600 transition-colors">Selesaikan Pembayaran</button>
</div>
<div className="bg-zinc-50 p-8 h-fit">
<h2 className="text-xl font-serif mb-6">Ringkasan Pesanan</h2>
<div className="flex justify-between text-sm mb-4"><span className="text-zinc-400">Jumlah Kecil</span><span>RM {total}</span></div>
<div className="flex justify-between text-sm mb-4 font-bold border-t border-zinc-200 pt-4"><span className="text-zinc-400 uppercase text-[10px]">Jumlah Akhir</span><span>RM {total}</span></div>
</div>
</div>
</div>
);
}
function SuccessView({ onNavigate }) {
return (
<div className="max-w-md mx-auto text-center py-32 px-6 animate-in zoom-in duration-500">
<Icon name="checkCircle" className="mx-auto text-emerald-500 mb-6" size={48} />
<h1 className="text-3xl font-serif mb-4">Terima Kasih</h1>
<p className="text-sm text-zinc-500 mb-10">Pesanan anda telah berjaya diterima dan akan diproses secepat mungkin.</p>
<button onClick={() => onNavigate('home')} className="w-full py-4 border border-zinc-900 text-[10px] uppercase font-bold tracking-widest hover:bg-zinc-900 hover:text-white transition-all">Kembali ke Laman Utama</button>
</div>
);
}
function AccountView() {
return (
<div className="max-w-4xl mx-auto px-6 py-20">
<h1 className="text-4xl font-serif mb-12">Akaun Saya</h1>
<div className="p-12 border border-zinc-100 text-center italic text-zinc-400 font-serif">Tiada rekod pesanan buat masa ini.</div>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
</script>
</body>
</html>