Update : added scroll candle animation nd fixed header css

This commit is contained in:
hardik 2026-05-29 11:31:02 +05:30
parent 6e3408ef7c
commit b4b163634f
17 changed files with 2579 additions and 870 deletions

View File

@ -1,6 +1,4 @@
import { Switch, Route, Router as WouterRouter } from "wouter";
import { Toaster } from "@/components/ui/toaster";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/not-found";
import { Layout } from "@/components/Layout";
@ -23,14 +21,11 @@ function Router() {
function App() {
return (
<TooltipProvider>
<WouterRouter base={import.meta.env.BASE_URL.replace(/\/$/, "")}>
<Layout>
<Router />
</Layout>
</WouterRouter>
<Toaster />
</TooltipProvider>
);
}

366
src/components/Layout.css Normal file
View File

@ -0,0 +1,366 @@
/* ─── Layout.css — Header & Footer ──────────────────────── */
/*
MARQUEE / TICKER BAR
*/
.ticker-bar {
background: var(--brand-purple-dark);
border-bottom: 1px solid rgba(212,162,76,0.25);
height: 36px;
overflow: hidden;
display: flex;
align-items: center;
}
.ticker-track {
display: flex;
align-items: center;
gap: 0;
animation: ticker-scroll 28s linear infinite;
white-space: nowrap;
will-change: transform;
}
.ticker-track:hover {
animation-play-state: paused;
}
@keyframes ticker-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.ticker-item {
display: inline-flex;
align-items: center;
gap: 0.6rem;
font-family: var(--font-display);
font-size: 0.62rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255,255,255,0.85);
padding: 0 2.5rem;
}
.ticker-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--brand-gold);
flex-shrink: 0;
display: inline-block;
}
/*
LOGO
*/
.logo-text {
font-family: var(--font-script);
font-size: 2rem;
color: #ffffff;
transition: color 0.3s ease;
line-height: 1;
}
.logo-text:hover { color: var(--brand-gold-light); }
.logo-img-wrap {
width: 44px;
height: 44px;
border-radius: 50%;
overflow: hidden;
border: 2px solid transparent;
transition: border-color 0.3s ease;
}
.logo-img-wrap:hover { border-color: var(--brand-gold); }
/*
NAV ITEM WITH DROPDOWN
*/
.nav-item {
position: relative;
}
.nav-link {
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #ffffff;
text-decoration: none;
padding-bottom: 2px;
border-bottom: 2px solid transparent;
transition: border-color 0.3s ease, color 0.3s ease;
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
background: none;
border-left: none;
border-right: none;
border-top: none;
}
.nav-link:hover {
color: var(--brand-gold-light);
border-bottom-color: rgba(212,162,76,0.6);
}
.nav-link.active {
color: var(--brand-gold-light);
border-bottom-color: var(--brand-gold);
border-bottom-width: 2.5px;
text-shadow:
0 0 12px rgba(255,211,107,0.9),
0 0 24px rgba(212,162,76,0.5);
font-weight: 600;
background: rgba(212,162,76,0.12);
border-radius: 4px;
padding: 3px 10px 5px;
}
.nav-chevron {
transition: transform 0.25s ease;
opacity: 0.75;
}
.nav-item:hover .nav-chevron {
transform: rotate(180deg);
opacity: 1;
}
/* DROPDOWN PANEL */
.nav-dropdown {
position: absolute;
top: calc(100% + 18px);
left: 50%;
transform: translateX(-50%) translateY(-8px);
min-width: 200px;
background: rgba(45, 13, 82, 0.96);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(212,162,76,0.2);
border-radius: 8px;
padding: 0.6rem 0;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.25s ease;
box-shadow: 0 16px 40px rgba(0,0,0,0.35);
z-index: 200;
}
/* Arrow notch */
.nav-dropdown::before {
content: '';
position: absolute;
top: -7px;
left: 50%;
transform: translateX(-50%);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid rgba(212,162,76,0.25);
}
.nav-dropdown::after {
content: '';
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid rgba(45,13,82,0.96);
}
.nav-item:hover .nav-dropdown {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
.dropdown-item {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.65rem 1.25rem;
font-family: var(--font-sans);
font-size: 0.83rem;
color: rgba(255,255,255,0.78);
text-decoration: none;
transition: background 0.2s ease, color 0.2s ease, padding-left 0.2s ease;
white-space: nowrap;
}
.dropdown-item:hover {
background: rgba(212,162,76,0.12);
color: var(--brand-gold-light);
padding-left: 1.6rem;
}
.dropdown-divider {
height: 1px;
background: rgba(255,255,255,0.08);
margin: 0.4rem 0;
}
/*
MOBILE TOGGLE
*/
.mobile-toggle {
background: none;
border: none;
color: #ffffff;
cursor: pointer;
display: flex;
align-items: center;
padding: 4px;
transition: color 0.3s ease;
}
.mobile-toggle:hover { color: var(--brand-gold-light); }
/*
MOBILE DRAWER
*/
.mobile-menu {
background: rgba(45, 13, 82, 0.97);
backdrop-filter: blur(16px);
border-bottom: 1px solid rgba(212, 162, 76, 0.2);
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.mobile-nav-section {
display: flex;
flex-direction: column;
}
.mobile-nav-link {
font-family: var(--font-display);
font-size: 0.85rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: #ffffff;
text-decoration: none;
padding: 0.8rem 0.5rem;
border-bottom: 1px solid rgba(255,255,255,0.07);
transition: color 0.3s ease, padding-left 0.2s ease;
display: block;
}
.mobile-nav-link:hover {
color: var(--brand-gold-light);
padding-left: 0.75rem;
}
.mobile-nav-link.active {
color: var(--brand-gold);
padding-left: 0.75rem;
text-shadow: 0 0 10px rgba(255,211,107,0.7);
font-weight: 600;
}
.mobile-dropdown-items {
display: flex;
flex-direction: column;
padding: 0.25rem 0 0.5rem 1rem;
gap: 0.1rem;
border-bottom: 1px solid rgba(255,255,255,0.07);
}
.mobile-dropdown-item {
font-family: var(--font-sans);
font-size: 0.82rem;
color: rgba(255,255,255,0.6);
text-decoration: none;
padding: 0.4rem 0.5rem;
transition: color 0.2s ease;
}
.mobile-dropdown-item:hover {
color: var(--brand-gold-light);
}
/*
FOOTER
*/
.site-footer {
background: var(--brand-purple-dark);
color: #ffffff;
padding: 4rem 1.5rem;
margin-top: 5rem;
}
.footer-inner {
max-width: 1280px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
text-align: center;
}
@media (min-width: 768px) {
.footer-inner {
grid-template-columns: 1fr 1fr 1fr;
text-align: left;
}
}
.footer-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
@media (min-width: 768px) {
.footer-col { align-items: flex-start; }
}
.footer-tagline {
font-family: var(--font-serif);
font-style: italic;
font-size: 1.1rem;
color: rgba(255,255,255,0.7);
}
.footer-heading {
font-family: var(--font-display);
font-size: 0.7rem;
letter-spacing: 0.25em;
text-transform: uppercase;
color: var(--brand-gold);
margin-bottom: 0.5rem;
}
.footer-link {
color: rgba(255,255,255,0.75);
font-size: 0.9rem;
text-decoration: none;
transition: color 0.3s ease;
}
.footer-link:hover { color: var(--brand-gold-light); }
.social-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255,255,255,0.08);
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
text-decoration: none;
transition: background 0.3s ease, color 0.3s ease;
}
.social-btn:hover {
background: var(--brand-gold);
color: var(--brand-purple-dark);
}
.footer-copy {
font-size: 0.78rem;
color: rgba(255,255,255,0.4);
margin-top: 1rem;
}

View File

@ -1,19 +1,71 @@
import { Link, useLocation } from "wouter";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X, Instagram, Facebook, Twitter } from "lucide-react";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import IconButton from "@mui/material/IconButton";
import { Menu, X, Instagram, Facebook, Twitter, ChevronDown, Flame, Sparkles, Star } from "lucide-react";
import { ScrollCandle } from "./ScrollCandle";
import "./Layout.css";
const logoPath = "/opengraph.jpg";
// ── Ticker messages ──────────────────────────────────────
const TICKER_ITEMS = [
{ icon: <Flame size={12} />, text: "Free shipping on orders over $50" },
{ icon: <Sparkles size={12} />, text: "New Collection — Summer Solstice" },
{ icon: <Star size={12} />, text: "Handpoured in small batches" },
{ icon: <Flame size={12} />, text: "100% Natural Soy Wax" },
{ icon: <Sparkles size={12} />, text: "Phthalate-free fragrance oils" },
{ icon: <Star size={12} />, text: "Gift wrapping available at checkout" },
];
// ── Nav structure with dropdowns ─────────────────────────
const NAV_LINKS = [
{ name: "Home", path: "/", dropdown: null },
{
name: "Products",
path: "/products",
dropdown: [
{ label: "All Collections", path: "/products" },
null, // divider
{ label: "Floral Scents", path: "/products" },
{ label: "Fresh Scents", path: "/products" },
{ label: "Woody Scents", path: "/products" },
{ label: "Sweet Scents", path: "/products" },
],
},
{
name: "About",
path: "/about",
dropdown: [
{ label: "Our Story", path: "/about" },
{ label: "Our Process", path: "/about" },
{ label: "Meet the Maker", path: "/about" },
],
},
{
name: "Contact",
path: "/contact",
dropdown: [
{ label: "Send a Message", path: "/contact" },
{ label: "Wholesale Inquiries", path: "/contact" },
null,
{ label: "Follow on Instagram", path: "/contact" },
],
},
];
export function Layout({ children }: { children: React.ReactNode }) {
const [location] = useLocation();
const [scrolled, setScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [mobileOpenSection, setMobileOpenSection] = useState<string | null>(null);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 20);
};
const handleScroll = () => setScrolled(window.scrollY > 20);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
@ -21,83 +73,181 @@ export function Layout({ children }: { children: React.ReactNode }) {
useEffect(() => {
window.scrollTo(0, 0);
setMobileMenuOpen(false);
setMobileOpenSection(null);
}, [location]);
const navLinks = [
{ name: "Home", path: "/" },
{ name: "Products", path: "/products" },
{ name: "About", path: "/about" },
{ name: "Contact", path: "/contact" },
];
// Duplicate ticker items to create seamless loop
const allTickerItems = [...TICKER_ITEMS, ...TICKER_ITEMS];
return (
<div className="min-h-screen flex flex-col font-sans bg-background text-foreground">
<header
className={`fixed top-0 w-full z-50 transition-all duration-500 ease-in-out ${
scrolled
? "bg-background/80 backdrop-blur-md shadow-[0_1px_0_0_hsl(var(--accent))]"
: "bg-transparent"
}`}
<Box sx={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
{/* ── Ticker Bar ── */}
<Box
className="ticker-bar"
sx={{ position: "fixed", top: 0, left: 0, right: 0, zIndex: 1200 }}
>
<div className="max-w-7xl mx-auto px-6 h-24 flex items-center justify-between">
<Link href="/" className="flex items-center gap-4 group">
<div className="w-12 h-12 rounded-full overflow-hidden border-2 border-transparent group-hover:border-accent transition-colors duration-300">
<img src={logoPath} alt="Mani Candles Co. Logo" className="w-full h-full object-cover" />
</div>
<span className="font-script text-3xl text-primary drop-shadow-sm group-hover:text-accent transition-colors duration-300">
Mani Candles
<div className="ticker-track">
{allTickerItems.map((item, i) => (
<span className="ticker-item" key={i}>
<span className="ticker-dot" />
{item.icon}
{item.text}
</span>
))}
</div>
</Box>
{/* ── Main AppBar ── */}
<AppBar
position="fixed"
sx={{
top: 36, // sits below the ticker bar
background: scrolled
? "rgba(91,30,140,0.78)"
: "rgba(91,30,140,0.58)",
backdropFilter: "blur(22px) saturate(1.6)",
WebkitBackdropFilter: "blur(22px) saturate(1.6)",
boxShadow: scrolled ? "0 1px 0 0 rgba(212,162,76,0.5), 0 4px 24px rgba(58,18,96,0.35)" : "none",
transition: "background 0.5s ease, box-shadow 0.5s ease",
overflow: "visible",
}}
>
<Container maxWidth="xl">
<Toolbar
disableGutters
sx={{ height: 76, justifyContent: "space-between", overflow: "visible" }}
>
{/* Logo */}
<Link
href="/"
style={{ display: "flex", alignItems: "center", gap: "0.9rem", textDecoration: "none" }}
>
<div className="logo-img-wrap">
<img
src={logoPath}
alt="Mani Candles Co. Logo"
style={{ width: "100%", height: "100%", objectFit: "cover" }}
/>
</div>
<span className="logo-text">Mani Candles</span>
</Link>
<nav className="hidden md:flex items-center gap-8">
{navLinks.map((link) => (
{/* Desktop Nav */}
<Box
component="nav"
sx={{
display: { xs: "none", md: "flex" },
gap: 4,
alignItems: "center",
overflow: "visible",
position: "relative",
}}
>
{NAV_LINKS.map((link) => (
<div className="nav-item" key={link.path + link.name}>
<Link
key={link.path}
href={link.path}
className={`font-display tracking-widest text-sm uppercase transition-colors duration-300 hover:text-accent ${
location === link.path ? "text-accent" : "text-primary"
}`}
className={`nav-link${location === link.path ? " active" : ""}`}
>
{link.name}
{link.dropdown && <ChevronDown size={13} className="nav-chevron" />}
</Link>
))}
</nav>
<button
className="md:hidden text-primary hover:text-accent transition-colors"
{/* Dropdown */}
{link.dropdown && (
<div className="nav-dropdown">
{link.dropdown.map((item, idx) =>
item === null ? (
<div key={`div-${idx}`} className="dropdown-divider" />
) : (
<Link key={item.label} href={item.path} className="dropdown-item">
{item.label}
</Link>
)
)}
</div>
)}
</div>
))}
</Box>
{/* Mobile Toggle */}
<IconButton
sx={{ display: { md: "none" }, color: "#ffffff" }}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? <X size={28} /> : <Menu size={28} />}
</button>
</div>
{mobileMenuOpen ? <X size={26} /> : <Menu size={26} />}
</IconButton>
</Toolbar>
</Container>
{/* Mobile Drawer */}
<AnimatePresence>
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="md:hidden bg-background/95 backdrop-blur-md border-b border-accent/20 overflow-hidden"
className="mobile-menu"
>
<div className="px-6 py-8 flex flex-col gap-6">
{navLinks.map((link) => (
<Link
key={link.path}
href={link.path}
className={`font-display tracking-widest text-lg uppercase transition-colors duration-300 ${
location === link.path ? "text-accent" : "text-primary"
}`}
{NAV_LINKS.map((link) => (
<div className="mobile-nav-section" key={link.name}>
{link.dropdown ? (
<>
<button
className={`mobile-nav-link${location === link.path ? " active" : ""}`}
style={{ background: "none", border: "none", textAlign: "left", cursor: "pointer", width: "100%", display: "flex", justifyContent: "space-between", alignItems: "center" }}
onClick={() => setMobileOpenSection(mobileOpenSection === link.name ? null : link.name)}
>
{link.name}
<ChevronDown
size={14}
style={{
transition: "transform 0.25s ease",
transform: mobileOpenSection === link.name ? "rotate(180deg)" : "rotate(0deg)",
opacity: 0.7,
}}
/>
</button>
<AnimatePresence>
{mobileOpenSection === link.name && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
className="mobile-dropdown-items"
>
{link.dropdown.map((item, i) =>
item === null ? (
<div key={i} style={{ height: 1, background: "rgba(255,255,255,0.07)", margin: "0.2rem 0" }} />
) : (
<Link key={item.label} href={item.path} className="mobile-dropdown-item">
{item.label}
</Link>
))}
</div>
)
)}
</motion.div>
)}
</AnimatePresence>
</header>
</>
) : (
<Link
href={link.path}
className={`mobile-nav-link${location === link.path ? " active" : ""}`}
>
{link.name}
</Link>
)}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</AppBar>
<main className="flex-grow pt-24">
{/* ── Page Content — offset for ticker + appbar ── */}
<Box component="main" sx={{ flexGrow: 1, pt: "112px" }}>
<AnimatePresence mode="wait">
<motion.div
key={location}
@ -109,52 +259,52 @@ export function Layout({ children }: { children: React.ReactNode }) {
{children}
</motion.div>
</AnimatePresence>
</main>
</Box>
<footer className="bg-primary text-primary-foreground py-16 mt-20">
<div className="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-3 gap-12 text-center md:text-left">
<div className="flex flex-col items-center md:items-start gap-6">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-full overflow-hidden bg-white p-1">
<img src={logoPath} alt="Mani Candles Co. Logo" className="w-full h-full object-cover rounded-full" />
{/* ── Scroll Candle ── */}
<ScrollCandle />
{/* ── Footer ── */}
<footer className="site-footer">
<div className="footer-inner">
<div className="footer-col">
<div style={{ display: "flex", alignItems: "center", gap: "1rem" }}>
<div style={{ width: 56, height: 56, borderRadius: "50%", overflow: "hidden", background: "#fff", padding: 3 }}>
<img
src={logoPath}
alt="Mani Candles Co."
style={{ width: "100%", height: "100%", objectFit: "cover", borderRadius: "50%" }}
/>
</div>
<span className="font-script text-4xl text-secondary">Mani Candles</span>
<span className="font-script" style={{ fontSize: "2rem", color: "var(--brand-gold-light)" }}>
Mani Candles
</span>
</div>
<p className="font-serif italic text-xl text-secondary/80">Handmade Natural Heartmade</p>
<p className="footer-tagline">Handmade Natural Heartmade</p>
</div>
<div className="flex flex-col items-center md:items-start gap-4">
<h4 className="font-display tracking-widest text-accent uppercase mb-2">Explore</h4>
{navLinks.map((link) => (
<Link
key={link.path}
href={link.path}
className="hover:text-accent transition-colors font-sans font-light"
>
<div className="footer-col">
<h4 className="footer-heading">Explore</h4>
{NAV_LINKS.map((link) => (
<Link key={link.path + link.name} href={link.path} className="footer-link">
{link.name}
</Link>
))}
</div>
<div className="flex flex-col items-center md:items-start gap-6">
<h4 className="font-display tracking-widest text-accent uppercase">Connect</h4>
<div className="flex gap-4">
<a href="#" className="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center hover:bg-accent hover:text-primary transition-all duration-300">
<Instagram size={20} />
</a>
<a href="#" className="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center hover:bg-accent hover:text-primary transition-all duration-300">
<Facebook size={20} />
</a>
<a href="#" className="w-10 h-10 rounded-full bg-secondary/10 flex items-center justify-center hover:bg-accent hover:text-primary transition-all duration-300">
<Twitter size={20} />
</a>
<div className="footer-col">
<h4 className="footer-heading">Connect</h4>
<div style={{ display: "flex", gap: "0.75rem" }}>
<a href="#" className="social-btn"><Instagram size={18} /></a>
<a href="#" className="social-btn"><Facebook size={18} /></a>
<a href="#" className="social-btn"><Twitter size={18} /></a>
</div>
<p className="font-sans font-light text-sm text-secondary/60 mt-4">
<p className="footer-copy">
© {new Date().getFullYear()} Mani Candles Co. All rights reserved.
</p>
</div>
</div>
</footer>
</div>
</Box>
);
}

View File

@ -0,0 +1,291 @@
/* ─── ScrollCandle.css ───────────────────────────────────── */
.scroll-candle-wrap {
position: fixed;
right: 28px;
bottom: 0px;
z-index: 999;
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
filter: drop-shadow(0 0 18px rgba(255, 160, 40, 0.35));
}
/* ── FLAME SYSTEM ─────────────────────────────────── */
.candle-flame-wrap {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: -2px;
}
/* Outer glow halo */
.flame-glow {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48px;
height: 48px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255, 200, 50, 0.45) 0%, transparent 70%);
animation: glow-pulse 1.8s ease-in-out infinite alternate;
z-index: 0;
}
@keyframes glow-pulse {
0% {
transform: translateX(-50%) scale(1);
opacity: 0.7;
}
100% {
transform: translateX(-50%) scale(1.35);
opacity: 1;
}
}
/* Flame outer — large orange teardrop */
.flame-outer {
position: relative;
z-index: 2;
width: 18px;
height: 34px;
background: linear-gradient(to top, #ff6a00, #ffaa00, #ffe066);
border-radius: 50% 50% 30% 30% / 60% 60% 40% 40%;
transform-origin: bottom center;
animation: flicker-outer 0.9s ease-in-out infinite alternate;
box-shadow:
0 0 8px 3px rgba(255, 140, 0, 0.6),
0 0 18px 6px rgba(255, 100, 0, 0.4);
}
@keyframes flicker-outer {
0% {
transform: scaleX(1) scaleY(1) rotate(-2deg);
}
25% {
transform: scaleX(0.88) scaleY(1.05) rotate(2deg);
}
50% {
transform: scaleX(1.05) scaleY(0.95) rotate(-1deg);
}
75% {
transform: scaleX(0.92) scaleY(1.08) rotate(3deg);
}
100% {
transform: scaleX(1.02) scaleY(1.02) rotate(-2deg);
}
}
/* Flame inner — bright white-yellow core */
.flame-inner {
position: absolute;
bottom: 4px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
width: 8px;
height: 18px;
background: linear-gradient(to top, #fff8c5, #fff, rgba(255, 255, 255, 0));
border-radius: 50% 50% 30% 30% / 60% 60% 40% 40%;
animation: flicker-inner 0.7s ease-in-out infinite alternate;
opacity: 0.9;
}
@keyframes flicker-inner {
0% {
transform: translateX(-50%) scaleX(1) scaleY(1);
}
50% {
transform: translateX(-50%) scaleX(0.85) scaleY(1.1);
}
100% {
transform: translateX(-50%) scaleX(1.1) scaleY(0.9);
}
}
/* ── SMOKE PARTICLES ─────────────────────────────── */
.smoke-wrap {
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 20px;
pointer-events: none;
z-index: 4;
}
.smoke-particle {
position: absolute;
bottom: 0;
left: 50%;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(200, 180, 220, 0.45);
transform: translateX(-50%);
animation: smoke-rise 2.4s ease-out infinite;
opacity: 0;
}
.smoke-particle:nth-child(2) {
animation-delay: 0.8s;
left: 40%;
}
.smoke-particle:nth-child(3) {
animation-delay: 1.6s;
left: 60%;
}
@keyframes smoke-rise {
0% {
transform: translateX(-50%) translateY(0) scale(0.6);
opacity: 0;
}
20% {
opacity: 0.5;
}
100% {
transform: translateX(-50%) translateY(-22px) scale(2);
opacity: 0;
}
}
/* ── WICK ──────────────────────────────────────────── */
.candle-wick {
width: 2px;
height: 8px;
background: linear-gradient(to bottom, #3a2000, #1a0f00);
border-radius: 1px;
position: relative;
z-index: 2;
margin-bottom: -1px;
}
/* ── CANDLE BODY ──────────────────────────────────── */
.candle-body {
position: relative;
width: 22px;
background: linear-gradient(to right,
#e8dcc8 0%,
#fdf6e3 25%,
#fff9f0 50%,
#fdf6e3 75%,
#d9cbb5 100%);
border-radius: 3px 3px 4px 4px;
transition: height 0.18s linear;
box-shadow:
inset -3px 0 6px rgba(0, 0, 0, 0.1),
inset 3px 0 4px rgba(255, 255, 255, 0.4);
overflow: visible;
}
/* Candle top melted rim */
.candle-body::before {
content: '';
position: absolute;
top: -3px;
left: -2px;
right: -2px;
height: 6px;
background: radial-gradient(ellipse at center, #ffe8b0 0%, #e8d5a0 100%);
border-radius: 50%;
z-index: 1;
}
/* ── WAX DRIPS ──────────────────────────────────────── */
.wax-drip {
position: absolute;
top: 2px;
width: 5px;
background: linear-gradient(to bottom, rgba(253, 246, 227, 0.95), rgba(232, 220, 200, 0.6));
border-radius: 0 0 4px 4px;
animation: drip-grow 4s ease-in-out infinite;
}
.wax-drip-1 {
left: -3px;
height: 14px;
animation-delay: 0s;
animation-duration: 4.5s;
}
.wax-drip-2 {
right: -3px;
height: 10px;
animation-delay: 1.2s;
animation-duration: 3.8s;
}
.wax-drip-3 {
left: 2px;
height: 18px;
animation-delay: 2.5s;
animation-duration: 5.2s;
}
@keyframes drip-grow {
0% {
height: 6px;
opacity: 0.7;
}
60% {
height: 20px;
opacity: 1;
}
100% {
height: 6px;
opacity: 0.7;
}
}
/* ── CANDLE BASE / HOLDER ────────────────────────── */
.candle-base {
width: 32px;
height: 10px;
background: linear-gradient(to bottom, #c8a96e, #a07840);
border-radius: 3px 3px 6px 6px;
box-shadow:
0 3px 8px rgba(0, 0, 0, 0.3),
inset 0 1px 2px rgba(255, 200, 100, 0.4);
position: relative;
}
/* Holder shine */
.candle-base::after {
content: '';
position: absolute;
top: 2px;
left: 5px;
width: 8px;
height: 3px;
background: rgba(255, 220, 140, 0.4);
border-radius: 50%;
}
/* ── SCROLL LABEL ───────────────────────────────── */
.candle-scroll-label {
margin-top: 6px;
font-family: var(--font-script);
font-size: 0.68rem;
color: rgba(212, 162, 76, 0.7);
letter-spacing: 0.05em;
white-space: nowrap;
text-align: center;
transition: opacity 0.3s ease;
}

View File

@ -0,0 +1,99 @@
import { useEffect, useState } from "react";
import "./ScrollCandle.css";
// Space taken by flame + wick + base + label + bottom offset + header (ticker 36 + AppBar 76) + safety gap
const NON_BODY_HEIGHT = 70 + 8 + 10 + 24 + 40 + 112 + 16;
const CANDLE_MIN_HEIGHT = 14; // px — almost fully melted
export function ScrollCandle() {
const [maxHeight, setMaxHeight] = useState(
() => Math.max(200, window.innerHeight - NON_BODY_HEIGHT)
);
const [candleHeight, setCandleHeight] = useState(
() => Math.max(200, window.innerHeight - NON_BODY_HEIGHT)
);
const [scrollPct, setScrollPct] = useState(0);
// Recalculate on resize
useEffect(() => {
const onResize = () => {
const newMax = Math.max(200, window.innerHeight - NON_BODY_HEIGHT);
setMaxHeight(newMax);
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.scrollY;
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
const pct = docHeight > 0 ? Math.min(scrollTop / docHeight, 1) : 0;
setScrollPct(pct);
setCandleHeight(
Math.max(CANDLE_MIN_HEIGHT, maxHeight - pct * (maxHeight - CANDLE_MIN_HEIGHT))
);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [maxHeight]);
// Flame shrinks slightly as candle melts
const flameScale = 0.6 + (candleHeight / maxHeight) * 0.4;
// Label changes as candle melts
const label =
scrollPct < 0.15 ? "just lit" :
scrollPct < 0.45 ? "burning..." :
scrollPct < 0.75 ? "melting..." :
scrollPct < 0.92 ? "almost gone" :
"★ keep scrolling";
return (
<div className="scroll-candle-wrap" aria-hidden="true">
{/* ── Flame ── */}
<div
className="candle-flame-wrap"
style={{ transform: `scale(${flameScale})`, transformOrigin: "bottom center" }}
>
{/* Smoke */}
<div className="smoke-wrap">
<div className="smoke-particle" />
<div className="smoke-particle" />
<div className="smoke-particle" />
</div>
{/* Glow halo */}
<div className="flame-glow" />
{/* Outer flame */}
<div className="flame-outer">
{/* Inner core */}
<div className="flame-inner" />
</div>
</div>
{/* ── Wick ── */}
<div className="candle-wick" />
{/* ── Candle Body (shrinks on scroll) ── */}
<div className="candle-body" style={{ height: candleHeight }}>
<div className="wax-drip wax-drip-1" />
<div className="wax-drip wax-drip-2" />
<div className="wax-drip wax-drip-3" />
</div>
{/* ── Base / Holder ── */}
<div className="candle-base" />
{/* ── Label ── */}
<p
className="candle-scroll-label"
style={{ opacity: scrollPct > 0.02 ? 1 : 0 }}
>
{label}
</p>
</div>
);
}

View File

@ -1,176 +1,85 @@
@import url('https://fonts.googleapis.com/css2?family=Cinzel:wght@400;500;600;700&family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400&family=Great+Vibes&family=Inter:wght@300;400;500;600&display=swap');
@import "tailwindcss";
@import "tw-animate-css";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--color-card-border: hsl(var(--card-border));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-popover-border: hsl(var(--popover-border));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-primary-border: var(--primary-border);
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-secondary-border: var(--secondary-border);
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-muted-border: var(--muted-border);
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-accent-border: var(--accent-border);
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-destructive-border: var(--destructive-border);
--color-chart-1: hsl(var(--chart-1));
--color-chart-2: hsl(var(--chart-2));
--color-chart-3: hsl(var(--chart-3));
--color-chart-4: hsl(var(--chart-4));
--color-chart-5: hsl(var(--chart-5));
--color-sidebar: hsl(var(--sidebar));
--color-sidebar-foreground: hsl(var(--sidebar-foreground));
--color-sidebar-border: hsl(var(--sidebar-border));
--color-sidebar-primary: hsl(var(--sidebar-primary));
--color-sidebar-primary-foreground: hsl(var(--sidebar-primary-foreground));
--color-sidebar-primary-border: var(--sidebar-primary-border);
--color-sidebar-accent: hsl(var(--sidebar-accent));
--color-sidebar-accent-foreground: hsl(var(--sidebar-accent-foreground));
--color-sidebar-accent-border: var(--sidebar-accent-border);
--color-sidebar-ring: hsl(var(--sidebar-ring));
--font-sans: var(--app-font-sans);
--font-serif: var(--app-font-serif);
--font-display: var(--app-font-display);
--font-script: var(--app-font-script);
--font-mono: var(--app-font-mono);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
/* ─── CSS Custom Properties ─────────────────────────────── */
:root {
--background: 272 68% 94%;
--foreground: 272 65% 18%;
--card: 272 68% 97%;
--card-foreground: 272 65% 18%;
--card-border: 272 40% 85%;
--popover: 272 68% 97%;
--popover-foreground: 272 65% 18%;
--popover-border: 272 40% 85%;
--primary: 272 65% 34%;
--primary-foreground: 0 0% 100%;
--secondary: 272 68% 87%;
--secondary-foreground: 272 65% 25%;
--muted: 272 40% 90%;
--muted-foreground: 272 30% 50%;
--accent: 38 59% 56%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 272 40% 82%;
--input: 272 40% 82%;
--ring: 272 65% 34%;
--chart-1: 272 65% 34%;
--chart-2: 38 59% 56%;
--chart-3: 272 68% 70%;
--chart-4: 44 100% 71%;
--chart-5: 272 50% 55%;
--sidebar: 272 68% 87%;
--sidebar-foreground: 272 65% 18%;
--sidebar-border: 272 40% 80%;
--sidebar-primary: 272 65% 34%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 38 59% 56%;
--sidebar-accent-foreground: 0 0% 100%;
--sidebar-ring: 272 65% 34%;
--app-font-sans: 'Inter', sans-serif;
--app-font-serif: 'Cormorant Garamond', serif;
--app-font-display: 'Cinzel', serif;
--app-font-script: 'Great Vibes', cursive;
--app-font-mono: Menlo, monospace;
--radius: 0.75rem;
--brand-purple: #5B1E8C;
--brand-purple-dark: #3A1260;
--brand-purple-mid: #7B3DB8;
--brand-lavender: #DCC6F2;
--brand-light-lavender: #EFE4FA;
--brand-light-lavender:#EFE4FA;
--brand-card: #F8F2FD;
--brand-gold: #D4A24C;
--brand-flame-glow: #FFD36B;
--brand-flame-core: #FFF3C4;
--brand-gold-light: #FFD36B;
--brand-gold-core: #FFF3C4;
--brand-text: #2D0D52;
--brand-muted: #7B5A9E;
--brand-border: #D5BEF0;
--font-sans: 'Inter', sans-serif;
--font-serif: 'Cormorant Garamond', serif;
--font-display: 'Cinzel', serif;
--font-script: 'Great Vibes', cursive;
}
.dark {
--background: 272 65% 10%;
--foreground: 272 68% 94%;
--card: 272 65% 15%;
--card-foreground: 272 68% 94%;
--card-border: 272 65% 25%;
--popover: 272 65% 15%;
--popover-foreground: 272 68% 94%;
--popover-border: 272 65% 25%;
--primary: 272 68% 87%;
--primary-foreground: 272 65% 18%;
--secondary: 272 65% 20%;
--secondary-foreground: 272 68% 94%;
--muted: 272 65% 20%;
--muted-foreground: 272 40% 70%;
--accent: 38 59% 56%;
--accent-foreground: 0 0% 100%;
--destructive: 0 84% 60%;
--destructive-foreground: 0 0% 100%;
--border: 272 65% 25%;
--input: 272 65% 25%;
--ring: 272 68% 87%;
--chart-1: 272 68% 87%;
--chart-2: 38 59% 56%;
--chart-3: 272 68% 70%;
--chart-4: 44 100% 71%;
--chart-5: 272 50% 55%;
/* ─── Reset / Base ───────────────────────────────────────── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@layer base {
* {
@apply border-border;
}
body {
@apply font-sans antialiased bg-background text-foreground;
}
h1, h2, h3, h4, h5, h6 {
@apply font-serif;
}
html {
scroll-behavior: smooth;
}
body {
font-family: var(--font-sans);
background-color: var(--brand-light-lavender);
color: var(--brand-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-serif);
line-height: 1.2;
}
a {
color: inherit;
text-decoration: none;
}
img {
max-width: 100%;
display: block;
}
/* ─── Keyframe Animations ────────────────────────────────── */
@keyframes flame-flicker {
0%, 100% {
transform: scale(1) translateY(0);
opacity: 0.9;
}
50% {
transform: scale(1.05) translateY(-2px);
opacity: 1;
}
0%, 100% { transform: scale(1) translateY(0); opacity: 0.9; }
50% { transform: scale(1.05) translateY(-2px); opacity: 1; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── Global Utility Classes ─────────────────────────────── */
.animate-flame {
animation: flame-flicker 2s ease-in-out infinite alternate;
}
.font-script {
font-family: var(--font-script);
}
.font-display {
font-family: var(--font-display);
}
.font-serif {
font-family: var(--font-serif);
}

View File

@ -1,5 +1,13 @@
import { createRoot } from "react-dom/client";
import { ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import App from "./App";
import theme from "./theme";
import "./index.css";
createRoot(document.getElementById("root")!).render(<App />);
createRoot(document.getElementById("root")!).render(
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
);

View File

@ -1,139 +1,105 @@
import { motion } from "framer-motion";
import { Droplets, Flame, HeartHandshake } from "lucide-react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import "./about.css";
const logoPath = "/opengraph.jpg";
export default function AboutPage() {
const fadeInUp = {
const fadeInUp = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: "easeOut" } },
};
};
const stagger = {
const stagger = {
visible: { transition: { staggerChildren: 0.2 } },
};
};
export default function AboutPage() {
return (
<div className="w-full min-h-screen bg-background">
{/* Hero */}
<section className="py-24 px-6 bg-card">
<motion.div
initial="hidden"
animate="visible"
variants={stagger}
className="max-w-4xl mx-auto text-center"
>
<motion.h1 variants={fadeInUp} className="font-serif text-5xl md:text-6xl lg:text-7xl text-primary mb-6">
The Light Behind the Wax
</motion.h1>
<motion.div variants={fadeInUp} className="w-24 h-1 bg-accent mx-auto mb-8" />
<motion.p variants={fadeInUp} className="font-sans text-lg text-muted-foreground leading-relaxed">
Mani Candles Co. is a small handmade candle brand crafted with love, natural ingredients, and intention. Every candle is poured by hand in small batches, designed to bring warmth, luxury, and a touch of romance into your sacred spaces.
<Box>
{/* ── Hero ── */}
<section className="about-hero">
<Container maxWidth="md">
<motion.div initial="hidden" animate="visible" variants={stagger}>
<motion.h1 variants={fadeInUp}>The Light Behind the Wax</motion.h1>
<div className="section-divider" />
<motion.p variants={fadeInUp}>
Mani Candles Co. is a small handmade candle brand crafted with love, natural ingredients, and intention.
Every candle is poured by hand in small batches, designed to bring warmth, luxury, and a touch of romance
into your sacred spaces.
</motion.p>
</motion.div>
</Container>
</section>
{/* Brand Values */}
<section className="py-24 px-6">
<div className="max-w-6xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{/* ── Brand Values ── */}
<section className="about-values">
<Container maxWidth="lg">
<div className="values-grid">
{[
{
icon: <HeartHandshake size={40} />,
title: "Handmade",
desc: "We believe in the power of human touch. No machines, no mass production. Just careful hands pouring beautiful creations."
},
{
icon: <Droplets size={40} />,
title: "Natural",
desc: "Pure soy wax, lead-free cotton wicks, and premium fragrance oils infused with essential oils. Clean burns only."
},
{
icon: <Flame size={40} />,
title: "Heartmade",
desc: "A candle is more than wax and scent. It's ambiance, memory, and comfort. We put our heart into every jar."
}
].map((val, i) => (
{ Icon: HeartHandshake, title: "Handmade", desc: "We believe in the power of human touch. No machines, no mass production. Just careful hands pouring beautiful creations." },
{ Icon: Droplets, title: "Natural", desc: "Pure soy wax, lead-free cotton wicks, and premium fragrance oils infused with essential oils. Clean burns only." },
{ Icon: Flame, title: "Heartmade", desc: "A candle is more than wax and scent. It's ambiance, memory, and comfort. We put our heart into every jar." },
].map(({ Icon, title, desc }, i) => (
<motion.div
key={i}
key={title}
className="value-card"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { delay: i * 0.2, duration: 0.6 } }
}}
className="text-center p-8 bg-card rounded-xl shadow-sm border border-border/50 hover:shadow-md transition-shadow"
variants={{ hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0, transition: { delay: i * 0.2, duration: 0.6 } } }}
>
<div className="text-accent mb-6 flex justify-center">{val.icon}</div>
<h3 className="font-serif text-2xl text-primary mb-4">{val.title}</h3>
<p className="font-sans text-muted-foreground">{val.desc}</p>
<div className="value-icon"><Icon size={40} /></div>
<h3>{title}</h3>
<p>{desc}</p>
</motion.div>
))}
</div>
</div>
</Container>
</section>
{/* Meet the Maker */}
<section className="py-24 px-6 bg-primary text-primary-foreground relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-r from-transparent to-primary/50 mix-blend-overlay" />
<div className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-16 items-center relative z-10">
{/* ── Meet the Maker ── */}
<section className="about-maker">
<div className="maker-gradient-overlay" />
<Container maxWidth="lg">
<div className="maker-grid">
<motion.div
className="maker-img-wrap"
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="aspect-square rounded-2xl overflow-hidden shadow-2xl relative"
>
<img
src={logoPath}
alt="Mani Candles Co. Logo Art"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 ring-1 ring-inset ring-white/20 rounded-2xl" />
<img src={logoPath} alt="Mani Candles Co. Logo Art" />
<div className="maker-img-ring" />
</motion.div>
<motion.div
className="maker-content"
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<h2 className="font-serif text-4xl md:text-5xl mb-6">Meet the Maker</h2>
<div className="w-20 h-1 bg-accent mb-8" />
<div className="font-sans space-y-6 text-primary-foreground/90 text-lg leading-relaxed">
<p>
What started as a quiet evening hobby quickly blossomed into a deep passion for the alchemy of scent and light.
</p>
<p>
I wanted to create more than just candles; I wanted to craft experiences. The flicker of the flame, the slow release of carefully blended perfumesthese elements have the power to transform a chaotic day into a moment of pure serenity.
</p>
<p>
Every Mani candle is a piece of my heart, poured in small batches to ensure that the luxury you experience is entirely personal and authentically handmade.
</p>
<div className="pt-6">
<span className="font-script text-4xl text-accent">With love,</span>
</div>
</div>
<h2>Meet the Maker</h2>
<div className="maker-divider" />
<p>What started as a quiet evening hobby quickly blossomed into a deep passion for the alchemy of scent and light.</p>
<p>I wanted to create more than just candles; I wanted to craft experiences. The flicker of the flame, the slow release of carefully blended perfumesthese elements have the power to transform a chaotic day into a moment of pure serenity.</p>
<p>Every Mani candle is a piece of my heart, poured in small batches to ensure that the luxury you experience is entirely personal and authentically handmade.</p>
<span className="maker-signature">With love,</span>
</motion.div>
</div>
</Container>
</section>
{/* Process Timeline */}
<section className="py-24 px-6 bg-background">
<div className="max-w-5xl mx-auto">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="text-center mb-20"
>
<h2 className="font-serif text-4xl text-primary mb-4">Our Process</h2>
<p className="font-sans text-muted-foreground">The journey of a Mani Candle.</p>
</motion.div>
{/* ── Process Timeline ── */}
<section className="about-process">
<Container maxWidth="md">
<h2>Our Process</h2>
<p className="subtitle">The journey of a Mani Candle.</p>
<div className="space-y-12">
<div className="timeline">
{[
{ step: "01", title: "Sourcing", desc: "We select only the finest natural soy wax and premium, phthalate-free fragrances." },
{ step: "02", title: "Blending", desc: "Fragrances are meticulously mixed at the exact temperature for optimal throw." },
@ -143,24 +109,22 @@ export default function AboutPage() {
].map((item, i) => (
<motion.div
key={i}
className="timeline-item"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.5 }}
className="flex flex-col md:flex-row gap-6 md:gap-12 items-start md:items-center"
>
<div className="font-serif text-6xl text-accent/30 font-light w-24 flex-shrink-0">
{item.step}
</div>
<div className="flex-grow pb-8 border-b border-border/50">
<h3 className="font-serif text-2xl text-primary mb-2">{item.title}</h3>
<p className="font-sans text-muted-foreground">{item.desc}</p>
<div className="timeline-step">{item.step}</div>
<div className="timeline-body">
<h3>{item.title}</h3>
<p>{item.desc}</p>
</div>
</motion.div>
))}
</div>
</div>
</Container>
</section>
</div>
</Box>
);
}

View File

@ -1,6 +1,15 @@
import { motion } from "framer-motion";
import { Mail, MapPin, Phone, Instagram } from "lucide-react";
import { HeartHandshake } from "lucide-react";
import { useState } from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import "./contact.css";
const fadeInUp = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: "easeOut" } },
};
export default function ContactPage() {
const [formStatus, setFormStatus] = useState<"idle" | "submitting" | "success">("idle");
@ -8,197 +17,117 @@ export default function ContactPage() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setFormStatus("submitting");
// Simulate network request
setTimeout(() => {
setFormStatus("success");
}, 1500);
};
const fadeInUp = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: "easeOut" } },
setTimeout(() => setFormStatus("success"), 1500);
};
return (
<div className="w-full min-h-screen bg-background">
<section className="py-24 px-6 bg-card text-center">
<motion.div
initial="hidden"
animate="visible"
variants={fadeInUp}
className="max-w-3xl mx-auto"
>
<h1 className="font-serif text-5xl md:text-6xl text-primary mb-6">Get in Touch</h1>
<p className="font-sans text-muted-foreground text-lg mb-8">
Whether you have a question about an order, wholesale inquiries, or simply want to share how much you love your new candle, we'd love to hear from you.
</p>
<div className="w-24 h-1 bg-accent mx-auto" />
<Box>
{/* ── Hero ── */}
<section className="contact-hero">
<Container maxWidth="md">
<motion.div initial="hidden" animate="visible" variants={fadeInUp}>
<h1>Get in Touch</h1>
<p>Whether you have a question about an order, wholesale inquiries, or simply want to share how much you love your new candle, we'd love to hear from you.</p>
<div className="section-divider" />
</motion.div>
</Container>
</section>
<section className="py-20 px-6">
<div className="max-w-6xl mx-auto grid grid-cols-1 lg:grid-cols-2 gap-16">
{/* Contact Info */}
{/* ── Contact Body ── */}
<section className="contact-body">
<Container maxWidth="lg">
<div className="contact-grid">
{/* Info Panel */}
<motion.div
className="contact-info-panel"
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="flex flex-col gap-10"
>
<h2>Contact Information</h2>
<div className="contact-info-list">
{[
{ Icon: Mail, label: "Email", value: <a href="mailto:hello@manicandles.co">hello@manicandles.co</a> },
{ Icon: Phone, label: "Phone", value: <>Available MonFri, 9am5pm EST<br />+1 (555) 123-4567</> },
{ Icon: MapPin, label: "Studio", value: <>New York, NY<br />(By appointment only)</> },
].map(({ Icon, label, value }) => (
<div key={label} className="contact-info-item">
<div className="contact-info-icon"><Icon size={20} /></div>
<div>
<h2 className="font-serif text-3xl text-primary mb-8">Contact Information</h2>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-secondary/50 flex items-center justify-center text-primary flex-shrink-0">
<Mail size={20} />
<div className="contact-info-label">{label}</div>
<div className="contact-info-value">{value}</div>
</div>
<div>
<h4 className="font-display tracking-widest uppercase text-sm text-primary mb-1">Email</h4>
<a href="mailto:hello@manicandles.co" className="font-sans text-muted-foreground hover:text-accent transition-colors">
hello@manicandles.co
</a>
</div>
))}
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-secondary/50 flex items-center justify-center text-primary flex-shrink-0">
<Phone size={20} />
</div>
<div>
<h4 className="font-display tracking-widest uppercase text-sm text-primary mb-1">Phone</h4>
<p className="font-sans text-muted-foreground">
Available Mon-Fri, 9am - 5pm EST<br/>
+1 (555) 123-4567
</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-secondary/50 flex items-center justify-center text-primary flex-shrink-0">
<MapPin size={20} />
</div>
<div>
<h4 className="font-display tracking-widest uppercase text-sm text-primary mb-1">Studio</h4>
<p className="font-sans text-muted-foreground">
New York, NY<br/>
(By appointment only)
</p>
</div>
</div>
</div>
</div>
<div className="p-8 bg-card border border-border rounded-xl mt-4">
<h3 className="font-serif text-2xl text-primary mb-4">Follow Our Journey</h3>
<p className="font-sans text-muted-foreground mb-6">
Join our community on Instagram for behind-the-scenes peeks, styling inspiration, and launch announcements.
</p>
<a href="#" className="inline-flex items-center gap-2 text-primary hover:text-accent transition-colors font-medium">
<div className="contact-social-box">
<h3>Follow Our Journey</h3>
<p>Join our community on Instagram for behind-the-scenes peeks, styling inspiration, and launch announcements.</p>
<a href="#" className="contact-ig-link">
<Instagram size={20} />
<span>@manicandles.co</span>
</a>
</div>
</motion.div>
{/* Form */}
{/* Form Panel */}
<motion.div
className="contact-form-panel"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8 }}
className="bg-white p-8 md:p-12 shadow-xl border border-border/50 rounded-none relative"
>
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-primary via-accent to-primary" />
<h2 className="font-serif text-3xl text-primary mb-8">Send a Message</h2>
<div className="contact-form-top-bar" />
{formStatus === "success" ? (
<motion.div
className="contact-success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="flex flex-col items-center justify-center text-center py-12"
>
<div className="w-16 h-16 bg-green-50 text-green-600 rounded-full flex items-center justify-center mb-6">
<HeartHandshake size={32} />
</div>
<h3 className="font-serif text-2xl text-primary mb-2">Thank you!</h3>
<p className="font-sans text-muted-foreground">
Your message has been received. We will get back to you as soon as possible.
</p>
<button
onClick={() => setFormStatus("idle")}
className="mt-8 text-accent hover:text-primary transition-colors font-display uppercase tracking-widest text-sm border-b border-accent pb-1"
>
Send another
</button>
<div className="success-icon-wrap"><HeartHandshake size={32} /></div>
<h3>Thank you!</h3>
<p>Your message has been received. We will get back to you as soon as possible.</p>
<button className="send-another-btn" onClick={() => setFormStatus("idle")}>Send another</button>
</motion.div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label htmlFor="name" className="font-display uppercase tracking-widest text-xs text-primary">Name</label>
<input
id="name"
type="text"
required
className="w-full bg-background border-b border-border py-3 px-4 focus:outline-none focus:border-accent transition-colors font-sans"
placeholder="Your name"
/>
<>
<h2>Send a Message</h2>
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-field">
<label htmlFor="name" className="form-label">Name</label>
<input id="name" type="text" className="form-input" placeholder="Your name" required />
</div>
<div className="space-y-2">
<label htmlFor="email" className="font-display uppercase tracking-widest text-xs text-primary">Email</label>
<input
id="email"
type="email"
required
className="w-full bg-background border-b border-border py-3 px-4 focus:outline-none focus:border-accent transition-colors font-sans"
placeholder="Your email"
/>
<div className="form-field">
<label htmlFor="email" className="form-label">Email</label>
<input id="email" type="email" className="form-input" placeholder="Your email" required />
</div>
</div>
<div className="space-y-2">
<label htmlFor="subject" className="font-display uppercase tracking-widest text-xs text-primary">Subject</label>
<input
id="subject"
type="text"
required
className="w-full bg-background border-b border-border py-3 px-4 focus:outline-none focus:border-accent transition-colors font-sans"
placeholder="How can we help?"
/>
<div className="form-field">
<label htmlFor="subject" className="form-label">Subject</label>
<input id="subject" type="text" className="form-input" placeholder="How can we help?" required />
</div>
<div className="space-y-2">
<label htmlFor="message" className="font-display uppercase tracking-widest text-xs text-primary">Message</label>
<textarea
id="message"
rows={5}
required
className="w-full bg-background border border-border py-3 px-4 focus:outline-none focus:border-accent transition-colors font-sans resize-none"
placeholder="Write your message here..."
/>
<div className="form-field">
<label htmlFor="message" className="form-label">Message</label>
<textarea id="message" className="form-textarea" placeholder="Write your message here..." required />
</div>
<button
type="submit"
disabled={formStatus === "submitting"}
className="w-full py-4 bg-primary text-primary-foreground font-display tracking-widest uppercase text-sm hover:bg-accent transition-colors disabled:opacity-70"
>
<button type="submit" className="form-submit" disabled={formStatus === "submitting"}>
{formStatus === "submitting" ? "Sending..." : "Send Message"}
</button>
</form>
</>
)}
</motion.div>
</div>
</Container>
</section>
<section className="py-16 text-center bg-background px-6">
<p className="font-script text-3xl text-primary max-w-xl mx-auto">
"May the light of our candles bring peace to your mind and warmth to your soul."
</p>
{/* ── Closing Quote ── */}
<section className="contact-quote">
<p>"May the light of our candles bring peace to your mind and warmth to your soul."</p>
</section>
</div>
</Box>
);
}
// Ensure HeartHandshake is imported
import { HeartHandshake } from "lucide-react";

View File

@ -1,5 +1,7 @@
import { motion } from "framer-motion";
import { useState } from "react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import birthdayImage from "@/assets/images/product-birthday.png";
import eucalyptusImage from "@/assets/images/product-eucalyptus.png";
import jasmineImage from "@/assets/images/product-jasmine.png";
@ -8,124 +10,56 @@ import linenImage from "@/assets/images/product-linen.png";
import oudImage from "@/assets/images/product-oud.png";
import roseImage from "@/assets/images/product-rose.png";
import sandalwoodImage from "@/assets/images/product-sandalwood.png";
import "./products.css";
const PRODUCTS = [
{
id: 1,
name: "Lavender Dream",
scent: "Calming French Lavender, Vanilla, & Bergamot",
price: "$28",
image: lavenderImage,
category: "Floral",
},
{
id: 2,
name: "Rose & Vanilla",
scent: "Damask Rose, Sweet Vanilla bean, & Amber",
price: "$32",
image: roseImage,
category: "Floral",
},
{
id: 3,
name: "Eucalyptus Mint",
scent: "Fresh Eucalyptus, Spearmint, & Cedar",
price: "$28",
image: eucalyptusImage,
category: "Fresh",
},
{
id: 4,
name: "Oud & Amber",
scent: "Rich Oud Wood, Warm Amber, & Dark Musk",
price: "$36",
image: oudImage,
category: "Woody",
},
{
id: 5,
name: "Jasmine Night",
scent: "Night-blooming Jasmine, Ylang Ylang, & Sandalwood",
price: "$30",
image: jasmineImage,
category: "Floral",
},
{
id: 6,
name: "Sandalwood Cedar",
scent: "Indian Sandalwood, Cedarwood, & Patchouli",
price: "$34",
image: sandalwoodImage,
category: "Woody",
},
{
id: 7,
name: "Fresh Linen",
scent: "Crisp Cotton, White Tea, & Lily of the Valley",
price: "$26",
image: linenImage,
category: "Fresh",
},
{
id: 8,
name: "Birthday Cake",
scent: "Vanilla Buttercream, Sugar Spun, & Joy",
price: "$28",
image: birthdayImage,
category: "Sweet",
},
{ id: 1, name: "Lavender Dream", scent: "Calming French Lavender, Vanilla, & Bergamot", price: "$28", image: lavenderImage, category: "Floral" },
{ id: 2, name: "Rose & Vanilla", scent: "Damask Rose, Sweet Vanilla Bean, & Amber", price: "$32", image: roseImage, category: "Floral" },
{ id: 3, name: "Eucalyptus Mint", scent: "Fresh Eucalyptus, Spearmint, & Cedar", price: "$28", image: eucalyptusImage, category: "Fresh" },
{ id: 4, name: "Oud & Amber", scent: "Rich Oud Wood, Warm Amber, & Dark Musk", price: "$36", image: oudImage, category: "Woody" },
{ id: 5, name: "Jasmine Night", scent: "Night-blooming Jasmine, Ylang Ylang, & Sandalwood", price: "$30", image: jasmineImage, category: "Floral" },
{ id: 6, name: "Sandalwood Cedar", scent: "Indian Sandalwood, Cedarwood, & Patchouli", price: "$34", image: sandalwoodImage, category: "Woody" },
{ id: 7, name: "Fresh Linen", scent: "Crisp Cotton, White Tea, & Lily of the Valley", price: "$26", image: linenImage, category: "Fresh" },
{ id: 8, name: "Birthday Cake", scent: "Vanilla Buttercream, Sugar Spun, & Joy", price: "$28", image: birthdayImage, category: "Sweet" },
];
export default function ProductsPage() {
const [filter, setFilter] = useState("All");
const categories = ["All", ...Array.from(new Set(PRODUCTS.map((p) => p.category)))];
const filteredProducts = filter === "All" ? PRODUCTS : PRODUCTS.filter(p => p.category === filter);
const fadeInUp = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } },
};
const filteredProducts = filter === "All" ? PRODUCTS : PRODUCTS.filter((p) => p.category === filter);
return (
<div className="w-full min-h-screen bg-background">
<section className="py-24 px-6 text-center bg-card">
<Box>
{/* ── Hero ── */}
<section className="products-hero">
<Container maxWidth="md">
<motion.div
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.8 } }
}}
className="max-w-3xl mx-auto"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<h1 className="font-serif text-5xl md:text-6xl text-primary mb-6">Our Collection</h1>
<p className="font-sans text-muted-foreground text-lg mb-8">
Each candle is meticulously hand-poured with natural soy wax and premium fragrance oils, designed to elevate your everyday rituals.
</p>
<div className="w-24 h-1 bg-accent mx-auto" />
<h1>Our Collection</h1>
<p>Each candle is meticulously hand-poured with natural soy wax and premium fragrance oils, designed to elevate your everyday rituals.</p>
<div className="section-divider" />
</motion.div>
</Container>
</section>
<section className="py-12 px-6">
<div className="max-w-7xl mx-auto">
{/* Filters */}
{/* ── Products Body ── */}
<section className="products-body">
<Container maxWidth="xl">
{/* Filter Bar */}
<motion.div
className="filter-bar"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="flex flex-wrap justify-center gap-4 mb-16"
>
{categories.map((cat) => (
<button
key={cat}
className={`filter-btn${filter === cat ? " active" : ""}`}
onClick={() => setFilter(cat)}
className={`font-display tracking-widest text-sm uppercase px-6 py-2 transition-all duration-300 ${
filter === cat
? "text-primary border-b-2 border-accent"
: "text-muted-foreground hover:text-primary"
}`}
>
{cat}
</button>
@ -133,45 +67,34 @@ export default function ProductsPage() {
</motion.div>
{/* Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-16">
<div className="products-grid">
{filteredProducts.map((product, index) => (
<motion.div
key={product.id}
initial="hidden"
whileInView="visible"
className="prod-card"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
variants={{
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { delay: (index % 4) * 0.1, duration: 0.6 } }
}}
className="group flex flex-col"
transition={{ delay: (index % 4) * 0.1, duration: 0.6 }}
>
<div className="relative aspect-[4/5] overflow-hidden mb-6 bg-card border border-border">
<img
src={product.image}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-primary/30 opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex items-center justify-center backdrop-blur-[2px]">
<button className="px-8 py-3 bg-background text-primary font-display tracking-widest text-sm uppercase translate-y-8 group-hover:translate-y-0 transition-transform duration-500 hover:bg-accent hover:text-primary-foreground shadow-lg">
Add to Cart
</button>
<div className="prod-img-wrap">
<img src={product.image} alt={product.name} />
<div className="prod-overlay">
<button className="prod-overlay-btn">Add to Cart</button>
</div>
</div>
<div className="flex-grow flex flex-col justify-between">
<div className="prod-info">
<div>
<h3 className="font-serif text-2xl text-primary mb-2">{product.name}</h3>
<p className="font-sans text-sm text-muted-foreground mb-4 line-clamp-2">
{product.scent}
</p>
<h3 className="prod-name">{product.name}</h3>
<p className="prod-scent">{product.scent}</p>
</div>
<p className="font-sans font-medium text-accent text-lg">{product.price}</p>
<p className="prod-price">{product.price}</p>
</div>
</motion.div>
))}
</div>
</div>
</Container>
</section>
</div>
</Box>
);
}

229
src/pages/about.css Normal file
View File

@ -0,0 +1,229 @@
/* ─── about.css ──────────────────────────────────────────── */
/* HERO */
.about-hero {
padding: 6rem 1.5rem;
background: var(--brand-card);
text-align: center;
}
.about-hero h1 {
font-family: var(--font-serif);
font-size: clamp(2.5rem, 6vw, 4.5rem);
color: var(--brand-purple);
margin-bottom: 1rem;
}
.about-hero p {
font-family: var(--font-sans);
font-size: 1.1rem;
color: var(--brand-muted);
line-height: 1.8;
max-width: 680px;
margin: 0 auto;
}
/* BRAND VALUES */
.about-values {
padding: 6rem 1.5rem;
background: var(--brand-light-lavender);
}
.values-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 1100px;
margin: 0 auto;
}
@media (min-width: 768px) {
.values-grid { grid-template-columns: 1fr 1fr 1fr; }
}
.value-card {
text-align: center;
padding: 2.5rem;
background: var(--brand-card);
border-radius: 0.75rem;
border: 1px solid rgba(213,190,240,0.5);
box-shadow: 0 2px 12px rgba(91,30,140,0.06);
transition: box-shadow 0.3s ease;
}
.value-card:hover {
box-shadow: 0 6px 24px rgba(91,30,140,0.12);
}
.value-icon {
color: var(--brand-gold);
display: flex;
justify-content: center;
margin-bottom: 1.5rem;
}
.value-card h3 {
font-family: var(--font-serif);
font-size: 1.5rem;
color: var(--brand-purple);
margin-bottom: 0.75rem;
}
.value-card p {
font-family: var(--font-sans);
color: var(--brand-muted);
line-height: 1.7;
}
/* MEET THE MAKER */
.about-maker {
padding: 6rem 1.5rem;
background: var(--brand-purple);
color: #ffffff;
position: relative;
overflow: hidden;
}
.maker-gradient-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to right, transparent, rgba(91,30,140,0.5));
mix-blend-mode: overlay;
}
.maker-grid {
display: grid;
grid-template-columns: 1fr;
gap: 4rem;
align-items: center;
max-width: 1100px;
margin: 0 auto;
position: relative;
z-index: 1;
}
@media (min-width: 1024px) {
.maker-grid { grid-template-columns: 1fr 1fr; }
}
.maker-img-wrap {
aspect-ratio: 1;
border-radius: 1rem;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
position: relative;
}
.maker-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
}
.maker-img-ring {
position: absolute;
inset: 0;
border-radius: 1rem;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.2);
}
.maker-content h2 {
font-family: var(--font-serif);
font-size: clamp(2rem, 4vw, 3rem);
color: #ffffff;
margin-bottom: 0.75rem;
}
.maker-divider {
width: 80px;
height: 4px;
background: var(--brand-gold);
border-radius: 2px;
margin-bottom: 2rem;
}
.maker-content p {
font-family: var(--font-sans);
font-size: 1.05rem;
line-height: 1.8;
color: rgba(255,255,255,0.88);
margin-bottom: 1.25rem;
}
.maker-signature {
font-family: var(--font-script);
font-size: 2.25rem;
color: var(--brand-gold);
margin-top: 1.5rem;
display: block;
}
/* PROCESS TIMELINE */
.about-process {
padding: 6rem 1.5rem;
background: var(--brand-light-lavender);
}
.about-process h2 {
font-family: var(--font-serif);
font-size: clamp(2rem, 4vw, 2.8rem);
color: var(--brand-purple);
text-align: center;
margin-bottom: 0.5rem;
}
.about-process .subtitle {
font-family: var(--font-sans);
color: var(--brand-muted);
text-align: center;
margin-bottom: 5rem;
}
.timeline {
display: flex;
flex-direction: column;
gap: 3rem;
max-width: 900px;
margin: 0 auto;
}
.timeline-item {
display: flex;
flex-direction: column;
gap: 1rem;
}
@media (min-width: 640px) {
.timeline-item {
flex-direction: row;
align-items: center;
gap: 3rem;
}
}
.timeline-step {
font-family: var(--font-serif);
font-size: 3.5rem;
color: rgba(212,162,76,0.3);
font-weight: 300;
min-width: 80px;
line-height: 1;
}
.timeline-body {
flex: 1;
padding-bottom: 2rem;
border-bottom: 1px solid rgba(213,190,240,0.5);
}
.timeline-body h3 {
font-family: var(--font-serif);
font-size: 1.4rem;
color: var(--brand-purple);
margin-bottom: 0.4rem;
}
.timeline-body p {
font-family: var(--font-sans);
color: var(--brand-muted);
}

316
src/pages/contact.css Normal file
View File

@ -0,0 +1,316 @@
/* ─── contact.css ────────────────────────────────────────── */
/* HERO */
.contact-hero {
padding: 6rem 1.5rem;
background: var(--brand-card);
text-align: center;
}
.contact-hero h1 {
font-family: var(--font-serif);
font-size: clamp(2.5rem, 6vw, 4rem);
color: var(--brand-purple);
margin-bottom: 1rem;
}
.contact-hero p {
font-family: var(--font-sans);
font-size: 1.05rem;
color: var(--brand-muted);
line-height: 1.8;
max-width: 640px;
margin: 0 auto 2rem;
}
/* CONTACT BODY */
.contact-body {
padding: 5rem 1.5rem;
background: var(--brand-light-lavender);
}
.contact-grid {
display: grid;
grid-template-columns: 1fr;
gap: 4rem;
max-width: 1100px;
margin: 0 auto;
}
@media (min-width: 1024px) {
.contact-grid { grid-template-columns: 1fr 1fr; }
}
/* INFO PANEL */
.contact-info-panel {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.contact-info-panel h2 {
font-family: var(--font-serif);
font-size: 1.9rem;
color: var(--brand-purple);
}
.contact-info-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.contact-info-item {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.contact-info-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(220,198,242,0.5);
display: flex;
align-items: center;
justify-content: center;
color: var(--brand-purple);
flex-shrink: 0;
}
.contact-info-label {
font-family: var(--font-display);
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--brand-purple);
margin-bottom: 0.25rem;
}
.contact-info-value {
font-family: var(--font-sans);
color: var(--brand-muted);
font-size: 0.95rem;
line-height: 1.6;
}
.contact-info-value a {
color: var(--brand-muted);
text-decoration: none;
transition: color 0.3s ease;
}
.contact-info-value a:hover {
color: var(--brand-gold);
}
/* SOCIAL BOX */
.contact-social-box {
padding: 2rem;
background: var(--brand-card);
border-radius: 0.75rem;
border: 1px solid var(--brand-border);
}
.contact-social-box h3 {
font-family: var(--font-serif);
font-size: 1.5rem;
color: var(--brand-purple);
margin-bottom: 0.5rem;
}
.contact-social-box p {
font-family: var(--font-sans);
color: var(--brand-muted);
margin-bottom: 1.25rem;
font-size: 0.9rem;
line-height: 1.6;
}
.contact-ig-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--brand-purple);
font-family: var(--font-sans);
font-weight: 500;
text-decoration: none;
transition: color 0.3s ease;
}
.contact-ig-link:hover {
color: var(--brand-gold);
}
/* FORM PANEL */
.contact-form-panel {
background: #ffffff;
border-radius: 0px;
padding: 3rem;
box-shadow: 0 12px 40px rgba(91,30,140,0.1);
border: 1px solid rgba(213,190,240,0.4);
position: relative;
}
.contact-form-top-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, var(--brand-purple), var(--brand-gold), var(--brand-purple));
border-radius: 0;
}
.contact-form-panel h2 {
font-family: var(--font-serif);
font-size: 1.9rem;
color: var(--brand-purple);
margin-bottom: 2rem;
}
.form-row {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
margin-bottom: 1.25rem;
}
@media (min-width: 640px) {
.form-row { grid-template-columns: 1fr 1fr; }
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.4rem;
margin-bottom: 1.25rem;
}
.form-label {
font-family: var(--font-display);
font-size: 0.65rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--brand-purple);
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.85rem 1rem;
background: var(--brand-light-lavender);
border: 1px solid var(--brand-border);
font-family: var(--font-sans);
font-size: 0.95rem;
color: var(--brand-text);
outline: none;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
border-radius: 0;
box-sizing: border-box;
}
.form-input:focus,
.form-textarea:focus {
border-color: var(--brand-gold);
box-shadow: 0 0 0 2px rgba(212,162,76,0.15);
}
.form-textarea {
resize: none;
height: 140px;
}
.form-submit {
width: 100%;
padding: 1rem;
background: var(--brand-purple);
color: #ffffff;
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
border: none;
cursor: pointer;
transition: background 0.3s ease;
margin-top: 0.5rem;
}
.form-submit:hover:not(:disabled) {
background: var(--brand-gold);
}
.form-submit:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* SUCCESS STATE */
.contact-success {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 0;
}
.success-icon-wrap {
width: 64px;
height: 64px;
border-radius: 50%;
background: #f0fdf4;
color: #16a34a;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
}
.contact-success h3 {
font-family: var(--font-serif);
font-size: 1.75rem;
color: var(--brand-purple);
margin-bottom: 0.5rem;
}
.contact-success p {
font-family: var(--font-sans);
color: var(--brand-muted);
}
.send-another-btn {
margin-top: 2rem;
background: none;
border: none;
border-bottom: 1px solid var(--brand-gold);
padding-bottom: 3px;
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--brand-gold);
cursor: pointer;
transition: color 0.3s ease;
}
.send-another-btn:hover {
color: var(--brand-purple);
border-color: var(--brand-purple);
}
/* CLOSING QUOTE */
.contact-quote {
padding: 4rem 1.5rem;
text-align: center;
background: var(--brand-light-lavender);
}
.contact-quote p {
font-family: var(--font-script);
font-size: clamp(1.5rem, 3vw, 2rem);
color: var(--brand-purple);
max-width: 640px;
margin: 0 auto;
line-height: 1.6;
}

386
src/pages/home.css Normal file
View File

@ -0,0 +1,386 @@
/* ─── home.css ───────────────────────────────────────────── */
/* HERO */
.home-hero {
position: relative;
min-height: 90vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
padding: 5rem 1.5rem;
text-align: center;
}
.home-hero-bg {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(220,198,242,0.4), var(--brand-light-lavender));
z-index: -1;
}
.home-hero-logo-wrap {
width: 140px;
height: 140px;
border-radius: 50%;
overflow: hidden;
border: 4px solid rgba(212,162,76,0.35);
box-shadow: 0 0 40px rgba(212,162,76,0.25);
position: relative;
margin: 0 auto 2rem;
}
.home-hero-flame {
position: absolute;
top: -12px;
right: -12px;
color: var(--brand-gold);
}
.home-hero h1 {
font-family: var(--font-serif);
font-size: clamp(2.5rem, 7vw, 5.5rem);
color: var(--brand-purple);
margin-bottom: 1rem;
drop-shadow: 0 2px 8px rgba(91,30,140,0.15);
}
.home-hero-tagline {
font-family: var(--font-script);
font-size: clamp(1.75rem, 4vw, 3rem);
color: var(--brand-gold);
margin-bottom: 3rem;
display: block;
}
.hero-cta {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 1rem 2.5rem;
background: var(--brand-purple);
color: #ffffff;
font-family: var(--font-display);
font-size: 0.78rem;
letter-spacing: 0.2em;
text-transform: uppercase;
text-decoration: none;
border: 1px solid transparent;
transition: background 0.4s ease, border-color 0.4s ease, color 0.4s ease, box-shadow 0.4s ease;
}
.hero-cta:hover {
background: transparent;
color: var(--brand-purple);
border-color: var(--brand-gold);
box-shadow: 0 0 20px rgba(212,162,76,0.35);
}
/* PHILOSOPHY */
.home-philosophy {
padding: 6rem 1.5rem;
background: var(--brand-card);
}
.home-philosophy h2 {
font-family: var(--font-serif);
font-size: clamp(2rem, 4vw, 3rem);
color: var(--brand-purple);
text-align: center;
margin-bottom: 0.75rem;
}
.section-divider {
width: 96px;
height: 4px;
background: var(--brand-gold);
margin: 0 auto 4rem;
border-radius: 2px;
}
.philosophy-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2.5rem;
max-width: 1100px;
margin: 0 auto;
}
@media (min-width: 768px) {
.philosophy-grid { grid-template-columns: 1fr 1fr 1fr; }
}
.philosophy-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 2.5rem;
border-radius: 1rem;
transition: background 0.4s ease;
}
.philosophy-card:hover {
background: rgba(220,198,242,0.3);
}
.philosophy-icon-wrap {
width: 64px;
height: 64px;
border-radius: 50%;
background: rgba(220,198,242,0.5);
display: flex;
align-items: center;
justify-content: center;
color: var(--brand-purple);
margin-bottom: 1.5rem;
}
.philosophy-card h3 {
font-family: var(--font-display);
font-size: 1.1rem;
letter-spacing: 0.1em;
color: var(--brand-purple);
margin-bottom: 0.75rem;
}
.philosophy-card p {
font-family: var(--font-sans);
color: var(--brand-muted);
line-height: 1.7;
}
/* FEATURED PRODUCTS */
.home-products {
padding: 6rem 1.5rem;
background: var(--brand-light-lavender);
}
.home-products-header {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 4rem;
}
@media (min-width: 768px) {
.home-products-header {
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
}
}
.home-products h2 {
font-family: var(--font-serif);
font-size: clamp(2rem, 4vw, 3rem);
color: var(--brand-purple);
margin-bottom: 0.5rem;
}
.view-all-link {
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--brand-gold);
text-decoration: none;
border-bottom: 1px solid transparent;
padding-bottom: 2px;
transition: color 0.3s ease, border-color 0.3s ease;
white-space: nowrap;
}
.view-all-link:hover {
color: var(--brand-purple);
border-color: var(--brand-purple);
}
.product-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 1280px;
margin: 0 auto;
}
@media (min-width: 600px) { .product-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .product-grid { grid-template-columns: repeat(4, 1fr); } }
.product-card {
cursor: pointer;
}
.product-img-wrap {
position: relative;
aspect-ratio: 1 / 1;
overflow: hidden;
background: var(--brand-card);
border-radius: 4px;
margin-bottom: 1.25rem;
}
.product-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.7s ease;
}
.product-card:hover .product-img-wrap img {
transform: scale(1.05);
}
.product-overlay {
position: absolute;
inset: 0;
background: rgba(91,30,140,0.22);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
transition: opacity 0.5s ease;
}
.product-card:hover .product-overlay {
opacity: 1;
}
.product-overlay-btn {
padding: 0.75rem 1.5rem;
background: rgba(239,228,250,0.92);
color: var(--brand-purple);
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
border: none;
cursor: pointer;
transform: translateY(16px);
transition: transform 0.5s ease, background 0.3s ease;
}
.product-card:hover .product-overlay-btn {
transform: translateY(0);
}
.product-name {
font-family: var(--font-serif);
font-size: 1.4rem;
color: var(--brand-purple);
margin-bottom: 0.25rem;
}
.product-price {
font-family: var(--font-sans);
color: var(--brand-gold);
font-weight: 500;
}
/* QUOTE BANNER */
.home-quote {
padding: 8rem 1.5rem;
background: var(--brand-purple);
position: relative;
overflow: hidden;
text-align: center;
}
.home-quote-texture {
position: absolute;
inset: 0;
background-image: url('https://www.transparenttextures.com/patterns/stardust.png');
opacity: 0.1;
}
.home-quote blockquote {
font-family: var(--font-serif);
font-style: italic;
font-size: clamp(1.4rem, 3.5vw, 2.5rem);
color: #ffffff;
line-height: 1.6;
max-width: 900px;
margin: 1.5rem auto 2rem;
position: relative;
z-index: 1;
}
.home-quote-divider {
width: 64px;
height: 1px;
background: var(--brand-gold);
margin: 0 auto;
position: relative;
z-index: 1;
}
/* NEWSLETTER */
.home-newsletter {
padding: 6rem 1.5rem;
background: var(--brand-card);
text-align: center;
}
.home-newsletter h2 {
font-family: var(--font-serif);
font-size: clamp(1.8rem, 3.5vw, 2.5rem);
color: var(--brand-purple);
margin-bottom: 0.75rem;
}
.home-newsletter p {
font-family: var(--font-sans);
color: var(--brand-muted);
margin-bottom: 2rem;
max-width: 520px;
margin-left: auto;
margin-right: auto;
}
.newsletter-form {
display: flex;
flex-direction: column;
gap: 1rem;
max-width: 440px;
margin: 0 auto;
}
@media (min-width: 480px) {
.newsletter-form { flex-direction: row; }
}
.newsletter-input {
flex: 1;
padding: 0.85rem 1.25rem;
background: var(--brand-light-lavender);
border: 1px solid var(--brand-border);
font-family: var(--font-sans);
font-size: 0.95rem;
outline: none;
transition: border-color 0.3s ease;
}
.newsletter-input:focus {
border-color: var(--brand-gold);
box-shadow: 0 0 0 2px rgba(212,162,76,0.15);
}
.newsletter-btn {
padding: 0.85rem 2rem;
background: var(--brand-purple);
color: #ffffff;
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
border: none;
cursor: pointer;
transition: background 0.3s ease;
white-space: nowrap;
}
.newsletter-btn:hover {
background: var(--brand-gold);
}

View File

@ -1,154 +1,96 @@
import { motion } from "framer-motion";
import { Link } from "wouter";
import { Flame, Heart, Leaf, Sparkles } from "lucide-react";
import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import lavenderImage from "@/assets/images/product-lavender.png";
import roseImage from "@/assets/images/product-rose.png";
import eucalyptusImage from "@/assets/images/product-eucalyptus.png";
import oudImage from "@/assets/images/product-oud.png";
import "./home.css";
const logoPath = "/opengraph.jpg";
export default function Home() {
const fadeInUp = {
const fadeInUp = {
hidden: { opacity: 0, y: 40 },
visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: "easeOut" } },
};
};
const stagger = {
const stagger = {
visible: { transition: { staggerChildren: 0.2 } },
};
};
export default function Home() {
return (
<div className="flex flex-col w-full">
{/* Hero Section */}
<section className="relative min-h-[90vh] flex flex-col items-center justify-center overflow-hidden px-4 py-20">
<div className="absolute inset-0 bg-gradient-to-b from-secondary/30 to-background -z-10" />
<motion.div
initial="hidden"
animate="visible"
variants={stagger}
className="flex flex-col items-center text-center max-w-4xl mx-auto"
>
<motion.div variants={fadeInUp} className="mb-8 relative">
<div className="w-32 h-32 md:w-48 md:h-48 rounded-full overflow-hidden border-4 border-accent/30 shadow-[0_0_40px_rgba(212,162,76,0.3)]">
<img src={logoPath} alt="Mani Candles Co." className="w-full h-full object-cover" />
<Box>
{/* ── Hero ── */}
<section className="home-hero">
<div className="home-hero-bg" />
<motion.div initial="hidden" animate="visible" variants={stagger}>
<motion.div variants={fadeInUp} style={{ position: "relative" }}>
<div className="home-hero-logo-wrap">
<img src={logoPath} alt="Mani Candles Co." style={{ width: "100%", height: "100%", objectFit: "cover" }} />
</div>
<div className="absolute -top-4 -right-4 text-accent animate-flame">
<Flame size={40} fill="currentColor" className="drop-shadow-[0_0_15px_rgba(255,211,107,0.8)]" />
<div className="home-hero-flame animate-flame">
<Flame size={40} fill="currentColor" style={{ filter: "drop-shadow(0 0 12px rgba(255,211,107,0.8))" }} />
</div>
</motion.div>
<motion.h1 variants={fadeInUp} className="font-serif text-5xl md:text-7xl lg:text-8xl text-primary mb-6 drop-shadow-sm">
Mani Candles Co.
</motion.h1>
<motion.h1 variants={fadeInUp}>Mani Candles Co.</motion.h1>
<motion.p variants={fadeInUp} className="font-script text-3xl md:text-5xl text-accent mb-12">
<motion.span variants={fadeInUp} className="home-hero-tagline">
Handmade Natural Heartmade
</motion.p>
</motion.span>
<motion.div variants={fadeInUp}>
<Link
href="/products"
className="inline-flex items-center justify-center px-8 py-4 bg-primary text-primary-foreground font-display tracking-widest uppercase text-sm rounded-none border border-transparent hover:border-accent hover:bg-transparent hover:text-primary transition-all duration-500 ease-out shadow-lg hover:shadow-[0_0_20px_rgba(212,162,76,0.4)]"
>
Shop the Collection
</Link>
<Link href="/products" className="hero-cta">Shop the Collection</Link>
</motion.div>
</motion.div>
</section>
{/* Why Mani Section */}
<section className="py-24 bg-card px-6">
<div className="max-w-6xl mx-auto">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={stagger}
className="text-center mb-16"
>
<motion.h2 variants={fadeInUp} className="font-serif text-4xl md:text-5xl text-primary mb-4">
The Mani Philosophy
</motion.h2>
<motion.div variants={fadeInUp} className="w-24 h-1 bg-accent mx-auto" />
{/* ── Philosophy ── */}
<section className="home-philosophy">
<Container maxWidth="lg">
<motion.div initial="hidden" whileInView="visible" viewport={{ once: true, margin: "-100px" }} variants={stagger}>
<motion.h2 variants={fadeInUp}>The Mani Philosophy</motion.h2>
<div className="section-divider" />
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
<div className="philosophy-grid">
{[
{ Icon: Leaf, title: "Natural", desc: "Crafted with pure soy wax and essential oils. Clean burns that respect your home and our earth." },
{ Icon: Sparkles, title: "Handmade", desc: "Poured by hand in small batches to ensure unparalleled quality and attention to detail." },
{ Icon: Heart, title: "Heartmade", desc: "Every candle is infused with love and intention, designed to bring warmth to your sacred spaces." },
].map(({ Icon, title, desc }, i) => (
<motion.div
key={title}
className="philosophy-card"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="flex flex-col items-center text-center p-8 rounded-2xl hover:bg-secondary/20 transition-colors duration-500"
variants={{ hidden: { opacity: 0, y: 30 }, visible: { opacity: 1, y: 0, transition: { delay: i * 0.2, duration: 0.6 } } }}
>
<div className="w-16 h-16 rounded-full bg-secondary/50 flex items-center justify-center text-primary mb-6">
<Leaf size={32} />
</div>
<h3 className="font-display tracking-wider text-xl text-primary mb-4">Natural</h3>
<p className="font-sans text-muted-foreground leading-relaxed">
Crafted with pure soy wax and essential oils. Clean burns that respect your home and our earth.
</p>
<div className="philosophy-icon-wrap"><Icon size={32} /></div>
<h3>{title}</h3>
<p>{desc}</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="flex flex-col items-center text-center p-8 rounded-2xl hover:bg-secondary/20 transition-colors duration-500"
>
<div className="w-16 h-16 rounded-full bg-secondary/50 flex items-center justify-center text-primary mb-6">
<Sparkles size={32} />
</div>
<h3 className="font-display tracking-wider text-xl text-primary mb-4">Handmade</h3>
<p className="font-sans text-muted-foreground leading-relaxed">
Poured by hand in small batches to ensure unparalleled quality and attention to detail.
</p>
</motion.div>
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="flex flex-col items-center text-center p-8 rounded-2xl hover:bg-secondary/20 transition-colors duration-500"
>
<div className="w-16 h-16 rounded-full bg-secondary/50 flex items-center justify-center text-primary mb-6">
<Heart size={32} />
</div>
<h3 className="font-display tracking-wider text-xl text-primary mb-4">Heartmade</h3>
<p className="font-sans text-muted-foreground leading-relaxed">
Every candle is infused with love and intention, designed to bring warmth to your sacred spaces.
</p>
</motion.div>
</div>
))}
</div>
</Container>
</section>
{/* Featured Products */}
<section className="py-24 px-6 bg-background">
<div className="max-w-7xl mx-auto">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="flex flex-col md:flex-row justify-between items-end mb-16 gap-6"
>
{/* ── Featured Products ── */}
<section className="home-products">
<Container maxWidth="xl">
<motion.div initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeInUp} className="home-products-header">
<div>
<h2 className="font-serif text-4xl md:text-5xl text-primary mb-4">Curated Favorites</h2>
<div className="w-24 h-1 bg-accent" />
<h2>Curated Favorites</h2>
<div className="section-divider" style={{ margin: "0.5rem 0 0", width: 80 }} />
</div>
<Link
href="/products"
className="font-display tracking-widest uppercase text-sm text-accent hover:text-primary transition-colors border-b border-transparent hover:border-primary pb-1"
>
View All Candles
</Link>
<Link href="/products" className="view-all-link">View All Candles</Link>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="product-grid">
{[
{ id: 1, name: "Lavender Dream", image: lavenderImage, price: "$28" },
{ id: 2, name: "Rose & Vanilla", image: roseImage, price: "$32" },
@ -157,82 +99,49 @@ export default function Home() {
].map((product, i) => (
<motion.div
key={product.id}
className="product-card"
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={{
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: { delay: i * 0.1, duration: 0.6 } }
}}
className="group cursor-pointer"
variants={{ hidden: { opacity: 0, y: 20 }, visible: { opacity: 1, y: 0, transition: { delay: i * 0.1, duration: 0.6 } } }}
>
<div className="relative aspect-square overflow-hidden mb-6 bg-card">
<img
src={product.image}
alt={product.name}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-primary/20 opacity-0 group-hover:opacity-100 transition-opacity duration-500 flex items-center justify-center backdrop-blur-[2px]">
<span className="px-6 py-3 bg-background/90 text-primary font-display tracking-widest text-sm uppercase translate-y-4 group-hover:translate-y-0 transition-transform duration-500">
Quick Add
</span>
<div className="product-img-wrap">
<img src={product.image} alt={product.name} />
<div className="product-overlay">
<button className="product-overlay-btn">Quick Add</button>
</div>
</div>
<h3 className="font-serif text-2xl text-primary mb-2">{product.name}</h3>
<p className="font-sans text-accent">{product.price}</p>
<h3 className="product-name">{product.name}</h3>
<p className="product-price">{product.price}</p>
</motion.div>
))}
</div>
</div>
</Container>
</section>
{/* Quote Section */}
<section className="py-32 px-6 bg-primary relative overflow-hidden">
<div className="absolute inset-0 opacity-10 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')]" />
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="max-w-4xl mx-auto text-center relative z-10"
>
<Flame className="text-accent mx-auto mb-8 animate-flame" size={48} />
<p className="font-serif italic text-3xl md:text-5xl text-primary-foreground leading-relaxed mb-8">
{/* ── Quote ── */}
<section className="home-quote">
<div className="home-quote-texture" />
<motion.div initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeInUp}>
<Flame className="animate-flame" size={48} style={{ color: "var(--brand-gold)", margin: "0 auto 1.5rem", display: "block" }} />
<blockquote>
"A candle loses nothing by lighting another candle. It only makes the room warmer, brighter, and more beautiful."
</p>
<div className="w-16 h-[1px] bg-accent mx-auto" />
</blockquote>
<div className="home-quote-divider" />
</motion.div>
</section>
{/* Newsletter */}
<section className="py-24 px-6 bg-card text-center">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
variants={fadeInUp}
className="max-w-2xl mx-auto"
>
<h2 className="font-serif text-4xl text-primary mb-4">Join Our Sanctuary</h2>
<p className="font-sans text-muted-foreground mb-8">
Subscribe to receive gentle updates on new collections, exclusive access, and a little light in your inbox.
</p>
<form className="flex flex-col sm:flex-row gap-4 max-w-md mx-auto" onSubmit={(e) => e.preventDefault()}>
<input
type="email"
placeholder="Your email address"
className="flex-grow px-6 py-3 bg-background border border-border focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent font-sans rounded-none transition-all"
required
/>
<button
type="submit"
className="px-8 py-3 bg-primary text-primary-foreground font-display tracking-widest text-sm uppercase hover:bg-accent transition-colors duration-300"
>
Subscribe
</button>
{/* ── Newsletter ── */}
<section className="home-newsletter">
<motion.div initial="hidden" whileInView="visible" viewport={{ once: true }} variants={fadeInUp}>
<h2>Join Our Sanctuary</h2>
<p>Subscribe to receive gentle updates on new collections, exclusive access, and a little light in your inbox.</p>
<form className="newsletter-form" onSubmit={(e) => e.preventDefault()}>
<input type="email" className="newsletter-input" placeholder="Your email address" required />
<button type="submit" className="newsletter-btn">Subscribe</button>
</form>
</motion.div>
</section>
</div>
</Box>
);
}

173
src/pages/products.css Normal file
View File

@ -0,0 +1,173 @@
/* ─── products.css ───────────────────────────────────────── */
/* HERO */
.products-hero {
padding: 6rem 1.5rem;
background: var(--brand-card);
text-align: center;
}
.products-hero h1 {
font-family: var(--font-serif);
font-size: clamp(2.5rem, 6vw, 4rem);
color: var(--brand-purple);
margin-bottom: 1rem;
}
.products-hero p {
font-family: var(--font-sans);
font-size: 1.05rem;
color: var(--brand-muted);
line-height: 1.8;
max-width: 640px;
margin: 0 auto 2rem;
}
/* FILTERS */
.products-body {
padding: 5rem 1.5rem;
background: var(--brand-light-lavender);
}
.filter-bar {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
margin-bottom: 4rem;
}
.filter-btn {
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
padding: 0.5rem 1.5rem;
background: none;
border: none;
border-bottom: 2px solid transparent;
color: var(--brand-muted);
cursor: pointer;
transition: color 0.3s ease, border-color 0.3s ease;
}
.filter-btn:hover {
color: var(--brand-purple);
}
.filter-btn.active {
color: var(--brand-purple);
border-bottom-color: var(--brand-gold);
}
/* PRODUCT GRID */
.products-grid {
display: grid;
grid-template-columns: 1fr;
gap: 3rem 2rem;
max-width: 1280px;
margin: 0 auto;
}
@media (min-width: 640px) { .products-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 1024px) { .products-grid { grid-template-columns: repeat(4, 1fr); } }
.prod-card {
display: flex;
flex-direction: column;
cursor: pointer;
}
.prod-img-wrap {
position: relative;
aspect-ratio: 4 / 5;
overflow: hidden;
background: var(--brand-card);
border: 1px solid var(--brand-border);
margin-bottom: 1.25rem;
}
.prod-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 1s ease;
}
.prod-card:hover .prod-img-wrap img {
transform: scale(1.05);
}
.prod-overlay {
position: absolute;
inset: 0;
background: rgba(91,30,140,0.3);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(2px);
transition: opacity 0.5s ease;
}
.prod-card:hover .prod-overlay {
opacity: 1;
}
.prod-overlay-btn {
padding: 0.75rem 2rem;
background: var(--brand-light-lavender);
color: var(--brand-purple);
font-family: var(--font-display);
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
border: none;
cursor: pointer;
transform: translateY(32px);
transition: transform 0.5s ease, background 0.3s ease, color 0.3s ease;
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.prod-card:hover .prod-overlay-btn {
transform: translateY(0);
}
.prod-overlay-btn:hover {
background: var(--brand-gold);
color: #ffffff;
}
.prod-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.prod-name {
font-family: var(--font-serif);
font-size: 1.4rem;
color: var(--brand-purple);
margin-bottom: 0.25rem;
}
.prod-scent {
font-family: var(--font-sans);
font-size: 0.85rem;
color: var(--brand-muted);
line-height: 1.5;
margin-bottom: 0.75rem;
/* two-line clamp */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.prod-price {
font-family: var(--font-sans);
font-weight: 500;
font-size: 1.05rem;
color: var(--brand-gold);
}

63
src/theme.ts Normal file
View File

@ -0,0 +1,63 @@
import { createTheme } from "@mui/material/styles";
const theme = createTheme({
palette: {
mode: "light",
primary: {
main: "#5B1E8C", // deep brand purple
light: "#DCC6F2", // lavender
dark: "#3A1260",
contrastText: "#ffffff",
},
secondary: {
main: "#D4A24C", // brand gold / accent
light: "#FFD36B",
dark: "#A07830",
contrastText: "#ffffff",
},
background: {
default: "#EFE4FA", // light lavender bg
paper: "#F8F2FD", // card bg
},
text: {
primary: "#2D0D52",
secondary: "#7B5A9E",
},
divider: "#D5BEF0",
},
typography: {
fontFamily: "'Inter', sans-serif",
h1: { fontFamily: "'Cormorant Garamond', serif" },
h2: { fontFamily: "'Cormorant Garamond', serif" },
h3: { fontFamily: "'Cormorant Garamond', serif" },
h4: { fontFamily: "'Cormorant Garamond', serif" },
h5: { fontFamily: "'Cormorant Garamond', serif" },
h6: { fontFamily: "'Cormorant Garamond', serif" },
subtitle1: { fontFamily: "'Cinzel', serif" },
subtitle2: { fontFamily: "'Cinzel', serif" },
},
shape: {
borderRadius: 12,
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
borderRadius: 0,
fontFamily: "'Cinzel', serif",
letterSpacing: "0.15em",
},
},
},
MuiAppBar: {
styleOverrides: {
root: {
boxShadow: "none",
},
},
},
},
});
export default theme;

View File

@ -1,13 +1,12 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";
import { fileURLToPath } from "url";
const projectRoot = fileURLToPath(new URL(".", import.meta.url));
export default defineConfig({
plugins: [react(), tailwindcss()],
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(projectRoot, "src"),