feat: Implement subscription deletion confirmation modal in admin subscriptions page

refactor: Update header component to conditionally show shop navigation

feat: Add environment variable check to control shop visibility in public and VIP shop pages

refactor: Clean up VIP shop page by removing unused collections and featured products sections
This commit is contained in:
seaznCode 2025-11-20 17:37:56 +01:00
parent cbf81e756b
commit d54a4024cb
6 changed files with 183 additions and 496 deletions

View File

@ -15,35 +15,25 @@ export default function CreateSubscriptionPage() {
// form state // form state
const [title, setTitle] = useState(''); const [title, setTitle] = useState('');
const [description, setDescription] = useState(''); const [description, setDescription] = useState('');
const [quantity, setQuantity] = useState(1);
const [price, setPrice] = useState(0); const [price, setPrice] = useState(0);
const [state, setState] = useState<'available'|'unavailable'>('available'); const [state, setState] = useState<'available'|'unavailable'>('available');
const [pictureFile, setPictureFile] = useState<File | undefined>(undefined); const [pictureFile, setPictureFile] = useState<File | undefined>(undefined);
const [currency, setCurrency] = useState('EUR'); const [currency, setCurrency] = useState('EUR');
const [taxRate, setTaxRate] = useState<number | undefined>(undefined);
const [isFeatured, setIsFeatured] = useState(false); const [isFeatured, setIsFeatured] = useState(false);
const [billingInterval, setBillingInterval] = useState<'day'|'week'|'month'|'year'|''>(''); // Fixed billing defaults (locked: month / 1)
const [intervalCount, setIntervalCount] = useState<number | undefined>(undefined); const billingInterval: 'month' = 'month';
const [sku, setSku] = useState<string>(''); const intervalCount: number = 1;
const [slug, setSlug] = useState<string>('');
const onCreate = async (e: React.FormEvent) => { const onCreate = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
setError(null); setError(null);
try { try {
const normalizedIntervalCount = billingInterval ? (intervalCount && intervalCount > 0 ? intervalCount : 1) : undefined;
await createProduct({ await createProduct({
title, title,
description, description,
quantity,
price, price,
currency, currency,
tax_rate: taxRate,
is_featured: isFeatured, is_featured: isFeatured,
billing_interval: billingInterval || undefined,
interval_count: normalizedIntervalCount,
sku: sku || undefined,
slug: slug || undefined,
state: state === 'available', state: state === 'available',
pictureFile pictureFile
}); });
@ -81,11 +71,6 @@ export default function CreateSubscriptionPage() {
<label htmlFor="title" className="block text-sm font-medium text-blue-900">Title</label> <label htmlFor="title" className="block text-sm font-medium text-blue-900">Title</label>
<input id="title" name="title" required className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <input id="title" name="title" required className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} />
</div> </div>
{/* Quantity */}
<div>
<label htmlFor="quantity" className="block text-sm font-medium text-blue-900">Quantity</label>
<input id="quantity" name="quantity" required min={1} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Quantity" type="number" value={quantity} onChange={e => setQuantity(Number(e.target.value))} />
</div>
{/* Price */} {/* Price */}
<div> <div>
<label htmlFor="price" className="block text-sm font-medium text-blue-900">Price</label> <label htmlFor="price" className="block text-sm font-medium text-blue-900">Price</label>
@ -96,49 +81,19 @@ export default function CreateSubscriptionPage() {
<label htmlFor="currency" className="block text-sm font-medium text-blue-900">Currency (e.g., EUR)</label> <label htmlFor="currency" className="block text-sm font-medium text-blue-900">Currency (e.g., EUR)</label>
<input id="currency" name="currency" required maxLength={3} pattern="[A-Za-z]{3}" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="EUR" value={currency} onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))} /> <input id="currency" name="currency" required maxLength={3} pattern="[A-Za-z]{3}" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="EUR" value={currency} onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))} />
</div> </div>
{/* Tax Rate */}
<div>
<label htmlFor="tax_rate" className="block text-sm font-medium text-blue-900">Tax rate (%)</label>
<input id="tax_rate" name="tax_rate" min={0} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="e.g. 19" type="number" step="0.01" value={taxRate ?? ''} onChange={e => setTaxRate(e.target.value === '' ? undefined : Number(e.target.value))} />
</div>
{/* Featured */} {/* Featured */}
<div className="flex items-center gap-2 mt-6"> <div className="flex items-center gap-2 mt-6">
<input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} /> <input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} />
<label htmlFor="featured" className="text-sm font-medium text-blue-900">Featured</label> <label htmlFor="featured" className="text-sm font-medium text-blue-900">Featured</label>
</div> </div>
{/* Billing Interval */} {/* Fixed Billing (Locked) */}
<div> <div className="sm:col-span-2">
<label htmlFor="billing_interval" className="block text-sm font-medium text-blue-900">Billing interval</label> <label className="block text-sm font-medium text-blue-900">Billing</label>
<select id="billing_interval" name="billing_interval" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black" value={billingInterval} onChange={e => { <p className="mt-1 text-xs text-gray-600">Fixed monthly billing (interval count = 1). These settings are locked.</p>
const v = e.target.value as any; <div className="mt-2 flex gap-4">
setBillingInterval(v); <input disabled value={billingInterval} className="w-40 rounded-lg border-gray-300 bg-gray-100 px-4 py-3 text-sm text-gray-600" />
if (v) { <input disabled value={intervalCount} className="w-24 rounded-lg border-gray-300 bg-gray-100 px-4 py-3 text-sm text-gray-600" />
setIntervalCount(c => (c && c > 0 ? c : 1)); </div>
} else {
setIntervalCount(undefined);
}
}}>
<option value="">One-time or N/A</option>
<option value="day">Day</option>
<option value="week">Week</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
</div>
{/* Interval Count */}
<div>
<label htmlFor="interval_count" className="block text-sm font-medium text-blue-900">Interval count</label>
<input id="interval_count" name="interval_count" min={1} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400 disabled:bg-gray-100 disabled:text-gray-500" placeholder="e.g. 6 for 6 months (defaults to 1)" type="number" value={intervalCount ?? ''} onChange={e => setIntervalCount(e.target.value === '' ? undefined : Number(e.target.value))} disabled={!billingInterval} />
</div>
{/* SKU */}
<div>
<label htmlFor="sku" className="block text-sm font-medium text-blue-900">SKU (optional)</label>
<input id="sku" name="sku" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="SKU" value={sku} onChange={e => setSku(e.target.value)} />
</div>
{/* Slug */}
<div>
<label htmlFor="slug" className="block text-sm font-medium text-blue-900">Slug (optional)</label>
<input id="slug" name="slug" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="slug" value={slug} onChange={e => setSlug(e.target.value)} />
</div> </div>
{/* Availability */} {/* Availability */}
<div> <div>

View File

@ -5,15 +5,11 @@ export type CoffeeItem = {
id: number; id: number;
title: string; title: string;
description: string; description: string;
quantity: number;
price: number; price: number;
currency?: string; currency?: string;
tax_rate?: number;
is_featured?: boolean; is_featured?: boolean;
billing_interval?: 'day'|'week'|'month'|'year'|null; billing_interval?: 'day'|'week'|'month'|'year'|null;
interval_count?: number|null; interval_count?: number|null;
sku?: string|null;
slug?: string|null;
object_storage_id?: string|null; object_storage_id?: string|null;
original_filename?: string|null; original_filename?: string|null;
state: boolean; state: boolean;
@ -72,13 +68,10 @@ export default function useCoffeeManagement() {
const listProducts = useCallback(async (): Promise<CoffeeItem[]> => { const listProducts = useCallback(async (): Promise<CoffeeItem[]> => {
const data = await authorizedFetch<any[]>('/api/admin/coffee', { method: 'GET' }); const data = await authorizedFetch<any[]>('/api/admin/coffee', { method: 'GET' });
if (!Array.isArray(data)) return []; if (!Array.isArray(data)) return [];
// Normalize numeric fields in case API returns strings (e.g., MySQL DECIMAL)
return data.map((r: any) => ({ return data.map((r: any) => ({
...r, ...r,
id: Number(r.id), id: Number(r.id),
quantity: r.quantity != null ? Number(r.quantity) : 0,
price: r.price != null && r.price !== '' ? Number(r.price) : 0, price: r.price != null && r.price !== '' ? Number(r.price) : 0,
tax_rate: r.tax_rate != null && r.tax_rate !== '' ? Number(r.tax_rate) : undefined,
interval_count: r.interval_count != null && r.interval_count !== '' ? Number(r.interval_count) : null, interval_count: r.interval_count != null && r.interval_count !== '' ? Number(r.interval_count) : null,
state: !!r.state, state: !!r.state,
})) as CoffeeItem[]; })) as CoffeeItem[];
@ -87,31 +80,22 @@ export default function useCoffeeManagement() {
const createProduct = useCallback(async (payload: { const createProduct = useCallback(async (payload: {
title: string; title: string;
description: string; description: string;
quantity: number;
price: number; price: number;
currency?: string; currency?: string;
tax_rate?: number;
is_featured?: boolean; is_featured?: boolean;
billing_interval?: 'day'|'week'|'month'|'year';
interval_count?: number;
sku?: string;
slug?: string;
state?: boolean; state?: boolean;
pictureFile?: File; pictureFile?: File;
}): Promise<CoffeeItem> => { }): Promise<CoffeeItem> => {
const fd = new FormData(); const fd = new FormData();
fd.append('title', payload.title); fd.append('title', payload.title);
fd.append('description', payload.description); fd.append('description', payload.description);
fd.append('quantity', String(payload.quantity));
fd.append('price', String(payload.price)); fd.append('price', String(payload.price));
if (payload.currency) fd.append('currency', payload.currency); if (payload.currency) fd.append('currency', payload.currency);
if (typeof payload.tax_rate === 'number') fd.append('tax_rate', String(payload.tax_rate));
if (typeof payload.is_featured === 'boolean') fd.append('is_featured', String(payload.is_featured)); if (typeof payload.is_featured === 'boolean') fd.append('is_featured', String(payload.is_featured));
if (payload.billing_interval) fd.append('billing_interval', payload.billing_interval);
if (typeof payload.interval_count === 'number') fd.append('interval_count', String(payload.interval_count));
if (payload.sku) fd.append('sku', payload.sku);
if (payload.slug) fd.append('slug', payload.slug);
if (typeof payload.state === 'boolean') fd.append('state', String(payload.state)); if (typeof payload.state === 'boolean') fd.append('state', String(payload.state));
// Fixed billing defaults
fd.append('billing_interval', 'month');
fd.append('interval_count', '1');
if (payload.pictureFile) fd.append('picture', payload.pictureFile); if (payload.pictureFile) fd.append('picture', payload.pictureFile);
return authorizedFetch<CoffeeItem>('/api/admin/coffee', { method: 'POST', body: fd }); return authorizedFetch<CoffeeItem>('/api/admin/coffee', { method: 'POST', body: fd });
}, [authorizedFetch]); }, [authorizedFetch]);
@ -119,31 +103,22 @@ export default function useCoffeeManagement() {
const updateProduct = useCallback(async (id: number, payload: Partial<{ const updateProduct = useCallback(async (id: number, payload: Partial<{
title: string; title: string;
description: string; description: string;
quantity: number;
price: number; price: number;
currency: string; currency: string;
tax_rate: number;
is_featured: boolean; is_featured: boolean;
billing_interval: 'day'|'week'|'month'|'year';
interval_count: number;
sku: string;
slug: string;
state: boolean; state: boolean;
pictureFile: File; pictureFile: File;
}>): Promise<CoffeeItem> => { }>): Promise<CoffeeItem> => {
const fd = new FormData(); const fd = new FormData();
if (payload.title !== undefined) fd.append('title', String(payload.title)); if (payload.title !== undefined) fd.append('title', String(payload.title));
if (payload.description !== undefined) fd.append('description', String(payload.description)); if (payload.description !== undefined) fd.append('description', String(payload.description));
if (payload.quantity !== undefined) fd.append('quantity', String(payload.quantity));
if (payload.price !== undefined) fd.append('price', String(payload.price)); if (payload.price !== undefined) fd.append('price', String(payload.price));
if (payload.currency !== undefined) fd.append('currency', payload.currency); if (payload.currency !== undefined) fd.append('currency', payload.currency);
if (payload.tax_rate !== undefined) fd.append('tax_rate', String(payload.tax_rate));
if (payload.is_featured !== undefined) fd.append('is_featured', String(payload.is_featured)); if (payload.is_featured !== undefined) fd.append('is_featured', String(payload.is_featured));
if (payload.billing_interval !== undefined) fd.append('billing_interval', payload.billing_interval);
if (payload.interval_count !== undefined) fd.append('interval_count', String(payload.interval_count));
if (payload.sku !== undefined) fd.append('sku', payload.sku);
if (payload.slug !== undefined) fd.append('slug', payload.slug);
if (payload.state !== undefined) fd.append('state', String(payload.state)); if (payload.state !== undefined) fd.append('state', String(payload.state));
// Keep fixed defaults
fd.append('billing_interval', 'month');
fd.append('interval_count', '1');
if (payload.pictureFile) fd.append('picture', payload.pictureFile); if (payload.pictureFile) fd.append('picture', payload.pictureFile);
return authorizedFetch<CoffeeItem>(`/api/admin/coffee/${id}`, { method: 'PUT', body: fd }); return authorizedFetch<CoffeeItem>(`/api/admin/coffee/${id}`, { method: 'PUT', body: fd });
}, [authorizedFetch]); }, [authorizedFetch]);

View File

@ -35,20 +35,22 @@ export default function AdminSubscriptionsPage() {
</span> </span>
); );
const [deleteTarget, setDeleteTarget] = useState<CoffeeItem | null>(null);
return ( return (
<PageLayout> <PageLayout>
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
{/* Header */} {/* Header */}
<header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-6 px-6 rounded-2xl shadow-lg mb-8">
<div className="flex items-center justify-between"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Subscription Products</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Subscription Products</h1>
<p className="text-lg text-blue-700 mt-2">Manage all products and subscription plans.</p> <p className="text-lg text-blue-700 mt-2">Manage all products and subscription plans.</p>
</div> </div>
<Link <Link
href="/admin/subscriptions/createSubscription" href="/admin/subscriptions/createSubscription"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition" className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition self-start sm:self-auto"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg> <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
Create Subscription Create Subscription
@ -74,22 +76,16 @@ export default function AdminSubscriptionsPage() {
<img src={item.pictureUrl} alt={item.title} className="mt-3 w-full h-40 object-cover rounded-xl ring-1 ring-gray-200" /> <img src={item.pictureUrl} alt={item.title} className="mt-3 w-full h-40 object-cover rounded-xl ring-1 ring-gray-200" />
)} )}
<p className="mt-3 text-sm text-gray-800 line-clamp-4">{item.description}</p> <p className="mt-3 text-sm text-gray-800 line-clamp-4">{item.description}</p>
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm"> <dl className="mt-4 grid grid-cols-1 gap-y-2 text-sm">
<div>
<dt className="text-gray-500">Qty</dt>
<dd className="font-medium text-gray-900">{item.quantity}</dd>
</div>
<div> <div>
<dt className="text-gray-500">Price</dt> <dt className="text-gray-500">Price</dt>
<dd className="font-medium text-gray-900"> <dd className="font-medium text-gray-900">
{item.currency || 'EUR'}{' '} {item.currency || 'EUR'} {Number.isFinite(Number(item.price)) ? Number(item.price).toFixed(2) : String(item.price)}
{Number.isFinite(Number(item.price)) ? Number(item.price).toFixed(2) : String(item.price)}
{Number.isFinite(Number(item.tax_rate)) ? ` + ${Number(item.tax_rate)}%` : ''}
</dd> </dd>
</div> </div>
{item.billing_interval && item.interval_count ? ( {item.billing_interval && item.interval_count ? (
<div className="col-span-2 text-gray-600"> <div className="text-gray-600">
<span className="text-xs">Every {item.interval_count} {item.billing_interval}{item.interval_count > 1 ? 's' : ''}</span> <span className="text-xs">Billing: {item.billing_interval} (x{item.interval_count})</span>
</div> </div>
) : null} ) : null}
</dl> </dl>
@ -105,7 +101,7 @@ export default function AdminSubscriptionsPage() {
</button> </button>
<button <button
className="inline-flex items-center rounded-lg bg-red-50 px-4 py-2 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100 shadow transition" className="inline-flex items-center rounded-lg bg-red-50 px-4 py-2 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100 shadow transition"
onClick={async () => { await deleteProduct(item.id); await load(); }} onClick={() => setDeleteTarget(item)}
> >
Delete Delete
</button> </button>
@ -116,6 +112,34 @@ export default function AdminSubscriptionsPage() {
<div className="col-span-full py-8 text-center text-sm text-gray-500">No subscriptions found.</div> <div className="col-span-full py-8 text-center text-sm text-gray-500">No subscriptions found.</div>
)} )}
</div> </div>
{/* Confirm Delete Modal */}
{deleteTarget && (
<div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/30" onClick={() => setDeleteTarget(null)} />
<div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-2xl bg-white shadow-xl ring-1 ring-gray-200">
<div className="px-6 pt-6">
<h3 className="text-lg font-semibold text-blue-900">Delete subscription?</h3>
<p className="mt-2 text-sm text-gray-700">You are about to delete "{deleteTarget.title}". This action cannot be undone.</p>
</div>
<div className="px-6 pb-6 pt-4 flex justify-end gap-3">
<button
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
onClick={() => setDeleteTarget(null)}
>
Cancel
</button>
<button
className="inline-flex items-center rounded-lg px-4 py-2 text-sm font-semibold text-white bg-red-600 hover:bg-red-500 shadow"
onClick={async () => { await deleteProduct(deleteTarget.id); setDeleteTarget(null); await load(); }}
>
Delete
</button>
</div>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
</PageLayout> </PageLayout>

View File

@ -42,6 +42,9 @@ const navLinks = [
{ name: 'About us', href: '/about-us' }, { name: 'About us', href: '/about-us' },
]; ];
// Toggle visibility of Shop navigation across header (desktop + mobile)
const showShop = false;
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [isDark, setIsDark] = useState(false) const [isDark, setIsDark] = useState(false)
@ -244,46 +247,47 @@ export default function Header() {
</button> </button>
</div> </div>
<PopoverGroup className="hidden lg:flex lg:gap-x-12"> <PopoverGroup className="hidden lg:flex lg:gap-x-12">
{/* Shop dropdown stays first */} {/* Shop dropdown stays first (hidden via flag) */}
<Popover> {showShop && (
<PopoverButton className="flex items-center gap-x-1 text-sm/6 font-semibold text-gray-900 dark:text-white"> <Popover>
Shop <PopoverButton className="flex items-center gap-x-1 text-sm/6 font-semibold text-gray-900 dark:text-white">
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" /> Shop
</PopoverButton> <ChevronDownIcon aria-hidden="true" className="size-5 flex-none text-gray-500" />
{/* ...existing Shop PopoverPanel... */} </PopoverButton>
<PopoverPanel <PopoverPanel
transition transition
className="absolute left-0 right-0 top-full z-50 rounded-b-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 dark:ring-white/15 overflow-hidden data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-enter:ease-out data-leave:duration-150 data-leave:ease-in" className="absolute left-0 right-0 top-full z-50 rounded-b-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 dark:ring-white/15 overflow-hidden data-closed:-translate-y-1 data-closed:opacity-0 data-enter:duration-200 data-enter:ease-out data-leave:duration-150 data-leave:ease-in"
style={{ style={{
background: 'linear-gradient(150deg, rgba(26,46,84,0.95) 0%, rgba(18,37,70,0.92) 45%, rgba(30,56,104,0.88) 100%)', background: 'linear-gradient(150deg, rgba(26,46,84,0.95) 0%, rgba(18,37,70,0.92) 45%, rgba(30,56,104,0.88) 100%)',
backdropFilter: 'blur(26px) saturate(175%)', backdropFilter: 'blur(26px) saturate(175%)',
WebkitBackdropFilter: 'blur(26px) saturate(175%)' WebkitBackdropFilter: 'blur(26px) saturate(175%)'
}} }}
> >
<div className="relative before:absolute before:inset-0 before:pointer-events-none before:bg-[radial-gradient(circle_at_18%_30%,rgba(56,124,255,0.30),transparent_62%),radial-gradient(circle_at_82%_40%,rgba(139,92,246,0.22),transparent_65%)]"> <div className="relative before:absolute before:inset-0 before:pointer-events-none before:bg-[radial-gradient(circle_at_18%_30%,rgba(56,124,255,0.30),transparent_62%),radial-gradient(circle_at_82%_40%,rgba(139,92,246,0.22),transparent_65%)]">
<div className="mx-auto grid max-w-7xl grid-cols-2 md:grid-cols-3 gap-x-4 px-6 py-10 lg:px-8 xl:gap-x-8"> <div className="mx-auto grid max-w-7xl grid-cols-2 md:grid-cols-3 gap-x-4 px-6 py-10 lg:px-8 xl:gap-x-8">
{shopItems.map(item => ( {shopItems.map(item => (
<div <div
key={item.name} key={item.name}
className="group relative rounded-lg p-6 text-sm/6 hover:bg-white/5 transition-colors" className="group relative rounded-lg p-6 text-sm/6 hover:bg-white/5 transition-colors"
>
<div className="flex size-11 items-center justify-center rounded-lg bg-white/10 backdrop-blur-md group-hover:bg-white/20 transition-colors">
<item.icon aria-hidden="true" className="size-6 text-gray-300 group-hover:text-white" />
</div>
<button
onClick={() => router.push(item.href)}
className="mt-6 block font-semibold text-white"
> >
{item.name} <div className="flex size-11 items-center justify-center rounded-lg bg-white/10 backdrop-blur-md group-hover:bg-white/20 transition-colors">
<span className="absolute inset-0" /> <item.icon aria-hidden="true" className="size-6 text-gray-300 group-hover:text-white" />
</button> </div>
<p className="mt-1 text-gray-300">{item.description}</p> <button
</div> onClick={() => router.push(item.href)}
))} className="mt-6 block font-semibold text-white"
>
{item.name}
<span className="absolute inset-0" />
</button>
<p className="mt-1 text-gray-300">{item.description}</p>
</div>
))}
</div>
</div> </div>
</div> </PopoverPanel>
</PopoverPanel> </Popover>
</Popover> )}
{/* Affiliate Links */} {/* Affiliate Links */}
<button <button
@ -572,24 +576,26 @@ export default function Header() {
</div> </div>
{/* Navigation / Shop after that */} {/* Navigation / Shop after that */}
<div className="space-y-2 py-6"> <div className="space-y-2 py-6">
<Disclosure as="div"> {showShop && (
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5"> <Disclosure as="div">
Shop <DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" /> Shop
</DisclosureButton> <ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
<DisclosurePanel className="mt-2 space-y-1"> </DisclosureButton>
{shopItems.map(item => ( <DisclosurePanel className="mt-2 space-y-1">
<DisclosureButton {shopItems.map(item => (
key={item.name} <DisclosureButton
as="button" key={item.name}
onClick={() => { router.push(item.href); setMobileMenuOpen(false); }} as="button"
className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" onClick={() => { router.push(item.href); setMobileMenuOpen(false); }}
> className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
{item.name} >
</DisclosureButton> {item.name}
))} </DisclosureButton>
</DisclosurePanel> ))}
</Disclosure> </DisclosurePanel>
</Disclosure>
)}
{/* Affiliate Links */} {/* Affiliate Links */}
<button <button
onClick={() => { router.push('/affiliate-links'); setMobileMenuOpen(false); }} onClick={() => { router.push('/affiliate-links'); setMobileMenuOpen(false); }}
@ -624,24 +630,26 @@ export default function Header() {
</> </>
) : ( ) : (
<div className="py-6 space-y-4"> <div className="py-6 space-y-4">
<Disclosure as="div"> {showShop && (
<DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5"> <Disclosure as="div">
Shop <DisclosureButton className="group flex w-full items-center justify-between rounded-lg py-2 px-3 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5">
<ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" /> Shop
</DisclosureButton> <ChevronDownIcon aria-hidden="true" className="size-5 flex-none group-data-open:rotate-180" />
<DisclosurePanel className="mt-2 space-y-1"> </DisclosureButton>
{shopItems.map(item => ( <DisclosurePanel className="mt-2 space-y-1">
<DisclosureButton {shopItems.map(item => (
key={item.name} <DisclosureButton
as="button" key={item.name}
onClick={() => { router.push(item.href); setMobileMenuOpen(false); }} as="button"
className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" onClick={() => { router.push(item.href); setMobileMenuOpen(false); }}
> className="block rounded-lg py-2 pl-6 pr-3 text-sm/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"
{item.name} >
</DisclosureButton> {item.name}
))} </DisclosureButton>
</DisclosurePanel> ))}
</Disclosure> </DisclosurePanel>
</Disclosure>
)}
<button <button
onClick={() => { router.push('/affiliate-links'); setMobileMenuOpen(false); }} onClick={() => { router.push('/affiliate-links'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left" className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"

View File

@ -1,6 +1,7 @@
'use client' 'use client'
import { useState } from 'react' import { useState } from 'react'
import { notFound } from 'next/navigation'
import { MagnifyingGlassIcon, FunnelIcon, Squares2X2Icon, ListBulletIcon, HeartIcon, StarIcon, ShoppingCartIcon } from '@heroicons/react/20/solid' import { MagnifyingGlassIcon, FunnelIcon, Squares2X2Icon, ListBulletIcon, HeartIcon, StarIcon, ShoppingCartIcon } from '@heroicons/react/20/solid'
import { HeartIcon as HeartOutlineIcon } from '@heroicons/react/24/outline' import { HeartIcon as HeartOutlineIcon } from '@heroicons/react/24/outline'
import { ChevronDownIcon } from '@heroicons/react/24/outline' import { ChevronDownIcon } from '@heroicons/react/24/outline'
@ -131,6 +132,8 @@ const sampleProducts = [
] ]
export default function StorePage() { export default function StorePage() {
const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true'
if (!SHOW_SHOP) notFound()
const [selectedCategory, setSelectedCategory] = useState('Alle Kategorien') const [selectedCategory, setSelectedCategory] = useState('Alle Kategorien')
const [searchQuery, setSearchQuery] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid') const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')

View File

@ -1,234 +1,64 @@
'use client' "use client"
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { notFound } from 'next/navigation'
import { StarIcon } from '@heroicons/react/20/solid' import { StarIcon } from '@heroicons/react/20/solid'
import { HeartIcon, ShoppingCartIcon } from '@heroicons/react/24/outline' import { HeartIcon, ShoppingCartIcon } from '@heroicons/react/24/outline'
import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid' import { HeartIcon as HeartIconSolid } from '@heroicons/react/24/solid'
import PageLayout from '../../components/PageLayout' import PageLayout from '../../components/PageLayout'
// Collections für die Promo Section
const collections = [
{
name: "Women's",
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-04-collection-01.jpg',
imageAlt: 'Woman wearing an off-white cotton t-shirt.',
},
{
name: "Men's",
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-04-collection-02.jpg',
imageAlt: 'Man wearing a charcoal gray cotton t-shirt.',
},
{
name: 'Desk Accessories',
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-04-collection-03.jpg',
imageAlt: 'Person sitting at a wooden desk with paper note organizer, pencil and tablet.',
},
]
// Featured Products für die Shop-Startseite
const featuredProducts = [ const featuredProducts = [
{ { id: 101, name: 'Black Basic Tee', price: '$32', href: '#', imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-01.jpg', imageAlt: "Model wearing women's black cotton crewneck tee." },
id: 101, { id: 102, name: 'Off-White Basic Tee', price: '$32', href: '#', imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-02.jpg', imageAlt: "Model wearing women's off-white cotton crewneck tee." },
name: 'Black Basic Tee', { id: 103, name: 'Mountains Artwork Tee', price: '$36', href: '#', imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-03.jpg', imageAlt: "Model wearing women's burgundy red crewneck artwork tee with small white triangle overlapping larger black triangle." },
price: '$32',
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-01.jpg',
imageAlt: "Model wearing women's black cotton crewneck tee.",
},
{
id: 102,
name: 'Off-White Basic Tee',
price: '$32',
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-02.jpg',
imageAlt: "Model wearing women's off-white cotton crewneck tee.",
},
{
id: 103,
name: 'Mountains Artwork Tee',
price: '$36',
href: '#',
imageSrc: 'https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-favorite-03.jpg',
imageAlt: "Model wearing women's burgundy red crewneck artwork tee with small white triangle overlapping larger black triangle.",
},
] ]
// Mock-Produktdaten im Tailwind UI Plus Format
const products = [ const products = [
{ { id: 1, name: 'Premium Bio-Kaffee Starter Set', price: '€24.99', rating: 5, reviewCount: 142, imageSrc: 'https://images.unsplash.com/photo-1559056199-641a0ac8b55e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Premium Bio-Kaffee Set mit Bohnen und Filter', href: '#', category: 'Getränke', inStock: true },
id: 1, { id: 2, name: 'Nachhaltiger Laptop-Ständer', price: '€89.99', rating: 5, reviewCount: 87, imageSrc: 'https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Ergonomischer Laptop-Ständer aus Bambus', href: '#', category: 'Technik', inStock: true },
name: 'Premium Bio-Kaffee Starter Set', { id: 3, name: 'Öko-Sportbekleidung Set', price: '€149.99', rating: 5, reviewCount: 203, imageSrc: 'https://images.unsplash.com/photo-1506629905607-b5f9a71351e8?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Nachhaltige Sportkleidung aus recycelten Materialien', href: '#', category: 'Kleidung', inStock: false },
price: '€24.99', { id: 4, name: 'Smart Home Energie-Monitor', price: '€199.99', rating: 4, reviewCount: 156, imageSrc: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Smart Home Gerät zur Energieüberwachung', href: '#', category: 'Technik', inStock: true },
rating: 5, { id: 5, name: 'Bio-Hautpflege Starter-Set', price: '€79.99', rating: 4, reviewCount: 92, imageSrc: 'https://images.unsplash.com/photo-1556228578-8c89e6adf883?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Natürliche Hautpflege Produkte ohne Chemikalien', href: '#', category: 'Beauty', inStock: true },
reviewCount: 142, { id: 6, name: 'Solarbetriebene Powerbank', price: '€129.99', rating: 5, reviewCount: 78, imageSrc: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Portable Solarenergie Powerbank', href: '#', category: 'Technik', inStock: true },
imageSrc: 'https://images.unsplash.com/photo-1559056199-641a0ac8b55e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', { id: 7, name: 'Nachhaltige Trinkflasche', price: '€25.99', rating: 4, reviewCount: 64, imageSrc: 'https://images.unsplash.com/photo-1523362628745-0c100150b504?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Wiederverwendbare Edelstahl Trinkflasche', href: '#', category: 'Lifestyle', inStock: true },
imageAlt: 'Premium Bio-Kaffee Set mit Bohnen und Filter', { id: 8, name: 'Öko-Notizbuch Set', price: '€19.99', rating: 5, reviewCount: 41, imageSrc: 'https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Recyceltes Papier Notizbuch Set', href: '#', category: 'Büro', inStock: true },
href: '#', { id: 9, name: 'Bambus Handy-Halterung', price: '€32.99', rating: 4, reviewCount: 24, imageSrc: 'https://images.unsplash.com/photo-1572635196237-14b3f281503f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80', imageAlt: 'Nachhaltige Bambus Handy-Halterung', href: '#', category: 'Technik', inStock: true },
category: 'Getränke',
inStock: true,
},
{
id: 2,
name: 'Nachhaltiger Laptop-Ständer',
price: '€89.99',
rating: 5,
reviewCount: 87,
imageSrc: 'https://images.unsplash.com/photo-1527864550417-7fd91fc51a46?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Ergonomischer Laptop-Ständer aus Bambus',
href: '#',
category: 'Technik',
inStock: true,
},
{
id: 3,
name: 'Öko-Sportbekleidung Set',
price: '€149.99',
rating: 5,
reviewCount: 203,
imageSrc: 'https://images.unsplash.com/photo-1506629905607-b5f9a71351e8?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Nachhaltige Sportkleidung aus recycelten Materialien',
href: '#',
category: 'Kleidung',
inStock: false,
},
{
id: 4,
name: 'Smart Home Energie-Monitor',
price: '€199.99',
rating: 4,
reviewCount: 156,
imageSrc: 'https://images.unsplash.com/photo-1558618666-fcd25c85cd64?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Smart Home Gerät zur Energieüberwachung',
href: '#',
category: 'Technik',
inStock: true,
},
{
id: 5,
name: 'Bio-Hautpflege Starter-Set',
price: '€79.99',
rating: 4,
reviewCount: 92,
imageSrc: 'https://images.unsplash.com/photo-1556228578-8c89e6adf883?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Natürliche Hautpflege Produkte ohne Chemikalien',
href: '#',
category: 'Beauty',
inStock: true,
},
{
id: 6,
name: 'Solarbetriebene Powerbank',
price: '€129.99',
rating: 5,
reviewCount: 78,
imageSrc: 'https://images.unsplash.com/photo-1609091839311-d5365f9ff1c5?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Portable Solarenergie Powerbank',
href: '#',
category: 'Technik',
inStock: true,
},
{
id: 7,
name: 'Nachhaltige Trinkflasche',
price: '€25.99',
rating: 4,
reviewCount: 64,
imageSrc: 'https://images.unsplash.com/photo-1523362628745-0c100150b504?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Wiederverwendbare Edelstahl Trinkflasche',
href: '#',
category: 'Lifestyle',
inStock: true,
},
{
id: 8,
name: 'Öko-Notizbuch Set',
price: '€19.99',
rating: 5,
reviewCount: 41,
imageSrc: 'https://images.unsplash.com/photo-1544716278-ca5e3f4abd8c?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Recyceltes Papier Notizbuch Set',
href: '#',
category: 'Büro',
inStock: true,
},
{
id: 9,
name: 'Bambus Handy-Halterung',
price: '€32.99',
rating: 4,
reviewCount: 24,
imageSrc: 'https://images.unsplash.com/photo-1572635196237-14b3f281503f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
imageAlt: 'Nachhaltige Bambus Handy-Halterung',
href: '#',
category: 'Technik',
inStock: true,
},
]
const stats = [
{ name: 'Total Users', stat: '71,897' },
{ name: 'Gold Members', stat: '68,161' },
{ name: 'Savings as Gold member', stat: '25k €' },
] ]
function classNames(...classes: (string | undefined | null | boolean)[]): string { function classNames(...classes: (string | undefined | null | boolean)[]): string {
return classes.filter(Boolean).join(' ') return classes.filter(Boolean).join(' ')
} }
export default function ShopPage() { export default function VipShopPage() {
const SHOW_SHOP = process.env.NEXT_PUBLIC_SHOW_SHOP === 'true'
if (!SHOW_SHOP) notFound()
const [favorites, setFavorites] = useState<number[]>([]) const [favorites, setFavorites] = useState<number[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
// Load favorites from localStorage on component mount
useEffect(() => { useEffect(() => {
const savedFavorites = localStorage.getItem('shop-favorites') const savedFavorites = localStorage.getItem('shop-favorites')
if (savedFavorites) { if (savedFavorites) {
try { try {
setFavorites(JSON.parse(savedFavorites)) setFavorites(JSON.parse(savedFavorites))
} catch (error) { } catch {
console.error('Error parsing favorites from localStorage:', error) // ignore
} }
} }
setIsLoading(false) setIsLoading(false)
}, []) }, [])
// Save favorites to localStorage whenever favorites change
useEffect(() => { useEffect(() => {
if (!isLoading) { if (!isLoading) localStorage.setItem('shop-favorites', JSON.stringify(favorites))
localStorage.setItem('shop-favorites', JSON.stringify(favorites))
}
}, [favorites, isLoading]) }, [favorites, isLoading])
const toggleFavorite = (productId: number) => { const toggleFavorite = (productId: number) => {
setFavorites(prev => { setFavorites(prev => prev.includes(productId) ? prev.filter(id => id !== productId) : [...prev, productId])
const newFavorites = prev.includes(productId)
? prev.filter(id => id !== productId)
: [...prev, productId]
// Show feedback to user
const product = products.find(p => p.id === productId)
if (product) {
if (newFavorites.includes(productId)) {
console.log(`❤️ ${product.name} zu Favoriten hinzugefügt`)
} else {
console.log(`💔 ${product.name} aus Favoriten entfernt`)
}
}
return newFavorites
})
} }
const addToCart = (productId: number) => { const addToCart = (productId: number) => {
const product = products.find(p => p.id === productId) const product = products.find(p => p.id === productId)
if (product) { if (product) console.log(`🛒 ${product.name} zum Warenkorb hinzugefügt`)
console.log(`🛒 ${product.name} zum Warenkorb hinzugefügt`)
// Hier würde die echte Add-to-Cart Logik implementiert werden
// z.B. API-Call oder Zustand-Update
}
} }
if (isLoading) { if (isLoading) {
@ -247,26 +77,10 @@ export default function ShopPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="bg-white"> <div className="bg-white">
{/* Promo Section with Stats Cards */}
<div className="relative overflow-hidden bg-white"> <div className="relative overflow-hidden bg-white">
<div className="pt-16 pb-80 sm:pt-24 sm:pb-40 lg:pt-40 lg:pb-48"> <div className="pt-16 pb-80 sm:pt-24 sm:pb-40 lg:pt-40 lg:pb-48">
<div className="relative mx-auto max-w-7xl px-4 sm:static sm:px-6 lg:px-8"> <div className="relative mx-auto max-w-7xl px-4 sm:static sm:px-6 lg:px-8">
<div className="sm:max-w-lg"> <div className="sm:max-w-lg">
{/* Stats Cards */}
{/* <div className="mb-8">
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-3 sm:gap-5">
{stats.map((item) => (
<div
key={item.name}
className="overflow-hidden rounded-lg bg-white px-4 py-5 shadow-lg border border-gray-200 sm:p-6"
>
<dt className="truncate text-sm font-medium text-gray-600">{item.name}</dt>
<dd className="mt-1 text-2xl font-semibold tracking-tight text-gray-900 sm:text-3xl">{item.stat}</dd>
</div>
))}
</dl>
</div> */}
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl"> <h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Shop with an infinite variety of products Shop with an infinite variety of products
</h1> </h1>
@ -276,76 +90,41 @@ export default function ShopPage() {
</div> </div>
<div> <div>
<div className="mt-10"> <div className="mt-10">
{/* Decorative image grid */} <div aria-hidden="true" className="pointer-events-none lg:absolute lg:inset-y-0 lg:mx-auto lg:w-full lg:max-w-7xl">
<div
aria-hidden="true"
className="pointer-events-none lg:absolute lg:inset-y-0 lg:mx-auto lg:w-full lg:max-w-7xl"
>
<div className="absolute transform sm:top-0 sm:left-1/2 sm:translate-x-8 lg:top-1/2 lg:left-1/2 lg:translate-x-8 lg:-translate-y-1/2"> <div className="absolute transform sm:top-0 sm:left-1/2 sm:translate-x-8 lg:top-1/2 lg:left-1/2 lg:translate-x-8 lg:-translate-y-1/2">
<div className="flex items-center space-x-6 lg:space-x-8"> <div className="flex items-center space-x-6 lg:space-x-8">
<div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8"> <div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8">
<div className="h-64 w-44 overflow-hidden rounded-lg sm:opacity-0 lg:opacity-100"> <div className="h-64 w-44 overflow-hidden rounded-lg sm:opacity-0 lg:opacity-100">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-01.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-01.jpg"
className="size-full object-cover"
/>
</div> </div>
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-02.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-02.jpg"
className="size-full object-cover"
/>
</div> </div>
</div> </div>
<div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8"> <div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8">
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-03.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-03.jpg"
className="size-full object-cover"
/>
</div> </div>
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-04.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-04.jpg"
className="size-full object-cover"
/>
</div> </div>
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-05.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-05.jpg"
className="size-full object-cover"
/>
</div> </div>
</div> </div>
<div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8"> <div className="grid shrink-0 grid-cols-1 gap-y-6 lg:gap-y-8">
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-06.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-06.jpg"
className="size-full object-cover"
/>
</div> </div>
<div className="h-64 w-44 overflow-hidden rounded-lg"> <div className="h-64 w-44 overflow-hidden rounded-lg">
<img <img alt="" src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-07.jpg" className="size-full object-cover" />
alt=""
src="https://tailwindcss.com/plus-assets/img/ecommerce-images/home-page-03-hero-image-tile-07.jpg"
className="size-full object-cover"
/>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<a <a href="#" className="inline-block rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-center font-medium text-white hover:bg-indigo-700">
href="#"
className="inline-block rounded-md border border-transparent bg-indigo-600 px-8 py-3 text-center font-medium text-white hover:bg-indigo-700"
>
Shop Collection Shop Collection
</a> </a>
</div> </div>
@ -354,7 +133,6 @@ export default function ShopPage() {
</div> </div>
</div> </div>
{/* Featured Products Section */}
<div className="bg-white"> <div className="bg-white">
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8"> <div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 sm:py-24 lg:px-8">
<div className="sm:flex sm:items-baseline sm:justify-between"> <div className="sm:flex sm:items-baseline sm:justify-between">
@ -368,11 +146,7 @@ export default function ShopPage() {
<div className="mt-6 grid grid-cols-1 gap-y-10 sm:grid-cols-3 sm:gap-x-6 sm:gap-y-0 lg:gap-x-8"> <div className="mt-6 grid grid-cols-1 gap-y-10 sm:grid-cols-3 sm:gap-x-6 sm:gap-y-0 lg:gap-x-8">
{featuredProducts.map((product) => ( {featuredProducts.map((product) => (
<div key={product.id} className="group relative"> <div key={product.id} className="group relative">
<img <img alt={product.imageAlt} src={product.imageSrc} className="h-96 w-full rounded-lg object-cover group-hover:opacity-75 sm:aspect-2/3 sm:h-auto" />
alt={product.imageAlt}
src={product.imageSrc}
className="h-96 w-full rounded-lg object-cover group-hover:opacity-75 sm:aspect-2/3 sm:h-auto"
/>
<h3 className="mt-4 text-base font-semibold text-gray-900"> <h3 className="mt-4 text-base font-semibold text-gray-900">
<a href={product.href}> <a href={product.href}>
<span className="absolute inset-0" /> <span className="absolute inset-0" />
@ -393,7 +167,6 @@ export default function ShopPage() {
</div> </div>
</div> </div>
{/* Products Section - Tailwind UI Plus "Product Grid" */}
<div className="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8"> <div className="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8">
<h2 className="sr-only">Products</h2> <h2 className="sr-only">Products</h2>
@ -401,71 +174,31 @@ export default function ShopPage() {
{products.map((product) => ( {products.map((product) => (
<a key={product.id} href={product.href} className="group"> <a key={product.id} href={product.href} className="group">
<div className="relative"> <div className="relative">
{/* Product Image */}
<div className="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg bg-gray-200 xl:aspect-h-8 xl:aspect-w-7"> <div className="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg bg-gray-200 xl:aspect-h-8 xl:aspect-w-7">
<img <img alt={product.imageAlt} src={product.imageSrc} className="h-full w-full object-cover object-center group-hover:opacity-75" />
alt={product.imageAlt}
src={product.imageSrc}
className="h-full w-full object-cover object-center group-hover:opacity-75"
/>
{/* Favorite Button - Now with better positioning */}
<button <button
onClick={(e) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); toggleFavorite(product.id) }}
e.preventDefault() className={classNames('absolute top-3 right-3 rounded-full bg-white p-2 text-gray-400 shadow-sm hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]', 'opacity-0 transition-opacity group-hover:opacity-100', favorites.includes(product.id) ? 'opacity-100 text-red-500 hover:text-red-600' : '')}
e.stopPropagation()
toggleFavorite(product.id)
}}
className={classNames(
'absolute top-3 right-3 rounded-full bg-white p-2 text-gray-400 shadow-sm hover:bg-gray-50 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-[#8D6B1D]',
'opacity-0 transition-opacity group-hover:opacity-100',
favorites.includes(product.id) ? 'opacity-100 text-red-500 hover:text-red-600' : ''
)}
title={favorites.includes(product.id) ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'} title={favorites.includes(product.id) ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'}
> >
{favorites.includes(product.id) ? ( {favorites.includes(product.id) ? (<HeartIconSolid className="h-5 w-5" />) : (<HeartIcon className="h-5 w-5" />)}
<HeartIconSolid className="h-5 w-5" />
) : (
<HeartIcon className="h-5 w-5" />
)}
</button> </button>
{/* Category Badge */}
<div className="absolute top-3 left-3"> <div className="absolute top-3 left-3">
<span className="inline-flex items-center rounded-full bg-[#8D6B1D] px-2 py-1 text-xs font-medium text-white"> <span className="inline-flex items-center rounded-full bg-[#8D6B1D] px-2 py-1 text-xs font-medium text-white">{product.category}</span>
{product.category}
</span>
</div> </div>
{/* Out of Stock Overlay */}
{!product.inStock && ( {!product.inStock && (
<div className="absolute inset-0 flex items-center justify-center rounded-lg bg-black bg-opacity-50"> <div className="absolute inset-0 flex items-center justify-center rounded-lg bg-black bg-opacity-50">
<span className="rounded-md bg-white px-3 py-1 text-sm font-semibold text-gray-900"> <span className="rounded-md bg-white px-3 py-1 text-sm font-semibold text-gray-900">Ausverkauft</span>
Ausverkauft
</span>
</div> </div>
)} )}
</div> </div>
{/* Product Info */}
<div className="mt-4 flex justify-between"> <div className="mt-4 flex justify-between">
<div> <div>
<h3 className="text-sm text-gray-700"> <h3 className="text-sm text-gray-700">{product.name}</h3>
{product.name}
</h3>
{/* Rating */}
<div className="mt-1 flex items-center"> <div className="mt-1 flex items-center">
<div className="flex items-center"> <div className="flex items-center">
{[0, 1, 2, 3, 4].map((rating) => ( {[0, 1, 2, 3, 4].map((rating) => (
<StarIcon <StarIcon key={rating} aria-hidden="true" className={classNames(product.rating > rating ? 'text-yellow-400' : 'text-gray-300', 'h-4 w-4 flex-shrink-0')} />
key={rating}
aria-hidden="true"
className={classNames(
product.rating > rating ? 'text-yellow-400' : 'text-gray-300',
'h-4 w-4 flex-shrink-0'
)}
/>
))} ))}
</div> </div>
<p className="ml-1 text-sm text-gray-500">({product.reviewCount})</p> <p className="ml-1 text-sm text-gray-500">({product.reviewCount})</p>
@ -473,21 +206,10 @@ export default function ShopPage() {
</div> </div>
<p className="text-lg font-medium text-gray-900">{product.price}</p> <p className="text-lg font-medium text-gray-900">{product.price}</p>
</div> </div>
{/* Add to Cart Button */}
<button <button
onClick={(e) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); addToCart(product.id) }}
e.preventDefault()
e.stopPropagation()
addToCart(product.id)
}}
disabled={!product.inStock} disabled={!product.inStock}
className={classNames( className={classNames('mt-4 flex w-full items-center justify-center rounded-md border border-transparent px-8 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2', product.inStock ? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A]' : 'bg-gray-100 text-gray-400 cursor-not-allowed')}
'mt-4 flex w-full items-center justify-center rounded-md border border-transparent px-8 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-[#8D6B1D] focus:ring-offset-2',
product.inStock
? 'bg-[#8D6B1D] text-white hover:bg-[#7A5E1A]'
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
)}
> >
<ShoppingCartIcon className="mr-2 h-4 w-4" /> <ShoppingCartIcon className="mr-2 h-4 w-4" />
{product.inStock ? 'In den Warenkorb' : 'Ausverkauft'} {product.inStock ? 'In den Warenkorb' : 'Ausverkauft'}