12 Commits

Author SHA1 Message Date
Unchained
ace1ac104e fix: cart delete mutation and console warnings
Some checks are pending
Build and Deploy / build (push) Waiting to run
- Fix checkoutLinesDelete mutation: use 'id' param and 'linesIds' instead of 'lineIds'
- Fix viewport metadata warning: move to separate viewport export in layout.tsx
- Add sizes prop to checkout Image with fill
- Fix CartDrawer init checkout useEffect to prevent re-render loops
- Various product detail improvements
2026-03-23 13:49:14 +02:00
Unchained
7f603c83e9 fix: correct checkoutLinesDelete parameter name lines -> lineIds
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-23 13:20:37 +02:00
Unchained
0e9ad28dcf Fix: remove priority attribute from regular img tag
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-23 11:18:29 +02:00
Unchained
70d6cfc9a7 Fix product images and add carousel; add transformation carousel on mobile
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-23 11:14:15 +02:00
Unchained
f3d60d3c5b Fix product images: use fill with aspect-square container
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-23 10:45:10 +02:00
Unchained
7ecd9c2e22 Fix product images on mobile: use explicit width/height instead of fill
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 21:27:38 +02:00
Unchained
e9b95c44b9 Fix hero section on mobile - use background image instead of broken video
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 21:19:47 +02:00
Unchained
8a418be7c3 Fix mobile responsiveness: viewport meta, standard Tailwind star colors
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 21:05:29 +02:00
Unchained
ba25261a3c Premium design updates: gold accents, improved sections, verified review badges, reordered homepage layout
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 17:08:06 +02:00
Unchained
77e19d841b Change review stars to gold color (#FFD700)
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 16:16:42 +02:00
Unchained
43d662b54e Add padding to header and ensure mobile menu works
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 12:42:38 +02:00
Unchained
625bd727d3 Fix product images showing full picture instead of cropped
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 12:05:40 +02:00
19 changed files with 378 additions and 158 deletions

View File

@@ -442,6 +442,7 @@ export default function CheckoutPage() {
src={line.variant.product.media[0].url}
alt={line.variant.product.name}
fill
sizes="64px"
className="object-cover"
/>
)}

View File

@@ -1,5 +1,5 @@
import "./globals.css";
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import ErrorBoundary from "@/components/providers/ErrorBoundary";
export const metadata: Metadata = {
@@ -7,7 +7,7 @@ export const metadata: Metadata = {
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
template: "%s | ManoonOils",
},
description: "Discover our premium collection of natural oils for hair and skin care. Handmade with love.",
description: "Discover our premium collection of natural oils for hair and skin care.",
robots: "index, follow",
openGraph: {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,6 +17,12 @@ export const metadata: Metadata = {
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 5,
};
// Suppress extension-caused hydration warnings
const suppressHydrationWarning = true;

View File

@@ -5,7 +5,7 @@ import HeroVideo from "@/components/home/HeroVideo";
import ProductCard from "@/components/product/ProductCard";
import TrustBadges from "@/components/home/TrustBadges";
import AsSeenIn from "@/components/home/AsSeenIn";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import ProductReviews from "@/components/product/ProductReviews";
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
import ProblemSection from "@/components/home/ProblemSection";
import HowItWorks from "@/components/home/HowItWorks";
@@ -36,15 +36,21 @@ export default async function Homepage() {
{/* Hero Section with Video Background */}
<HeroVideo />
{/* Trust Badges */}
<TrustBadges />
{/* As Seen In */}
<AsSeenIn />
{/* Testimonials Section */}
<ProductReviews />
{/* Trust Badges */}
<TrustBadges />
{/* Problem Section - Create empathy */}
<ProblemSection />
{/* Before/After Gallery */}
<BeforeAfterGallery />
{/* Main Content */}
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
{/* Products Grid Section */}
@@ -84,9 +90,6 @@ export default async function Homepage() {
</section>
)}
{/* Before/After Gallery */}
<BeforeAfterGallery />
{/* How It Works */}
<HowItWorks />
@@ -129,49 +132,64 @@ export default async function Homepage() {
</section>
{/* Benefits Section */}
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-white to-[#faf9f7]">
<div className="max-w-7xl mx-auto">
<div className="text-center mb-16">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
Why Choose Us
</span>
<h2 className="text-3xl md:text-4xl font-medium">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium text-[#1a1a1a]">
The Manoon Difference
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-6 rounded-full" />
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
{[
{
title: "100% Natural",
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
icon: (
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#7eb89e"/>
<path stroke="#7eb89e" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
),
},
{
title: "Handcrafted",
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
icon: (
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z"/>
</svg>
),
},
{
title: "Sustainable",
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
icon: (
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path stroke="#e8967a" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M12.75 3.03v.568c0 .334.148.65.405.864l1.068.89c.442.369.535 1.01.216 1.49l-.51.766a2.25 2.25 0 01-1.161.886l-.143.048a1.107 1.107 0 00-.57 1.664c.369.555.169 1.307-.427 1.605L9 13.125l.423 1.059a.956.956 0 11-1.652.928l-.714-.093a1.125 1.125 0 00-1.906.172L4.5 15.75l-.612.153M12.75 3.031l.002-.004m0 0a8.955 8.955 0 00-4.943.834 8.974 8.974 0 004.943.834m4.943-.834a8.955 8.955 0 00-4.943-.834c2.687 0 5.18.948 7.161 2.664a8.974 8.974 0 014.943-.834z"/>
</svg>
),
},
].map((benefit, index) => (
<div key={index} className="text-center">
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
<span className="text-2xl font-medium text-[#1a1a1a]">
{String(index + 1).padStart(2, '0')}
</span>
<div
key={index}
className="relative text-center p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500 group"
>
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-md border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
{benefit.icon}
</div>
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
<p className="text-[#666666]">{benefit.description}</p>
<h3 className="text-xl font-semibold text-[#1a1a1a] mb-3">{benefit.title}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{benefit.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Testimonials Section */}
<TestimonialsSection />
{/* Newsletter Section */}
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white">
<div className="max-w-7xl mx-auto">

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
@@ -27,11 +27,15 @@ export default function CartDrawer() {
const lines = getLines();
const total = getTotal();
const lineCount = getLineCount();
const [initialized, setInitialized] = useState(false);
// Initialize checkout on mount
// Initialize checkout on mount (only once)
useEffect(() => {
initCheckout();
}, [initCheckout]);
if (!initialized) {
initCheckout();
setInitialized(true);
}
}, [initialized]);
// Lock body scroll when cart is open
useEffect(() => {

View File

@@ -38,7 +38,7 @@ export default function AsSeenIn() {
<section className="py-12 bg-[#1a1a1a] overflow-hidden border-y border-white/10">
<div className="container mx-auto px-4 mb-8">
<motion.p
className="text-center text-[10px] uppercase tracking-[0.4em] text-white/30 font-medium"
className="text-center text-[10px] uppercase tracking-[0.4em] text-[#c9a962] font-bold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}

View File

@@ -104,7 +104,7 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
<div className="flex items-center gap-1.5">
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 fill-gold text-gold" viewBox="0 0 24 24">
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
))}
@@ -125,6 +125,16 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
}
export default function BeforeAfterGallery() {
const [selectedIndex, setSelectedIndex] = useState(0);
const goToPrev = () => {
setSelectedIndex(prev => prev === 0 ? results.length - 1 : prev - 1);
};
const goToNext = () => {
setSelectedIndex(prev => prev === results.length - 1 ? 0 : prev + 1);
};
return (
<section className="py-24 bg-[#faf9f7]">
<div className="container mx-auto px-4">
@@ -143,8 +153,8 @@ export default function BeforeAfterGallery() {
</h2>
</motion.div>
{/* Two transformations side by side */}
<div className="flex gap-6 max-w-6xl mx-auto">
{/* Desktop: Two transformations side by side */}
<div className="hidden md:flex gap-6 max-w-6xl mx-auto">
{results.map((result, index) => (
<motion.div
key={result.id}
@@ -159,6 +169,55 @@ export default function BeforeAfterGallery() {
))}
</div>
{/* Mobile: Carousel with one transformation at a time */}
<div className="md:hidden relative max-w-md mx-auto">
<div className="overflow-hidden">
<motion.div
key={selectedIndex}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3 }}
>
<BeforeAfterSlider result={results[selectedIndex]} />
</motion.div>
</div>
{/* Carousel Navigation */}
<button
onClick={goToPrev}
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
aria-label="Previous transformation"
>
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<button
onClick={goToNext}
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
aria-label="Next transformation"
>
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Dot Indicators */}
<div className="flex justify-center gap-2 mt-6">
{results.map((_, index) => (
<button
key={index}
onClick={() => setSelectedIndex(index)}
className={`w-2 h-2 rounded-full transition-all ${
selectedIndex === index ? "bg-black w-4" : "bg-gray-300"
}`}
aria-label={`Go to transformation ${index + 1}`}
/>
))}
</div>
</div>
{/* CTA */}
<motion.div
className="text-center mt-12"

View File

@@ -13,26 +13,8 @@ export default function HeroVideo() {
};
return (
<section className="relative h-screen w-full overflow-hidden">
{/* Video Background */}
<div className="absolute inset-0">
<video
autoPlay
muted
loop
playsInline
poster="/images/hero-poster.jpg"
className="w-full h-full object-cover"
>
{/* Placeholder - Add actual video files when available */}
{/* <source src="/videos/hero.webm" type="video/webm" /> */}
{/* <source src="/videos/hero.mp4" type="video/mp4" /> */}
</video>
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/30 to-black/60" />
</div>
{/* Fallback Background (shown when video isn't loaded) */}
<section className="relative min-h-screen w-full overflow-hidden">
{/* Background Image with Overlay */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
@@ -43,7 +25,7 @@ export default function HeroVideo() {
</div>
{/* Content */}
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center text-white px-4">
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center text-center text-white px-4 py-20">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@@ -59,7 +41,7 @@ export default function HeroVideo() {
>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 fill-gold text-gold" viewBox="0 0 24 24">
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
))}

View File

@@ -8,42 +8,59 @@ export default function HowItWorks() {
number: "01",
title: "Choose Your Oil",
description: "Select from our collection of pure, cold-pressed oils formulated for your specific hair and skin needs.",
icon: (
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007z" />
</svg>
),
},
{
number: "02",
title: "Apply Daily",
description: " Massage a few drops into damp hair or skin. Our oils absorb instantly—never greasy, always nourishing.",
description: "Massage a few drops into damp hair or skin. Our oils absorb instantly—never greasy, always nourishing.",
icon: (
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
),
},
{
number: "03",
title: "See Results",
description: "Experience transformation in 4-6 weeks. Shinier hair, radiant skin, and confidence that glows.",
icon: (
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="#FFD700">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
),
},
];
return (
<section className="py-24 bg-white">
<section className="py-24 bg-gradient-to-b from-white to-[#faf9f7]">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-16"
className="text-center mb-20"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
Simple Process
</span>
<h2 className="text-3xl md:text-4xl font-medium">
<h2 className="text-4xl md:text-5xl font-medium text-[#1a1a1a]">
How ManoonOils Works
</h2>
<div className="w-24 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-6 rounded-full" />
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12 max-w-5xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16 max-w-6xl mx-auto">
{steps.map((step, index) => (
<motion.div
key={index}
className="relative text-center"
className="relative text-center group"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -51,26 +68,45 @@ export default function HowItWorks() {
>
{/* Connector line (not on last item) */}
{index < steps.length - 1 && (
<div className="hidden md:block absolute top-10 left-[60%] w-[80%] h-[2px] bg-gradient-to-r from-[#e5e5e5] to-transparent" />
<div className="hidden md:block absolute top-16 left-[55%] w-[90%] h-[2px]">
<div className="absolute inset-0 bg-gradient-to-r from-[#c9a962]/40 to-transparent rounded-full" />
<motion.div
className="absolute inset-y-0 left-0 w-2 bg-[#FFD700] rounded-full"
initial={{ scaleX: 0 }}
whileInView={{ scaleX: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.5 + index * 0.2 }}
style={{ originX: 0 }}
/>
</div>
)}
{/* Number circle */}
<div className="relative inline-flex items-center justify-center w-20 h-20 mb-6">
<div className="absolute inset-0 rounded-full bg-[#1a1a1a]" />
<span className="relative text-white text-2xl font-medium">{step.number}</span>
</div>
{/* Step card */}
<div className="relative p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500">
{/* Number badge */}
<div className="absolute -top-5 left-1/2 -translate-x-1/2">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#c9a962] to-[#FFD700] flex items-center justify-center shadow-lg">
<span className="text-white text-lg font-bold">{step.number}</span>
</div>
</div>
<h3 className="text-xl font-medium mb-3">{step.title}</h3>
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mx-auto">
{step.description}
</p>
{/* Icon */}
<div className="w-20 h-20 mx-auto mt-4 mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
{step.icon}
</div>
<h3 className="text-xl font-semibold text-[#1a1a1a] mb-3">{step.title}</h3>
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mx-auto">
{step.description}
</p>
</div>
</motion.div>
))}
</div>
{/* CTA */}
<motion.div
className="text-center mt-16"
className="text-center mt-20"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
@@ -78,9 +114,12 @@ export default function HowItWorks() {
>
<a
href="/products"
className="inline-block px-10 py-4 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333] transition-colors"
className="group relative inline-flex items-center gap-3 px-12 py-5 bg-gradient-to-r from-[#1a1a1a] to-[#333333] text-white text-[13px] uppercase tracking-[0.2em] font-semibold hover:from-[#c9a962] hover:to-[#FFD700] transition-all duration-500 rounded-full shadow-lg hover:shadow-xl"
>
Start Your Transformation
<span>Start Your Transformation</span>
<svg className="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
</svg>
</a>
</motion.div>
</div>

View File

@@ -4,7 +4,7 @@ import { motion } from "framer-motion";
export default function ProblemSection() {
return (
<section className="py-24 bg-[#faf9f7]">
<section className="py-24 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
<div className="container mx-auto px-4">
<motion.div
className="max-w-3xl mx-auto text-center"
@@ -13,20 +13,24 @@ export default function ProblemSection() {
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-4 block">
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
The Problem
</span>
<h2 className="text-3xl md:text-4xl font-medium mb-8 leading-tight">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6 leading-tight text-[#1a1a1a]">
Tired of Hair & Skin Products That Don&apos;t Deliver?
</h2>
<p className="text-[#666666] text-lg max-w-xl mx-auto">
You deserve better than products filled with harsh chemicals and empty promises
</p>
<div className="w-16 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-8 rounded-full" />
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto mt-12">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto mt-16">
{[
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
<path stroke="#c9a962" strokeLinecap="round" strokeLinejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
problem: "Dry, Damaged Hair",
@@ -34,8 +38,8 @@ export default function ProblemSection() {
},
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
<path stroke="#e8967a" strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
problem: "Confusing Ingredients",
@@ -43,8 +47,8 @@ export default function ProblemSection() {
},
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
<path stroke="#7eb89e" strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
),
problem: "No Real Results",
@@ -53,16 +57,20 @@ export default function ProblemSection() {
].map((item, index) => (
<motion.div
key={index}
className="text-center p-8 bg-white rounded-2xl shadow-sm border border-[#f0ede8]"
className="relative text-center p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500 group"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -5 }}
>
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-red-50 flex items-center justify-center text-red-400">
{/* Decorative top border */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-20 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] rounded-b-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-md border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
{item.icon}
</div>
<h3 className="text-lg font-medium mb-3">{item.problem}</h3>
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-3">{item.problem}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{item.description}</p>
</motion.div>
))}

View File

@@ -5,17 +5,17 @@ import { motion } from "framer-motion";
const badges = [
{
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<svg className="w-6 h-6 text-yellow-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
),
stats: "4.9/5",
label: "Average Rating",
subtext: "Based on 2,847 reviews",
subtext: "Based on 1000+ reviews",
},
{
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
),
@@ -25,7 +25,7 @@ const badges = [
},
{
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#7eb89e" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
</svg>
),
@@ -35,7 +35,7 @@ const badges = [
},
{
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#e8967a" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
</svg>
),
@@ -47,10 +47,10 @@ const badges = [
export default function TrustBadges() {
return (
<section className="py-14 bg-gradient-to-b from-[#faf9f7] to-white border-b border-[#e8e4e0]">
<section className="py-16 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
<div className="container mx-auto px-4">
<motion.div
className="grid grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8"
className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
@@ -59,22 +59,23 @@ export default function TrustBadges() {
{badges.map((badge, index) => (
<motion.div
key={index}
className="flex flex-col items-center text-center p-6 bg-white rounded-2xl shadow-sm border border-[#f0ede8]"
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
whileHover={{ y: -3 }}
>
<div className="w-12 h-12 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white mb-4">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
{badge.icon}
</div>
<p className="text-2xl lg:text-3xl font-semibold text-[#1a1a1a] tracking-tight">
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
{badge.stats}
</p>
<p className="text-sm font-medium text-[#1a1a1a] mt-1">
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
{badge.label}
</p>
<p className="text-xs text-[#888888] mt-1">
<p className="text-xs text-[#888888] mt-0.5">
{badge.subtext}
</p>
</motion.div>

View File

@@ -56,7 +56,7 @@ export default function Header() {
: "bg-white/80 backdrop-blur-sm"
}`}
>
<div className="relative flex items-center justify-between h-[72px]">
<div className="relative flex items-center justify-between h-[72px] px-4 lg:px-6">
{/* Mobile Menu Button */}
<button
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"

View File

@@ -9,8 +9,9 @@ interface ProductBenefitsProps {
const benefits = [
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z" />
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}>
<path stroke="#c9a962" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
<path stroke="#c9a962" strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
),
title: "Pure & Natural",
@@ -18,8 +19,9 @@ const benefits = [
},
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#e8967a"/>
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
),
title: "Cruelty Free",
@@ -27,8 +29,9 @@ const benefits = [
},
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#7eb89e"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
),
title: "Made with Love",
@@ -36,8 +39,9 @@ const benefits = [
},
{
icon: (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="#c9a962"/>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="#b8944f" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
),
title: "Visible Results",
@@ -56,7 +60,7 @@ export default function ProductBenefits({ locale = "SR" }: ProductBenefitsProps)
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
<span className="text-xs uppercase tracking-[0.2em] text-[#c9a962] mb-3 block font-medium">
{locale === "EN" ? "Why Choose This Product" : "Zašto odabrati ovaj proizvod"}
</span>
<h2 className="text-3xl md:text-4xl font-medium">
@@ -68,17 +72,18 @@ export default function ProductBenefits({ locale = "SR" }: ProductBenefitsProps)
{benefits.map((benefit, index) => (
<motion.div
key={index}
className="text-center p-6 bg-white rounded-2xl shadow-sm border border-[#f0ede8]"
className="text-center p-6 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
whileHover={{ y: -5 }}
>
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white">
<div className="w-20 h-20 mx-auto mb-5 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm border border-[#e8e4dc]">
{benefit.icon}
</div>
<h3 className="text-base font-medium mb-2">{benefit.title}</h3>
<p className="text-sm text-[#666666]">{benefit.description}</p>
<h3 className="text-base font-medium mb-2 text-[#1a1a1a]">{benefit.title}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{benefit.description}</p>
</motion.div>
))}
</div>

View File

@@ -27,14 +27,13 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
>
<Link href={`/products/${localized.slug}`} className="group block">
{/* Image Container */}
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
{image ? (
<Image
<img
src={image}
alt={localized.name}
fill
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
loading="lazy"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">

View File

@@ -1,13 +1,13 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronDown, Star, Minus, Plus } from "lucide-react";
import type { Product } from "@/types/saleor";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { getProductPrice, getLocalizedProduct } from "@/lib/saleor";
import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor";
import ProductCard from "@/components/product/ProductCard";
import ProductBenefits from "@/components/product/ProductBenefits";
import ProductReviews from "@/components/product/ProductReviews";
@@ -76,12 +76,12 @@ function StarRating({ rating = 5, count = 0 }: { rating?: number; count?: number
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${i < rating ? 'fill-black text-black' : 'text-[#e5e5e5]'}`}
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
/>
))}
</div>
{count > 0 && (
<span className="text-sm text-[#666666] ml-1">({count})</span>
<span className="text-sm text-[#666666] ml-1">({count >= 1000 ? '1000+' : count})</span>
)}
</div>
);
@@ -91,8 +91,23 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
const [selectedImage, setSelectedImage] = useState(0);
const [quantity, setQuantity] = useState(1);
const [isAdding, setIsAdding] = useState(false);
const [urgencyIndex, setUrgencyIndex] = useState(0);
const { addLine, openCart } = useSaleorCheckoutStore();
// Cycle through urgency messages
useEffect(() => {
const interval = setInterval(() => {
setUrgencyIndex(prev => (prev + 1) % 3);
}, 3000);
return () => clearInterval(interval);
}, []);
const urgencyMessages = [
{ icon: "🚀", text: "Hurry up! 500+ items sold in the last 3 days!" },
{ icon: "🛒", text: "In the carts of 2.5K people - buy before its gone!" },
{ icon: "👀", text: "7,562 people viewed this product in the last 24 hours!" },
];
const localized = getLocalizedProduct(product, locale);
const variant = product.variants?.[0];
@@ -115,6 +130,8 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
const isAvailable = variant?.quantityAvailable > 0;
const price = getProductPrice(product);
const priceAmount = getProductPriceAmount(product);
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
// Extract short description (first sentence or first 100 chars)
const shortDescription = localized.description
@@ -152,7 +169,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
className="flex gap-4"
className="flex flex-col md:flex-row gap-4"
>
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
{images.length > 1 && (
@@ -167,12 +184,10 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
: "border-transparent hover:border-[#999999]"
}`}
>
<Image
<img
src={image.url}
alt={image.alt || localized.name}
fill
className="object-cover"
sizes="80px"
className="w-full h-full object-cover"
/>
</button>
))}
@@ -180,24 +195,53 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
)}
{/* Main Image */}
<div className="flex-1 relative aspect-square bg-[#f8f9fa] overflow-hidden">
{images[selectedImage] && (
<Image
src={images[selectedImage].url}
alt={images[selectedImage].alt || localized.name}
fill
className="object-cover"
priority
sizes="(max-width: 1024px) 100vw, 50vw"
/>
)}
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
<img
src={images[selectedImage].url}
alt={images[selectedImage].alt || localized.name}
className="w-full h-full object-cover"
/>
{/* Award Badge - Optional */}
<div className="absolute top-4 left-4">
<div className="bg-black text-white text-[10px] uppercase tracking-[0.1em] px-3 py-1.5">
{locale === "EN" ? "Bestseller" : "Najprodavanije"}
</div>
</div>
{/* Carousel Navigation - Mobile Only */}
{images.length > 1 && (
<>
{/* Left Arrow */}
<button
onClick={() => setSelectedImage(prev => prev === 0 ? images.length - 1 : prev - 1)}
className="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
aria-label="Previous image"
>
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Right Arrow */}
<button
onClick={() => setSelectedImage(prev => prev === images.length - 1 ? 0 : prev + 1)}
className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
aria-label="Next image"
>
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Dot Indicators - Mobile Only */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 md:hidden">
{images.map((_, index) => (
<button
key={index}
onClick={() => setSelectedImage(index)}
className={`w-2 h-2 rounded-full transition-all ${
selectedImage === index ? "bg-white w-4" : "bg-white/50"
}`}
aria-label={`Go to image ${index + 1}`}
/>
))}
</div>
</>
)}
</div>
</motion.div>
@@ -208,24 +252,65 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
transition={{ duration: 0.6, delay: 0.2 }}
className="lg:pl-8"
>
{/* Urgency Sales Banner */}
<motion.div
key={urgencyIndex}
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 10 }}
transition={{ duration: 0.3 }}
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
>
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
{urgencyMessages[urgencyIndex].text}
</motion.div>
{/* Product Name */}
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
{localized.name}
</h1>
{/* Short Description */}
<p className="text-[#666666] leading-relaxed mb-6">
<p className="text-[#666666] leading-relaxed mb-4">
{shortDescription}
</p>
{/* Price & Rating */}
<div className="flex items-center justify-between mb-8">
<span className="text-3xl font-medium">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
{/* Stock Warning - Static */}
<div className="flex items-center justify-start gap-2 mb-6">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
</span>
<StarRating rating={5} count={12} />
<span className="text-red-600 text-sm font-medium">Stocks are running out!</span>
</div>
{/* Discount Price Display */}
{originalPrice && priceAmount > 0 && (
<div className="mb-4">
<div className="flex items-center gap-3 mb-2">
<span className="text-xl text-[#666666] line-through">
{originalPrice}
</span>
<span className="bg-[#b91c1c] text-white text-xs font-bold px-2 py-1 rounded">
-30%
</span>
</div>
<span className="text-3xl font-bold text-[#b91c1c]">
{price}
</span>
</div>
)}
{/* Price & Rating */}
{!originalPrice && (
<div className="flex items-center justify-between mb-8">
<span className="text-3xl font-medium">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
</span>
<StarRating rating={5} count={1000} />
</div>
)}
{/* Divider */}
<div className="border-t border-[#e5e5e5] mb-8" />

View File

@@ -65,7 +65,7 @@ function ReviewCard({ review }: { review: typeof reviews[0] }) {
<div className="flex-shrink-0 w-80 bg-white p-6 rounded-2xl shadow-sm border border-[#f0ede8] mx-3">
<div className="flex items-center gap-1 mb-3">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-4 h-4 fill-gold text-gold" viewBox="0 0 24 24">
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
))}
@@ -76,7 +76,13 @@ function ReviewCard({ review }: { review: typeof reviews[0] }) {
{review.name.charAt(0)}
</div>
<div>
<p className="text-sm font-medium">{review.name}</p>
<div className="flex items-center gap-1.5">
<p className="text-sm font-medium">{review.name}</p>
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<span className="text-xs text-green-700 font-medium">Verified</span>
</div>
<p className="text-xs text-[#888888]">{review.location}</p>
</div>
</div>
@@ -107,12 +113,12 @@ export default function ProductReviews({ locale = "SR", productName = "this prod
<div>
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<svg key={star} className="w-5 h-5 fill-gold text-gold" viewBox="0 0 24 24">
<svg key={star} className="w-5 h-5 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
))}
</div>
<p className="text-sm text-[#666666] mt-1">Based on 50 reviews</p>
<p className="text-sm text-[#666666] mt-1">Based on 1000+ reviews</p>
</div>
</div>
</motion.div>

View File

@@ -28,6 +28,7 @@ export {
getProducts,
getProductBySlug,
getProductPrice,
getProductPriceAmount,
getProductImage,
isProductAvailable,
formatPrice,

View File

@@ -50,8 +50,8 @@ export const CHECKOUT_LINES_UPDATE = gql`
`;
export const CHECKOUT_LINES_DELETE = gql`
mutation CheckoutLinesDelete($checkoutId: ID!, $lineIds: [ID!]!) {
checkoutLinesDelete(checkoutId: $checkoutId, lines: $lineIds) {
mutation CheckoutLinesDelete($id: ID!, $linesIds: [ID!]!) {
checkoutLinesDelete(id: $id, linesIds: $linesIds) {
checkout {
...CheckoutFragment
}

View File

@@ -68,6 +68,11 @@ export function getProductPrice(product: Product): string {
);
}
export function getProductPriceAmount(product: Product): number {
const variant = product.variants?.[0];
return variant?.pricing?.price?.gross?.amount || 0;
}
export function getProductImage(product: Product): string {
if (product.media && product.media.length > 0) {
return product.media[0].url;
@@ -88,7 +93,8 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
return new Intl.NumberFormat("sr-RS", {
style: "currency",
currency: currency,
minimumFractionDigits: 0,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}

View File

@@ -221,8 +221,8 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
mutation: CHECKOUT_LINES_DELETE,
variables: {
checkoutId: checkout.id,
lineIds: [lineId],
id: checkout.id,
linesIds: [lineId],
},
});