10 Commits

Author SHA1 Message Date
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
32 changed files with 3308 additions and 1510 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

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

420
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.6", "@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.34.4", "framer-motion": "^12.34.4",
"graphql": "^16.13.1", "graphql": "^16.13.1",
@@ -28,6 +27,7 @@
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"playwright": "^1.58.2",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
} }
@@ -2739,21 +2739,6 @@
"win32" "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": { "node_modules/@wry/caches": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz",
@@ -3052,16 +3037,11 @@
"node": ">= 0.4" "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": { "node_modules/available-typed-arrays": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"possible-typed-array-names": "^1.0.0" "possible-typed-array-names": "^1.0.0"
@@ -3083,17 +3063,6 @@
"node": ">=4" "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": { "node_modules/axobject-query": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3185,6 +3154,7 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.0", "call-bind-apply-helpers": "^1.0.0",
@@ -3203,6 +3173,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -3216,6 +3187,7 @@
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -3275,20 +3247,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "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": { "node_modules/client-only": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
@@ -3324,18 +3282,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -3350,39 +3296,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3501,6 +3414,7 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0", "es-define-property": "^1.0.0",
@@ -3532,15 +3446,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -3567,6 +3472,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.1", "call-bind-apply-helpers": "^1.0.1",
@@ -3678,6 +3584,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -3687,6 +3594,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -3724,6 +3632,7 @@
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0" "es-errors": "^1.3.0"
@@ -3736,6 +3645,7 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -4356,30 +4266,11 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/for-each": {
"version": "0.3.5", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-callable": "^1.2.7" "is-callable": "^1.2.7"
@@ -4391,22 +4282,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/framer-motion": {
"version": "12.34.4", "version": "12.34.4",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.4.tgz",
@@ -4434,10 +4309,26 @@
} }
} }
}, },
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@@ -4498,6 +4389,7 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind-apply-helpers": "^1.0.2", "call-bind-apply-helpers": "^1.0.2",
@@ -4522,6 +4414,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dunder-proto": "^1.0.1", "dunder-proto": "^1.0.1",
@@ -4609,6 +4502,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4675,6 +4569,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-define-property": "^1.0.0" "es-define-property": "^1.0.0"
@@ -4703,6 +4598,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -4715,6 +4611,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -4726,25 +4623,11 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@@ -4822,12 +4705,6 @@
"node": ">=0.8.19" "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": { "node_modules/internal-slot": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -4953,6 +4830,7 @@
"version": "1.2.7", "version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -5209,6 +5087,7 @@
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"which-typed-array": "^1.1.16" "which-typed-array": "^1.1.16"
@@ -5270,6 +5149,7 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
"integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/isexe": { "node_modules/isexe": {
@@ -5764,22 +5644,12 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "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": { "node_modules/merge2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5804,27 +5674,6 @@
"node": ">=8.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": { "node_modules/minimatch": {
"version": "3.1.5", "version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
@@ -6109,12 +5958,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -6377,6 +6220,38 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/po-parser": { "node_modules/po-parser": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz", "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.1.1.tgz",
@@ -6387,6 +6262,7 @@
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -6431,12 +6307,6 @@
"node": ">= 0.8.0" "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": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6449,12 +6319,6 @@
"react-is": "^16.13.1" "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": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6465,12 +6329,6 @@
"node": ">=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": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -6520,33 +6378,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/reflect.getprototypeof": {
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6591,12 +6422,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/resolve": {
"version": "1.22.11", "version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -6649,19 +6474,6 @@
"node": ">=0.10.0" "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": { "node_modules/run-parallel": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6716,26 +6528,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/safe-push-apply": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6791,6 +6583,7 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"define-data-property": "^1.1.4", "define-data-property": "^1.1.4",
@@ -6835,26 +6628,6 @@
"node": ">= 0.4" "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": { "node_modules/sharp": {
"version": "0.34.5", "version": "0.34.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
@@ -7042,21 +6815,6 @@
"node": ">= 0.4" "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": { "node_modules/string.prototype.includes": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -7321,20 +7079,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7410,6 +7154,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
"integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bound": "^1.0.3", "call-bound": "^1.0.3",
@@ -7623,16 +7368,6 @@
"punycode": "^2.1.0" "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": { "node_modules/use-intl": {
"version": "4.8.3", "version": "4.8.3",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
@@ -7654,12 +7389,6 @@
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7747,6 +7476,7 @@
"version": "1.1.20", "version": "1.1.20",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
"integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"available-typed-arrays": "^1.0.7", "available-typed-arrays": "^1.0.7",

View File

@@ -10,7 +10,6 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^4.1.6", "@apollo/client": "^4.1.6",
"@woocommerce/woocommerce-rest-api": "^1.0.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.34.4", "framer-motion": "^12.34.4",
"graphql": "^16.13.1", "graphql": "^16.13.1",
@@ -29,6 +28,7 @@
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"playwright": "^1.58.2",
"tailwindcss": "^4", "tailwindcss": "^4",
"typescript": "^5" "typescript": "^5"
} }

View File

@@ -8,59 +8,114 @@ export const metadata = {
export default function AboutPage() { export default function AboutPage() {
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<main className="min-h-screen bg-white">
<section className="py-20 px-4"> {/* Page Header */}
<div className="max-w-4xl mx-auto"> <div className="pt-[104px]">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8"> <div className="container py-12 md:py-16">
Our Story <div className="max-w-2xl mx-auto text-center">
</h1> <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">
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6"> About ManoonOils
<p> </h1>
ManoonOils was born from a passion for natural beauty and the belief </div>
that the best skincare comes from nature itself. Our journey began with </div>
a simple question: how can we create products that truly nurture both </div>
hair and skin?
</p> {/* Hero Image */}
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
<p> <img
We believe in the power of natural ingredients. Every oil in our src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
collection is carefully selected for its unique properties and alt="Natural oils production"
benefits. From nourishing oils that restore hair vitality to serums className="w-full h-full object-cover"
that rejuvenate skin, we craft each product with love and attention />
to detail. <div className="absolute inset-0 bg-black/20" />
</p> </div>
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4"> {/* Content */}
Our Mission <section className="py-16 md:py-24">
</h2> <div className="container">
<p> <div className="max-w-3xl mx-auto">
Our mission is to provide premium quality, natural products that {/* Introduction */}
enhance your daily beauty routine. We are committed to: <div className="mb-16">
</p> <p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
<ul className="list-disc pl-6 space-y-2"> ManoonOils was born from a passion for natural beauty and the belief
<li>Using only the finest natural ingredients</li> that the best skincare comes from nature itself.
<li>Cruelty-free and ethical production</li> </p>
<li>Sustainable packaging practices</li> <p className="text-[#666666] leading-relaxed">
<li>Transparency in our formulations</li> We believe in the power of natural ingredients. Every oil in our
</ul> collection is carefully selected for its unique properties and
benefits. From nourishing oils that restore hair vitality to serums
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4"> that rejuvenate skin, we craft each product with love and attention
Handmade with Love to detail.
</h2> </p>
<p> </div>
Every bottle of ManoonOils is handcrafted with care. We small-batch
produce our products to ensure the highest quality and freshness. {/* Values Grid */}
When you use ManoonOils, you can feel confident that you're using <div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
something made with genuine care and expertise. <div className="p-6 bg-[#f8f9fa]">
</p> <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>
</div> </div>
</section> </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 { useState } from "react";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import { Mail, MapPin, Truck, Check } from "lucide-react";
export default function ContactPage() { export default function ContactPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -18,97 +19,185 @@ export default function ContactPage() {
}; };
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<main className="min-h-screen bg-white">
<section className="py-20 px-4"> {/* Page Header */}
<div className="max-w-2xl mx-auto"> <div className="pt-[104px]">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8"> <div className="container py-12 md:py-16">
Contact Us <div className="max-w-2xl mx-auto text-center">
</h1> <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">
<p className="text-foreground-muted text-center mb-12"> Contact Us
Have questions? We'd love to hear from you. </h1>
</p> <p className="text-[#666666]">
Have questions? We&apos;d love to hear from you.
{submitted ? ( </p>
<div className="bg-green-50 text-green-700 p-6 text-center"> </div>
<p className="text-lg">Thank you for your message!</p> </div>
<p className="mt-2">We'll get back to you soon.</p> </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> </div>
) : (
<form onSubmit={handleSubmit} className="space-y-6"> {/* Contact Form */}
<div> <div className="bg-[#f8f9fa] p-8 md:p-10">
<label htmlFor="name" className="block text-sm font-medium mb-2"> {submitted ? (
Name <div className="text-center py-12">
</label> <div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
<input <Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
type="text" </div>
id="name" <h3 className="text-xl font-medium mb-2">Thank You!</h3>
required <p className="text-[#666666]">
value={formData.name} Your message has been sent. We&apos;ll get back to you soon.
onChange={(e) => setFormData({ ...formData, name: e.target.value })} </p>
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground" </div>
/> ) : (
</div> <form onSubmit={handleSubmit} className="space-y-6">
<div>
<div> <label htmlFor="name" className="block text-sm font-medium mb-2">
<label htmlFor="email" className="block text-sm font-medium mb-2"> Name
Email </label>
</label> <input
<input type="text"
type="email" id="name"
id="email" required
required value={formData.name}
value={formData.email} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
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"
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground" placeholder="Your name"
/> />
</div> </div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium mb-2"> <label htmlFor="email" className="block text-sm font-medium mb-2">
Message Email
</label> </label>
<textarea <input
id="message" type="email"
required id="email"
rows={5} required
value={formData.message} value={formData.email}
onChange={(e) => setFormData({ ...formData, message: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none" 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>
<button
type="submit" <div>
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors" <label htmlFor="message" className="block text-sm font-medium mb-2">
> Message
Send Message </label>
</button> <textarea
</form> id="message"
)} required
rows={5}
<div className="mt-16 pt-8 border-t border-border/30"> value={formData.message}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center"> onChange={(e) => setFormData({ ...formData, message: e.target.value })}
<div> className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
<h3 className="font-serif mb-2">Email</h3> placeholder="How can we help you?"
<p className="text-foreground-muted">hello@manoonoils.com</p> />
</div> </div>
<div>
<h3 className="font-serif mb-2">Shipping</h3> <button
<p className="text-foreground-muted">Free over 3000 RSD</p> type="submit"
</div> className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
<div> >
<h3 className="font-serif mb-2">Location</h3> Send Message
<p className="text-foreground-muted">Serbia</p> </button>
</div> </form>
)}
</div> </div>
</div> </div>
</div> </div>
</section> </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 /> <div className="pt-16">
</main> <Footer />
</div>
</>
); );
} }

View File

@@ -1,66 +1,2 @@
import Header from "@/components/layout/Header"; // Re-export from main about page
import Footer from "@/components/layout/Footer"; export { default, metadata } from "../../about/page";
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>
);
}

View File

@@ -1,12 +1,8 @@
import { getProducts } from "@/lib/woocommerce"; import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar"; import HeroVideo from "@/components/home/HeroVideo";
import NewHero from "@/components/home/NewHero"; import ProductCard from "@/components/product/ProductCard";
import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import NewsletterSection from "@/components/home/NewsletterSection";
export const metadata = { export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin", title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,61 +13,160 @@ export const metadata = {
export default async function Homepage() { export default async function Homepage() {
let products: any[] = []; let products: any[] = [];
try { try {
products = await getProducts(); products = await getProducts("EN");
} catch (e) { } catch (e) {
// Fallback for build time when API is unavailable
console.log('Failed to fetch products during build'); console.log('Failed to fetch products during build');
} }
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products const featuredProducts = products.slice(0, 4);
.filter((p) => p.status === "publish")
.slice(0, 4);
return ( return (
<main className="min-h-screen bg-white"> <>
<AnnouncementBar /> <Header />
<div className="pt-10">
<Header /> <main className="min-h-screen bg-white">
</div> {/* Hero Section with Video Background */}
<HeroVideo />
{/* New Hero Section */} {/* Main Content */}
<NewHero featuredProduct={featuredProduct} /> <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 */} {/* Products Grid */}
<StatsSection /> <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 */} {/* View All Link */}
<FeaturesSection /> <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 */} {/* Brand Story Section */}
<TestimonialsSection /> <section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
<div className="container">
{/* Newsletter Section */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<NewsletterSection /> <div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
{/* Products Grid Section */} <h2 className="text-3xl md:text-4xl font-medium mb-6">Handmade with Love</h2>
{publishedProducts.length > 0 && ( <p className="text-[#666666] mb-6 leading-relaxed">
<section className="py-20 px-6 bg-white"> Every bottle of ManoonOils is crafted with care using traditional
<div className="max-w-[1400px] mx-auto"> methods passed down through generations. We source only the finest
<h2 className="font-serif italic text-4xl text-center mb-4"> organic ingredients to bring you oils that nourish both hair and skin.
Our Collection </p>
</h2> <p className="text-[#666666] mb-8 leading-relaxed">
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto"> Our commitment to purity means no additives, no preservatives -
Cold-pressed, pure, and natural oils for your daily beauty routine just nature&apos;s goodness in its most potent form.
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> <a
{publishedProducts.map((product, index) => ( href="/en/about"
<ProductCard key={product.id} product={product} index={index} /> 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>
</div> </section>
</section>
)}
<Footer /> {/* Benefits Section */}
</main> <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 Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; 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 // Generate static params for all products
export const generateStaticParams = undefined; export async function generateStaticParams() {
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
let product = null;
try { try {
const products = await getProducts(); const products = await getProducts("EN", 100);
product = products.find((p) => (p.slug || p.id.toString()) === slug); 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) { } 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) { if (!product) {
return ( return (
<main className="min-h-screen"> <main className="min-h-screen bg-white">
<Header /> <Header />
<div className="pt-24 text-center"> <div className="pt-[120px] text-center px-4">
<h1 className="text-2xl">Product not found</h1> <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> </div>
<Footer /> <Footer />
</main> </main>
); );
} }
const image = product.images?.[0]?.src || '/placeholder.jpg'; // Get related products
const price = product.sale_price || product.price; 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 ( return (
<main className="min-h-screen"> <main className="min-h-screen bg-white">
<Header /> <Header />
<ProductDetail
<section className="pt-24 pb-20 px-4"> product={product}
<div className="max-w-7xl mx-auto"> relatedProducts={relatedProducts}
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> locale="EN"
<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>
<Footer /> <Footer />
</main> </main>
); );

View File

@@ -1,7 +1,8 @@
import { getProducts } from "@/lib/woocommerce"; import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react";
export const metadata = { export const metadata = {
title: "Products - ManoonOils", title: "Products - ManoonOils",
@@ -9,38 +10,71 @@ export const metadata = {
}; };
export default async function ProductsPage() { export default async function ProductsPage() {
let products: any[] = []; const products = await getProducts("EN");
try {
products = await getProducts();
} catch (e) {
console.log('Failed to fetch products during build');
}
const publishedProducts = products.filter((p) => p.status === "publish");
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<section className="py-20 px-4"> <main className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto"> {/* Page Header */}
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16"> <div className="pt-[104px]">
All Products <div className="border-b border-[#e5e5e5]">
</h1> <div className="container py-8 md:py-12">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
{publishedProducts.length === 0 ? ( <div>
<p className="text-center text-foreground-muted">No products available</p> <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 className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> </div>
{publishedProducts.map((product, index) => (
<ProductCard key={product.id} product={product} index={index} /> {/* 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>
)} </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> </div>
</section> </main>
<Footer /> <div className="pt-16">
</main> <Footer />
</div>
</>
); );
} }

View File

@@ -1,36 +1,75 @@
@import "tailwindcss"; @import "tailwindcss";
:root { /* ============================================
--background: #f0f4f8; MANOONOILS DESIGN SYSTEM
--background-ice: #e8f0f5; Tailwind 4 compatible - uses CSS layers
--foreground: #1a1a1a; ============================================ */
--foreground-muted: #666666;
--accent: #a8c5d8;
--accent-dark: #7ba3bc;
--white: #ffffff;
--border: #d1d9e0;
}
@theme inline { @theme inline {
--color-background: var(--background); /* Colors - reference CSS variables */
--color-background-ice: var(--background-ice); --color-white: var(--color-white);
--color-foreground: var(--foreground); --color-background: var(--color-background);
--color-foreground-muted: var(--foreground-muted); --color-background-alt: var(--color-background-alt);
--color-accent: var(--accent); --color-foreground: var(--color-foreground);
--color-accent-dark: var(--accent-dark); --color-foreground-muted: var(--color-foreground-muted);
--color-white: var(--white); --color-foreground-subtle: var(--color-foreground-subtle);
--color-border: var(--border); --color-accent: var(--color-accent);
--font-display: var(--font-cedrat); --color-accent-dark: var(--color-accent-dark);
--font-body: var(--font-dm-sans); --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'; CSS VARIABLES
src: url('https://fonts.gstatic.com/s/cedratdisplay/v16/0nkoC9_pK3CvS5lZuZ7MAUmK5w.woff2') format('woff2'); ============================================ */
font-weight: 400 900;
font-display: swap; :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-face {
font-family: 'DM Sans'; font-family: 'DM Sans';
src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2'); src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2');
@@ -38,77 +77,294 @@
font-display: swap; font-display: swap;
} }
* { @font-face {
box-sizing: border-box; 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); BASE STYLES (in Tailwind base layer)
color: var(--foreground); ============================================ */
font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 { @layer base {
font-family: 'Cedrat Display', serif; html {
} scroll-behavior: smooth;
/* Marquee Animations */
@keyframes marquee {
0% {
transform: translateX(0);
} }
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% { COMPONENTS
transform: translateX(0); ============================================ */
@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 { @media (prefers-reduced-motion: reduce) {
animation: marquee 15s linear infinite; *,
} *::before,
*::after {
/* Utility Classes */ animation-duration: 0.01ms !important;
.font-serif { animation-iteration-count: 1 !important;
font-family: 'Cedrat Display', serif; transition-duration: 0.01ms !important;
} }
/* Smooth scroll */ html {
html { scroll-behavior: auto;
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;
} }

View File

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

View File

@@ -1,12 +1,8 @@
import { getProducts } from "@/lib/woocommerce"; import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import AnnouncementBar from "@/components/home/AnnouncementBar"; import HeroVideo from "@/components/home/HeroVideo";
import NewHero from "@/components/home/NewHero"; import ProductCard from "@/components/product/ProductCard";
import StatsSection from "@/components/home/StatsSection";
import FeaturesSection from "@/components/home/FeaturesSection";
import TestimonialsSection from "@/components/home/TestimonialsSection";
import NewsletterSection from "@/components/home/NewsletterSection";
export const metadata = { export const metadata = {
title: "ManoonOils - Premium Natural Oils for Hair & Skin", title: "ManoonOils - Premium Natural Oils for Hair & Skin",
@@ -17,61 +13,175 @@ export const metadata = {
export default async function Homepage() { export default async function Homepage() {
let products: any[] = []; let products: any[] = [];
try { try {
products = await getProducts(); products = await getProducts("SR");
} catch (e) { } catch (e) {
// Fallback for build time when API is unavailable // Fallback for build time when API is unavailable
console.log('Failed to fetch products during build'); console.log('Failed to fetch products during build');
} }
const featuredProduct = products.find((p) => p.status === "publish");
const publishedProducts = products const featuredProducts = products?.slice(0, 4) || [];
.filter((p) => p.status === "publish") const hasProducts = featuredProducts.length > 0;
.slice(0, 4);
return ( return (
<main className="min-h-screen bg-white"> <>
<AnnouncementBar /> <Header />
<div className="pt-10">
<Header /> <main className="min-h-screen bg-white">
</div> {/* Hero Section with Video Background */}
<HeroVideo />
{/* New Hero Section */} {/* Main Content */}
<NewHero featuredProduct={featuredProduct} /> <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>
{/* Stats & Philosophy Section */} {/* Products Grid */}
<StatsSection /> <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>
{/* Features Section */} {/* View All Link */}
<FeaturesSection /> <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>
)}
{/* Testimonials Section */} {/* Brand Story Section */}
<TestimonialsSection /> <section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
<div className="max-w-7xl mx-auto">
{/* Newsletter Section */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<NewsletterSection /> <div>
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
{/* Products Grid Section */} Our Story
{publishedProducts.length > 0 && ( </span>
<section className="py-20 px-6 bg-white"> <h2 className="text-3xl md:text-4xl font-medium mb-6">
<div className="max-w-[1400px] mx-auto"> Handmade with Love
<h2 className="font-serif italic text-4xl text-center mb-4"> </h2>
Our Collection <p className="text-[#666666] mb-6 leading-relaxed">
</h2> Every bottle of ManoonOils is crafted with care using traditional
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto"> methods passed down through generations. We source only the finest
Cold-pressed, pure, and natural oils for your daily beauty routine organic ingredients to bring you oils that nourish both hair and skin.
</p> </p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> <p className="text-[#666666] mb-8 leading-relaxed">
{publishedProducts.map((product, index) => ( Our commitment to purity means no additives, no preservatives -
<ProductCard key={product.id} product={product} index={index} /> 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>
</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>
{/* 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 /> <Footer />
</main> </>
); );
} }
// Import ProductCard here to avoid circular dependency
import ProductCard from "@/components/product/ProductCard";

View File

@@ -1,7 +1,7 @@
import Image from "next/image"; import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
import { getProductBySlug, getProducts, getProductPrice, getProductImage, getLocalizedProduct, formatPrice } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import ProductDetail from "@/components/product/ProductDetail";
import type { Product } from "@/types/saleor"; import type { Product } from "@/types/saleor";
interface ProductPageProps { interface ProductPageProps {
@@ -52,6 +52,12 @@ export async function generateMetadata({ params }: ProductPageProps) {
"en": product.translation?.slug ? `/products/${product.translation.slug}` : `/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',
},
}; };
} }
@@ -61,111 +67,57 @@ export default async function ProductPage({ params }: ProductPageProps) {
if (!product) { if (!product) {
return ( return (
<main className="min-h-screen"> <>
<Header /> <Header />
<div className="pt-24 text-center"> <main className="min-h-screen bg-white">
<h1 className="text-2xl"> <div className="pt-[180px] lg:pt-[200px] pb-20 text-center px-4">
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"} <h1 className="text-2xl font-medium mb-4">
</h1> {locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
</div> </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 /> <Footer />
</main> </>
); );
} }
const localized = getLocalizedProduct(product, locale.toUpperCase());
const image = getProductImage(product);
const price = getProductPrice(product);
const variant = product.variants?.[0];
const isAvailable = variant?.quantityAvailable > 0;
// Determine language based on which slug matched // Determine language based on which slug matched
const isEnglishSlug = slug === product.translation?.slug; const isEnglishSlug = slug === product.translation?.slug;
const currentLocale = isEnglishSlug ? "en" : "sr"; const currentLocale = isEnglishSlug ? "EN" : "SR";
// URLs for language switcher // Get related products (same category or just other products)
const serbianUrl = `/products/${product.slug}`; let relatedProducts: Product[] = [];
const englishUrl = product.translation?.slug try {
? `/products/${product.translation.slug}` const allProducts = await getProducts(currentLocale, 8);
: serbianUrl; relatedProducts = allProducts
.filter((p: Product) => p.id !== product.id)
.slice(0, 4);
} catch (e) {
// Ignore error, just won't show related products
}
return ( return (
<main className="min-h-screen"> <>
<Header /> <Header />
<main className="min-h-screen bg-white">
<section className="pt-24 pb-20 px-4"> <ProductDetail
<div className="max-w-7xl mx-auto"> product={product}
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> relatedProducts={relatedProducts}
{/* Product Image */} locale={currentLocale}
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden"> />
<Image </main>
src={image}
alt={localized.name}
fill
className="object-cover"
priority
/>
</div>
{/* Product Info */}
<div className="flex flex-col">
<h1 className="text-4xl font-serif mb-4">{localized.name}</h1>
{price && (
<div className="text-2xl mb-6">{price}</div>
)}
{localized.description && (
<div
className="prose max-w-none mb-8"
dangerouslySetInnerHTML={{ __html: localized.description }}
/>
)}
{/* Add to Cart Button */}
<button
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!isAvailable}
>
{isAvailable
? (currentLocale === "en" ? "Add to Cart" : "Dodaj u korpu")
: (currentLocale === "en" ? "Out of Stock" : "Nema na stanju")
}
</button>
{/* SKU */}
{variant?.sku && (
<p className="mt-4 text-sm text-foreground-muted">
SKU: {variant.sku}
</p>
)}
{/* Language Switcher */}
<div className="mt-8 pt-8 border-t">
<p className="text-sm text-foreground-muted mb-2">
{currentLocale === "en" ? "Language:" : "Jezik:"}
</p>
<div className="flex gap-4">
<a
href={serbianUrl}
className={`text-sm font-medium ${currentLocale === "sr" ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`}
>
🇷🇸 Srpski
</a>
<a
href={englishUrl}
className={`text-sm font-medium ${currentLocale === "en" ? "text-foreground" : "text-foreground-muted hover:text-foreground"}`}
>
🇬🇧 English
</a>
</div>
</div>
</div>
</div>
</div>
</section>
<Footer /> <Footer />
</main> </>
); );
} }

View File

@@ -2,6 +2,7 @@ import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react";
export const metadata = { export const metadata = {
title: "Products - ManoonOils", title: "Products - ManoonOils",
@@ -15,37 +16,91 @@ interface ProductsPageProps {
export default async function ProductsPage({ params }: ProductsPageProps) { export default async function ProductsPage({ params }: ProductsPageProps) {
const { locale = "sr" } = await params; const { locale = "sr" } = await params;
const products = await getProducts(locale.toUpperCase()); const products = await getProducts(locale.toUpperCase());
const localeUpper = locale.toUpperCase();
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<section className="py-20 px-4"> <main className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto"> {/* Page Header */}
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16"> <div className="pt-[72px] lg:pt-[72px]">
{locale === "en" ? "All Products" : "Svi Proizvodi"} <div className="border-b border-[#e5e5e5]">
</h1> <div className="container py-8 md:py-12">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
{products.length === 0 ? ( <div>
<p className="text-center text-foreground-muted"> <span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">
{locale === "en" ? "No products available" : "Nema dostupnih proizvoda"} {localeUpper === "EN" ? "Our Collection" : "Naša kolekcija"}
</p> </span>
) : ( <h1 className="text-3xl md:text-4xl font-medium">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> {localeUpper === "EN" ? "All Products" : "Svi Proizvodi"}
{products.map((product, index) => ( </h1>
<ProductCard </div>
key={product.id}
product={product} {/* Sort Dropdown */}
index={index} <div className="flex items-center gap-3">
locale={locale.toUpperCase()} <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>
)} </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> </div>
</section> </main>
<Footer /> <div className="pt-16">
</main> <Footer />
</div>
</>
); );
} }

View File

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

View File

@@ -4,6 +4,7 @@ import { useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { X, Minus, Plus, Trash2, ShoppingBag } from "lucide-react";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { formatPrice } from "@/lib/saleor"; import { formatPrice } from "@/lib/saleor";
@@ -32,13 +33,25 @@ export default function CartDrawer() {
initCheckout(); initCheckout();
}, [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 ( return (
<AnimatePresence> <AnimatePresence>
{isOpen && ( {isOpen && (
<> <>
{/* Backdrop */} {/* Backdrop */}
<motion.div <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 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
@@ -47,77 +60,99 @@ export default function CartDrawer() {
{/* Drawer */} {/* Drawer */}
<motion.div <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%" }} initial={{ x: "100%" }}
animate={{ x: 0 }} animate={{ x: 0 }}
exit={{ x: "100%" }} exit={{ x: "100%" }}
transition={{ type: "tween", duration: 0.3 }} transition={{ type: "tween", duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
> >
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border/30"> <div className="flex items-center justify-between px-6 py-5 border-b border-[#e5e5e5]">
<h2 className="text-xl font-serif"> <h2 className="text-sm uppercase tracking-[0.1em] font-medium">
Your Cart ({lineCount}) Your Cart ({lineCount})
</h2> </h2>
<button <button
onClick={closeCart} onClick={closeCart}
className="p-2" className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
aria-label="Close cart" aria-label="Close cart"
> >
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <X className="w-5 h-5" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
{/* Error Message */} {/* Error Message */}
{error && ( <AnimatePresence>
<div className="p-4 bg-red-50 border-b border-red-100"> {error && (
<p className="text-red-600 text-sm">{error}</p> <motion.div
<button initial={{ height: 0, opacity: 0 }}
onClick={clearError} animate={{ height: "auto", opacity: 1 }}
className="text-red-600 text-xs underline mt-1" exit={{ height: 0, opacity: 0 }}
className="overflow-hidden"
> >
Dismiss <div className="p-4 bg-red-50 border-b border-red-100">
</button> <p className="text-red-600 text-sm">{error}</p>
</div> <button
)} onClick={clearError}
className="text-red-600 text-xs underline mt-1 hover:no-underline"
>
Dismiss
</button>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Cart Items */} {/* Cart Items */}
<div className="flex-1 overflow-y-auto p-6"> <div className="flex-1 overflow-y-auto">
{lines.length === 0 ? ( {lines.length === 0 ? (
<div className="text-center py-12"> <div className="flex flex-col items-center justify-center h-full px-6">
<p className="text-foreground-muted mb-6">Your cart is empty</p> <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 <Link
href="/products" href="/products"
onClick={closeCart} 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> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="p-6 space-y-6">
{lines.map((line) => ( {lines.map((line) => (
<div key={line.id} className="flex gap-4"> <div key={line.id} className="flex gap-4">
{/* Product Image */} {/* Product Image */}
<div className="w-20 h-20 bg-background-ice relative flex-shrink-0"> <div className="w-24 h-24 bg-[#f8f9fa] relative flex-shrink-0 overflow-hidden">
{line.variant.product.media[0]?.url && ( {line.variant.product.media[0]?.url ? (
<Image <Image
src={line.variant.product.media[0].url} src={line.variant.product.media[0].url}
alt={line.variant.product.name} alt={line.variant.product.name}
fill fill
className="object-cover" 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>
{/* Product Info */} {/* Product Info */}
<div className="flex-1"> <div className="flex-1 min-w-0">
<h3 className="font-serif text-sm">{line.variant.product.name}</h3> <h3 className="text-sm font-medium truncate">
{line.variant.product.name}
</h3>
{line.variant.name !== "Default" && ( {line.variant.name !== "Default" && (
<p className="text-foreground-muted text-xs">{line.variant.name}</p> <p className="text-[#999999] text-xs mt-0.5">
{line.variant.name}
</p>
)} )}
<p className="text-foreground-muted text-sm mt-1"> <p className="text-[#666666] text-sm mt-2">
{formatPrice( {formatPrice(
line.variant.pricing?.price?.gross?.amount || 0, line.variant.pricing?.price?.gross?.amount || 0,
line.variant.pricing?.price?.gross?.currency line.variant.pricing?.price?.gross?.currency
@@ -125,30 +160,35 @@ export default function CartDrawer() {
</p> </p>
{/* Quantity Controls */} {/* Quantity Controls */}
<div className="flex items-center gap-3 mt-2"> <div className="flex items-center justify-between mt-3">
<button <div className="flex items-center border border-[#e5e5e5]">
onClick={() => updateLine(line.id, line.quantity - 1)} <button
disabled={isLoading} onClick={() => updateLine(line.id, line.quantity - 1)}
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50" disabled={isLoading || line.quantity <= 1}
> className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
- >
</button> <Minus className="w-3 h-3" />
<span>{line.quantity}</span> </button>
<button <span className="w-10 text-center text-sm font-medium">
onClick={() => updateLine(line.id, line.quantity + 1)} {line.quantity}
disabled={isLoading} </span>
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50" <button
> onClick={() => updateLine(line.id, line.quantity + 1)}
+ disabled={isLoading}
</button> 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 <button
onClick={() => removeLine(line.id)} onClick={() => removeLine(line.id)}
disabled={isLoading} disabled={isLoading}
className="ml-auto text-foreground-muted hover:text-red-500" className="p-2 text-[#999999] hover:text-red-500 transition-colors"
aria-label="Remove item"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Trash2 className="w-4 h-4" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button> </button>
</div> </div>
</div> </div>
@@ -160,46 +200,65 @@ export default function CartDrawer() {
{/* Footer with Checkout */} {/* Footer with Checkout */}
{lines.length > 0 && ( {lines.length > 0 && (
<div className="p-6 border-t border-border/30"> <div className="border-t border-[#e5e5e5] bg-white">
{/* Subtotal */} {/* Order Summary */}
<div className="flex items-center justify-between mb-2"> <div className="p-6 space-y-3">
<span className="text-foreground-muted">Subtotal</span> {/* Subtotal */}
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span> <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> </div>
{/* Shipping */} {/* Actions */}
<div className="flex items-center justify-between mb-4"> <div className="px-6 pb-6 space-y-3">
<span className="text-foreground-muted">Shipping</span> {/* Checkout Button */}
<span> <Link
{checkout?.shippingPrice?.gross?.amount href="/checkout"
? formatPrice(checkout.shippingPrice.gross.amount) onClick={closeCart}
: "Calculated at checkout" 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"
} >
</span> {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> </div>
{/* Total */}
<div className="flex items-center justify-between mb-4 pt-4 border-t border-border/30">
<span className="font-serif">Total</span>
<span className="font-serif text-lg">{formatPrice(total)}</span>
</div>
{/* Checkout Button */}
<Link
href="/checkout"
onClick={closeCart}
className="block w-full py-3 bg-foreground text-white text-center font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
>
{isLoading ? "Processing..." : "Checkout"}
</Link>
{/* Continue Shopping */}
<button
onClick={closeCart}
className="block w-full py-3 text-center text-foreground-muted hover:text-foreground mt-2"
>
Continue Shopping
</button>
</div> </div>
)} )}
</motion.div> </motion.div>

View File

@@ -0,0 +1,117 @@
"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/30 via-black/20 to-black/50" />
</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-black/40" />
</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"
>
{/* Tagline */}
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="inline-block text-xs md:text-sm uppercase tracking-[0.3em] mb-6 text-white/90"
>
Premium Organic Oils
</motion.span>
{/* Main Heading */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="text-5xl md:text-7xl lg:text-8xl font-medium mb-6 tracking-tight"
>
ManoonOils
</motion.h1>
{/* Subtitle */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.9 }}
className="text-lg md:text-xl text-white/80 mb-10 font-light max-w-xl mx-auto"
>
For hair and skin care
</motion.p>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1.1 }}
>
<Link
href="/products"
className="inline-block px-10 py-4 bg-white text-black text-[13px] uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors duration-300"
>
Shop Now
</Link>
</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/80 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

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

View File

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

View File

@@ -1,71 +1,161 @@
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; 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() { export default function Footer() {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
return ( return (
<footer className="bg-background-ice border-t border-border/30"> <footer className="bg-white border-t border-[#e5e5e5]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> {/* Main Footer */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-8"> <div className="container py-16 lg:py-20">
<div className="md:col-span-2"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
<Image {/* Brand Column */}
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" <div className="lg:col-span-4">
alt="ManoonOils" <Link href="/" className="inline-block mb-6">
width={180} <Image
height={48} src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
className="h-10 w-auto object-contain mb-4" alt="ManoonOils"
/> width={150}
<p className="text-foreground-muted max-w-md"> height={40}
Premium natural oils for hair and skin care. Crafted with love for your daily beauty routine. 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> </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>
<div> {/* Links Columns - All aligned at top */}
<h4 className="font-serif mb-4">Quick Links</h4> <div className="lg:col-span-8">
<ul className="space-y-2"> <div className="grid grid-cols-2 md:grid-cols-3 gap-8">
<li> {/* Shop */}
<Link href="/products" className="text-foreground-muted hover:text-foreground transition-colors"> <div className="flex flex-col">
Products <h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
</Link> Shop
</li> </h4>
<li> <ul className="space-y-3">
<Link href="/about" className="text-foreground-muted hover:text-foreground transition-colors"> {footerLinks.shop.map((link) => (
About Us <li key={link.label}>
</Link> <Link
</li> href={link.href}
<li> className="text-sm text-[#666666] hover:text-black transition-colors"
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors"> >
Contact {link.label}
</Link> </Link>
</li> </li>
</ul> ))}
</div> </ul>
</div>
<div> {/* About */}
<h4 className="font-serif mb-4">Customer Service</h4> <div className="flex flex-col">
<ul className="space-y-2"> <h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
<li> About
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors"> </h4>
Shipping Info <ul className="space-y-3">
</Link> {footerLinks.about.map((link) => (
</li> <li key={link.label}>
<li> <Link
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors"> href={link.href}
Returns className="text-sm text-[#666666] hover:text-black transition-colors"
</Link> >
</li> {link.label}
<li> </Link>
<a href="https://manoonoils.com" className="text-foreground-muted hover:text-foreground transition-colors"> </li>
WooCommerce Store ))}
</a> </ul>
</li> </div>
</ul>
{/* 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>
</div>
<div className="border-t border-border/30 mt-12 pt-8 text-center text-foreground-muted text-sm"> {/* Bottom Bar */}
<p>&copy; {currentYear} ManoonOils. All rights reserved.</p> <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>
</div> </div>
</footer> </footer>

View File

@@ -3,14 +3,20 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { AnimatePresence } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu } from "lucide-react"; import { User, ShoppingBag, Menu, X } from "lucide-react";
import MobileMenu from "./MobileMenu";
import CartDrawer from "@/components/cart/CartDrawer"; import CartDrawer from "@/components/cart/CartDrawer";
const navLinks = [
{ href: "/products", label: "Products" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
];
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore(); const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
const itemCount = getLineCount(); const itemCount = getLineCount();
@@ -20,81 +26,172 @@ export default function Header() {
initCheckout(); initCheckout();
}, [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 ( return (
<> <>
<header className="sticky top-10 z-40 bg-white border-b border-[#1A1A1A]/[0.06]"> <header
<div className="max-w-[1400px] mx-auto px-6"> className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
<div className="flex items-center justify-between h-16"> scrolled
{/* Mobile Menu Button */} ? "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 <button
className="lg:hidden p-2 -ml-2" className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
onClick={() => setMobileMenuOpen(true)} aria-label="Account"
aria-label="Open menu"
> >
<Menu className="w-5 h-5" /> <User className="w-5 h-5" strokeWidth={1.5} />
</button> </button>
{/* Logo */} <button
<Link href="/" className="flex-shrink-0"> className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
<Image onClick={toggleCart}
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" aria-label="Open cart"
alt="ManoonOils" >
width={150} <ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
height={40} {itemCount > 0 && (
className="h-8 w-auto object-contain" <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}
</Link> </span>
)}
{/* Desktop Navigation */} </button>
<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>
</div> </div>
</div> </div>
</header> </header>
{/* Mobile Menu Overlay */}
<AnimatePresence> <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> </AnimatePresence>
<CartDrawer /> <CartDrawer />

View File

@@ -26,31 +26,55 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
transition={{ duration: 0.5, delay: index * 0.1 }} transition={{ duration: 0.5, delay: index * 0.1 }}
> >
<Link href={`/products/${localized.slug}`} className="group block"> <Link href={`/products/${localized.slug}`} className="group block">
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden mb-4"> {/* Image Container */}
{image && ( <div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
{image ? (
<Image <Image
src={image} src={image}
alt={localized.name} alt={localized.name}
fill 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"
/> />
) : (
<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 && ( {!isAvailable && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center"> <div className="absolute inset-0 bg-white/80 flex items-center justify-center">
<span className="text-white font-medium"> <span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
{locale === "en" ? "Out of Stock" : "Nema na stanju"} {locale === "EN" ? "Out of Stock" : "Nema na stanju"}
</span> </span>
</div> </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> </div>
<h3 className="font-serif text-lg mb-1 group-hover:text-accent-dark transition-colors"> {/* Product Info */}
{localized.name} <div className="text-center">
</h3> <h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
{localized.name}
<p className="text-foreground-muted"> </h3>
{price || (locale === "en" ? "Contact for price" : "Kontaktirajte za cenu")}
</p> <p className="text-[14px] text-[#666666]">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
</p>
</div>
</Link> </Link>
</motion.div> </motion.div>
); );

View File

@@ -2,10 +2,12 @@
import { useState } from "react"; import { useState } from "react";
import Image from "next/image"; import Image from "next/image";
import { motion } from "framer-motion"; 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 type { Product } from "@/types/saleor";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor"; import { getProductPrice, getLocalizedProduct } from "@/lib/saleor";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
interface ProductDetailProps { interface ProductDetailProps {
@@ -14,6 +16,70 @@ interface ProductDetailProps {
locale?: string; locale?: string;
} }
// 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) { export default function ProductDetail({ product, relatedProducts, locale = "SR" }: ProductDetailProps) {
const [selectedImage, setSelectedImage] = useState(0); const [selectedImage, setSelectedImage] = useState(0);
const [quantity, setQuantity] = useState(1); const [quantity, setQuantity] = useState(1);
@@ -22,9 +88,11 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
const localized = getLocalizedProduct(product, locale); const localized = getLocalizedProduct(product, locale);
const variant = product.variants?.[0]; const variant = product.variants?.[0];
// Get all images from media
const images = product.media?.length > 0 const images = product.media?.length > 0
? product.media ? product.media.filter(m => m.type === "IMAGE")
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" }]; : [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" as const }];
const handleAddToCart = async () => { const handleAddToCart = async () => {
if (!variant?.id) return; if (!variant?.id) return;
@@ -38,45 +106,58 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
} }
}; };
const stripHtml = (html: string) => {
if (!html) return "";
return html.replace(/<[^>]*>/g, "");
};
const isAvailable = variant?.quantityAvailable > 0; const isAvailable = variant?.quantityAvailable > 0;
const price = getProductPrice(product); 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 ( return (
<> <>
<section className="py-12 md:py-20 px-4"> <section className="min-h-screen" id="product-detail">
<div className="max-w-7xl mx-auto"> {/* Breadcrumb - with proper top padding for fixed header */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12"> <div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
{/* Product Images */} <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 <motion.div
initial={{ opacity: 0, x: -20 }} initial={{ opacity: 0 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1 }}
transition={{ duration: 0.6 }} transition={{ duration: 0.6 }}
className="flex gap-4"
> >
<div className="relative aspect-square bg-background-ice mb-4"> {/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
{images[selectedImage] && (
<Image
src={images[selectedImage].url}
alt={images[selectedImage].alt || localized.name}
fill
className="object-cover"
priority
/>
)}
</div>
{images.length > 1 && ( {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) => ( {images.map((image, index) => (
<button <button
key={image.id} key={image.id}
onClick={() => setSelectedImage(index)} onClick={() => setSelectedImage(index)}
className={`relative w-20 h-20 flex-shrink-0 ${ className={`relative aspect-square w-full overflow-hidden border-2 transition-colors ${
selectedImage === index ? "ring-2 ring-foreground" : "" selectedImage === index
? "border-black"
: "border-transparent hover:border-[#999999]"
}`} }`}
> >
<Image <Image
@@ -84,103 +165,208 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
alt={image.alt || localized.name} alt={image.alt || localized.name}
fill fill
className="object-cover" className="object-cover"
sizes="80px"
/> />
</button> </button>
))} ))}
</div> </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> </motion.div>
{/* Product Info */} {/* Product Info - Right Side */}
<motion.div <motion.div
initial={{ opacity: 0, x: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }} transition={{ duration: 0.6, delay: 0.2 }}
className="lg:pl-8"
> >
<h1 className="text-3xl md:text-4xl font-serif mb-4"> {/* Product Name */}
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
{localized.name} {localized.name}
</h1> </h1>
<p className="text-2xl text-foreground-muted mb-6">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
</p>
{/* Short Description */} {/* Short Description */}
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted"> <p className="text-[#666666] leading-relaxed mb-6">
<p>{stripHtml(localized.description).slice(0, 200)}...</p> {shortDescription}
</p>
{/* Price & Rating */}
<div className="flex items-center justify-between mb-8">
<span className="text-3xl font-medium">
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
</span>
<StarRating rating={5} count={12} />
</div> </div>
{/* Add to Cart */} {/* Divider */}
{isAvailable ? ( <div className="border-t border-[#e5e5e5] mb-8" />
<div className="flex items-center gap-4 mb-8">
{/* Quantity Selector */} {/* Size Selector */}
<div className="flex items-center border border-border"> {product.variants && product.variants.length > 1 && (
<button <div className="mb-8">
onClick={() => setQuantity(Math.max(1, quantity - 1))} <div className="flex items-center justify-between mb-4">
className="px-4 py-3 hover:bg-gray-50" <span className="text-sm uppercase tracking-[0.1em] font-medium">
> {locale === "EN" ? "Size" : "Veličina"}
- </span>
</button>
<span className="px-4 py-3 min-w-[3rem] text-center">{quantity}</span>
<button
onClick={() => setQuantity(quantity + 1)}
className="px-4 py-3 hover:bg-gray-50"
>
+
</button>
</div> </div>
<div className="flex gap-3">
{/* Add to Cart Button */} {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>
</div>
)}
{/* 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 <button
onClick={handleAddToCart} onClick={() => setQuantity(Math.max(1, quantity - 1))}
disabled={isAdding} className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors disabled:opacity-50" disabled={quantity <= 1}
> >
{isAdding <Minus className="w-4 h-4" />
? (locale === "EN" ? "Adding..." : "Dodavanje...") </button>
: (locale === "EN" ? "Add to Cart" : "Dodaj u korpu") <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> </button>
</div> </div>
</div>
{/* Add to Cart Button */}
{isAvailable ? (
<button
onClick={handleAddToCart}
disabled={isAdding}
className="w-full h-16 bg-black text-white text-base uppercase tracking-[0.15em] font-medium hover:bg-[#333333] active:bg-[#1a1a1a] transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-8"
>
{isAdding
? (locale === "EN" ? "Adding..." : "Dodavanje...")
: (locale === "EN" ? "Add to Cart — Free Shipping" : "Dodaj u korpu — Besplatna dostava")
}
</button>
) : ( ) : (
<div className="py-3 bg-red-50 text-red-600 text-center mb-8"> <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"} {locale === "EN" ? "Out of Stock" : "Nema na stanju"}
</div> </div>
)} )}
{/* Free Shipping Note */}
<p className="text-center text-sm text-[#666666] mb-10">
{locale === "EN"
? "Free shipping on orders over 3,000 RSD"
: "Besplatna dostava za porudžbine preko 3.000 RSD"}
</p>
{/* 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 */} {/* SKU */}
{variant?.sku && ( {variant?.sku && (
<p className="text-sm text-foreground-muted mb-4"> <p className="text-xs text-[#999999] mt-8">
SKU: {variant.sku} SKU: {variant.sku}
</p> </p>
)} )}
{/* Full Description */}
{localized.description && (
<div className="border-t border-border/30 pt-6">
<h3 className="font-serif text-lg mb-4">
{locale === "EN" ? "Description" : "Opis"}
</h3>
<div
className="prose max-w-none text-foreground-muted"
dangerouslySetInnerHTML={{ __html: localized.description }}
/>
</div>
)}
</motion.div> </motion.div>
</div> </div>
</div> </div>
</section> </section>
{/* Related Products */} {/* Related Products */}
{relatedProducts.length > 0 && ( {relatedProducts && relatedProducts.length > 0 && (
<section className="py-12 px-4 bg-background-ice"> <section className="py-20 lg:py-28 bg-[#f8f9fa]">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl font-serif text-center mb-8"> <div className="text-center mb-16">
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"} <span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
</h2> {locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> </span>
{relatedProducts.map((relatedProduct, index) => ( <h2 className="text-3xl md:text-4xl font-medium">
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
{relatedProducts.filter(p => p && p.id).slice(0, 4).map((relatedProduct, index) => (
<ProductCard <ProductCard
key={relatedProduct.id} key={relatedProduct.id}
product={relatedProduct} product={relatedProduct}

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

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

View File

@@ -1,15 +1,26 @@
import { saleorClient } from "./client"; import { saleorClient } from "./client";
import { GET_PRODUCTS, GET_PRODUCT_BY_SLUG } from "./queries/Products"; 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"; 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( export async function getProducts(
locale: string = "SR", locale: string = "SR",
first: number = 100 first: number = 100
): Promise<Product[]> { ): Promise<Product[]> {
try { try {
const { data } = await saleorClient.query({ const { data } = await saleorClient.query<ProductsResponse>({
query: GET_PRODUCTS, query: GET_PRODUCTS,
variables: { variables: {
channel: CHANNEL, 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) { } catch (error) {
console.error("Error fetching products from Saleor:", error); console.error("Error fetching products from Saleor:", error);
return []; return [];
@@ -30,7 +41,7 @@ export async function getProductBySlug(
locale: string = "SR" locale: string = "SR"
): Promise<Product | null> { ): Promise<Product | null> {
try { try {
const { data } = await saleorClient.query({ const { data } = await saleorClient.query<ProductResponse>({
query: GET_PRODUCT_BY_SLUG, query: GET_PRODUCT_BY_SLUG,
variables: { variables: {
slug, slug,
@@ -81,6 +92,39 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
}).format(amount); }).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 // Get localized product data
export function getLocalizedProduct( export function getLocalizedProduct(
product: Product, product: Product,
@@ -94,11 +138,13 @@ export function getLocalizedProduct(
} { } {
const isEnglish = locale.toLowerCase() === "en"; const isEnglish = locale.toLowerCase() === "en";
const translation = isEnglish ? product.translation : null; const translation = isEnglish ? product.translation : null;
const rawDescription = translation?.description || product.description;
return { return {
name: translation?.name || product.name, name: translation?.name || product.name,
slug: translation?.slug || product.slug, slug: translation?.slug || product.slug,
description: translation?.description || product.description, description: parseDescription(rawDescription),
seoTitle: translation?.seoTitle || product.seoTitle, seoTitle: translation?.seoTitle || product.seoTitle,
seoDescription: translation?.seoDescription || product.seoDescription, seoDescription: translation?.seoDescription || product.seoDescription,
}; };

View File

@@ -18,4 +18,3 @@ export const GET_CHECKOUT_BY_ID = gql`
} }
${CHECKOUT_FRAGMENT} ${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

@@ -15,6 +15,46 @@ import type { Checkout, CheckoutLine } from "@/types/saleor";
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel"; 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 { interface SaleorCheckoutStore {
checkout: Checkout | null; checkout: Checkout | null;
checkoutToken: string | null; checkoutToken: string | null;
@@ -55,7 +95,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
if (checkoutToken) { if (checkoutToken) {
// Try to fetch existing checkout // Try to fetch existing checkout
try { try {
const { data } = await saleorClient.query({ const { data } = await saleorClient.query<GetCheckoutResponse>({
query: GET_CHECKOUT, query: GET_CHECKOUT,
variables: { token: checkoutToken }, variables: { token: checkoutToken },
}); });
@@ -71,7 +111,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
// Create new checkout // Create new checkout
try { try {
const { data } = await saleorClient.mutate({ const { data } = await saleorClient.mutate<CheckoutCreateResponse>({
mutation: CHECKOUT_CREATE, mutation: CHECKOUT_CREATE,
variables: { variables: {
input: { input: {
@@ -109,7 +149,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
throw new Error("Failed to initialize checkout"); throw new Error("Failed to initialize checkout");
} }
const { data } = await saleorClient.mutate({ const { data } = await saleorClient.mutate<CheckoutLinesAddResponse>({
mutation: CHECKOUT_LINES_ADD, mutation: CHECKOUT_LINES_ADD,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -123,7 +163,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
isOpen: true, isOpen: true,
isLoading: false, isLoading: false,
}); });
} else if (data?.checkoutLinesAdd?.errors?.length > 0) { } else if (data?.checkoutLinesAdd?.errors && data.checkoutLinesAdd.errors.length > 0) {
throw new Error(data.checkoutLinesAdd.errors[0].message); throw new Error(data.checkoutLinesAdd.errors[0].message);
} }
} catch (e: any) { } catch (e: any) {
@@ -147,7 +187,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
return; return;
} }
const { data } = await saleorClient.mutate({ const { data } = await saleorClient.mutate<CheckoutLinesUpdateResponse>({
mutation: CHECKOUT_LINES_UPDATE, mutation: CHECKOUT_LINES_UPDATE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -160,7 +200,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
checkout: data.checkoutLinesUpdate.checkout, checkout: data.checkoutLinesUpdate.checkout,
isLoading: false, isLoading: false,
}); });
} else if (data?.checkoutLinesUpdate?.errors?.length > 0) { } else if (data?.checkoutLinesUpdate?.errors && data.checkoutLinesUpdate.errors.length > 0) {
throw new Error(data.checkoutLinesUpdate.errors[0].message); throw new Error(data.checkoutLinesUpdate.errors[0].message);
} }
} catch (e: any) { } catch (e: any) {
@@ -178,7 +218,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
throw new Error("No active checkout"); throw new Error("No active checkout");
} }
const { data } = await saleorClient.mutate({ const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
mutation: CHECKOUT_LINES_DELETE, mutation: CHECKOUT_LINES_DELETE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -191,7 +231,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
checkout: data.checkoutLinesDelete.checkout, checkout: data.checkoutLinesDelete.checkout,
isLoading: false, isLoading: false,
}); });
} else if (data?.checkoutLinesDelete?.errors?.length > 0) { } else if (data?.checkoutLinesDelete?.errors && data.checkoutLinesDelete.errors.length > 0) {
throw new Error(data.checkoutLinesDelete.errors[0].message); throw new Error(data.checkoutLinesDelete.errors[0].message);
} }
} catch (e: any) { } catch (e: any) {
@@ -209,7 +249,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
throw new Error("No active checkout"); throw new Error("No active checkout");
} }
const { data } = await saleorClient.mutate({ const { data } = await saleorClient.mutate<CheckoutEmailUpdateResponse>({
mutation: CHECKOUT_EMAIL_UPDATE, mutation: CHECKOUT_EMAIL_UPDATE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -222,7 +262,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
checkout: data.checkoutEmailUpdate.checkout, checkout: data.checkoutEmailUpdate.checkout,
isLoading: false, isLoading: false,
}); });
} else if (data?.checkoutEmailUpdate?.errors?.length > 0) { } else if (data?.checkoutEmailUpdate?.errors && data.checkoutEmailUpdate.errors.length > 0) {
throw new Error(data.checkoutEmailUpdate.errors[0].message); throw new Error(data.checkoutEmailUpdate.errors[0].message);
} }
} catch (e: any) { } catch (e: any) {
@@ -236,7 +276,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
if (!checkoutToken) return; if (!checkoutToken) return;
try { try {
const { data } = await saleorClient.query({ const { data } = await saleorClient.query<GetCheckoutResponse>({
query: GET_CHECKOUT, query: GET_CHECKOUT,
variables: { token: checkoutToken }, variables: { token: checkoutToken },
}); });