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

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Node.js
node_modules/
dist/
build/
.env
# Python
venv/
__pycache__/
*.pyc
# System
.DS_Store
Thumbs.db
# IDE
.vscode/

24
Pottery-website/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

34
Pottery-website/App.tsx Normal file
View File

@ -0,0 +1,34 @@
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Footer from './components/Footer';
import ScrollToTop from './components/ScrollToTop';
import RouteTransition from './components/RouteTransition';
// Lazy load pages for better performance
const Home = lazy(() => import('./pages/Home'));
const Collections = lazy(() => import('./pages/Collections'));
const Atelier = lazy(() => import('./pages/Atelier'));
const Editorial = lazy(() => import('./pages/Editorial'));
function App() {
return (
<Router>
<ScrollToTop />
<Header />
<RouteTransition>
<Suspense fallback={<div className="h-screen w-full bg-stone-100 dark:bg-stone-900" />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/collections" element={<Collections />} />
<Route path="/atelier" element={<Atelier />} />
<Route path="/editorial" element={<Editorial />} />
</Routes>
</Suspense>
</RouteTransition>
<Footer />
</Router>
);
}
export default App;

20
Pottery-website/README.md Normal file
View File

@ -0,0 +1,20 @@
<div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
</div>
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
View your app in AI Studio: https://ai.studio/apps/drive/1xMGYSZE5oFy0eXRMfs741pq-jMBBDS7Y
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

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;
}

View File

@ -0,0 +1,121 @@
import { NavItem, CollectionItem, JournalEntry, FooterSection } from './types';
export const NAV_ITEMS: NavItem[] = [
{ label: 'Collections', href: '#' },
{ label: 'Atelier', href: '#' },
{ label: 'Editorial', href: '#' },
];
export const COLLECTIONS: CollectionItem[] = [
{
id: 1,
title: 'Tableware',
number: '01',
image: '/collection-tableware.png',
aspectRatio: 'aspect-[3/4]',
},
{
id: 2,
title: 'Lighting',
number: '04',
image: '/collection-lighting.png',
aspectRatio: 'aspect-[4/3]',
},
{
id: 3,
title: 'Vases',
number: '02',
image: '/collection-vases.png',
aspectRatio: 'aspect-square',
},
{
id: 4,
title: 'Serving',
number: '05',
image: '/pottery-bowls.png',
aspectRatio: 'aspect-[3/4]',
},
{
id: 5,
title: 'Kitchenware',
number: '03',
image: '/collection-kitchenware.png',
aspectRatio: 'aspect-[3/5]',
},
{
id: 6,
title: 'Textiles',
number: '06',
image: '/pottery-plates.png',
aspectRatio: 'aspect-square',
},
];
export const JOURNAL_ENTRIES: JournalEntry[] = [
{
id: 1,
category: 'Studio',
date: 'Oct 03',
title: 'Product Photography for Small Businesses',
description: "Learning that beautiful products aren't enough on their own — you also need beautiful photos to tell the story.",
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ',
},
{
id: 2,
category: 'Guide',
date: 'Jul 15',
title: 'The Art of Packaging',
description: "A practical guide for potters who want to package and send their handmade ceramics with care and confidence.",
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAaWGnX_NYT3S_lOflL2NJZGbWge4AAkvra4ymvF8ag-c1UKsOAIB-rsLVQXW5xIlPZipDiK8-ysPyv22xdgsvzs4EOXSSCcrT4Lb2YCe0u5orxRaZEA5TgxeoKq15zaWKSlmnHyPGjPd_7yglpfO13eZmbU5KaxFJ1KGO0UAxoO9BpsyCYgbgINMoSz3epGe5ZdwBWRH-5KCzjoLuXimFTLcd5bqg9T1YofTxgy2hWBMJzKkafyEniq8dP6hMmfNCLVcCHHHx0hRU',
marginTop: true,
},
{
id: 3,
category: 'Wellness',
date: 'Jun 11',
title: 'Finding Motivation in Clay',
description: "10 gentle, practical tips to help potters find motivation during slow or uncertain moments in the creative process.",
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuB8NOE5fGfN4d87cbcB27_Sh-nrlZlqxsTlYKbCZk98SoL-gHsPSWFNuxd1DxBq0g8Qysh0RBZ_btu-_WaH68UjV8SXPUalyxREvUqao4oXmra--pWAsaooWwKvWCzReYZ8kj7G-KIYIAo5mqudzB8n9C6-HVTNPPx9QgZHr_YsojMxlmmVcQ5bqk7-Lp0KtSAiVIPD2-1UE1dMGnkVSLUXKdgA65JIh8M3TtNkaJTGONuFKoTERrYOWe7u2BILnqyukTzlNcvK7Sc',
},
];
export const GALLERY_IMAGES = [
{ src: '/ceramic-cups.png', likes: 2847, comments: 124, caption: 'Morning rituals ☕' },
{ src: '/pottery-vase.png', likes: 3521, comments: 89, caption: 'Crafted with intention 🏺' },
{ src: '/pottery-bowls.png', likes: 1956, comments: 67, caption: 'Wabi-sabi collection' },
{ src: '/pottery-plates.png', likes: 4102, comments: 156, caption: 'Ready for your table ✨' },
{ src: '/pottery-studio.png', likes: 5234, comments: 203, caption: 'Where the magic happens' },
{ src: '/collection-tableware.png', likes: 2678, comments: 94, caption: 'Stacked with love' },
{ src: '/collection-vases.png', likes: 3189, comments: 112, caption: 'Organic forms' },
{ src: '/collection-kitchenware.png', likes: 1847, comments: 78, caption: 'Matcha time 🍵' },
];
export const FOOTER_LINKS: FooterSection[] = [
{
title: 'Shop',
links: [
{ label: 'All Ceramics', href: '#' },
{ label: 'New Arrivals', href: '#' },
{ label: 'Best Sellers', href: '#' },
{ label: 'Gift Cards', href: '#' },
],
},
{
title: 'Company',
links: [
{ label: 'Our Story', href: '#' },
{ label: 'Sustainability', href: '#' },
{ label: 'Careers', href: '#' },
{ label: 'Press', href: '#' },
],
},
{
title: 'Support',
links: [
{ label: 'FAQ', href: '#' },
{ label: 'Shipping', href: '#' },
{ label: 'Returns', href: '#' },
{ label: 'Contact', href: '#' },
],
},
];

View File

@ -0,0 +1,103 @@
import { useEffect, useRef, RefObject } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
interface ScrollAnimationOptions {
trigger?: RefObject<HTMLElement>;
start?: string;
end?: string;
scrub?: boolean | number;
markers?: boolean;
}
export const useScrollFadeIn = (
ref: RefObject<HTMLElement>,
options: ScrollAnimationOptions = {}
) => {
useEffect(() => {
const element = ref.current;
if (!element) return;
gsap.fromTo(
element,
{ opacity: 0, y: 60 },
{
opacity: 1,
y: 0,
duration: 1,
ease: 'power3.out',
scrollTrigger: {
trigger: options.trigger?.current || element,
start: options.start || 'top 85%',
end: options.end || 'top 20%',
toggleActions: 'play none none reverse',
},
}
);
return () => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
};
}, [ref, options]);
};
export const useParallax = (
ref: RefObject<HTMLElement>,
speed: number = 0.5
) => {
useEffect(() => {
const element = ref.current;
if (!element) return;
gsap.to(element, {
yPercent: -50 * speed,
ease: 'none',
scrollTrigger: {
trigger: element,
start: 'top bottom',
end: 'bottom top',
scrub: true,
},
});
return () => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
};
}, [ref, speed]);
};
export const useStaggerReveal = (
containerRef: RefObject<HTMLElement>,
childSelector: string,
options: ScrollAnimationOptions = {}
) => {
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const children = container.querySelectorAll(childSelector);
gsap.fromTo(
children,
{ opacity: 0, y: 40 },
{
opacity: 1,
y: 0,
duration: 0.8,
stagger: 0.15,
ease: 'power2.out',
scrollTrigger: {
trigger: container,
start: options.start || 'top 80%',
toggleActions: 'play none none reverse',
},
}
);
return () => {
ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
};
}, [containerRef, childSelector, options]);
};

View File

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>HOTCHPOTSH Ceramics - Editorial Collection</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&amp;family=Manrope:wght@200;300;400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#292524", // Warm Charcoal
secondary: "#78716C", // Warm Stone
"background-light": "#F5F4F0", // Soft Sand / Alabaster (Hero)
"background-dark": "#1C1917", // Dark Warm Grey
// New Sectional Colors
"sage": "#D4D9D1", // Soft Sage Green
"warm-grey": "#DAD7D4", // Warm Grey
"clay-dark": "#33302D", // Deep Charcoal / Clay
"terracotta-soft": "#E6DDD5", // Pale Ochre / Soft Terracotta
"accent-sand": "#E7E5E4",
"accent-warm": "#D6D3D1",
"text-main": "#1C1917",
"text-muted": "#57534E",
},
fontFamily: {
display: ["Cormorant Garamond", "serif"],
body: ["Manrope", "sans-serif"],
},
fontSize: {
'10xl': '10rem',
},
spacing: {
'128': '32rem',
}
},
},
};
</script>
<style>
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24
}
html {
scroll-behavior: smooth;
}
.parallax-bg {
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #F5F4F0;
}
::-webkit-scrollbar-thumb {
background: #D6D3D1;
}
::-webkit-scrollbar-thumb:hover {
background: #78716C;
}
</style>
<script type="importmap">
{
"imports": {
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"react/": "https://esm.sh/react@^19.2.3/",
"react": "https://esm.sh/react@^19.2.3"
}
}
</script>
</head>
<body class="font-body bg-background-light dark:bg-background-dark text-text-main dark:text-gray-200 antialiased transition-colors duration-500 selection:bg-stone-200 selection:text-black">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
Pottery-website/index.tsx Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,5 @@
{
"name": "IKKAI Ceramics",
"description": "A high-fidelity recreation of the IKKAI Ceramics editorial e-commerce website featuring a minimalist design, custom typography, and responsive layout.",
"requestFramePermissions": []
}

1798
Pottery-website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "ikkai-ceramics",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"framer-motion": "^12.26.0",
"gsap": "^3.14.2",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-router-dom": "^7.12.0"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0"
}
}

View File

@ -0,0 +1,73 @@
import React from 'react';
import { motion } from 'framer-motion';
const Atelier: React.FC = () => {
return (
<div className="bg-stone-50 dark:bg-stone-900 min-h-screen pt-32 pb-24">
<div className="max-w-[1920px] mx-auto px-6 md:px-12">
{/* Intro */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-12 mb-32 items-center">
<div className="md:col-span-5 md:col-start-2">
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1 }}
className="block text-xs uppercase tracking-[0.3em] text-stone-400 mb-6"
>
The Studio
</motion.span>
<motion.h1
initial={{ y: 30, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.8 }}
className="font-display text-5xl md:text-7xl lg:text-8xl leading-none text-text-main dark:text-white mb-8"
>
Formed by<br />Hand & Fire
</motion.h1>
<motion.p
initial={{ y: 30, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
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.
</motion.p>
</div>
<div className="md:col-span-12 lg:col-span-6 relative h-[600px] lg:h-[800px] w-full">
<motion.div
initial={{ clipPath: 'inset(100% 0 0 0)' }}
animate={{ clipPath: 'inset(0% 0 0 0)' }}
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" />
</motion.div>
</div>
</div>
{/* 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." }
].map((item, idx) => (
<motion.div
key={item.title}
initial={{ y: 20, opacity: 0 }}
whileInView={{ y: 0, opacity: 1 }}
viewport={{ once: true }}
transition={{ delay: idx * 0.2, duration: 0.8 }}
className="p-8 hover:bg-white dark:hover:bg-black transition-colors duration-500"
>
<h3 className="font-display text-2xl mb-4 text-text-main dark:text-white">{item.title}</h3>
<p className="font-body font-light text-stone-500 leading-relaxed">{item.text}</p>
</motion.div>
))}
</div>
</div>
</div>
);
};
export default Atelier;

View File

@ -0,0 +1,68 @@
import React from 'react';
import { motion } from 'framer-motion';
import { COLLECTIONS } from '../constants';
const Collections: React.FC = () => {
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>
{/* 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) => (
<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"
>
{/* 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 }}
/>
{/* 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" />
</div>
<div className="flex justify-between items-end border-b border-stone-200 dark:border-stone-800 pb-4">
<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>
</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>
</div>
</motion.div>
))}
</div>
</div>
</section>
</>
);
};
export default Collections;

View File

@ -0,0 +1,97 @@
import React from 'react';
import { motion } from 'framer-motion';
import { JOURNAL_ENTRIES } from '../constants';
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>
{/* 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>
</div>
</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>
</div>
);
};
export default Editorial;

View File

@ -0,0 +1,24 @@
import React from 'react';
import Hero from '../components/Hero';
import FeatureSection from '../components/FeatureSection';
import HorizontalScrollSection from '../components/HorizontalScrollSection';
import Collections from '../components/Collections';
import QuoteSection from '../components/QuoteSection';
import JournalSection from '../components/JournalSection';
import GallerySection from '../components/GallerySection';
const Home: React.FC = () => {
return (
<main>
<Hero />
<FeatureSection />
<HorizontalScrollSection />
<Collections />
<QuoteSection />
<JournalSection />
<GallerySection />
</main>
);
};
export default Home;

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 794 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 KiB

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

28
Pottery-website/types.ts Normal file
View File

@ -0,0 +1,28 @@
export interface NavItem {
label: string;
href: string;
}
export interface CollectionItem {
id: number;
title: string;
number: string;
image: string;
aspectRatio: string; // Tailwind class like aspect-[3/4]
gridClasses?: string; // Optional layout adjustments
}
export interface JournalEntry {
id: number;
category: string;
date: string;
title: string;
description: string;
image: string;
marginTop?: boolean;
}
export interface FooterSection {
title: string;
links: NavItem[];
}

View File

@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});

379
code.html Normal file
View File

@ -0,0 +1,379 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>IKKAI Ceramics - Editorial Collection</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&amp;family=Manrope:wght@200;300;400;500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
primary: "#292524", // Warm Charcoal
secondary: "#78716C", // Warm Stone
"background-light": "#F5F4F0", // Soft Sand / Alabaster (Hero)
"background-dark": "#1C1917", // Dark Warm Grey
// New Sectional Colors
"sage": "#D4D9D1", // Soft Sage Green
"warm-grey": "#DAD7D4", // Warm Grey
"clay-dark": "#33302D", // Deep Charcoal / Clay
"terracotta-soft": "#E6DDD5", // Pale Ochre / Soft Terracotta
"accent-sand": "#E7E5E4",
"accent-warm": "#D6D3D1",
"text-main": "#1C1917",
"text-muted": "#57534E",
},
fontFamily: {
display: ["Cormorant Garamond", "serif"],
body: ["Manrope", "sans-serif"],
},
fontSize: {
'10xl': '10rem',
},
spacing: {
'128': '32rem',
}
},
},
};
</script>
<style>
.material-symbols-outlined {
font-variation-settings:
'FILL' 0,
'wght' 300,
'GRAD' 0,
'opsz' 24
}
html {
scroll-behavior: smooth;
}
.parallax-bg {
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #F5F4F0;
}
::-webkit-scrollbar-thumb {
background: #D6D3D1;
}
::-webkit-scrollbar-thumb:hover {
background: #78716C;
}
</style>
</head>
<body class="font-body bg-background-light dark:bg-background-dark text-text-main dark:text-gray-200 antialiased transition-colors duration-500 selection:bg-stone-200 selection:text-black">
<div class="bg-primary dark:bg-black text-white text-[10px] tracking-[0.2em] text-center py-3 uppercase font-light">
Complimentary shipping on orders over €200
</div>
<header class="fixed top-0 w-full z-50 bg-background-light/80 dark:bg-background-dark/80 backdrop-blur-md border-b border-stone-200/50 dark:border-stone-800/50 transition-all duration-300">
<div class="max-w-[1920px] mx-auto px-6 md:px-12">
<div class="flex justify-between items-center h-24">
<div class="flex items-center md:hidden">
<button class="text-text-main dark:text-white p-2" type="button">
<span class="material-symbols-outlined">menu</span>
</button>
</div>
<div class="flex-shrink-0">
<a class="font-display text-4xl md:text-5xl font-light tracking-widest uppercase text-text-main dark:text-white" href="#">
IKKAI
</a>
</div>
<nav class="hidden md:flex space-x-16">
<a class="text-xs uppercase tracking-[0.15em] text-text-muted dark:text-gray-400 hover:text-primary dark:hover:text-white transition-colors duration-300" href="#">Collections</a>
<a class="text-xs uppercase tracking-[0.15em] text-text-muted dark:text-gray-400 hover:text-primary dark:hover:text-white transition-colors duration-300" href="#">Atelier</a>
<a class="text-xs uppercase tracking-[0.15em] text-text-muted dark:text-gray-400 hover:text-primary dark:hover:text-white transition-colors duration-300" href="#">Editorial</a>
</nav>
<div class="flex items-center space-x-6 text-text-main dark:text-white">
<button class="hover:text-text-muted transition-colors hidden sm:block">
<span class="material-symbols-outlined text-xl font-light">search</span>
</button>
<a class="hover:text-text-muted transition-colors relative group" href="#">
<span class="material-symbols-outlined text-xl font-light">shopping_bag</span>
<span class="absolute -top-1 -right-2 bg-text-main dark:bg-white text-white dark:text-black text-[9px] w-4 h-4 flex items-center justify-center rounded-full opacity-0 group-hover:opacity-100 transition-opacity">2</span>
</a>
</div>
</div>
</div>
</header>
<section class="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 class="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 class="font-body text-xs uppercase tracking-[0.3em] text-text-muted mb-8 ml-1 block">New Collection 2024</span>
<h1 class="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 class="italic pl-12 md:pl-20 text-text-muted">of</span> Earth
</h1>
<p class="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 class="ml-1">
<a class="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 class="w-full md:w-7/12 h-[60vh] md:h-screen relative">
<div class="absolute inset-0 bg-stone-200 dark:bg-stone-800">
<img alt="Minimalist ceramic vase with single branch" class="w-full h-full object-cover object-center brightness-95" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBvZAnP7XY-eGn5eL8LHgtIhpGoP7m9sPeESpuqYuRRomwq-VbkWnk3og2Zz3Y008nJOg3m1_HX59c-oDrJrtdXltWuBceYV0538LLAwbtslwnO_7BOuBw5y4v-4m9JtFou3lwflr2jbi_6zW8EZaxmGL6_EqVOkYct5HiXbw0JYTYhxPegtBET_-AeTOqJHvuDJGSzRAImHVh74ucDQgnl6QzlQZ17IKZU8o-1SdfLMvL8EvTb-jAeb7wv-wHpLSPbHK4XwYiVszk"/>
</div>
<div class="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 class="font-display italic text-xl text-text-main dark:text-gray-200">"In emptiness, there is fullness."</p>
</div>
</div>
</section>
<section class="py-32 md:py-48 bg-sage dark:bg-stone-900 overflow-hidden relative transition-colors duration-500">
<div class="max-w-[1800px] mx-auto px-6">
<div class="relative flex flex-col md:block">
<div class="hidden md:block absolute -top-24 left-10 z-0 select-none opacity-[0.03] dark:opacity-[0.05] pointer-events-none">
<span class="font-display text-[20rem] leading-none text-black dark:text-white">CRAFT</span>
</div>
<div class="w-full md:w-3/5 h-[600px] md:h-[800px] ml-auto relative z-10 parallax-bg shadow-2xl" style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAwcT6-AV8qpFzjZnVg9E60XgAEjN8kSfvJhBviAQySkGqDm950ofKMSXpKUvN44YobZIMvBeV-QcLz_xE7hQKdYdSIjoPasavbYAMbtqN4XySDFYqxgVq34e2R0BJJqX0WAzSFTcTd1WbnDjhlb8Vr8NyVtoB-09ArCsO6ZwnpvplPNHWwqzA0pebI6c32n8BPTMwvL5MuqUV8T5-tEw6MiNVyXJKGX-EIAxboK60MBn0tdYRBneueLDgcjvJ-s7R6yVBe1H4j1kc');">
</div>
<div class="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 class="block w-12 h-[1px] bg-text-main mb-8"></span>
<h2 class="font-display text-4xl md:text-5xl font-light mb-8 text-text-main dark:text-white leading-tight">
The Art of <br/><i class="font-thin">Slow Living</i>
</h2>
<p class="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 class="group inline-flex items-center text-xs uppercase tracking-[0.2em] text-text-main dark:text-white font-medium" href="#">
Read Our Story <span class="ml-2 group-hover:translate-x-1 transition-transform"></span>
</a>
</div>
</div>
</div>
</section>
<section class="py-32 bg-warm-grey dark:bg-[#141210] transition-colors duration-500">
<div class="max-w-[1920px] mx-auto px-6 md:px-12">
<div class="flex flex-col md:flex-row justify-between items-end mb-20 md:mb-32 px-4">
<h2 class="font-display text-5xl md:text-7xl font-thin text-text-main dark:text-white">Curated <span class="italic text-text-muted">Editions</span></h2>
<p class="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>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 lg:gap-16">
<div class="flex flex-col space-y-16 md:space-y-32">
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-[3/4] mb-6">
<img alt="Tableware collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDEMG0U2yN-srAgpA4aSXewbFyRyFWnm181AquAJCRzLwgPHNHbs-fxFKQ8DMbozvyRU-s0LUPRKoZtht1-Lp3RDOfKE3jCrAD_A4tl9BXwHGUcAPWj0jBq3C9plosFkHIzYUDBtbq_Azg3RK2csufB9tH_tIJhMW--_IIfZeAltM9sgTD5wAPRPIUyV-0iemF2eWLZnx0IfTLZSkN930lHZ6aHxWChqHqoVMUTdYxqHPt0tpUW3C082em7_4fuoRpWdf4_flYYoO4"/>
<div class="absolute inset-0 bg-black/10 group-hover:bg-transparent transition-colors duration-500"></div>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Tableware</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">01</span>
</div>
</a>
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-[4/3] mb-6">
<img alt="Lamp Shades collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAz5MOY7i5TxXxrGVaW7nItrMPEhwnNz5VkQ7BwzWHUBMfV3j8A42PekcfAMOXu7nP2pX7m-Trx0lBWwFq4RuDfJMghT-DwyJAP4nT2sTCgX_WosvcMQfj5koFU-CLX7CMboAxAPXWUWe3Q8xU4Zl0kysFKLG34fR_GaRlN0diovvLg1SQ6fLq2dMRg2o523onwafjD0f6XBDxbtWBsnfIp_2U1_0zFahOkW2JyyJhIZFVCTiP61CY2rkwqtmupBjzzY7iKcMtszhE"/>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Lighting</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">04</span>
</div>
</a>
</div>
<div class="flex flex-col space-y-16 md:space-y-32 md:pt-32">
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-square mb-6">
<img alt="Home Decor collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBV8jgjafGxXCrSmiIUCN5Zwqv0cl-Ivw5hjQoa5ejFBx3a0C7zgeI_pywBpee7f0scHB_zrYQbI0zryX0P_F2w_xefVbl_8vkvSRMPhsqrs_z9u16FlDVgmXX9_PxhC8oRWZmGbtHsvXhfDEtvAi94tBJeQYTdG2a-XJ7gB0F8GLyvVl7_NHu9iB_TyVhbKIOv354VUmcNAehnGfuK0fTtAjQr0qxaHt8CD9pLJvfTeVJZF2VPRgToY5dN4eqRTRJrQPuLIW2aP9k"/>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Vases</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">02</span>
</div>
</a>
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-[3/4] mb-6">
<img alt="Accessories collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDlTM_GLiQWReZzrfgwPZXJ2OjRaelPwrF2URWBLiqQjOdyxy0onUB9FoX4kcIzTnQSRIfRZsg6Dt5BS6j5bE6SYhdkZ60HAOZJNybnBvZqfICwldKNMiTg9-fm4X1otiHO8vO_Hr-DuwsaE818YSDiW2vyVH947T8peRurHz-sYZu9gJgq9R4D3BtLrdbf9R6MaYmqGZ47NAwHV1BHicOSMFGxfhK-p5exDM963E8qBTwl3PEXcRdnAq6-B-ada0XJ3jz8iA4Cavo"/>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Serving</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">05</span>
</div>
</a>
</div>
<div class="flex flex-col space-y-16 md:space-y-32 md:pt-16 lg:pt-0">
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-[3/5] mb-6">
<img alt="Matcha Bowls collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD_jVXnz1bLom_g2aBEGMY2fmAe2vuv4pwP_NdhGGqtKhcIr36eQ-BQ5d3cMXXjIDURtvBt9juNVX8kJG1T404125GGwEoQmMCD4IawdGY7hDBeByI8PG9Z8Ioc-skCG9X5bdU9-O4PS6KBglPV8fnkyG6FjPkN0RdGvHWMZQ6iInrJhOqiqX3r-6YvmIpGSi5FoXyFnmcfLnf1faHq8kWIMuv0WgLHSlOFlB5MeIJAQwvuk5Gbk4drXCt2heYy5WRWIdutVdiQOa4"/>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Kitchenware</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">03</span>
</div>
</a>
<a class="group block cursor-pointer" href="#">
<div class="relative overflow-hidden aspect-square mb-6">
<img alt="Furoshiki collection" class="w-full h-full object-cover transition-transform duration-[1.5s] ease-out group-hover:scale-110" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAI1p48nya5N3xLUE8fx6a0Cwniu_QuS5yo-wq6PE1W77n0orf57QyF1g4426uqGtiv02HHHXd40Sq81usdXnpOqvLiviW_gSGdtlorcOpaSl6R8k23cG_I-5v4pPVJiaTPqrhK1U3VtxLX5Bpj8x7NOtZT4K1jtI4NHt-S1A0GvBjM7jCfH_0y8Xw8L_R5br8I8_KmdbC7ACaNd4OAZUpJdt4UUANVy664jG4m9dZshHpa8Og4aFzZ1CRxmQExSVEzc0CKZ9GSLB0"/>
</div>
<div class="flex justify-between items-center border-t border-gray-400/50 dark:border-gray-800 pt-4">
<h3 class="font-display text-3xl font-light text-text-main dark:text-white group-hover:italic transition-all">Textiles</h3>
<span class="text-xs uppercase tracking-widest text-text-muted">06</span>
</div>
</a>
</div>
</div>
</div>
</section>
<section class="py-32 bg-clay-dark dark:bg-black border-y border-stone-800/50 transition-colors duration-500">
<div class="max-w-5xl mx-auto px-6 text-center">
<span class="material-symbols-outlined text-4xl mb-8 text-stone-500 font-thin">format_quote</span>
<h3 class="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 class="font-body text-xs uppercase tracking-[0.2em] text-stone-400">
Anonymous — Verified Collector
</p>
</div>
</section>
<section class="relative py-32 bg-terracotta-soft dark:bg-black overflow-hidden transition-colors duration-500">
<div class="absolute inset-0 z-0 mix-blend-multiply opacity-30">
<img alt="Atmospheric studio background" class="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 class="max-w-[1920px] mx-auto px-6 md:px-12 relative z-10">
<div class="flex justify-between items-baseline mb-20 border-b border-text-main/20 dark:border-gray-800 pb-6">
<h2 class="font-display text-6xl font-thin text-text-main dark:text-white">The <span class="italic">Journal</span></h2>
<a class="text-xs uppercase tracking-[0.2em] text-text-main dark:text-white hover:text-text-muted transition-colors" href="#">View Archive</a>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-12">
<article class="group cursor-pointer">
<div class="relative h-[500px] overflow-hidden mb-8 shadow-md">
<img alt="Pottery workshop" class="w-full h-full object-cover transition-transform duration-[1.5s] group-hover:scale-105" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAipMlYLTcRT_hdc3VePfFIlrA56VzZ5G2y3gcRfmIZMERwGFKq2N19Gqo6mw7uZowXmjl2eJ89TI3Mcud2OyOfadO3mPVF_v0sI0OHupqM49WEFcWzH-Wbu3DL6bQ46F2Y8SIAk-NUQy8psjcIdBKRrM8fqdn4eOPANYTXpVxkLMAm4R0Axy4aEKNdmj917ZKKTxvXB-J8nGlITJkJ-ua7XcZOwGnfK5ttzyWW35A0oOSffCf972gmpV27wrVQgYJNLS7UyDdyQIQ"/>
<div class="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors duration-500"></div>
</div>
<div class="flex flex-col">
<div class="flex items-center space-x-4 mb-4">
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-2 py-1 rounded-full">Studio</span>
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted">Oct 03</span>
</div>
<h3 class="font-display text-3xl text-text-main dark:text-white mb-4 leading-tight group-hover:underline decoration-1 underline-offset-4">Product Photography for Small Businesses</h3>
<p class="text-sm font-light text-text-muted dark:text-gray-400 leading-loose max-w-sm">
Learning that beautiful products aren't enough on their own — you also need beautiful photos to tell the story.
</p>
</div>
</article>
<article class="group cursor-pointer lg:mt-20">
<div class="relative h-[500px] overflow-hidden mb-8 shadow-md">
<img alt="Packing ceramics" class="w-full h-full object-cover transition-transform duration-[1.5s] group-hover:scale-105" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAaWGnX_NYT3S_lOflL2NJZGbWge4AAkvra4ymvF8ag-c1UKsOAIB-rsLVQXW5xIlPZipDiK8-ysPyv22xdgsvzs4EOXSSCcrT4Lb2YCe0u5orxRaZEA5TgxeoKq15zaWKSlmnHyPGjPd_7yglpfO13eZmbU5KaxFJ1KGO0UAxoO9BpsyCYgbgINMoSz3epGe5ZdwBWRH-5KCzjoLuXimFTLcd5bqg9T1YofTxgy2hWBMJzKkafyEniq8dP6hMmfNCLVcCHHHx0hRU"/>
<div class="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors duration-500"></div>
</div>
<div class="flex flex-col">
<div class="flex items-center space-x-4 mb-4">
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-2 py-1 rounded-full">Guide</span>
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted">Jul 15</span>
</div>
<h3 class="font-display text-3xl text-text-main dark:text-white mb-4 leading-tight group-hover:underline decoration-1 underline-offset-4">The Art of Packaging</h3>
<p class="text-sm font-light text-text-muted dark:text-gray-400 leading-loose max-w-sm">
A practical guide for potters who want to package and send their handmade ceramics with care and confidence.
</p>
</div>
</article>
<article class="group cursor-pointer">
<div class="relative h-[500px] overflow-hidden mb-8 shadow-md">
<img alt="Pottery tools" class="w-full h-full object-cover transition-transform duration-[1.5s] group-hover:scale-105" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB8NOE5fGfN4d87cbcB27_Sh-nrlZlqxsTlYKbCZk98SoL-gHsPSWFNuxd1DxBq0g8Qysh0RBZ_btu-_WaH68UjV8SXPUalyxREvUqao4oXmra--pWAsaooWwKvWCzReYZ8kj7G-KIYIAo5mqudzB8n9C6-HVTNPPx9QgZHr_YsojMxlmmVcQ5bqk7-Lp0KtSAiVIPD2-1UE1dMGnkVSLUXKdgA65JIh8M3TtNkaJTGONuFKoTERrYOWe7u2BILnqyukTzlNcvK7Sc"/>
<div class="absolute inset-0 bg-black/10 group-hover:bg-black/0 transition-colors duration-500"></div>
</div>
<div class="flex flex-col">
<div class="flex items-center space-x-4 mb-4">
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted border border-text-muted/30 px-2 py-1 rounded-full">Wellness</span>
<span class="text-[10px] uppercase tracking-[0.2em] text-text-muted">Jun 11</span>
</div>
<h3 class="font-display text-3xl text-text-main dark:text-white mb-4 leading-tight group-hover:underline decoration-1 underline-offset-4">Finding Motivation in Clay</h3>
<p class="text-sm font-light text-text-muted dark:text-gray-400 leading-loose max-w-sm">
10 gentle, practical tips to help potters find motivation during slow or uncertain moments in the creative process.
</p>
</div>
</article>
</div>
</div>
</section>
<section class="py-20 bg-white dark:bg-background-dark">
<div class="max-w-[1920px] mx-auto px-4">
<div class="flex justify-between items-center mb-8 px-2">
<h4 class="font-display text-2xl text-text-main dark:text-white">@ikkai_ceramics</h4>
<a class="text-xs uppercase tracking-widest text-text-muted hover:text-primary transition-colors" href="#">Follow</a>
</div>
<div class="grid grid-cols-2 md:grid-cols-6 gap-px bg-stone-100 dark:bg-stone-800 border border-stone-100 dark:border-stone-800">
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 1" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDGY1PXPxlhIz9qU-XbDrdJjrRnX1pFo8YpH3HM1Crq9C6iVApx-qFkpjTj_MDOXXrX4jprk69hA0fmwR2EdURQyKaBLDAdkIE3vLKCyTRMhgyGerlpsy6_KZkZs-9hiaoWZPBFzvBIGWZ0i7sfbbtkQdBGJfK30yftDOPjI1NJfzBtsKNMbYOnXfmm-6u7uiovrM54rtRNWzsxmcvhRKQebZIUrERvGGYsUvUVARYEzSs4thyJnMYROk0LmoCrJ03_QjDvLzy_zjw"/>
</div>
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 2" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBLafDd9RRuru_JkGfpxms6q-dFwzNrjKIazUT33dREB4eWcGSGtNYEKzTCgaRJYIGNvhE1z1tYSrr3ZMrMSQs_3oIJz2hrlYYq49EJaI8VD2YrM9akd50BsF3voaGW1yZFHM5S36ZbrCx3A8Id2wkDnlJ7TnUYdo76-TErMa6h94HxQYBSwLQESFBrPfDEi5Qf6MDfE-6i3HJNIYGS79zemso4U8mMRi-HA17y4ilifDoI2B4vc3ROE3HFbTVP6JxJxjklnlbMt28"/>
</div>
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 3" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDGyeyp1BDBjdasLDmyEWUfA0eeSJ8qMmrlPS0IgsyNdW_-0NEbFZhO72z3kox8ys3T2PZOprxzbnDwBQfjdwLiISmT3eHil4LQgRNt35AucCh1b-BFmUjXB9vuQ-JAFY122ABF9AmGWIhKCH7HHJj-Pibiz4NlcEGf_-59GAtt_y4g6OzzZpBKzfZAXd2_h-2I3ZbMaNDYEVK9dZSwVSrUNxQEFRFLUoqoNm8_VqgfpwpXmGwSmEZfy-lfjnXDBF1AS3ipD7JR04E"/>
</div>
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 4" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBxM-yqwsAyWN-4sNS6FCNyry1QP6_yKpP7av1uPQTEArxL29_Ir1vfmlNJ50UMMYBRXHwSYUS9dwgO9hsccM8QKk0DybH5Hsa0k1oA1PSD2fIButt6JbICrrLhqC51S37PtN7vpPxtlqFPXQyaGEQl8r8eZbbROIqtUCGdWks_prak9UNTbeph0gHDa0Xlw8HtXRSgQCqONjRuWVWC9ynnqpqXXgLIeCvCVBUiQuXxCnhLsMDWOcw8sjaVSFKo1tRi9IUFRaXEGu0"/>
</div>
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 5" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDD63SnScOyJcWSHLRucaOZ46wQpGV0c-ljb4DvnaURof8aTmuMD75dT1UUKXzDwWBUKGqu3nNlUUOXnEfjLE8pwAdvRKHhunOunZW5qbw35eY1vH14HXGqQSe90m7RUxku70QRlVS338tKQEAJ9TasOSte56oSEmKzUC0q9VF9P8GTl-0R8CcmvQ9hfwRIe34s2QUEwE96LYTREHdWZI6RRZojG8MTeV1qGFgFgjwEqnYbIGCFdW5TFMyTvkuPd1R0IBNfWZzhJkM"/>
</div>
<div class="aspect-square relative group overflow-hidden">
<img alt="Gallery image 6" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105 grayscale hover:grayscale-0" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBQAOyWAgHl3Uf9zP9-cmp1ZPrAw5wi7GP1div8GHtarOvO68Kn8069PqiCFYs91kLD1YWcb9tk3y9Fm12AmMfRpIIvNTvAWxkZ7xL0BWM_UZ5BPmvSVuRDXKcvg5_qQVXJOy5ub3Yu3oBqKhR617MhwY4F_Am0cNClmSgPaYHALRi-CB3_hlLdgXQhI0dP5j7yNqlrTxHHv34vRQWvg2_Htkum0XcSQHuK9-A89-Cgcz5-V-FzCjxKPzAROoN0OKL9YxjRXHkwQjk"/>
</div>
</div>
</div>
</section>
<footer class="bg-primary dark:bg-black text-stone-400 py-24 px-6 md:px-12">
<div class="max-w-[1920px] mx-auto">
<div class="grid grid-cols-1 md:grid-cols-12 gap-12 border-b border-stone-700 pb-20 mb-12">
<div class="md:col-span-4">
<a class="font-display text-4xl text-white block mb-8" href="#">IKKAI</a>
<p class="font-body text-sm font-light leading-loose max-w-sm mb-8">
Handcrafted ceramics for the modern home. Created with intention, fired with patience, and delivered with care.
</p>
<form class="flex border-b border-stone-600 pb-2 max-w-xs">
<input class="bg-transparent border-none text-white placeholder-stone-500 focus:ring-0 text-sm w-full p-0" placeholder="Join our newsletter" type="email"/>
<button class="text-xs uppercase tracking-widest text-white hover:text-stone-300" type="submit">Subscribe</button>
</form>
</div>
<div class="md:col-span-2 md:col-start-7">
<h4 class="text-white text-xs font-bold uppercase tracking-widest mb-8">Shop</h4>
<ul class="space-y-4 text-xs font-light tracking-widest uppercase">
<li><a class="hover:text-white transition-colors" href="#">All Ceramics</a></li>
<li><a class="hover:text-white transition-colors" href="#">New Arrivals</a></li>
<li><a class="hover:text-white transition-colors" href="#">Best Sellers</a></li>
<li><a class="hover:text-white transition-colors" href="#">Gift Cards</a></li>
</ul>
</div>
<div class="md:col-span-2">
<h4 class="text-white text-xs font-bold uppercase tracking-widest mb-8">Company</h4>
<ul class="space-y-4 text-xs font-light tracking-widest uppercase">
<li><a class="hover:text-white transition-colors" href="#">Our Story</a></li>
<li><a class="hover:text-white transition-colors" href="#">Sustainability</a></li>
<li><a class="hover:text-white transition-colors" href="#">Careers</a></li>
<li><a class="hover:text-white transition-colors" href="#">Press</a></li>
</ul>
</div>
<div class="md:col-span-2">
<h4 class="text-white text-xs font-bold uppercase tracking-widest mb-8">Support</h4>
<ul class="space-y-4 text-xs font-light tracking-widest uppercase">
<li><a class="hover:text-white transition-colors" href="#">FAQ</a></li>
<li><a class="hover:text-white transition-colors" href="#">Shipping</a></li>
<li><a class="hover:text-white transition-colors" href="#">Returns</a></li>
<li><a class="hover:text-white transition-colors" href="#">Contact</a></li>
</ul>
</div>
</div>
<div class="flex flex-col md:flex-row justify-between items-center text-[10px] tracking-widest uppercase">
<p>© 2024 IKKAI Ceramics. All rights reserved.</p>
<div class="flex space-x-6 mt-4 md:mt-0">
<a class="hover:text-white" href="#">Privacy</a>
<a class="hover:text-white" href="#">Terms</a>
<a class="hover:text-white" href="#">Cookies</a>
</div>
</div>
</div>
</footer>
</body></html>

BIN
product-scroll-poc/cup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

Binary file not shown.

View File

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HOTCHPOTSH — The Art of the Spin</title>
<link rel="stylesheet" href="style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,200,0,0" />
</head>
<body>
<header class="prototype-header">
<div class="logo">HOTCHPOTSH</div>
<div class="nav-minimal">Prototype 02 / Product Spin</div>
</header>
<div class="scroll-container">
<!-- Intro Section -->
<section class="intro-section">
<div class="intro-content">
<span class="label">Excellence in Form</span>
<h1>Tracing the Hand of the Maker</h1>
<p>A study of rotation, material, and light. Scroll to explore the silhouette of our signature Stoneware Mug.</p>
<div class="scroll-indicator">
<span class="material-symbols-outlined animate-bounce">expand_more</span>
</div>
</div>
</section>
<!-- Sticky Product Wrapper -->
<div class="product-stage">
<div class="cup-wrapper">
<!-- Canvas for smooth image sequence -->
<canvas id="cup-canvas"></canvas>
<!-- Hidden video for source buffering -->
<video id="cup-video" src="cup_spin.mp4" playsinline muted preload="auto" style="display: none;"></video>
<div class="stage-overlay"></div>
<!-- Buffering Status -->
<div id="buffer-status" class="buffer-info">
<span class="loader-dots"></span>
<span class="status-text">Refining silhouette...</span>
</div>
</div>
</div>
<!-- Scrolling Text Sections -->
<div class="content-section" data-step="1">
<div class="text-block">
<span class="step-num">01</span>
<h2>The Form</h2>
<p>Hand-thrown on the wheel, embracing imperfect symmetry. Every curve is a witness to the tactile dialogue between hands and clay.</p>
</div>
</div>
<div class="content-section" data-step="2">
<div class="text-block">
<span class="step-num">02</span>
<h2>The Clay</h2>
<p>Locally sourced stoneware, rich in texture and history. We use a high-iron body that produces subtle speckling during the long firing process.</p>
</div>
</div>
<div class="content-section" data-step="3">
<div class="text-block">
<span class="step-num">03</span>
<h2>The Glaze</h2>
<p>Fired at 1200°C to achieve a unique, stone-like finish. Our satin-matte glaze is applied by dipping, creating natural variations in depth.</p>
</div>
</div>
<div class="content-section" data-step="4">
<div class="text-block">
<span class="step-num">04</span>
<h2>Daily Ritual</h2>
<p>Designed to be held, used, and cherished every morning. A vessel not just for coffee, but for a moment of quiet reflection.</p>
</div>
</div>
<!-- Outro Section -->
<section class="outro-section">
<div class="outro-content">
<h2>Limited Release</h2>
<p>The Studio Collection / Batch 014</p>
<button class="btn-minimal">Shop Collection</button>
</div>
</section>
</div>
<script src="script.js"></script>
</body>
</html>

View File

@ -0,0 +1,24 @@
from PIL import Image
import numpy as np
def remove_white_background(input_path, output_path, threshold=240):
img = Image.open(input_path).convert("RGBA")
data = np.array(img)
# RGB values
r, g, b, a = data.T
# Define white areas (pixels > threshold)
white_areas = (r > threshold) & (g > threshold) & (b > threshold)
# Set alpha to 0 for white areas
data[..., 3][white_areas.T] = 0
# Create new image
new_img = Image.fromarray(data)
new_img.save(output_path)
print(f"Saved transparent image to {output_path}")
if __name__ == "__main__":
# Use relative paths so it works in WSL/Linux too
remove_white_background("cup.png", "cup_transparent.png")

View File

@ -0,0 +1,137 @@
gsap.registerPlugin(ScrollTrigger);
const canvas = document.querySelector("#cup-canvas");
const context = canvas.getContext("2d");
const video = document.querySelector("#cup-video");
const status = document.querySelector("#buffer-status");
const frames = [];
let isBuffered = false;
// 1. Canvas Setup
function resizeCanvas() {
if (video.videoWidth) {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
} else {
canvas.width = 1000;
canvas.height = 1000;
}
}
// 2. Buffering Logic (Capturing frames into memory)
video.addEventListener("loadedmetadata", () => {
resizeCanvas();
startBuffering();
});
async function startBuffering() {
video.currentTime = 0;
video.playbackRate = 1; // Standard speed for better capture quality
await video.play();
function capture() {
if (video.paused || video.ended) {
finishBuffering();
return;
}
// Draw video frame to worker canvas and store as ImageBitmap
createImageBitmap(video).then(bitmap => {
frames.push(bitmap);
// Draw first frame immediately
if (frames.length === 1) renderFrame(0);
requestAnimationFrame(capture);
});
}
requestAnimationFrame(capture);
}
function finishBuffering() {
isBuffered = true;
status.style.opacity = "0";
video.pause();
initScrollAnimation();
}
function renderFrame(index) {
if (!frames[index]) return;
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(frames[index], 0, 0, canvas.width, canvas.height);
}
// 3. Optimized Scroll Animation
function initScrollAnimation() {
const totalFrames = frames.length - 1;
const scrollObj = { frame: 0 };
// GSAP Timeline for the whole experience
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".scroll-container",
start: "top top",
end: "bottom bottom",
scrub: 0.5, // Slight lag-behind for smoothness
}
});
// Frame scrubbing
tl.to(scrollObj, {
frame: totalFrames,
ease: "none",
onUpdate: () => renderFrame(Math.floor(scrollObj.frame))
}, 0);
// Intro Fade
gsap.to(".intro-content", {
opacity: 0,
y: -50,
scrollTrigger: {
trigger: ".intro-section",
start: "top top",
end: "bottom center",
scrub: true
}
});
// Sub-animations for depth
tl.to(canvas, {
scale: 0.95,
rotateY: 5,
ease: "sine.inOut"
}, 0);
// Content text reveal
const sections = document.querySelectorAll(".content-section");
sections.forEach((section) => {
const text = section.querySelector(".text-block");
gsap.fromTo(text,
{ opacity: 0, y: 40 },
{
opacity: 1,
y: 0,
duration: 1,
scrollTrigger: {
trigger: section,
start: "top 70%",
end: "top 30%",
toggleActions: "play reverse play reverse",
}
}
);
});
// Exit
gsap.to(".product-stage", {
opacity: 0,
scrollTrigger: {
trigger: ".outro-section",
start: "top center",
end: "bottom bottom",
scrub: true
}
});
}

View File

@ -0,0 +1,334 @@
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@300&display=swap');
:root {
--bg-color: #f8f6f3;
--text-color: #1c1917;
--accent-color: #44403c;
--muted-color: #a8a29e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Inter', sans-serif;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
/* Optional: Grain Texture Overlay for that premium tactile feel */
body::after {
content: "";
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url("https://grainy-gradients.vercel.app/noise.svg");
opacity: 0.04;
pointer-events: none;
z-index: 999;
}
/* Header */
.prototype-header {
position: fixed;
top: 0;
width: 100%;
padding: 3rem 4rem;
/* More air */
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.3em;
}
.logo {
font-weight: 500;
border-bottom: 1px solid var(--text-color);
padding-bottom: 0.5rem;
}
/* Scroll Container */
.scroll-container {
position: relative;
width: 100%;
}
/* Sections */
section {
position: relative;
height: 120vh;
/* More scroll space */
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 0 4rem;
z-index: 10;
}
/* Intro */
.intro-content {
text-align: center;
max-width: 900px;
}
.intro-content .label {
display: block;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.6em;
color: var(--muted-color);
margin-bottom: 3rem;
}
.intro-content h1 {
font-family: 'Playfair Display', serif;
font-size: 6rem;
font-weight: 400;
margin-bottom: 3rem;
line-height: 1.05;
letter-spacing: -0.02em;
}
.intro-content p {
font-size: 1.1rem;
color: var(--accent-color);
line-height: 1.8;
max-width: 500px;
margin: 0 auto 4rem;
font-weight: 300;
}
.scroll-indicator {
display: flex;
justify-content: center;
}
/* Sticky Product Stage */
.product-stage {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
z-index: 1;
pointer-events: none;
perspective: 1500px;
}
.cup-wrapper {
width: 600px;
height: 600px;
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
#cup-canvas {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 40px 80px rgba(0, 0, 0, 0.08));
will-change: transform;
}
.buffer-info {
position: absolute;
bottom: -40px;
left: 50%;
transform: translateX(-50%);
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
color: var(--muted-color);
text-transform: uppercase;
letter-spacing: 0.2em;
display: flex;
align-items: center;
gap: 10px;
transition: opacity 0.5s ease;
}
.loader-dots::after {
content: " .";
animation: dots 1.5s steps(5, end) infinite;
}
@keyframes dots {
20% {
content: " .";
}
40% {
content: " ..";
}
60% {
content: " ...";
}
80% {
content: " ....";
}
}
.cup-video {
display: none;
}
.stage-overlay {
position: absolute;
inset: 0;
background: radial-gradient(circle at center, transparent 30%, var(--bg-color) 70%);
opacity: 0.4;
}
/* Content Sections */
.content-section {
position: relative;
height: 150vh;
/* Slow down the scroll feel */
display: flex;
align-items: center;
padding: 0 12%;
z-index: 10;
}
.content-section:nth-child(odd) {
justify-content: flex-end;
}
.content-section:nth-child(even) {
justify-content: flex-start;
}
.text-block {
max-width: 450px;
opacity: 1;
/* Handled by GSAP now */
}
.step-num {
display: block;
font-family: 'JetBrains Mono', monospace;
font-size: 0.8rem;
color: var(--muted-color);
margin-bottom: 2rem;
position: relative;
}
.step-num::after {
content: "";
position: absolute;
left: 40px;
top: 50%;
width: 60px;
height: 1px;
background: var(--muted-color);
opacity: 0.3;
}
.text-block h2 {
font-family: 'Playfair Display', serif;
font-size: 4.5rem;
font-weight: 400;
margin-bottom: 2.5rem;
line-height: 1.1;
}
.text-block p {
font-size: 1.05rem;
line-height: 2;
color: var(--accent-color);
font-weight: 300;
max-width: 320px;
}
/* Outro */
.outro-content {
text-align: center;
}
.outro-content h2 {
font-family: 'Playfair Display', serif;
font-size: 4rem;
font-weight: 400;
margin-bottom: 1rem;
}
.btn-minimal {
margin-top: 3rem;
padding: 1rem 3rem;
border: 1px solid var(--text-color);
background: transparent;
font-family: 'JetBrains Mono', monospace;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.2em;
cursor: pointer;
transition: all 0.4s ease;
}
.btn-minimal:hover {
background: var(--text-color);
color: white;
}
/* Animations */
.animate-bounce {
animation: bounce 2s infinite;
}
@keyframes bounce {
0%,
20%,
50%,
80%,
100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
/* Mobile Responsive */
@media (max-width: 768px) {
.intro-content h1 {
font-size: 3rem;
}
.cup-wrapper {
width: 400px;
height: 400px;
}
.text-block h2 {
font-size: 2.5rem;
}
.prototype-header {
padding: 1.5rem;
}
}

BIN
screen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB