Compare commits
7 Commits
8a418be7c3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ace1ac104e | ||
|
|
7f603c83e9 | ||
|
|
0e9ad28dcf | ||
|
|
70d6cfc9a7 | ||
|
|
f3d60d3c5b | ||
|
|
7ecd9c2e22 | ||
|
|
e9b95c44b9 |
@@ -442,6 +442,7 @@ export default function CheckoutPage() {
|
|||||||
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
|
||||||
|
sizes="64px"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
@@ -7,9 +7,8 @@ export const metadata: Metadata = {
|
|||||||
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||||
template: "%s | ManoonOils",
|
template: "%s | ManoonOils",
|
||||||
},
|
},
|
||||||
description: "Discover our premium collection of natural oils for hair and skin care. Handmade with love.",
|
description: "Discover our premium collection of natural oils for hair and skin care.",
|
||||||
robots: "index, follow",
|
robots: "index, follow",
|
||||||
viewport: "width=device-width, initial-scale=1, maximum-scale=5",
|
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||||
description: "Discover our premium collection of natural oils for hair and skin care.",
|
description: "Discover our premium collection of natural oils for hair and skin care.",
|
||||||
@@ -18,6 +17,12 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
maximumScale: 5,
|
||||||
|
};
|
||||||
|
|
||||||
// Suppress extension-caused hydration warnings
|
// Suppress extension-caused hydration warnings
|
||||||
const suppressHydrationWarning = true;
|
const suppressHydrationWarning = true;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } 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";
|
||||||
@@ -27,11 +27,15 @@ export default function CartDrawer() {
|
|||||||
const lines = getLines();
|
const lines = getLines();
|
||||||
const total = getTotal();
|
const total = getTotal();
|
||||||
const lineCount = getLineCount();
|
const lineCount = getLineCount();
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
// Initialize checkout on mount
|
// Initialize checkout on mount (only once)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!initialized) {
|
||||||
initCheckout();
|
initCheckout();
|
||||||
}, [initCheckout]);
|
setInitialized(true);
|
||||||
|
}
|
||||||
|
}, [initialized]);
|
||||||
|
|
||||||
// Lock body scroll when cart is open
|
// Lock body scroll when cart is open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -125,6 +125,16 @@ function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function BeforeAfterGallery() {
|
export default function BeforeAfterGallery() {
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
const goToPrev = () => {
|
||||||
|
setSelectedIndex(prev => prev === 0 ? results.length - 1 : prev - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNext = () => {
|
||||||
|
setSelectedIndex(prev => prev === results.length - 1 ? 0 : prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-[#faf9f7]">
|
<section className="py-24 bg-[#faf9f7]">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
@@ -143,8 +153,8 @@ export default function BeforeAfterGallery() {
|
|||||||
</h2>
|
</h2>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Two transformations side by side */}
|
{/* Desktop: Two transformations side by side */}
|
||||||
<div className="flex gap-6 max-w-6xl mx-auto">
|
<div className="hidden md:flex gap-6 max-w-6xl mx-auto">
|
||||||
{results.map((result, index) => (
|
{results.map((result, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={result.id}
|
key={result.id}
|
||||||
@@ -159,6 +169,55 @@ export default function BeforeAfterGallery() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: Carousel with one transformation at a time */}
|
||||||
|
<div className="md:hidden relative max-w-md mx-auto">
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<motion.div
|
||||||
|
key={selectedIndex}
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<BeforeAfterSlider result={results[selectedIndex]} />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Carousel Navigation */}
|
||||||
|
<button
|
||||||
|
onClick={goToPrev}
|
||||||
|
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||||
|
aria-label="Previous transformation"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goToNext}
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center"
|
||||||
|
aria-label="Next transformation"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dot Indicators */}
|
||||||
|
<div className="flex justify-center gap-2 mt-6">
|
||||||
|
{results.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedIndex(index)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
selectedIndex === index ? "bg-black w-4" : "bg-gray-300"
|
||||||
|
}`}
|
||||||
|
aria-label={`Go to transformation ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="text-center mt-12"
|
className="text-center mt-12"
|
||||||
|
|||||||
@@ -13,26 +13,8 @@ export default function HeroVideo() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative h-screen w-full overflow-hidden">
|
<section className="relative min-h-screen w-full overflow-hidden">
|
||||||
{/* Video Background */}
|
{/* Background Image with Overlay */}
|
||||||
<div className="absolute inset-0">
|
|
||||||
<video
|
|
||||||
autoPlay
|
|
||||||
muted
|
|
||||||
loop
|
|
||||||
playsInline
|
|
||||||
poster="/images/hero-poster.jpg"
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
>
|
|
||||||
{/* Placeholder - Add actual video files when available */}
|
|
||||||
{/* <source src="/videos/hero.webm" type="video/webm" /> */}
|
|
||||||
{/* <source src="/videos/hero.mp4" type="video/mp4" /> */}
|
|
||||||
</video>
|
|
||||||
{/* Gradient Overlay */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/30 to-black/60" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Fallback Background (shown when video isn't loaded) */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
@@ -43,7 +25,7 @@ export default function HeroVideo() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative z-10 h-full flex flex-col items-center justify-center text-center text-white px-4">
|
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center text-center text-white px-4 py-20">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|||||||
@@ -27,14 +27,13 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
|
|||||||
>
|
>
|
||||||
<Link href={`/products/${localized.slug}`} className="group block">
|
<Link href={`/products/${localized.slug}`} className="group block">
|
||||||
{/* Image Container */}
|
{/* Image Container */}
|
||||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||||
{image ? (
|
{image ? (
|
||||||
<Image
|
<img
|
||||||
src={image}
|
src={image}
|
||||||
alt={localized.name}
|
alt={localized.name}
|
||||||
fill
|
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||||
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
loading="lazy"
|
||||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { ChevronDown, Star, Minus, Plus } from "lucide-react";
|
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, getLocalizedProduct } from "@/lib/saleor";
|
import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor";
|
||||||
import ProductCard from "@/components/product/ProductCard";
|
import ProductCard from "@/components/product/ProductCard";
|
||||||
import ProductBenefits from "@/components/product/ProductBenefits";
|
import ProductBenefits from "@/components/product/ProductBenefits";
|
||||||
import ProductReviews from "@/components/product/ProductReviews";
|
import ProductReviews from "@/components/product/ProductReviews";
|
||||||
@@ -91,8 +91,23 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
const [selectedImage, setSelectedImage] = useState(0);
|
const [selectedImage, setSelectedImage] = useState(0);
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [urgencyIndex, setUrgencyIndex] = useState(0);
|
||||||
const { addLine, openCart } = useSaleorCheckoutStore();
|
const { addLine, openCart } = useSaleorCheckoutStore();
|
||||||
|
|
||||||
|
// Cycle through urgency messages
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setUrgencyIndex(prev => (prev + 1) % 3);
|
||||||
|
}, 3000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const urgencyMessages = [
|
||||||
|
{ icon: "🚀", text: "Hurry up! 500+ items sold in the last 3 days!" },
|
||||||
|
{ icon: "🛒", text: "In the carts of 2.5K people - buy before its gone!" },
|
||||||
|
{ icon: "👀", text: "7,562 people viewed this product in the last 24 hours!" },
|
||||||
|
];
|
||||||
|
|
||||||
const localized = getLocalizedProduct(product, locale);
|
const localized = getLocalizedProduct(product, locale);
|
||||||
const variant = product.variants?.[0];
|
const variant = product.variants?.[0];
|
||||||
|
|
||||||
@@ -115,6 +130,8 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
|
|
||||||
const isAvailable = variant?.quantityAvailable > 0;
|
const isAvailable = variant?.quantityAvailable > 0;
|
||||||
const price = getProductPrice(product);
|
const price = getProductPrice(product);
|
||||||
|
const priceAmount = getProductPriceAmount(product);
|
||||||
|
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
|
||||||
|
|
||||||
// Extract short description (first sentence or first 100 chars)
|
// Extract short description (first sentence or first 100 chars)
|
||||||
const shortDescription = localized.description
|
const shortDescription = localized.description
|
||||||
@@ -152,7 +169,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.6 }}
|
transition={{ duration: 0.6 }}
|
||||||
className="flex gap-4"
|
className="flex flex-col md:flex-row gap-4"
|
||||||
>
|
>
|
||||||
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
@@ -167,29 +184,63 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
: "border-transparent hover:border-[#999999]"
|
: "border-transparent hover:border-[#999999]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Image
|
<img
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.alt || localized.name}
|
alt={image.alt || localized.name}
|
||||||
fill
|
className="w-full h-full object-cover"
|
||||||
className="object-cover"
|
|
||||||
sizes="80px"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Main Image */}
|
{/* Main Image */}
|
||||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden">
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
|
||||||
{images[selectedImage] && (
|
<img
|
||||||
<Image
|
|
||||||
src={images[selectedImage].url}
|
src={images[selectedImage].url}
|
||||||
alt={images[selectedImage].alt || localized.name}
|
alt={images[selectedImage].alt || localized.name}
|
||||||
fill
|
className="w-full h-full object-cover"
|
||||||
className="object-cover"
|
|
||||||
priority
|
|
||||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Carousel Navigation - Mobile Only */}
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
{/* Left Arrow */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(prev => prev === 0 ? images.length - 1 : prev - 1)}
|
||||||
|
className="absolute left-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
|
||||||
|
aria-label="Previous image"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Right Arrow */}
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedImage(prev => prev === images.length - 1 ? 0 : prev + 1)}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 w-10 h-10 bg-white/80 hover:bg-white rounded-full flex items-center justify-center shadow-md transition-all hover:scale-110 md:hidden"
|
||||||
|
aria-label="Next image"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Dot Indicators - Mobile Only */}
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 md:hidden">
|
||||||
|
{images.map((_, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => setSelectedImage(index)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
selectedImage === index ? "bg-white w-4" : "bg-white/50"
|
||||||
|
}`}
|
||||||
|
aria-label={`Go to image ${index + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -201,23 +252,64 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
transition={{ duration: 0.6, delay: 0.2 }}
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
className="lg:pl-8"
|
className="lg:pl-8"
|
||||||
>
|
>
|
||||||
|
{/* Urgency Sales Banner */}
|
||||||
|
<motion.div
|
||||||
|
key={urgencyIndex}
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
|
||||||
|
>
|
||||||
|
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||||
|
{urgencyMessages[urgencyIndex].text}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Product Name */}
|
{/* Product Name */}
|
||||||
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
||||||
{localized.name}
|
{localized.name}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Short Description */}
|
{/* Short Description */}
|
||||||
<p className="text-[#666666] leading-relaxed mb-6">
|
<p className="text-[#666666] leading-relaxed mb-4">
|
||||||
{shortDescription}
|
{shortDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Stock Warning - Static */}
|
||||||
|
<div className="flex items-center justify-start gap-2 mb-6">
|
||||||
|
<span className="relative flex h-3 w-3">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
|
||||||
|
</span>
|
||||||
|
<span className="text-red-600 text-sm font-medium">Stocks are running out!</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discount Price Display */}
|
||||||
|
{originalPrice && priceAmount > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<span className="text-xl text-[#666666] line-through">
|
||||||
|
{originalPrice}
|
||||||
|
</span>
|
||||||
|
<span className="bg-[#b91c1c] text-white text-xs font-bold px-2 py-1 rounded">
|
||||||
|
-30%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-3xl font-bold text-[#b91c1c]">
|
||||||
|
{price}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Price & Rating */}
|
{/* Price & Rating */}
|
||||||
|
{!originalPrice && (
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<span className="text-3xl font-medium">
|
<span className="text-3xl font-medium">
|
||||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||||
</span>
|
</span>
|
||||||
<StarRating rating={5} count={1000} />
|
<StarRating rating={5} count={1000} />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="border-t border-[#e5e5e5] mb-8" />
|
<div className="border-t border-[#e5e5e5] mb-8" />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export {
|
|||||||
getProducts,
|
getProducts,
|
||||||
getProductBySlug,
|
getProductBySlug,
|
||||||
getProductPrice,
|
getProductPrice,
|
||||||
|
getProductPriceAmount,
|
||||||
getProductImage,
|
getProductImage,
|
||||||
isProductAvailable,
|
isProductAvailable,
|
||||||
formatPrice,
|
formatPrice,
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export const CHECKOUT_LINES_UPDATE = gql`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CHECKOUT_LINES_DELETE = gql`
|
export const CHECKOUT_LINES_DELETE = gql`
|
||||||
mutation CheckoutLinesDelete($checkoutId: ID!, $lineIds: [ID!]!) {
|
mutation CheckoutLinesDelete($id: ID!, $linesIds: [ID!]!) {
|
||||||
checkoutLinesDelete(checkoutId: $checkoutId, lines: $lineIds) {
|
checkoutLinesDelete(id: $id, linesIds: $linesIds) {
|
||||||
checkout {
|
checkout {
|
||||||
...CheckoutFragment
|
...CheckoutFragment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export function getProductPrice(product: Product): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProductPriceAmount(product: Product): number {
|
||||||
|
const variant = product.variants?.[0];
|
||||||
|
return variant?.pricing?.price?.gross?.amount || 0;
|
||||||
|
}
|
||||||
|
|
||||||
export function getProductImage(product: Product): string {
|
export function getProductImage(product: Product): string {
|
||||||
if (product.media && product.media.length > 0) {
|
if (product.media && product.media.length > 0) {
|
||||||
return product.media[0].url;
|
return product.media[0].url;
|
||||||
@@ -88,7 +93,8 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
|
|||||||
return new Intl.NumberFormat("sr-RS", {
|
return new Intl.NumberFormat("sr-RS", {
|
||||||
style: "currency",
|
style: "currency",
|
||||||
currency: currency,
|
currency: currency,
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,8 +221,8 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
|||||||
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
|
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
|
||||||
mutation: CHECKOUT_LINES_DELETE,
|
mutation: CHECKOUT_LINES_DELETE,
|
||||||
variables: {
|
variables: {
|
||||||
checkoutId: checkout.id,
|
id: checkout.id,
|
||||||
lineIds: [lineId],
|
linesIds: [lineId],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user