Working state: Serbian at root (/), English at /en, proper i18n structure
This commit is contained in:
125
src/components/cart/CartDrawer.tsx
Normal file
125
src/components/cart/CartDrawer.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useCartStore } from "@/stores/cartStore";
|
||||
import { formatPrice } from "@/lib/woocommerce";
|
||||
|
||||
export default function CartDrawer() {
|
||||
const { items, isOpen, closeCart, removeItem, updateQuantity, getTotal } = useCartStore();
|
||||
|
||||
const total = getTotal();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/50 z-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={closeCart}
|
||||
/>
|
||||
<motion.div
|
||||
className="fixed top-0 right-0 bottom-0 w-full max-w-md bg-white z-50 shadow-xl flex flex-col"
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "tween", duration: 0.3 }}
|
||||
>
|
||||
<div className="flex items-center justify-between p-6 border-b border-border/30">
|
||||
<h2 className="text-xl font-serif">Your Cart</h2>
|
||||
<button
|
||||
onClick={closeCart}
|
||||
className="p-2"
|
||||
aria-label="Close cart"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{items.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-foreground-muted mb-6">Your cart is empty</p>
|
||||
<Link
|
||||
href="/en/products"
|
||||
onClick={closeCart}
|
||||
className="inline-block px-6 py-3 bg-foreground text-white"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex gap-4">
|
||||
<div className="w-20 h-20 bg-background-ice relative flex-shrink-0">
|
||||
{item.image && (
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-serif text-sm">{item.name}</h3>
|
||||
<p className="text-foreground-muted text-sm mt-1">
|
||||
{formatPrice(item.price)}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
className="w-8 h-8 border border-border flex items-center justify-center"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
className="w-8 h-8 border border-border flex items-center justify-center"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeItem(item.id)}
|
||||
className="ml-auto text-foreground-muted hover:text-red-500"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{items.length > 0 && (
|
||||
<div className="p-6 border-t border-border/30">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="font-serif">Subtotal</span>
|
||||
<span className="font-serif text-lg">{formatPrice(total.toString())}</span>
|
||||
</div>
|
||||
<a
|
||||
href="https://manoonoils.com/checkout"
|
||||
className="block w-full py-3 bg-foreground text-white text-center font-medium hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Checkout
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
51
src/components/home/BrandStory.tsx
Normal file
51
src/components/home/BrandStory.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function BrandStory() {
|
||||
return (
|
||||
<section className="py-20 px-4 bg-background-ice">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-serif mb-6">
|
||||
Our Story
|
||||
</h2>
|
||||
<div className="space-y-4 text-foreground-muted leading-relaxed">
|
||||
<p>
|
||||
ManoonOils was born from a passion for natural beauty and the belief
|
||||
that the best skincare comes from nature itself.
|
||||
</p>
|
||||
<p>
|
||||
Our premium oils are carefully crafted using only the finest natural
|
||||
ingredients, handpicked for their nourishing and restorative properties.
|
||||
</p>
|
||||
<p>
|
||||
Each product is made with love and attention to detail, ensuring
|
||||
that you receive the very best for your hair and skin.
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="relative aspect-square bg-gradient-to-br from-accent/20 to-accent-dark/20"
|
||||
initial={{ opacity: 0, x: 30 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
<div className="absolute inset-8 border border-foreground/10" />
|
||||
<div className="absolute inset-12 bg-white/50 backdrop-blur-sm flex items-center justify-center">
|
||||
<span className="font-serif text-3xl text-foreground/60">ManoonOils</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
75
src/components/home/Hero.tsx
Normal file
75
src/components/home/Hero.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="relative h-screen min-h-[600px] flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background-ice/50 to-background" />
|
||||
|
||||
<div className="absolute inset-0 opacity-5">
|
||||
<div className="absolute top-20 left-10 w-64 h-64 rounded-full bg-accent blur-3xl" />
|
||||
<div className="absolute bottom-20 right-10 w-96 h-96 rounded-full bg-accent-dark blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
<span className="inline-block text-sm tracking-[0.3em] text-foreground-muted mb-6">
|
||||
PREMIUM NATURAL OILS
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
className="text-5xl md:text-7xl lg:text-8xl font-serif mb-6 leading-tight"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.4 }}
|
||||
>
|
||||
ManoonOils
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
className="text-xl md:text-2xl text-foreground-muted mb-10 font-light"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6 }}
|
||||
>
|
||||
For hair and skin care
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
>
|
||||
<Link
|
||||
href="/en/products"
|
||||
className="inline-block px-10 py-4 bg-foreground text-white text-lg tracking-wide hover:bg-accent-dark transition-colors duration-300"
|
||||
>
|
||||
Shop Now
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.8 }}
|
||||
>
|
||||
<div className="w-6 h-10 border-2 border-foreground/30 rounded-full flex justify-center pt-2">
|
||||
<motion.div
|
||||
className="w-1 h-2 bg-foreground/50 rounded-full"
|
||||
animate={{ y: [0, 12, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5 }}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
68
src/components/home/Newsletter.tsx
Normal file
68
src/components/home/Newsletter.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function Newsletter() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-20 px-4 bg-foreground text-white">
|
||||
<div className="max-w-3xl mx-auto text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-3xl md font-serif mb-:text-4xl4">
|
||||
Stay Updated
|
||||
</h2>
|
||||
<p className="text-white/60 mb-8">
|
||||
Subscribe to receive news about new products and special offers
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{submitted ? (
|
||||
<motion.p
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-accent"
|
||||
>
|
||||
Thank you for subscribing!
|
||||
</motion.p>
|
||||
) : (
|
||||
<motion.form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-col sm:flex-row gap-4 max-w-lg mx-auto"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="flex-1 px-6 py-3 bg-white/10 border border-white/20 text-white placeholder:text-white/40 focus:outline-none focus:border-accent"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-8 py-3 bg-accent hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</motion.form>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
38
src/components/home/ProductShowcase.tsx
Normal file
38
src/components/home/ProductShowcase.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { WooProduct } from "@/lib/woocommerce";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
interface ProductShowcaseProps {
|
||||
products: WooProduct[];
|
||||
}
|
||||
|
||||
export default function ProductShowcase({ products }: ProductShowcaseProps) {
|
||||
if (!products || products.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-serif mb-4">Our Products</h2>
|
||||
<p className="text-foreground-muted max-w-2xl mx-auto">
|
||||
Discover our premium collection of natural oils for hair and skin care
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
63
src/components/home/Testimonials.tsx
Normal file
63
src/components/home/Testimonials.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "My hair has never looked better. The Morning Glow oil is a miracle in a bottle!",
|
||||
author: "Ana M.",
|
||||
},
|
||||
{
|
||||
quote: "Finally found products that truly work. My skin feels amazing every day.",
|
||||
author: "Jelena K.",
|
||||
},
|
||||
{
|
||||
quote: "The Anti-age Serum is my go-to. Love the natural ingredients.",
|
||||
author: "Milica P.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function Testimonials() {
|
||||
return (
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-serif mb-4">
|
||||
What Our Customers Say
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="bg-white p-8 shadow-sm"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
>
|
||||
<svg
|
||||
className="w-8 h-8 text-accent mb-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
|
||||
</svg>
|
||||
<p className="font-serif text-lg italic mb-4 text-foreground-muted">
|
||||
{testimonial.quote}
|
||||
</p>
|
||||
<p className="font-medium">— {testimonial.author}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
35
src/components/home/TickerBar.tsx
Normal file
35
src/components/home/TickerBar.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function TickerBar() {
|
||||
const items = [
|
||||
"Free shipping on orders over 3000 RSD",
|
||||
"Natural ingredients",
|
||||
"Cruelty-free",
|
||||
"Handmade with love",
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-foreground text-white py-3 overflow-hidden">
|
||||
<motion.div
|
||||
className="flex whitespace-nowrap"
|
||||
animate={{ x: ["0%", "-50%"] }}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 20,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{[...items, ...items, ...items, ...items].map((item, index) => (
|
||||
<span key={index} className="mx-8 text-sm tracking-wide">
|
||||
{item}
|
||||
</span>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/layout/Footer.tsx
Normal file
66
src/components/layout/Footer.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-background-ice border-t border-border/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="md:col-span-2">
|
||||
<h3 className="text-2xl font-serif mb-4">ManoonOils</h3>
|
||||
<p className="text-foreground-muted max-w-md">
|
||||
Premium natural oils for hair and skin care. Crafted with love for your daily beauty routine.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif mb-4">Quick Links</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/en/products" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Products
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/en/about" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
About Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif mb-4">Customer Service</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Shipping Info
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/en/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Returns
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://manoonoils.com" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
WooCommerce Store
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/30 mt-12 pt-8 text-center text-foreground-muted text-sm">
|
||||
<p>© {currentYear} ManoonOils. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
79
src/components/layout/Header.tsx
Normal file
79
src/components/layout/Header.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useCartStore } from "@/stores/cartStore";
|
||||
import { formatPrice } from "@/lib/woocommerce";
|
||||
import MobileMenu from "./MobileMenu";
|
||||
import CartDrawer from "@/components/cart/CartDrawer";
|
||||
|
||||
export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const { items, toggleCart } = useCartStore();
|
||||
|
||||
const itemCount = items.reduce((count, item) => count + item.quantity, 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="fixed top-0 left-0 right-0 z-50 bg-white/90 backdrop-blur-md">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16 md:h-20">
|
||||
<button
|
||||
className="md:hidden p-2"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Link href="/" className="flex-shrink-0">
|
||||
<img
|
||||
src="/manoon-logo.jpg"
|
||||
alt="ManoonOils"
|
||||
className="h-8 w-[124px] md:h-10 md:w-[154px]"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
<Link href="/en/products" className="text-foreground hover:text-accent-dark transition-colors">
|
||||
Products
|
||||
</Link>
|
||||
<Link href="/en/about" className="text-foreground hover:text-accent-dark transition-colors">
|
||||
About
|
||||
</Link>
|
||||
<Link href="/en/contact" className="text-foreground hover:text-accent-dark transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
className="p-2 relative"
|
||||
onClick={toggleCart}
|
||||
aria-label="Open cart"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" />
|
||||
</svg>
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-accent-dark text-white text-xs w-5 h-5 rounded-full flex items-center justify-center">
|
||||
{itemCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<MobileMenu onClose={() => setMobileMenuOpen(false)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<CartDrawer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
64
src/components/layout/MobileMenu.tsx
Normal file
64
src/components/layout/MobileMenu.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
|
||||
interface MobileMenuProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export default function MobileMenu({ onClose }: MobileMenuProps) {
|
||||
const menuItems = [
|
||||
{ href: "/en", label: "Home" },
|
||||
{ href: "/en/products", label: "Products" },
|
||||
{ href: "/en/about", label: "About" },
|
||||
{ href: "/en/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/50 z-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 bottom-0 w-80 bg-white z-50 shadow-xl"
|
||||
initial={{ x: "-100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "-100%" }}
|
||||
transition={{ type: "tween", duration: 0.3 }}
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-serif">ManoonOils</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="space-y-4">
|
||||
{menuItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="block text-xl font-serif py-2 border-b border-border/30"
|
||||
onClick={onClose}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
50
src/components/product/ProductCard.tsx
Normal file
50
src/components/product/ProductCard.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: WooProduct;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, index = 0 }: ProductCardProps) {
|
||||
const image = getProductImage(product);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Link href={`/products/${product.slug}`} className="group block">
|
||||
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden mb-4">
|
||||
{image && (
|
||||
<Image
|
||||
src={image}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
{product.stock_status === "outofstock" && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||
<span className="text-white font-medium">Out of Stock</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif text-lg mb-1 group-hover:text-accent-dark transition-colors">
|
||||
{product.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-foreground-muted">
|
||||
{product.price ? formatPrice(product.price) : "Contact for price"}
|
||||
</p>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
182
src/components/product/ProductDetail.tsx
Normal file
182
src/components/product/ProductDetail.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
|
||||
import { useCartStore } from "@/stores/cartStore";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
interface ProductDetailProps {
|
||||
product: WooProduct;
|
||||
relatedProducts: WooProduct[];
|
||||
}
|
||||
|
||||
export default function ProductDetail({ product, relatedProducts }: ProductDetailProps) {
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [activeTab, setActiveTab] = useState<"details" | "ingredients" | "usage">("details");
|
||||
const addItem = useCartStore((state) => state.addItem);
|
||||
|
||||
const images = product.images?.length > 0
|
||||
? product.images
|
||||
: [{ id: 0, src: "/placeholder-product.jpg", alt: product.name }];
|
||||
|
||||
const handleAddToCart = () => {
|
||||
addItem({
|
||||
id: product.id,
|
||||
name: product.name,
|
||||
price: product.price || product.regular_price,
|
||||
quantity,
|
||||
image: images[0]?.src || "",
|
||||
sku: product.sku || "",
|
||||
});
|
||||
};
|
||||
|
||||
const stripHtml = (html: string) => {
|
||||
return html.replace(/<[^>]*>/g, "");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="py-12 md:py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<div className="relative aspect-square bg-background-ice mb-4">
|
||||
{images[selectedImage] && (
|
||||
<Image
|
||||
src={images[selectedImage].src}
|
||||
alt={images[selectedImage].alt || product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{images.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => setSelectedImage(index)}
|
||||
className={`relative w-20 h-20 flex-shrink-0 ${
|
||||
selectedImage === index ? "ring-2 ring-foreground" : ""
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src={image.src}
|
||||
alt={image.alt || product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<h1 className="text-3xl md:text-4xl font-serif mb-4">
|
||||
{product.name}
|
||||
</h1>
|
||||
|
||||
<p className="text-2xl text-foreground-muted mb-6">
|
||||
{product.price ? formatPrice(product.price) : "Contact for price"}
|
||||
</p>
|
||||
|
||||
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted">
|
||||
<p>{stripHtml(product.short_description || product.description.slice(0, 200))}</p>
|
||||
</div>
|
||||
|
||||
{product.stock_status === "instock" ? (
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
<div className="flex items-center border border-border">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="px-4 py-3"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="px-4 py-3">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="px-4 py-3"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-3 bg-red-50 text-red-600 text-center mb-8">
|
||||
Out of Stock
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-border/30">
|
||||
<div className="flex border-b border-border/30">
|
||||
{(["details", "ingredients", "usage"] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 py-4 font-medium capitalize ${
|
||||
activeTab === tab
|
||||
? "border-b-2 border-foreground"
|
||||
: "text-foreground-muted"
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="py-6 text-foreground-muted">
|
||||
{activeTab === "details" && (
|
||||
<p>{stripHtml(product.description)}</p>
|
||||
)}
|
||||
{activeTab === "ingredients" && (
|
||||
<p>Natural ingredients - Contact for detailed information.</p>
|
||||
)}
|
||||
{activeTab === "usage" && (
|
||||
<p>Apply to clean skin or hair. Use daily for best results.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{relatedProducts.length > 0 && (
|
||||
<section className="py-12 px-4 bg-background-ice">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-serif text-center mb-8">
|
||||
You May Also Like
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{relatedProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
24
src/components/providers/LocaleProvider.tsx
Normal file
24
src/components/providers/LocaleProvider.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function LocaleProvider({
|
||||
children,
|
||||
locale,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
locale: string;
|
||||
}) {
|
||||
const locales = ["en", "sr"];
|
||||
if (!locales.includes(locale)) notFound();
|
||||
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user