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
This commit is contained in:
444
REDESIGN_SPECIFICATION.md
Normal file
444
REDESIGN_SPECIFICATION.md
Normal 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
|
||||||
@@ -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">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-medium tracking-tight">
|
||||||
|
About ManoonOils
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6">
|
{/* Hero Image */}
|
||||||
<p>
|
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
||||||
|
alt="Natural oils production"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/20" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<section className="py-16 md:py-24">
|
||||||
|
<div className="container">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
{/* Introduction */}
|
||||||
|
<div className="mb-16">
|
||||||
|
<p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
|
||||||
ManoonOils was born from a passion for natural beauty and the belief
|
ManoonOils was born from a passion for natural beauty and the belief
|
||||||
that the best skincare comes from nature itself. Our journey began with
|
that the best skincare comes from nature itself.
|
||||||
a simple question: how can we create products that truly nurture both
|
|
||||||
hair and skin?
|
|
||||||
</p>
|
</p>
|
||||||
|
<p className="text-[#666666] leading-relaxed">
|
||||||
<p>
|
|
||||||
We believe in the power of natural ingredients. Every oil in our
|
We believe in the power of natural ingredients. Every oil in our
|
||||||
collection is carefully selected for its unique properties and
|
collection is carefully selected for its unique properties and
|
||||||
benefits. From nourishing oils that restore hair vitality to serums
|
benefits. From nourishing oils that restore hair vitality to serums
|
||||||
that rejuvenate skin, we craft each product with love and attention
|
that rejuvenate skin, we craft each product with love and attention
|
||||||
to detail.
|
to detail.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
{/* Values Grid */}
|
||||||
Our Mission
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
|
||||||
</h2>
|
<div className="p-6 bg-[#f8f9fa]">
|
||||||
<p>
|
<h3 className="text-lg font-medium mb-3">Natural Ingredients</h3>
|
||||||
Our mission is to provide premium quality, natural products that
|
<p className="text-[#666666] text-sm leading-relaxed">
|
||||||
enhance your daily beauty routine. We are committed to:
|
We use only the finest natural ingredients, sourced ethically and sustainably
|
||||||
|
from trusted suppliers around the world.
|
||||||
</p>
|
</p>
|
||||||
<ul className="list-disc pl-6 space-y-2">
|
</div>
|
||||||
<li>Using only the finest natural ingredients</li>
|
<div className="p-6 bg-[#f8f9fa]">
|
||||||
<li>Cruelty-free and ethical production</li>
|
<h3 className="text-lg font-medium mb-3">Cruelty-Free</h3>
|
||||||
<li>Sustainable packaging practices</li>
|
<p className="text-[#666666] text-sm leading-relaxed">
|
||||||
<li>Transparency in our formulations</li>
|
Our products are never tested on animals. We believe in beauty
|
||||||
</ul>
|
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>
|
||||||
|
|
||||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
{/* Mission */}
|
||||||
Handmade with Love
|
<div className="text-center py-12 border-t border-b border-[#e5e5e5]">
|
||||||
</h2>
|
<span className="text-caption text-[#666666] mb-4 block">Our Mission</span>
|
||||||
<p>
|
<blockquote className="text-2xl md:text-3xl font-medium tracking-tight">
|
||||||
|
“To provide premium quality, natural products that enhance
|
||||||
|
your daily beauty routine.”
|
||||||
|
</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
|
Every bottle of ManoonOils is handcrafted with care. We small-batch
|
||||||
produce our products to ensure the highest quality and freshness.
|
produce our products to ensure the highest quality and freshness.
|
||||||
When you use ManoonOils, you can feel confident that you're using
|
When you use ManoonOils, you can feel confident that you're using
|
||||||
something made with genuine care and expertise.
|
something made with genuine care and expertise.
|
||||||
</p>
|
</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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,29 @@ import {
|
|||||||
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||||
CHECKOUT_COMPLETE,
|
CHECKOUT_COMPLETE,
|
||||||
} from "@/lib/saleor/mutations/Checkout";
|
} 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 {
|
interface AddressForm {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@@ -94,7 +117,7 @@ export default function CheckoutPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Update shipping address
|
// Update shipping address
|
||||||
const shippingResult = await saleorClient.mutate({
|
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
|
||||||
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||||
variables: {
|
variables: {
|
||||||
checkoutId: checkout.id,
|
checkoutId: checkout.id,
|
||||||
@@ -105,12 +128,12 @@ export default function CheckoutPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors?.length > 0) {
|
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
|
||||||
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
|
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update billing address
|
// Update billing address
|
||||||
const billingResult = await saleorClient.mutate({
|
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
|
||||||
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||||
variables: {
|
variables: {
|
||||||
checkoutId: checkout.id,
|
checkoutId: checkout.id,
|
||||||
@@ -121,19 +144,19 @@ export default function CheckoutPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (billingResult.data?.checkoutBillingAddressUpdate?.errors?.length > 0) {
|
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
|
||||||
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
|
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete checkout (creates order)
|
// Complete checkout (creates order)
|
||||||
const completeResult = await saleorClient.mutate({
|
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
|
||||||
mutation: CHECKOUT_COMPLETE,
|
mutation: CHECKOUT_COMPLETE,
|
||||||
variables: {
|
variables: {
|
||||||
checkoutId: checkout.id,
|
checkoutId: checkout.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (completeResult.data?.checkoutComplete?.errors?.length > 0) {
|
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
|
||||||
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
|
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,9 +177,10 @@ export default function CheckoutPage() {
|
|||||||
// Order Success Page
|
// Order Success Page
|
||||||
if (orderComplete) {
|
if (orderComplete) {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen">
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<section className="pt-24 pb-20 px-4">
|
<main className="min-h-screen">
|
||||||
|
<section className="pt-[120px] pb-20 px-4">
|
||||||
<div className="max-w-2xl mx-auto text-center">
|
<div className="max-w-2xl mx-auto text-center">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
@@ -187,16 +211,19 @@ export default function CheckoutPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen">
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
|
<main className="min-h-screen">
|
||||||
<section className="pt-24 pb-20 px-4">
|
<section className="pt-[120px] pb-20 px-4">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
|
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
|
||||||
|
|
||||||
@@ -457,8 +484,11 @@ export default function CheckoutPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +19,83 @@ 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">
|
||||||
|
<div className="max-w-2xl mx-auto text-center">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Get in Touch</span>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-medium tracking-tight mb-4">
|
||||||
Contact Us
|
Contact Us
|
||||||
</h1>
|
</h1>
|
||||||
|
<p className="text-[#666666]">
|
||||||
|
Have questions? We'd love to hear from you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="text-foreground-muted text-center mb-12">
|
{/* Contact Section */}
|
||||||
Have questions? We'd love to hear from you.
|
<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're here to help! Whether you have questions about our products,
|
||||||
|
need assistance with an order, or just want to say hello, we'd love to hear from you.
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<div className="bg-[#f8f9fa] p-8 md:p-10">
|
||||||
{submitted ? (
|
{submitted ? (
|
||||||
<div className="bg-green-50 text-green-700 p-6 text-center">
|
<div className="text-center py-12">
|
||||||
<p className="text-lg">Thank you for your message!</p>
|
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||||
<p className="mt-2">We'll get back to you soon.</p>
|
<Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-medium mb-2">Thank You!</h3>
|
||||||
|
<p className="text-[#666666]">
|
||||||
|
Your message has been sent. We'll get back to you soon.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
@@ -48,7 +109,8 @@ export default function ContactPage() {
|
|||||||
required
|
required
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
|
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||||
|
placeholder="Your name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,7 +124,8 @@ export default function ContactPage() {
|
|||||||
required
|
required
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => setFormData({ ...formData, email: 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"
|
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>
|
||||||
|
|
||||||
@@ -76,39 +139,65 @@ export default function ContactPage() {
|
|||||||
rows={5}
|
rows={5}
|
||||||
value={formData.message}
|
value={formData.message}
|
||||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none"
|
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
|
||||||
|
placeholder="How can we help you?"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||||
>
|
>
|
||||||
Send Message
|
Send Message
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-16 pt-8 border-t border-border/30">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-serif mb-2">Email</h3>
|
|
||||||
<p className="text-foreground-muted">hello@manoonoils.com</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-serif mb-2">Shipping</h3>
|
|
||||||
<p className="text-foreground-muted">Free over 3000 RSD</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-serif mb-2">Location</h3>
|
|
||||||
<p className="text-foreground-muted">Serbia</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Footer />
|
{/* 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>
|
</main>
|
||||||
|
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 />
|
|
||||||
<div className="pt-10">
|
|
||||||
<Header />
|
<Header />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Hero Section */}
|
<main className="min-h-screen bg-white">
|
||||||
<NewHero featuredProduct={featuredProduct} />
|
{/* Hero Section with Video Background */}
|
||||||
|
<HeroVideo />
|
||||||
{/* Stats & Philosophy Section */}
|
|
||||||
<StatsSection />
|
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<FeaturesSection />
|
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
|
||||||
<TestimonialsSection />
|
|
||||||
|
|
||||||
{/* Newsletter Section */}
|
|
||||||
<NewsletterSection />
|
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div id="main-content">
|
||||||
{/* Products Grid Section */}
|
{/* Products Grid Section */}
|
||||||
{publishedProducts.length > 0 && (
|
{featuredProducts.length > 0 && (
|
||||||
<section className="py-20 px-6 bg-white">
|
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="max-w-[1400px] mx-auto">
|
<div className="container">
|
||||||
<h2 className="font-serif italic text-4xl text-center mb-4">
|
{/* Section Header */}
|
||||||
Our Collection
|
<div className="text-center mb-16">
|
||||||
</h2>
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Collection</span>
|
||||||
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
|
<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
|
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||||
</p>
|
</p>
|
||||||
<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} />
|
{/* Products Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||||
|
{featuredProducts.map((product, index) => (
|
||||||
|
<ProductCard key={product.id} product={product} index={index} locale="EN" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* View All Link */}
|
||||||
|
<div className="text-center mt-12">
|
||||||
|
<a
|
||||||
|
href="/en/products"
|
||||||
|
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||||
|
>
|
||||||
|
View All Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Footer />
|
{/* Brand Story Section */}
|
||||||
|
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||||
|
<div className="container">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-medium mb-6">Handmade with Love</h2>
|
||||||
|
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||||
|
Every bottle of ManoonOils is crafted with care using traditional
|
||||||
|
methods passed down through generations. We source only the finest
|
||||||
|
organic ingredients to bring you oils that nourish both hair and skin.
|
||||||
|
</p>
|
||||||
|
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||||
|
Our commitment to purity means no additives, no preservatives -
|
||||||
|
just nature's goodness in its most potent form.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/en/about"
|
||||||
|
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||||
|
>
|
||||||
|
Learn More
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
||||||
|
alt="Natural oils production"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Benefits Section */}
|
||||||
|
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
|
<div className="container">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Why Choose Us</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-medium">The Manoon Difference</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
title: "100% Natural",
|
||||||
|
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Handcrafted",
|
||||||
|
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Sustainable",
|
||||||
|
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
|
||||||
|
},
|
||||||
|
].map((benefit, index) => (
|
||||||
|
<div key={index} className="text-center">
|
||||||
|
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
|
||||||
|
<span className="text-2xl font-medium text-[#1a1a1a]">
|
||||||
|
{String(index + 1).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
|
||||||
|
<p className="text-[#666666]">{benefit.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Newsletter Section */}
|
||||||
|
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white mb-16">
|
||||||
|
<div className="container">
|
||||||
|
<div className="max-w-2xl mx-auto text-center">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">Stay Connected</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">Join Our Community</h2>
|
||||||
|
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||||
|
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<form className="inline-flex flex-col sm:flex-row">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
className="w-64 sm:w-80 px-5 h-14 bg-white/10 border border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import ProductCard here to avoid circular dependency
|
|
||||||
import ProductCard from "@/components/product/ProductCard";
|
|
||||||
|
|||||||
@@ -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
|
}
|
||||||
export const generateStaticParams = undefined;
|
|
||||||
|
|
||||||
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
||||||
const { slug } = await params;
|
|
||||||
let product = null;
|
|
||||||
|
|
||||||
|
// Generate static params for all products
|
||||||
|
export async function generateStaticParams() {
|
||||||
try {
|
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 }> = [];
|
||||||
} catch (e) {
|
|
||||||
// Fallback
|
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) {
|
||||||
|
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're looking for doesn'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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">Our Collection</span>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-medium">All Products</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
{publishedProducts.length === 0 ? (
|
{/* Sort Dropdown */}
|
||||||
<p className="text-center text-foreground-muted">No products available</p>
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-[#666666]">{products.length} products</span>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||||
|
defaultValue="featured"
|
||||||
|
>
|
||||||
|
<option value="featured">Featured</option>
|
||||||
|
<option value="newest">Newest</option>
|
||||||
|
<option value="price-low">Price: Low to High</option>
|
||||||
|
<option value="price-high">Price: High to Low</option>
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products Grid */}
|
||||||
|
<section className="py-12 md:py-16">
|
||||||
|
<div className="container">
|
||||||
|
{products.length === 0 ? (
|
||||||
|
<div className="text-center py-20">
|
||||||
|
<p className="text-[#666666] mb-4">No products available</p>
|
||||||
|
<p className="text-sm text-[#999999]">Please check back later for new arrivals.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||||
{publishedProducts.map((product, index) => (
|
{products.map((product, index) => (
|
||||||
<ProductCard key={product.id} product={product} index={index} />
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
index={index}
|
||||||
|
locale="EN"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,87 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
MANOONOILS DESIGN SYSTEM
|
||||||
|
Inspired by premium skincare brands
|
||||||
|
============================================ */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #f0f4f8;
|
/* Primary Colors */
|
||||||
--background-ice: #e8f0f5;
|
--color-white: #ffffff;
|
||||||
--foreground: #1a1a1a;
|
--color-background: #fafafa;
|
||||||
--foreground-muted: #666666;
|
--color-background-alt: #f5f5f5;
|
||||||
--accent: #a8c5d8;
|
--color-foreground: #1a1a1a;
|
||||||
--accent-dark: #7ba3bc;
|
--color-foreground-muted: #666666;
|
||||||
--white: #ffffff;
|
--color-foreground-subtle: #999999;
|
||||||
--border: #d1d9e0;
|
|
||||||
|
/* Accent Colors */
|
||||||
|
--color-accent: #e8f0f5;
|
||||||
|
--color-accent-dark: #a8c5d8;
|
||||||
|
--color-accent-blue: #e8f0f5;
|
||||||
|
--color-gold: #c9a962;
|
||||||
|
--color-gold-light: #d4b978;
|
||||||
|
|
||||||
|
/* UI Colors */
|
||||||
|
--color-border: #e5e5e5;
|
||||||
|
--color-border-dark: #d1d1d1;
|
||||||
|
--color-cta: #000000;
|
||||||
|
--color-cta-hover: #333333;
|
||||||
|
--color-overlay: rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--space-xs: 4px;
|
||||||
|
--space-sm: 8px;
|
||||||
|
--space-md: 16px;
|
||||||
|
--space-lg: 24px;
|
||||||
|
--space-xl: 32px;
|
||||||
|
--space-2xl: 48px;
|
||||||
|
--space-3xl: 64px;
|
||||||
|
--space-4xl: 96px;
|
||||||
|
--space-5xl: 128px;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-display: 'DM Sans', sans-serif;
|
||||||
|
--font-body: 'Inter', sans-serif;
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 150ms ease;
|
||||||
|
--transition-base: 250ms ease;
|
||||||
|
--transition-slow: 350ms ease;
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
/* Colors - reference :root 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';
|
FONT IMPORTS
|
||||||
src: url('https://fonts.gstatic.com/s/cedratdisplay/v16/0nkoC9_pK3CvS5lZuZ7MAUmK5w.woff2') format('woff2');
|
============================================ */
|
||||||
font-weight: 400 900;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'DM Sans';
|
font-family: 'DM Sans';
|
||||||
@@ -38,20 +90,284 @@
|
|||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2') format('woff2');
|
||||||
|
font-weight: 400 700;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
BASE STYLES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--color-background);
|
||||||
color: var(--foreground);
|
color: var(--color-foreground);
|
||||||
font-family: 'DM Sans', sans-serif;
|
font-family: var(--font-body);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
TYPOGRAPHY
|
||||||
|
============================================ */
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: 'Cedrat Display', serif;
|
font-family: var(--font-display);
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: clamp(1.25rem, 3vw, 1.75rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-display {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-body {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-uppercase {
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-caption {
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--color-foreground-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-subtle {
|
||||||
|
color: var(--color-foreground-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
UTILITY CLASSES
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section spacing */
|
||||||
|
.section {
|
||||||
|
padding-top: var(--space-4xl);
|
||||||
|
padding-bottom: var(--space-4xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-sm {
|
||||||
|
padding-top: var(--space-2xl);
|
||||||
|
padding-bottom: var(--space-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Flex utilities */
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
INTERACTIVE ELEMENTS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Button Base */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 14px 32px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 animation */
|
||||||
|
.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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
FORM ELEMENTS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
input, textarea, select {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SCROLLBAR
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--color-border-dark);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: var(--color-foreground-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ANIMATIONS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn var(--transition-slow) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp var(--transition-slow) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-in-right {
|
||||||
|
animation: slideInRight var(--transition-slow) forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Marquee Animations */
|
/* Marquee Animations */
|
||||||
@@ -64,51 +380,48 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes marquee-slow {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-marquee {
|
.animate-marquee {
|
||||||
animation: marquee 25s linear infinite;
|
animation: marquee 25s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-marquee-slow {
|
.animate-marquee-slow {
|
||||||
animation: marquee-slow 35s linear infinite;
|
animation: marquee 35s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-marquee-fast {
|
/* ============================================
|
||||||
animation: marquee 15s linear infinite;
|
ACCESSIBILITY
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Focus visible styles */
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid var(--color-foreground);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Utility Classes */
|
/* Reduced motion */
|
||||||
.font-serif {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
font-family: 'Cedrat Display', serif;
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth scroll */
|
/* Screen reader only */
|
||||||
html {
|
.sr-only {
|
||||||
scroll-behavior: smooth;
|
position: absolute;
|
||||||
}
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
/* Custom scrollbar */
|
padding: 0;
|
||||||
::-webkit-scrollbar {
|
margin: -1px;
|
||||||
width: 8px;
|
overflow: hidden;
|
||||||
}
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
::-webkit-scrollbar-track {
|
border-width: 0;
|
||||||
background: #f1f1f1;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #c1c1c1;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #a8a8a8;
|
|
||||||
}
|
}
|
||||||
|
|||||||
181
src/app/page.tsx
181
src/app/page.tsx
@@ -1,12 +1,7 @@
|
|||||||
import { getProducts } from "@/lib/saleor";
|
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 StatsSection from "@/components/home/StatsSection";
|
|
||||||
import FeaturesSection from "@/components/home/FeaturesSection";
|
|
||||||
import TestimonialsSection from "@/components/home/TestimonialsSection";
|
|
||||||
import NewsletterSection from "@/components/home/NewsletterSection";
|
|
||||||
import ProductCard from "@/components/product/ProductCard";
|
import ProductCard from "@/components/product/ProductCard";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
@@ -24,51 +19,169 @@ export default async function Homepage() {
|
|||||||
console.log('Failed to fetch products during build');
|
console.log('Failed to fetch products during build');
|
||||||
}
|
}
|
||||||
|
|
||||||
const featuredProduct = products[0];
|
const featuredProducts = products?.slice(0, 4) || [];
|
||||||
const publishedProducts = products.slice(0, 4);
|
const hasProducts = featuredProducts.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-white">
|
<>
|
||||||
<AnnouncementBar />
|
|
||||||
<div className="pt-10">
|
|
||||||
<Header />
|
<Header />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Hero Section */}
|
<main className="min-h-screen bg-white">
|
||||||
<NewHero featuredProduct={featuredProduct} />
|
{/* Hero Section with Video Background */}
|
||||||
|
<HeroVideo />
|
||||||
{/* Stats & Philosophy Section */}
|
|
||||||
<StatsSection />
|
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<FeaturesSection />
|
|
||||||
|
|
||||||
{/* Testimonials Section */}
|
|
||||||
<TestimonialsSection />
|
|
||||||
|
|
||||||
{/* Newsletter Section */}
|
|
||||||
<NewsletterSection />
|
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
|
||||||
{/* Products Grid Section */}
|
{/* Products Grid Section */}
|
||||||
{publishedProducts.length > 0 && (
|
{hasProducts && (
|
||||||
<section className="py-20 px-6 bg-white">
|
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||||
<div className="max-w-[1400px] mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<h2 className="font-serif italic text-4xl text-center mb-4">
|
{/* Section Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||||
Our Collection
|
Our Collection
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||||
|
Premium Natural Oils
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
|
<p className="text-[#666666] max-w-xl mx-auto">
|
||||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
</div>
|
||||||
{publishedProducts.map((product, index) => (
|
|
||||||
|
{/* Products Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||||
|
{featuredProducts.map((product, index) => (
|
||||||
<ProductCard key={product.id} product={product} index={index} locale="SR" />
|
<ProductCard key={product.id} product={product} index={index} locale="SR" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* View All Link */}
|
||||||
|
<div className="text-center mt-12">
|
||||||
|
<a
|
||||||
|
href="/products"
|
||||||
|
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||||
|
>
|
||||||
|
View All Products
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Footer />
|
{/* Brand Story Section */}
|
||||||
|
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||||
|
Our Story
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-medium mb-6">
|
||||||
|
Handmade with Love
|
||||||
|
</h2>
|
||||||
|
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||||
|
Every bottle of ManoonOils is crafted with care using traditional
|
||||||
|
methods passed down through generations. We source only the finest
|
||||||
|
organic ingredients to bring you oils that nourish both hair and skin.
|
||||||
|
</p>
|
||||||
|
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||||
|
Our commitment to purity means no additives, no preservatives -
|
||||||
|
just nature'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>
|
||||||
|
</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-center justify-center w-full sm:w-auto gap-0">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email"
|
||||||
|
className="w-full sm:w-64 md:w-80 px-5 h-14 bg-white/10 border border-white/20 border-b-0 sm:border-b border-r-0 sm:border-r border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full sm:w-auto px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap flex-shrink-0"
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
<h1 className="text-2xl font-medium mb-4">
|
||||||
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
||||||
</h1>
|
</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>
|
</div>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
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 />
|
|
||||||
</main>
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-[140px] lg:pt-[160px]">
|
||||||
{locale === "en" ? "All Products" : "Svi Proizvodi"}
|
<div className="border-b border-[#e5e5e5]">
|
||||||
|
<div className="container py-8 md:py-12">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">
|
||||||
|
{localeUpper === "EN" ? "Our Collection" : "Naša kolekcija"}
|
||||||
|
</span>
|
||||||
|
<h1 className="text-3xl md:text-4xl font-medium">
|
||||||
|
{localeUpper === "EN" ? "All Products" : "Svi Proizvodi"}
|
||||||
</h1>
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sort Dropdown */}
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-[#666666]">
|
||||||
|
{products.length} {localeUpper === "EN" ? "products" : "proizvoda"}
|
||||||
|
</span>
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||||
|
defaultValue="featured"
|
||||||
|
>
|
||||||
|
<option value="featured">
|
||||||
|
{localeUpper === "EN" ? "Featured" : "Istaknuto"}
|
||||||
|
</option>
|
||||||
|
<option value="newest">
|
||||||
|
{localeUpper === "EN" ? "Newest" : "Najnovije"}
|
||||||
|
</option>
|
||||||
|
<option value="price-low">
|
||||||
|
{localeUpper === "EN" ? "Price: Low to High" : "Cena: Rastuće"}
|
||||||
|
</option>
|
||||||
|
<option value="price-high">
|
||||||
|
{localeUpper === "EN" ? "Price: High to Low" : "Cena: Opadajuće"}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Products Grid */}
|
||||||
|
<section className="py-12 md:py-16">
|
||||||
|
<div className="container">
|
||||||
{products.length === 0 ? (
|
{products.length === 0 ? (
|
||||||
<p className="text-center text-foreground-muted">
|
<div className="text-center py-20">
|
||||||
{locale === "en" ? "No products available" : "Nema dostupnih proizvoda"}
|
<p className="text-[#666666] mb-4">
|
||||||
|
{localeUpper === "EN" ? "No products available" : "Nema dostupnih proizvoda"}
|
||||||
</p>
|
</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-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
|
<ProductCard
|
||||||
key={product.id}
|
key={product.id}
|
||||||
product={product}
|
product={product}
|
||||||
index={index}
|
index={index}
|
||||||
locale={locale.toUpperCase()}
|
locale={localeUpper}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div className="pt-16">
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
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")
|
|
||||||
.map((product) => ({
|
|
||||||
url: `${baseUrl}/products/${product.slug}`,
|
url: `${baseUrl}/products/${product.slug}`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "weekly" as const,
|
changeFrequency: "weekly" as const,
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
<AnimatePresence>
|
||||||
{error && (
|
{error && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0, opacity: 0 }}
|
||||||
|
animate={{ height: "auto", opacity: 1 }}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
className="overflow-hidden"
|
||||||
|
>
|
||||||
<div className="p-4 bg-red-50 border-b border-red-100">
|
<div className="p-4 bg-red-50 border-b border-red-100">
|
||||||
<p className="text-red-600 text-sm">{error}</p>
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={clearError}
|
onClick={clearError}
|
||||||
className="text-red-600 text-xs underline mt-1"
|
className="text-red-600 text-xs underline mt-1 hover:no-underline"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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'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">
|
||||||
|
<div className="flex items-center border border-[#e5e5e5]">
|
||||||
<button
|
<button
|
||||||
onClick={() => updateLine(line.id, line.quantity - 1)}
|
onClick={() => updateLine(line.id, line.quantity - 1)}
|
||||||
disabled={isLoading}
|
disabled={isLoading || line.quantity <= 1}
|
||||||
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50"
|
className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
-
|
<Minus className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
<span>{line.quantity}</span>
|
<span className="w-10 text-center text-sm font-medium">
|
||||||
|
{line.quantity}
|
||||||
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateLine(line.id, line.quantity + 1)}
|
onClick={() => updateLine(line.id, line.quantity + 1)}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50"
|
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>
|
</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,17 +200,21 @@ 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">
|
||||||
|
{/* Order Summary */}
|
||||||
|
<div className="p-6 space-y-3">
|
||||||
{/* Subtotal */}
|
{/* Subtotal */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-foreground-muted">Subtotal</span>
|
<span className="text-[#666666]">Subtotal</span>
|
||||||
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
<span className="font-medium">
|
||||||
|
{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Shipping */}
|
{/* Shipping */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between text-sm">
|
||||||
<span className="text-foreground-muted">Shipping</span>
|
<span className="text-[#666666]">Shipping</span>
|
||||||
<span>
|
<span className="text-[#666666]">
|
||||||
{checkout?.shippingPrice?.gross?.amount
|
{checkout?.shippingPrice?.gross?.amount
|
||||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||||
: "Calculated at checkout"
|
: "Calculated at checkout"
|
||||||
@@ -178,17 +222,31 @@ export default function CartDrawer() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="border-t border-[#e5e5e5] my-4" />
|
||||||
|
|
||||||
{/* Total */}
|
{/* Total */}
|
||||||
<div className="flex items-center justify-between mb-4 pt-4 border-t border-border/30">
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-serif">Total</span>
|
<span className="text-sm uppercase tracking-[0.05em] font-medium">Total</span>
|
||||||
<span className="font-serif text-lg">{formatPrice(total)}</span>
|
<span className="text-lg font-medium">
|
||||||
|
{formatPrice(total)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(checkout?.subtotalPrice?.gross?.amount || 0) < 5000 && (
|
||||||
|
<p className="text-xs text-[#666666] text-center">
|
||||||
|
Free shipping on orders over {formatPrice(5000)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="px-6 pb-6 space-y-3">
|
||||||
{/* Checkout Button */}
|
{/* Checkout Button */}
|
||||||
<Link
|
<Link
|
||||||
href="/checkout"
|
href="/checkout"
|
||||||
onClick={closeCart}
|
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"
|
className="block w-full py-4 bg-black text-white text-center text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||||
>
|
>
|
||||||
{isLoading ? "Processing..." : "Checkout"}
|
{isLoading ? "Processing..." : "Checkout"}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -196,11 +254,12 @@ export default function CartDrawer() {
|
|||||||
{/* Continue Shopping */}
|
{/* Continue Shopping */}
|
||||||
<button
|
<button
|
||||||
onClick={closeCart}
|
onClick={closeCart}
|
||||||
className="block w-full py-3 text-center text-foreground-muted hover:text-foreground mt-2"
|
className="block w-full py-3 text-center text-sm text-[#666666] hover:text-black transition-colors"
|
||||||
>
|
>
|
||||||
Continue Shopping
|
Continue Shopping
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
117
src/components/home/HeroVideo.tsx
Normal file
117
src/components/home/HeroVideo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
{/* Brand Column */}
|
||||||
|
<div className="lg:col-span-4">
|
||||||
|
<Link href="/" className="inline-block mb-6">
|
||||||
<Image
|
<Image
|
||||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||||
alt="ManoonOils"
|
alt="ManoonOils"
|
||||||
width={180}
|
width={150}
|
||||||
height={48}
|
height={40}
|
||||||
className="h-10 w-auto object-contain mb-4"
|
className="h-8 w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
<p className="text-foreground-muted max-w-md">
|
</Link>
|
||||||
Premium natural oils for hair and skin care. Crafted with love for your daily beauty routine.
|
<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>
|
||||||
</div>
|
{/* Social Links */}
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
<div>
|
<a
|
||||||
<h4 className="font-serif mb-4">Quick Links</h4>
|
href="https://instagram.com"
|
||||||
<ul className="space-y-2">
|
target="_blank"
|
||||||
<li>
|
rel="noopener noreferrer"
|
||||||
<Link href="/products" className="text-foreground-muted hover:text-foreground transition-colors">
|
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"
|
||||||
Products
|
aria-label="Instagram"
|
||||||
</Link>
|
>
|
||||||
</li>
|
<Instagram className="w-4 h-4" />
|
||||||
<li>
|
</a>
|
||||||
<Link href="/about" className="text-foreground-muted hover:text-foreground transition-colors">
|
<a
|
||||||
About Us
|
href="https://facebook.com"
|
||||||
</Link>
|
target="_blank"
|
||||||
</li>
|
rel="noopener noreferrer"
|
||||||
<li>
|
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"
|
||||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
aria-label="Facebook"
|
||||||
Contact
|
>
|
||||||
</Link>
|
<Facebook className="w-4 h-4" />
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-serif mb-4">Customer Service</h4>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
<li>
|
|
||||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
|
||||||
Shipping Info
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
|
||||||
Returns
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://manoonoils.com" className="text-foreground-muted hover:text-foreground transition-colors">
|
|
||||||
WooCommerce Store
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t border-border/30 mt-12 pt-8 text-center text-foreground-muted text-sm">
|
{/* Links Columns - All aligned at top */}
|
||||||
<p>© {currentYear} ManoonOils. All rights reserved.</p>
|
<div className="lg:col-span-8">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
|
||||||
|
{/* Shop */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||||
|
Shop
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{footerLinks.shop.map((link) => (
|
||||||
|
<li key={link.label}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* About */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||||
|
About
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{footerLinks.about.map((link) => (
|
||||||
|
<li key={link.label}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||||
|
Help
|
||||||
|
</h4>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{footerLinks.help.map((link) => (
|
||||||
|
<li key={link.label}>
|
||||||
|
<Link
|
||||||
|
href={link.href}
|
||||||
|
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Bar */}
|
||||||
|
<div className="border-t border-[#e5e5e5]">
|
||||||
|
<div className="container py-6">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
|
{/* Copyright */}
|
||||||
|
<p className="text-xs text-[#999999]">
|
||||||
|
© {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>
|
||||||
|
|||||||
@@ -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,14 +26,41 @@ 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
|
||||||
|
? "bg-white/95 backdrop-blur-md shadow-sm"
|
||||||
|
: "bg-transparent"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="container">
|
||||||
|
<div className="flex items-center justify-between h-[72px]">
|
||||||
{/* Mobile Menu Button */}
|
{/* Mobile Menu Button */}
|
||||||
<button
|
<button
|
||||||
className="lg:hidden p-2 -ml-2"
|
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
|
||||||
onClick={() => setMobileMenuOpen(true)}
|
onClick={() => setMobileMenuOpen(true)}
|
||||||
aria-label="Open menu"
|
aria-label="Open menu"
|
||||||
>
|
>
|
||||||
@@ -35,56 +68,49 @@ export default function Header() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href="/" className="flex-shrink-0">
|
<Link href="/" className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
|
||||||
<Image
|
<Image
|
||||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||||
alt="ManoonOils"
|
alt="ManoonOils"
|
||||||
width={150}
|
width={150}
|
||||||
height={40}
|
height={40}
|
||||||
className="h-8 w-auto object-contain"
|
className="h-7 w-auto object-contain"
|
||||||
|
priority
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation - Centered */}
|
||||||
<nav className="hidden lg:flex items-center gap-8">
|
<nav className="hidden lg:flex items-center gap-10 mx-auto">
|
||||||
|
{navLinks.map((link) => (
|
||||||
<Link
|
<Link
|
||||||
href="/products"
|
key={link.href}
|
||||||
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
|
href={link.href}
|
||||||
|
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
|
||||||
>
|
>
|
||||||
Products
|
{link.label}
|
||||||
</Link>
|
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
|
||||||
<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>
|
</Link>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Icons */}
|
{/* Icons */}
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<button
|
<button
|
||||||
className="p-2 hidden sm:block"
|
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
||||||
aria-label="Account"
|
aria-label="Account"
|
||||||
>
|
>
|
||||||
<User className="w-5 h-5" />
|
<User className="w-5 h-5" strokeWidth={1.5} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="p-2 relative"
|
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
|
||||||
onClick={toggleCart}
|
onClick={toggleCart}
|
||||||
aria-label="Open cart"
|
aria-label="Open cart"
|
||||||
>
|
>
|
||||||
<ShoppingBag className="w-5 h-5" />
|
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||||
{itemCount > 0 && (
|
{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">
|
<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}
|
{itemCount > 99 ? "99+" : itemCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
@@ -93,8 +119,81 @@ export default function Header() {
|
|||||||
</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 />
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
|
||||||
{localized.name}
|
{localized.name}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className="text-foreground-muted">
|
<p className="text-[14px] text-[#666666]">
|
||||||
{price || (locale === "en" ? "Contact for price" : "Kontaktirajte za cenu")}
|
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
<motion.div
|
<nav className="flex items-center gap-2 text-sm">
|
||||||
initial={{ opacity: 0, x: -20 }}
|
<Link href="/" className="text-[#666666] hover:text-black transition-colors">
|
||||||
animate={{ opacity: 1, x: 0 }}
|
{locale === "EN" ? "Home" : "Početna"}
|
||||||
transition={{ duration: 0.6 }}
|
</Link>
|
||||||
>
|
<span className="text-[#999999]">/</span>
|
||||||
<div className="relative aspect-square bg-background-ice mb-4">
|
<span className="text-[#1a1a1a]">{localized.name}</span>
|
||||||
{images[selectedImage] && (
|
</nav>
|
||||||
<Image
|
</div>
|
||||||
src={images[selectedImage].url}
|
|
||||||
alt={images[selectedImage].alt || localized.name}
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Product Content */}
|
||||||
|
<div className="container py-12 lg:py-16">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||||
|
{/* Image Gallery - Left Side */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="flex gap-4"
|
||||||
|
>
|
||||||
|
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
||||||
{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">
|
{/* Short Description */}
|
||||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
<p className="text-[#666666] leading-relaxed mb-6">
|
||||||
|
{shortDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Short Description */}
|
{/* Price & Rating */}
|
||||||
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<p>{stripHtml(localized.description).slice(0, 200)}...</p>
|
<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" />
|
||||||
|
|
||||||
|
{/* Size Selector */}
|
||||||
|
{product.variants && product.variants.length > 1 && (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<span className="text-sm uppercase tracking-[0.1em] font-medium">
|
||||||
|
{locale === "EN" ? "Size" : "Veličina"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{product.variants.map((v) => (
|
||||||
|
<button
|
||||||
|
key={v.id}
|
||||||
|
className={`px-5 py-3 text-sm border-2 transition-colors ${
|
||||||
|
v.id === variant?.id
|
||||||
|
? "border-black bg-black text-white"
|
||||||
|
: "border-[#e5e5e5] hover:border-[#999999]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{v.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Quantity */}
|
||||||
<div className="flex items-center gap-4 mb-8">
|
<div className="flex items-center gap-4 mb-8">
|
||||||
{/* Quantity Selector */}
|
<span className="text-sm uppercase tracking-[0.1em] font-medium w-16">
|
||||||
<div className="flex items-center border border-border">
|
{locale === "EN" ? "Qty" : "Kol"}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center border-2 border-[#1a1a1a]">
|
||||||
<button
|
<button
|
||||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||||
className="px-4 py-3 hover:bg-gray-50"
|
className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
|
||||||
|
disabled={quantity <= 1}
|
||||||
>
|
>
|
||||||
-
|
<Minus className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<span className="px-4 py-3 min-w-[3rem] text-center">{quantity}</span>
|
<span className="w-14 text-center text-base font-medium">{quantity}</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setQuantity(quantity + 1)}
|
onClick={() => setQuantity(quantity + 1)}
|
||||||
className="px-4 py-3 hover:bg-gray-50"
|
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 */}
|
{/* Add to Cart Button */}
|
||||||
|
{isAvailable ? (
|
||||||
<button
|
<button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
disabled={isAdding}
|
disabled={isAdding}
|
||||||
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors disabled:opacity-50"
|
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
|
{isAdding
|
||||||
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
||||||
: (locale === "EN" ? "Add to Cart" : "Dodaj u korpu")
|
: (locale === "EN" ? "Add to Cart — Free Shipping" : "Dodaj u korpu — Besplatna dostava")
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<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">
|
||||||
|
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||||
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
||||||
|
</span>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-medium">
|
||||||
|
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
</div>
|
||||||
{relatedProducts.map((relatedProduct, index) => (
|
<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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user