Initial commit of project structure
This commit is contained in:
118
Pottery-website/components/Collections.tsx
Normal file
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
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
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
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
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
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
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
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
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
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
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
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;
|
||||
}
|
||||
Reference in New Issue
Block a user