Shop integration

This commit is contained in:
2026-01-14 17:47:58 +01:00
parent be7f7b7bf7
commit 21b78f8d17
52 changed files with 5288 additions and 198 deletions

View File

@@ -0,0 +1,795 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
type Tab = 'dashboard' | 'shop' | 'editorial' | 'orders';
type Section = {
id: string;
type: 'text' | 'image';
content: string;
};
// Mock Data Types
type Product = { id?: number; title: string; price: number; image: string; images: string[]; description?: string; details?: string[] };
type Article = { id?: number; title: string; date: string; image: string; sections: Section[]; category?: string; isFeatured?: boolean };
const Admin: React.FC = () => {
const [activeTab, setActiveTab] = useState<Tab>('dashboard');
// Modal State
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalMode, setModalMode] = useState<'add' | 'edit'>('add');
const [editType, setEditType] = useState<'shop' | 'editorial' | null>(null);
const [productForm, setProductForm] = useState<Product>({ id: '', title: '', price: 0, image: '', images: [], details: [] });
const [articleForm, setArticleForm] = useState<Article>({ id: '', title: '', date: '', image: '', sections: [], isFeatured: false });
const [pendingSectionId, setPendingSectionId] = useState<string | null>(null);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const galleryInputRef = React.useRef<HTMLInputElement>(null);
// Handlers
const openAddModal = (type: 'shop' | 'editorial') => {
setEditType(type);
setModalMode('add');
// Reset forms
setProductForm({ title: '', price: 0, image: '', images: [], details: [] });
setArticleForm({ title: '', date: new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit' }), image: '', sections: [], isFeatured: false });
setIsModalOpen(true);
};
const openEditModal = (type: 'shop' | 'editorial', item: any) => {
setEditType(type);
setModalMode('edit');
if (type === 'shop') {
setProductForm({ ...item, images: item.images || [], details: item.details || [] });
} else {
setArticleForm({
...item,
sections: item.sections || [],
isFeatured: !!item.isFeatured
});
}
setIsModalOpen(true);
};
const addSection = (type: 'text' | 'image') => {
setArticleForm(prev => ({
...prev,
sections: [...prev.sections, { id: Math.random().toString(), type, content: '' }]
}));
};
const updateSection = (id: string, content: string) => {
setArticleForm(prev => ({
...prev,
sections: prev.sections.map(s => s.id === id ? { ...s, content } : s)
}));
};
const removeSection = (id: string) => {
setArticleForm(prev => ({
...prev,
sections: prev.sections.filter(s => s.id !== id)
}));
};
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>, isGallery: boolean = false) => {
const files = e.target.files;
if (files && files[0]) {
const reader = new FileReader();
reader.onload = (event) => {
const url = event.target?.result as string;
if (pendingSectionId) {
setArticleForm(prev => ({
...prev,
sections: prev.sections.map(s => s.id === pendingSectionId ? { ...s, content: url } : s)
}));
setPendingSectionId(null);
} else if (isGallery) {
// For gallery we handle multiple if needed, but let's stick to simple one at a time for base64
setProductForm(prev => ({ ...prev, images: [...prev.images, url] }));
} else {
if (editType === 'shop') {
setProductForm(prev => ({ ...prev, image: url }));
} else {
setArticleForm(prev => ({ ...prev, image: url }));
}
}
};
reader.readAsDataURL(files[0]);
}
};
const {
products,
articles,
orders,
fetchOrders,
updateOrderStatus,
addProduct,
updateProduct,
deleteProduct,
addArticle,
updateArticle,
deleteArticle
} = useStore();
const [selectedOrder, setSelectedOrder] = useState<any>(null);
React.useEffect(() => {
if (activeTab === 'orders') {
fetchOrders();
}
}, [activeTab]);
const handleSave = async () => {
try {
if (editType === 'shop') {
const newProduct = {
...productForm,
id: modalMode === 'add' ? undefined : productForm.id,
slug: productForm.title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]/g, ''),
number: '01',
images: productForm.images.includes(productForm.image) ? productForm.images : [productForm.image, ...productForm.images.filter(img => img !== productForm.image)],
aspectRatio: 'aspect-[4/5]',
details: productForm.details || []
};
if (modalMode === 'add') {
await addProduct(newProduct as any);
} else {
await updateProduct(newProduct as any);
}
} else {
const newArticle = {
...articleForm,
id: modalMode === 'add' ? undefined : articleForm.id,
slug: articleForm.title.toLowerCase().replace(/ /g, '-').replace(/[^\w-]/g, ''),
category: articleForm.category || 'Studio Life',
description: articleForm.sections.find(s => s.type === 'text')?.content?.substring(0, 150) || 'New article...',
isFeatured: articleForm.isFeatured
};
if (modalMode === 'add') {
await addArticle(newArticle as any);
} else {
await updateArticle(newArticle as any);
}
}
alert('Saved successfully!');
setIsModalOpen(false);
} catch (err) {
console.error('Save failed:', err);
alert('Failed to save. Please make sure the server is running.');
}
};
return (
<div className="flex h-screen bg-stone-100 dark:bg-stone-900 font-body relative overflow-hidden">
<AnimatePresence>
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setIsModalOpen(false)}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 50, scale: 0.95 }}
className="bg-white dark:bg-stone-900 w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg shadow-2xl relative z-10 flex flex-col"
>
{/* Modal Header */}
<div className="p-8 border-b border-stone-100 dark:border-stone-800 flex justify-between items-center sticky top-0 bg-white dark:bg-stone-900 z-20">
<div>
<span className="text-xs font-bold uppercase tracking-widest text-stone-400">
{modalMode === 'add' ? 'Create New' : 'Editing'}
</span>
<h2 className="font-display text-3xl text-text-main dark:text-white mt-1">
{editType === 'shop' ? (modalMode === 'add' ? 'Product' : productForm.title) : (modalMode === 'add' ? 'Article' : articleForm.title)}
</h2>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-stone-100 dark:hover:bg-stone-800 rounded-full transition-colors">
<svg className="w-6 h-6 text-stone-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-8 space-y-8 flex-1">
{editType === 'shop' ? (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="space-y-6">
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Product Title</label>
<input
type="text"
value={productForm.title}
onChange={(e) => setProductForm({ ...productForm, title: e.target.value })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-4 text-lg focus:outline-none focus:border-stone-400 rounded-sm"
placeholder="e.g. Speckled Vase"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Price ($)</label>
<input
type="number"
value={productForm.price || ''}
onChange={(e) => setProductForm({ ...productForm, price: parseFloat(e.target.value) })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-4 text-lg focus:outline-none focus:border-stone-400 rounded-sm"
placeholder="0.00"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Description</label>
<textarea
value={productForm.description || ''}
onChange={(e) => setProductForm({ ...productForm, description: e.target.value })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400 h-32 rounded-sm resize-none"
placeholder="Describe the product details..."
/>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Product Details (List)</label>
<div className="space-y-2">
{(productForm.details || []).map((detail, idx) => (
<div key={idx} className="flex gap-2">
<input
type="text"
value={detail}
onChange={(e) => {
const newDetails = [...(productForm.details || [])];
newDetails[idx] = e.target.value;
setProductForm({ ...productForm, details: newDetails });
}}
className="flex-1 bg-white dark:bg-black border border-stone-200 dark:border-stone-800 p-2 text-sm rounded-sm"
/>
<button
onClick={() => {
const newDetails = productForm.details?.filter((_, i) => i !== idx);
setProductForm({ ...productForm, details: newDetails });
}}
className="text-red-400 hover:text-red-600 px-2"
>
×
</button>
</div>
))}
<button
onClick={() => setProductForm({ ...productForm, details: [...(productForm.details || []), ''] })}
className="text-xs uppercase tracking-widest text-stone-400 hover:text-black dark:hover:text-white"
>
+ Add Detail
</button>
</div>
</div>
</div>
{/* Shop Image Uploader */}
<div className="space-y-4">
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Main Image</label>
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-stone-200 dark:border-stone-800 rounded-lg h-64 flex flex-col items-center justify-center text-stone-400 hover:border-stone-400 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-all cursor-pointer group relative overflow-hidden"
>
<input
type="file"
ref={fileInputRef}
onChange={(e) => handleImageUpload(e)}
className="hidden"
accept="image/*"
/>
{productForm.image ? (
<img src={productForm.image} alt="Preview" className="h-full w-full object-cover" />
) : (
<>
<svg className="w-10 h-10 mb-4 text-stone-300 group-hover:text-stone-500 transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="text-sm font-medium">Upload Main Image</span>
</>
)}
</div>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Gallery Images</label>
<div className="grid grid-cols-4 gap-4">
{productForm.images.map((img, idx) => (
<div key={idx} className="aspect-square bg-stone-100 rounded-lg overflow-hidden relative group">
<img src={img} alt="" className="w-full h-full object-cover" />
<button
onClick={(e) => {
e.stopPropagation();
setProductForm(prev => ({ ...prev, images: prev.images.filter((_, i) => i !== idx) }));
}}
className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
</button>
</div>
))}
<div
onClick={() => galleryInputRef.current?.click()}
className="aspect-square border-2 border-dashed border-stone-200 dark:border-stone-800 rounded-lg flex flex-col items-center justify-center text-stone-400 hover:border-stone-400 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-all cursor-pointer"
>
<input
type="file"
ref={galleryInputRef}
onChange={(e) => handleImageUpload(e, true)}
className="hidden"
accept="image/*"
multiple
/>
<span className="text-2xl">+</span>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="space-y-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="col-span-2 space-y-6">
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Article Title</label>
<input
type="text"
value={articleForm.title}
onChange={(e) => setArticleForm({ ...articleForm, title: e.target.value })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-4 text-2xl font-display focus:outline-none focus:border-stone-400 rounded-sm"
placeholder="Enter an engaging title..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Publish Date</label>
<input
type="text"
value={articleForm.date}
onChange={(e) => setArticleForm({ ...articleForm, date: e.target.value })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-3 text-sm focus:outline-none focus:border-stone-400 rounded-sm"
/>
</div>
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Category</label>
<select
value={articleForm.category || 'Studio Life'}
onChange={(e) => setArticleForm({ ...articleForm, category: e.target.value })}
className="w-full bg-stone-50 dark:bg-black border border-stone-200 dark:border-stone-800 p-3 text-sm focus:outline-none focus:border-stone-400 rounded-sm"
>
<option>Guide</option>
<option>Studio Life</option>
<option>Technique</option>
</select>
</div>
<div className="flex items-center space-x-3 pt-6">
<input
type="checkbox"
id="isFeatured"
checked={articleForm.isFeatured}
onChange={(e) => setArticleForm({ ...articleForm, isFeatured: e.target.checked })}
className="w-4 h-4 rounded border-stone-300 text-stone-900 focus:ring-stone-900"
/>
<label htmlFor="isFeatured" className="text-xs uppercase tracking-widest text-stone-500 cursor-pointer">Featured Article</label>
</div>
</div>
</div>
{/* Cover Image */}
<div>
<label className="block text-xs uppercase tracking-widest text-stone-500 mb-2">Cover Image</label>
<div
onClick={() => fileInputRef.current?.click()}
className="border-2 border-dashed border-stone-200 dark:border-stone-800 rounded-lg h-48 flex flex-col items-center justify-center text-stone-400 hover:border-stone-400 hover:bg-stone-50 dark:hover:bg-stone-800/50 transition-all cursor-pointer"
>
<input
type="file"
ref={fileInputRef}
onChange={(e) => handleImageUpload(e)}
className="hidden"
accept="image/*"
/>
{articleForm.image ? (
<img src={articleForm.image} alt="Cover" className="h-full w-full object-cover rounded-lg" />
) : (
<span className="text-xs">Upload Cover</span>
)}
</div>
</div>
</div>
<hr className="border-stone-100 dark:border-stone-800" />
{/* Content Builder */}
<div>
<div className="flex justify-between items-center mb-6">
<label className="block text-xs uppercase tracking-widest text-stone-500">Content Sections</label>
</div>
<div className="space-y-4 mb-6">
{articleForm.sections.map((section, index) => (
<div key={section.id} className="group relative border border-stone-200 dark:border-stone-800 rounded-md p-4 bg-stone-50/50 dark:bg-stone-900/50">
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => removeSection(section.id)} className="text-red-400 hover:text-red-600 text-xs uppercase font-bold p-2">Remove</button>
</div>
<span className="text-[10px] uppercase text-stone-400 mb-2 block tracking-wider">{index + 1}. {section.type}</span>
{section.type === 'text' ? (
<textarea
value={section.content}
onChange={(e) => updateSection(section.id, e.target.value)}
className="w-full bg-white dark:bg-black border border-stone-200 dark:border-stone-800 p-3 text-sm focus:outline-none focus:border-stone-400 rounded-sm resize-y"
rows={3}
placeholder="Write your paragraph here..."
/>
) : (
<div
onClick={() => {
setPendingSectionId(section.id);
fileInputRef.current?.click();
}}
className="border border-dashed border-stone-300 dark:border-stone-700 bg-white dark:bg-black p-4 rounded-sm flex items-center justify-center h-24 text-stone-400 hover:border-stone-500 cursor-pointer overflow-hidden"
>
{section.content ? (
<img src={section.content} alt="Section" className="h-full w-full object-cover" />
) : (
<span className="text-xs">Select Image</span>
)}
</div>
)}
</div>
))}
{articleForm.sections.length === 0 && (
<div className="text-center py-12 border-2 border-dashed border-stone-100 dark:border-stone-800 rounded-lg">
<p className="text-stone-400 text-sm">Start building your story</p>
</div>
)}
</div>
<div className="flex gap-4 justify-center">
<button onClick={() => addSection('text')} className="flex items-center gap-2 px-4 py-2 bg-stone-100 dark:bg-stone-800 hover:bg-stone-200 dark:hover:bg-stone-700 rounded-full text-xs font-bold uppercase tracking-wider transition-colors">
<span>+ Add Text</span>
</button>
<button onClick={() => addSection('image')} className="flex items-center gap-2 px-4 py-2 bg-stone-100 dark:bg-stone-800 hover:bg-stone-200 dark:hover:bg-stone-700 rounded-full text-xs font-bold uppercase tracking-wider transition-colors">
<span>+ Add Image</span>
</button>
</div>
</div>
</div>
)}
</div>
<div className="p-8 border-t border-stone-100 dark:border-stone-800 bg-stone-50 dark:bg-stone-900/50 flex justify-end gap-4 rounded-b-lg">
<button
onClick={() => setIsModalOpen(false)}
className="px-6 py-3 text-xs uppercase tracking-widest text-stone-500 hover:text-black dark:hover:text-white transition-colors"
>
Cancel
</button>
<button
onClick={handleSave}
className="bg-black dark:bg-white text-white dark:text-black px-8 py-3 text-xs uppercase tracking-widest font-bold hover:opacity-90 transition-opacity shadow-lg"
>
{modalMode === 'add' ? 'Create Item' : 'Save Changes'}
</button>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
{/* Sidebar */}
<aside className="w-64 bg-white dark:bg-black border-r border-stone-200 dark:border-stone-800 flex flex-col pt-32 pb-6 px-6 fixed h-full z-10">
<Link to="/" className="font-display text-2xl mb-12 block hover:text-stone-500 transition-colors">
Back to Site
</Link>
<nav className="space-y-2">
<button
onClick={() => setActiveTab('dashboard')}
className={`w-full text-left px-4 py-3 rounded-md transition-colors ${activeTab === 'dashboard' ? 'bg-stone-100 dark:bg-stone-800 font-medium' : 'hover:bg-stone-50 dark:hover:bg-stone-900 text-stone-500'}`}
>
Dashboard
</button>
<button
onClick={() => setActiveTab('shop')}
className={`w-full text-left px-4 py-3 rounded-md transition-colors ${activeTab === 'shop' ? 'bg-stone-100 dark:bg-stone-800 font-medium' : 'hover:bg-stone-50 dark:hover:bg-stone-900 text-stone-500'}`}
>
Shop Products
</button>
<button
onClick={() => setActiveTab('editorial')}
className={`w-full text-left px-4 py-3 rounded-md transition-colors ${activeTab === 'editorial' ? 'bg-stone-100 dark:bg-stone-800 font-medium' : 'hover:bg-stone-50 dark:hover:bg-stone-900 text-stone-500'}`}
>
Editorial
</button>
<button
onClick={() => setActiveTab('orders')}
className={`w-full text-left px-4 py-3 rounded-md transition-colors ${activeTab === 'orders' ? 'bg-stone-100 dark:bg-stone-800 font-medium' : 'hover:bg-stone-50 dark:hover:bg-stone-900 text-stone-500'}`}
>
Orders
</button>
</nav>
<div className="mt-auto text-xs text-stone-400">
Admin v0.2.0
</div>
</aside>
{/* Main Content */}
<main className="flex-1 ml-64 p-12 overflow-y-auto pt-32 h-full">
<div className="max-w-6xl mx-auto">
<h1 className="font-display text-4xl mb-8 capitalize">{activeTab}</h1>
{activeTab === 'dashboard' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className="bg-white dark:bg-black p-8 rounded-sm shadow-sm border border-stone-100 dark:border-stone-800">
<h3 className="text-stone-500 uppercase tracking-widest text-xs mb-2">Total Products</h3>
<p className="font-display text-6xl">{products.length}</p>
</div>
<div className="bg-white dark:bg-black p-8 rounded-sm shadow-sm border border-stone-100 dark:border-stone-800">
<h3 className="text-stone-500 uppercase tracking-widest text-xs mb-2">Published Articles</h3>
<p className="font-display text-6xl">{articles.length}</p>
</div>
</div>
)}
{activeTab === 'shop' && (
<div className="space-y-6">
<div className="flex justify-end">
<button
onClick={() => openAddModal('shop')}
className="bg-black dark:bg-white text-white dark:text-black px-6 py-3 uppercase tracking-widest text-xs font-bold hover:shadow-lg hover:-translate-y-0.5 transition-all"
>
+ Add Product
</button>
</div>
<div className="bg-white dark:bg-black rounded-sm shadow-sm border border-stone-100 dark:border-stone-800 overflow-hidden">
<table className="w-full text-left">
<thead className="bg-stone-50 dark:bg-stone-900 text-xs uppercase tracking-widest text-stone-500">
<tr>
<th className="px-6 py-4">Product</th>
<th className="px-6 py-4">Price</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100 dark:divide-stone-800">
{products.map(item => (
<tr key={item.id} className="hover:bg-stone-50 dark:hover:bg-stone-900 transition-colors">
<td className="px-6 py-4 flex items-center space-x-4">
<div className="w-12 h-12 bg-stone-100 rounded-sm overflow-hidden border border-stone-200 dark:border-stone-800">
<img src={item.image} alt="" className="w-full h-full object-cover" />
</div>
<span className="font-medium">{item.title}</span>
</td>
<td className="px-6 py-4 font-light">${item.price}</td>
<td className="px-6 py-4 text-right space-x-4">
<button
onClick={() => openEditModal('shop', item)}
className="text-xs uppercase tracking-wider text-stone-400 hover:text-black dark:hover:text-white transition-colors"
>
Edit
</button>
<button
onClick={() => { if (confirm('Delete this product?')) deleteProduct(item.id); }}
className="text-xs uppercase tracking-wider text-red-300 hover:text-red-500 transition-colors"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'editorial' && (
<div className="space-y-6">
<div className="flex justify-end">
<button
onClick={() => openAddModal('editorial')}
className="bg-black dark:bg-white text-white dark:text-black px-6 py-3 uppercase tracking-widest text-xs font-bold hover:shadow-lg hover:-translate-y-0.5 transition-all"
>
+ Add Article
</button>
</div>
<div className="bg-white dark:bg-black rounded-sm shadow-sm border border-stone-100 dark:border-stone-800 overflow-hidden">
<table className="w-full text-left">
<thead className="bg-stone-50 dark:bg-stone-900 text-xs uppercase tracking-widest text-stone-500">
<tr>
<th className="px-6 py-4">Status</th>
<th className="px-6 py-4">Title</th>
<th className="px-6 py-4">Date</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100 dark:divide-stone-800">
{articles.map(post => (
<tr key={post.id} className="hover:bg-stone-50 dark:hover:bg-stone-900 transition-colors">
<td className="px-6 py-4"><span className="inline-block w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]"></span></td>
<td className="px-6 py-4 font-medium">
<div className="flex items-center space-x-2">
{post.isFeatured && <span className="text-yellow-500 text-xs"></span>}
<span>{post.title}</span>
</div>
</td>
<td className="px-6 py-4 text-stone-500 text-sm">{post.date}</td>
<td className="px-6 py-4 text-right space-x-4">
{!post.isFeatured && (
<button
onClick={() => updateArticle({ ...post, isFeatured: true })}
className="text-xs uppercase tracking-wider text-yellow-600 hover:text-yellow-700 transition-colors font-medium"
>
Feature
</button>
)}
<button
onClick={() => openEditModal('editorial', post)}
className="text-xs uppercase tracking-wider text-stone-400 hover:text-black dark:hover:text-white transition-colors"
>
Edit
</button>
<button
onClick={() => { if (confirm('Delete this article?')) deleteArticle(post.id); }}
className="text-xs uppercase tracking-wider text-red-300 hover:text-red-500 transition-colors"
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'orders' && (
<div className="space-y-6">
<div className="bg-white dark:bg-black rounded-sm shadow-sm border border-stone-100 dark:border-stone-800 overflow-hidden">
<table className="w-full text-left">
<thead className="bg-stone-50 dark:bg-stone-900 text-xs uppercase tracking-widest text-stone-500">
<tr>
<th className="px-6 py-4">Order ID</th>
<th className="px-6 py-4">Customer</th>
<th className="px-6 py-4 text-center">Total</th>
<th className="px-6 py-4 text-center">Status</th>
<th className="px-6 py-4 text-center">Date</th>
<th className="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-stone-100 dark:divide-stone-800">
{orders.map(order => (
<tr key={order.id} className="hover:bg-stone-50 dark:hover:bg-stone-900 transition-colors">
<td className="px-6 py-4 font-medium">#{order.id}</td>
<td className="px-6 py-4 font-light">{order.customer_name}</td>
<td className="px-6 py-4 font-light text-center">${order.total_amount}</td>
<td className="px-6 py-4 text-center">
<span className={`px-2 py-1 text-[10px] uppercase font-bold tracking-tighter rounded-sm ${order.shipping_status === 'shipped' ? 'bg-blue-100 text-blue-600' : order.shipping_status === 'delivered' ? 'bg-green-100 text-green-600' : 'bg-stone-100 text-stone-600'}`}>
{order.shipping_status}
</span>
</td>
<td className="px-6 py-4 text-stone-400 font-light text-center">{new Date(order.created_at).toLocaleDateString()}</td>
<td className="px-6 py-4 text-right">
<button
onClick={() => setSelectedOrder(order)}
className="text-stone-400 hover:text-black dark:hover:text-white transition-colors"
>
<span className="material-symbols-outlined text-base">visibility</span>
</button>
</td>
</tr>
))}
{orders.length === 0 && (
<tr>
<td colSpan={6} className="px-6 py-24 text-center text-stone-400 font-light">No orders found.</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
</div>
</main>
<AnimatePresence>
{selectedOrder && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedOrder(null)}
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, y: 50, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 50, scale: 0.95 }}
className="bg-white dark:bg-stone-900 w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-lg shadow-2xl relative z-10 flex flex-col"
>
<div className="p-8 border-b border-stone-100 dark:border-stone-800 flex justify-between items-center sticky top-0 bg-white dark:bg-stone-900 z-20">
<div>
<span className="text-xs font-bold uppercase tracking-widest text-stone-400">Order #{selectedOrder.id}</span>
<h2 className="font-display text-3xl text-text-main dark:text-white mt-1">Fulfillment Details</h2>
</div>
<button onClick={() => setSelectedOrder(null)} className="p-2 hover:bg-stone-100 dark:hover:bg-stone-800 rounded-full transition-colors">
<span className="material-symbols-outlined text-stone-500">close</span>
</button>
</div>
<div className="p-8 flex-1 grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="space-y-8">
<section>
<h3 className="text-[10px] uppercase font-bold tracking-widest text-stone-400 mb-4">Customer Info</h3>
<div className="space-y-1 text-sm font-light">
<p className="font-medium">{selectedOrder.customer_name}</p>
<p>{selectedOrder.customer_email}</p>
<p className="pt-2">{selectedOrder.shipping_address.address}</p>
<p>{selectedOrder.shipping_address.city}, {selectedOrder.shipping_address.postalCode}</p>
</div>
</section>
<section>
<h3 className="text-[10px] uppercase font-bold tracking-widest text-stone-400 mb-4">Items Summary</h3>
<div className="space-y-4">
{selectedOrder.items.map((item: any, idx: number) => (
<div key={idx} className="flex justify-between text-sm">
<span className="font-light">{item.title} x {item.quantity}</span>
<span className="font-medium">${(item.price * item.quantity).toFixed(2)}</span>
</div>
))}
<div className="pt-4 border-t border-stone-100 dark:border-stone-800 flex justify-between items-center font-display text-2xl">
<span>Total</span>
<span>${selectedOrder.total_amount}</span>
</div>
</div>
</section>
</div>
<div className="space-y-8">
<section>
<h3 className="text-[10px] uppercase font-bold tracking-widest text-stone-400 mb-4">Manage Fulfillment</h3>
<div className="grid grid-cols-1 gap-2">
{['pending', 'shipped', 'delivered'].map((status) => (
<button
key={status}
onClick={() => updateOrderStatus(selectedOrder.id, status)}
className={`w-full py-4 text-[10px] uppercase tracking-widest font-bold border transition-all ${selectedOrder.shipping_status === status ? 'bg-black text-white dark:bg-white dark:text-black border-transparent' : 'bg-transparent border-stone-200 dark:border-stone-800 text-stone-400 hover:border-stone-400'}`}
>
Mark as {status}
</button>
))}
</div>
</section>
<div className="bg-stone-50 dark:bg-stone-900/50 p-6 rounded-sm">
<h3 className="text-[10px] uppercase font-bold tracking-widest text-stone-400 mb-2">Internal Note</h3>
<p className="text-xs text-stone-500 leading-relaxed italic">Payment confirmed via mock provider. Order is ready for processing.</p>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};
export default Admin;