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;

View File

@@ -0,0 +1,52 @@
import React from 'react';
import { useParams, Navigate } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
import BlogPostLayout from '../components/BlogPostLayout';
const ArticleDetail: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const { articles } = useStore();
const article = articles.find(a => a.slug === slug);
if (!article) {
// You might want to show a 404 or just redirect back
return <Navigate to="/editorial" replace />;
}
return (
<BlogPostLayout
title={article.title}
category={article.category || 'Studio Life'}
date={article.date}
image={article.image}
imageAlt={article.title}
>
<div className="space-y-12">
{article.sections && article.sections.length > 0 ? (
article.sections.map((section: any) => (
<div key={section.id}>
{section.type === 'text' ? (
<p className="mb-6 leading-relaxed text-lg font-light text-stone-600 dark:text-stone-300">
{section.content}
</p>
) : (
<div className="my-12">
<img
src={section.content}
alt="Article detail"
className="w-full shadow-lg rounded-sm"
/>
</div>
)}
</div>
))
) : (
<p className="italic text-stone-400">No content available for this article yet.</p>
)}
</div>
</BlogPostLayout>
);
};
export default ArticleDetail;

View File

@@ -30,7 +30,7 @@ const Atelier: React.FC = () => {
transition={{ delay: 0.4, duration: 0.8 }}
className="font-body text-lg font-light text-stone-500 leading-relaxed max-w-lg"
>
Our atelier is a sanctuary of slow creation. Located in the quiet hills, we practice the ancient art of wheel-throwing, honoring the raw beauty of natural clay.
Our atelier is a sanctuary of slow creation. Located in the heart of Corpus Christi, we practice the ancient art of wheel-throwing, honoring the raw beauty of the Texas Coast.
</motion.p>
</div>
<div className="md:col-span-12 lg:col-span-6 relative h-[600px] lg:h-[800px] w-full">
@@ -40,7 +40,7 @@ const Atelier: React.FC = () => {
transition={{ delay: 0.2, duration: 1.5, ease: "easeOut" }}
className="h-full w-full"
>
<img src="/pottery-studio.png" alt="Atelier Studio" className="w-full h-full object-cover" />
<img src="/pottery-studio.png" alt="Pottery Studio in Corpus Christi" className="w-full h-full object-cover" />
</motion.div>
</div>
</div>
@@ -48,9 +48,9 @@ const Atelier: React.FC = () => {
{/* Philosophy Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 border-t border-stone-200 dark:border-stone-800 pt-24">
{[
{ title: "Material", text: "We work exclusively with locally sourced stoneware clay bodies, rich in iron and character." },
{ title: "Process", text: "Every piece is wheel-thrown, trimmed, and glazed by hand, ensuring no two objects are identical." },
{ title: "Function", text: "Designed to be used and loved. Our ceramics are durable, food-safe, and meant for daily rituals." }
{ title: "Coastal Clay", text: "We work with stoneware clay bodies that reflect the sandy textures of the Gulf Coast." },
{ title: "Electric Firing", text: "Fired in oxidation to cone 6, creating durable surfaces that mimic the bleached colors of driftwood and shell." },
{ title: "Functional Art", text: "Designed to be used and loved. Our ceramics are durable, dishwasher safe, and meant for daily coastal living." }
].map((item, idx) => (
<motion.div
key={item.title}

View File

@@ -0,0 +1,150 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { Link, useNavigate } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
const Checkout: React.FC = () => {
const { cart } = useStore();
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
address: '',
city: '',
postalCode: ''
});
const subtotal = cart.reduce((total, item) => total + (item.price * item.quantity), 0);
const shipping = subtotal > 150 ? 0 : 15;
const total = subtotal + shipping;
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleProceed = (e: React.FormEvent) => {
e.preventDefault();
const orderData = {
customer_email: formData.email,
customer_name: `${formData.firstName} ${formData.lastName}`,
shipping_address: {
address: formData.address,
city: formData.city,
postalCode: formData.postalCode
},
items: cart.map(item => ({
id: item.id,
title: item.title,
quantity: item.quantity,
price: item.price
})),
total_amount: total
};
navigate('/mock-payment', { state: { orderData } });
};
if (cart.length === 0) {
return (
<div className="min-h-screen pt-48 flex flex-col items-center justify-center text-text-main dark:text-white px-6">
<h2 className="font-display text-4xl mb-8">Your bag is empty</h2>
<Link to="/collections" className="text-xs uppercase tracking-widest underline underline-offset-8 hover:text-stone-500 transition-colors">
View Collections
</Link>
</div>
);
}
return (
<div className="bg-white dark:bg-stone-950 min-h-screen pt-32 pb-24">
<div className="max-w-[1400px] mx-auto px-6 md:px-12">
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="font-display text-5xl md:text-7xl font-light text-text-main dark:text-white mb-16"
>
Checkout
</motion.h1>
<form onSubmit={handleProceed} className="grid grid-cols-1 lg:grid-cols-12 gap-16 xl:gap-24">
{/* Order Summary Form */}
<div className="lg:col-span-7 space-y-12">
<section>
<h3 className="text-xs uppercase tracking-[0.3em] text-stone-400 mb-8 border-b border-stone-100 dark:border-stone-900 pb-4">Contact Information</h3>
<div className="grid grid-cols-1 gap-6">
<input
required
name="email"
type="email"
placeholder="Email Address"
value={formData.email}
onChange={handleInputChange}
className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400 transition-colors"
/>
</div>
</section>
<section>
<h3 className="text-xs uppercase tracking-[0.3em] text-stone-400 mb-8 border-b border-stone-100 dark:border-stone-900 pb-4">Shipping Address</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<input required name="firstName" type="text" placeholder="First Name" value={formData.firstName} onChange={handleInputChange} className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400" />
<input required name="lastName" type="text" placeholder="Last Name" value={formData.lastName} onChange={handleInputChange} className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400" />
<div className="md:col-span-2">
<input required name="address" type="text" placeholder="Address" value={formData.address} onChange={handleInputChange} className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400" />
</div>
<input required name="city" type="text" placeholder="City" value={formData.city} onChange={handleInputChange} className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400" />
<input required name="postalCode" type="text" placeholder="Postal Code" value={formData.postalCode} onChange={handleInputChange} className="w-full bg-stone-50 dark:bg-stone-900 border border-stone-200 dark:border-stone-800 p-4 text-sm focus:outline-none focus:border-stone-400" />
</div>
</section>
<button type="submit" className="w-full bg-black dark:bg-white text-white dark:text-black py-5 uppercase tracking-[0.3em] text-xs font-bold hover:opacity-90 transition-opacity flex items-center justify-center gap-4 shadow-xl">
Proceed to Payment
<span className="material-symbols-outlined text-sm">arrow_forward</span>
</button>
</div>
{/* Cart Preview Sidebar */}
<div className="lg:col-span-5">
<div className="lg:sticky lg:top-32 bg-stone-50 dark:bg-stone-900/40 p-8 md:p-12 rounded-sm border border-stone-100 dark:border-stone-900">
<h3 className="text-xs uppercase tracking-[0.3em] text-stone-400 mb-8">In your bag</h3>
<div className="space-y-8 mb-12 max-h-96 overflow-y-auto pr-4 custom-scrollbar">
{cart.map((item) => (
<div key={item.id} className="flex gap-6">
<div className="w-20 aspect-[4/5] bg-stone-200 dark:bg-stone-800 flex-shrink-0 overflow-hidden">
<img src={item.image} alt={item.title} className="w-full h-full object-cover" />
</div>
<div className="flex-1 space-y-1">
<h4 className="font-display text-lg text-text-main dark:text-white">{item.title}</h4>
<p className="text-xs text-stone-500 uppercase tracking-widest">Qty: {item.quantity}</p>
<p className="text-sm font-light pt-2">${(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
))}
</div>
<div className="space-y-4 pt-8 border-t border-stone-200 dark:border-stone-800">
<div className="flex justify-between text-sm">
<span className="text-stone-500 font-light">Subtotal</span>
<span>${subtotal.toFixed(2)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-stone-500 font-light">Shipping</span>
<span>{shipping === 0 ? 'Free' : `$${shipping.toFixed(2)}`}</span>
</div>
<div className="flex justify-between text-xl font-display pt-4">
<span>Total</span>
<span className="text-text-main dark:text-white">${total.toFixed(2)}</span>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
);
};
export default Checkout;

View File

@@ -1,67 +1,72 @@
import React from 'react';
import { motion } from 'framer-motion';
import { COLLECTIONS } from '../constants';
import { Link } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
const Collections: React.FC = () => {
const { products } = useStore();
return (
<>
<section className="pt-32 pb-24 px-6 md:px-12 bg-stone-50 dark:bg-stone-900 min-h-screen">
<div className="max-w-[1920px] mx-auto">
{/* Header */}
<div className="mb-24 text-center">
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5, duration: 0.8 }}
className="font-display text-5xl md:text-7xl font-light mb-6 text-text-main dark:text-white"
>
Collections
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.7, duration: 0.8 }}
className="font-body text-stone-500 max-w-xl mx-auto text-lg font-light leading-relaxed"
>
Curated series of functional objects. Each collection explores a distinct form language and glaze palette.
</motion.p>
</div>
<section className="pt-32 pb-24 px-6 md:px-12 bg-stone-50 dark:bg-stone-900 min-h-screen">
<div className="max-w-[1920px] mx-auto">
{/* Header */}
<div className="mb-24 text-center">
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.5, duration: 0.8 }}
className="font-display text-5xl md:text-7xl font-light mb-6 text-text-main dark:text-white"
>
Shop Collection
</motion.h1>
<motion.p
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.7, duration: 0.8 }}
className="font-body text-stone-500 max-w-xl mx-auto text-lg font-light leading-relaxed"
>
Curated series of functional objects. From our 'Sandstone' mugs to 'Seafoam' vases, each collection celebrates the palette of the Texas coast.
</motion.p>
</div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-16">
{COLLECTIONS.map((item, index) => (
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-12 gap-y-16 lg:gap-x-16 px-4">
{products.map((collection, index) => (
<Link to={`/collections/${collection.slug}`} key={collection.id} className="block group cursor-pointer">
<motion.div
key={item.id}
initial={{ y: 40, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 + (index * 0.1), duration: 0.8, ease: "easeOut" }}
className="group cursor-pointer"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: index * 0.1 }}
>
{/* Image Container with Darker Background for Contrast */}
<div className={`relative overflow-hidden mb-6 ${item.aspectRatio || 'aspect-[3/4]'} bg-stone-200 dark:bg-stone-800`}>
<motion.img
src={item.image}
alt={item.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 relative z-10"
whileHover={{ scale: 1.05 }}
<div className="relative overflow-hidden mb-6 aspect-[4/5] bg-stone-100">
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-500 z-10" />
<img
src={collection.image}
alt={collection.title}
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-110"
/>
{/* Overlay on hover */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-500 z-20 pointer-events-none" />
{/* Quick overlay info */}
<div className="absolute bottom-6 left-6 z-20 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<span className="bg-white dark:bg-black px-4 py-2 text-xs uppercase tracking-widest text-text-main dark:text-white">View Item</span>
</div>
</div>
<div className="flex justify-between items-end border-b border-stone-200 dark:border-stone-800 pb-4">
<div className="flex justify-between items-baseline pr-2">
<div>
<span className="text-xs uppercase tracking-widest text-stone-400 mb-1 block">{item.number}</span>
<h3 className="font-display text-2xl text-text-main dark:text-white">{item.title}</h3>
<h2 className="font-display text-3xl font-light text-text-main dark:text-white mb-1 group-hover:underline decoration-1 underline-offset-4">
{collection.title}
</h2>
</div>
<span className="material-symbols-outlined opacity-0 -translate-x-4 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-300 text-stone-400">arrow_forward</span>
<span className="text-lg font-light text-text-main dark:text-white">
${collection.price}
</span>
</div>
</motion.div>
))}
</div>
</Link>
))}
</div>
</section>
</>
</div>
</section>
);
};

View File

@@ -1,95 +1,105 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { JOURNAL_ENTRIES } from '../constants';
import { useStore } from '../src/context/StoreContext';
const Editorial: React.FC = () => {
return (
<div className="bg-white dark:bg-black min-h-screen pt-32 pb-24">
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
<div className="text-center mb-24">
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="font-display text-xs tracking-[0.3em] uppercase mb-4 block text-stone-400"
>
The Journal
</motion.span>
<motion.h1
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2 }}
className="font-display text-6xl md:text-9xl font-light text-text-main dark:text-white"
>
Editorial
</motion.h1>
</div>
const { articles, isLoading } = useStore();
{/* Featured Article */}
<div className="relative w-full h-[70vh] mb-24 cursor-pointer group overflow-hidden">
<motion.img
initial={{ scale: 1.1 }}
animate={{ scale: 1 }}
transition={{ duration: 1.5 }}
src={JOURNAL_ENTRIES[0].image}
alt="Featured Article"
className="w-full h-full object-cover transition-transform duration-[2s] group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-colors duration-500" />
<div className="absolute bottom-0 left-0 p-8 md:p-16 text-white w-full md:w-2/3">
<span className="uppercase tracking-widest text-xs border border-white/30 px-3 py-1 mb-6 inline-block backdrop-blur-sm">Featured Story</span>
<h2 className="font-display text-4xl md:text-6xl mb-6 leading-tight">{JOURNAL_ENTRIES[0].title}</h2>
<p className="font-body text-lg md:text-xl font-light opacity-90 max-w-xl">{JOURNAL_ENTRIES[0].description}</p>
<div className="mt-8 flex items-center space-x-2 text-xs uppercase tracking-widest">
<span>Read Article</span>
<span className="material-symbols-outlined text-sm">arrow_forward</span>
if (isLoading) return <div className="min-h-screen flex items-center justify-center pt-24 font-light text-stone-400">Loading Journal...</div>;
if (!articles || articles.length === 0) {
return (
<div className="bg-white dark:bg-black min-h-screen pt-32 pb-24 text-center">
<h1 className="font-display text-4xl mb-8">Editorial</h1>
<p className="font-body text-stone-500">No stories yet. Stay tuned!</p>
</div>
);
}
// Sort: Featured first, then rest
const featuredArticle = articles.find(a => a.isFeatured) || articles[0];
const otherArticles = articles.filter(a => a.id !== featuredArticle.id);
return (
<div className="min-h-screen pt-32 pb-24 bg-white dark:bg-stone-950 transition-colors duration-500">
{/* Featured Post */}
<section className="px-6 mb-32">
<div className="max-w-[1400px] mx-auto">
<Link to={`/editorial/${featuredArticle.slug}`} className="group block">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
<div className="lg:col-span-8 overflow-hidden rounded-sm aspect-[16/9]">
<motion.img
initial={{ scale: 1.1, opacity: 0 }}
whileInView={{ scale: 1, opacity: 1 }}
transition={{ duration: 1.2, ease: [0.22, 1, 0.36, 1] }}
viewport={{ once: true }}
src={featuredArticle.image}
alt={featuredArticle.title}
className="w-full h-full object-cover grayscale-[0.2] group-hover:grayscale-0 group-hover:scale-105 transition-all duration-700"
/>
</div>
<div className="lg:col-span-4 space-y-6">
<div className="flex items-center space-x-4">
<span className="text-[10px] uppercase tracking-[0.3em] text-stone-400">{featuredArticle.category}</span>
<span className="w-8 h-[1px] bg-stone-200"></span>
<span className="text-[10px] uppercase tracking-[0.3em] text-stone-400">{featuredArticle.date}</span>
</div>
<h2 className="text-4xl md:text-5xl font-light leading-tight tracking-tight text-stone-900 dark:text-stone-100">
{featuredArticle.title}
</h2>
<p className="text-stone-500 font-light leading-relaxed max-w-md">
{featuredArticle.description}
</p>
<div className="pt-4">
<span className="inline-block text-[10px] uppercase tracking-[0.3em] font-medium border-b border-stone-200 pb-2 group-hover:border-stone-900 transition-colors">
Read Story
</span>
</div>
</div>
</div>
</Link>
</div>
</section>
{/* Other Articles Grid */}
<section className="px-6">
<div className="max-w-[1400px] mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-16">
{otherArticles.map((entry, idx) => (
<Link key={entry.id} to={`/editorial/${entry.slug}`} className="group block">
<motion.div
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.1 }}
className="space-y-6"
>
<div className="aspect-[4/3] overflow-hidden rounded-sm">
<img
src={entry.image}
alt={entry.title}
className="w-full h-full object-cover grayscale-[0.2] group-hover:grayscale-0 group-hover:scale-105 transition-all duration-700"
/>
</div>
<div className="space-y-3">
<div className="flex items-center space-x-4">
<span className="text-[10px] uppercase tracking-[0.3em] text-stone-400">{entry.category}</span>
<span className="w-4 h-[1px] bg-stone-200"></span>
<span className="text-[10px] uppercase tracking-[0.3em] text-stone-400">{entry.date}</span>
</div>
<h3 className="text-2xl font-light text-stone-900 dark:text-stone-100 group-hover:text-stone-500 transition-colors">
{entry.title}
</h3>
<p className="text-stone-500 font-light text-sm leading-relaxed line-clamp-2">
{entry.description}
</p>
</div>
</motion.div>
</Link>
))}
</div>
</div>
{/* Article Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-20 max-w-5xl mx-auto">
{JOURNAL_ENTRIES.slice(1).map((entry, idx) => (
<motion.div
key={entry.id}
initial={{ y: 40, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.2 }}
className="group cursor-pointer"
>
<div className="aspect-[4/3] overflow-hidden mb-8">
<img src={entry.image} alt={entry.title} className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" />
</div>
<div className="flex items-center space-x-4 mb-4 text-xs uppercase tracking-widest text-stone-400">
<span>{entry.category}</span>
<span className="w-1 h-1 bg-stone-300 rounded-full" />
<span>{entry.date}</span>
</div>
<h3 className="font-display text-3xl mb-4 text-text-main dark:text-white group-hover:underline decoration-1 underline-offset-4">{entry.title}</h3>
<p className="font-body font-light text-stone-500">{entry.description}</p>
</motion.div>
))}
{/* Dummy extra entry to fill grid */}
<motion.div
initial={{ y: 40, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: 0.4 }}
className="group cursor-pointer"
>
<div className="aspect-[4/3] overflow-hidden mb-8 bg-stone-100 dark:bg-stone-800 flex items-center justify-center">
<img src="/collection-tableware.png" alt="Archive" className="w-full h-full object-cover opacity-80 transition-transform duration-700 group-hover:scale-105" />
</div>
<div className="flex items-center space-x-4 mb-4 text-xs uppercase tracking-widest text-stone-400">
<span>Archive</span>
<span className="w-1 h-1 bg-stone-300 rounded-full" />
<span>2023</span>
</div>
<h3 className="font-display text-3xl mb-4 text-text-main dark:text-white group-hover:underline decoration-1 underline-offset-4">Explore Past Issues</h3>
<p className="font-body font-light text-stone-500">Dive into our archive of stories, guides, and studio updates.</p>
</motion.div>
</div>
</div>
</section>
</div>
);
};

View File

@@ -7,6 +7,8 @@ import QuoteSection from '../components/QuoteSection';
import JournalSection from '../components/JournalSection';
import GallerySection from '../components/GallerySection';
import FAQ from '../components/FAQ';
const Home: React.FC = () => {
return (
<main>
@@ -14,9 +16,10 @@ const Home: React.FC = () => {
<FeatureSection />
<HorizontalScrollSection />
<Collections />
<QuoteSection />
<JournalSection />
<GallerySection />
<JournalSection />
<QuoteSection />
<FAQ />
</main>
);
};

View File

@@ -0,0 +1,96 @@
import React from 'react';
import { Link } from 'react-router-dom';
import BlogPostLayout from '../../components/BlogPostLayout';
const MotivationInClay: React.FC = () => {
React.useEffect(() => {
document.title = "Creative Block for Potters: 10 Tips for Motivation | Hotchpotsh";
let meta = document.querySelector('meta[name="description"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'description');
document.head.appendChild(meta);
}
meta.setAttribute('content', 'Overcoming Creative Block for Potters is possible. Use these 10 gentle, practical tips to rediscover your motivation and love for clay. Read more now.');
}, []);
return (
<BlogPostLayout
title="Creative Block for Potters: 10 Tips for Motivation"
category="Wellness"
date="Jun 11"
image="https://lh3.googleusercontent.com/aida-public/AB6AXuB8NOE5fGfN4d87cbcB27_Sh-nrlZlqxsTlYKbCZk98SoL-gHsPSWFNuxd1DxBq0g8Qysh0RBZ_btu-_WaH68UjV8SXPUalyxREvUqao4oXmra--pWAsaooWwKvWCzReYZ8kj7G-KIYIAo5mqudzB8n9C6-HVTNPPx9QgZHr_YsojMxlmmVcQ5bqk7-Lp0KtSAiVIPD2-1UE1dMGnkVSLUXKdgA65JIh8M3TtNkaJTGONuFKoTERrYOWe7u2BILnqyukTzlNcvK7Sc"
imageAlt="Creative Block for Potters tips"
>
<p className="lead text-xl text-stone-600 dark:text-stone-300 italic mb-8">
Dealing with <strong>Creative Block for Potters</strong> (and finding new <strong>Pottery Inspiration</strong>) is a common struggle in the studio. Where the physical labor is intense and the failure rate is high, burnout is real. Whether you are facing general exhaustion or a specific artistic wall, know that this season is part of the cycle.
</p>
<p className="mb-6">
Here is how to overcome <strong>Creative Block for Potters</strong> and find your flow again.
</p>
<img
src="https://images.unsplash.com/photo-1459156212016-c812468e2115?q=80&w=2574&auto=format&fit=crop"
alt="Creative Block for Potters guide"
className="w-full my-12 shadow-lg"
/>
<h2 className="mt-16 mb-8 text-3xl">1. Play without Purpose</h2>
<p className="mb-6">
Stop making <Link to="/collections">Collections</Link>. Stop thinking about what will sell. Grab a lump of clay and just <em>pinch</em>. When you remove the pressure, you often solve the <strong>Creative Block for Potters</strong> naturally.
</p>
<h2 className="mt-16 mb-8 text-3xl">2. Switch Your Technique</h2>
<p className="mb-6">
If you are a wheel thrower, try <strong>hand building</strong>. Changing your physical movements can unlock new neural pathways.
</p>
<h2 className="mt-16 mb-8 text-3xl">3. The "100 Pattern" Challenge</h2>
<p className="mb-6">
Commit to making 100 small test tiles. Constraints actually breed creativity.
</p>
<h2 className="mt-16 mb-8 text-3xl">4. Clean Your Studio (Reset)</h2>
<p className="mb-6">
A cluttered space leads to a cluttered mind. Spend a day organizing your <Link to="/atelier">Atelier</Link>. A fresh, clean bat on the wheel is an invitation.
</p>
<h2 className="mt-16 mb-8 text-3xl">5. Look Outside of Pottery</h2>
<p className="mb-6">
Don't look at other potters on Instagram. That leads to comparison. instead, look at:
</p>
<ul className="mb-12 space-y-4">
<li><strong>Architecture</strong>: for structural shapes.</li>
<li><strong>Nature</strong>: for textures (tree bark, river stones).</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">6. Take a Class</h2>
<p className="mb-6">
Even masters are students. Taking a workshop puts you back in the "beginner's mind," which is a fertile place for ideas.
</p>
<h2 className="mt-16 mb-8 text-3xl">7. Revisit Your "Why"</h2>
<p className="mb-6">
Look at the very first pot you ever kept. Reconnecting with your origin story can fuel your current practice.
</p>
<h2 className="mt-16 mb-8 text-3xl">8. Limit Your Time</h2>
<p className="mb-6">
Tell yourself, "I will only work for 20 minutes." Often, the hardest part is just starting.
</p>
<h2 className="mt-16 mb-8 text-3xl">9. Embrace functionality</h2>
<p className="mb-6">
Make something you <em>need</em>. A spoon rest. A soap dish. Solving a simple, functional problem is a great way to handle <strong>Creative Block for Potters</strong>.
</p>
<h2 className="mt-16 mb-8 text-3xl">10. Rest</h2>
<p className="mb-6">
Sometimes, the block isn't mental; it's physical. Take a week off. The clay will be there when you get back.
</p>
</BlogPostLayout>
);
};
export default MotivationInClay;

View File

@@ -0,0 +1,90 @@
import React from 'react';
import { Link } from 'react-router-dom';
import BlogPostLayout from '../../components/BlogPostLayout';
const PackagingGuide: React.FC = () => {
React.useEffect(() => {
document.title = "How to Package Pottery for Shipping: A Safe Guide | Hotchpotsh";
let meta = document.querySelector('meta[name="description"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'description');
document.head.appendChild(meta);
}
meta.setAttribute('content', 'Learn how to package pottery for shipping safely. Use our double-box method and sustainable tips to ensure your handmade ceramics arrive intact. Read now.');
}, []);
return (
<BlogPostLayout
title="How to Package Pottery for Shipping"
category="Guide"
date="Jul 15"
image="https://lh3.googleusercontent.com/aida-public/AB6AXuAaWGnX_NYT3S_lOflL2NJZGbWge4AAkvra4ymvF8ag-c1UKsOAIB-rsLVQXW5xIlPZipDiK8-ysPyv22xdgsvzs4EOXSSCcrT4Lb2YCe0u5orxRaZEA5TgxeoKq15zaWKSlmnHyPGjPd_7yglpfO13eZmbU5KaxFJ1KGO0UAxoO9BpsyCYgbgINMoSz3epGe5ZdwBWRH-5KCzjoLuXimFTLcd5bqg9T1YofTxgy2hWBMJzKkafyEniq8dP6hMmfNCLVcCHHHx0hRU"
imageAlt="How to Package Pottery for Shipping Safely guide"
>
<p className="lead text-xl text-stone-600 dark:text-stone-300 italic mb-8">
<strong>How to Package Pottery for Shipping</strong> safely is the most important skill for a small business owner. There is nothing more heartbreaking than a shattered creation, so mastering this art form ensures your hard work survives the journey.
</p>
<p className="mb-6">
Here is your comprehensive guide on shipping handmade art so it arrives safely every single time.
</p>
<h2 className="mt-16 mb-8 text-3xl">1. The Double-Box Method (The Golden Rule)</h2>
<p className="mb-6">
When considering safe deliveryespecially for large itemsthe <strong>double-box method</strong> is the industry standard.
</p>
<ul className="mb-12 space-y-4">
<li><strong>Inner Box</strong>: Wrap your <Link to="/collections">Collections</Link> piece and place it in a small box. It should fit snugly.</li>
<li><strong>Outer Box</strong>: Place the small box inside a larger shipping box, with at least 2 inches of padding on all sides.</li>
<li><em>Why?</em> The outer box absorbs the shock, keeping your art safe.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">2. Wrapping Materials: Layers Matter</h2>
<p className="mb-6">
Don't rely on just one material when you plan your packing strategy.
</p>
<ul className="mb-12 space-y-4">
<li><strong>Layer 1: Tissue Paper</strong>: Protects the glaze.</li>
<li><strong>Layer 2: Bubble Wrap</strong>: The workhorse. Wrap the piece <em>tightly</em> in small-bubble wrap.</li>
<li><strong>The Shake Test</strong>: Shake the box hard. If you hear movement, add tougher filler.</li>
</ul>
<div className="my-16">
<img
src="/assets/images/packaging_guide.png"
alt="Sustainable pottery packaging materials including honeycomb paper and packing peanuts"
className="w-full shadow-lg rounded-sm"
/>
<p className="text-sm text-center text-stone-500 mt-4 italic">Eco-friendly packaging materials ready for use.</p>
</div>
<h2 className="mt-16 mb-8 text-3xl">3. Sustainable Packaging Alternatives</h2>
<p className="mb-6">
Many customers value sustainability in our <Link to="/atelier">Atelier</Link>.
</p>
<ul>
<li><strong>Honeycomb Paper</strong>: A biodegradable alternative.</li>
<li><strong>Corn Starch Peanuts</strong>: Dissolve in water.</li>
<li><strong>Cardboard Scraps</strong>: Excellent dense filler.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">4. Branding Your Unboxing Experience</h2>
<ul className="mb-12 space-y-4">
<li><strong>The "Thank You" Note</strong>: Builds a connection.</li>
<li><strong>Care Instructions</strong>: Explain microwave/dishwasher safety.</li>
<li><strong>Stickers</strong>: Build anticipation.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">5. Insurance and labeling</h2>
<ul className="mb-12 space-y-4">
<li><strong>Fragile Stickers</strong>: Helpful, but not a guarantee.</li>
<li><strong>Shipping Insurance</strong>: Always pay the extra few dollars for peace of mind.</li>
</ul>
</BlogPostLayout>
);
};
export default PackagingGuide;

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { Link } from 'react-router-dom';
import BlogPostLayout from '../../components/BlogPostLayout';
// Wait, I don't know if react-helmet is installed. Checking package.json... it was not.
// I will adhere to the "no new dependencies" rule unless necessary. I'll just render the meta tags usually, but without Helmet they won't lift to head.
// The user asked for "Meta Title" and "Meta Description" implementation. I will add a helper to update document.title.
const ProductPhotography: React.FC = () => {
React.useEffect(() => {
document.title = "Product Photography for Pottery: Tips for Sales | Hotchpotsh";
// Simple meta description update for basic SPA
let meta = document.querySelector('meta[name="description"]');
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', 'description');
document.head.appendChild(meta);
}
meta.setAttribute('content', 'Master Product Photography for Pottery with our DIY guide. Learn lighting and styling tips to boost your handmade ceramic sales online. Read more now.');
}, []);
return (
<BlogPostLayout
title="Product Photography for Pottery"
category="Studio"
date="Oct 03"
image="https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ"
imageAlt="DIY Product Photography for Pottery setup with natural light"
>
<p className="lead text-xl text-stone-600 dark:text-stone-300 italic mb-8">
Mastering <strong>Product Photography for Pottery</strong> is essential because in the world of handmade business, your work is only as good as the photo that represents it. Since customers can't touch your mugs online, your photos must bridge the gap between browsing and buying.
</p>
<p>
Here is how to elevate your <strong>Product Photography for Pottery</strong> without expensive gear.
</p>
<img
src="https://images.unsplash.com/photo-1516483638261-f4dbaf036963?q=80&w=2574&auto=format&fit=crop"
alt="Product Photography for Pottery setup"
title="DIY Setup"
className="w-full my-12 shadow-lg"
/>
<h2 className="mt-16 mb-8 text-3xl">1. Treasure the Natural Light</h2>
<p className="mb-6">
Lighting is the single most critical element of successful <strong>Product Photography for Pottery</strong>. Avoid the harsh, yellow glow of indoor lamps. Instead, set up your "studio" next to a large North or South-facing window, similar to the natural light in our <Link to="/atelier">Atelier</Link>.
</p>
<ul className="mb-12 space-y-4">
<li><strong>Diffused Light is Best</strong>: If the sun is beaming directly in, tape a sheet of white parchment paper over the window. This creates soft shadows that highlight the curves of your <strong>ceramic vessels</strong> without blinding glare.</li>
<li><strong>The Golden Hour</strong>: For lifestyle shots, try shooting during the hour after sunrise or before sunset for a warm, magical glow.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">2. Master the "Hero Shot"</h2>
<p className="mb-6">
Every listing needs a clear shot. When mastering <strong>Product Photography for Pottery</strong>, the "Hero Shot" usually requires a clean background for your <Link to="/collections">Collections</Link>.
</p>
<ul className="mb-12 space-y-4">
<li><strong>The Infinite Curve</strong>: Use a large sheet of white poster board. Tape one end to the wall and let it curve gently down onto the table. This seamless background eliminates distracting horizon lines.</li>
<li><strong>Tripod Stability</strong>: Blurry photos are a dealbreaker. If you don't have a tripod, prop your phone up against a stack of books.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">3. Tell a Story with Props</h2>
<p className="mb-6">
While a clean background shows the details, lifestyle **Product Photography for Pottery** sells the <em>dream</em>.
</p>
<ul className="mb-12 space-y-4">
<li><strong>Context matters</strong>: Don't just show a mug; show it steaming with coffee next to a half-read book.</li>
<li><strong>Keep it subtle</strong>: Your props should never compete with your work. Neutral linens complement the vibrant <strong>glaze colors</strong> of your <Link to="/collections">Collections</Link>.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">4. Angles & Details</h2>
<p className="mb-6">
Don't stop at one angle. Online buyers need to see everything.
</p>
<ul className="mb-12 space-y-4">
<li><strong>The Eye-Level Shot</strong>: Perfect for showing the profile of a vase.</li>
<li><strong>The Top-Down Shot</strong>: Ideal for plates and bowls.</li>
<li><strong>The Detail Macro</strong>: Get close. Show the texture of the raw clay body.</li>
</ul>
<h2 className="mt-16 mb-8 text-3xl">5. Editing: Less is More</h2>
<p className="mb-6">
You don't need Photoshop. Free apps like <strong>Snapseed</strong> or <strong>Lightroom Mobile</strong> are powerful tools for editing <strong>Product Photography for Pottery</strong>.
</p>
<ul className="mb-12 space-y-4">
<li><strong>Correction, not Alteration</strong>: Adjust brightness, contrast, and white balance.</li>
<li><strong>True-to-Life Color</strong>: Be very careful not to over-saturate.</li>
</ul>
</BlogPostLayout>
);
};
export default ProductPhotography;

View File

@@ -0,0 +1,77 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { useNavigate, useLocation } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
const MockPayment: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const { placeOrder, clearCart } = useStore();
const [isSimulating, setIsSimulating] = useState(false);
const orderData = location.state?.orderData;
const handleSimulatePayment = async () => {
if (!orderData) return;
setIsSimulating(true);
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 2000));
try {
await placeOrder(orderData);
clearCart();
navigate('/success');
} catch (err) {
alert('Payment simulation failed');
setIsSimulating(false);
}
};
if (!orderData) {
return (
<div className="min-h-screen pt-48 flex flex-col items-center justify-center">
<p>No order data found. Please go back to checkout.</p>
<button onClick={() => navigate('/checkout')} className="mt-4 underline">Back to Checkout</button>
</div>
);
}
return (
<div className="min-h-screen bg-stone-50 dark:bg-stone-950 pt-48 px-6">
<div className="max-w-md mx-auto bg-white dark:bg-stone-900 p-12 rounded-sm shadow-xl border border-stone-100 dark:border-stone-800 text-center">
<span className="material-symbols-outlined text-6xl text-stone-300 mb-8">account_balance_wallet</span>
<h1 className="font-display text-3xl text-text-main dark:text-white mb-4">Payment Simulation</h1>
<p className="text-stone-500 text-sm mb-12 leading-relaxed">
This is a secure test environment. Click the button below to simulate a successful transaction of
<span className="font-bold text-text-main dark:text-white ml-1">${orderData.total_amount.toFixed(2)}</span>.
</p>
<button
onClick={handleSimulatePayment}
disabled={isSimulating}
className="w-full bg-black dark:bg-white text-white dark:text-black py-5 uppercase tracking-[0.3em] text-xs font-bold hover:opacity-90 transition-opacity flex items-center justify-center gap-4"
>
{isSimulating ? (
<>
<span className="animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full" />
Processing...
</>
) : (
'Simulate Success'
)}
</button>
<button
onClick={() => navigate('/checkout')}
disabled={isSimulating}
className="mt-6 text-[10px] text-stone-400 uppercase tracking-widest hover:text-stone-600 transition-colors"
>
Cancel and go back
</button>
</div>
</div>
);
};
export default MockPayment;

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { useParams, Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { useStore } from '../src/context/StoreContext';
const ProductDetail: React.FC = () => {
const { slug } = useParams<{ slug: string }>();
const { products, addToCart } = useStore();
const product = products.find(item => item.slug === slug);
if (!product) {
return (
<div className="min-h-screen pt-32 flex flex-col items-center justify-center text-text-main dark:text-white">
<h2 className="text-4xl font-display mb-4">Product Not Found</h2>
<Link to="/collections" className="underline hover:text-stone-500">Return to Shop</Link>
</div>
);
}
return (
<div className="bg-white dark:bg-stone-950 min-h-screen pt-32 pb-24">
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
{/* Breadcrumb */}
<div className="mb-12 text-sm uppercase tracking-widest text-stone-500">
<Link to="/collections" className="hover:text-text-main dark:hover:text-white transition-colors">Shop</Link>
<span className="mx-2">/</span>
<span className="text-text-main dark:text-white">{product.title}</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 xl:gap-24">
{/* Images Column */}
<div className="space-y-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="aspect-[4/5] w-full overflow-hidden bg-stone-100 rounded-sm"
>
<img
src={product.image}
alt={product.title}
className="w-full h-full object-cover"
/>
</motion.div>
{/* Additional images grid */}
<div className="grid grid-cols-2 gap-4">
{product.images?.slice(1).map((img, idx) => (
<motion.div
key={idx}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * (idx + 1) }}
className="aspect-square w-full overflow-hidden bg-stone-100 rounded-sm"
>
<img src={img} alt={`${product.title} detail ${idx + 1}`} className="w-full h-full object-cover" />
</motion.div>
))}
</div>
</div>
{/* Info Column */}
<div className="lg:sticky lg:top-32 h-fit">
<motion.h1
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="font-display text-5xl md:text-7xl font-light text-text-main dark:text-white mb-6"
>
{product.title}
</motion.h1>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-2xl font-light text-stone-600 dark:text-stone-300 mb-8"
>
${product.price}
</motion.p>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="prose prose-stone dark:prose-invert max-w-none mb-12 font-light leading-relaxed text-lg"
>
<p>{product.description}</p>
<ul>
{product.details && product.details.map((detail, i) => (
<li key={i}>{detail}</li>
))}
<li>Made in Corpus Christi, TX</li>
</ul>
</motion.div>
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
onClick={() => addToCart(product)}
type="button"
className="w-full bg-text-main dark:bg-white text-white dark:text-text-main py-4 uppercase tracking-[0.2em] hover:bg-stone-800 dark:hover:bg-stone-200 transition-colors"
>
Add to Cart
</motion.button>
<div className="mt-12 pt-12 border-t border-stone-200 dark:border-stone-800 text-sm text-stone-500 font-light space-y-2">
<p>Free shipping on orders over $150</p>
<p>Ships within 3-5 business days</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default ProductDetail;

View File

@@ -0,0 +1,50 @@
import React from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
const Success: React.FC = () => {
return (
<div className="min-h-screen pt-48 pb-24 px-6 flex flex-col items-center text-center">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="w-24 h-24 bg-stone-100 dark:bg-stone-900 rounded-full flex items-center justify-center mb-12"
>
<span className="material-symbols-outlined text-4xl text-text-main dark:text-white">check_circle</span>
</motion.div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="font-display text-5xl md:text-7xl font-light text-text-main dark:text-white mb-8"
>
Thank You
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="font-body text-lg font-light text-stone-500 max-w-md mb-16 leading-relaxed"
>
Your order has been placed successfully. We've sent a confirmation email with all the details of your purchase.
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Link
to="/"
className="bg-black dark:bg-white text-white dark:text-black px-12 py-5 uppercase tracking-[0.3em] text-xs font-bold hover:opacity-90 transition-opacity inline-block shadow-xl"
>
Back to Home
</Link>
</motion.div>
</div>
);
};
export default Success;