11 Commits

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

Verified fixes with Playwright screenshots at 1280x800 and 390x844
2026-03-21 17:21:00 +02:00
Unchained
7c05bd2346 Redesign phase 1: Homepage polish and design system foundation
- Fix newsletter subscribe box centering on homepage
- Fix header overlap on product pages (pt-[72px] instead of pt-[100px])
- Add scroll-mt-[72px] for smooth scroll anchor offset
- Add HeroVideo component with video hero placeholder
- Add REDESIGN_SPECIFICATION.md with 9-phase design plan
- Clean up globals.css theme declarations and comments
- Update Header with improved sticky behavior and cart
- Update ProductDetail with better layout and spacing
- Update CartDrawer with improved slide-out cart UI
- Add English translations for updated pages
- Various CSS refinements across pages
2026-03-21 16:22:17 +02:00
Unchained
9d639fbd64 fix: Parse JSON description in NewHero component
- Import parseDescription in NewHero.tsx
- Use parseDescription for featured product description
2026-03-21 13:12:30 +02:00
Unchained
0831968881 fix: Suppress hydration warnings from browser extensions
- Add suppressHydrationWarning to html and body elements
- Prevents FoxClocks and other extensions from causing errors
- Extensions modifying DOM won't break React hydration
2026-03-21 13:09:31 +02:00
Unchained
3aaad57076 fix: Parse Saleor JSON description format to plain text
- Add parseDescription() helper to extract text from EditorJS JSON
- Update getLocalizedProduct to use parsed description
- Fix product descriptions showing raw JSON on frontend
2026-03-21 13:06:14 +02:00
Unchained
01d553bfea fix: Add error boundary to handle browser extension errors
- Create ErrorBoundary component to catch extension errors
- Ignore TronLink and other chrome-extension errors
- Prevent extension conflicts from crashing the app
2026-03-21 13:02:55 +02:00
Unchained
a47698d5ca fix(saleor): Fix remaining WooCommerce references and configuration
- Fix syntax error in Checkout.ts (extra semicolon)
- Update NewHero.tsx to use Saleor types and store
- Update page.tsx to use Saleor getProducts
- Add Saleor API domain to next.config.ts images config
2026-03-21 13:00:16 +02:00
30 changed files with 2789 additions and 1000 deletions

444
REDESIGN_SPECIFICATION.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -14,6 +14,29 @@ import {
CHECKOUT_BILLING_ADDRESS_UPDATE, CHECKOUT_BILLING_ADDRESS_UPDATE,
CHECKOUT_COMPLETE, CHECKOUT_COMPLETE,
} from "@/lib/saleor/mutations/Checkout"; } from "@/lib/saleor/mutations/Checkout";
import type { Checkout } from "@/types/saleor";
// GraphQL Response Types
interface ShippingAddressUpdateResponse {
checkoutShippingAddressUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface BillingAddressUpdateResponse {
checkoutBillingAddressUpdate?: {
checkout?: Checkout;
errors?: Array<{ message: string }>;
};
}
interface CheckoutCompleteResponse {
checkoutComplete?: {
order?: { number: string };
errors?: Array<{ message: string }>;
};
}
interface AddressForm { interface AddressForm {
firstName: string; firstName: string;
@@ -94,7 +117,7 @@ export default function CheckoutPage() {
try { try {
// Update shipping address // Update shipping address
const shippingResult = await saleorClient.mutate({ const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE, mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -105,12 +128,12 @@ export default function CheckoutPage() {
}, },
}); });
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors?.length > 0) { if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message); throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
} }
// Update billing address // Update billing address
const billingResult = await saleorClient.mutate({ const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE, mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
@@ -121,19 +144,19 @@ export default function CheckoutPage() {
}, },
}); });
if (billingResult.data?.checkoutBillingAddressUpdate?.errors?.length > 0) { if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message); throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
} }
// Complete checkout (creates order) // Complete checkout (creates order)
const completeResult = await saleorClient.mutate({ const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
mutation: CHECKOUT_COMPLETE, mutation: CHECKOUT_COMPLETE,
variables: { variables: {
checkoutId: checkout.id, checkoutId: checkout.id,
}, },
}); });
if (completeResult.data?.checkoutComplete?.errors?.length > 0) { if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
throw new Error(completeResult.data.checkoutComplete.errors[0].message); throw new Error(completeResult.data.checkoutComplete.errors[0].message);
} }
@@ -154,9 +177,10 @@ export default function CheckoutPage() {
// Order Success Page // Order Success Page
if (orderComplete) { if (orderComplete) {
return ( return (
<main className="min-h-screen"> <>
<Header /> <Header />
<section className="pt-24 pb-20 px-4"> <main className="min-h-screen">
<section className="pt-[120px] pb-20 px-4">
<div className="max-w-2xl mx-auto text-center"> <div className="max-w-2xl mx-auto text-center">
<div className="mb-6"> <div className="mb-6">
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4"> <div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
@@ -187,16 +211,19 @@ export default function CheckoutPage() {
</Link> </Link>
</div> </div>
</section> </section>
<Footer /> </main>
</main> <div className="pt-16">
<Footer />
</div>
</>
); );
} }
return ( return (
<main className="min-h-screen"> <>
<Header /> <Header />
<main className="min-h-screen">
<section className="pt-24 pb-20 px-4"> <section className="pt-[120px] pb-20 px-4">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-serif mb-8">Checkout</h1> <h1 className="text-3xl font-serif mb-8">Checkout</h1>
@@ -457,8 +484,11 @@ export default function CheckoutPage() {
</div> </div>
</div> </div>
</section> </section>
</main>
<Footer /> <div className="pt-16">
</main> <Footer />
</div>
</>
); );
} }

View File

@@ -3,6 +3,7 @@
import { useState } from "react"; import { useState } from "react";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import { Mail, MapPin, Truck, Check } from "lucide-react";
export default function ContactPage() { export default function ContactPage() {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -18,97 +19,185 @@ export default function ContactPage() {
}; };
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<main className="min-h-screen bg-white">
{/* Page Header */}
<div className="pt-[104px]">
<div className="container py-12 md:py-16">
<div className="max-w-2xl mx-auto text-center">
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Get in Touch</span>
<h1 className="text-4xl md:text-5xl font-medium tracking-tight mb-4">
Contact Us
</h1>
<p className="text-[#666666]">
Have questions? We&apos;d love to hear from you.
</p>
</div>
</div>
</div>
<section className="py-20 px-4"> {/* Contact Section */}
<div className="max-w-2xl mx-auto"> <section className="py-12 md:py-16">
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8"> <div className="container">
Contact Us <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
</h1> {/* Contact Info */}
<div>
<h2 className="text-2xl font-medium mb-6">Get in Touch</h2>
<p className="text-[#666666] mb-8 leading-relaxed">
We&apos;re here to help! Whether you have questions about our products,
need assistance with an order, or just want to say hello, we&apos;d love to hear from you.
</p>
<p className="text-foreground-muted text-center mb-12"> <div className="space-y-6">
Have questions? We'd love to hear from you. <div className="flex items-start gap-4">
</p> <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>
{submitted ? ( <div className="flex items-start gap-4">
<div className="bg-green-50 text-green-700 p-6 text-center"> <div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
<p className="text-lg">Thank you for your message!</p> <Truck className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
<p className="mt-2">We'll get back to you soon.</p> </div>
<div>
<h3 className="font-medium mb-1">Shipping</h3>
<p className="text-[#666666] text-sm">Free shipping over 3,000 RSD</p>
<p className="text-[#999999] text-xs mt-1">Delivered within 2-5 business days</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
<MapPin className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-medium mb-1">Location</h3>
<p className="text-[#666666] text-sm">Serbia</p>
<p className="text-[#999999] text-xs mt-1">Shipping nationwide</p>
</div>
</div>
</div>
</div> </div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<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> {/* Contact Form */}
<label htmlFor="email" className="block text-sm font-medium mb-2"> <div className="bg-[#f8f9fa] p-8 md:p-10">
Email {submitted ? (
</label> <div className="text-center py-12">
<input <div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
type="email" <Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
id="email" </div>
required <h3 className="text-xl font-medium mb-2">Thank You!</h3>
value={formData.email} <p className="text-[#666666]">
onChange={(e) => setFormData({ ...formData, email: e.target.value })} Your message has been sent. We&apos;ll get back to you soon.
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground" </p>
/> </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 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
placeholder="Your name"
/>
</div>
<div> <div>
<label htmlFor="message" className="block text-sm font-medium mb-2"> <label htmlFor="email" className="block text-sm font-medium mb-2">
Message Email
</label> </label>
<textarea <input
id="message" type="email"
required id="email"
rows={5} required
value={formData.message} value={formData.email}
onChange={(e) => setFormData({ ...formData, message: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none" className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
/> placeholder="your@email.com"
</div> />
</div>
<button <div>
type="submit" <label htmlFor="message" className="block text-sm font-medium mb-2">
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors" Message
> </label>
Send Message <textarea
</button> id="message"
</form> 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>
<div className="mt-16 pt-8 border-t border-border/30"> <button
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center"> type="submit"
<div> className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
<h3 className="font-serif mb-2">Email</h3> >
<p className="text-foreground-muted">hello@manoonoils.com</p> Send Message
</div> </button>
<div> </form>
<h3 className="font-serif mb-2">Shipping</h3> )}
<p className="text-foreground-muted">Free over 3000 RSD</p>
</div>
<div>
<h3 className="font-serif mb-2">Location</h3>
<p className="text-foreground-muted">Serbia</p>
</div>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<Footer /> {/* FAQ Section */}
</main> <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>
<div className="pt-16">
<Footer />
</div>
</>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,75 @@
@import "tailwindcss"; @import "tailwindcss";
:root { /* ============================================
--background: #f0f4f8; MANOONOILS DESIGN SYSTEM
--background-ice: #e8f0f5; Tailwind 4 compatible - uses CSS layers
--foreground: #1a1a1a; ============================================ */
--foreground-muted: #666666;
--accent: #a8c5d8;
--accent-dark: #7ba3bc;
--white: #ffffff;
--border: #d1d9e0;
}
@theme inline { @theme inline {
--color-background: var(--background); /* Colors - reference CSS variables */
--color-background-ice: var(--background-ice); --color-white: var(--color-white);
--color-foreground: var(--foreground); --color-background: var(--color-background);
--color-foreground-muted: var(--foreground-muted); --color-background-alt: var(--color-background-alt);
--color-accent: var(--accent); --color-foreground: var(--color-foreground);
--color-accent-dark: var(--accent-dark); --color-foreground-muted: var(--color-foreground-muted);
--color-white: var(--white); --color-foreground-subtle: var(--color-foreground-subtle);
--color-border: var(--border); --color-accent: var(--color-accent);
--font-display: var(--font-cedrat); --color-accent-dark: var(--color-accent-dark);
--font-body: var(--font-dm-sans); --color-accent-blue: var(--color-accent-blue);
--color-gold: var(--color-gold);
--color-gold-light: var(--color-gold-light);
--color-border: var(--color-border);
--color-border-dark: var(--color-border-dark);
--color-cta: var(--color-cta);
--color-cta-hover: var(--color-cta-hover);
--color-overlay: var(--color-overlay);
/* Typography */
--font-display: var(--font-display);
--font-body: var(--font-body);
} }
@font-face { /* ============================================
font-family: 'Cedrat Display'; CSS VARIABLES
src: url('https://fonts.gstatic.com/s/cedratdisplay/v16/0nkoC9_pK3CvS5lZuZ7MAUmK5w.woff2') format('woff2'); ============================================ */
font-weight: 400 900;
font-display: swap; :root {
--color-white: #ffffff;
--color-background: #fafafa;
--color-background-alt: #f5f5f5;
--color-foreground: #1a1a1a;
--color-foreground-muted: #666666;
--color-foreground-subtle: #999999;
--color-accent: #e8f0f5;
--color-accent-dark: #a8c5d8;
--color-accent-blue: #e8f0f5;
--color-gold: #c9a962;
--color-gold-light: #d4b978;
--color-border: #e5e5e5;
--color-border-dark: #d1d1d1;
--color-cta: #000000;
--color-cta-hover: #333333;
--color-overlay: rgba(0, 0, 0, 0.4);
--font-display: 'DM Sans', sans-serif;
--font-body: 'Inter', sans-serif;
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 350ms ease;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
} }
/* ============================================
FONT IMPORTS
============================================ */
@font-face { @font-face {
font-family: 'DM Sans'; font-family: 'DM Sans';
src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2'); src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2');
@@ -38,77 +77,294 @@
font-display: swap; font-display: swap;
} }
* { @font-face {
box-sizing: border-box; font-family: 'Inter';
src: url('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2') format('woff2');
font-weight: 400 700;
font-display: swap;
} }
body { /* ============================================
background: var(--background); BASE STYLES (in Tailwind base layer)
color: var(--foreground); ============================================ */
font-family: 'DM Sans', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 { @layer base {
font-family: 'Cedrat Display', serif; html {
} scroll-behavior: smooth;
/* Marquee Animations */
@keyframes marquee {
0% {
transform: translateX(0);
} }
100% {
transform: translateX(-50%); body {
background: var(--color-background);
color: var(--color-foreground);
font-family: var(--font-body);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-display);
font-weight: 500;
line-height: 1.2;
letter-spacing: -0.02em;
}
h1 {
font-size: clamp(2rem, 5vw, 3.5rem);
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
}
h3 {
font-size: clamp(1.25rem, 3vw, 1.75rem);
}
input, textarea, select {
font-family: var(--font-body);
font-size: 16px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--color-foreground);
}
:focus-visible {
outline: 2px solid var(--color-foreground);
outline-offset: 2px;
} }
} }
@keyframes marquee-slow { /* ============================================
0% { COMPONENTS
transform: translateX(0); ============================================ */
@layer components {
.container {
width: 100%;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
padding-left: 24px;
padding-right: 24px;
} }
100% {
transform: translateX(-50%); @media (min-width: 640px) {
.container {
padding-left: 32px;
padding-right: 32px;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 48px;
padding-right: 48px;
}
}
.container-narrow {
max-width: 1200px;
}
.container-wide {
max-width: 1600px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 14px 32px;
font-size: 13px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
border: none;
cursor: pointer;
transition: all var(--transition-base);
}
.btn-primary {
background: var(--color-cta);
color: var(--color-white);
}
.btn-primary:hover {
background: var(--color-cta-hover);
}
.btn-secondary {
background: transparent;
color: var(--color-foreground);
border: 1px solid var(--color-border-dark);
}
.btn-secondary:hover {
background: var(--color-foreground);
color: var(--color-white);
border-color: var(--color-foreground);
}
.link-underline {
position: relative;
text-decoration: none;
}
.link-underline::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 1px;
background: currentColor;
transition: width var(--transition-base);
}
.link-underline:hover::after {
width: 100%;
}
.text-display {
font-family: var(--font-display);
font-weight: 500;
letter-spacing: -0.02em;
}
.text-body {
font-family: var(--font-body);
}
.text-uppercase {
text-transform: uppercase;
letter-spacing: 0.05em;
}
.text-caption {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 500;
}
.text-muted {
color: var(--color-foreground-muted);
}
.text-subtle {
color: var(--color-foreground-subtle);
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
} }
} }
.animate-marquee { /* ============================================
animation: marquee 25s linear infinite; UTILITIES
============================================ */
@layer utilities {
.section {
padding-top: 96px;
padding-bottom: 96px;
}
.section-sm {
padding-top: 48px;
padding-bottom: 48px;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-background-alt);
}
::-webkit-scrollbar-thumb {
background: var(--color-border-dark);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-foreground-muted);
}
.animate-fade-in {
animation: fadeIn var(--transition-slow) forwards;
}
.animate-slide-up {
animation: slideUp var(--transition-slow) forwards;
}
.animate-slide-in-right {
animation: slideInRight var(--transition-slow) forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideInRight {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 25s linear infinite;
}
.animate-marquee-slow {
animation: marquee 35s linear infinite;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
} }
.animate-marquee-slow { /* ============================================
animation: marquee-slow 35s linear infinite; REDUCED MOTION
} ============================================ */
.animate-marquee-fast { @media (prefers-reduced-motion: reduce) {
animation: marquee 15s linear infinite; *,
} *::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
/* Utility Classes */ html {
.font-serif { scroll-behavior: auto;
font-family: 'Cedrat Display', serif; }
}
/* Smooth scroll */
html {
scroll-behavior: smooth;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
} }

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { getProducts } from "@/lib/saleor";
import Header from "@/components/layout/Header"; import Header from "@/components/layout/Header";
import Footer from "@/components/layout/Footer"; import Footer from "@/components/layout/Footer";
import ProductCard from "@/components/product/ProductCard"; import ProductCard from "@/components/product/ProductCard";
import { ChevronDown } from "lucide-react";
export const metadata = { export const metadata = {
title: "Products - ManoonOils", title: "Products - ManoonOils",
@@ -15,37 +16,91 @@ interface ProductsPageProps {
export default async function ProductsPage({ params }: ProductsPageProps) { export default async function ProductsPage({ params }: ProductsPageProps) {
const { locale = "sr" } = await params; const { locale = "sr" } = await params;
const products = await getProducts(locale.toUpperCase()); const products = await getProducts(locale.toUpperCase());
const localeUpper = locale.toUpperCase();
return ( return (
<main className="min-h-screen pt-16 md:pt-20"> <>
<Header /> <Header />
<section className="py-20 px-4"> <main className="min-h-screen bg-white">
<div className="max-w-7xl mx-auto"> {/* Page Header */}
<h1 className="text-4xl md:text-5xl font-serif text-center mb-16"> <div className="pt-[72px] lg:pt-[72px]">
{locale === "en" ? "All Products" : "Svi Proizvodi"} <div className="border-b border-[#e5e5e5]">
</h1> <div className="container py-8 md:py-12">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<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>
{products.length === 0 ? ( {/* Sort Dropdown */}
<p className="text-center text-foreground-muted"> <div className="flex items-center gap-3">
{locale === "en" ? "No products available" : "Nema dostupnih proizvoda"} <span className="text-sm text-[#666666]">
</p> {products.length} {localeUpper === "EN" ? "products" : "proizvoda"}
) : ( </span>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"> <div className="relative">
{products.map((product, index) => ( <select
<ProductCard className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
key={product.id} defaultValue="featured"
product={product} >
index={index} <option value="featured">
locale={locale.toUpperCase()} {localeUpper === "EN" ? "Featured" : "Istaknuto"}
/> </option>
))} <option value="newest">
{localeUpper === "EN" ? "Newest" : "Najnovije"}
</option>
<option value="price-low">
{localeUpper === "EN" ? "Price: Low to High" : "Cena: Rastuće"}
</option>
<option value="price-high">
{localeUpper === "EN" ? "Price: High to Low" : "Cena: Opadajuće"}
</option>
</select>
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
</div>
</div>
</div>
</div> </div>
)} </div>
</div>
</section>
<Footer /> {/* Products Grid */}
</main> <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>
</main>
<div className="pt-16">
<Footer />
</div>
</>
); );
} }

View File

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

View File

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

View File

@@ -0,0 +1,117 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
import { ChevronDown } from "lucide-react";
export default function HeroVideo() {
const scrollToContent = () => {
const element = document.getElementById("main-content");
if (element) {
element.scrollIntoView({ behavior: "smooth" });
}
};
return (
<section className="relative h-screen w-full overflow-hidden">
{/* Video Background */}
<div className="absolute inset-0">
<video
autoPlay
muted
loop
playsInline
poster="/images/hero-poster.jpg"
className="w-full h-full object-cover"
>
{/* Placeholder - Add actual video files when available */}
{/* <source src="/videos/hero.webm" type="video/webm" /> */}
{/* <source src="/videos/hero.mp4" type="video/mp4" /> */}
</video>
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-black/20 to-black/50" />
</div>
{/* Fallback Background (shown when video isn't loaded) */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop')`,
}}
>
<div className="absolute inset-0 bg-black/40" />
</div>
{/* Content */}
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center text-white px-4">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
className="max-w-4xl mx-auto"
>
{/* Tagline */}
<motion.span
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="inline-block text-xs md:text-sm uppercase tracking-[0.3em] mb-6 text-white/90"
>
Premium Organic Oils
</motion.span>
{/* Main Heading */}
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.7 }}
className="text-5xl md:text-7xl lg:text-8xl font-medium mb-6 tracking-tight"
>
ManoonOils
</motion.h1>
{/* Subtitle */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.9 }}
className="text-lg md:text-xl text-white/80 mb-10 font-light max-w-xl mx-auto"
>
For hair and skin care
</motion.p>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1.1 }}
>
<Link
href="/products"
className="inline-block px-10 py-4 bg-white text-black text-[13px] uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors duration-300"
>
Shop Now
</Link>
</motion.div>
</motion.div>
</div>
{/* Scroll Indicator */}
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 0.8 }}
onClick={scrollToContent}
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/80 hover:text-white transition-colors cursor-pointer"
aria-label="Scroll to content"
>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
>
<ChevronDown className="w-6 h-6" strokeWidth={1.5} />
</motion.div>
</motion.button>
</section>
);
}

View File

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

View File

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

View File

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

View File

@@ -3,14 +3,20 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { AnimatePresence } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore"; import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
import { User, ShoppingBag, Menu } from "lucide-react"; import { User, ShoppingBag, Menu, X } from "lucide-react";
import MobileMenu from "./MobileMenu";
import CartDrawer from "@/components/cart/CartDrawer"; import CartDrawer from "@/components/cart/CartDrawer";
const navLinks = [
{ href: "/products", label: "Products" },
{ href: "/about", label: "About" },
{ href: "/contact", label: "Contact" },
];
export default function Header() { export default function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore(); const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
const itemCount = getLineCount(); const itemCount = getLineCount();
@@ -20,81 +26,172 @@ export default function Header() {
initCheckout(); initCheckout();
}, [initCheckout]); }, [initCheckout]);
// Track scroll for header styling
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
// Lock body scroll when mobile menu is open
useEffect(() => {
if (mobileMenuOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [mobileMenuOpen]);
return ( return (
<> <>
<header className="sticky top-10 z-40 bg-white border-b border-[#1A1A1A]/[0.06]"> <header
<div className="max-w-[1400px] mx-auto px-6"> className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
<div className="flex items-center justify-between h-16"> scrolled
{/* Mobile Menu Button */} ? "bg-white/95 backdrop-blur-md shadow-sm"
: "bg-white/80 backdrop-blur-sm"
}`}
>
<div className="relative flex items-center justify-between h-[72px]">
{/* Mobile Menu Button */}
<button
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(true)}
aria-label="Open menu"
>
<Menu className="w-5 h-5" />
</button>
{/* Left side - Desktop Nav */}
<nav className="hidden lg:flex items-center gap-10">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
>
{link.label}
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
</Link>
))}
</nav>
{/* Logo - Centered (absolute on desktop, flex on mobile) */}
<Link href="/" className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
width={150}
height={40}
className="h-7 w-auto object-contain"
priority
/>
</Link>
{/* Right side - Icons */}
<div className="flex items-center gap-1">
<button <button
className="lg:hidden p-2 -ml-2" className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
onClick={() => setMobileMenuOpen(true)} aria-label="Account"
aria-label="Open menu"
> >
<Menu className="w-5 h-5" /> <User className="w-5 h-5" strokeWidth={1.5} />
</button> </button>
{/* Logo */} <button
<Link href="/" className="flex-shrink-0"> className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
<Image onClick={toggleCart}
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png" aria-label="Open cart"
alt="ManoonOils" >
width={150} <ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
height={40} {itemCount > 0 && (
className="h-8 w-auto object-contain" <span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
/> {itemCount > 99 ? "99+" : itemCount}
</Link> </span>
)}
{/* Desktop Navigation */} </button>
<nav className="hidden lg:flex items-center gap-8">
<Link
href="/products"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
Products
</Link>
<Link
href="/about"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
About
</Link>
<Link
href="/contact"
className="text-sm text-[#1A1A1A] hover:text-[#1A1A1A]/70 transition-colors"
>
Contact
</Link>
</nav>
{/* Icons */}
<div className="flex items-center gap-1">
<button
className="p-2 hidden sm:block"
aria-label="Account"
>
<User className="w-5 h-5" />
</button>
<button
className="p-2 relative"
onClick={toggleCart}
aria-label="Open cart"
>
<ShoppingBag className="w-5 h-5" />
{itemCount > 0 && (
<span className="absolute -top-0.5 -right-0.5 bg-[#1A1A1A] text-white text-[10px] w-4 h-4 rounded-full flex items-center justify-center">
{itemCount}
</span>
)}
</button>
</div>
</div> </div>
</div> </div>
</header> </header>
{/* Mobile Menu Overlay */}
<AnimatePresence> <AnimatePresence>
{mobileMenuOpen && <MobileMenu onClose={() => setMobileMenuOpen(false)} />} {mobileMenuOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-[60] bg-white"
>
<div className="container h-full flex flex-col">
{/* Mobile Header */}
<div className="flex items-center justify-between h-[72px]">
<Link href="/" onClick={() => setMobileMenuOpen(false)}>
<Image
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
alt="ManoonOils"
width={150}
height={40}
className="h-7 w-auto object-contain"
/>
</Link>
<button
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
onClick={() => setMobileMenuOpen(false)}
aria-label="Close menu"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Mobile Navigation */}
<nav className="flex-1 flex flex-col justify-center gap-8">
{navLinks.map((link, index) => (
<motion.div
key={link.href}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 + 0.1 }}
>
<Link
href={link.href}
onClick={() => setMobileMenuOpen(false)}
className="text-3xl font-medium tracking-tight hover:text-[#666666] transition-colors"
>
{link.label}
</Link>
</motion.div>
))}
</nav>
{/* Mobile Footer */}
<div className="py-8 border-t border-[#e5e5e5]">
<div className="flex items-center justify-between">
<button
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
onClick={() => {
setMobileMenuOpen(false);
toggleCart();
}}
>
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
Cart ({itemCount})
</button>
<button
className="flex items-center gap-2 text-sm text-[#666666] hover:text-black transition-colors"
>
<User className="w-5 h-5" strokeWidth={1.5} />
Account
</button>
</div>
</div>
</div>
</motion.div>
)}
</AnimatePresence> </AnimatePresence>
<CartDrawer /> <CartDrawer />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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