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() {
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
|
||||
Our Story
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6">
|
||||
<p>
|
||||
ManoonOils was born from a passion for natural beauty and the belief
|
||||
that the best skincare comes from nature itself. Our journey began with
|
||||
a simple question: how can we create products that truly nurture both
|
||||
hair and skin?
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We believe in the power of natural ingredients. Every oil in our
|
||||
collection is carefully selected for its unique properties and
|
||||
benefits. From nourishing oils that restore hair vitality to serums
|
||||
that rejuvenate skin, we craft each product with love and attention
|
||||
to detail.
|
||||
</p>
|
||||
|
||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
||||
Our Mission
|
||||
</h2>
|
||||
<p>
|
||||
Our mission is to provide premium quality, natural products that
|
||||
enhance your daily beauty routine. We are committed to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>Using only the finest natural ingredients</li>
|
||||
<li>Cruelty-free and ethical production</li>
|
||||
<li>Sustainable packaging practices</li>
|
||||
<li>Transparency in our formulations</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
||||
Handmade with Love
|
||||
</h2>
|
||||
<p>
|
||||
Every bottle of ManoonOils is handcrafted with care. We small-batch
|
||||
produce our products to ensure the highest quality and freshness.
|
||||
When you use ManoonOils, you can feel confident that you're using
|
||||
something made with genuine care and expertise.
|
||||
</p>
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[104px]">
|
||||
<div className="container py-12 md:py-16">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
|
||||
<h1 className="text-4xl md:text-5xl font-medium tracking-tight">
|
||||
About ManoonOils
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Image */}
|
||||
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
||||
alt="Natural oils production"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Introduction */}
|
||||
<div className="mb-16">
|
||||
<p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
|
||||
ManoonOils was born from a passion for natural beauty and the belief
|
||||
that the best skincare comes from nature itself.
|
||||
</p>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
We believe in the power of natural ingredients. Every oil in our
|
||||
collection is carefully selected for its unique properties and
|
||||
benefits. From nourishing oils that restore hair vitality to serums
|
||||
that rejuvenate skin, we craft each product with love and attention
|
||||
to detail.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Values Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Natural Ingredients</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
We use only the finest natural ingredients, sourced ethically and sustainably
|
||||
from trusted suppliers around the world.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Cruelty-Free</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
Our products are never tested on animals. We believe in beauty
|
||||
without compromise.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Sustainable Packaging</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
We use eco-friendly packaging materials and minimize waste
|
||||
throughout our production process.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Handcrafted Quality</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
Every bottle is handcrafted in small batches to ensure
|
||||
the highest quality and freshness.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission */}
|
||||
<div className="text-center py-12 border-t border-b border-[#e5e5e5]">
|
||||
<span className="text-caption text-[#666666] mb-4 block">Our Mission</span>
|
||||
<blockquote className="text-2xl md:text-3xl font-medium tracking-tight">
|
||||
“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
|
||||
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>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
Our journey began with a simple question: how can we create products
|
||||
that truly nurture both hair and skin? Today, we continue to innovate
|
||||
while staying true to our commitment to natural, effective beauty solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,29 @@ import {
|
||||
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
CHECKOUT_COMPLETE,
|
||||
} from "@/lib/saleor/mutations/Checkout";
|
||||
import type { Checkout } from "@/types/saleor";
|
||||
|
||||
// GraphQL Response Types
|
||||
interface ShippingAddressUpdateResponse {
|
||||
checkoutShippingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface BillingAddressUpdateResponse {
|
||||
checkoutBillingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutCompleteResponse {
|
||||
checkoutComplete?: {
|
||||
order?: { number: string };
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddressForm {
|
||||
firstName: string;
|
||||
@@ -94,7 +117,7 @@ export default function CheckoutPage() {
|
||||
|
||||
try {
|
||||
// Update shipping address
|
||||
const shippingResult = await saleorClient.mutate({
|
||||
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
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);
|
||||
}
|
||||
|
||||
// Update billing address
|
||||
const billingResult = await saleorClient.mutate({
|
||||
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
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);
|
||||
}
|
||||
|
||||
// Complete checkout (creates order)
|
||||
const completeResult = await saleorClient.mutate({
|
||||
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
|
||||
mutation: CHECKOUT_COMPLETE,
|
||||
variables: {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -154,9 +177,10 @@ export default function CheckoutPage() {
|
||||
// Order Success Page
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<>
|
||||
<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="mb-6">
|
||||
<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>
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<>
|
||||
<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-7xl mx-auto">
|
||||
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
|
||||
|
||||
@@ -457,8 +484,11 @@ export default function CheckoutPage() {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState } from "react";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { Mail, MapPin, Truck, Check } from "lucide-react";
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -18,97 +19,185 @@ export default function ContactPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
|
||||
Contact Us
|
||||
</h1>
|
||||
|
||||
<p className="text-foreground-muted text-center mb-12">
|
||||
Have questions? We'd love to hear from you.
|
||||
</p>
|
||||
|
||||
{submitted ? (
|
||||
<div className="bg-green-50 text-green-700 p-6 text-center">
|
||||
<p className="text-lg">Thank you for your message!</p>
|
||||
<p className="mt-2">We'll get back to you soon.</p>
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[104px]">
|
||||
<div className="container py-12 md:py-16">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Get in Touch</span>
|
||||
<h1 className="text-4xl md:text-5xl font-medium tracking-tight mb-4">
|
||||
Contact Us
|
||||
</h1>
|
||||
<p className="text-[#666666]">
|
||||
Have questions? We'd love to hear from you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-medium mb-6">Get in Touch</h2>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
We'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>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Email</h3>
|
||||
<p className="text-[#666666] text-sm">hello@manoonoils.com</p>
|
||||
<p className="text-[#999999] text-xs mt-1">We reply within 24 hours</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Truck className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Shipping</h3>
|
||||
<p className="text-[#666666] text-sm">Free shipping over 3,000 RSD</p>
|
||||
<p className="text-[#999999] text-xs mt-1">Delivered within 2-5 business days</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<MapPin className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Location</h3>
|
||||
<p className="text-[#666666] text-sm">Serbia</p>
|
||||
<p className="text-[#999999] text-xs mt-1">Shipping nationwide</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-2">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-border/30">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Email</h3>
|
||||
<p className="text-foreground-muted">hello@manoonoils.com</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Shipping</h3>
|
||||
<p className="text-foreground-muted">Free over 3000 RSD</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Location</h3>
|
||||
<p className="text-foreground-muted">Serbia</p>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="bg-[#f8f9fa] p-8 md:p-10">
|
||||
{submitted ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-2">Thank You!</h3>
|
||||
<p className="text-[#666666]">
|
||||
Your message has been sent. We'll get back to you soon.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-2">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
|
||||
placeholder="How can we help you?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="py-16 md:py-24 border-t border-[#e5e5e5]">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl font-medium text-center mb-12">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
q: "How long does shipping take?",
|
||||
a: "Orders are typically delivered within 2-5 business days for domestic shipping. You'll receive a tracking number once your order ships."
|
||||
},
|
||||
{
|
||||
q: "Are your products 100% natural?",
|
||||
a: "Yes! All our oils are 100% natural, cold-pressed, and free from any additives, preservatives, or artificial fragrances."
|
||||
},
|
||||
{
|
||||
q: "What is your return policy?",
|
||||
a: "We accept returns within 14 days of delivery for unopened products. Please contact us if you have any issues with your order."
|
||||
},
|
||||
{
|
||||
q: "Do you offer wholesale?",
|
||||
a: "Yes, we offer wholesale pricing for bulk orders. Please contact us at hello@manoonoils.com for more information."
|
||||
}
|
||||
].map((faq, index) => (
|
||||
<div key={index} className="border-b border-[#e5e5e5] pb-6">
|
||||
<h3 className="font-medium mb-2">{faq.q}</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">{faq.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,2 @@
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
|
||||
export const metadata = {
|
||||
title: "About - ManoonOils",
|
||||
description: "Learn about ManoonOils - our story, mission, and commitment to natural beauty.",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
|
||||
Our Story
|
||||
</h1>
|
||||
|
||||
<div className="prose prose-lg max-w-none text-foreground-muted space-y-6">
|
||||
<p>
|
||||
ManoonOils was born from a passion for natural beauty and the belief
|
||||
that the best skincare comes from nature itself. Our journey began with
|
||||
a simple question: how can we create products that truly nurture both
|
||||
hair and skin?
|
||||
</p>
|
||||
|
||||
<p>
|
||||
We believe in the power of natural ingredients. Every oil in our
|
||||
collection is carefully selected for its unique properties and
|
||||
benefits. From nourishing oils that restore hair vitality to serums
|
||||
that rejuvenate skin, we craft each product with love and attention
|
||||
to detail.
|
||||
</p>
|
||||
|
||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
||||
Our Mission
|
||||
</h2>
|
||||
<p>
|
||||
Our mission is to provide premium quality, natural products that
|
||||
enhance your daily beauty routine. We are committed to:
|
||||
</p>
|
||||
<ul className="list-disc pl-6 space-y-2">
|
||||
<li>Using only the finest natural ingredients</li>
|
||||
<li>Cruelty-free and ethical production</li>
|
||||
<li>Sustainable packaging practices</li>
|
||||
<li>Transparency in our formulations</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="font-serif text-2xl text-foreground mt-8 mb-4">
|
||||
Handmade with Love
|
||||
</h2>
|
||||
<p>
|
||||
Every bottle of ManoonOils is handcrafted with care. We small-batch
|
||||
produce our products to ensure the highest quality and freshness.
|
||||
When you use ManoonOils, you can feel confident that you're using
|
||||
something made with genuine care and expertise.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
// Re-export from main about page
|
||||
export { default, metadata } from "../../about/page";
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { getProducts } from "@/lib/woocommerce";
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import AnnouncementBar from "@/components/home/AnnouncementBar";
|
||||
import NewHero from "@/components/home/NewHero";
|
||||
import StatsSection from "@/components/home/StatsSection";
|
||||
import FeaturesSection from "@/components/home/FeaturesSection";
|
||||
import TestimonialsSection from "@/components/home/TestimonialsSection";
|
||||
import NewsletterSection from "@/components/home/NewsletterSection";
|
||||
import HeroVideo from "@/components/home/HeroVideo";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
@@ -17,61 +13,160 @@ export const metadata = {
|
||||
export default async function Homepage() {
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts();
|
||||
products = await getProducts("EN");
|
||||
} catch (e) {
|
||||
// Fallback for build time when API is unavailable
|
||||
console.log('Failed to fetch products during build');
|
||||
}
|
||||
const featuredProduct = products.find((p) => p.status === "publish");
|
||||
const publishedProducts = products
|
||||
.filter((p) => p.status === "publish")
|
||||
.slice(0, 4);
|
||||
|
||||
const featuredProducts = products.slice(0, 4);
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<AnnouncementBar />
|
||||
<div className="pt-10">
|
||||
<Header />
|
||||
</div>
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Hero Section with Video Background */}
|
||||
<HeroVideo />
|
||||
|
||||
{/* New Hero Section */}
|
||||
<NewHero featuredProduct={featuredProduct} />
|
||||
{/* Main Content */}
|
||||
<div id="main-content">
|
||||
{/* Products Grid Section */}
|
||||
{featuredProducts.length > 0 && (
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="container">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Collection</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">Premium Natural Oils</h2>
|
||||
<p className="text-[#666666] max-w-xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats & Philosophy Section */}
|
||||
<StatsSection />
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{featuredProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} locale="EN" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<FeaturesSection />
|
||||
{/* View All Link */}
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="/en/products"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
View All Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<TestimonialsSection />
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<NewsletterSection />
|
||||
|
||||
{/* Products Grid Section */}
|
||||
{publishedProducts.length > 0 && (
|
||||
<section className="py-20 px-6 bg-white">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
<h2 className="font-serif italic text-4xl text-center mb-4">
|
||||
Our Collection
|
||||
</h2>
|
||||
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{publishedProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
))}
|
||||
{/* Brand Story Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-6">Handmade with Love</h2>
|
||||
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||
Every bottle of ManoonOils is crafted with care using traditional
|
||||
methods passed down through generations. We source only the finest
|
||||
organic ingredients to bring you oils that nourish both hair and skin.
|
||||
</p>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
Our commitment to purity means no additives, no preservatives -
|
||||
just nature's goodness in its most potent form.
|
||||
</p>
|
||||
<a
|
||||
href="/en/about"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
||||
alt="Natural oils production"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
{/* Benefits Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Why Choose Us</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">The Manoon Difference</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{[
|
||||
{
|
||||
title: "100% Natural",
|
||||
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||
},
|
||||
{
|
||||
title: "Handcrafted",
|
||||
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||
},
|
||||
{
|
||||
title: "Sustainable",
|
||||
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
|
||||
},
|
||||
].map((benefit, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
|
||||
<span className="text-2xl font-medium text-[#1a1a1a]">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
|
||||
<p className="text-[#666666]">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white mb-16">
|
||||
<div className="container">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">Stay Connected</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">Join Our Community</h2>
|
||||
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<form className="inline-flex flex-col sm:flex-row">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="w-64 sm:w-80 px-5 h-14 bg-white/10 border border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Import ProductCard here to avoid circular dependency
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
@@ -1,70 +1,97 @@
|
||||
import { getProducts } from "@/lib/woocommerce";
|
||||
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductDetail from "@/components/product/ProductDetail";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
interface ProductPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
// Disable static generation - this page will be server-rendered
|
||||
export const generateStaticParams = undefined;
|
||||
|
||||
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
let product = null;
|
||||
|
||||
// Generate static params for all products
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const products = await getProducts();
|
||||
product = products.find((p) => (p.slug || p.id.toString()) === slug);
|
||||
const products = await getProducts("EN", 100);
|
||||
const params: Array<{ slug: string }> = [];
|
||||
|
||||
products.forEach((product: Product) => {
|
||||
// English slug (if translation exists)
|
||||
if (product.translation?.slug) {
|
||||
params.push({ slug: product.translation.slug });
|
||||
} else {
|
||||
params.push({ slug: product.slug });
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
} catch (e) {
|
||||
// Fallback
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const product = await getProductBySlug(slug, "EN");
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
title: "Product Not Found",
|
||||
};
|
||||
}
|
||||
|
||||
const localized = getLocalizedProduct(product, "EN");
|
||||
|
||||
return {
|
||||
title: localized.name,
|
||||
description: localized.seoDescription || localized.description?.slice(0, 160),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
const { slug } = await params;
|
||||
const product = await getProductBySlug(slug, "EN");
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<main className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<div className="pt-24 text-center">
|
||||
<h1 className="text-2xl">Product not found</h1>
|
||||
<div className="pt-[120px] text-center px-4">
|
||||
<h1 className="text-2xl font-medium mb-4">Product not found</h1>
|
||||
<p className="text-[#666666] mb-8">
|
||||
The product you'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>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const image = product.images?.[0]?.src || '/placeholder.jpg';
|
||||
const price = product.sale_price || product.price;
|
||||
// Get related products
|
||||
let relatedProducts: Product[] = [];
|
||||
try {
|
||||
const allProducts = await getProducts("EN", 8);
|
||||
relatedProducts = allProducts
|
||||
.filter((p: Product) => p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
} catch (e) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<main className="min-h-screen bg-white">
|
||||
<Header />
|
||||
|
||||
<section className="pt-24 pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden">
|
||||
<img
|
||||
src={image}
|
||||
alt={product.name}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-4xl font-serif mb-4">{product.name}</h1>
|
||||
|
||||
<div className="text-2xl mb-6">{price} RSD</div>
|
||||
|
||||
<div className="prose max-w-none mb-8" dangerouslySetInnerHTML={{ __html: product.description || '' }} />
|
||||
|
||||
<button
|
||||
className="inline-block bg-foreground text-white px-8 py-4 text-lg font-medium text-center hover:bg-opacity-90 transition-all"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ProductDetail
|
||||
product={product}
|
||||
relatedProducts={relatedProducts}
|
||||
locale="EN"
|
||||
/>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { getProducts } from "@/lib/woocommerce";
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Products - ManoonOils",
|
||||
@@ -9,38 +10,71 @@ export const metadata = {
|
||||
};
|
||||
|
||||
export default async function ProductsPage() {
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts();
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch products during build');
|
||||
}
|
||||
|
||||
const publishedProducts = products.filter((p) => p.status === "publish");
|
||||
const products = await getProducts("EN");
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16">
|
||||
All Products
|
||||
</h1>
|
||||
|
||||
{publishedProducts.length === 0 ? (
|
||||
<p className="text-center text-foreground-muted">No products available</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{publishedProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
))}
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[104px]">
|
||||
<div className="border-b border-[#e5e5e5]">
|
||||
<div className="container py-8 md:py-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">Our Collection</span>
|
||||
<h1 className="text-3xl md:text-4xl font-medium">All Products</h1>
|
||||
</div>
|
||||
|
||||
{/* Sort Dropdown */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[#666666]">{products.length} products</span>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||
defaultValue="featured"
|
||||
>
|
||||
<option value="featured">Featured</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="price-low">Price: Low to High</option>
|
||||
<option value="price-high">Price: High to Low</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-[#666666] mb-4">No products available</p>
|
||||
<p className="text-sm text-[#999999]">Please check back later for new arrivals.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale="EN"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +1,87 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* ============================================
|
||||
MANOONOILS DESIGN SYSTEM
|
||||
Inspired by premium skincare brands
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
--background: #f0f4f8;
|
||||
--background-ice: #e8f0f5;
|
||||
--foreground: #1a1a1a;
|
||||
--foreground-muted: #666666;
|
||||
--accent: #a8c5d8;
|
||||
--accent-dark: #7ba3bc;
|
||||
--white: #ffffff;
|
||||
--border: #d1d9e0;
|
||||
/* Primary Colors */
|
||||
--color-white: #ffffff;
|
||||
--color-background: #fafafa;
|
||||
--color-background-alt: #f5f5f5;
|
||||
--color-foreground: #1a1a1a;
|
||||
--color-foreground-muted: #666666;
|
||||
--color-foreground-subtle: #999999;
|
||||
|
||||
/* Accent Colors */
|
||||
--color-accent: #e8f0f5;
|
||||
--color-accent-dark: #a8c5d8;
|
||||
--color-accent-blue: #e8f0f5;
|
||||
--color-gold: #c9a962;
|
||||
--color-gold-light: #d4b978;
|
||||
|
||||
/* UI Colors */
|
||||
--color-border: #e5e5e5;
|
||||
--color-border-dark: #d1d1d1;
|
||||
--color-cta: #000000;
|
||||
--color-cta-hover: #333333;
|
||||
--color-overlay: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
--space-3xl: 64px;
|
||||
--space-4xl: 96px;
|
||||
--space-5xl: 128px;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 'DM Sans', sans-serif;
|
||||
--font-body: 'Inter', sans-serif;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 250ms ease;
|
||||
--transition-slow: 350ms ease;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-background-ice: var(--background-ice);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-foreground-muted: var(--foreground-muted);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-dark: var(--accent-dark);
|
||||
--color-white: var(--white);
|
||||
--color-border: var(--border);
|
||||
--font-display: var(--font-cedrat);
|
||||
--font-body: var(--font-dm-sans);
|
||||
/* Colors - reference :root variables */
|
||||
--color-white: var(--color-white);
|
||||
--color-background: var(--color-background);
|
||||
--color-background-alt: var(--color-background-alt);
|
||||
--color-foreground: var(--color-foreground);
|
||||
--color-foreground-muted: var(--color-foreground-muted);
|
||||
--color-foreground-subtle: var(--color-foreground-subtle);
|
||||
--color-accent: var(--color-accent);
|
||||
--color-accent-dark: var(--color-accent-dark);
|
||||
--color-accent-blue: var(--color-accent-blue);
|
||||
--color-gold: var(--color-gold);
|
||||
--color-gold-light: var(--color-gold-light);
|
||||
--color-border: var(--color-border);
|
||||
--color-border-dark: var(--color-border-dark);
|
||||
--color-cta: var(--color-cta);
|
||||
--color-cta-hover: var(--color-cta-hover);
|
||||
--color-overlay: var(--color-overlay);
|
||||
|
||||
/* Typography */
|
||||
--font-display: var(--font-display);
|
||||
--font-body: var(--font-body);
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cedrat Display';
|
||||
src: url('https://fonts.gstatic.com/s/cedratdisplay/v16/0nkoC9_pK3CvS5lZuZ7MAUmK5w.woff2') format('woff2');
|
||||
font-weight: 400 900;
|
||||
font-display: swap;
|
||||
}
|
||||
/* ============================================
|
||||
FONT IMPORTS
|
||||
============================================ */
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Sans';
|
||||
@@ -38,20 +90,284 @@
|
||||
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;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
============================================ */
|
||||
|
||||
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 */
|
||||
@@ -64,51 +380,48 @@ h1, h2, h3, h4, h5, h6 {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-slow {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-marquee {
|
||||
animation: marquee 25s linear infinite;
|
||||
}
|
||||
|
||||
.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 */
|
||||
.font-serif {
|
||||
font-family: 'Cedrat Display', serif;
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth scroll */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
/* Screen reader only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
199
src/app/page.tsx
199
src/app/page.tsx
@@ -1,12 +1,7 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import AnnouncementBar from "@/components/home/AnnouncementBar";
|
||||
import NewHero from "@/components/home/NewHero";
|
||||
import StatsSection from "@/components/home/StatsSection";
|
||||
import FeaturesSection from "@/components/home/FeaturesSection";
|
||||
import TestimonialsSection from "@/components/home/TestimonialsSection";
|
||||
import NewsletterSection from "@/components/home/NewsletterSection";
|
||||
import HeroVideo from "@/components/home/HeroVideo";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
export const metadata = {
|
||||
@@ -24,51 +19,169 @@ export default async function Homepage() {
|
||||
console.log('Failed to fetch products during build');
|
||||
}
|
||||
|
||||
const featuredProduct = products[0];
|
||||
const publishedProducts = products.slice(0, 4);
|
||||
const featuredProducts = products?.slice(0, 4) || [];
|
||||
const hasProducts = featuredProducts.length > 0;
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<AnnouncementBar />
|
||||
<div className="pt-10">
|
||||
<Header />
|
||||
</div>
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Hero Section with Video Background */}
|
||||
<HeroVideo />
|
||||
|
||||
{/* New Hero Section */}
|
||||
<NewHero featuredProduct={featuredProduct} />
|
||||
{/* Main Content */}
|
||||
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
|
||||
{/* Products Grid Section */}
|
||||
{hasProducts && (
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Our Collection
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||
Premium Natural Oils
|
||||
</h2>
|
||||
<p className="text-[#666666] max-w-xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats & Philosophy Section */}
|
||||
<StatsSection />
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{featuredProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} locale="SR" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Features Section */}
|
||||
<FeaturesSection />
|
||||
{/* View All Link */}
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="/products"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
View All Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Testimonials Section */}
|
||||
<TestimonialsSection />
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<NewsletterSection />
|
||||
|
||||
{/* Products Grid Section */}
|
||||
{publishedProducts.length > 0 && (
|
||||
<section className="py-20 px-6 bg-white">
|
||||
<div className="max-w-[1400px] mx-auto">
|
||||
<h2 className="font-serif italic text-4xl text-center mb-4">
|
||||
Our Collection
|
||||
</h2>
|
||||
<p className="text-center text-[#4A4A4A] mb-12 max-w-2xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{publishedProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} locale="SR" />
|
||||
))}
|
||||
{/* 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>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Why Choose Us
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
The Manoon Difference
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{[
|
||||
{
|
||||
title: "100% Natural",
|
||||
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||
},
|
||||
{
|
||||
title: "Handcrafted",
|
||||
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||
},
|
||||
{
|
||||
title: "Sustainable",
|
||||
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
|
||||
},
|
||||
].map((benefit, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
|
||||
<span className="text-2xl font-medium text-[#1a1a1a]">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
|
||||
<p className="text-[#666666]">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">
|
||||
Stay Connected
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">
|
||||
Join Our Community
|
||||
</h2>
|
||||
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
|
||||
</p>
|
||||
{/* Newsletter Form - Centered */}
|
||||
<form className="flex flex-col sm:flex-row items-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>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Image from "next/image";
|
||||
import { getProductBySlug, getProducts, getProductPrice, getProductImage, getLocalizedProduct, formatPrice } from "@/lib/saleor";
|
||||
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductDetail from "@/components/product/ProductDetail";
|
||||
import type { Product } from "@/types/saleor";
|
||||
|
||||
interface ProductPageProps {
|
||||
@@ -52,6 +52,12 @@ export async function generateMetadata({ params }: ProductPageProps) {
|
||||
"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) {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<>
|
||||
<Header />
|
||||
<div className="pt-24 text-center">
|
||||
<h1 className="text-2xl">
|
||||
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
||||
</h1>
|
||||
</div>
|
||||
<main className="min-h-screen bg-white">
|
||||
<div className="pt-[180px] lg:pt-[200px] pb-20 text-center px-4">
|
||||
<h1 className="text-2xl font-medium mb-4">
|
||||
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
||||
</h1>
|
||||
<p className="text-[#666666] mb-8">
|
||||
{locale === "en"
|
||||
? "The product you're looking for doesn't exist or has been removed."
|
||||
: "Proizvod koji tražite ne postoji ili je uklonjen."}
|
||||
</p>
|
||||
<a
|
||||
href="/products"
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{locale === "en" ? "Browse Products" : "Pregledaj proizvode"}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const 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
|
||||
const isEnglishSlug = slug === product.translation?.slug;
|
||||
const currentLocale = isEnglishSlug ? "en" : "sr";
|
||||
const currentLocale = isEnglishSlug ? "EN" : "SR";
|
||||
|
||||
// URLs for language switcher
|
||||
const serbianUrl = `/products/${product.slug}`;
|
||||
const englishUrl = product.translation?.slug
|
||||
? `/products/${product.translation.slug}`
|
||||
: serbianUrl;
|
||||
// Get related products (same category or just other products)
|
||||
let relatedProducts: Product[] = [];
|
||||
try {
|
||||
const allProducts = await getProducts(currentLocale, 8);
|
||||
relatedProducts = allProducts
|
||||
.filter((p: Product) => p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
} catch (e) {
|
||||
// Ignore error, just won't show related products
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<section className="pt-24 pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
{/* Product Image */}
|
||||
<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>
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
<ProductDetail
|
||||
product={product}
|
||||
relatedProducts={relatedProducts}
|
||||
locale={currentLocale}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Products - ManoonOils",
|
||||
@@ -15,37 +16,91 @@ interface ProductsPageProps {
|
||||
export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
const { locale = "sr" } = await params;
|
||||
const products = await getProducts(locale.toUpperCase());
|
||||
const localeUpper = locale.toUpperCase();
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16">
|
||||
{locale === "en" ? "All Products" : "Svi Proizvodi"}
|
||||
</h1>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<p className="text-center text-foreground-muted">
|
||||
{locale === "en" ? "No products available" : "Nema dostupnih proizvoda"}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale={locale.toUpperCase()}
|
||||
/>
|
||||
))}
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[140px] lg:pt-[160px]">
|
||||
<div className="border-b border-[#e5e5e5]">
|
||||
<div className="container py-8 md:py-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">
|
||||
{localeUpper === "EN" ? "Our Collection" : "Naša kolekcija"}
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl font-medium">
|
||||
{localeUpper === "EN" ? "All Products" : "Svi Proizvodi"}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Sort Dropdown */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[#666666]">
|
||||
{products.length} {localeUpper === "EN" ? "products" : "proizvoda"}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||
defaultValue="featured"
|
||||
>
|
||||
<option value="featured">
|
||||
{localeUpper === "EN" ? "Featured" : "Istaknuto"}
|
||||
</option>
|
||||
<option value="newest">
|
||||
{localeUpper === "EN" ? "Newest" : "Najnovije"}
|
||||
</option>
|
||||
<option value="price-low">
|
||||
{localeUpper === "EN" ? "Price: Low to High" : "Cena: Rastuće"}
|
||||
</option>
|
||||
<option value="price-high">
|
||||
{localeUpper === "EN" ? "Price: High to Low" : "Cena: Opadajuće"}
|
||||
</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-[#666666] mb-4">
|
||||
{localeUpper === "EN" ? "No products available" : "Nema dostupnih proizvoda"}
|
||||
</p>
|
||||
<p className="text-sm text-[#999999]">
|
||||
{localeUpper === "EN"
|
||||
? "Please check back later for new arrivals."
|
||||
: "Molimo proverite ponovo kasnije za nove proizvode."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale={localeUpper}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { MetadataRoute } from "next";
|
||||
import { getProducts } from "@/lib/woocommerce";
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
|
||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
||||
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts();
|
||||
products = await getProducts("SR", 100);
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch products for sitemap during build');
|
||||
}
|
||||
|
||||
const productUrls = products
|
||||
.filter((p) => p.status === "publish")
|
||||
.map((product) => ({
|
||||
url: `${baseUrl}/products/${product.slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
const productUrls = products.map((product) => ({
|
||||
url: `${baseUrl}/products/${product.slug}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly" as const,
|
||||
priority: 0.8,
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { X, Minus, Plus, Trash2, ShoppingBag } from "lucide-react";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { formatPrice } from "@/lib/saleor";
|
||||
|
||||
@@ -32,13 +33,25 @@ export default function CartDrawer() {
|
||||
initCheckout();
|
||||
}, [initCheckout]);
|
||||
|
||||
// Lock body scroll when cart is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/50 z-50"
|
||||
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
@@ -47,77 +60,99 @@ export default function CartDrawer() {
|
||||
|
||||
{/* Drawer */}
|
||||
<motion.div
|
||||
className="fixed top-0 right-0 bottom-0 w-full max-w-md bg-white z-50 shadow-xl flex flex-col"
|
||||
className="fixed top-0 right-0 bottom-0 w-full max-w-[420px] bg-white z-50 shadow-2xl flex flex-col"
|
||||
initial={{ x: "100%" }}
|
||||
animate={{ x: 0 }}
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "tween", duration: 0.3 }}
|
||||
transition={{ type: "tween", duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-border/30">
|
||||
<h2 className="text-xl font-serif">
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-[#e5e5e5]">
|
||||
<h2 className="text-sm uppercase tracking-[0.1em] font-medium">
|
||||
Your Cart ({lineCount})
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeCart}
|
||||
className="p-2"
|
||||
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
aria-label="Close cart"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-5 h-5" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 border-b border-red-100">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={clearError}
|
||||
className="text-red-600 text-xs underline mt-1"
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="p-4 bg-red-50 border-b border-red-100">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
<button
|
||||
onClick={clearError}
|
||||
className="text-red-600 text-xs underline mt-1 hover:no-underline"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{lines.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-foreground-muted mb-6">Your cart is empty</p>
|
||||
<div className="flex flex-col items-center justify-center h-full px-6">
|
||||
<div className="w-16 h-16 rounded-full bg-[#f8f9fa] flex items-center justify-center mb-6">
|
||||
<ShoppingBag className="w-8 h-8 text-[#999999]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-[#666666] mb-2">Your cart is empty</p>
|
||||
<p className="text-sm text-[#999999] mb-8 text-center">
|
||||
Looks like you haven't added anything to your cart yet.
|
||||
</p>
|
||||
<Link
|
||||
href="/products"
|
||||
onClick={closeCart}
|
||||
className="inline-block px-6 py-3 bg-foreground text-white"
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
Start Shopping
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="p-6 space-y-6">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex gap-4">
|
||||
{/* Product Image */}
|
||||
<div className="w-20 h-20 bg-background-ice relative flex-shrink-0">
|
||||
{line.variant.product.media[0]?.url && (
|
||||
<div className="w-24 h-24 bg-[#f8f9fa] relative flex-shrink-0 overflow-hidden">
|
||||
{line.variant.product.media[0]?.url ? (
|
||||
<Image
|
||||
src={line.variant.product.media[0].url}
|
||||
alt={line.variant.product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="96px"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||
<ShoppingBag className="w-6 h-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-serif text-sm">{line.variant.product.name}</h3>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium truncate">
|
||||
{line.variant.product.name}
|
||||
</h3>
|
||||
{line.variant.name !== "Default" && (
|
||||
<p className="text-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(
|
||||
line.variant.pricing?.price?.gross?.amount || 0,
|
||||
line.variant.pricing?.price?.gross?.currency
|
||||
@@ -125,30 +160,35 @@ export default function CartDrawer() {
|
||||
</p>
|
||||
|
||||
{/* Quantity Controls */}
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<button
|
||||
onClick={() => updateLine(line.id, line.quantity - 1)}
|
||||
disabled={isLoading}
|
||||
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span>{line.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateLine(line.id, line.quantity + 1)}
|
||||
disabled={isLoading}
|
||||
className="w-8 h-8 border border-border flex items-center justify-center disabled:opacity-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div className="flex items-center border border-[#e5e5e5]">
|
||||
<button
|
||||
onClick={() => updateLine(line.id, line.quantity - 1)}
|
||||
disabled={isLoading || line.quantity <= 1}
|
||||
className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Minus className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="w-10 text-center text-sm font-medium">
|
||||
{line.quantity}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => updateLine(line.id, line.quantity + 1)}
|
||||
disabled={isLoading}
|
||||
className="w-8 h-8 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
<button
|
||||
onClick={() => removeLine(line.id)}
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<Trash2 className="w-4 h-4" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,46 +200,65 @@ export default function CartDrawer() {
|
||||
|
||||
{/* Footer with Checkout */}
|
||||
{lines.length > 0 && (
|
||||
<div className="p-6 border-t border-border/30">
|
||||
{/* Subtotal */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-foreground-muted">Subtotal</span>
|
||||
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
||||
<div className="border-t border-[#e5e5e5] bg-white">
|
||||
{/* Order Summary */}
|
||||
<div className="p-6 space-y-3">
|
||||
{/* Subtotal */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-[#666666]">Subtotal</span>
|
||||
<span className="font-medium">
|
||||
{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Shipping */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-[#666666]">Shipping</span>
|
||||
<span className="text-[#666666]">
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||
: "Calculated at checkout"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="border-t border-[#e5e5e5] my-4" />
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm uppercase tracking-[0.05em] font-medium">Total</span>
|
||||
<span className="text-lg font-medium">
|
||||
{formatPrice(total)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(checkout?.subtotalPrice?.gross?.amount || 0) < 5000 && (
|
||||
<p className="text-xs text-[#666666] text-center">
|
||||
Free shipping on orders over {formatPrice(5000)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Shipping */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-foreground-muted">Shipping</span>
|
||||
<span>
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||
: "Calculated at checkout"
|
||||
}
|
||||
</span>
|
||||
{/* Actions */}
|
||||
<div className="px-6 pb-6 space-y-3">
|
||||
{/* Checkout Button */}
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={closeCart}
|
||||
className="block w-full py-4 bg-black text-white text-center text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{isLoading ? "Processing..." : "Checkout"}
|
||||
</Link>
|
||||
|
||||
{/* Continue Shopping */}
|
||||
<button
|
||||
onClick={closeCart}
|
||||
className="block w-full py-3 text-center text-sm text-[#666666] hover:text-black transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Total */}
|
||||
<div className="flex items-center justify-between mb-4 pt-4 border-t border-border/30">
|
||||
<span className="font-serif">Total</span>
|
||||
<span className="font-serif text-lg">{formatPrice(total)}</span>
|
||||
</div>
|
||||
|
||||
{/* Checkout Button */}
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={closeCart}
|
||||
className="block w-full py-3 bg-foreground text-white text-center font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? "Processing..." : "Checkout"}
|
||||
</Link>
|
||||
|
||||
{/* Continue Shopping */}
|
||||
<button
|
||||
onClick={closeCart}
|
||||
className="block w-full py-3 text-center text-foreground-muted hover:text-foreground mt-2"
|
||||
>
|
||||
Continue Shopping
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</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";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { WooProduct } from "@/lib/woocommerce";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
interface ProductShowcaseProps {
|
||||
products: WooProduct[];
|
||||
products: Product[];
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function ProductShowcase({ products }: ProductShowcaseProps) {
|
||||
export default function ProductShowcase({ products, locale = "SR" }: ProductShowcaseProps) {
|
||||
if (!products || products.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="container">
|
||||
<motion.div
|
||||
className="text-center mb-16"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
@@ -21,15 +22,16 @@ export default function ProductShowcase({ products }: ProductShowcaseProps) {
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-serif mb-4">Our Products</h2>
|
||||
<p className="text-foreground-muted max-w-2xl mx-auto">
|
||||
<span className="text-caption text-[#666666] mb-4 block">Our Collection</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">Our Products</h2>
|
||||
<p className="text-[#666666] max-w-2xl mx-auto">
|
||||
Discover our premium collection of natural oils for hair and skin care
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
<ProductCard key={product.id} product={product} index={index} locale={locale} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,71 +1,161 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Instagram, Facebook } from "lucide-react";
|
||||
|
||||
const footerLinks = {
|
||||
shop: [
|
||||
{ label: "All Products", href: "/products" },
|
||||
{ label: "Hair Care", href: "/products" },
|
||||
{ label: "Skin Care", href: "/products" },
|
||||
{ label: "Gift Sets", href: "/products" },
|
||||
],
|
||||
about: [
|
||||
{ label: "Our Story", href: "/about" },
|
||||
{ label: "Process", href: "/about" },
|
||||
{ label: "Sustainability", href: "/about" },
|
||||
],
|
||||
help: [
|
||||
{ label: "FAQ", href: "/contact" },
|
||||
{ label: "Shipping", href: "/contact" },
|
||||
{ label: "Returns", href: "/contact" },
|
||||
{ label: "Contact Us", href: "/contact" },
|
||||
],
|
||||
};
|
||||
|
||||
export default function Footer() {
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
return (
|
||||
<footer className="bg-background-ice border-t border-border/30">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div className="md:col-span-2">
|
||||
<Image
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
width={180}
|
||||
height={48}
|
||||
className="h-10 w-auto object-contain mb-4"
|
||||
/>
|
||||
<p className="text-foreground-muted max-w-md">
|
||||
Premium natural oils for hair and skin care. Crafted with love for your daily beauty routine.
|
||||
<footer className="bg-white border-t border-[#e5e5e5]">
|
||||
{/* Main Footer */}
|
||||
<div className="container py-16 lg:py-20">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
|
||||
{/* Brand Column */}
|
||||
<div className="lg:col-span-4">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<Image
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
width={150}
|
||||
height={40}
|
||||
className="h-8 w-auto object-contain"
|
||||
/>
|
||||
</Link>
|
||||
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mb-6">
|
||||
Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.
|
||||
</p>
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full border border-[#e5e5e5] flex items-center justify-center text-[#666666] hover:border-black hover:text-black transition-colors"
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<Instagram className="w-4 h-4" />
|
||||
</a>
|
||||
<a
|
||||
href="https://facebook.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-10 h-10 rounded-full border border-[#e5e5e5] flex items-center justify-center text-[#666666] hover:border-black hover:text-black transition-colors"
|
||||
aria-label="Facebook"
|
||||
>
|
||||
<Facebook className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif mb-4">Quick Links</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/products" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Products
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/about" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
About Us
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{/* Links Columns - All aligned at top */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{/* Shop */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
Shop
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.shop.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-serif mb-4">Customer Service</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Shipping Info
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/contact" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
Returns
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://manoonoils.com" className="text-foreground-muted hover:text-foreground transition-colors">
|
||||
WooCommerce Store
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{/* About */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
About
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.about.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Help */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
Help
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.help.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm text-[#666666] hover:text-black transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/30 mt-12 pt-8 text-center text-foreground-muted text-sm">
|
||||
<p>© {currentYear} ManoonOils. All rights reserved.</p>
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-[#e5e5e5]">
|
||||
<div className="container py-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Copyright */}
|
||||
<p className="text-xs text-[#999999]">
|
||||
© {currentYear} ManoonOils. All rights reserved.
|
||||
</p>
|
||||
|
||||
{/* Payment Methods */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-[#999999]">We accept:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
|
||||
Visa
|
||||
</span>
|
||||
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
|
||||
MC
|
||||
</span>
|
||||
<span className="text-xs font-medium text-[#666666] px-2 py-1 border border-[#e5e5e5] rounded">
|
||||
COD
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -3,14 +3,20 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { User, ShoppingBag, Menu } from "lucide-react";
|
||||
import MobileMenu from "./MobileMenu";
|
||||
import { User, ShoppingBag, Menu, X } from "lucide-react";
|
||||
import CartDrawer from "@/components/cart/CartDrawer";
|
||||
|
||||
const navLinks = [
|
||||
{ href: "/products", label: "Products" },
|
||||
{ href: "/about", label: "About" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
|
||||
|
||||
const itemCount = getLineCount();
|
||||
@@ -20,14 +26,41 @@ export default function Header() {
|
||||
initCheckout();
|
||||
}, [initCheckout]);
|
||||
|
||||
// Track scroll for header styling
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 50);
|
||||
};
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "";
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [mobileMenuOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-10 z-40 bg-white border-b border-[#1A1A1A]/[0.06]">
|
||||
<div className="max-w-[1400px] mx-auto px-6">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-white/95 backdrop-blur-md shadow-sm"
|
||||
: "bg-transparent"
|
||||
}`}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between h-[72px]">
|
||||
{/* Mobile Menu 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)}
|
||||
aria-label="Open menu"
|
||||
>
|
||||
@@ -35,56 +68,49 @@ export default function Header() {
|
||||
</button>
|
||||
|
||||
{/* 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
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
width={150}
|
||||
height={40}
|
||||
className="h-8 w-auto object-contain"
|
||||
className="h-7 w-auto object-contain"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="hidden lg:flex items-center gap-8">
|
||||
<Link
|
||||
href="/products"
|
||||
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
|
||||
>
|
||||
Contact
|
||||
</Link>
|
||||
{/* Desktop Navigation - Centered */}
|
||||
<nav className="hidden lg:flex items-center gap-10 mx-auto">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
|
||||
>
|
||||
{link.label}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Icons */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="p-2 hidden sm:block"
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
||||
aria-label="Account"
|
||||
>
|
||||
<User className="w-5 h-5" />
|
||||
<User className="w-5 h-5" strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="p-2 relative"
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
|
||||
onClick={toggleCart}
|
||||
aria-label="Open cart"
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" />
|
||||
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 bg-[#1A1A1A] text-white text-[10px] w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{itemCount}
|
||||
<span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
|
||||
{itemCount > 99 ? "99+" : itemCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -93,8 +119,81 @@ export default function Header() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && <MobileMenu onClose={() => setMobileMenuOpen(false)} />}
|
||||
{mobileMenuOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[60] bg-white"
|
||||
>
|
||||
<div className="container h-full flex flex-col">
|
||||
{/* Mobile Header */}
|
||||
<div className="flex items-center justify-between h-[72px]">
|
||||
<Link href="/" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Image
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
width={150}
|
||||
height={40}
|
||||
className="h-7 w-auto object-contain"
|
||||
/>
|
||||
</Link>
|
||||
<button
|
||||
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<nav className="flex-1 flex flex-col justify-center gap-8">
|
||||
{navLinks.map((link, index) => (
|
||||
<motion.div
|
||||
key={link.href}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 + 0.1 }}
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
className="text-3xl font-medium tracking-tight hover:text-[#666666] transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Mobile Footer */}
|
||||
<div className="py-8 border-t border-[#e5e5e5]">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
|
||||
onClick={() => {
|
||||
setMobileMenuOpen(false);
|
||||
toggleCart();
|
||||
}}
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||
Cart ({itemCount})
|
||||
</button>
|
||||
<button
|
||||
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
|
||||
>
|
||||
<User className="w-5 h-5" strokeWidth={1.5} />
|
||||
Account
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<CartDrawer />
|
||||
|
||||
@@ -26,31 +26,55 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Link href={`/products/${localized.slug}`} className="group block">
|
||||
<div className="relative aspect-[4/5] bg-background-ice overflow-hidden mb-4">
|
||||
{image && (
|
||||
{/* Image Container */}
|
||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||
{image ? (
|
||||
<Image
|
||||
src={image}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||
<span className="text-sm">No image</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Out of Stock Overlay */}
|
||||
{!isAvailable && (
|
||||
<div className="absolute inset-0 bg-black/50 flex items-center justify-center">
|
||||
<span className="text-white font-medium">
|
||||
{locale === "en" ? "Out of Stock" : "Nema na stanju"}
|
||||
<div className="absolute inset-0 bg-white/80 flex items-center justify-center">
|
||||
<span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
|
||||
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover Quick Add (optional) */}
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button
|
||||
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
// Quick add functionality can be added here
|
||||
}}
|
||||
>
|
||||
{locale === "EN" ? "Quick Add" : "Dodaj u korpu"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="font-serif text-lg mb-1 group-hover:text-accent-dark transition-colors">
|
||||
{localized.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-foreground-muted">
|
||||
{price || (locale === "en" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
</p>
|
||||
{/* Product Info */}
|
||||
<div className="text-center">
|
||||
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
|
||||
{localized.name}
|
||||
</h3>
|
||||
|
||||
<p className="text-[14px] text-[#666666]">
|
||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useState } from "react";
|
||||
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 { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
||||
import { getProductPrice, getLocalizedProduct } from "@/lib/saleor";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
interface ProductDetailProps {
|
||||
@@ -14,6 +16,70 @@ interface ProductDetailProps {
|
||||
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) {
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
@@ -22,9 +88,11 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
|
||||
const localized = getLocalizedProduct(product, locale);
|
||||
const variant = product.variants?.[0];
|
||||
|
||||
// Get all images from media
|
||||
const images = product.media?.length > 0
|
||||
? product.media
|
||||
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" }];
|
||||
? product.media.filter(m => m.type === "IMAGE")
|
||||
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" as const }];
|
||||
|
||||
const handleAddToCart = async () => {
|
||||
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 price = getProductPrice(product);
|
||||
|
||||
// Extract short description (first sentence or first 100 chars)
|
||||
const shortDescription = localized.description
|
||||
? localized.description.split('.')[0] + '.'
|
||||
: locale === "EN" ? "Premium natural oil for your beauty routine." : "Premium prirodno ulje za vašu rutinu lepote.";
|
||||
|
||||
// Parse benefits from product metadata or use defaults
|
||||
const benefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',') || [
|
||||
locale === "EN" ? "Natural" : "Prirodno",
|
||||
locale === "EN" ? "Organic" : "Organsko",
|
||||
locale === "EN" ? "Cruelty-free" : "Bez okrutnosti",
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="py-12 md:py-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Product Images */}
|
||||
<section className="min-h-screen" id="product-detail">
|
||||
{/* Breadcrumb - with proper top padding for fixed header */}
|
||||
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
|
||||
<div className="container py-5">
|
||||
<nav className="flex items-center gap-2 text-sm">
|
||||
<Link href="/" className="text-[#666666] hover:text-black transition-colors">
|
||||
{locale === "EN" ? "Home" : "Početna"}
|
||||
</Link>
|
||||
<span className="text-[#999999]">/</span>
|
||||
<span className="text-[#1a1a1a]">{localized.name}</span>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Content */}
|
||||
<div className="container py-12 lg:py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||
{/* Image Gallery - Left Side */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex gap-4"
|
||||
>
|
||||
<div className="relative aspect-square bg-background-ice mb-4">
|
||||
{images[selectedImage] && (
|
||||
<Image
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
||||
{images.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
<div className="hidden md:flex flex-col gap-3 w-20 flex-shrink-0">
|
||||
{images.map((image, index) => (
|
||||
<button
|
||||
key={image.id}
|
||||
onClick={() => setSelectedImage(index)}
|
||||
className={`relative w-20 h-20 flex-shrink-0 ${
|
||||
selectedImage === index ? "ring-2 ring-foreground" : ""
|
||||
className={`relative aspect-square w-full overflow-hidden border-2 transition-colors ${
|
||||
selectedImage === index
|
||||
? "border-black"
|
||||
: "border-transparent hover:border-[#999999]"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
@@ -84,103 +165,208 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
alt={image.alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="80px"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Image */}
|
||||
<div className="flex-1 relative aspect-square bg-[#f8f9fa] overflow-hidden">
|
||||
{images[selectedImage] && (
|
||||
<Image
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Award Badge - Optional */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<div className="bg-black text-white text-[10px] uppercase tracking-[0.1em] px-3 py-1.5">
|
||||
{locale === "EN" ? "Bestseller" : "Najprodavanije"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Product Info */}
|
||||
{/* Product Info - Right Side */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="lg:pl-8"
|
||||
>
|
||||
<h1 className="text-3xl md:text-4xl font-serif mb-4">
|
||||
{/* Product Name */}
|
||||
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
||||
{localized.name}
|
||||
</h1>
|
||||
|
||||
<p className="text-2xl text-foreground-muted mb-6">
|
||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
</p>
|
||||
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="prose prose-sm max-w-none mb-8 text-foreground-muted">
|
||||
<p>{stripHtml(localized.description).slice(0, 200)}...</p>
|
||||
<p className="text-[#666666] leading-relaxed mb-6">
|
||||
{shortDescription}
|
||||
</p>
|
||||
|
||||
{/* Price & Rating */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<span className="text-3xl font-medium">
|
||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
</span>
|
||||
<StarRating rating={5} count={12} />
|
||||
</div>
|
||||
|
||||
{/* Add to Cart */}
|
||||
{isAvailable ? (
|
||||
<div className="flex items-center gap-4 mb-8">
|
||||
{/* Quantity Selector */}
|
||||
<div className="flex items-center border border-border">
|
||||
<button
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="px-4 py-3 hover:bg-gray-50"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="px-4 py-3 min-w-[3rem] text-center">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="px-4 py-3 hover:bg-gray-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
{/* Divider */}
|
||||
<div className="border-t border-[#e5e5e5] mb-8" />
|
||||
|
||||
{/* Size Selector */}
|
||||
{product.variants && product.variants.length > 1 && (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm uppercase tracking-[0.1em] font-medium">
|
||||
{locale === "EN" ? "Size" : "Veličina"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
<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">
|
||||
<span className="text-sm uppercase tracking-[0.1em] font-medium w-16">
|
||||
{locale === "EN" ? "Qty" : "Kol"}
|
||||
</span>
|
||||
<div className="flex items-center border-2 border-[#1a1a1a]">
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={isAdding}
|
||||
className="flex-1 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
onClick={() => setQuantity(Math.max(1, quantity - 1))}
|
||||
className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
|
||||
disabled={quantity <= 1}
|
||||
>
|
||||
{isAdding
|
||||
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
||||
: (locale === "EN" ? "Add to Cart" : "Dodaj u korpu")
|
||||
}
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="w-14 text-center text-base font-medium">{quantity}</span>
|
||||
<button
|
||||
onClick={() => setQuantity(quantity + 1)}
|
||||
className="w-12 h-12 flex items-center justify-center hover:bg-[#f8f9fa] transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add to Cart Button */}
|
||||
{isAvailable ? (
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
disabled={isAdding}
|
||||
className="w-full h-16 bg-black text-white text-base uppercase tracking-[0.15em] font-medium hover:bg-[#333333] active:bg-[#1a1a1a] transition-colors disabled:opacity-50 disabled:cursor-not-allowed mb-8"
|
||||
>
|
||||
{isAdding
|
||||
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
||||
: (locale === "EN" ? "Add to Cart — Free Shipping" : "Dodaj u korpu — Besplatna dostava")
|
||||
}
|
||||
</button>
|
||||
) : (
|
||||
<div className="py-3 bg-red-50 text-red-600 text-center mb-8">
|
||||
<div className="w-full h-16 bg-[#f8f9fa] text-[#666666] flex items-center justify-center text-base uppercase tracking-[0.15em] mb-8">
|
||||
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
|
||||
</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 */}
|
||||
{variant?.sku && (
|
||||
<p className="text-sm text-foreground-muted mb-4">
|
||||
<p className="text-xs text-[#999999] mt-8">
|
||||
SKU: {variant.sku}
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Products */}
|
||||
{relatedProducts.length > 0 && (
|
||||
<section className="py-12 px-4 bg-background-ice">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-2xl font-serif text-center mb-8">
|
||||
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{relatedProducts.map((relatedProduct, index) => (
|
||||
{relatedProducts && relatedProducts.length > 0 && (
|
||||
<section className="py-20 lg:py-28 bg-[#f8f9fa]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="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
|
||||
key={relatedProduct.id}
|
||||
product={relatedProduct}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { saleorClient } from "./client";
|
||||
import { GET_PRODUCTS, GET_PRODUCT_BY_SLUG } from "./queries/Products";
|
||||
import type { Product, ProductList } from "@/types/saleor";
|
||||
import type { Product } from "@/types/saleor";
|
||||
|
||||
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
|
||||
|
||||
// GraphQL Response Types
|
||||
interface ProductsResponse {
|
||||
products?: {
|
||||
edges: Array<{ node: Product }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProductResponse {
|
||||
product?: Product | null;
|
||||
}
|
||||
|
||||
export async function getProducts(
|
||||
locale: string = "SR",
|
||||
first: number = 100
|
||||
): Promise<Product[]> {
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
const { data } = await saleorClient.query<ProductsResponse>({
|
||||
query: GET_PRODUCTS,
|
||||
variables: {
|
||||
channel: CHANNEL,
|
||||
@@ -18,7 +29,7 @@ export async function getProducts(
|
||||
},
|
||||
});
|
||||
|
||||
return data?.products?.edges.map((edge: { node: Product }) => edge.node) || [];
|
||||
return data?.products?.edges.map((edge) => edge.node) || [];
|
||||
} catch (error) {
|
||||
console.error("Error fetching products from Saleor:", error);
|
||||
return [];
|
||||
@@ -30,7 +41,7 @@ export async function getProductBySlug(
|
||||
locale: string = "SR"
|
||||
): Promise<Product | null> {
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
const { data } = await saleorClient.query<ProductResponse>({
|
||||
query: GET_PRODUCT_BY_SLUG,
|
||||
variables: {
|
||||
slug,
|
||||
|
||||
@@ -15,6 +15,46 @@ import type { Checkout, CheckoutLine } from "@/types/saleor";
|
||||
|
||||
const CHANNEL = process.env.NEXT_PUBLIC_SALEOR_CHANNEL || "default-channel";
|
||||
|
||||
// GraphQL Response Types
|
||||
interface CheckoutCreateResponse {
|
||||
checkoutCreate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutLinesAddResponse {
|
||||
checkoutLinesAdd?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutLinesUpdateResponse {
|
||||
checkoutLinesUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutLinesDeleteResponse {
|
||||
checkoutLinesDelete?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutEmailUpdateResponse {
|
||||
checkoutEmailUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetCheckoutResponse {
|
||||
checkout?: Checkout;
|
||||
}
|
||||
|
||||
interface SaleorCheckoutStore {
|
||||
checkout: Checkout | null;
|
||||
checkoutToken: string | null;
|
||||
@@ -55,7 +95,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
if (checkoutToken) {
|
||||
// Try to fetch existing checkout
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
const { data } = await saleorClient.query<GetCheckoutResponse>({
|
||||
query: GET_CHECKOUT,
|
||||
variables: { token: checkoutToken },
|
||||
});
|
||||
@@ -71,7 +111,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
|
||||
// Create new checkout
|
||||
try {
|
||||
const { data } = await saleorClient.mutate({
|
||||
const { data } = await saleorClient.mutate<CheckoutCreateResponse>({
|
||||
mutation: CHECKOUT_CREATE,
|
||||
variables: {
|
||||
input: {
|
||||
@@ -109,7 +149,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
throw new Error("Failed to initialize checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
const { data } = await saleorClient.mutate<CheckoutLinesAddResponse>({
|
||||
mutation: CHECKOUT_LINES_ADD,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
@@ -123,7 +163,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
isOpen: true,
|
||||
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);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -147,7 +187,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
return;
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
const { data } = await saleorClient.mutate<CheckoutLinesUpdateResponse>({
|
||||
mutation: CHECKOUT_LINES_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
@@ -160,7 +200,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
checkout: data.checkoutLinesUpdate.checkout,
|
||||
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);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -178,7 +218,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
throw new Error("No active checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
|
||||
mutation: CHECKOUT_LINES_DELETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
@@ -191,7 +231,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
checkout: data.checkoutLinesDelete.checkout,
|
||||
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);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -209,7 +249,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
throw new Error("No active checkout");
|
||||
}
|
||||
|
||||
const { data } = await saleorClient.mutate({
|
||||
const { data } = await saleorClient.mutate<CheckoutEmailUpdateResponse>({
|
||||
mutation: CHECKOUT_EMAIL_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
@@ -222,7 +262,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
checkout: data.checkoutEmailUpdate.checkout,
|
||||
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);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -236,7 +276,7 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
if (!checkoutToken) return;
|
||||
|
||||
try {
|
||||
const { data } = await saleorClient.query({
|
||||
const { data } = await saleorClient.query<GetCheckoutResponse>({
|
||||
query: GET_CHECKOUT,
|
||||
variables: { token: checkoutToken },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user