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,112 @@
import React, { useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
interface BlogPostLayoutProps {
title: string;
category: string;
date: string;
image: string;
imageAlt: string;
children: React.ReactNode;
}
const BlogPostLayout: React.FC<BlogPostLayoutProps> = ({
title,
category,
date,
image,
imageAlt,
children,
}) => {
const { articles } = useStore();
// Scroll to top on mount
useEffect(() => {
window.scrollTo(0, 0);
}, []);
const nextArticles = articles.filter(post => post.title !== title).slice(0, 2);
return (
<div className="bg-stone-50 dark:bg-black min-h-screen font-body transition-colors duration-500">
<main className="pt-32 pb-24">
{/* Article Header */}
<article className="max-w-4xl mx-auto px-6 md:px-12">
<div className="flex items-center space-x-4 mb-8 justify-center">
<span className="text-xs uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-3 py-1 rounded-full">{category}</span>
<span className="text-xs uppercase tracking-[0.2em] text-text-muted">{date}</span>
</div>
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
className="font-display text-5xl md:text-6xl lg:text-7xl text-center text-text-main dark:text-white mb-16 leading-tight"
>
{title}
</motion.h1>
{/* Hero Image */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.2 }}
className="w-full h-[40vh] md:h-[50vh] relative mb-16 overflow-hidden shadow-xl rounded-sm"
>
<img
src={image}
alt={imageAlt}
className="w-full h-full object-cover"
/>
</motion.div>
{/* Content Container */}
<div className="prose prose-stone dark:prose-invert max-w-none mx-auto prose-headings:font-display prose-headings:font-light prose-p:font-light prose-p:leading-loose prose-a:text-terracotta hover:prose-a:text-terracotta-dark prose-img:rounded-sm">
{children}
</div>
{/* Read Next Section */}
{nextArticles.length > 0 && (
<div className="mt-24 pt-16 border-t border-stone-200 dark:border-stone-800">
<h3 className="font-display text-3xl text-center mb-12 text-text-main dark:text-white">Read Next</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{nextArticles.map((post) => (
<Link key={post.id} to={`/editorial/${post.slug}`} className="group block">
<div className="aspect-[3/2] overflow-hidden bg-stone-100 mb-4">
<img
src={post.image}
alt={post.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
</div>
<h4 className="font-display text-2xl text-text-main dark:text-white group-hover:underline decoration-1 underline-offset-4 mb-2">
{post.title}
</h4>
<div className="flex items-center space-x-2 text-sm text-stone-500 uppercase tracking-widest">
<span>{post.category}</span>
<span></span>
<span>{post.date}</span>
</div>
</Link>
))}
</div>
</div>
)}
{/* Back Link */}
<div className="mt-20 text-center">
<Link to="/editorial" className="inline-block border-b border-black dark:border-white pb-1 text-sm uppercase tracking-widest hover:text-stone-500 transition-colors">
Back to Editorial
</Link>
</div>
</article>
</main>
</div>
);
};
export default BlogPostLayout;

View File

@@ -0,0 +1,131 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useNavigate } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
const Cart: React.FC = () => {
const { cart, isCartOpen, setCartOpen, removeFromCart, updateQuantity } = useStore();
const navigate = useNavigate();
const subtotal = cart.reduce((total, item) => total + (item.price * item.quantity), 0);
const handleCheckout = () => {
setCartOpen(false);
navigate('/checkout');
};
return (
<AnimatePresence>
{isCartOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setCartOpen(false)}
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-[60]"
/>
{/* Drawer */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="fixed top-0 right-0 h-full w-full max-w-md bg-white dark:bg-stone-950 z-[70] shadow-2xl flex flex-col"
>
{/* Header */}
<div className="p-8 border-b border-stone-100 dark:border-stone-900 flex justify-between items-center">
<h2 className="font-display text-2xl uppercase tracking-widest text-text-main dark:text-white">Your Bag</h2>
<button
onClick={() => setCartOpen(false)}
className="p-2 hover:bg-stone-50 dark:hover:bg-stone-900 rounded-full transition-colors"
>
<span className="material-symbols-outlined text-stone-500">close</span>
</button>
</div>
{/* Items List */}
<div className="flex-1 overflow-y-auto p-8 space-y-8">
{cart.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-stone-400 space-y-4">
<span className="material-symbols-outlined text-4xl font-light">shopping_bag</span>
<p className="font-light tracking-wide uppercase text-xs">Your bag is empty</p>
<button
onClick={() => setCartOpen(false)}
className="text-text-main dark:text-white underline underline-offset-4 text-xs uppercase tracking-widest pt-4"
>
Start Shopping
</button>
</div>
) : (
cart.map((item) => (
<div key={item.id} className="flex gap-6 group">
<div className="w-24 aspect-[4/5] bg-stone-100 dark:bg-stone-900 overflow-hidden rounded-sm flex-shrink-0">
<img src={item.image} alt={item.title} className="w-full h-full object-cover" />
</div>
<div className="flex-1 flex flex-col justify-between py-1">
<div className="flex justify-between items-start">
<div>
<h3 className="font-display text-lg text-text-main dark:text-white">{item.title}</h3>
<p className="text-xs text-stone-500 uppercase tracking-widest mt-1">Ref. {item.number}</p>
</div>
<button
onClick={() => removeFromCart(item.id)}
className="text-stone-300 hover:text-red-400 transition-colors"
>
<span className="material-symbols-outlined text-sm">delete</span>
</button>
</div>
<div className="flex justify-between items-end">
<div className="flex items-center border border-stone-100 dark:border-stone-800 rounded-sm">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="px-2 py-1 hover:bg-stone-50 dark:hover:bg-stone-900 transition-colors"
>
<span className="material-symbols-outlined text-xs">remove</span>
</button>
<span className="px-3 text-xs font-medium w-8 text-center">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="px-2 py-1 hover:bg-stone-50 dark:hover:bg-stone-900 transition-colors"
>
<span className="material-symbols-outlined text-xs">add</span>
</button>
</div>
<p className="text-sm font-light text-text-main dark:text-white">${(item.price * item.quantity).toFixed(2)}</p>
</div>
</div>
</div>
))
)}
</div>
{/* Footer */}
{cart.length > 0 && (
<div className="p-8 border-t border-stone-100 dark:border-stone-900 space-y-6">
<div className="flex justify-between items-center bg-stone-50 dark:bg-stone-900/50 p-6 rounded-sm">
<span className="text-xs uppercase tracking-widest text-stone-500">Subtotal</span>
<span className="text-xl font-display text-text-main dark:text-white">${subtotal.toFixed(2)}</span>
</div>
<button
onClick={handleCheckout}
type="button"
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 shadow-xl"
>
Proceed to Checkout
</button>
<p className="text-[10px] text-center text-stone-400 uppercase tracking-widest italic">
Shipping & taxes calculated at checkout
</p>
</div>
)}
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default Cart;

View File

@@ -0,0 +1,92 @@
import React, { useRef } from 'react';
import { useScrollFadeIn } from '../hooks/useScrollAnimations';
interface FAQItemProps {
question: string;
answer: string;
}
const FAQItem: React.FC<FAQItemProps> = ({ question, answer }) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<div className="border-b border-stone-200 dark:border-stone-700">
<button
className="w-full py-6 flex justify-between items-center text-left focus:outline-none"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
<h3 className="text-lg font-display text-text-main dark:text-white">{question}</h3>
<span className={`transform transition-transform duration-300 ${isOpen ? 'rotate-45' : ''}`}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</span>
</button>
<div
className={`overflow-hidden transition-all duration-500 ease-in-out ${isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'}`}
>
<p className="pb-6 text-text-muted dark:text-gray-400 font-body leading-relaxed max-w-2xl">
{answer}
</p>
</div>
</div>
);
};
const FAQ: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
useScrollFadeIn(containerRef);
const faqs = [
{
question: "Do you ship your ceramics internationally?",
answer: "Currently, we ship our handmade pottery mainly within Texas and the United States. We occasionally open international shipping spots for specific drops. Sign up for our newsletter to be notified."
},
{
question: "Are your pieces dishwasher and microwave safe?",
answer: "Yes! Our functional stoneware, including mugs, plates, and bowls, is fired to cone 6 oxidation, making it durable for daily use. However, hand washing is always recommended to prolong the life of your unique handmade ceramics."
},
{
question: "Where is your studio located?",
answer: "Our studio is based in the heart of Corpus Christi, Texas. We take inspiration from the Gulf Coast landscape. We offer local pickup for our Corpus Christi neighbors!"
},
{
question: "Do you offer pottery classes in Corpus Christi?",
answer: "We are working on bringing intimate wheel-throwing workshops to our Corpus Christi studio soon. Check our 'Atelier' page or follow us on Instagram for announcements."
},
{
question: "Do you take custom orders or commissions?",
answer: "We accept a limited number of custom dinnerware commissions each year. If you are looking for a bespoke set for your home or restaurant, please contact us directly."
},
{
question: "How often do you restock the shop?",
answer: "We work in small batches and typically release a new 'Sandstone' or 'Seafoam' collection every 4-6 weeks. Join our email list to get early access to the next kiln opening."
},
{
question: "What clay bodies and glazes do you use?",
answer: "We use a proprietary blend of stoneware clay that mimics the texture of Texas limestone. Our glazes are formulated in-house to reflect colors of the sea and sand."
}
];
return (
<section ref={containerRef} className="py-24 px-6 md:px-20 bg-stone-50 dark:bg-stone-900/50">
<div className="max-w-4xl mx-auto">
<span className="block font-body text-xs uppercase tracking-[0.2em] text-text-muted mb-8">
Common Questions
</span>
<h2 className="font-display text-4xl md:text-5xl text-text-main dark:text-white mb-16">
Studio FAQ
</h2>
<div className="flex flex-col">
{faqs.map((faq, index) => (
<FAQItem key={index} question={faq.question} answer={faq.answer} />
))}
</div>
</div>
</section>
);
};
export default FAQ;

View File

@@ -10,7 +10,7 @@ const Footer: React.FC = () => {
<div className="lg:col-span-5 flex flex-col justify-between h-full">
<div>
<h2 className="font-display text-6xl md:text-8xl leading-none tracking-tighter mb-8 bg-gradient-to-br from-white to-stone-400 bg-clip-text text-transparent">
HOTCHPOTSH
HOTSCHPOTSH
</h2>
<p className="font-body text-lg font-light text-stone-400 leading-relaxed max-w-md">
Handcrafted ceramics for the modern home. Created with intention, fired with patience, and delivered with care.
@@ -78,11 +78,12 @@ const Footer: React.FC = () => {
{/* Bottom Bar */}
<div className="border-t border-white/10 pt-12 flex flex-col md:flex-row justify-between items-center text-xs text-stone-500 tracking-widest uppercase font-light">
<p>© 2024 HOTCHPOTSH Ceramics. All rights reserved.</p>
<p>© 2024 HOTSCHPOTSH Ceramics. All rights reserved.</p>
<div className="flex space-x-8 mt-6 md:mt-0">
<a className="hover:text-white transition-colors" href="#">Privacy</a>
<a className="hover:text-white transition-colors" href="#">Terms</a>
<a className="hover:text-white transition-colors" href="#">Cookies</a>
<a className="hover:text-white transition-colors" href="/admin">Admin</a>
</div>
</div>
</div>

View File

@@ -2,8 +2,10 @@ import React, { useState, useEffect } from 'react';
import { NAV_ITEMS } from '../constants';
import { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useStore } from '../src/context/StoreContext';
const Header: React.FC = () => {
const { cart, setCartOpen } = useStore();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
@@ -18,8 +20,8 @@ const Header: React.FC = () => {
return (
<header
className={`fixed top-0 w-full z-50 transition-all duration-500 ${scrolled
? 'bg-white/80 dark:bg-black/80 backdrop-blur-xl py-2 border-b border-stone-200/50 dark:border-stone-800/50'
: 'bg-transparent py-6'
? 'bg-white/80 dark:bg-black/80 backdrop-blur-xl py-2 border-b border-stone-200/50 dark:border-stone-800/50'
: 'bg-transparent py-6'
}`}
>
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
@@ -38,7 +40,7 @@ const Header: React.FC = () => {
{/* Logo */}
<div className="flex-shrink-0 relative group cursor-pointer">
<Link className="font-display text-4xl md:text-5xl font-light tracking-widest uppercase text-text-main dark:text-white" to="/">
HOTCHPOTSH
HOTSCHPOTSH
</Link>
{/* Subtle glow effect on hover */}
<div className="absolute -inset-4 bg-white/20 blur-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-full" />
@@ -64,10 +66,18 @@ const Header: React.FC = () => {
<button className="hover:scale-110 transition-transform duration-300 hidden sm:block p-2">
<span className="material-symbols-outlined text-xl font-light">search</span>
</button>
<a className="hover:scale-110 transition-transform duration-300 relative group p-2" href="#">
<button
onClick={() => setCartOpen(true)}
className="hover:scale-110 transition-transform duration-300 relative group p-2"
type="button"
>
<span className="material-symbols-outlined text-xl font-light">shopping_bag</span>
<span className="absolute top-0 right-0 bg-black dark:bg-white text-white dark:text-black text-[9px] w-4 h-4 flex items-center justify-center rounded-full opacity-0 scale-50 group-hover:opacity-100 group-hover:scale-100 transition-all duration-300">2</span>
</a>
{cart.length > 0 && (
<span className="absolute top-0 right-0 bg-black dark:bg-white text-white dark:text-black text-[9px] w-4 h-4 flex items-center justify-center rounded-full opacity-100 scale-100 transition-all duration-300">
{cart.reduce((total, item) => total + item.quantity, 0)}
</span>
)}
</button>
</div>
</div>
</div>

View File

@@ -8,10 +8,10 @@ const Hero: React.FC = () => {
New Collection 2024
</span>
<h1 className="font-display text-6xl md:text-7xl lg:text-8xl xl:text-9xl text-text-main dark:text-white font-thin leading-[0.9] mb-10">
Form <br /><span className="italic pl-12 md:pl-20 text-text-muted">of</span> Earth
Earth <br /><span className="italic pl-12 md:pl-20 text-text-muted">of</span> Ocean
</h1>
<p className="font-body text-text-muted dark:text-gray-400 text-sm md:text-base font-light mb-12 max-w-sm leading-loose ml-1">
Discover the imperfect perfection of hand-thrown stoneware. Pieces that bring silence and intention to your daily rituals.
Handcrafted ceramics from the Texas Coast. Functional art inspired by the raw textures and colors of Corpus Christi. Small batch, slow-made.
</p>
<div className="ml-1">
<a className="inline-block border-b border-text-main dark:border-white pb-1 text-text-main dark:text-white font-body text-xs uppercase tracking-[0.2em] hover:text-text-muted transition-colors duration-300" href="#">

View File

@@ -1,45 +1,48 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { JOURNAL_ENTRIES } from '../constants';
const JournalSection: React.FC = () => {
return (
<section className="relative py-32 bg-terracotta-soft dark:bg-black overflow-hidden transition-colors duration-500">
<div className="absolute inset-0 z-0 mix-blend-multiply opacity-30">
<img
alt="Atmospheric studio background"
className="w-full h-full object-cover blur-3xl scale-110 grayscale"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ"
<img
alt="Atmospheric studio background"
className="w-full h-full object-cover blur-3xl scale-110 grayscale"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ"
/>
</div>
<div className="max-w-[1920px] mx-auto px-6 md:px-12 relative z-10">
<div className="flex justify-between items-baseline mb-20 border-b border-text-main/20 dark:border-gray-800 pb-6">
<h2 className="font-display text-6xl font-thin text-text-main dark:text-white">The <span className="italic">Journal</span></h2>
<a className="text-xs uppercase tracking-[0.2em] text-text-main dark:text-white hover:text-text-muted transition-colors" href="#">View Archive</a>
<h2 className="font-display text-6xl font-thin text-text-main dark:text-white">Editorial</h2>
<Link className="text-xs uppercase tracking-[0.2em] text-text-main dark:text-white hover:text-text-muted transition-colors" to="/editorial">View Archive</Link>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{JOURNAL_ENTRIES.map((entry) => (
<article key={entry.id} className={`group cursor-pointer ${entry.marginTop ? 'lg:mt-20' : ''}`}>
<div className="relative h-[500px] overflow-hidden mb-8 shadow-md">
<img
alt={entry.title}
className="w-full h-full object-cover transition-transform duration-[1.5s] group-hover:scale-105"
src={entry.image}
/>
<div className="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors duration-500"></div>
</div>
<div className="flex flex-col">
<div className="flex items-center space-x-4 mb-4">
<span className="text-[10px] uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-2 py-1 rounded-full">{entry.category}</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-text-muted">{entry.date}</span>
<Link key={entry.id} to={entry.slug} className={`group cursor-pointer block ${entry.marginTop ? 'lg:mt-20' : ''}`}>
<article>
<div className="relative h-[500px] overflow-hidden mb-8 shadow-md">
<img
alt={entry.title}
className="w-full h-full object-cover transition-transform duration-[1.5s] group-hover:scale-105"
src={entry.image}
/>
<div className="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors duration-500"></div>
</div>
<h3 className="font-display text-3xl text-text-main dark:text-white mb-4 leading-tight group-hover:underline decoration-1 underline-offset-4">
{entry.title}
</h3>
<p className="text-sm font-light text-text-muted dark:text-gray-400 leading-loose max-w-sm">
{entry.description}
</p>
</div>
</article>
<div className="flex flex-col">
<div className="flex items-center space-x-4 mb-4">
<span className="text-[10px] uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-2 py-1 rounded-full">{entry.category}</span>
<span className="text-[10px] uppercase tracking-[0.2em] text-text-muted">{entry.date}</span>
</div>
<h3 className="font-display text-3xl text-text-main dark:text-white mb-4 leading-tight group-hover:underline decoration-1 underline-offset-4">
{entry.title}
</h3>
<p className="text-sm font-light text-text-muted dark:text-gray-400 leading-loose max-w-sm">
{entry.description}
</p>
</div>
</article>
</Link>
))}
</div>
</div>