Initial commit of project structure
24
Pottery-website/.gitignore
vendored
Normal 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
@@ -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
@@ -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`
|
||||
118
Pottery-website/components/Collections.tsx
Normal 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;
|
||||
89
Pottery-website/components/FeatureSection.tsx
Normal 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;
|
||||
93
Pottery-website/components/Footer.tsx
Normal 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;
|
||||
179
Pottery-website/components/GallerySection.tsx
Normal 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;
|
||||
109
Pottery-website/components/Header.tsx
Normal 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;
|
||||
40
Pottery-website/components/Hero.tsx
Normal 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;
|
||||
84
Pottery-website/components/HorizontalScrollSection.tsx
Normal 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;
|
||||
50
Pottery-website/components/JournalSection.tsx
Normal 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;
|
||||
61
Pottery-website/components/PageLoader.tsx
Normal 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;
|
||||
19
Pottery-website/components/QuoteSection.tsx
Normal 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;
|
||||
40
Pottery-website/components/RouteTransition.tsx
Normal 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;
|
||||
12
Pottery-website/components/ScrollToTop.tsx
Normal 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;
|
||||
}
|
||||
121
Pottery-website/constants.ts
Normal 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: '#' },
|
||||
],
|
||||
},
|
||||
];
|
||||
103
Pottery-website/hooks/useScrollAnimations.ts
Normal 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]);
|
||||
};
|
||||
88
Pottery-website/index.html
Normal 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&family=Manrope:wght@200;300;400;500&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&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
@@ -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>
|
||||
);
|
||||
5
Pottery-website/metadata.json
Normal 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
24
Pottery-website/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
73
Pottery-website/pages/Atelier.tsx
Normal 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;
|
||||
68
Pottery-website/pages/Collections.tsx
Normal 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;
|
||||
97
Pottery-website/pages/Editorial.tsx
Normal 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;
|
||||
24
Pottery-website/pages/Home.tsx
Normal 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;
|
||||
BIN
Pottery-website/public/ceramic-cups.png
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
Pottery-website/public/collection-kitchenware.png
Normal file
|
After Width: | Height: | Size: 566 KiB |
BIN
Pottery-website/public/collection-lighting.png
Normal file
|
After Width: | Height: | Size: 794 KiB |
BIN
Pottery-website/public/collection-tableware.png
Normal file
|
After Width: | Height: | Size: 808 KiB |
BIN
Pottery-website/public/collection-vases.png
Normal file
|
After Width: | Height: | Size: 631 KiB |
BIN
Pottery-website/public/pottery-bowls.png
Normal file
|
After Width: | Height: | Size: 695 KiB |
BIN
Pottery-website/public/pottery-plates.png
Normal file
|
After Width: | Height: | Size: 696 KiB |
BIN
Pottery-website/public/pottery-studio.png
Normal file
|
After Width: | Height: | Size: 879 KiB |
BIN
Pottery-website/public/pottery-vase.png
Normal file
|
After Width: | Height: | Size: 701 KiB |
29
Pottery-website/tsconfig.json
Normal 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
@@ -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[];
|
||||
}
|
||||
23
Pottery-website/vite.config.ts
Normal 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, '.'),
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||