hotschpotsh/Pottery-website/pages/Admin.tsx

796 lines
56 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

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;