32 Commits

Author SHA1 Message Date
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
Unchained
7c05bd2346 Redesign phase 1: Homepage polish and design system foundation
- Fix newsletter subscribe box centering on homepage
- Fix header overlap on product pages (pt-[72px] instead of pt-[100px])
- Add scroll-mt-[72px] for smooth scroll anchor offset
- Add HeroVideo component with video hero placeholder
- Add REDESIGN_SPECIFICATION.md with 9-phase design plan
- Clean up globals.css theme declarations and comments
- Update Header with improved sticky behavior and cart
- Update ProductDetail with better layout and spacing
- Update CartDrawer with improved slide-out cart UI
- Add English translations for updated pages
- Various CSS refinements across pages
2026-03-21 16:22:17 +02:00
Unchained
9d639fbd64 fix: Parse JSON description in NewHero component
- Import parseDescription in NewHero.tsx
- Use parseDescription for featured product description
2026-03-21 13:12:30 +02:00
Unchained
0831968881 fix: Suppress hydration warnings from browser extensions
- Add suppressHydrationWarning to html and body elements
- Prevents FoxClocks and other extensions from causing errors
- Extensions modifying DOM won't break React hydration
2026-03-21 13:09:31 +02:00
Unchained
3aaad57076 fix: Parse Saleor JSON description format to plain text
- Add parseDescription() helper to extract text from EditorJS JSON
- Update getLocalizedProduct to use parsed description
- Fix product descriptions showing raw JSON on frontend
2026-03-21 13:06:14 +02:00
Unchained
01d553bfea fix: Add error boundary to handle browser extension errors
- Create ErrorBoundary component to catch extension errors
- Ignore TronLink and other chrome-extension errors
- Prevent extension conflicts from crashing the app
2026-03-21 13:02:55 +02:00
Unchained
a47698d5ca fix(saleor): Fix remaining WooCommerce references and configuration
- Fix syntax error in Checkout.ts (extra semicolon)
- Update NewHero.tsx to use Saleor types and store
- Update page.tsx to use Saleor getProducts
- Add Saleor API domain to next.config.ts images config
2026-03-21 13:00:16 +02:00
Unchained
1b733c63d5 feat(saleor): Phase 5 - Remove WooCommerce
- Remove @woocommerce/woocommerce-rest-api dependency
- Delete src/lib/woocommerce.ts
- Delete src/stores/cartStore.ts (replaced by saleorCheckoutStore)
- Clean up package.json dependencies
- Project now fully migrated to Saleor GraphQL API
2026-03-21 12:45:56 +02:00
Unchained
d43481716d feat(saleor): Phase 4 - Checkout Flow
- Create checkout page with form validation
- Implement shipping/billing address forms
- Add Cash on Delivery (COD) payment method
- Integrate Saleor checkout completion mutation
- Add order success page with confirmation
- Handle checkout errors gracefully
- Display order summary with line items
2026-03-21 12:45:09 +02:00
Unchained
8b3389725e feat(saleor): Phase 3 - Cart Migration
- Create Saleor checkout store (Zustand + persist)
- Update CartDrawer to use Saleor checkout
- Update Header to use Saleor checkout store
- Update ProductDetail with Add to Cart functionality
- Add checkout initialization on app load
- Handle checkout line add/update/delete operations
- Add error handling and loading states
2026-03-21 12:42:41 +02:00
Unchained
5706792980 feat(saleor): Phase 2 - Product Migration
- Update ProductCard to use Saleor Product type
- Update products listing page to fetch from Saleor
- Update product detail page with Saleor integration
- Add language switching support (SR/EN)
- Add SEO metadata generation
- Implement static params generation for all product slugs
- Add availability checking based on variant quantity
2026-03-21 12:38:24 +02:00
41 changed files with 4626 additions and 1498 deletions

444
REDESIGN_SPECIFICATION.md Normal file
View File

@@ -0,0 +1,444 @@
# ManoonOils Redesign Specification
## Inspired by moumoujus.com Premium Skincare Aesthetic
---
## Design Analysis Summary
### Key Visual Elements from moumoujus.com:
1. **Hero Section**: Full-screen video background with autoplay, muted, loop
2. **Navigation**: Minimalist sticky header with logo left, nav center, icons right
3. **Typography**: Clean sans-serif, generous letter-spacing, all-caps for headings
4. **Color Palette**:
- White/Off-white backgrounds
- Soft blue-gray accents (#e8f0f5 range)
- Black for CTAs and text
- Gold/bronze highlights for luxury feel
5. **Product Pages**: Two-column layout, vertical thumbnails, expandable sections
6. **Cart**: Slide-out drawer from right
---
## Phase 1: Global Design System & Theme
### Color Palette Refinement
```
Primary:
- Background: #ffffff (pure white)
- Background-alt: #f8f9fa (soft gray-white)
- Text: #1a1a1a (near black)
- Text-muted: #666666 (gray)
Accent:
- Accent-blue: #e8f0f5 (soft blue-gray)
- Accent-blue-dark: #a8c5d8
- CTA-black: #000000
- Gold: #c9a962 (for awards/accents)
UI:
- Border: #e5e5e5
- Border-dark: #d1d1d1
```
### Typography System
```
Display Font: Inter or DM Sans (clean, modern)
- H1: 48px/56px, font-weight: 500, letter-spacing: -0.02em
- H2: 36px/44px, font-weight: 500
- H3: 24px/32px, font-weight: 500
- Body: 16px/24px
- Small: 14px/20px
- Caption: 12px/16px, uppercase, letter-spacing: 0.1em
```
### Spacing System
```
- xs: 4px
- sm: 8px
- md: 16px
- lg: 24px
- xl: 32px
- 2xl: 48px
- 3xl: 64px
- 4xl: 96px
- 5xl: 128px
```
### TODOs:
- [ ] Update CSS variables in globals.css
- [ ] Define new color tokens
- [ ] Update font system (keep DM Sans, add Inter for UI)
- [ ] Create design token file
- [ ] Update Tailwind theme config
---
## Phase 2: Navigation & Header Redesign
### Header Layout (inspired by moumoujus.com)
```
[Logo] [Shop] [About] [Library] [Contact] [Account] [Cart (0)]
```
### Specifications:
- **Height**: 72px desktop, 64px mobile
- **Background**: White with subtle bottom border (#e5e5e5)
- **Position**: Sticky top-0 (not 10px offset like current)
- **Logo**: Centered on mobile, left on desktop
- **Nav Links**: Centered, uppercase, letter-spacing: 0.05em, font-size: 13px
- **Icons**: User outline, Shopping bag outline
- **Cart Badge**: Small dot or number in circle
### Mobile Menu:
- Full-screen overlay
- Large typography for nav links
- Close button top right
- Social links at bottom
### TODOs:
- [ ] Redesign Header.tsx with new layout
- [ ] Update MobileMenu.tsx with full-screen overlay
- [ ] Implement sticky header behavior
- [ ] Add scroll-based background change (transparent → white)
- [ ] Update cart icon with new design
- [ ] Add hover states for nav links (underline animation)
---
## Phase 3: Homepage Hero with Video Background
### Hero Section Specifications:
```
┌─────────────────────────────────────────────────────┐
│ [Video Background - Full Screen] │
│ │
│ │
│ [Product Shot or Lifestyle Video] │
│ │
│ │
│ [Brand Tagline] │
│ PREMIUM ORGANIC OILS │
│ │
│ [Shop Now Button - Black] │
└─────────────────────────────────────────────────────┘
```
### Technical Requirements:
- Video: MP4/WebM format, 1920x1080, <5MB
- Autoplay, muted, loop, playsinline
- Poster image for loading state
- Gradient overlay for text readability
- Text centered, white color
- Scroll indicator at bottom
### TODOs:
- [ ] Create new HeroVideo component
- [ ] Add video asset (placeholder for now)
- [ ] Implement video background with overlay
- [ ] Add centered text content with animation
- [ ] Create scroll-down indicator
- [ ] Add poster image fallback
- [ ] Ensure mobile fallback (image instead of video)
---
## Phase 4: Product Detail Page Redesign
### Layout Structure (Two-Column):
```
┌─────────────────────────────────────────────────────┐
│ [Header - Sticky] │
├─────────────────────────────────────────────────────┤
│ Home / [Product Name] │
├──────────────────────┬──────────────────────────────┤
│ │ │
│ [Thumbnail 1] │ [Award Badge - optional] │
│ [Thumbnail 2] │ │
│ [Thumbnail 3] │ PRODUCT NAME │
│ │ Short description │
│ [Main Image] │ │
│ [Large, centered] │ £XX.00 ★★★★★ (12) │
│ │ │
│ │ ────────────────────── │
│ │ SIZE │
│ │ [50ml] [100ml] [250ml] │
│ │ ────────────────────── │
│ │ │
│ │ [ADD TO CART - FREE │
│ │ SHIPPING - Black Button] │
│ │ │
│ │ ────────────────────── │
│ │ BENEFITS │
│ │ [Tag 1] [Tag 2] [Tag 3] │
│ │ ────────────────────── │
│ │ DESCRIPTION [+] │
│ │ ────────────────────── │
│ │ HOW TO USE [+] │
│ │ ────────────────────── │
│ │ INGREDIENTS [+] │
│ │ │
└──────────────────────┴──────────────────────────────┘
```
### Component Specifications:
#### Image Gallery:
- Vertical thumbnail list on left (desktop)
- Horizontal thumbnails below (mobile)
- Click to change main image
- Zoom on hover (optional)
- Smooth transitions
#### Product Info:
- Breadcrumb: Home / [Product Name]
- Product name: 24-32px, font-weight: 500
- Short description below name
- Price + reviews on same line
- Size selector: Pill buttons
- CTA: Full-width black button
#### Expandable Sections:
- Accordion style
- Plus/minus icons
- Smooth expand/collapse animation
- Content: Description, How to Use, Ingredients
### TODOs:
- [ ] Redesign ProductDetail.tsx with new two-column layout
- [ ] Create ProductImageGallery component with vertical thumbnails
- [ ] Add breadcrumb navigation
- [ ] Create size selector component (pill buttons)
- [ ] Implement expandable accordion sections
- [ ] Add benefits/tags display
- [ ] Style "Add to Cart" button (black, full-width)
- [ ] Add star rating component
- [ ] Make layout responsive
---
## Phase 5: Product Listing/Shop Page
### Layout:
```
┌─────────────────────────────────────────────────────┐
│ [Header] │
├─────────────────────────────────────────────────────┤
│ All Products [Sort]
├─────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ [Image] │ │ [Image] │ │ [Image] │ │
│ │ │ │ │ │ │ │
│ │ Product │ │ Product │ │ Product │ │
│ │ £XX.00 │ │ £XX.00 │ │ £XX.00 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ [Load More / Pagination] │
└─────────────────────────────────────────────────────┘
```
### Product Card Specifications:
- Image: Square aspect ratio, object-cover
- Product name: 14-16px, single line, truncate
- Price: 14px, below name
- Hover: Slight image zoom, shadow
- Clean white background
### TODOs:
- [ ] Redesign ProductCard.tsx
- [ ] Create grid layout (3 columns desktop, 2 tablet, 1 mobile)
- [ ] Add sorting dropdown
- [ ] Implement hover effects
- [ ] Add pagination or infinite scroll
---
## Phase 6: Cart Drawer & Checkout Flow
### Cart Drawer Design:
```
┌──────────────────────────────────┐
│ YOUR CART [X] │
├──────────────────────────────────┤
│ │
│ ┌────┐ Product Name 🗑️ │
│ │IMG │ Variant info │
│ └────┤ [-] 1 [+] £XX.00 │
│ │
│ ─────────────────────────────── │
│ │
│ ┌────┐ Another Product │
│ │IMG │ [-] 2 [+] £XX.00 │
│ └────┘ │
│ │
├──────────────────────────────────┤
│ Subtotal £XX.00 │
│ Shipping FREE │
├──────────────────────────────────┤
│ TOTAL £XX.00 │
│ │
│ [CHECKOUT - Black Button] │
│ [Continue Shopping] │
└──────────────────────────────────┘
```
### Specifications:
- Slide in from right
- Width: 400px desktop, 100% mobile
- Backdrop blur/overlay
- Quantity controls (+/-)
- Remove item button
- Clear subtotal/total breakdown
- Prominent checkout CTA
### Checkout Page:
- Multi-step or single-page
- Shipping info
- Payment method (COD for Serbia)
- Order summary sidebar
### TODOs:
- [ ] Redesign CartDrawer.tsx with slide-out design
- [ ] Update cart item layout
- [ ] Add quantity stepper controls
- [ ] Style cart totals section
- [ ] Improve checkout button
- [ ] Add backdrop overlay
- [ ] Add empty cart state
- [ ] Test checkout flow end-to-end
---
## Phase 7: Footer & Trust Signals
### Footer Layout:
```
┌─────────────────────────────────────────────────────┐
│ │
│ [NEWSLETTER SECTION] │
│ Stay updated with our latest offers │
│ [Email Input] [Subscribe] │
│ │
├─────────────────────────────────────────────────────┤
│ │
│ SHOP ABOUT HELP SOCIAL │
│ - Products - Our Story - FAQ - IG │
│ - Bundles - Process - Shipping - FB │
│ - Gifts - Sourcing - Returns - X │
│ │
├─────────────────────────────────────────────────────┤
│ │
│ [Payment Icons] [Security Badges] │
│ │
│ © 2024 ManoonOils. All rights reserved. │
│ │
└─────────────────────────────────────────────────────┘
```
### Trust Signals to Add:
- Payment icons (Visa, Mastercard, PayPal)
- Security badges (SSL, Secure checkout)
- Shipping info
- Money-back guarantee
### TODOs:
- [ ] Redesign Footer.tsx
- [ ] Add newsletter signup section
- [ ] Create link columns
- [ ] Add payment/security badges
- [ ] Add social media links
- [ ] Style copyright section
---
## Phase 8: Mobile Responsive Optimization
### Breakpoints:
- Mobile: < 640px
- Tablet: 640px - 1024px
- Desktop: > 1024px
### Mobile-Specific Changes:
- Hamburger menu with full-screen overlay
- Single column product pages
- Bottom sticky add-to-cart bar
- Simplified navigation
- Touch-friendly tap targets (min 44px)
### TODOs:
- [ ] Test all pages on mobile viewport
- [ ] Add bottom sticky CTA on product pages
- [ ] Optimize images for mobile
- [ ] Ensure touch targets are 44px+
- [ ] Test mobile navigation flow
---
## Phase 9: Performance & SEO Polish
### Performance:
- Lazy load images
- Video optimization (WebM + MP4)
- Font preloading
- CSS optimization
### SEO:
- Meta titles/descriptions
- Structured data (Product schema)
- Open Graph tags
- Alt text for images
### TODOs:
- [ ] Add Next.js Image optimization
- [ ] Implement lazy loading
- [ ] Add meta tags for all pages
- [ ] Add JSON-LD structured data
- [ ] Optimize Core Web Vitals
- [ ] Add sitemap.xml
---
## Asset Requirements
### Images Needed:
1. Hero video (MP4/WebM, 1920x1080)
2. Hero poster image (fallback)
3. Product photography (high-res, consistent style)
4. Lifestyle images for homepage sections
### Icons (Lucide):
- All current icons are good
- May need: Award, Leaf, Droplet (for benefits)
---
## Implementation Order
### Week 1: Foundation
1. Phase 1: Design System
2. Phase 2: Navigation
### Week 2: Core Pages
3. Phase 3: Hero Video
4. Phase 4: Product Detail Page
### Week 3: E-commerce
5. Phase 5: Shop Page
6. Phase 6: Cart & Checkout
### Week 4: Polish
7. Phase 7: Footer
8. Phase 8: Mobile
9. Phase 9: Performance
---
## Success Metrics
- [ ] Homepage video loads < 3s
- [ ] Product page LCP < 2.5s
- [ ] Mobile score 90+ on Lighthouse
- [ ] All pages responsive
- [ ] Cart drawer works smoothly
- [ ] No console errors
- [ ] WCAG AA accessibility compliance

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

@@ -17,6 +17,16 @@ const nextConfig: NextConfig = {
hostname: "minio-api.nodecrew.me",
pathname: "/**",
},
{
protocol: "https",
hostname: "api.manoonoils.com",
pathname: "/**",
},
{
protocol: "https",
hostname: "**.saleor.cloud",
pathname: "/**",
},
],
},
};

372
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "0.1.0",
"dependencies": {
"@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.4",
"graphql": "^16.13.1",
@@ -2739,21 +2738,6 @@
"win32"
]
},
"node_modules/@woocommerce/woocommerce-rest-api": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@woocommerce/woocommerce-rest-api/-/woocommerce-rest-api-1.0.2.tgz",
"integrity": "sha512-G+0VwM0MINF83KnT7Rg/htm9EEYADWvDPT/UWEJdZ0de1vXvsPrr4M1ksKaxgKHO8qIJViRrIHCtrui2JoVA+Q==",
"license": "MIT",
"dependencies": {
"axios": "^1.6.8",
"create-hmac": "^1.1.7",
"oauth-1.0a": "^2.2.6",
"url-parse": "^1.4.7"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@wry/caches": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
@@ -3052,16 +3036,11 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
@@ -3083,17 +3062,6 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3185,6 +3153,7 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
@@ -3203,6 +3172,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -3216,6 +3186,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -3275,20 +3246,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/cipher-base": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
"integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.2"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3324,18 +3281,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3350,39 +3295,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/create-hash": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"license": "MIT",
"dependencies": {
"cipher-base": "^1.0.1",
"inherits": "^2.0.1",
"md5.js": "^1.3.4",
"ripemd160": "^2.0.1",
"sha.js": "^2.4.0"
}
},
"node_modules/create-hmac": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"license": "MIT",
"dependencies": {
"cipher-base": "^1.0.3",
"create-hash": "^1.1.0",
"inherits": "^2.0.1",
"ripemd160": "^2.0.0",
"safe-buffer": "^5.0.1",
"sha.js": "^2.4.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3501,6 +3413,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
@@ -3532,15 +3445,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3567,6 +3471,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@@ -3678,6 +3583,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3687,6 +3593,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -3724,6 +3631,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@@ -3736,6 +3644,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -4356,30 +4265,11 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
@@ -4391,22 +4281,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/framer-motion": {
"version": "12.34.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz",
@@ -4438,6 +4312,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -4498,6 +4373,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -4522,6 +4398,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@@ -4609,6 +4486,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4675,6 +4553,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
@@ -4703,6 +4582,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -4715,6 +4595,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@@ -4726,25 +4607,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash-base": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
"integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^2.3.8",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -4822,12 +4689,6 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4953,6 +4814,7 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -5209,6 +5071,7 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
@@ -5270,6 +5133,7 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -5764,22 +5628,12 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
"integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.0.0",
"inherits": "^2.0.1",
"safe-buffer": "^5.1.2"
}
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5804,27 +5658,6 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -6109,12 +5942,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/oauth-1.0a": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz",
"integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==",
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6387,6 +6214,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -6431,12 +6259,6 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6449,12 +6271,6 @@
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6465,12 +6281,6 @@
"node": ">=6"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
"license": "MIT"
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6520,33 +6330,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6591,12 +6374,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6649,19 +6426,6 @@
"node": ">=0.10.0"
}
},
"node_modules/ripemd160": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
"integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
"license": "MIT",
"dependencies": {
"hash-base": "^3.1.2",
"inherits": "^2.0.4"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6716,26 +6480,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6791,6 +6535,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
@@ -6835,26 +6580,6 @@
"node": ">= 0.4"
}
},
"node_modules/sha.js": {
"version": "2.4.12",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
"integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
"license": "(MIT AND BSD-3-Clause)",
"dependencies": {
"inherits": "^2.0.4",
"safe-buffer": "^5.2.1",
"to-buffer": "^1.2.0"
},
"bin": {
"sha.js": "bin.js"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/sharp": {
"version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -7042,21 +6767,6 @@
"node": ">= 0.4"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string_decoder/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7321,20 +7031,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-buffer": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
"integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
"license": "MIT",
"dependencies": {
"isarray": "^2.0.5",
"safe-buffer": "^5.2.1",
"typed-array-buffer": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7410,6 +7106,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
@@ -7623,16 +7320,6 @@
"punycode": "^2.1.0"
}
},
"node_modules/url-parse": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
"license": "MIT",
"dependencies": {
"querystringify": "^2.1.1",
"requires-port": "^1.0.0"
}
},
"node_modules/use-intl": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
@@ -7654,12 +7341,6 @@
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7747,6 +7428,7 @@
"version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",

View File

@@ -10,7 +10,6 @@
},
"dependencies": {
"@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1",
"framer-motion": "^12.34.4",
"graphql": "^16.13.1",

View File

@@ -8,59 +8,114 @@ export const metadata = {
export default function AboutPage() {
return (
<main className="min-h-screen pt-16 md:pt-20">
<>
<Header />
<section className="py-20 px-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
Our Story
</h1>
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6">
<p>
ManoonOils was born from a passion for natural beauty and the belief
that the best skincare comes from nature itself. Our journey began with
a simple question: how can we create products that truly nurture both
hair and skin?
</p>
<p>
We believe in the power of natural ingredients. Every oil in our
collection is carefully selected for its unique properties and
benefits. From nourishing oils that restore hair vitality to serums
that rejuvenate skin, we craft each product with love and attention
to detail.
</p>
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
Our Mission
</h2>
<p>
Our mission is to provide premium quality, natural products that
enhance your daily beauty routine. We are committed to:
</p>
<ul className="list-disc pl-6 space-y-2">
<li>Using only the finest natural ingredients</li>
<li>Cruelty-free and ethical production</li>
<li>Sustainable packaging practices</li>
<li>Transparency in our formulations</li>
</ul>
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
Handmade with Love
</h2>
<p>
Every bottle of ManoonOils is handcrafted with care. We small-batch
produce our products to ensure the highest quality and freshness.
When you use ManoonOils, you can feel confident that you're using
something made with genuine care and expertise.
</p>
<main className="min-h-screen bg-white">
{/* Page Header */}
<div className="pt-[104px]">
<div className="container py-12 md:py-16">
<div className="max-w-2xl mx-auto text-center">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
<h1 className="text-4xl md:text-5xl font-medium tracking-tight">
About ManoonOils
</h1>
</div>
</div>
</div>
{/* Hero Image */}
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
<img
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
alt="Natural oils production"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/20" />
</div>
{/* Content */}
<section className="py-16 md:py-24">
<div className="container">
<div className="max-w-3xl mx-auto">
{/* Introduction */}
<div className="mb-16">
<p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
ManoonOils was born from a passion for natural beauty and the belief
that the best skincare comes from nature itself.
</p>
<p className="text-[#666666] leading-relaxed">
We believe in the power of natural ingredients. Every oil in our
collection is carefully selected for its unique properties and
benefits. From nourishing oils that restore hair vitality to serums
that rejuvenate skin, we craft each product with love and attention
to detail.
</p>
</div>
{/* Values Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
<div className="p-6 bg-[#f8f9fa]">
<h3 className="text-lg font-medium mb-3">Natural Ingredients</h3>
<p className="text-[#666666] text-sm leading-relaxed">
We use only the finest natural ingredients, sourced ethically and sustainably
from trusted suppliers around the world.
</p>
</div>
<div className="p-6 bg-[#f8f9fa]">
<h3 className="text-lg font-medium mb-3">Cruelty-Free</h3>
<p className="text-[#666666] text-sm leading-relaxed">
Our products are never tested on animals. We believe in beauty
without compromise.
</p>
</div>
<div className="p-6 bg-[#f8f9fa]">
<h3 className="text-lg font-medium mb-3">Sustainable Packaging</h3>
<p className="text-[#666666] text-sm leading-relaxed">
We use eco-friendly packaging materials and minimize waste
throughout our production process.
</p>
</div>
<div className="p-6 bg-[#f8f9fa]">
<h3 className="text-lg font-medium mb-3">Handcrafted Quality</h3>
<p className="text-[#666666] text-sm leading-relaxed">
Every bottle is handcrafted in small batches to ensure
the highest quality and freshness.
</p>
</div>
</div>
{/* Mission */}
<div className="text-center py-12 border-t border-b border-[#e5e5e5]">
<span className="text-caption text-[#666666] mb-4 block">Our Mission</span>
<blockquote className="text-2xl md:text-3xl font-medium tracking-tight">
&ldquo;To provide premium quality, natural products that enhance
your daily beauty routine.&rdquo;
</blockquote>
</div>
{/* Story Section */}
<div className="mt-16">
<h2 className="text-2xl font-medium mb-6">Handmade with Love</h2>
<p className="text-[#666666] leading-relaxed mb-6">
Every bottle of ManoonOils is handcrafted with care. We small-batch
produce our products to ensure the highest quality and freshness.
When you use ManoonOils, you can feel confident that you&apos;re using
something made with genuine care and expertise.
</p>
<p className="text-[#666666] leading-relaxed">
Our journey began with a simple question: how can we create products
that truly nurture both hair and skin? Today, we continue to innovate
while staying true to our commitment to natural, effective beauty solutions.
</p>
</div>
</div>
</div>
</section>
<Footer />
</main>
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}

494
src/app/checkout/page.tsx Normal file
View File

@@ -0,0 +1,494 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { formatPrice } from "@/lib/saleor";
import { saleorClient } from "@/lib/saleor/client";
import {
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE,
} from "@/lib/saleor/mutations/Checkout";
import type { Checkout } from "@/types/saleor";
// GraphQL Response Types
interface ShippingAddressUpdateResponse {
checkoutShippingAddressUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface BillingAddressUpdateResponse {
checkoutBillingAddressUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutCompleteResponse {
checkoutComplete?: {
order?: { number: string };
errors?: Array<{ message: string }>;
};
}
interface AddressForm {
firstName: string;
lastName: string;
streetAddress1: string;
streetAddress2: string;
city: string;
postalCode: string;
phone: string;
}
export default function CheckoutPage() {
const router = useRouter();
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [orderComplete, setOrderComplete] = useState(false);
const [orderNumber, setOrderNumber] = useState<string | null>(null);
const [sameAsShipping, setSameAsShipping] = useState(true);
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
firstName: "",
lastName: "",
streetAddress1: "",
streetAddress2: "",
city: "",
postalCode: "",
phone: "",
});
const [billingAddress, setBillingAddress] = useState<AddressForm>({
firstName: "",
lastName: "",
streetAddress1: "",
streetAddress2: "",
city: "",
postalCode: "",
phone: "",
});
const lines = getLines();
const total = getTotal();
useEffect(() => {
if (!checkout) {
refreshCheckout();
}
}, [checkout, refreshCheckout]);
// Redirect if cart is empty
useEffect(() => {
if (lines.length === 0 && !orderComplete) {
// Optionally redirect to cart or products
// router.push("/products");
}
}, [lines, orderComplete, router]);
const handleShippingChange = (field: keyof AddressForm, value: string) => {
setShippingAddress((prev) => ({ ...prev, [field]: value }));
if (sameAsShipping) {
setBillingAddress((prev) => ({ ...prev, [field]: value }));
}
};
const handleBillingChange = (field: keyof AddressForm, value: string) => {
setBillingAddress((prev) => ({ ...prev, [field]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!checkout) {
setError("No active checkout. Please try again.");
return;
}
setIsLoading(true);
setError(null);
try {
// Update shipping address
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
shippingAddress: {
...shippingAddress,
country: "RS", // Serbia
},
},
});
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
}
// Update billing address
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: {
checkoutId: checkout.id,
billingAddress: {
...billingAddress,
country: "RS",
},
},
});
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
}
// Complete checkout (creates order)
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
mutation: CHECKOUT_COMPLETE,
variables: {
checkoutId: checkout.id,
},
});
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
}
const order = completeResult.data?.checkoutComplete?.order;
if (order) {
setOrderNumber(order.number);
setOrderComplete(true);
} else {
throw new Error("Failed to create order");
}
} catch (err: any) {
setError(err.message || "An error occurred during checkout");
} finally {
setIsLoading(false);
}
};
// Order Success Page
if (orderComplete) {
return (
<>
<Header />
<main className="min-h-screen">
<section className="pt-[120px] pb-20 px-4">
<div className="max-w-2xl mx-auto text-center">
<div className="mb-6">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h1 className="text-3xl font-serif mb-2">Order Confirmed!</h1>
<p className="text-foreground-muted">Thank you for your purchase.</p>
</div>
{orderNumber && (
<div className="bg-background-ice p-6 rounded-lg mb-6">
<p className="text-sm text-foreground-muted mb-1">Order Number</p>
<p className="text-2xl font-serif">#{orderNumber}</p>
</div>
)}
<p className="text-foreground-muted mb-8">
You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery.
</p>
<Link
href="/products"
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
>
Continue Shopping
</Link>
</div>
</section>
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}
return (
<>
<Header />
<main className="min-h-screen">
<section className="pt-[120px] pb-20 px-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
{error && (
<div className="bg-red-50 border border-red-200 text-red-600 p-4 mb-6 rounded">
{error}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
{/* Checkout Form */}
<div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Shipping Address */}
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Shipping Address</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">First Name</label>
<input
type="text"
required
value={shippingAddress.firstName}
onChange={(e) => handleShippingChange("firstName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Last Name</label>
<input
type="text"
required
value={shippingAddress.lastName}
onChange={(e) => handleShippingChange("lastName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Street Address</label>
<input
type="text"
required
value={shippingAddress.streetAddress1}
onChange={(e) => handleShippingChange("streetAddress1", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<input
type="text"
value={shippingAddress.streetAddress2}
onChange={(e) => handleShippingChange("streetAddress2", e.target.value)}
placeholder="Apartment, suite, etc. (optional)"
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">City</label>
<input
type="text"
required
value={shippingAddress.city}
onChange={(e) => handleShippingChange("city", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Postal Code</label>
<input
type="text"
required
value={shippingAddress.postalCode}
onChange={(e) => handleShippingChange("postalCode", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
required
value={shippingAddress.phone}
onChange={(e) => handleShippingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
</div>
</div>
{/* Billing Address Toggle */}
<div className="border-b border-border pb-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={sameAsShipping}
onChange={(e) => setSameAsShipping(e.target.checked)}
className="w-4 h-4"
/>
<span>Billing address same as shipping</span>
</label>
</div>
{/* Billing Address (if different) */}
{!sameAsShipping && (
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Billing Address</h2>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">First Name</label>
<input
type="text"
required
value={billingAddress.firstName}
onChange={(e) => handleBillingChange("firstName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Last Name</label>
<input
type="text"
required
value={billingAddress.lastName}
onChange={(e) => handleBillingChange("lastName", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Street Address</label>
<input
type="text"
required
value={billingAddress.streetAddress1}
onChange={(e) => handleBillingChange("streetAddress1", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">City</label>
<input
type="text"
required
value={billingAddress.city}
onChange={(e) => handleBillingChange("city", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Postal Code</label>
<input
type="text"
required
value={billingAddress.postalCode}
onChange={(e) => handleBillingChange("postalCode", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium mb-1">Phone</label>
<input
type="tel"
required
value={billingAddress.phone}
onChange={(e) => handleBillingChange("phone", e.target.value)}
className="w-full border border-border px-4 py-2 rounded"
/>
</div>
</div>
</div>
)}
{/* Payment Method */}
<div className="border-b border-border pb-6">
<h2 className="text-xl font-serif mb-4">Payment Method</h2>
<div className="bg-background-ice p-4 rounded">
<div className="flex items-center gap-3">
<input
type="radio"
checked
readOnly
className="w-4 h-4"
/>
<span>Cash on Delivery (COD)</span>
</div>
<p className="text-sm text-foreground-muted mt-2 ml-7">
Pay when your order is delivered to your door.
</p>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={isLoading || lines.length === 0}
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
>
{isLoading ? "Processing..." : `Complete Order - ${formatPrice(total)}`}
</button>
</form>
</div>
{/* Order Summary */}
<div className="bg-background-ice p-6 rounded-lg h-fit">
<h2 className="text-xl font-serif mb-6">Order Summary</h2>
{lines.length === 0 ? (
<p className="text-foreground-muted">Your cart is empty</p>
) : (
<>
<div className="space-y-4 mb-6">
{lines.map((line) => (
<div key={line.id} className="flex gap-4">
<div className="w-16 h-16 bg-white relative flex-shrink-0">
{line.variant.product.media[0]?.url && (
<Image
src={line.variant.product.media[0].url}
alt={line.variant.product.name}
fill
className="object-cover"
/>
)}
</div>
<div className="flex-1">
<h3 className="font-medium text-sm">{line.variant.product.name}</h3>
<p className="text-foreground-muted text-sm">
Qty: {line.quantity}
</p>
<p className="text-sm">
{formatPrice(line.totalPrice.gross.amount)}
</p>
</div>
</div>
))}
</div>
<div className="border-t border-border pt-4 space-y-2">
<div className="flex justify-between">
<span className="text-foreground-muted">Subtotal</span>
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-foreground-muted">Shipping</span>
<span>
{checkout?.shippingPrice?.gross?.amount
? formatPrice(checkout.shippingPrice.gross.amount)
: "Calculated"
}
</span>
</div>
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
<span>Total</span>
<span>{formatPrice(total)}</span>
</div>
</div>
</>
)}
</div>
</div>
</div>
</section>
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}

View File

@@ -3,6 +3,7 @@
import { useState } from "react";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import { Mail, MapPin, Truck, Check } from "lucide-react";
export default function ContactPage() {
const [formData, setFormData] = useState({
@@ -18,97 +19,185 @@ export default function ContactPage() {
};
return (
<main className="min-h-screen pt-16 md:pt-20">
<>
<Header />
<section className="py-20 px-4">
<div className="max-w-2xl mx-auto">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
Contact Us
</h1>
<p className="text-foreground-muted text-center mb-12">
Have questions? We'd love to hear from you.
</p>
{submitted ? (
<div className="bg-green-50 text-green-700 p-6 text-center">
<p className="text-lg">Thank you for your message!</p>
<p className="mt-2">We'll get back to you soon.</p>
<main className="min-h-screen bg-white">
{/* Page Header */}
<div className="pt-[104px]">
<div className="container py-12 md:py-16">
<div className="max-w-2xl mx-auto text-center">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Get in Touch</span>
<h1 className="text-4xl md:text-5xl font-medium tracking-tight mb-4">
Contact Us
</h1>
<p className="text-[#666666]">
Have questions? We&apos;d love to hear from you.
</p>
</div>
</div>
</div>
{/* Contact Section */}
<section className="py-12 md:py-16">
<div className="container">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
{/* Contact Info */}
<div>
<h2 className="text-2xl font-medium mb-6">Get in Touch</h2>
<p className="text-[#666666] mb-8 leading-relaxed">
We&apos;re here to help! Whether you have questions about our products,
need assistance with an order, or just want to say hello, we&apos;d love to hear from you.
</p>
<div className="space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
<Mail className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-medium mb-1">Email</h3>
<p className="text-[#666666] text-sm">hello@manoonoils.com</p>
<p className="text-[#999999] text-xs mt-1">We reply within 24 hours</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
<Truck className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-medium mb-1">Shipping</h3>
<p className="text-[#666666] text-sm">Free shipping over 3,000 RSD</p>
<p className="text-[#999999] text-xs mt-1">Delivered within 2-5 business days</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
<MapPin className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-medium mb-1">Location</h3>
<p className="text-[#666666] text-sm">Serbia</p>
<p className="text-[#999999] text-xs mt-1">Shipping nationwide</p>
</div>
</div>
</div>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-2">
Name
</label>
<input
type="text"
id="name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email
</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-2">
Message
</label>
<textarea
id="message"
required
rows={5}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none"
/>
</div>
<button
type="submit"
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
>
Send Message
</button>
</form>
)}
<div className="mt-16 pt-8 border-t border-border/30">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
<div>
<h3 className="font-serif mb-2">Email</h3>
<p className="text-foreground-muted">hello@manoonoils.com</p>
</div>
<div>
<h3 className="font-serif mb-2">Shipping</h3>
<p className="text-foreground-muted">Free over 3000 RSD</p>
</div>
<div>
<h3 className="font-serif mb-2">Location</h3>
<p className="text-foreground-muted">Serbia</p>
</div>
{/* Contact Form */}
<div className="bg-[#f8f9fa] p-8 md:p-10">
{submitted ? (
<div className="text-center py-12">
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
<Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
</div>
<h3 className="text-xl font-medium mb-2">Thank You!</h3>
<p className="text-[#666666]">
Your message has been sent. We&apos;ll get back to you soon.
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="name" className="block text-sm font-medium mb-2">
Name
</label>
<input
type="text"
id="name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
placeholder="Your name"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email
</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
placeholder="your@email.com"
/>
</div>
<div>
<label htmlFor="message" className="block text-sm font-medium mb-2">
Message
</label>
<textarea
id="message"
required
rows={5}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
placeholder="How can we help you?"
/>
</div>
<button
type="submit"
className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
>
Send Message
</button>
</form>
)}
</div>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-16 md:py-24 border-t border-[#e5e5e5]">
<div className="container">
<div className="max-w-3xl mx-auto">
<h2 className="text-2xl font-medium text-center mb-12">
Frequently Asked Questions
</h2>
<div className="space-y-6">
{[
{
q: "How long does shipping take?",
a: "Orders are typically delivered within 2-5 business days for domestic shipping. You'll receive a tracking number once your order ships."
},
{
q: "Are your products 100% natural?",
a: "Yes! All our oils are 100% natural, cold-pressed, and free from any additives, preservatives, or artificial fragrances."
},
{
q: "What is your return policy?",
a: "We accept returns within 14 days of delivery for unopened products. Please contact us if you have any issues with your order."
},
{
q: "Do you offer wholesale?",
a: "Yes, we offer wholesale pricing for bulk orders. Please contact us at hello@manoonoils.com for more information."
}
].map((faq, index) => (
<div key={index} className="border-b border-[#e5e5e5] pb-6">
<h3 className="font-medium mb-2">{faq.q}</h3>
<p className="text-[#666666] text-sm leading-relaxed">{faq.a}</p>
</div>
))}
</div>
</div>
</div>
</section>
</main>
<Footer />
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}

View File

@@ -1,66 +1,2 @@
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
export const metadata = {
title: "About - ManoonOils",
description: "Learn about ManoonOils - our story, mission, and commitment to natural beauty.",
};
export default function AboutPage() {
return (
<main className="min-h-screen pt-16 md:pt-20">
<Header />
<section className="py-20 px-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
Our Story
</h1>
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6">
<p>
ManoonOils was born from a passion for natural beauty and the belief
that the best skincare comes from nature itself. Our journey began with
a simple question: how can we create products that truly nurture both
hair and skin?
</p>
<p>
We believe in the power of natural ingredients. Every oil in our
collection is carefully selected for its unique properties and
benefits. From nourishing oils that restore hair vitality to serums
that rejuvenate skin, we craft each product with love and attention
to detail.
</p>
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
Our Mission
</h2>
<p>
Our mission is to provide premium quality, natural products that
enhance your daily beauty routine. We are committed to:
</p>
<ul className="list-disc pl-6 space-y-2">
<li>Using only the finest natural ingredients</li>
<li>Cruelty-free and ethical production</li>
<li>Sustainable packaging practices</li>
<li>Transparency in our formulations</li>
</ul>
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
Handmade with Love
</h2>
<p>
Every bottle of ManoonOils is handcrafted with care. We small-batch
produce our products to ensure the highest quality and freshness.
When you use ManoonOils, you can feel confident that you're using
something made with genuine care and expertise.
</p>
</div>
</div>
</section>
<Footer />
</main>
);
}
// Re-export from main about page
export { default, metadata } from "../../about/page";

View File

@@ -1,12 +1,8 @@
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar";
import NewHero from "@/components/home/NewHero";
import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import NewsletterSection from "@/components/home/NewsletterSection";
import HeroVideo from "@/components/home/HeroVideo";
import ProductCard from "@/components/product/ProductCard";
export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,61 +13,160 @@ export const metadata = {
export default async function Homepage() {
let products: any[] = [];
try {
products = await getProducts();
products = await getProducts("EN");
} catch (e) {
// Fallback for build time when API is unavailable
console.log('Failed to fetch products during build');
}
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products
.filter((p) => p.status === "publish")
.slice(0, 4);
const featuredProducts = products.slice(0, 4);
return (
<main className="min-h-screen bg-white">
<AnnouncementBar />
<div className="pt-10">
<Header />
</div>
<>
<Header />
<main className="min-h-screen bg-white">
{/* Hero Section with Video Background */}
<HeroVideo />
{/* New Hero Section */}
<NewHero featuredProduct={featuredProduct} />
{/* Main Content */}
<div id="main-content">
{/* Products Grid Section */}
{featuredProducts.length > 0 && (
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
<div className="container">
{/* Section Header */}
<div className="text-center mb-16">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Collection</span>
<h2 className="text-3xl md:text-4xl font-medium mb-4">Premium Natural Oils</h2>
<p className="text-[#666666] max-w-xl mx-auto">
Cold-pressed, pure, and natural oils for your daily beauty routine
</p>
</div>
{/* Stats & Philosophy Section */}
<StatsSection />
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{featuredProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} locale="EN" />
))}
</div>
{/* Features Section */}
<FeaturesSection />
{/* View All Link */}
<div className="text-center mt-12">
<a
href="/en/products"
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
>
View All Products
</a>
</div>
</div>
</section>
)}
{/* Testimonials Section */}
<TestimonialsSection />
{/* Newsletter Section */}
<NewsletterSection />
{/* Products Grid Section */}
{publishedProducts.length > 0 && (
<section className="py-20 px-6 bg-white">
<div className="max-w-[1400px] mx-auto">
<h2 className="font-serif italic text-4xl text-center mb-4">
Our Collection
</h2>
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
Cold-pressed, pure, and natural oils for your daily beauty routine
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
{/* Brand Story Section */}
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
<div className="container">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
<h2 className="text-3xl md:text-4xl font-medium mb-6">Handmade with Love</h2>
<p className="text-[#666666] mb-6 leading-relaxed">
Every bottle of ManoonOils is crafted with care using traditional
methods passed down through generations. We source only the finest
organic ingredients to bring you oils that nourish both hair and skin.
</p>
<p className="text-[#666666] mb-8 leading-relaxed">
Our commitment to purity means no additives, no preservatives -
just nature&apos;s goodness in its most potent form.
</p>
<a
href="/en/about"
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
>
Learn More
</a>
</div>
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
<img
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
alt="Natural oils production"
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
</div>
</section>
)}
</section>
<Footer />
</main>
{/* Benefits Section */}
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
<div className="container">
<div className="text-center mb-16">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Why Choose Us</span>
<h2 className="text-3xl md:text-4xl font-medium">The Manoon Difference</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
{[
{
title: "100% Natural",
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
},
{
title: "Handcrafted",
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
},
{
title: "Sustainable",
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
},
].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>
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
<p className="text-[#666666]">{benefit.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Newsletter Section */}
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white mb-16">
<div className="container">
<div className="max-w-2xl mx-auto text-center">
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">Stay Connected</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">Join Our Community</h2>
<p className="text-white/70 mb-10 mx-auto text-lg">
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
</p>
<div className="flex justify-center">
<form className="inline-flex flex-col sm:flex-row">
<input
type="email"
placeholder="Enter your email"
className="w-64 sm:w-80 px-5 h-14 bg-white/10 border border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
/>
<button
type="submit"
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"
>
Subscribe
</button>
</form>
</div>
</div>
</div>
</section>
</div>
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}
// Import ProductCard here to avoid circular dependency
import ProductCard from "@/components/product/ProductCard";

View File

@@ -1,70 +1,97 @@
import { getProducts } from "@/lib/woocommerce";
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductDetail from "@/components/product/ProductDetail";
import type { Product } from "@/types/saleor";
import type { Metadata } from "next";
export const dynamic = 'force-dynamic';
interface ProductPageProps {
params: Promise<{ slug: string }>;
}
// Disable static generation - this page will be server-rendered
export const generateStaticParams = undefined;
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
let product = null;
// Generate static params for all products
export async function generateStaticParams() {
try {
const products = await getProducts();
product = products.find((p) => (p.slug || p.id.toString()) === slug);
const products = await getProducts("EN", 100);
const params: Array<{ slug: string }> = [];
products.forEach((product: Product) => {
// English slug (if translation exists)
if (product.translation?.slug) {
params.push({ slug: product.translation.slug });
} else {
params.push({ slug: product.slug });
}
});
return params;
} catch (e) {
// Fallback
return [];
}
}
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params;
const product = await getProductBySlug(slug, "EN");
if (!product) {
return {
title: "Product Not Found",
};
}
const localized = getLocalizedProduct(product, "EN");
return {
title: localized.name,
description: localized.seoDescription || localized.description?.slice(0, 160),
};
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
const product = await getProductBySlug(slug, "EN");
if (!product) {
return (
<main className="min-h-screen">
<main className="min-h-screen bg-white">
<Header />
<div className="pt-24 text-center">
<h1 className="text-2xl">Product not found</h1>
<div className="pt-[120px] text-center px-4">
<h1 className="text-2xl font-medium mb-4">Product not found</h1>
<p className="text-[#666666] mb-8">
The product you&apos;re looking for doesn&apos;t exist or has been removed.
</p>
<a
href="/products"
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
>
Browse Products
</a>
</div>
<Footer />
</main>
);
}
const image = product.images?.[0]?.src || '/placeholder.jpg';
const price = product.sale_price || product.price;
// Get related products
let relatedProducts: Product[] = [];
try {
const allProducts = await getProducts("EN", 8);
relatedProducts = allProducts
.filter((p: Product) => p.id !== product.id)
.slice(0, 4);
} catch (e) {
// Ignore error
}
return (
<main className="min-h-screen">
<main className="min-h-screen bg-white">
<Header />
<section className="pt-24 pb-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden">
<img
src={image}
alt={product.name}
className="object-cover w-full h-full"
/>
</div>
<div className="flex flex-col">
<h1 className="text-4xl font-serif mb-4">{product.name}</h1>
<div className="text-2xl mb-6">{price} RSD</div>
<div className="prose max-w-none mb-8" dangerouslySetInnerHTML={{ __html: product.description || '' }} />
<button
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all"
>
Add to Cart
</button>
</div>
</div>
</div>
</section>
<ProductDetail
product={product}
relatedProducts={relatedProducts}
locale="EN"
/>
<Footer />
</main>
);

View File

@@ -1,7 +1,8 @@
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react";
export const metadata = {
title: "Products - ManoonOils",
@@ -9,38 +10,71 @@ export const metadata = {
};
export default async function ProductsPage() {
let products: any[] = [];
try {
products = await getProducts();
} catch (e) {
console.log('Failed to fetch products during build');
}
const publishedProducts = products.filter((p) => p.status === "publish");
const products = await getProducts("EN");
return (
<main className="min-h-screen pt-16 md:pt-20">
<>
<Header />
<section className="py-20 px-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16">
All Products
</h1>
{publishedProducts.length === 0 ? (
<p className="text-center text-foreground-muted">No products available</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
<main className="min-h-screen bg-white">
{/* Page Header */}
<div className="pt-[104px]">
<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">
<div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">Our Collection</span>
<h1 className="text-3xl md:text-4xl font-medium">All Products</h1>
</div>
{/* Sort Dropdown */}
<div className="flex items-center gap-3">
<span className="text-sm text-[#666666]">{products.length} products</span>
<div className="relative">
<select
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
defaultValue="featured"
>
<option value="featured">Featured</option>
<option value="newest">Newest</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
</div>
</div>
</div>
</div>
)}
</div>
{/* Products Grid */}
<section className="py-12 md:py-16">
<div className="container">
{products.length === 0 ? (
<div className="text-center py-20">
<p className="text-[#666666] mb-4">No products available</p>
<p className="text-sm text-[#999999]">Please check back later for new arrivals.</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{products.map((product, index) => (
<ProductCard
key={product.id}
product={product}
index={index}
locale="EN"
/>
))}
</div>
)}
</div>
</section>
</div>
</section>
</main>
<Footer />
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}

View File

@@ -1,36 +1,75 @@
@import "tailwindcss";
:root {
--background: #f0f4f8;
--background-ice: #e8f0f5;
--foreground: #1a1a1a;
--foreground-muted: #666666;
--accent: #a8c5d8;
--accent-dark: #7ba3bc;
--white: #ffffff;
--border: #d1d9e0;
}
/* ============================================
MANOONOILS DESIGN SYSTEM
Tailwind 4 compatible - uses CSS layers
============================================ */
@theme inline {
--color-background: var(--background);
--color-background-ice: var(--background-ice);
--color-foreground: var(--foreground);
--color-foreground-muted: var(--foreground-muted);
--color-accent: var(--accent);
--color-accent-dark: var(--accent-dark);
--color-white: var(--white);
--color-border: var(--border);
--font-display: var(--font-cedrat);
--font-body: var(--font-dm-sans);
/* Colors - reference CSS variables */
--color-white: var(--color-white);
--color-background: var(--color-background);
--color-background-alt: var(--color-background-alt);
--color-foreground: var(--color-foreground);
--color-foreground-muted: var(--color-foreground-muted);
--color-foreground-subtle: var(--color-foreground-subtle);
--color-accent: var(--color-accent);
--color-accent-dark: var(--color-accent-dark);
--color-accent-blue: var(--color-accent-blue);
--color-gold: var(--color-gold);
--color-gold-light: var(--color-gold-light);
--color-border: var(--color-border);
--color-border-dark: var(--color-border-dark);
--color-cta: var(--color-cta);
--color-cta-hover: var(--color-cta-hover);
--color-overlay: var(--color-overlay);
/* Typography */
--font-display: var(--font-display);
--font-body: var(--font-body);
}
@font-face {
font-family: 'Cedrat Display';
src: url('https://fonts.gstatic.com/s/cedratdisplay/v16/0nkoC9_pK3CvS5lZuZ7MAUmK5w.woff2') format('woff2');
font-weight: 400 900;
font-display: swap;
/* ============================================
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
============================================ */
@font-face {
font-family: 'DM Sans';
src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2');
@@ -38,77 +77,294 @@
font-display: swap;
}
* {
box-sizing: border-box;
@font-face {
font-family: 'Inter';
src: url('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2') format('woff2');
font-weight: 400 700;
font-display: swap;
}
body {
background: var(--background);
color: var(--foreground);
font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ============================================
BASE STYLES (in Tailwind base layer)
============================================ */
h1, h2, h3, h4, h5, h6 {
font-family: 'Cedrat Display', serif;
}
/* Marquee Animations */
@keyframes marquee {
0% {
transform: translateX(0);
@layer base {
html {
scroll-behavior: smooth;
}
100% {
transform: translateX(-50%);
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;
}
}
@keyframes marquee-slow {
0% {
transform: translateX(0);
/* ============================================
COMPONENTS
============================================ */
@layer components {
.container {
width: 100%;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
padding-left: 24px;
padding-right: 24px;
}
100% {
transform: translateX(-50%);
@media (min-width: 640px) {
.container {
padding-left: 32px;
padding-right: 32px;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 48px;
padding-right: 48px;
}
}
.container-narrow {
max-width: 1200px;
}
.container-wide {
max-width: 1600px;
}
.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);
}
.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;
}
}
.animate-marquee {
animation: marquee 25s linear infinite;
/* ============================================
UTILITIES
============================================ */
@layer utilities {
.section {
padding-top: 96px;
padding-bottom: 96px;
}
.section-sm {
padding-top: 48px;
padding-bottom: 48px;
}
::-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);
}
.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;
}
@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;
}
}
.animate-marquee-slow {
animation: marquee-slow 35s linear infinite;
}
/* ============================================
REDUCED MOTION
============================================ */
.animate-marquee-fast {
animation: marquee 15s linear infinite;
}
/* Utility Classes */
.font-serif {
font-family: 'Cedrat Display', serif;
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
html {
scroll-behavior: auto;
}
}

View File

@@ -1,5 +1,6 @@
import "./globals.css";
import type { Metadata } from "next";
import ErrorBoundary from "@/components/providers/ErrorBoundary";
export const metadata: Metadata = {
title: {
@@ -16,15 +17,20 @@ export const metadata: Metadata = {
},
};
// Suppress extension-caused hydration warnings
const suppressHydrationWarning = true;
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="antialiased">
{children}
<html lang="en" suppressHydrationWarning>
<body className="antialiased" suppressHydrationWarning>
<ErrorBoundary>
{children}
</ErrorBoundary>
</body>
</html>
);

View File

@@ -1,12 +1,14 @@
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar";
import NewHero from "@/components/home/NewHero";
import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection";
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 NewsletterSection from "@/components/home/NewsletterSection";
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",
@@ -17,61 +19,193 @@ export const metadata = {
export default async function Homepage() {
let products: any[] = [];
try {
products = await getProducts();
products = await getProducts("SR");
} catch (e) {
// Fallback for build time when API is unavailable
console.log('Failed to fetch products during build');
}
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products
.filter((p) => p.status === "publish")
.slice(0, 4);
const featuredProducts = products?.slice(0, 4) || [];
const hasProducts = featuredProducts.length > 0;
return (
<main className="min-h-screen bg-white">
<AnnouncementBar />
<div className="pt-10">
<Header />
</div>
<>
<Header />
<main className="min-h-screen bg-white">
{/* Hero Section with Video Background */}
<HeroVideo />
{/* New Hero Section */}
<NewHero featuredProduct={featuredProduct} />
{/* Trust Badges */}
<TrustBadges />
{/* Stats & Philosophy Section */}
<StatsSection />
{/* As Seen In */}
<AsSeenIn />
{/* Features Section */}
<FeaturesSection />
{/* Problem Section - Create empathy */}
<ProblemSection />
{/* Testimonials Section */}
<TestimonialsSection />
{/* Main Content */}
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
{/* Products Grid Section */}
{hasProducts && (
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
<div className="max-w-7xl mx-auto">
{/* Section Header */}
<div className="text-center mb-16">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
Our Collection
</span>
<h2 className="text-3xl md:text-4xl font-medium mb-4">
Premium Natural Oils
</h2>
<p className="text-[#666666] max-w-xl mx-auto">
Cold-pressed, pure, and natural oils for your daily beauty routine
</p>
</div>
{/* Newsletter Section */}
<NewsletterSection />
{/* Products Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{featuredProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} locale="SR" />
))}
</div>
{/* Products Grid Section */}
{publishedProducts.length > 0 && (
<section className="py-20 px-6 bg-white">
<div className="max-w-[1400px] mx-auto">
<h2 className="font-serif italic text-4xl text-center mb-4">
Our Collection
</h2>
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
Cold-pressed, pure, and natural oils for your daily beauty routine
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
{/* View All Link */}
<div className="text-center mt-12">
<a
href="/products"
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
>
View All Products
</a>
</div>
</div>
</section>
)}
{/* Before/After Gallery */}
<BeforeAfterGallery />
{/* 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">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
Our Story
</span>
<h2 className="text-3xl md:text-4xl font-medium mb-6">
Handmade with Love
</h2>
<p className="text-[#666666] mb-6 leading-relaxed">
Every bottle of ManoonOils is crafted with care using traditional
methods passed down through generations. We source only the finest
organic ingredients to bring you oils that nourish both hair and skin.
</p>
<p className="text-[#666666] mb-8 leading-relaxed">
Our commitment to purity means no additives, no preservatives -
just nature&apos;s goodness in its most potent form.
</p>
<a
href="/about"
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
>
Learn More
</a>
</div>
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
<img
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
alt="Natural oils production"
className="w-full h-full object-cover"
/>
</div>
</div>
</div>
</div>
</section>
)}
</section>
{/* Benefits Section */}
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
<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">
Why Choose Us
</span>
<h2 className="text-3xl md:text-4xl font-medium">
The Manoon Difference
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
{[
{
title: "100% Natural",
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
},
{
title: "Handcrafted",
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
},
{
title: "Sustainable",
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
},
].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>
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
<p className="text-[#666666]">{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">
<div className="max-w-2xl mx-auto text-center">
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">
Stay Connected
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">
Join Our Community
</h2>
<p className="text-white/70 mb-10 mx-auto text-lg">
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-stretch justify-center max-w-md mx-auto gap-0">
<input
type="email"
placeholder="Enter your email"
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="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>
</form>
</div>
</div>
</section>
</div>
</main>
<Footer />
</main>
</>
);
}
// Import ProductCard here to avoid circular dependency
import ProductCard from "@/components/product/ProductCard";

View File

@@ -1,71 +1,123 @@
import { getProducts } from "@/lib/woocommerce";
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductDetail from "@/components/product/ProductDetail";
import type { Product } from "@/types/saleor";
export const dynamic = 'force-dynamic';
interface ProductPageProps {
params: Promise<{ slug: string; locale?: string }>;
}
// Disable static generation - this page will be server-rendered
export const generateStaticParams = undefined;
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
let product = null;
// Generate static params for all products
export async function generateStaticParams() {
try {
const products = await getProducts();
product = products.find((p) => (p.slug || p.id.toString()) === slug);
const products = await getProducts("SR", 100);
const params: Array<{ slug: string; locale: string }> = [];
products.forEach((product: Product) => {
// Serbian slug
params.push({ slug: product.slug, locale: "sr" });
// English slug (if translation exists)
if (product.translation?.slug) {
params.push({ slug: product.translation.slug, locale: "en" });
}
});
return params;
} catch (e) {
// Fallback
return [];
}
}
export async function generateMetadata({ params }: ProductPageProps) {
const { slug, locale = "sr" } = await params;
const product = await getProductBySlug(slug, locale.toUpperCase());
if (!product) {
return {
title: locale === "en" ? "Product Not Found" : "Proizvod nije pronađen",
};
}
const localized = getLocalizedProduct(product, locale.toUpperCase());
return {
title: localized.name,
description: localized.seoDescription || localized.description?.slice(0, 160),
alternates: {
canonical: `/products/${product.slug}`,
languages: {
"sr": `/products/${product.slug}`,
"en": product.translation?.slug ? `/products/${product.translation.slug}` : `/products/${product.slug}`,
},
},
openGraph: {
title: localized.name,
description: localized.seoDescription || localized.description?.slice(0, 160),
images: product.media?.[0]?.url ? [product.media[0].url] : [],
type: 'website',
},
};
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug, locale = "sr" } = await params;
const product = await getProductBySlug(slug, locale.toUpperCase());
if (!product) {
return (
<main className="min-h-screen">
<>
<Header />
<div className="pt-24 text-center">
<h1 className="text-2xl">Product not found</h1>
</div>
<main className="min-h-screen bg-white">
<div className="pt-[180px] lg:pt-[200px] pb-20 text-center px-4">
<h1 className="text-2xl font-medium mb-4">
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
</h1>
<p className="text-[#666666] mb-8">
{locale === "en"
? "The product you're looking for doesn't exist or has been removed."
: "Proizvod koji tražite ne postoji ili je uklonjen."}
</p>
<a
href="/products"
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
>
{locale === "en" ? "Browse Products" : "Pregledaj proizvode"}
</a>
</div>
</main>
<Footer />
</main>
</>
);
}
const image = product.images?.[0]?.src || '/placeholder.jpg';
const price = product.sale_price || product.price;
// Determine language based on which slug matched
const isEnglishSlug = slug === product.translation?.slug;
const currentLocale = isEnglishSlug ? "EN" : "SR";
// Get related products (same category or just other products)
let relatedProducts: Product[] = [];
try {
const allProducts = await getProducts(currentLocale, 8);
relatedProducts = allProducts
.filter((p: Product) => p.id !== product.id)
.slice(0, 4);
} catch (e) {
// Ignore error, just won't show related products
}
return (
<main className="min-h-screen">
<>
<Header />
<section className="pt-24 pb-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden">
<img
src={image}
alt={product.name}
className="object-cover w-full h-full"
/>
</div>
<div className="flex flex-col">
<h1 className="text-4xl font-serif mb-4">{product.name}</h1>
<div className="text-2xl mb-6">{price} RSD</div>
<div className="prose max-w-none mb-8" dangerouslySetInnerHTML={{ __html: product.description || '' }} />
<button
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all"
>
Add to Cart
</button>
</div>
</div>
</div>
</section>
<main className="min-h-screen bg-white">
<ProductDetail
product={product}
relatedProducts={relatedProducts}
locale={currentLocale}
/>
</main>
<Footer />
</main>
</>
);
}

View File

@@ -1,46 +1,106 @@
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react";
export const metadata = {
title: "Products - ManoonOils",
description: "Browse our collection of premium natural oils for hair and skin care.",
};
export default async function ProductsPage() {
let products: any[] = [];
try {
products = await getProducts();
} catch (e) {
console.log('Failed to fetch products during build');
}
const publishedProducts = products.filter((p) => p.status === "publish");
interface ProductsPageProps {
params: Promise<{ locale: string }>;
}
export default async function ProductsPage({ params }: ProductsPageProps) {
const { locale = "sr" } = await params;
const products = await getProducts(locale.toUpperCase());
const localeUpper = locale.toUpperCase();
return (
<main className="min-h-screen pt-16 md:pt-20">
<>
<Header />
<section className="py-20 px-4">
<div className="max-w-7xl mx-auto">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16">
All Products
</h1>
{publishedProducts.length === 0 ? (
<p className="text-center text-foreground-muted">No products available</p>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
))}
<main className="min-h-screen bg-white">
{/* Page Header */}
<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">
<div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">
{localeUpper === "EN" ? "Our Collection" : "Naša kolekcija"}
</span>
<h1 className="text-3xl md:text-4xl font-medium">
{localeUpper === "EN" ? "All Products" : "Svi Proizvodi"}
</h1>
</div>
{/* Sort Dropdown */}
<div className="flex items-center gap-3">
<span className="text-sm text-[#666666]">
{products.length} {localeUpper === "EN" ? "products" : "proizvoda"}
</span>
<div className="relative">
<select
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
defaultValue="featured"
>
<option value="featured">
{localeUpper === "EN" ? "Featured" : "Istaknuto"}
</option>
<option value="newest">
{localeUpper === "EN" ? "Newest" : "Najnovije"}
</option>
<option value="price-low">
{localeUpper === "EN" ? "Price: Low to High" : "Cena: Rastuće"}
</option>
<option value="price-high">
{localeUpper === "EN" ? "Price: High to Low" : "Cena: Opadajuće"}
</option>
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
</div>
</div>
</div>
</div>
)}
</div>
{/* Products Grid */}
<section className="py-12 md:py-16">
<div className="container">
{products.length === 0 ? (
<div className="text-center py-20">
<p className="text-[#666666] mb-4">
{localeUpper === "EN" ? "No products available" : "Nema dostupnih proizvoda"}
</p>
<p className="text-sm text-[#999999]">
{localeUpper === "EN"
? "Please check back later for new arrivals."
: "Molimo proverite ponovo kasnije za nove proizvode."}
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{products.map((product, index) => (
<ProductCard
key={product.id}
product={product}
index={index}
locale={localeUpper}
/>
))}
</div>
)}
</div>
</section>
</div>
</section>
</main>
<Footer />
</main>
<div className="pt-16">
<Footer />
</div>
</>
);
}

View File

@@ -1,24 +1,22 @@
import { MetadataRoute } from "next";
import { getProducts } from "@/lib/woocommerce";
import { getProducts } from "@/lib/saleor";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
let products: any[] = [];
try {
products = await getProducts();
products = await getProducts("SR", 100);
} catch (e) {
console.log('Failed to fetch products for sitemap during build');
}
const productUrls = products
.filter((p) => p.status === "publish")
.map((product) => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.8,
}));
const productUrls = products.map((product) => ({
url: `${baseUrl}/products/${product.slug}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.8,
}));
return [
{

View File

@@ -1,99 +1,194 @@
"use client";
import { useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { useCartStore } from "@/stores/cartStore";
import { formatPrice } from "@/lib/woocommerce";
import { X, Minus, Plus, Trash2, ShoppingBag } from "lucide-react";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { formatPrice } from "@/lib/saleor";
export default function CartDrawer() {
const { items, isOpen, closeCart, removeItem, updateQuantity, getTotal } = useCartStore();
const {
checkout,
isOpen,
isLoading,
error,
closeCart,
removeLine,
updateLine,
getTotal,
getLineCount,
getLines,
initCheckout,
clearError,
} = useSaleorCheckoutStore();
const lines = getLines();
const total = getTotal();
const lineCount = getLineCount();
// Initialize checkout on mount
useEffect(() => {
initCheckout();
}, [initCheckout]);
// Lock body scroll when cart is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [isOpen]);
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 bg-black/50 z-50"
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={closeCart}
/>
{/* Drawer */}
<motion.div
className="fixed top-0 right-0 bottom-0 w-full max-w-md bg-white z-50 shadow-xl flex flex-col"
className="fixed top-0 right-0 bottom-0 w-full max-w-[420px] bg-white z-50 shadow-2xl flex flex-col"
initial={{ x: "100%" }}
animate={{ x: 0 }}
exit={{ x: "100%" }}
transition={{ type: "tween", duration: 0.3 }}
transition={{ type: "tween", duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
>
<div className="flex items-center justify-between p-6 border-b border-border/30">
<h2 className="text-xl font-serif">Your Cart</h2>
{/* Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-[#e5e5e5]">
<h2 className="text-sm uppercase tracking-[0.1em] font-medium">
Your Cart ({lineCount})
</h2>
<button
onClick={closeCart}
className="p-2"
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
aria-label="Close cart"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
<X className="w-5 h-5" strokeWidth={1.5} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6">
{items.length === 0 ? (
<div className="text-center py-12">
<p className="text-foreground-muted mb-6">Your cart is empty</p>
{/* Error Message */}
<AnimatePresence>
{error && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
>
<div className="p-4 bg-red-50 border-b border-red-100">
<p className="text-red-600 text-sm">{error}</p>
<button
onClick={clearError}
className="text-red-600 text-xs underline mt-1 hover:no-underline"
>
Dismiss
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Cart Items */}
<div className="flex-1 overflow-y-auto">
{lines.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full px-6">
<div className="w-16 h-16 rounded-full bg-[#f8f9fa] flex items-center justify-center mb-6">
<ShoppingBag className="w-8 h-8 text-[#999999]" strokeWidth={1.5} />
</div>
<p className="text-[#666666] mb-2">Your cart is empty</p>
<p className="text-sm text-[#999999] mb-8 text-center">
Looks like you haven&apos;t added anything to your cart yet.
</p>
<Link
href="/en/products"
href="/products"
onClick={closeCart}
className="inline-block px-6 py-3 bg-foreground text-white"
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
>
Continue Shopping
Start Shopping
</Link>
</div>
) : (
<div className="space-y-6">
{items.map((item) => (
<div key={item.id} className="flex gap-4">
<div className="w-20 h-20 bg-background-ice relative flex-shrink-0">
{item.image && (
<div className="p-6 space-y-6">
{lines.map((line) => (
<div key={line.id} className="flex gap-4">
{/* Product Image */}
<div className="w-24 h-24 bg-[#f8f9fa] relative flex-shrink-0 overflow-hidden">
{line.variant.product.media[0]?.url ? (
<Image
src={item.image}
alt={item.name}
src={line.variant.product.media[0].url}
alt={line.variant.product.name}
fill
className="object-cover"
sizes="96px"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
<ShoppingBag className="w-6 h-6" strokeWidth={1.5} />
</div>
)}
</div>
<div className="flex-1">
<h3 className="font-serif text-sm">{item.name}</h3>
<p className="text-foreground-muted text-sm mt-1">
{formatPrice(item.price)}
{/* Product Info */}
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium truncate">
{line.variant.product.name}
</h3>
{line.variant.name !== "Default" && (
<p className="text-[#999999] text-xs mt-0.5">
{line.variant.name}
</p>
)}
<p className="text-[#666666] text-sm mt-2">
{formatPrice(
line.variant.pricing?.price?.gross?.amount || 0,
line.variant.pricing?.price?.gross?.currency
)}
</p>
<div className="flex items-center gap-3 mt-2">
{/* Quantity Controls */}
<div className="flex items-center justify-between mt-3">
<div className="flex items-center border border-[#e5e5e5]">
<button
onClick={() => updateLine(line.id, line.quantity - 1)}
disabled={isLoading || line.quantity <= 1}
className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
>
<Minus className="w-3 h-3" />
</button>
<span className="w-10 text-center text-sm font-medium">
{line.quantity}
</span>
<button
onClick={() => updateLine(line.id, line.quantity + 1)}
disabled={isLoading}
className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
>
<Plus className="w-3 h-3" />
</button>
</div>
{/* Remove Button */}
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
className="w-8 h-8 border border-border flex items-center justify-center"
onClick={() => removeLine(line.id)}
disabled={isLoading}
className="p-2 text-[#999999] hover:text-red-500 transition-colors"
aria-label="Remove item"
>
-
</button>
<span>{item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
className="w-8 h-8 border border-border flex items-center justify-center"
>
+
</button>
<button
onClick={() => removeItem(item.id)}
className="ml-auto text-foreground-muted hover:text-red-500"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<Trash2 className="w-4 h-4" strokeWidth={1.5} />
</button>
</div>
</div>
@@ -103,18 +198,67 @@ export default function CartDrawer() {
)}
</div>
{items.length > 0 && (
<div className="p-6 border-t border-border/30">
<div className="flex items-center justify-between mb-4">
<span className="font-serif">Subtotal</span>
<span className="font-serif text-lg">{formatPrice(total.toString())}</span>
{/* Footer with Checkout */}
{lines.length > 0 && (
<div className="border-t border-[#e5e5e5] bg-white">
{/* Order Summary */}
<div className="p-6 space-y-3">
{/* Subtotal */}
<div className="flex items-center justify-between text-sm">
<span className="text-[#666666]">Subtotal</span>
<span className="font-medium">
{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}
</span>
</div>
{/* Shipping */}
<div className="flex items-center justify-between text-sm">
<span className="text-[#666666]">Shipping</span>
<span className="text-[#666666]">
{checkout?.shippingPrice?.gross?.amount
? formatPrice(checkout.shippingPrice.gross.amount)
: "Calculated at checkout"
}
</span>
</div>
{/* Divider */}
<div className="border-t border-[#e5e5e5] my-4" />
{/* Total */}
<div className="flex items-center justify-between">
<span className="text-sm uppercase tracking-[0.05em] font-medium">Total</span>
<span className="text-lg font-medium">
{formatPrice(total)}
</span>
</div>
{(checkout?.subtotalPrice?.gross?.amount || 0) < 5000 && (
<p className="text-xs text-[#666666] text-center">
Free shipping on orders over {formatPrice(5000)}
</p>
)}
</div>
{/* Actions */}
<div className="px-6 pb-6 space-y-3">
{/* Checkout Button */}
<Link
href="/checkout"
onClick={closeCart}
className="block w-full py-4 bg-black text-white text-center text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
>
{isLoading ? "Processing..." : "Checkout"}
</Link>
{/* Continue Shopping */}
<button
onClick={closeCart}
className="block w-full py-3 text-center text-sm text-[#666666] hover:text-black transition-colors"
>
Continue Shopping
</button>
</div>
<a
href="https://manoonoils.com/checkout"
className="block w-full py-3 bg-foreground text-white text-center font-medium hover:bg-accent-dark transition-colors"
>
Checkout
</a>
</div>
)}
</motion.div>

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-white/30 font-medium"
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,180 @@
"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-gold text-gold" 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() {
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>
{/* Two transformations side by side */}
<div className="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>
{/* 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

@@ -0,0 +1,163 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { ChevronDown } from "lucide-react";
export default function HeroVideo() {
const scrollToContent = () => {
const element = document.getElementById("main-content");
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
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) */}
<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-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">
<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"
>
{/* Social Proof Micro */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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-gold text-gold" 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.15em] font-semibold hover:bg-white/90 transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl"
>
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>
{/* Scroll Indicator */}
<motion.button
initial={{ opacity: 0 }}
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/60 hover:text-white transition-colors cursor-pointer"
aria-label="Scroll to content"
>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
>
<ChevronDown className="w-6 h-6" strokeWidth={1.5} />
</motion.div>
</motion.button>
</section>
);
}

View File

@@ -0,0 +1,89 @@
"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.",
},
{
number: "02",
title: "Apply Daily",
description: " Massage a few drops into damp hair or skin. Our oils absorb instantly—never greasy, always nourishing.",
},
{
number: "03",
title: "See Results",
description: "Experience transformation in 4-6 weeks. Shinier hair, radiant skin, and confidence that glows.",
},
];
return (
<section className="py-24 bg-white">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-16"
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">
Simple Process
</span>
<h2 className="text-3xl md:text-4xl font-medium">
How ManoonOils Works
</h2>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12 max-w-5xl mx-auto">
{steps.map((step, index) => (
<motion.div
key={index}
className="relative text-center"
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-10 left-[60%] w-[80%] h-[2px] bg-gradient-to-r from-[#e5e5e5] to-transparent" />
)}
{/* 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>
<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>
</motion.div>
))}
</div>
{/* CTA */}
<motion.div
className="text-center mt-16"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<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

@@ -4,30 +4,28 @@ import { motion } from "framer-motion";
import { Star, ShoppingBag } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useCartStore } from "@/stores/cartStore";
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, formatPrice, parseDescription } from "@/lib/saleor";
interface NewHeroProps {
featuredProduct?: WooProduct;
featuredProduct?: Product;
}
export default function NewHero({ featuredProduct }: NewHeroProps) {
const { addItem, openCart } = useCartStore();
const { addLine, openCart } = useSaleorCheckoutStore();
const handleAddToCart = () => {
if (featuredProduct) {
addItem({
id: featuredProduct.id,
name: featuredProduct.name,
price: featuredProduct.price,
quantity: 1,
image: getProductImage(featuredProduct),
sku: featuredProduct.sku,
});
const handleAddToCart = async () => {
const variant = featuredProduct?.variants?.[0];
if (variant?.id) {
await addLine(variant.id, 1);
openCart();
}
};
const price = featuredProduct ? getProductPrice(featuredProduct) : "";
const image = featuredProduct ? getProductImage(featuredProduct) : "";
return (
<section className="relative h-screen min-h-[700px] flex flex-col overflow-hidden pt-10">
{/* Background Image */}
@@ -63,7 +61,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Product Image */}
<div className="relative aspect-square bg-[#E8F4F8]">
<Image
src={getProductImage(featuredProduct)}
src={image}
alt={featuredProduct.name}
fill
className="object-cover"
@@ -89,7 +87,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
{/* Description */}
<p className="text-sm text-[#4A4A4A]/70 mt-1 line-clamp-2">
{featuredProduct.short_description?.replace(/<[^>]*>/g, "") ||
{parseDescription(featuredProduct.description).slice(0, 100) ||
"Premium natural oil for hair and skin care"}
</p>
@@ -107,7 +105,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
<div className="flex items-center justify-between mt-4 pt-4 border-t border-[#1A1A1A]/6">
<div>
<span className="text-lg font-medium text-[#1A1A1A]">
{formatPrice(featuredProduct.price)}
{price}
</span>
<span className="text-xs text-[#4A4A4A]/60 ml-2">50ml</span>
</div>

View File

@@ -0,0 +1,73 @@
"use client";
import { motion } from "framer-motion";
export default function ProblemSection() {
return (
<section className="py-24 bg-[#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.2em] text-[#666666] mb-4 block">
The Problem
</span>
<h2 className="text-3xl md:text-4xl font-medium mb-8 leading-tight">
Tired of Hair & Skin Products That Don&apos;t Deliver?
</h2>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto mt-12">
{[
{
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>
),
problem: "Dry, Damaged Hair",
description: "Products leave your hair brittle, frizzy, and breaking despite expensive treatments",
},
{
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>
),
problem: "Confusing Ingredients",
description: "Can't pronounce what's in your skincare. parabens, sulfates, synthetic fragrances—dangerous toxins",
},
{
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>
),
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="text-center p-8 bg-white rounded-2xl shadow-sm border border-[#f0ede8]"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-red-50 flex items-center justify-center text-red-400">
{item.icon}
</div>
<h3 className="text-lg font-medium mb-3">{item.problem}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{item.description}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -1,19 +1,20 @@
"use client";
import { motion } from "framer-motion";
import { WooProduct } from "@/lib/woocommerce";
import type { Product } from "@/types/saleor";
import ProductCard from "@/components/product/ProductCard";
interface ProductShowcaseProps {
products: WooProduct[];
products: Product[];
locale?: string;
}
export default function ProductShowcase({ products }: ProductShowcaseProps) {
export default function ProductShowcase({ products, locale = "SR" }: ProductShowcaseProps) {
if (!products || products.length === 0) return null;
return (
<section className="py-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="container">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
@@ -21,15 +22,16 @@ export default function ProductShowcase({ products }: ProductShowcaseProps) {
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="text-4xl md:text-5xl font-serif mb-4">Our Products</h2>
<p className="text-foreground-muted max-w-2xl mx-auto">
<span className="text-caption text-[#666666] mb-4 block">Our Collection</span>
<h2 className="text-3xl md:text-4xl font-medium mb-4">Our Products</h2>
<p className="text-[#666666] max-w-2xl mx-auto">
Discover our premium collection of natural oils for hair and skin care
</p>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{products.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
<ProductCard key={product.id} product={product} index={index} locale={locale} />
))}
</div>
</div>

View File

@@ -0,0 +1,86 @@
"use client";
import { motion } from "framer-motion";
const badges = [
{
icon: (
<svg className="w-5 h-5" 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",
},
{
icon: (
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" 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-14 bg-gradient-to-b from-[#faf9f7] to-white border-b border-[#e8e4e0]">
<div className="container mx-auto px-4">
<motion.div
className="grid grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8"
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-6 bg-white rounded-2xl shadow-sm border border-[#f0ede8]"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<div className="w-12 h-12 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white mb-4">
{badge.icon}
</div>
<p className="text-2xl lg:text-3xl font-semibold text-[#1a1a1a] tracking-tight">
{badge.stats}
</p>
<p className="text-sm font-medium text-[#1a1a1a] mt-1">
{badge.label}
</p>
<p className="text-xs text-[#888888] mt-1">
{badge.subtext}
</p>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}

View File

@@ -1,71 +1,161 @@
import Link from "next/link";
import Image from "next/image";
import { Instagram, Facebook } from "lucide-react";
const footerLinks = {
shop: [
{ label: "All Products", href: "/products" },
{ label: "Hair Care", href: "/products" },
{ label: "Skin Care", href: "/products" },
{ label: "Gift Sets", href: "/products" },
],
about: [
{ label: "Our Story", href: "/about" },
{ label: "Process", href: "/about" },
{ label: "Sustainability", href: "/about" },
],
help: [
{ label: "FAQ", href: "/contact" },
{ label: "Shipping", href: "/contact" },
{ label: "Returns", href: "/contact" },
{ label: "Contact Us", href: "/contact" },
],
};
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer className="bg-background-ice border-t border-border/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
<div className="md:col-span-2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
width={180}
height={48}
className="h-10 w-auto object-contain mb-4"
/>
<p className="text-foreground-muted max-w-md">
Premium natural oils for hair and skin care. Crafted with love for your daily beauty routine.
<footer className="bg-white border-t border-[#e5e5e5]">
{/* Main Footer */}
<div className="container py-16 lg:py-20">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
{/* Brand Column */}
<div className="lg:col-span-4">
<Link href="/" className="inline-block mb-6">
<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-8 w-auto object-contain"
/>
</Link>
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mb-6">
Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.
</p>
{/* Social Links */}
<div className="flex items-center gap-4">
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full border border-[#e5e5e5] flex items-center justify-center text-[#666666] hover:border-black hover:text-black transition-colors"
aria-label="Instagram"
>
<Instagram className="w-4 h-4" />
</a>
<a
href="https://facebook.com"
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full border border-[#e5e5e5] flex items-center justify-center text-[#666666] hover:border-black hover:text-black transition-colors"
aria-label="Facebook"
>
<Facebook className="w-4 h-4" />
</a>
</div>
</div>
<div>
<h4 className="font-serif mb-4">Quick Links</h4>
<ul className="space-y-2">
<li>
<Link href="/products" className="text-foreground-muted hover:text-foreground transition-colors">
Products
</Link>
</li>
<li>
<Link href="/about" className="text-foreground-muted hover:text-foreground transition-colors">
About Us
</Link>
</li>
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Contact
</Link>
</li>
</ul>
</div>
{/* Links Columns - All aligned at top */}
<div className="lg:col-span-8">
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
{/* Shop */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
Shop
</h4>
<ul className="space-y-3">
{footerLinks.shop.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-[#666666] hover:text-black transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
<div>
<h4 className="font-serif mb-4">Customer Service</h4>
<ul className="space-y-2">
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Shipping Info
</Link>
</li>
<li>
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
Returns
</Link>
</li>
<li>
<a href="https://manoonoils.com" className="text-foreground-muted hover:text-foreground transition-colors">
WooCommerce Store
</a>
</li>
</ul>
{/* About */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
About
</h4>
<ul className="space-y-3">
{footerLinks.about.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-[#666666] hover:text-black transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
{/* Help */}
<div className="flex flex-col">
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
Help
</h4>
<ul className="space-y-3">
{footerLinks.help.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-[#666666] hover:text-black transition-colors"
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
<div className="border-t border-border/30 mt-12 pt-8 text-center text-foreground-muted text-sm">
<p>&copy; {currentYear} ManoonOils. All rights reserved.</p>
{/* Bottom Bar */}
<div className="border-t border-[#e5e5e5]">
<div className="container py-6">
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
{/* Copyright */}
<p className="text-xs text-[#999999]">
&copy; {currentYear} ManoonOils. All rights reserved.
</p>
{/* Payment Methods */}
<div className="flex items-center gap-3">
<span className="text-xs text-[#999999]">We accept:</span>
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
Visa
</span>
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
MC
</span>
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
COD
</span>
</div>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -1,95 +1,197 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { AnimatePresence } from "framer-motion";
import { useCartStore } from "@/stores/cartStore";
import { User, ShoppingBag, Menu } from "lucide-react";
import MobileMenu from "./MobileMenu";
import { AnimatePresence, motion } from "framer-motion";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu, X } from "lucide-react";
import CartDrawer from "@/components/cart/CartDrawer";
const navLinks = [
{ href: "/products", label: "Products" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
];
export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { items, toggleCart } = useCartStore();
const [scrolled, setScrolled] = useState(false);
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
const itemCount = items.reduce((count, item) => count + item.quantity, 0);
const itemCount = getLineCount();
// Initialize checkout on mount
useEffect(() => {
initCheckout();
}, [initCheckout]);
// Track scroll for header styling
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Lock body scroll when mobile menu is open
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [mobileMenuOpen]);
return (
<>
<header className="sticky top-10 z-40 bg-white border-b border-[#1A1A1A]/[0.06]">
<div className="max-w-[1400px] mx-auto px-6">
<div className="flex items-center justify-between h-16">
{/* Mobile Menu Button */}
<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-white/80 backdrop-blur-sm"
}`}
>
<div className="relative flex items-center justify-between h-[72px]">
{/* 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"
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">
<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-8 w-auto object-contain"
/>
</Link>
{/* Desktop Navigation */}
<nav className="hidden lg:flex items-center gap-8">
<Link
href="/products"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
Products
</Link>
<Link
href="/about"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
About
</Link>
<Link
href="/contact"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
Contact
</Link>
</nav>
{/* Icons */}
<div className="flex items-center gap-1">
<button
className="p-2 hidden sm:block"
aria-label="Account"
>
<User className="w-5 h-5" />
</button>
<button
className="p-2 relative"
onClick={toggleCart}
aria-label="Open cart"
>
<ShoppingBag className="w-5 h-5" />
{itemCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 bg-[#1A1A1A] text-white text-[10px] w-4 h-4 rounded-full flex items-center justify-center">
{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>
{/* Mobile Menu Overlay */}
<AnimatePresence>
{mobileMenuOpen && <MobileMenu onClose={() => setMobileMenuOpen(false)} />}
{mobileMenuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[60] bg-white"
>
<div className="container h-full flex flex-col">
{/* Mobile Header */}
<div className="flex items-center justify-between h-[72px]">
<Link href="/" onClick={() => setMobileMenuOpen(false)}>
<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"
/>
</Link>
<button
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(false)}
aria-label="Close menu"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Mobile Navigation */}
<nav className="flex-1 flex flex-col justify-center gap-8">
{navLinks.map((link, index) => (
<motion.div
key={link.href}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 + 0.1 }}
>
<Link
href={link.href}
onClick={() => setMobileMenuOpen(false)}
className="text-3xl font-medium tracking-tight hover:text-[#666666] transition-colors"
>
{link.label}
</Link>
</motion.div>
))}
</nav>
{/* Mobile Footer */}
<div className="py-8 border-t border-[#e5e5e5]">
<div className="flex items-center justify-between">
<button
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
onClick={() => {
setMobileMenuOpen(false);
toggleCart();
}}
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
Cart ({itemCount})
</button>
<button
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
>
<User className="w-5 h-5" strokeWidth={1.5} />
Account
</button>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<CartDrawer />

View File

@@ -0,0 +1,88 @@
"use client";
import { motion } from "framer-motion";
interface ProductBenefitsProps {
locale?: string;
}
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>
),
title: "Pure & Natural",
description: "100% natural ingredients with no additives or preservatives",
},
{
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>
),
title: "Cruelty Free",
description: "Never tested on animals, ethically sourced ingredients",
},
{
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>
),
title: "Made with Love",
description: "Handcrafted in small batches for maximum quality",
},
{
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>
),
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-[#666666] mb-3 block">
{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-sm border border-[#f0ede8]"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: index * 0.1 }}
>
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white">
{benefit.icon}
</div>
<h3 className="text-base font-medium mb-2">{benefit.title}</h3>
<p className="text-sm text-[#666666]">{benefit.description}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View File

@@ -3,15 +3,20 @@
import { motion } from "framer-motion";
import Image from "next/image";
import Link from "next/link";
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
import type { Product } from "@/types/saleor";
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
interface ProductCardProps {
product: WooProduct;
product: Product;
index?: number;
locale?: string;
}
export default function ProductCard({ product, index = 0 }: ProductCardProps) {
export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) {
const image = getProductImage(product);
const price = getProductPrice(product);
const localized = getLocalizedProduct(product, locale);
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
return (
<motion.div
@@ -20,30 +25,56 @@ export default function ProductCard({ product, index = 0 }: ProductCardProps) {
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Link href={`/products/${product.slug}`} className="group block">
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden mb-4">
{image && (
<Link href={`/products/${localized.slug}`} className="group block">
{/* Image Container */}
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
{image ? (
<Image
src={image}
alt={product.name}
alt={localized.name}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
/>
)}
{product.stock_status === "outofstock" && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
<span className="text-white font-medium">Out of Stock</span>
) : (
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
<span className="text-sm">No image</span>
</div>
)}
{/* Out of Stock Overlay */}
{!isAvailable && (
<div className="absolute inset-0 bg-white/80 flex items-center justify-center">
<span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
</span>
</div>
)}
{/* Hover Quick Add (optional) */}
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
<button
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
onClick={(e) => {
e.preventDefault();
// Quick add functionality can be added here
}}
>
{locale === "EN" ? "Quick Add" : "Dodaj u korpu"}
</button>
</div>
</div>
<h3 className="font-serif text-lg mb-1 group-hover:text-accent-dark transition-colors">
{product.name}
</h3>
<p className="text-foreground-muted">
{product.price ? formatPrice(product.price) : "Contact for price"}
</p>
{/* Product Info */}
<div className="text-center">
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
{localized.name}
</h3>
<p className="text-[14px] text-[#666666]">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
</p>
</div>
</Link>
</motion.div>
);

View File

@@ -2,181 +2,444 @@
import { useState } from "react";
import Image from "next/image";
import { motion } from "framer-motion";
import { WooProduct, formatPrice, getProductImage } from "@/lib/woocommerce";
import { useCartStore } from "@/stores/cartStore";
import 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 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: WooProduct;
relatedProducts: WooProduct[];
product: Product;
relatedProducts: Product[];
locale?: string;
}
export default function ProductDetail({ product, relatedProducts }: ProductDetailProps) {
// Expandable Section Component
function ExpandableSection({
title,
children,
defaultOpen = false
}: {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
}) {
const [isOpen, setIsOpen] = useState(defaultOpen);
return (
<div className="border-b border-[#e5e5e5]">
<button
onClick={() => setIsOpen(!isOpen)}
className="w-full py-5 flex items-center justify-between text-left group"
>
<span className="text-sm uppercase tracking-[0.1em] font-medium">
{title}
</span>
<ChevronDown
className={`w-5 h-5 transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}
strokeWidth={1.5}
/>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<div className="pb-6 text-[#666666] text-sm leading-relaxed">
{children}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
// Star Rating Component
function StarRating({ rating = 5, count = 0 }: { rating?: number; count?: number }) {
return (
<div className="flex items-center gap-1">
<div className="flex">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${i < rating ? 'fill-black text-black' : 'text-[#e5e5e5]'}`}
/>
))}
</div>
{count > 0 && (
<span className="text-sm text-[#666666] ml-1">({count})</span>
)}
</div>
);
}
export default function ProductDetail({ product, relatedProducts, locale = "SR" }: ProductDetailProps) {
const [selectedImage, setSelectedImage] = useState(0);
const [quantity, setQuantity] = useState(1);
const [activeTab, setActiveTab] = useState<"details" | "ingredients" | "usage">("details");
const addItem = useCartStore((state) => state.addItem);
const [isAdding, setIsAdding] = useState(false);
const { addLine, openCart } = useSaleorCheckoutStore();
const images = product.images?.length > 0
? product.images
: [{ id: 0, src: "/placeholder-product.jpg", alt: product.name }];
const localized = getLocalizedProduct(product, locale);
const variant = product.variants?.[0];
// Get all images from media
const images = product.media?.length > 0
? product.media.filter(m => m.type === "IMAGE")
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" as const }];
const handleAddToCart = () => {
addItem({
id: product.id,
name: product.name,
price: product.price || product.regular_price,
quantity,
image: images[0]?.src || "",
sku: product.sku || "",
});
const handleAddToCart = async () => {
if (!variant?.id) return;
setIsAdding(true);
try {
await addLine(variant.id, quantity);
openCart();
} finally {
setIsAdding(false);
}
};
const stripHtml = (html: string) => {
return html.replace(/<[^>]*>/g, "");
};
const isAvailable = variant?.quantityAvailable > 0;
const price = getProductPrice(product);
// Extract short description (first sentence or first 100 chars)
const shortDescription = localized.description
? localized.description.split('.')[0] + '.'
: locale === "EN" ? "Premium natural oil for your beauty routine." : "Premium prirodno ulje za vašu rutinu lepote.";
// Parse benefits from product metadata or use defaults
const benefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',') || [
locale === "EN" ? "Natural" : "Prirodno",
locale === "EN" ? "Organic" : "Organsko",
locale === "EN" ? "Cruelty-free" : "Bez okrutnosti",
];
return (
<>
<section className="py-12 md:py-20 px-4">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
<section className="min-h-screen" id="product-detail">
{/* Breadcrumb - with proper top padding for fixed header */}
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
<div className="container py-5">
<nav className="flex items-center gap-2 text-sm">
<Link href="/" className="text-[#666666] hover:text-black transition-colors">
{locale === "EN" ? "Home" : "Početna"}
</Link>
<span className="text-[#999999]">/</span>
<span className="text-[#1a1a1a]">{localized.name}</span>
</nav>
</div>
</div>
{/* Product Content */}
<div className="container py-12 lg:py-16">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
{/* Image Gallery - Left Side */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
className="flex gap-4"
>
<div className="relative aspect-square bg-background-ice mb-4">
{images[selectedImage] && (
<Image
src={images[selectedImage].src}
alt={images[selectedImage].alt || product.name}
fill
className="object-cover"
priority
/>
)}
</div>
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
{images.length > 1 && (
<div className="flex gap-2 overflow-x-auto">
<div className="hidden md:flex flex-col gap-3 w-20 flex-shrink-0">
{images.map((image, index) => (
<button
key={image.id}
onClick={() => setSelectedImage(index)}
className={`relative w-20 h-20 flex-shrink-0 ${
selectedImage === index ? "ring-2 ring-foreground" : ""
className={`relative aspect-square w-full overflow-hidden border-2 transition-colors ${
selectedImage === index
? "border-black"
: "border-transparent hover:border-[#999999]"
}`}
>
<Image
src={image.src}
alt={image.alt || product.name}
src={image.url}
alt={image.alt || localized.name}
fill
className="object-cover"
sizes="80px"
/>
</button>
))}
</div>
)}
{/* 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"
/>
)}
{/* 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>
</div>
</motion.div>
{/* Product Info - Right Side */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="lg:pl-8"
>
<h1 className="text-3xl md:text-4xl font-serif mb-4">
{product.name}
{/* Product Name */}
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
{localized.name}
</h1>
<p className="text-2xl text-foreground-muted mb-6">
{product.price ? formatPrice(product.price) : "Contact for price"}
{/* Short Description */}
<p className="text-[#666666] leading-relaxed mb-6">
{shortDescription}
</p>
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted">
<p>{stripHtml(product.short_description || product.description.slice(0, 200))}</p>
{/* 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")}
</span>
<StarRating rating={5} count={12} />
</div>
{product.stock_status === "instock" ? (
<div className="flex items-center gap-4 mb-8">
<div className="flex items-center border border-border">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="px-4 py-3"
>
-
</button>
<span className="px-4 py-3">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="px-4 py-3"
>
+
</button>
{/* Divider */}
<div className="border-t border-[#e5e5e5] mb-8" />
{/* Size Selector */}
{product.variants && product.variants.length > 1 && (
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<span className="text-sm uppercase tracking-[0.1em] font-medium">
{locale === "EN" ? "Size" : "Veličina"}
</span>
</div>
<div className="flex gap-3">
{product.variants.map((v) => (
<button
key={v.id}
className={`px-5 py-3 text-sm border-2 transition-colors ${
v.id === variant?.id
? "border-black bg-black text-white"
: "border-[#e5e5e5] hover:border-[#999999]"
}`}
>
{v.name}
</button>
))}
</div>
<button
onClick={handleAddToCart}
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
>
Add to Cart
</button>
</div>
) : (
<div className="py-3 bg-red-50 text-red-600 text-center mb-8">
Out of Stock
</div>
)}
<div className="border-t border-border/30">
<div className="flex border-b border-border/30">
{(["details", "ingredients", "usage"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`flex-1 py-4 font-medium capitalize ${
activeTab === tab
? "border-b-2 border-foreground"
: "text-foreground-muted"
}`}
>
{tab}
</button>
))}
</div>
<div className="py-6 text-foreground-muted">
{activeTab === "details" && (
<p>{stripHtml(product.description)}</p>
)}
{activeTab === "ingredients" && (
<p>Natural ingredients - Contact for detailed information.</p>
)}
{activeTab === "usage" && (
<p>Apply to clean skin or hair. Use daily for best results.</p>
)}
{/* Quantity */}
<div className="flex items-center gap-4 mb-8">
<span className="text-sm uppercase tracking-[0.1em] font-medium w-16">
{locale === "EN" ? "Qty" : "Kol"}
</span>
<div className="flex items-center border-2 border-[#1a1a1a]">
<button
onClick={() => setQuantity(Math.max(1, quantity - 1))}
className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
disabled={quantity <= 1}
>
<Minus className="w-4 h-4" />
</button>
<span className="w-14 text-center text-base font-medium">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
>
<Plus className="w-4 h-4" />
</button>
</div>
</div>
{/* Add to Cart Button - Action verb + value */}
{isAvailable ? (
<button
onClick={handleAddToCart}
disabled={isAdding}
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" ? "Transform My Hair & Skin" : "Transformiši kosu i kožu")
}
</button>
) : (
<div className="w-full h-16 bg-[#f8f9fa] text-[#666666] flex items-center justify-center text-base uppercase tracking-[0.15em] mb-8">
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
</div>
)}
{/* 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" />
{/* Benefits */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<span className="text-sm uppercase tracking-[0.1em] font-medium">
{locale === "EN" ? "Benefits" : "Prednosti"}
</span>
</div>
<div className="flex flex-wrap gap-2">
{benefits.map((benefit, index) => (
<span
key={index}
className="px-4 py-2 text-sm border border-[#e5e5e5] text-[#666666]"
>
{benefit.trim()}
</span>
))}
</div>
</div>
{/* Expandable Sections */}
<div>
<ExpandableSection title={locale === "EN" ? "Description" : "Opis"}>
<div dangerouslySetInnerHTML={{ __html: localized.description }} />
</ExpandableSection>
<ExpandableSection title={locale === "EN" ? "How to Use" : "Kako koristiti"}>
<p>
{locale === "EN"
? "Apply a small amount to clean, damp hair or skin. Massage gently until absorbed. Use daily for best results."
: "Nanesite malu količinu na čistu, vlažnu kosu ili kožu. Nežno masirajte dok se ne upije. Koristite svakodnevno za najbolje rezultate."
}
</p>
</ExpandableSection>
<ExpandableSection title={locale === "EN" ? "Ingredients" : "Sastojci"}>
<p>
{locale === "EN"
? "100% Pure Natural Oil. No additives, preservatives, or artificial fragrances."
: "100% čisto prirodno ulje. Bez dodataka, konzervansa ili veštačkih mirisa."
}
</p>
</ExpandableSection>
</div>
{/* SKU */}
{variant?.sku && (
<p className="text-xs text-[#999999] mt-8">
SKU: {variant.sku}
</p>
)}
</motion.div>
</div>
</div>
</section>
{relatedProducts.length > 0 && (
<section className="py-12 px-4 bg-background-ice">
<div className="max-w-7xl mx-auto">
<h2 className="text-2xl font-serif text-center mb-8">
You May Also Like
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
{relatedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} />
{/* 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]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
</span>
<h2 className="text-3xl md:text-4xl font-medium">
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
</h2>
</div>
<div className="flex flex-wrap justify-center gap-6 lg:gap-8">
{relatedProducts.filter(p => p && p.id).slice(0, 4).map((relatedProduct, index) => (
<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,175 @@
"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-gold text-gold" 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>
<p className="text-sm font-medium">{review.name}</p>
<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-gold text-gold" 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>
</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

@@ -0,0 +1,63 @@
"use client";
import { Component, ErrorInfo, ReactNode } from "react";
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export default class ErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
};
public static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Ignore browser extension errors
if (error.message?.includes('tron') ||
error.message?.includes('chrome-extension') ||
error.stack?.includes('chrome-extension')) {
console.warn('Browser extension error ignored:', error.message);
// Reset error state to continue rendering
this.setState({ hasError: false });
return;
}
console.error("Uncaught error:", error, errorInfo);
}
public render() {
if (this.state.hasError) {
// Check if it's an extension error
if (this.state.error?.message?.includes('tron') ||
this.state.error?.stack?.includes('chrome-extension')) {
// Silently recover and render children
return this.props.children;
}
return (
<div className="min-h-screen flex items-center justify-center p-4">
<div className="text-center">
<h2 className="text-2xl font-serif mb-4">Something went wrong</h2>
<button
onClick={() => this.setState({ hasError: false })}
className="px-6 py-3 bg-foreground text-white"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}

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

@@ -32,4 +32,5 @@ export {
isProductAvailable,
formatPrice,
getLocalizedProduct,
parseDescription,
} from "./products";

View File

@@ -1,15 +1,26 @@
import { saleorClient } from "./client";
import { GET_PRODUCTS, GET_PRODUCT_BY_SLUG } from "./queries/Products";
import type { Product, ProductList } from "@/types/saleor";
import type { Product } from "@/types/saleor";
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
// GraphQL Response Types
interface ProductsResponse {
products?: {
edges: Array<{ node: Product }>;
};
}
interface ProductResponse {
product?: Product | null;
}
export async function getProducts(
locale: string = "SR",
first: number = 100
): Promise<Product[]> {
try {
const { data } = await saleorClient.query({
const { data } = await saleorClient.query<ProductsResponse>({
query: GET_PRODUCTS,
variables: {
channel: CHANNEL,
@@ -18,7 +29,7 @@ export async function getProducts(
},
});
return data?.products?.edges.map((edge: { node: Product }) => edge.node) || [];
return data?.products?.edges.map((edge) => edge.node) || [];
} catch (error) {
console.error("Error fetching products from Saleor:", error);
return [];
@@ -30,7 +41,7 @@ export async function getProductBySlug(
locale: string = "SR"
): Promise<Product | null> {
try {
const { data } = await saleorClient.query({
const { data } = await saleorClient.query<ProductResponse>({
query: GET_PRODUCT_BY_SLUG,
variables: {
slug,
@@ -81,6 +92,39 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
}).format(amount);
}
// Parse Saleor's JSON description format (EditorJS) to plain text/HTML
export function parseDescription(description: string | null | undefined): string {
if (!description) return "";
// If it's already plain text (not JSON), return as-is
if (!description.startsWith("{")) {
return description;
}
try {
const parsed = JSON.parse(description);
// Handle EditorJS format: { blocks: [{ data: { text: "..." } }] }
if (parsed.blocks && Array.isArray(parsed.blocks)) {
return parsed.blocks
.map((block: any) => {
if (block.data?.text) {
return block.data.text;
}
return "";
})
.filter(Boolean)
.join("\n\n");
}
// Fallback: return stringified if unknown format
return description;
} catch (e) {
// If JSON parse fails, return original
return description;
}
}
// Get localized product data
export function getLocalizedProduct(
product: Product,
@@ -94,11 +138,13 @@ export function getLocalizedProduct(
} {
const isEnglish = locale.toLowerCase() === "en";
const translation = isEnglish ? product.translation : null;
const rawDescription = translation?.description || product.description;
return {
name: translation?.name || product.name,
slug: translation?.slug || product.slug,
description: translation?.description || product.description,
description: parseDescription(rawDescription),
seoTitle: translation?.seoTitle || product.seoTitle,
seoDescription: translation?.seoDescription || product.seoDescription,
};

View File

@@ -18,4 +18,3 @@ export const GET_CHECKOUT_BY_ID = gql`
}
${CHECKOUT_FRAGMENT}
`;
`;

View File

@@ -1,129 +0,0 @@
import WooCommerceRestApi from "@woocommerce/woocommerce-rest-api";
// Lazy initialization - only create API client when needed
let apiInstance: WooCommerceRestApi | null = null;
function getApi(): WooCommerceRestApi {
if (!apiInstance) {
const url = process.env.NEXT_PUBLIC_WOOCOMMERCE_URL;
const consumerKey = process.env.NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_KEY;
const consumerSecret = process.env.NEXT_PUBLIC_WOOCOMMERCE_CONSUMER_SECRET;
if (!url || !consumerKey || !consumerSecret) {
throw new Error("WooCommerce API credentials not configured");
}
apiInstance = new WooCommerceRestApi({
url,
consumerKey,
consumerSecret,
version: "wc/v3",
queryStringAuth: true, // Use query string auth instead of basic auth (more reliable)
});
}
return apiInstance;
}
export interface WooProduct {
id: number;
name: string;
slug: string;
price: string;
regular_price: string;
sale_price: string;
description: string;
short_description: string;
status: "publish" | "draft" | "private";
stock_status: "instock" | "outofstock";
images: { id: number; src: string; alt: string }[];
sku: string;
categories: { id: number; name: string; slug: string }[];
meta_data: { key: string; value: string }[];
}
export interface WooCategory {
id: number;
name: string;
slug: string;
description: string;
image: { src: string } | null;
}
export async function getProducts(perPage = 100): Promise<WooProduct[]> {
try {
const api = getApi();
const response = await api.get("products", { per_page: perPage });
return response.data;
} catch (error) {
console.error("Error fetching products:", error);
return [];
}
}
export async function getProduct(id: number): Promise<WooProduct | null> {
try {
const api = getApi();
const response = await api.get(`products/${id}`);
return response.data;
} catch (error) {
console.error(`Error fetching product ${id}:`, error);
return null;
}
}
export async function getProductBySlug(slug: string): Promise<WooProduct | null> {
try {
const api = getApi();
const response = await api.get("products", { slug });
return response.data[0] || null;
} catch (error) {
console.error(`Error fetching product by slug ${slug}:`, error);
return null;
}
}
export async function getCategories(): Promise<WooCategory[]> {
try {
const api = getApi();
const response = await api.get("product-categories", { per_page: 100 });
return response.data;
} catch (error) {
console.error("Error fetching categories:", error);
return [];
}
}
export async function getProductsByCategory(
categoryId: number
): Promise<WooProduct[]> {
try {
const api = getApi();
const response = await api.get("products", {
category: categoryId,
per_page: 100,
});
return response.data;
} catch (error) {
console.error(`Error fetching products for category ${categoryId}:`, error);
return [];
}
}
export function formatPrice(price: string, currency = "RSD"): string {
const num = parseFloat(price);
if (isNaN(num)) return "0 RSD";
return new Intl.NumberFormat("sr-RS", {
style: "currency",
currency: currency,
minimumFractionDigits: 0,
}).format(num);
}
export function getProductImage(product: WooProduct): string {
if (product.images && product.images.length > 0) {
return product.images[0].src;
}
return "/placeholder-product.jpg";
}
export default getApi;

View File

@@ -1,86 +0,0 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export interface CartItem {
id: number;
name: string;
price: string;
quantity: number;
image: string;
sku: string;
}
interface CartStore {
items: CartItem[];
isOpen: boolean;
addItem: (item: CartItem) => void;
removeItem: (id: number) => void;
updateQuantity: (id: number, quantity: number) => void;
toggleCart: () => void;
openCart: () => void;
closeCart: () => void;
clearCart: () => void;
getTotal: () => number;
getItemCount: () => number;
}
export const useCartStore = create<CartStore>()(
persist(
(set, get) => ({
items: [],
isOpen: false,
addItem: (item) => {
const items = get().items;
const existingItem = items.find((i) => i.id === item.id);
if (existingItem) {
set({
items: items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
});
} else {
set({ items: [...items, item] });
}
set({ isOpen: true });
},
removeItem: (id) => {
set({ items: get().items.filter((i) => i.id !== id) });
},
updateQuantity: (id, quantity) => {
if (quantity <= 0) {
set({ items: get().items.filter((i) => i.id !== id) });
} else {
set({
items: get().items.map((i) =>
i.id === id ? { ...i, quantity } : i
),
});
}
},
toggleCart: () => set({ isOpen: !get().isOpen }),
openCart: () => set({ isOpen: true }),
closeCart: () => set({ isOpen: false }),
clearCart: () => set({ items: [] }),
getTotal: () => {
return get().items.reduce((total, item) => {
return total + parseFloat(item.price) * item.quantity;
}, 0);
},
getItemCount: () => {
return get().items.reduce((count, item) => count + item.quantity, 0);
},
}),
{
name: "manoonoils-cart",
}
)
);

View File

@@ -0,0 +1,321 @@
"use client";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { saleorClient } from "@/lib/saleor/client";
import {
CHECKOUT_CREATE,
CHECKOUT_LINES_ADD,
CHECKOUT_LINES_UPDATE,
CHECKOUT_LINES_DELETE,
CHECKOUT_EMAIL_UPDATE,
} from "@/lib/saleor/mutations/Checkout";
import { GET_CHECKOUT } from "@/lib/saleor/queries/Checkout";
import type { Checkout, CheckoutLine } from "@/types/saleor";
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
// GraphQL Response Types
interface CheckoutCreateResponse {
checkoutCreate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutLinesAddResponse {
checkoutLinesAdd?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutLinesUpdateResponse {
checkoutLinesUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutLinesDeleteResponse {
checkoutLinesDelete?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutEmailUpdateResponse {
checkoutEmailUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface GetCheckoutResponse {
checkout?: Checkout;
}
interface SaleorCheckoutStore {
checkout: Checkout | null;
checkoutToken: string | null;
isOpen: boolean;
isLoading: boolean;
error: string | null;
// Actions
initCheckout: () => Promise<void>;
addLine: (variantId: string, quantity: number) => Promise<void>;
updateLine: (lineId: string, quantity: number) => Promise<void>;
removeLine: (lineId: string) => Promise<void>;
setEmail: (email: string) => Promise<void>;
refreshCheckout: () => Promise<void>;
toggleCart: () => void;
openCart: () => void;
closeCart: () => void;
clearError: () => void;
// Getters
getLineCount: () => number;
getTotal: () => number;
getLines: () => CheckoutLine[];
}
export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
persist(
(set, get) => ({
checkout: null,
checkoutToken: null,
isOpen: false,
isLoading: false,
error: null,
initCheckout: async () => {
const { checkoutToken } = get();
if (checkoutToken) {
// Try to fetch existing checkout
try {
const { data } = await saleorClient.query<GetCheckoutResponse>({
query: GET_CHECKOUT,
variables: { token: checkoutToken },
});
if (data?.checkout) {
set({ checkout: data.checkout });
return;
}
} catch (e) {
// Checkout not found or expired, create new one
}
}
// Create new checkout
try {
const { data } = await saleorClient.mutate<CheckoutCreateResponse>({
mutation: CHECKOUT_CREATE,
variables: {
input: {
channel: CHANNEL,
lines: [],
},
},
});
if (data?.checkoutCreate?.checkout) {
set({
checkout: data.checkoutCreate.checkout,
checkoutToken: data.checkoutCreate.checkout.token,
});
}
} catch (e: any) {
set({ error: e.message });
}
},
addLine: async (variantId: string, quantity: number) => {
set({ isLoading: true, error: null });
try {
let { checkout, checkoutToken } = get();
// Initialize checkout if needed
if (!checkout) {
await get().initCheckout();
checkout = get().checkout;
checkoutToken = get().checkoutToken;
}
if (!checkout) {
throw new Error("Failed to initialize checkout");
}
const { data } = await saleorClient.mutate<CheckoutLinesAddResponse>({
mutation: CHECKOUT_LINES_ADD,
variables: {
checkoutId: checkout.id,
lines: [{ variantId, quantity }],
},
});
if (data?.checkoutLinesAdd?.checkout) {
set({
checkout: data.checkoutLinesAdd.checkout,
isOpen: true,
isLoading: false,
});
} else if (data?.checkoutLinesAdd?.errors && data.checkoutLinesAdd.errors.length > 0) {
throw new Error(data.checkoutLinesAdd.errors[0].message);
}
} catch (e: any) {
set({ error: e.message, isLoading: false });
}
},
updateLine: async (lineId: string, quantity: number) => {
set({ isLoading: true, error: null });
try {
const { checkout } = get();
if (!checkout) {
throw new Error("No active checkout");
}
if (quantity <= 0) {
// Remove line if quantity is 0 or less
await get().removeLine(lineId);
return;
}
const { data } = await saleorClient.mutate<CheckoutLinesUpdateResponse>({
mutation: CHECKOUT_LINES_UPDATE,
variables: {
checkoutId: checkout.id,
lines: [{ lineId, quantity }],
},
});
if (data?.checkoutLinesUpdate?.checkout) {
set({
checkout: data.checkoutLinesUpdate.checkout,
isLoading: false,
});
} else if (data?.checkoutLinesUpdate?.errors && data.checkoutLinesUpdate.errors.length > 0) {
throw new Error(data.checkoutLinesUpdate.errors[0].message);
}
} catch (e: any) {
set({ error: e.message, isLoading: false });
}
},
removeLine: async (lineId: string) => {
set({ isLoading: true, error: null });
try {
const { checkout } = get();
if (!checkout) {
throw new Error("No active checkout");
}
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
mutation: CHECKOUT_LINES_DELETE,
variables: {
checkoutId: checkout.id,
lineIds: [lineId],
},
});
if (data?.checkoutLinesDelete?.checkout) {
set({
checkout: data.checkoutLinesDelete.checkout,
isLoading: false,
});
} else if (data?.checkoutLinesDelete?.errors && data.checkoutLinesDelete.errors.length > 0) {
throw new Error(data.checkoutLinesDelete.errors[0].message);
}
} catch (e: any) {
set({ error: e.message, isLoading: false });
}
},
setEmail: async (email: string) => {
set({ isLoading: true, error: null });
try {
const { checkout } = get();
if (!checkout) {
throw new Error("No active checkout");
}
const { data } = await saleorClient.mutate<CheckoutEmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE,
variables: {
checkoutId: checkout.id,
email,
},
});
if (data?.checkoutEmailUpdate?.checkout) {
set({
checkout: data.checkoutEmailUpdate.checkout,
isLoading: false,
});
} else if (data?.checkoutEmailUpdate?.errors && data.checkoutEmailUpdate.errors.length > 0) {
throw new Error(data.checkoutEmailUpdate.errors[0].message);
}
} catch (e: any) {
set({ error: e.message, isLoading: false });
}
},
refreshCheckout: async () => {
const { checkoutToken } = get();
if (!checkoutToken) return;
try {
const { data } = await saleorClient.query<GetCheckoutResponse>({
query: GET_CHECKOUT,
variables: { token: checkoutToken },
});
if (data?.checkout) {
set({ checkout: data.checkout });
}
} catch (e) {
// Checkout might be expired
set({ checkout: null, checkoutToken: null });
}
},
toggleCart: () => set((state) => ({ isOpen: !state.isOpen })),
openCart: () => set({ isOpen: true }),
closeCart: () => set({ isOpen: false }),
clearError: () => set({ error: null }),
getLineCount: () => {
const { checkout } = get();
if (!checkout?.lines) return 0;
return checkout.lines.reduce((count, line) => count + line.quantity, 0);
},
getTotal: () => {
const { checkout } = get();
return checkout?.totalPrice?.gross?.amount || 0;
},
getLines: () => {
const { checkout } = get();
return checkout?.lines || [];
},
}),
{
name: "manoonoils-saleor-checkout",
partialize: (state) => ({
checkoutToken: state.checkoutToken,
}),
}
)
);