Initial commit of project structure

This commit is contained in:
Timo Knuth
2026-01-13 08:13:48 +01:00
commit be7f7b7bf7
47 changed files with 4455 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { motion } from 'framer-motion';
import { COLLECTIONS } from '../constants';
import { CollectionItem } from '../types';
const cardVariants = {
hidden: {
opacity: 0,
y: 80,
rotateX: 15,
},
visible: (index: number) => ({
opacity: 1,
y: 0,
rotateX: 0,
transition: {
delay: index * 0.15,
duration: 0.8,
ease: [0.25, 0.46, 0.45, 0.94],
},
}),
};
const Collections: React.FC = () => {
const col1 = [COLLECTIONS[0], COLLECTIONS[1]];
const col2 = [COLLECTIONS[2], COLLECTIONS[3]];
const col3 = [COLLECTIONS[4], COLLECTIONS[5]];
const renderCard = (item: CollectionItem, index: number) => (
<motion.a
key={item.id}
className="group block cursor-pointer"
href="#"
variants={cardVariants}
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
custom={index}
>
<div className={`relative overflow-hidden ${item.aspectRatio} mb-6`}>
{/* Image with clean hover effect */}
<motion.img
alt={`${item.title} collection`}
className="w-full h-full object-cover"
src={item.image}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] }}
/>
{/* Subtle overlay that fades out on hover */}
<motion.div
className="absolute inset-0 bg-black/5"
initial={{ opacity: 1 }}
whileHover={{ opacity: 0 }}
transition={{ duration: 0.4 }}
/>
{/* Clean reveal line effect on hover */}
<motion.div
className="absolute bottom-0 left-0 right-0 h-1 bg-white/80"
initial={{ scaleX: 0 }}
whileHover={{ scaleX: 1 }}
transition={{ duration: 0.5, ease: "easeOut" }}
style={{ transformOrigin: "left" }}
/>
</div>
<motion.div
className="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4"
initial={{ opacity: 0.8 }}
whileHover={{ opacity: 1 }}
>
<h3 className="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all duration-300">
{item.title}
</h3>
<motion.span
className="text-xs uppercase tracking-widest text-text-muted"
whileHover={{ x: 5 }}
transition={{ duration: 0.3 }}
>
{item.number}
</motion.span>
</motion.div>
</motion.a>
);
return (
<section className="py-32 bg-warm-grey dark:bg-[#141210] transition-colors duration-500">
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
<motion.div
className="flex flex-col md:flex-row justify-between items-end mb-20 md:mb-32 px-4"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<h2 className="font-display text-5xl md:text-7xl font-thin text-text-main dark:text-white">
Curated <span className="italic text-text-muted">Editions</span>
</h2>
<p className="hidden md:block font-body text-sm text-text-muted max-w-xs leading-relaxed text-right">
Explore our seasonal collections, fired in small batches.
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-16">
<div className="flex flex-col space-y-16 md:space-y-32">
{col1.map((item, idx) => renderCard(item, idx))}
</div>
<div className="flex flex-col space-y-16 md:space-y-32 md:pt-32">
{col2.map((item, idx) => renderCard(item, idx + 2))}
</div>
<div className="flex flex-col space-y-16 md:space-y-32 md:pt-16 lg:pt-0">
{col3.map((item, idx) => renderCard(item, idx + 4))}
</div>
</div>
</div>
</section>
);
};
export default Collections;

View File

@@ -0,0 +1,89 @@
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const FeatureSection: React.FC = () => {
const sectionRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const section = sectionRef.current;
const image = imageRef.current;
const content = contentRef.current;
if (!section || !image || !content) return;
// Image reveal animation
gsap.fromTo(
image,
{ clipPath: 'inset(100% 0 0 0)', opacity: 0 },
{
clipPath: 'inset(0% 0 0 0)',
opacity: 1,
duration: 1.2,
ease: 'power3.out',
scrollTrigger: {
trigger: section,
start: 'top 60%',
toggleActions: 'play none none reverse',
},
}
);
// Content fade in animation
gsap.fromTo(
content,
{ x: -60, opacity: 0 },
{
x: 0,
opacity: 1,
duration: 1,
ease: 'power3.out',
scrollTrigger: {
trigger: section,
start: 'top 50%',
toggleActions: 'play none none reverse',
},
}
);
return () => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
};
}, []);
return (
<section ref={sectionRef} className="py-32 md:py-48 bg-sage dark:bg-stone-900 overflow-hidden relative transition-colors duration-500">
<div className="max-w-[1800px] mx-auto px-6">
<div className="relative flex flex-col md:block">
<div className="hidden md:block absolute -top-24 left-10 z-0 select-none opacity-[0.03] dark:opacity-[0.05] pointer-events-none">
<span className="font-display text-[20rem] leading-none text-black dark:text-white">CRAFT</span>
</div>
<div
ref={imageRef}
className="w-full md:w-3/5 h-[600px] md:h-[800px] ml-auto relative z-10 shadow-2xl bg-center bg-cover bg-no-repeat"
style={{ backgroundImage: "url('/ceramic-cups.png')" }}
>
</div>
<div ref={contentRef} className="relative z-20 mt-[-100px] md:mt-0 md:absolute md:top-1/2 md:left-20 md:-translate-y-1/2 bg-white dark:bg-stone-900 p-12 md:p-20 shadow-xl max-w-xl mx-auto md:mx-0">
<span className="block w-12 h-[1px] bg-text-main mb-8"></span>
<h2 className="font-display text-4xl md:text-5xl font-light mb-8 text-text-main dark:text-white leading-tight">
The Art of <br /><i className="font-thin">Slow Living</i>
</h2>
<p className="font-body font-light text-text-muted dark:text-gray-400 mb-10 leading-loose text-sm">
We believe in the beauty of handmade objects. Our collection features a curated selection of ceramics designed to elevate the everyday. From sturdy mugs for your morning coffee to elegant vases that breathe life into a room, each piece is crafted with patience and intention.
</p>
<a className="group inline-flex items-center text-xs uppercase tracking-[0.2em] text-text-main dark:text-white font-medium" href="#">
Read Our Story <span className="ml-2 group-hover:translate-x-1 transition-transform"></span>
</a>
</div>
</div>
</div>
</section>
);
};
export default FeatureSection;

View File

@@ -0,0 +1,93 @@
import React from 'react';
import { FOOTER_LINKS } from '../constants';
const Footer: React.FC = () => {
return (
<footer className="bg-primary dark:bg-black text-white pt-32 pb-12 px-6 md:px-12 border-t border-stone-800">
<div className="max-w-[1920px] mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 mb-32">
{/* Brand & Mission */}
<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
</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.
</p>
</div>
<div className="mt-16 lg:mt-0">
<h4 className="text-sm font-bold uppercase tracking-widest mb-6">Join our newsletter</h4>
<form className="flex flex-col sm:flex-row gap-4 max-w-md" onSubmit={(e) => e.preventDefault()}>
<input
className="bg-white/5 border border-white/10 text-white placeholder-stone-500 focus:outline-none focus:border-white/30 text-sm px-6 py-4 w-full transition-colors"
placeholder="Enter your email"
type="email"
/>
<button
className="bg-white text-black px-8 py-4 text-xs font-bold uppercase tracking-widest hover:bg-stone-200 transition-colors"
type="submit"
>
Subscribe
</button>
</form>
</div>
</div>
{/* Links Columns */}
<div className="lg:col-span-7 grid grid-cols-2 md:grid-cols-3 gap-12 pt-4">
<div>
<h4 className="text-xs font-bold uppercase tracking-widest mb-8 text-stone-500">{FOOTER_LINKS[0].title}</h4>
<ul className="space-y-6">
{FOOTER_LINKS[0].links.map((link) => (
<li key={link.label}>
<a className="text-lg font-light hover:text-stone-400 hover:pl-2 transition-all duration-300 block" href={link.href}>
{link.label}
</a>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-xs font-bold uppercase tracking-widest mb-8 text-stone-500">{FOOTER_LINKS[1].title}</h4>
<ul className="space-y-6">
{FOOTER_LINKS[1].links.map((link) => (
<li key={link.label}>
<a className="text-lg font-light hover:text-stone-400 hover:pl-2 transition-all duration-300 block" href={link.href}>
{link.label}
</a>
</li>
))}
</ul>
</div>
<div>
<h4 className="text-xs font-bold uppercase tracking-widest mb-8 text-stone-500">{FOOTER_LINKS[2].title}</h4>
<ul className="space-y-6">
{FOOTER_LINKS[2].links.map((link) => (
<li key={link.label}>
<a className="text-lg font-light hover:text-stone-400 hover:pl-2 transition-all duration-300 block" href={link.href}>
{link.label}
</a>
</li>
))}
</ul>
</div>
</div>
</div>
{/* 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>
<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>
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -0,0 +1,179 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { GALLERY_IMAGES } from '../constants';
interface GalleryImage {
src: string;
likes: number;
comments: number;
caption: string;
}
const GallerySection: React.FC = () => {
const [selectedImage, setSelectedImage] = useState<GalleryImage | null>(null);
// Double the images for seamless infinite scroll
const duplicatedImages = [...GALLERY_IMAGES, ...GALLERY_IMAGES] as GalleryImage[];
return (
<>
<section className="py-20 bg-white dark:bg-background-dark overflow-hidden">
<div className="max-w-[1920px] mx-auto px-4">
<motion.div
className="flex justify-between items-center mb-8 px-2"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-500 via-pink-500 to-orange-400 p-[2px]">
<div className="w-full h-full rounded-full bg-white dark:bg-background-dark flex items-center justify-center">
<span className="font-display text-lg">H</span>
</div>
</div>
<div>
<h4 className="font-display text-xl text-text-main dark:text-white">@hotchpotsh_ceramics</h4>
<p className="text-xs text-text-muted">24.8k followers</p>
</div>
</div>
<a className="px-6 py-2 border border-text-main dark:border-white text-xs uppercase tracking-widest text-text-main dark:text-white hover:bg-text-main hover:text-white dark:hover:bg-white dark:hover:text-black transition-all duration-300 rounded-full" href="#">
Follow
</a>
</motion.div>
{/* Infinite Carousel */}
<div className="relative group overflow-hidden">
<style>{`
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-${GALLERY_IMAGES.length * 304}px); }
}
.animate-marquee {
animation: marquee 40s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
`}</style>
<div className="flex gap-4 animate-marquee w-max py-4">
{duplicatedImages.map((img, idx) => (
<motion.div
key={idx}
className="relative flex-shrink-0 w-72 h-72 overflow-hidden cursor-pointer rounded-lg"
whileHover={{ scale: 1.02 }}
onClick={() => setSelectedImage(img)}
>
<img
alt={img.caption}
className="w-full h-full object-cover"
src={img.src}
/>
{/* Instagram-style hover overlay */}
<motion.div
className="absolute inset-0 bg-black/50 flex items-center justify-center gap-8 text-white"
initial={{ opacity: 0 }}
whileHover={{ opacity: 1 }}
transition={{ duration: 0.2 }}
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined" style={{ fontVariationSettings: "'FILL' 1" }}>favorite</span>
<span className="font-bold">{img.likes.toLocaleString()}</span>
</div>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined">chat_bubble</span>
<span className="font-bold">{img.comments}</span>
</div>
</motion.div>
</motion.div>
))}
</div>
</div>
</div>
</section>
{/* Lightbox Modal */}
<AnimatePresence>
{
selectedImage && (
<motion.div
className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedImage(null)}
>
<motion.div
className="relative max-w-4xl w-full bg-white dark:bg-stone-900 rounded-xl overflow-hidden flex flex-col md:flex-row"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
onClick={(e) => e.stopPropagation()}
>
{/* Image */}
<div className="md:w-2/3 aspect-square md:aspect-auto">
<img
src={selectedImage.src}
alt={selectedImage.caption}
className="w-full h-full object-cover"
/>
</div>
{/* Side panel */}
<div className="md:w-1/3 p-6 flex flex-col">
{/* Header */}
<div className="flex items-center gap-3 pb-4 border-b border-stone-200 dark:border-stone-700">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-purple-500 via-pink-500 to-orange-400 p-[2px]">
<div className="w-full h-full rounded-full bg-white dark:bg-stone-900 flex items-center justify-center">
<span className="font-display text-sm">H</span>
</div>
</div>
<span className="font-bold text-sm text-text-main dark:text-white">hotchpotsh_ceramics</span>
</div>
{/* Caption */}
<div className="flex-1 py-4">
<p className="text-sm text-text-main dark:text-white">
<span className="font-bold">hotchpotsh_ceramics</span> {selectedImage.caption}
</p>
</div>
{/* Actions */}
<div className="border-t border-stone-200 dark:border-stone-700 pt-4">
<div className="flex gap-4 mb-4">
<button className="hover:scale-110 transition-transform">
<span className="material-symbols-outlined text-2xl text-text-main dark:text-white" style={{ fontVariationSettings: "'FILL' 1" }}>favorite</span>
</button>
<button className="hover:scale-110 transition-transform">
<span className="material-symbols-outlined text-2xl text-text-main dark:text-white">chat_bubble</span>
</button>
<button className="hover:scale-110 transition-transform">
<span className="material-symbols-outlined text-2xl text-text-main dark:text-white">send</span>
</button>
<button className="hover:scale-110 transition-transform ml-auto">
<span className="material-symbols-outlined text-2xl text-text-main dark:text-white">bookmark</span>
</button>
</div>
<p className="text-sm font-bold text-text-main dark:text-white">{selectedImage.likes.toLocaleString()} likes</p>
<p className="text-xs text-text-muted mt-1">{selectedImage.comments} comments</p>
</div>
</div>
{/* Close button */}
<button
className="absolute top-4 right-4 text-white bg-black/50 rounded-full p-2 hover:bg-black/70 transition-colors"
onClick={() => setSelectedImage(null)}
>
<span className="material-symbols-outlined">close</span>
</button>
</motion.div>
</motion.div>
)
}
</AnimatePresence >
</>
);
};
export default GallerySection;

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { NAV_ITEMS } from '../constants';
import { motion, AnimatePresence } from 'framer-motion';
import { Link } from 'react-router-dom';
const Header: React.FC = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
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'
}`}
>
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
<div className="flex justify-between items-center h-20">
{/* Mobile Menu Button */}
<div className="flex items-center md:hidden">
<button
className="text-text-main dark:text-white p-2 hover:bg-stone-100 dark:hover:bg-stone-800 rounded-full transition-colors"
type="button"
onClick={() => setIsMenuOpen(!isMenuOpen)}
>
<span className="material-symbols-outlined">menu</span>
</button>
</div>
{/* 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
</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" />
</div>
{/* Desktop Nav */}
<nav className="hidden md:flex space-x-12">
{NAV_ITEMS.map((item) => (
<Link
key={item.label}
className="group relative text-xs uppercase tracking-[0.2em] text-text-main dark:text-white hover:text-black dark:hover:text-white transition-colors duration-300 py-2"
to={item.label === 'Collections' ? '/collections' : item.label === 'Atelier' ? '/atelier' : '/editorial'}
>
{item.label}
{/* Underline Reveal Animation */}
<span className="absolute bottom-0 left-0 w-0 h-[1px] bg-black dark:bg-white transition-all duration-300 group-hover:w-full" />
</Link>
))}
</nav>
{/* Icons */}
<div className="flex items-center space-x-6 text-text-main dark:text-white">
<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="#">
<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>
</div>
</div>
</div>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden absolute top-full left-0 w-full bg-white/95 dark:bg-black/95 backdrop-blur-xl border-b border-stone-200 dark:border-stone-800 shadow-2xl overflow-hidden"
>
<div className="flex flex-col p-8 space-y-6">
{NAV_ITEMS.map((item, idx) => (
<Link
key={item.label}
to={item.label === 'Collections' ? '/collections' : item.label === 'Atelier' ? '/atelier' : '/editorial'}
className="text-lg uppercase tracking-[0.2em] text-text-main dark:text-white hover:pl-4 transition-all duration-300 border-l-2 border-transparent hover:border-black dark:hover:border-white"
onClick={() => setIsMenuOpen(false)}
>
<motion.span
initial={{ x: -20, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
transition={{ delay: idx * 0.1 }}
>
{item.label}
</motion.span>
</Link>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</header>
);
};
export default Header;

View File

@@ -0,0 +1,40 @@
import React from 'react';
const Hero: React.FC = () => {
return (
<section className="relative min-h-screen pt-24 w-full flex flex-col md:flex-row items-center overflow-hidden bg-background-light dark:bg-background-dark">
<div className="w-full md:w-5/12 h-full flex flex-col justify-center px-6 md:pl-20 md:pr-12 py-20 z-10">
<span className="font-body text-xs uppercase tracking-[0.3em] text-text-muted mb-8 ml-1 block">
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
</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.
</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 The Collection
</a>
</div>
</div>
<div className="w-full md:w-7/12 h-[60vh] md:h-screen relative">
<div className="absolute inset-0 bg-stone-200 dark:bg-stone-800">
<img
alt="Minimalist ceramic vase with single branch"
className="w-full h-full object-cover object-center brightness-95"
src="/pottery-studio.png"
/>
</div>
<div className="absolute bottom-10 left-10 md:left-auto md:right-20 bg-background-light/90 dark:bg-background-dark/90 backdrop-blur p-6 max-w-xs hidden md:block shadow-sm">
<p className="font-display italic text-xl text-text-main dark:text-gray-200">
"In emptiness, there is fullness."
</p>
</div>
</div>
</section>
);
};
export default Hero;

View File

@@ -0,0 +1,84 @@
import React, { useEffect, useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const horizontalImages = [
{ src: '/pottery-vase.png', title: 'Handcrafted Vases', description: 'Each vase tells a story of patience and craft' },
{ src: '/pottery-bowls.png', title: 'Artisan Bowls', description: 'Organic forms inspired by nature' },
{ src: '/pottery-plates.png', title: 'Dinner Collection', description: 'Elevate your everyday dining experience' },
{ src: '/pottery-studio.png', title: 'Our Studio', description: 'Where creativity meets tradition' },
{ src: '/ceramic-cups.png', title: 'Ceramic Cups', description: 'Handmade with love and intention' },
];
const HorizontalScrollSection: React.FC = () => {
const containerRef = useRef<HTMLDivElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = containerRef.current;
const scrollContainer = scrollRef.current;
if (!container || !scrollContainer) return;
const scrollWidth = scrollContainer.scrollWidth - window.innerWidth;
const tween = gsap.to(scrollContainer, {
x: -scrollWidth,
ease: 'none',
scrollTrigger: {
trigger: container,
start: 'top top',
end: () => `+=${scrollWidth * 0.5}`,
scrub: 1,
pin: true,
anticipatePin: 1,
},
});
return () => {
tween.scrollTrigger?.kill();
tween.kill();
};
}, []);
return (
<section ref={containerRef} className="relative overflow-hidden bg-clay-dark">
<div
ref={scrollRef}
className="flex h-screen items-center"
>
{horizontalImages.map((image, index) => (
<div
key={index}
className="relative flex-shrink-0 w-[90vw] md:w-[75vw] h-screen flex items-center justify-center p-4 md:p-8"
>
<div className="relative w-full h-full max-w-5xl max-h-[80vh] overflow-hidden rounded-lg shadow-2xl group">
<img
src={image.src}
alt={image.title}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent" />
<div className="absolute bottom-0 left-0 p-12 text-white">
<h3 className="font-display text-5xl md:text-6xl font-light mb-4">{image.title}</h3>
<p className="font-body text-lg font-light opacity-80 max-w-md">{image.description}</p>
</div>
</div>
<div className="absolute top-1/2 right-8 -translate-y-1/2 text-white/20 font-display text-[15rem] leading-none select-none pointer-events-none">
{String(index + 1).padStart(2, '0')}
</div>
</div>
))}
</div>
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex items-center gap-4 text-white/60">
<span className="material-symbols-outlined text-sm">arrow_back</span>
<span className="text-xs uppercase tracking-[0.3em] font-light">Scroll to explore</span>
<span className="material-symbols-outlined text-sm">arrow_forward</span>
</div>
</section>
);
};
export default HorizontalScrollSection;

View File

@@ -0,0 +1,50 @@
import React from 'react';
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"
/>
</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>
</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>
</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>
</div>
</section>
);
};
export default JournalSection;

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { motion } from 'framer-motion';
const PageLoader: React.FC = () => {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-stone-100 dark:bg-stone-900">
<div className="relative">
{/* Animated Pottery Outline */}
<motion.svg
width="120"
height="160"
viewBox="0 0 120 160"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<motion.path
d="M30 150C30 155.523 34.4772 160 40 160H80C85.5228 160 90 155.523 90 150V140H30V150Z"
stroke="currentColor"
strokeWidth="2"
className="text-stone-800 dark:text-stone-200"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatType: "reverse" }}
/>
<motion.path
d="M30 140C30 140 10 100 10 60C10 32.3858 32.3858 10 60 10C87.6142 10 110 32.3858 110 60C110 100 90 140 90 140H30Z"
stroke="currentColor"
strokeWidth="2"
className="text-stone-800 dark:text-stone-200"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatType: "reverse", delay: 0.5 }}
/>
<motion.ellipse
cx="60"
cy="10"
rx="25"
ry="5"
stroke="currentColor"
strokeWidth="2"
className="text-stone-800 dark:text-stone-200"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatType: "reverse", delay: 1 }}
/>
</motion.svg>
<motion.div
className="mt-8 text-center"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, repeat: Infinity, repeatType: "reverse" }}
>
<span className="font-display text-sm tracking-[0.3em] uppercase text-stone-500">Shaping</span>
</motion.div>
</div>
</div>
);
};
export default PageLoader;

View File

@@ -0,0 +1,19 @@
import React from 'react';
const QuoteSection: React.FC = () => {
return (
<section className="py-32 bg-clay-dark dark:bg-black border-y border-stone-800/50 transition-colors duration-500">
<div className="max-w-5xl mx-auto px-6 text-center">
<span className="material-symbols-outlined text-4xl mb-8 text-stone-500 font-thin">format_quote</span>
<h3 className="font-display text-3xl md:text-5xl font-thin leading-snug text-stone-100 dark:text-stone-200 mb-10 italic">
"My pottery is designed to be both beautiful and practical. From minimalist vases to durable dinner plates, each piece has its own character."
</h3>
<p className="font-body text-xs uppercase tracking-[0.2em] text-stone-400">
Anonymous Verified Collector
</p>
</div>
</section>
);
};
export default QuoteSection;

View File

@@ -0,0 +1,40 @@
import React, { useState, useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { motion, AnimatePresence } from 'framer-motion';
import PageLoader from './PageLoader';
const RouteTransition: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
// Trigger loading on route change
setIsLoading(true);
const timer = setTimeout(() => {
setIsLoading(false);
}, 2000); // 2 seconds loader on every page transition
return () => clearTimeout(timer);
}, [location.pathname]);
return (
<>
<AnimatePresence mode="wait">
{isLoading && (
<motion.div
key="loader"
exit={{ opacity: 0, transition: { duration: 0.5 } }}
className="fixed inset-0 z-[60]" // Higher z-index to cover everything
>
<PageLoader />
</motion.div>
)}
</AnimatePresence>
<div className={isLoading ? 'opacity-0' : 'opacity-100 transition-opacity duration-500'}>
{children}
</div>
</>
);
};
export default RouteTransition;

View File

@@ -0,0 +1,12 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
export default function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}