34 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
Unchained
44d938953b Center related products using flexbox instead of grid
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-22 11:00:44 +02:00
Unchained
97fc5f5f1d Fix JSX indentation in Similar Products section 2026-03-22 09:11:13 +02:00
Unchained
140d82c7f4 Center Similar Products grid on product pages 2026-03-22 09:04:35 +02:00
Unchained
80a388cd7c fix: Center the similar products grid on product pages 2026-03-22 09:00:01 +02:00
Unchained
c3bd0408f4 feat: Add newsletter section back to product pages 2026-03-22 08:57:05 +02:00
Unchained
7618cfa6df fix: Remove non-scrolling testimonials and newsletter from product pages 2026-03-22 08:54:29 +02:00
Unchained
0827147745 fix: Slow down reviews scroll tempo for readability 2026-03-22 08:47:06 +02:00
Unchained
c5e96718a4 feat: Add scrolling reviews marquee with 50 reviews
- Reviews scroll continuously (alternating left-to-right and right-to-left)
- 50 varied Serbian customer reviews praising Manoon products
- Mentions Anti-age Serum, Day Serum, Night Serum, Morning Glow, Anti-age Set
- Scroll effect similar to As Seen In banner
2026-03-22 08:43:25 +02:00
Unchained
7febe90b36 fix: Move Customer Reviews above AsSeenIn on product pages 2026-03-22 08:39:44 +02:00
Unchained
c723d72508 fix: Move BeforeAfterGallery to right after AsSeenIn on product pages 2026-03-22 08:37:12 +02:00
Unchained
bf6362d3ad feat: Add second transformation with side-by-side sliders
- First transformation: use_case_2 (4-6 weeks)
- Second transformation: use_case_3 (6-8 weeks)
- Both sliders displayed side by side on same line
2026-03-22 08:32:22 +02:00
Unchained
9e901d7dfe feat: Use actual moumoujus before/after images from MinIO
- Before: use_case_2.webp (wrinkled skin)
- After: use_case_2_1.webp (smooth skin)
- Both images from minio-api.nodecrew.me
2026-03-22 08:26:50 +02:00
Unchained
0e727b2648 feat: Add slider comparison to before/after gallery
- Drag slider to reveal before/after (like example screenshot)
- Timeline showing '4-6 Weeks'
- Stars rating with review count
- Verified Results badge
- Matches moumoujus.com style
2026-03-22 07:32:47 +02:00
Unchained
d6523deae5 feat: Add all homepage sections to product pages
Product pages now include:
- Product Benefits
- Product Reviews
- Trust Badges
- Before/After Gallery
- How It Works
- Testimonials
- Newsletter
2026-03-21 20:13:05 +02:00
Unchained
5216abbcc0 feat: Landing page design improvements
Based on landing-page-design skill principles:

Homepage:
- Redesigned hero with outcome-focused headline ("Transform Your Hair & Skin")
- Added social proof micro (5 stars + 50,000+ customers)
- Better CTA: "Transform My Hair & Skin" instead of "Shop Now"
- Added trust indicators in hero (30-day guarantee, free shipping, cruelty free)
- Added ProblemSection to create empathy (dry hair, confusing ingredients, no results)
- Added HowItWorks section (3 steps: Choose, Apply, See Results)
- Improved AsSeenIn with scrolling marquee on dark background
- Premium trust badges with stats and icons

Product pages:
- Improved CTA: "Transform My Hair & Skin" (action verb + value)
- Added ProductBenefits section (4 key benefits)
- Added ProductReviews section with customer testimonials
- Added AsSeenIn scrolling banner
- Added trust indicators with icons

Section order now follows proven conversion sequence:
1. Hero (headline + outcome + CTA)
2. Social Proof (trust badges, logos)
3. Problem (empathy)
4. Solution (products)
5. How It Works
6. Testimonials
7. Final CTA
2026-03-21 19:59:09 +02:00
Unchained
4af5412c76 feat: Add trust indicators to product page
- Add 30-day money-back guarantee, secure checkout, easy returns icons
- Reorganize product page layout with clearer trust messaging
- Update free shipping threshold messaging
2026-03-21 19:00:31 +02:00
Unchained
d381cba302 feat: Add social proof sections to homepage
- Add TrustBadges component with ratings, customer count, secure payment icons
- Add AsSeenIn media logos banner
- Add BeforeAfterGallery with interactive gallery
- Add TestimonialsSection (already existed, now integrated into homepage)
- Connect all sections in homepage page.tsx

Sections added to homepage flow:
1. HeroVideo
2. TrustBadges
3. AsSeenIn
4. Products Grid
5. BeforeAfterGallery
6. Brand Story
7. Benefits
8. TestimonialsSection
9. Newsletter
2026-03-21 18:58:33 +02:00
Unchained
26212dec1c fix: Apollo Client cache merge causing product duplication
Some checks failed
Build and Deploy / build (push) Has been cancelled
The merge function was concatenating products on each query, causing
4 products to become 8, then 12, etc. Changed to replace incoming
data instead of merging.
2026-03-21 18:04:11 +02:00
Unchained
2876a8f80e fix: Replace WooCommerce env vars with Saleor API URL
Some checks failed
Build and Deploy / build (push) Has been cancelled
- NEXT_PUBLIC_WOOCOMMERCE_URL → NEXT_PUBLIC_SALEOR_API_URL
- Remove WooCommerce consumer key/secret (not needed for Saleor public API)
- Saleor API is public, no authentication required
2026-03-21 17:58:13 +02:00
Unchained
93005af0a1 Remove playwright - testing tool only
Some checks failed
Build and Deploy / build (push) Has been cancelled
2026-03-21 17:26:46 +02:00
Unchained
0b4e3f89d1 Add playwright for visual testing 2026-03-21 17:25:45 +02:00
Unchained
ec287c85ea Fix CSS cascade layers and header layout
- Rewrite globals.css to work properly with Tailwind 4 cascade layers
  - Remove conflicting * { padding: 0 } reset that broke Tailwind utilities
  - Organize styles into @layer base, @layer components, @layer utilities
- Fix newsletter centering (was off due to CSS layer conflicts)
- Fix header overlap on products pages (proper pt-[72px] spacing)
- Add solid header background (bg-white/80) instead of transparent
- Fix logo/nav positioning on desktop

Verified fixes with Playwright screenshots at 1280x800 and 390x844
2026-03-21 17:21:00 +02:00
23 changed files with 1616 additions and 574 deletions

View File

@@ -72,21 +72,8 @@ spec:
env:
- name: NODE_ENV
value: "production"
- name: NEXT_PUBLIC_WOOCOMMERCE_URL
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_URL
- name: NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_KEY
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_CONSUMER_KEY
- name: NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_SECRET
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_CONSUMER_SECRET
- name: NEXT_PUBLIC_SALEOR_API_URL
value: "https://api.manoonoils.com/graphql/"
- name: NEXT_PUBLIC_SITE_URL
value: "https://dev.manoonoils.com"
volumeMounts:
@@ -117,21 +104,8 @@ spec:
value: "3000"
- name: HOSTNAME
value: "0.0.0.0"
- name: NEXT_PUBLIC_WOOCOMMERCE_URL
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_URL
- name: NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_KEY
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_CONSUMER_KEY
- name: NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_SECRET
valueFrom:
secretKeyRef:
name: woocommerce-credentials
key: WOOCOMMERCE_CONSUMER_SECRET
- name: NEXT_PUBLIC_SALEOR_API_URL
value: "https://api.manoonoils.com/graphql/"
- name: NEXT_PUBLIC_SITE_URL
value: "https://dev.manoonoils.com"
resources:

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

@@ -2,61 +2,11 @@
/* ============================================
MANOONOILS DESIGN SYSTEM
Inspired by premium skincare brands
Tailwind 4 compatible - uses CSS layers
============================================ */
:root {
/* Primary Colors */
--color-white: #ffffff;
--color-background: #fafafa;
--color-background-alt: #f5f5f5;
--color-foreground: #1a1a1a;
--color-foreground-muted: #666666;
--color-foreground-subtle: #999999;
/* Accent Colors */
--color-accent: #e8f0f5;
--color-accent-dark: #a8c5d8;
--color-accent-blue: #e8f0f5;
--color-gold: #c9a962;
--color-gold-light: #d4b978;
/* UI Colors */
--color-border: #e5e5e5;
--color-border-dark: #d1d1d1;
--color-cta: #000000;
--color-cta-hover: #333333;
--color-overlay: rgba(0, 0, 0, 0.4);
/* Spacing */
--space-xs: 4px;
--space-sm: 8px;
--space-md: 16px;
--space-lg: 24px;
--space-xl: 32px;
--space-2xl: 48px;
--space-3xl: 64px;
--space-4xl: 96px;
--space-5xl: 128px;
/* Typography */
--font-display: 'DM Sans', sans-serif;
--font-body: 'Inter', sans-serif;
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 350ms ease;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
@theme inline {
/* Colors - reference :root variables */
/* Colors - reference CSS variables */
--color-white: var(--color-white);
--color-background: var(--color-background);
--color-background-alt: var(--color-background-alt);
@@ -79,6 +29,43 @@
--font-body: var(--font-body);
}
/* ============================================
CSS VARIABLES
============================================ */
:root {
--color-white: #ffffff;
--color-background: #fafafa;
--color-background-alt: #f5f5f5;
--color-foreground: #1a1a1a;
--color-foreground-muted: #666666;
--color-foreground-subtle: #999999;
--color-accent: #e8f0f5;
--color-accent-dark: #a8c5d8;
--color-accent-blue: #e8f0f5;
--color-gold: #c9a962;
--color-gold-light: #d4b978;
--color-border: #e5e5e5;
--color-border-dark: #d1d1d1;
--color-cta: #000000;
--color-cta-hover: #333333;
--color-overlay: rgba(0, 0, 0, 0.4);
--font-display: 'DM Sans', sans-serif;
--font-body: 'Inter', sans-serif;
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 350ms ease;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
/* ============================================
FONT IMPORTS
============================================ */
@@ -98,307 +85,276 @@
}
/* ============================================
BASE STYLES
BASE STYLES (in Tailwind base layer)
============================================ */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
@layer base {
html {
scroll-behavior: smooth;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
h3 {
font-size: clamp(1.25rem, 3vw, 1.75rem);
}
input, textarea, select {
font-family: var(--font-body);
font-size: 16px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--color-foreground);
}
:focus-visible {
outline: 2px solid var(--color-foreground);
outline-offset: 2px;
}
}
/* ============================================
TYPOGRAPHY
COMPONENTS
============================================ */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
h3 {
font-size: clamp(1.25rem, 3vw, 1.75rem);
}
.text-display {
font-family: var(--font-display);
font-weight: 500;
letter-spacing: -0.02em;
}
.text-body {
font-family: var(--font-body);
}
.text-uppercase {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.text-caption {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 500;
}
.text-muted {
color: var(--color-foreground-muted);
}
.text-subtle {
color: var(--color-foreground-subtle);
}
/* ============================================
UTILITY CLASSES
============================================ */
.container {
width: 100%;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
padding-left: 24px;
padding-right: 24px;
}
@media (min-width: 640px) {
@layer components {
.container {
padding-left: 32px;
padding-right: 32px;
width: 100%;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
padding-left: 24px;
padding-right: 24px;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 48px;
padding-right: 48px;
@media (min-width: 640px) {
.container {
padding-left: 32px;
padding-right: 32px;
}
}
}
.container-narrow {
max-width: 1200px;
}
@media (min-width: 1024px) {
.container {
padding-left: 48px;
padding-right: 48px;
}
}
.container-wide {
max-width: 1600px;
}
.container-narrow {
max-width: 1200px;
}
/* Section spacing */
.section {
padding-top: var(--space-4xl);
padding-bottom: var(--space-4xl);
}
.container-wide {
max-width: 1600px;
}
.section-sm {
padding-top: var(--space-2xl);
padding-bottom: var(--space-2xl);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 32px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
border: none;
cursor: pointer;
transition: all var(--transition-base);
}
/* Flex utilities */
.flex-center {
display: flex;
align-items: center;
justify-content: center;
.btn-primary {
background: var(--color-cta);
color: var(--color-white);
}
.btn-primary:hover {
background: var(--color-cta-hover);
}
.btn-secondary {
background: transparent;
color: var(--color-foreground);
border: 1px solid var(--color-border-dark);
}
.btn-secondary:hover {
background: var(--color-foreground);
color: var(--color-white);
border-color: var(--color-foreground);
}
.link-underline {
position: relative;
text-decoration: none;
}
.link-underline::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: currentColor;
transition: width var(--transition-base);
}
.link-underline:hover::after {
width: 100%;
}
.text-display {
font-family: var(--font-display);
font-weight: 500;
letter-spacing: -0.02em;
}
.text-body {
font-family: var(--font-body);
}
.text-uppercase {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.text-caption {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 500;
}
.text-muted {
color: var(--color-foreground-muted);
}
.text-subtle {
color: var(--color-foreground-subtle);
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
}
/* ============================================
INTERACTIVE ELEMENTS
UTILITIES
============================================ */
/* Button Base */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 32px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
border: none;
cursor: pointer;
transition: all var(--transition-base);
}
@layer utilities {
.section {
padding-top: 96px;
padding-bottom: 96px;
}
.btn-primary {
background: var(--color-cta);
color: var(--color-white);
}
.section-sm {
padding-top: 48px;
padding-bottom: 48px;
}
.btn-primary:hover {
background: var(--color-cta-hover);
}
::-webkit-scrollbar {
width: 8px;
}
.btn-secondary {
background: transparent;
color: var(--color-foreground);
border: 1px solid var(--color-border-dark);
}
::-webkit-scrollbar-track {
background: var(--color-background-alt);
}
.btn-secondary:hover {
background: var(--color-foreground);
color: var(--color-white);
border-color: var(--color-foreground);
}
::-webkit-scrollbar-thumb {
background: var(--color-border-dark);
border-radius: 4px;
}
/* Link underline animation */
.link-underline {
position: relative;
text-decoration: none;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-foreground-muted);
}
.link-underline::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: currentColor;
transition: width var(--transition-base);
}
.animate-fade-in {
animation: fadeIn var(--transition-slow) forwards;
}
.link-underline:hover::after {
width: 100%;
.animate-slide-up {
animation: slideUp var(--transition-slow) forwards;
}
.animate-slide-in-right {
animation: slideInRight var(--transition-slow) forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 25s linear infinite;
}
.animate-marquee-slow {
animation: marquee 35s linear infinite;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
}
/* ============================================
FORM ELEMENTS
REDUCED MOTION
============================================ */
input, textarea, select {
font-family: var(--font-body);
font-size: 16px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--color-foreground);
}
/* ============================================
SCROLLBAR
============================================ */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background-alt);
}
::-webkit-scrollbar-thumb {
background: var(--color-border-dark);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-foreground-muted);
}
/* ============================================
ANIMATIONS
============================================ */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fade-in {
animation: fadeIn var(--transition-slow) forwards;
}
.animate-slide-up {
animation: slideUp var(--transition-slow) forwards;
}
.animate-slide-in-right {
animation: slideInRight var(--transition-slow) forwards;
}
/* Marquee Animations */
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
.animate-marquee {
animation: marquee 25s linear infinite;
}
.animate-marquee-slow {
animation: marquee 35s linear infinite;
}
/* ============================================
ACCESSIBILITY
============================================ */
/* Focus visible styles */
:focus-visible {
outline: 2px solid var(--color-foreground);
outline-offset: 2px;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -412,16 +368,3 @@ input:focus, textarea:focus, select:focus {
scroll-behavior: auto;
}
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}

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

@@ -3,6 +3,12 @@ import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
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 ProductReviews from "@/components/product/ProductReviews";
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
import ProblemSection from "@/components/home/ProblemSection";
import HowItWorks from "@/components/home/HowItWorks";
export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -30,6 +36,21 @@ export default async function Homepage() {
{/* Hero Section with Video Background */}
<HeroVideo />
{/* 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 */}
@@ -69,6 +90,9 @@ export default async function Homepage() {
</section>
)}
{/* How It Works */}
<HowItWorks />
{/* Brand Story Section */}
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
<div className="max-w-7xl mx-auto">
@@ -108,40 +132,58 @@ 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>
@@ -162,15 +204,15 @@ export default async function Homepage() {
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
</p>
{/* Newsletter Form - Centered */}
<form className="flex flex-col sm:flex-row items-center justify-center w-full sm:w-auto gap-0">
<form className="flex flex-col sm:flex-row items-stretch justify-center max-w-md mx-auto gap-0">
<input
type="email"
placeholder="Enter your email"
className="w-full sm:w-64 md:w-80 px-5 h-14 bg-white/10 border border-white/20 border-b-0 sm:border-b border-r-0 sm:border-r border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
className="flex-1 min-w-0 px-5 h-14 bg-white/10 border border-white/20 border-b-0 sm:border-b border-r-0 sm:border-r border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left rounded-t sm:rounded-l sm:rounded-tr-none"
/>
<button
type="submit"
className="w-full sm:w-auto px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap flex-shrink-0"
className="px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap flex-shrink-0 rounded-b sm:rounded-r sm:rounded-bl-none"
>
Subscribe
</button>

View File

@@ -24,7 +24,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<main className="min-h-screen bg-white">
{/* Page Header */}
<div className="pt-[140px] lg:pt-[160px]">
<div className="pt-[72px] lg:pt-[72px]">
<div className="border-b border-[#e5e5e5]">
<div className="container py-8 md:py-12">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">

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

@@ -0,0 +1,88 @@
"use client";
import { motion } from "framer-motion";
const mediaLogos = [
{ name: "VOGUE", style: "serif" },
{ name: "Allure", style: "sans" },
{ name: "ELLE", style: "serif" },
{ name: "COSMOPOLITAN", style: "serif" },
{ name: "Bazaar", style: "serif" },
{ name: "GLAMOUR", style: "serif" },
{ name: "WOMEN'S HEALTH", style: "sans" },
{ name: "Shape", style: "sans" },
];
function LogoItem({ name }: { name: string }) {
const isSerif = name === "VOGUE" || name === "ELLE" || name === "COSMOPOLITAN" || name === "Bazaar" || name === "GLAMOUR";
return (
<div className="flex items-center justify-center px-10 py-4 grayscale opacity-40 hover:grayscale-0 hover:opacity-100 transition-all duration-500 flex-shrink-0">
<span
className={`
text-xl md:text-2xl tracking-[0.15em] text-white font-bold
${isSerif ? 'font-serif italic' : 'font-sans uppercase'}
`}
style={{
textShadow: '0 0 20px rgba(255,255,255,0.1)',
}}
>
{name}
</span>
</div>
);
}
export default function AsSeenIn() {
return (
<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-[#c9a962] font-bold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
As Featured In
</motion.p>
</div>
{/* Scrolling Marquee */}
<div className="relative">
{/* Left gradient fade */}
<div className="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-[#1a1a1a] to-transparent z-10 pointer-events-none" />
{/* Right gradient fade */}
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-[#1a1a1a] to-transparent z-10 pointer-events-none" />
{/* Marquee container */}
<div className="flex overflow-hidden">
<motion.div
className="flex items-center gap-16"
animate={{
x: [0, -50 + "%"],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 30,
ease: "linear",
},
}}
>
{/* First set of logos */}
{mediaLogos.map((logo, index) => (
<LogoItem key={`first-${index}`} name={logo.name} />
))}
{/* Duplicate for seamless loop */}
{mediaLogos.map((logo, index) => (
<LogoItem key={`second-${index}`} name={logo.name} />
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,239 @@
"use client";
import { motion } from "framer-motion";
import { useState, useRef } from "react";
const results = [
{
id: 1,
name: "Facial Skin Transformation",
beforeImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_2.webp",
afterImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_2_1.webp",
timeline: "4-6 Weeks",
rating: 5,
reviewCount: 2847,
},
{
id: 2,
name: "Skin Radiance Transformation",
beforeImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_3.webp",
afterImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_3_1.webp",
timeline: "6-8 Weeks",
rating: 5,
reviewCount: 1856,
},
];
function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
const [sliderPosition, setSliderPosition] = useState(50);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
setSliderPosition(Math.max(0, Math.min(100, x)));
};
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = ((e.touches[0].clientX - rect.left) / rect.width) * 100;
setSliderPosition(Math.max(0, Math.min(100, x)));
};
return (
<div className="flex-1 min-w-0">
{/* Before/After Slider */}
<div
ref={containerRef}
className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl cursor-ew-resize select-none"
onMouseMove={handleMouseMove}
onTouchMove={handleTouchMove}
>
{/* After Image */}
<img
src={result.afterImg}
alt="After - Smooth skin"
className="absolute inset-0 w-full h-full object-cover"
/>
{/* Before Image (clipped) */}
<div
className="absolute inset-0 overflow-hidden"
style={{ width: `${sliderPosition}%` }}
>
<img
src={result.beforeImg}
alt="Before - Wrinkled skin"
className="absolute inset-0 h-full object-cover"
style={{ width: `${100 / (sliderPosition / 100)}%`, maxWidth: 'none' }}
/>
</div>
{/* Slider Handle */}
<div
className="absolute top-0 bottom-0 w-1 bg-white shadow-lg cursor-ew-resize"
style={{ left: `${sliderPosition}%`, transform: 'translateX(-50%)' }}
>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center">
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
</svg>
</div>
</div>
{/* Labels */}
<div className="absolute top-3 left-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
BEFORE
</div>
<div className="absolute top-3 right-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
AFTER
</div>
</div>
{/* Timeline and Rating */}
<div className="flex items-center justify-center gap-4 mt-4">
<div className="flex items-center gap-1.5">
<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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-xs font-medium">{result.timeline}</span>
</div>
<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-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>
<span className="text-xs text-[#666666]">({result.reviewCount.toLocaleString()})</span>
</div>
</div>
{/* Verified Badge */}
<div className="flex items-center justify-center gap-1.5 mt-2">
<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>
</div>
);
}
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">
<motion.div
className="text-center mb-12"
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">
Real Results
</span>
<h2 className="text-3xl md:text-4xl font-medium mb-4">
See the Transformation
</h2>
</motion.div>
{/* 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}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: index * 0.1 }}
className="flex-1 min-w-0"
>
<BeforeAfterSlider result={result} />
</motion.div>
))}
</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"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
>
<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"
>
Start Your Transformation
</a>
</motion.div>
</div>
</section>
);
}

View File

@@ -13,85 +13,113 @@ 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/30 via-black/20 to-black/50" />
</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={{
backgroundImage: `url('https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop')`,
}}
>
<div className="absolute inset-0 bg-black/40" />
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" />
</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 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="max-w-4xl mx-auto"
>
{/* Tagline */}
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="inline-block text-xs md:text-sm uppercase tracking-[0.3em] mb-6 text-white/90"
>
Premium Organic Oils
</motion.span>
{/* Main Heading */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="text-5xl md:text-7xl lg:text-8xl font-medium mb-6 tracking-tight"
>
ManoonOils
</motion.h1>
{/* Subtitle */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.9 }}
className="text-lg md:text-xl text-white/80 mb-10 font-light max-w-xl mx-auto"
>
For hair and skin care
</motion.p>
{/* CTA Button */}
{/* Social Proof Micro */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1.1 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="flex items-center justify-center gap-2 mb-6"
>
<div className="flex">
{[1, 2, 3, 4, 5].map((star) => (
<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>
))}
</div>
<span className="text-sm text-white/80">
Loved by 50,000+ customers worldwide
</span>
</motion.div>
{/* Main Heading - Outcome Focused */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight"
>
Transform Your Hair & Skin
<br />
<span className="text-white/90">with 100% Natural Oils</span>
</motion.h1>
{/* Subtitle - Expands on how */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.7 }}
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed"
>
Cold-pressed, organic oils handcrafted with love.
No additives, no preservativesjust nature&apos;s purest goodness for your daily beauty ritual.
</motion.p>
{/* CTA Button - Action verb + value */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.9 }}
className="flex flex-col sm:flex-row items-center justify-center gap-4"
>
<Link
href="/products"
className="inline-block px-10 py-4 bg-white text-black text-[13px] uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors duration-300"
className="inline-block px-10 py-4 bg-white text-black text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-white/90 transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl"
>
Shop Now
Transform My Hair & Skin
</Link>
<Link
href="/about"
className="inline-block px-10 py-4 border border-white/50 text-white text-[13px] uppercase tracking-[0.15em] font-medium hover:bg-white/10 transition-all duration-300"
>
Learn Our Story
</Link>
</motion.div>
{/* Trust Indicators */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.2, duration: 0.8 }}
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60"
>
<div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>30-Day Money Back</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
</svg>
<span>Free Shipping Over 3,000 RSD</span>
</div>
<div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
<span>Cruelty Free</span>
</div>
</motion.div>
</motion.div>
</div>
@@ -102,7 +130,7 @@ export default function HeroVideo() {
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 0.8 }}
onClick={scrollToContent}
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/80 hover:text-white transition-colors cursor-pointer"
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer"
aria-label="Scroll to content"
>
<motion.div

View File

@@ -0,0 +1,128 @@
"use client";
import { motion } from "framer-motion";
export default function HowItWorks() {
const steps = [
{
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.",
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-gradient-to-b from-white to-[#faf9f7]">
<div className="container mx-auto px-4">
<motion.div
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.3em] text-[#c9a962] mb-4 block font-medium">
Simple Process
</span>
<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-16 max-w-6xl mx-auto">
{steps.map((step, index) => (
<motion.div
key={index}
className="relative text-center group"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.15 }}
>
{/* Connector line (not on last item) */}
{index < steps.length - 1 && (
<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>
)}
{/* 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>
{/* 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-20"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<a
href="/products"
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"
>
<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>
</section>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { motion } from "framer-motion";
export default function ProblemSection() {
return (
<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"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<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 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-6 lg:gap-8 max-w-5xl mx-auto mt-16">
{[
{
icon: (
<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",
description: "Products leave your hair brittle, frizzy, and breaking despite expensive treatments",
},
{
icon: (
<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",
description: "Can't pronounce what's in your skincare. parabens, sulfates, synthetic fragrances—dangerous toxins",
},
{
icon: (
<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",
description: "Countless products promise miracles but deliver nothing but empty promises and wasted money",
},
].map((item, index) => (
<motion.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"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -5 }}
>
{/* 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-semibold text-[#1a1a1a] mb-3">{item.problem}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{item.description}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { motion } from "framer-motion";
const badges = [
{
icon: (
<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 1000+ reviews",
},
{
icon: (
<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>
),
stats: "50,000+",
label: "Happy Customers",
subtext: "Worldwide",
},
{
icon: (
<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>
),
stats: "100%",
label: "Natural Ingredients",
subtext: "No additives",
},
{
icon: (
<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>
),
stats: "Free",
label: "Shipping",
subtext: "Orders over 3,000 RSD",
},
];
export default function TrustBadges() {
return (
<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-4 lg:gap-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{badges.map((badge, index) => (
<motion.div
key={index}
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-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-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
{badge.stats}
</p>
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
{badge.label}
</p>
<p className="text-xs text-[#888888] mt-0.5">
{badge.subtext}
</p>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -53,68 +53,66 @@ export default function Header() {
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-white/95 backdrop-blur-md shadow-sm"
: "bg-transparent"
: "bg-white/80 backdrop-blur-sm"
}`}
>
<div className="container">
<div className="flex items-center justify-between h-[72px]">
{/* Mobile Menu Button */}
<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"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
>
<Menu className="w-5 h-5" />
</button>
{/* Left side - Desktop Nav */}
<nav className="hidden lg:flex items-center gap-10">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
>
{link.label}
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
</Link>
))}
</nav>
{/* Logo - Centered (absolute on desktop, flex on mobile) */}
<Link href="/" className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
width={150}
height={40}
className="h-7 w-auto object-contain"
priority
/>
</Link>
{/* Right side - Icons */}
<div className="flex items-center gap-1">
<button
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
aria-label="Account"
>
<Menu className="w-5 h-5" />
<User className="w-5 h-5" strokeWidth={1.5} />
</button>
{/* Logo */}
<Link href="/" className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
width={150}
height={40}
className="h-7 w-auto object-contain"
priority
/>
</Link>
{/* Desktop Navigation - Centered */}
<nav className="hidden lg:flex items-center gap-10 mx-auto">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
>
{link.label}
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
</Link>
))}
</nav>
{/* Icons */}
<div className="flex items-center gap-1">
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
aria-label="Account"
>
<User className="w-5 h-5" strokeWidth={1.5} />
</button>
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
onClick={toggleCart}
aria-label="Open cart"
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
{itemCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
{itemCount > 99 ? "99+" : itemCount}
</span>
)}
</button>
</div>
<button
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
onClick={toggleCart}
aria-label="Open cart"
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
{itemCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
{itemCount > 99 ? "99+" : itemCount}
</span>
)}
</button>
</div>
</div>
</header>

View File

@@ -0,0 +1,93 @@
"use client";
import { motion } from "framer-motion";
interface ProductBenefitsProps {
locale?: string;
}
const benefits = [
{
icon: (
<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",
description: "100% natural ingredients with no additives or preservatives",
},
{
icon: (
<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",
description: "Never tested on animals, ethically sourced ingredients",
},
{
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 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",
description: "Handcrafted in small batches for maximum quality",
},
{
icon: (
<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",
description: "See noticeable improvements in 4-6 weeks",
},
];
export default function ProductBenefits({ locale = "SR" }: ProductBenefitsProps) {
return (
<section className="py-20 bg-gradient-to-b from-white to-[#faf9f7]">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-12"
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-[#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">
{locale === "EN" ? "The Manoon Difference" : "Manoon razlika"}
</h2>
</motion.div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 max-w-5xl mx-auto">
{benefits.map((benefit, index) => (
<motion.div
key={index}
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-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 text-[#1a1a1a]">{benefit.title}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{benefit.description}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

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,14 +1,21 @@
"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";
import AsSeenIn from "@/components/home/AsSeenIn";
import TrustBadges from "@/components/home/TrustBadges";
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
import HowItWorks from "@/components/home/HowItWorks";
import NewsletterSection from "@/components/home/NewsletterSection";
interface ProductDetailProps {
product: Product;
@@ -69,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>
);
@@ -84,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];
@@ -108,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
@@ -145,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 && (
@@ -160,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>
))}
@@ -173,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>
@@ -201,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" />
@@ -270,16 +362,16 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
</div>
</div>
{/* Add to Cart Button */}
{/* Add to Cart Button - Action verb + value */}
{isAvailable ? (
<button
onClick={handleAddToCart}
disabled={isAdding}
className="w-full h-16 bg-black text-white text-base uppercase tracking-[0.15em] font-medium hover:bg-[#333333] active:bg-[#1a1a1a] transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-8"
className="w-full h-16 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333333] active:bg-[#1a1a1a] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed mb-6 hover:scale-[1.02] shadow-lg hover:shadow-xl"
>
{isAdding
? (locale === "EN" ? "Adding..." : "Dodavanje...")
: (locale === "EN" ? "Add to Cart — Free Shipping" : "Dodaj u korpu — Besplatna dostava")
: (locale === "EN" ? "Transform My Hair & Skin" : "Transformiši kosu i kožu")
}
</button>
) : (
@@ -288,12 +380,45 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
</div>
)}
{/* Free Shipping Note */}
<p className="text-center text-sm text-[#666666] mb-10">
{locale === "EN"
? "Free shipping on orders over 3,000 RSD"
: "Besplatna dostava za porudžbine preko 3.000 RSD"}
</p>
{/* Free Shipping Note - with urgency */}
<div className="flex items-center justify-center gap-2 mb-6">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<p className="text-sm text-[#666666]">
{locale === "EN"
? "Free shipping on orders over 3,000 RSD"
: "Besplatna dostava za porudžbine preko 3.000 RSD"}
</p>
</div>
{/* Trust Indicators */}
<div className="grid grid-cols-3 gap-4 mb-8 p-4 bg-[#f8f9fa] rounded-lg">
<div className="text-center">
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} 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>
<p className="text-xs text-[#666666]">
{locale === "EN" ? "30-Day Guarantee" : "30-dnevna garancija"}
</p>
</div>
<div className="text-center">
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
<p className="text-xs text-[#666666]">
{locale === "EN" ? "Secure Checkout" : "Sigurno plaćanje"}
</p>
</div>
<div className="text-center">
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-8m15.357 8H15" />
</svg>
<p className="text-xs text-[#666666]">
{locale === "EN" ? "Easy Returns" : "Lak povrat"}
</p>
</div>
</div>
{/* Divider */}
<div className="border-t border-[#e5e5e5] mb-8" />
@@ -353,6 +478,15 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
</div>
</section>
{/* Customer Reviews */}
<ProductReviews locale={locale} productName={localized.name} />
{/* As Featured In - Full Width */}
<AsSeenIn />
{/* Before/After Gallery */}
<BeforeAfterGallery />
{/* Related Products */}
{relatedProducts && relatedProducts.length > 0 && (
<section className="py-20 lg:py-28 bg-[#f8f9fa]">
@@ -365,19 +499,32 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
<div className="flex flex-wrap justify-center gap-6 lg:gap-8">
{relatedProducts.filter(p => p && p.id).slice(0, 4).map((relatedProduct, index) => (
<ProductCard
key={relatedProduct.id}
product={relatedProduct}
index={index}
locale={locale}
/>
<div key={relatedProduct.id} className="w-full sm:w-[calc(50%-12px)] lg:w-[calc(25%-18px)]">
<ProductCard
product={relatedProduct}
index={index}
locale={locale}
/>
</div>
))}
</div>
</div>
</section>
)}
{/* Product Benefits */}
<ProductBenefits locale={locale} />
{/* Trust Badges */}
<TrustBadges />
{/* How It Works */}
<HowItWorks />
{/* Newsletter */}
<NewsletterSection />
</>
);
}

View File

@@ -0,0 +1,181 @@
"use client";
import { motion } from "framer-motion";
interface ProductReviewsProps {
locale?: string;
productName?: string;
}
const reviews = [
{ id: 1, name: "Ana M.", location: "Belgrade", text: "Manoon Anti-age Serum transformed my skin in just 2 weeks!", rating: 5 },
{ id: 2, name: "Milica P.", location: "Novi Sad", text: "The best day serum I've ever used. My wrinkles are visibly reduced.", rating: 5 },
{ id: 3, name: "Jelena K.", location: "Belgrade", text: "Manoon night serum is pure magic. Wake up with glowing skin every morning.", rating: 5 },
{ id: 4, name: "Stefan R.", location: "Subotica", text: "The Anti-age Set is worth every dinar. My wife and I both use it.", rating: 5 },
{ id: 5, name: "Marija T.", location: "Kragujevac", text: "Finally found a serum that actually works! Manoon delivers on its promises.", rating: 5 },
{ id: 6, name: "Nikola V.", location: "Niš", text: "My fine lines are disappearing. This day serum is incredible.", rating: 5 },
{ id: 7, name: "Ivana L.", location: "Belgrade", text: "Manoon morning glow serum smells divine and works even better.", rating: 5 },
{ id: 8, name: "Dejan M.", location: "Novi Sad", text: "The night serum has transformed my skincare routine completely.", rating: 5 },
{ id: 9, name: "Sanja B.", location: "Kragujevac", text: "My skin looks 10 years younger after using Manoon for a month.", rating: 5 },
{ id: 10, name: "Marko J.", location: "Subotica", text: "The anti-age set makes a perfect gift. My mother loves it!", rating: 5 },
{ id: 11, name: "Petra D.", location: "Niš", text: "The texture of Manoon serum is so luxurious. Worth every penny.", rating: 5 },
{ id: 12, name: "Luka G.", location: "Belgrade", text: "Day serum absorbs instantly. No greasy feeling at all!", rating: 5 },
{ id: 13, name: "Maja S.", location: "Novi Sad", text: "My esthetician asked what I'm using. Manoon is now my secret!", rating: 5 },
{ id: 14, name: "Vladimir P.", location: "Kragujevac", text: "The night serum works while I sleep. Wake up to visibly smoother skin.", rating: 5 },
{ id: 15, name: "Katarina N.", location: "Subotica", text: "The Anti-age Set arrived beautifully packaged. Perfect for gifting.", rating: 5 },
{ id: 16, name: "Bojan R.", location: "Niš", text: "Been using Manoon for 3 months. My wrinkles are noticeably reduced.", rating: 5 },
{ id: 17, name: "Tamara F.", location: "Belgrade", text: "The day serum provides the perfect base under makeup.", rating: 5 },
{ id: 18, name: "Aleksandar K.", location: "Novi Sad", text: "Finally a Serbian brand that competes with luxury international brands!", rating: 5 },
{ id: 19, name: "Natalia M.", location: "Kragujevac", text: "My sensitive skin loves Manoon. No irritation at all.", rating: 5 },
{ id: 20, name: "Filip T.", location: "Subotica", text: "The anti-age serum is lightweight yet incredibly effective.", rating: 5 },
{ id: 21, name: "Andrea L.", location: "Niš", text: "Manoon night serum is my evening ritual. Skin looks amazing!", rating: 5 },
{ id: 22, name: "Ognjen P.", location: "Belgrade", text: "My friends keep asking what changed in my skincare routine.", rating: 5 },
{ id: 23, name: "Mila J.", location: "Novi Sad", text: "The Anti-age Set includes everything you need. Great value!", rating: 5 },
{ id: 24, name: "Dragan S.", location: "Kragujevac", text: "Even my husband noticed the difference. He now uses the day serum too!", rating: 5 },
{ id: 25, name: "Jovana V.", location: "Subotica", text: "The morning glow serum gives the most beautiful luminosity.", rating: 5 },
{ id: 26, name: "Stefan M.", location: "Niš", text: "Manoon products are now essential in my daily routine.", rating: 5 },
{ id: 27, name: "Ana R.", location: "Belgrade", text: "The night serum helped clear my complexion. Skin looks so healthy!", rating: 5 },
{ id: 28, name: "Nenad L.", location: "Novi Sad", text: "Anti-aging results visible within weeks. Highly recommend Manoon!", rating: 5 },
{ id: 29, name: "Sofija D.", location: "Kragujevac", text: "The texture is divine. Feels like a luxury spa treatment at home.", rating: 5 },
{ id: 30, name: "Velibor K.", location: "Subotica", text: "My crow's feet have diminished significantly. Thank you Manoon!", rating: 5 },
{ id: 31, name: "Irena M.", location: "Niš", text: "The Anti-age Set makes the perfect birthday gift for my mother.", rating: 5 },
{ id: 32, name: "Radoslav P.", location: "Belgrade", text: "Professional quality serum at an honest price. Serbian excellence!", rating: 5 },
{ id: 33, name: "Jelena B.", location: "Novi Sad", text: "My skin has never been this hydrated. Day serum is amazing!", rating: 5 },
{ id: 34, name: "Dimitrije S.", location: "Kragujevac", text: "The night serum is worth its weight in gold. Pure luxury!", rating: 5 },
{ id: 35, name: "Minela G.", location: "Subotica", text: "Manoon lives up to the hype. My skin looks refreshed and young.", rating: 5 },
{ id: 36, name: "Zoran T.", location: "Niš", text: "I've tried many serums. Manoon is by far the most effective.", rating: 5 },
{ id: 37, name: "Mirjana F.", location: "Belgrade", text: "The Anti-age Set transformed my mother's skincare routine completely.", rating: 5 },
{ id: 38, name: "Ivan J.", location: "Novi Sad", text: "Fast-acting serum with real results. I recommend Manoon to everyone.", rating: 5 },
{ id: 39, name: "Kristina P.", location: "Kragujevac", text: "The morning glow serum gives such a beautiful dewy finish.", rating: 5 },
{ id: 40, name: "Bratislav L.", location: "Subotica", text: "Noticeable results in just 2 weeks. This serum is the real deal!", rating: 5 },
{ id: 41, name: "Zorica M.", location: "Niš", text: "The night serum erased years from my face. Absolutely miraculous!", rating: 5 },
{ id: 42, name: "Patrik N.", location: "Belgrade", text: "Premium quality Serbian skincare that rivals international luxury brands.", rating: 5 },
{ id: 43, name: "Simona K.", location: "Novi Sad", text: "Manoon Anti-age Serum is the best investment in my skin ever.", rating: 5 },
{ id: 44, name: "Mladen D.", location: "Kragujevac", text: "The day serum absorbs in seconds. No waiting around!", rating: 5 },
{ id: 45, name: "Ljiljana R.", location: "Subotica", text: "Gifting the Anti-age Set to my sisters. They loved it!", rating: 5 },
{ id: 46, name: "Tomislav V.", location: "Niš", text: "My wrinkles are visibly reduced after using Manoon for a month.", rating: 5 },
{ id: 47, name: "Emilija S.", location: "Belgrade", text: "The night serum leaves my skin so soft and renewed every morning.", rating: 5 },
{ id: 48, name: "Andrija P.", location: "Novi Sad", text: "Manoon day serum is perfect under sunscreen. Essential duo!", rating: 5 },
{ id: 49, name: "Miona L.", location: "Kragujevac", text: "My skin looks radiant and youthful. Couldn't be happier with Manoon!", rating: 5 },
{ id: 50, name: "Slavko M.", location: "Subotica", text: "The Anti-age Set delivers visible results. True Serbian quality!", rating: 5 },
];
function ReviewCard({ review }: { review: typeof reviews[0] }) {
return (
<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-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-[#444444] text-sm leading-relaxed mb-4">"{review.text}"</p>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white text-sm font-medium">
{review.name.charAt(0)}
</div>
<div>
<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>
</div>
);
}
export default function ProductReviews({ locale = "SR", productName = "this product" }: ProductReviewsProps) {
return (
<section className="py-16 bg-[#faf9f7] overflow-hidden">
<div className="container mx-auto px-4 mb-8">
<motion.div
className="text-center"
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">
Customer Reviews
</span>
<h2 className="text-3xl md:text-4xl font-medium">
What Customers Say
</h2>
<div className="flex items-center justify-center gap-4 mt-4">
<span className="text-5xl font-bold text-[#1a1a1a]">4.9</span>
<div>
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((star) => (
<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 1000+ reviews</p>
</div>
</div>
</motion.div>
</div>
{/* Scrolling Reviews Marquee */}
<div className="relative">
{/* Left gradient fade */}
<div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-[#faf9f7] to-transparent z-10 pointer-events-none" />
{/* Right gradient fade */}
<div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-[#faf9f7] to-transparent z-10 pointer-events-none" />
{/* First row - left to right */}
<div className="flex overflow-hidden mb-4">
<motion.div
className="flex items-center gap-0"
animate={{
x: [0, -50 + "%"],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 120,
ease: "linear",
},
}}
>
{[...reviews, ...reviews].map((review, index) => (
<ReviewCard key={`first-${index}-${review.id}`} review={review} />
))}
</motion.div>
</div>
{/* Second row - right to left */}
<div className="flex overflow-hidden">
<motion.div
className="flex items-center gap-0"
animate={{
x: [-50 + "%", 0],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 120,
ease: "linear",
},
}}
>
{[...reviews.slice(25), ...reviews.slice(0, 25), ...reviews.slice(25), ...reviews.slice(0, 25)].map((review, index) => (
<ReviewCard key={`second-${index}-${review.id}`} review={review} />
))}
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -24,12 +24,8 @@ export const saleorClient = new ApolloClient({
fields: {
products: {
keyArgs: ["channel", "filter"],
merge(existing, incoming) {
if (!existing) return incoming;
return {
...incoming,
edges: [...existing.edges, ...incoming.edges],
};
merge(_existing, incoming) {
return incoming;
},
},
},

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],
},
});