Compare commits
38 Commits
7c05bd2346
...
feature/i1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92b6c830e1 | ||
|
|
5bd1a0f167 | ||
|
|
bcc51ce282 | ||
|
|
f72f32fe60 | ||
|
|
ace1ac104e | ||
|
|
7f603c83e9 | ||
|
|
0e9ad28dcf | ||
|
|
70d6cfc9a7 | ||
|
|
f3d60d3c5b | ||
|
|
7ecd9c2e22 | ||
|
|
e9b95c44b9 | ||
|
|
8a418be7c3 | ||
|
|
ba25261a3c | ||
|
|
77e19d841b | ||
|
|
43d662b54e | ||
|
|
625bd727d3 | ||
|
|
44d938953b | ||
|
|
97fc5f5f1d | ||
|
|
140d82c7f4 | ||
|
|
80a388cd7c | ||
|
|
c3bd0408f4 | ||
|
|
7618cfa6df | ||
|
|
0827147745 | ||
|
|
c5e96718a4 | ||
|
|
7febe90b36 | ||
|
|
c723d72508 | ||
|
|
bf6362d3ad | ||
|
|
9e901d7dfe | ||
|
|
0e727b2648 | ||
|
|
d6523deae5 | ||
|
|
5216abbcc0 | ||
|
|
4af5412c76 | ||
|
|
d381cba302 | ||
|
|
26212dec1c | ||
|
|
2876a8f80e | ||
|
|
93005af0a1 | ||
|
|
0b4e3f89d1 | ||
|
|
ec287c85ea |
@@ -72,21 +72,8 @@ spec:
|
||||
env:
|
||||
- name: NODE_ENV
|
||||
value: "production"
|
||||
- name: NEXT_PUBLIC_WOOCOMMERCE_URL
|
||||
valueFrom:
|
||||
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_SALEOR_API_URL
|
||||
value: "https://api.manoonoils.com/graphql/"
|
||||
- name: NEXT_PUBLIC_SITE_URL
|
||||
value: "https://dev.manoonoils.com"
|
||||
volumeMounts:
|
||||
@@ -117,21 +104,8 @@ spec:
|
||||
value: "3000"
|
||||
- name: HOSTNAME
|
||||
value: "0.0.0.0"
|
||||
- name: NEXT_PUBLIC_WOOCOMMERCE_URL
|
||||
valueFrom:
|
||||
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_SALEOR_API_URL
|
||||
value: "https://api.manoonoils.com/graphql/"
|
||||
- name: NEXT_PUBLIC_SITE_URL
|
||||
value: "https://dev.manoonoils.com"
|
||||
resources:
|
||||
|
||||
14
middleware.ts
Normal file
14
middleware.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import createMiddleware from "next-intl/middleware";
|
||||
import { routing } from "./src/i18n/routing";
|
||||
|
||||
export default createMiddleware({
|
||||
...routing,
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/",
|
||||
"/(sr|en|de|fr)/:path*",
|
||||
"/((?!api|_next|_vercel|.*\\..*).*)",
|
||||
],
|
||||
};
|
||||
128
src/app/[locale]/about/page.tsx
Normal file
128
src/app/[locale]/about/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
|
||||
interface AboutPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: AboutPageProps) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
title: locale === "sr"
|
||||
? "O nama - ManoonOils"
|
||||
: "About - ManoonOils",
|
||||
description: locale === "sr"
|
||||
? "Saznajte više o ManoonOils - naša priča, misija i posvećenost prirodnoj lepoti."
|
||||
: "Learn more about ManoonOils - our story, mission, and commitment to natural beauty.",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function AboutPage({ params }: AboutPageProps) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations("About");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
<main className="min-h-screen bg-white">
|
||||
<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">
|
||||
{t("subtitle")}
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-5xl font-medium tracking-tight">
|
||||
{t("title")}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
||||
alt={locale === "sr" ? "Proizvodnja prirodnih ulja" : "Natural oils production"}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
</div>
|
||||
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="mb-16">
|
||||
<p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
|
||||
{t("intro")}
|
||||
</p>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
{t("intro2")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">
|
||||
{t("naturalIngredients")}
|
||||
</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
{t("naturalIngredientsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">
|
||||
{t("crueltyFree")}
|
||||
</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
{t("crueltyFreeDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">
|
||||
{t("sustainablePackaging")}
|
||||
</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
{t("sustainablePackagingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">
|
||||
{t("handcraftedQuality")}
|
||||
</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
{t("handcraftedQualityDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center py-12 border-t border-b border-[#e5e5e5]">
|
||||
<span className="text-caption text-[#666666] mb-4 block">
|
||||
{t("mission")}
|
||||
</span>
|
||||
<blockquote className="text-2xl md:text-3xl font-medium tracking-tight">
|
||||
“{t("missionQuote")}”
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div className="mt-16">
|
||||
<h2 className="text-2xl font-medium mb-6">
|
||||
{t("handmadeTitle")}
|
||||
</h2>
|
||||
<p className="text-[#666666] leading-relaxed mb-6">
|
||||
{t("handmadeText1")}
|
||||
</p>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
{t("handmadeText2")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer locale={locale} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
384
src/app/[locale]/checkout/page.tsx
Normal file
384
src/app/[locale]/checkout/page.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { formatPrice } from "@/lib/saleor";
|
||||
import { saleorClient } from "@/lib/saleor/client";
|
||||
import {
|
||||
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
CHECKOUT_COMPLETE,
|
||||
} from "@/lib/saleor/mutations/Checkout";
|
||||
import type { Checkout } from "@/types/saleor";
|
||||
|
||||
interface ShippingAddressUpdateResponse {
|
||||
checkoutShippingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface BillingAddressUpdateResponse {
|
||||
checkoutBillingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutCompleteResponse {
|
||||
checkoutComplete?: {
|
||||
order?: { number: string };
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddressForm {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
streetAddress1: string;
|
||||
streetAddress2: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const t = useTranslations("Checkout");
|
||||
const locale = useLocale();
|
||||
const router = useRouter();
|
||||
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orderComplete, setOrderComplete] = useState(false);
|
||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||
|
||||
const [sameAsShipping, setSameAsShipping] = useState(true);
|
||||
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
const [billingAddress, setBillingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
|
||||
const lines = getLines();
|
||||
const total = getTotal();
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkout) {
|
||||
refreshCheckout();
|
||||
}
|
||||
}, [checkout, refreshCheckout]);
|
||||
|
||||
const handleShippingChange = (field: keyof AddressForm, value: string) => {
|
||||
setShippingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
if (sameAsShipping) {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBillingChange = (field: keyof AddressForm, value: string) => {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!checkout) {
|
||||
setError(t("errorNoCheckout"));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
shippingAddress: {
|
||||
...shippingAddress,
|
||||
country: "RS",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
|
||||
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
billingAddress: {
|
||||
...billingAddress,
|
||||
country: "RS",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
|
||||
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
|
||||
mutation: CHECKOUT_COMPLETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
|
||||
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
|
||||
}
|
||||
|
||||
const order = completeResult.data?.checkoutComplete?.order;
|
||||
if (order) {
|
||||
setOrderNumber(order.number);
|
||||
setOrderComplete(true);
|
||||
} else {
|
||||
throw new Error(t("errorCreatingOrder"));
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const errorMessage = err instanceof Error ? err.message : null;
|
||||
setError(errorMessage || t("errorOccurred"));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
<main className="min-h-screen">
|
||||
<section className="pt-[120px] pb-20 px-4">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-serif mb-2">{t("orderConfirmed")}</h1>
|
||||
<p className="text-foreground-muted">{t("thankYou")}</p>
|
||||
</div>
|
||||
|
||||
{orderNumber && (
|
||||
<div className="bg-background-ice p-6 rounded-lg mb-6">
|
||||
<p className="text-sm text-foreground-muted mb-1">{t("orderNumber")}</p>
|
||||
<p className="text-2xl font-serif">#{orderNumber}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-foreground-muted mb-8">
|
||||
{t("confirmationEmail")}
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/products`}
|
||||
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
{t("continueShoppingBtn")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer locale={locale} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen">
|
||||
<section className="pt-[120px] pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-serif mb-8">{t("checkout")}</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 p-4 mb-6 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">{t("shippingAddress")}</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("firstName")}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.firstName}
|
||||
onChange={(e) => handleShippingChange("firstName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("lastName")}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.lastName}
|
||||
onChange={(e) => handleShippingChange("lastName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">{t("streetAddress")}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.streetAddress1}
|
||||
onChange={(e) => handleShippingChange("streetAddress1", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<input
|
||||
type="text"
|
||||
value={shippingAddress.streetAddress2}
|
||||
onChange={(e) => handleShippingChange("streetAddress2", e.target.value)}
|
||||
placeholder={t("streetAddressOptional")}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("city")}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.city}
|
||||
onChange={(e) => handleShippingChange("city", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">{t("postalCode")}</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.postalCode}
|
||||
onChange={(e) => handleShippingChange("postalCode", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">{t("phone")}</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={shippingAddress.phone}
|
||||
onChange={(e) => handleShippingChange("phone", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-border pb-6">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sameAsShipping}
|
||||
onChange={(e) => setSameAsShipping(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>{t("billingAddressSame")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || lines.length === 0}
|
||||
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? t("processing") : t("completeOrder", { total: formatPrice(total) })}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="bg-background-ice p-6 rounded-lg h-fit">
|
||||
<h2 className="text-xl font-serif mb-6">{t("orderSummary")}</h2>
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<p className="text-foreground-muted">{t("yourCartEmpty")}</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4 mb-6">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex gap-4">
|
||||
<div className="w-16 h-16 bg-white relative flex-shrink-0">
|
||||
{line.variant.product.media[0]?.url && (
|
||||
<Image
|
||||
src={line.variant.product.media[0].url}
|
||||
alt={line.variant.product.name}
|
||||
fill
|
||||
sizes="64px"
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-sm">{line.variant.product.name}</h3>
|
||||
<p className="text-foreground-muted text-sm">
|
||||
{t("qty")}: {line.quantity}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{formatPrice(line.totalPrice.gross.amount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4 space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-muted">{t("subtotal")}</span>
|
||||
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
|
||||
<span>{t("total")}</span>
|
||||
<span>{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
192
src/app/[locale]/contact/page.tsx
Normal file
192
src/app/[locale]/contact/page.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { Mail, MapPin, Truck, Check } from "lucide-react";
|
||||
|
||||
export default function ContactPage() {
|
||||
const t = useTranslations("Contact");
|
||||
const locale = useLocale();
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
<main className="min-h-screen bg-white">
|
||||
<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">
|
||||
{t("subtitle")}
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-5xl font-medium tracking-tight mb-4">
|
||||
{t("title")}
|
||||
</h1>
|
||||
<p className="text-[#666666]">
|
||||
{t("getInTouchDesc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||
<div>
|
||||
<h2 className="text-2xl font-medium mb-6">
|
||||
{t("getInTouch")}
|
||||
</h2>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
{t("getInTouchDesc")}
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">{t("email")}</h3>
|
||||
<p className="text-[#666666] text-sm">hello@manoonoils.com</p>
|
||||
<p className="text-[#999999] text-xs mt-1">{t("emailReply")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Truck className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">{t("shippingTitle")}</h3>
|
||||
<p className="text-[#666666] text-sm">{t("freeShipping")}</p>
|
||||
<p className="text-[#999999] text-xs mt-1">{t("deliveryTime")}</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">{t("location")}</h3>
|
||||
<p className="text-[#666666] text-sm">{t("locationDesc")}</p>
|
||||
<p className="text-[#999999] text-xs mt-1">{t("worldwideShipping")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#f8f9fa] p-8 md:p-10">
|
||||
{submitted ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-2">{t("thankYou")}</h3>
|
||||
<p className="text-[#666666]">
|
||||
{t("thankYouDesc")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
{t("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={t("namePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||||
{t("emailField")}
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-2">
|
||||
{t("message")}
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
|
||||
placeholder={t("messagePlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{t("sendMessage")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-16 md:py-24 border-t border-[#e5e5e5]">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl font-medium text-center mb-12">
|
||||
{t("faqTitle")}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{ q: t("faq1q"), a: t("faq1a") },
|
||||
{ q: t("faq2q"), a: t("faq2a") },
|
||||
{ q: t("faq3q"), a: t("faq3a") },
|
||||
{ q: t("faq4q"), a: t("faq4a") },
|
||||
].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 locale={locale} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
25
src/app/[locale]/layout.tsx
Normal file
25
src/app/[locale]/layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||
import { routing } from "@/i18n/routing";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return routing.locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const messages = await getMessages();
|
||||
|
||||
return (
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
</NextIntlClientProvider>
|
||||
);
|
||||
}
|
||||
225
src/app/[locale]/page.tsx
Normal file
225
src/app/[locale]/page.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import HeroVideo from "@/components/home/HeroVideo";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import TrustBadges from "@/components/home/TrustBadges";
|
||||
import AsSeenIn from "@/components/home/AsSeenIn";
|
||||
import ProductReviews from "@/components/product/ProductReviews";
|
||||
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
|
||||
import ProblemSection from "@/components/home/ProblemSection";
|
||||
import HowItWorks from "@/components/home/HowItWorks";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
return {
|
||||
title: locale === "sr"
|
||||
? "ManoonOils - Premium prirodna ulja za negu kose i kože"
|
||||
: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
description: locale === "sr"
|
||||
? "Otkrijte našu premium kolekciju prirodnih ulja za negu kose i kože."
|
||||
: "Discover our premium collection of natural oils for hair and skin care.",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Homepage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations("Home");
|
||||
const tBenefits = await getTranslations("Benefits");
|
||||
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts(productLocale);
|
||||
} catch (e) {
|
||||
console.log("Failed to fetch products during build");
|
||||
}
|
||||
|
||||
const featuredProducts = products?.slice(0, 4) || [];
|
||||
const hasProducts = featuredProducts.length > 0;
|
||||
|
||||
const basePath = `/${locale}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
<HeroVideo locale={locale} />
|
||||
|
||||
<AsSeenIn />
|
||||
|
||||
<ProductReviews />
|
||||
|
||||
<TrustBadges />
|
||||
|
||||
<ProblemSection />
|
||||
|
||||
<BeforeAfterGallery />
|
||||
|
||||
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
|
||||
{hasProducts && (
|
||||
<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">
|
||||
{t("collection")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||
{t("premiumOils")}
|
||||
</h2>
|
||||
<p className="text-[#666666] max-w-xl mx-auto">
|
||||
{t("oilsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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={productLocale} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href={`${basePath}/products`}
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
{t("viewAll")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<HowItWorks />
|
||||
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
{t("ourStory")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-6">
|
||||
{t("handmadeWithLove")}
|
||||
</h2>
|
||||
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||
{t("storyText1")}
|
||||
</p>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
{t("storyText2")}
|
||||
</p>
|
||||
<a
|
||||
href={`${basePath}/about`}
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
{t("learnMore")}
|
||||
</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={locale === "sr" ? "Proizvodnja prirodnih ulja" : "Natural oils production"}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-white to-[#faf9f7]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
|
||||
{t("whyChooseUs")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium text-[#1a1a1a]">
|
||||
{t("manoonDifference")}
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-6 rounded-full" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8">
|
||||
{[
|
||||
{
|
||||
title: tBenefits("natural"),
|
||||
description: tBenefits("naturalDesc"),
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#7eb89e"/>
|
||||
<path stroke="#7eb89e" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: tBenefits("handcrafted"),
|
||||
description: tBenefits("handcraftedDesc"),
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M15.182 15.182a4.5 4.5 0 01-6.364 0M21 12a9 9 0 11-18 0 9 9 0 0118 0zM9.75 9.75c0 .414-.168.75-.375.75S9 10.164 9 9.75 9.168 9 9.375 9s.375.336.375.75zm-.375 0h.008v.015h-.008V9.75zm5.625 0c0 .414-.168.75-.375.75s-.375-.336-.375-.75.168-.75.375-.75.375.336.375.75zm-.375 0h.008v.015h-.008V9.75z"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: tBenefits("sustainable"),
|
||||
description: tBenefits("sustainableDesc"),
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path stroke="#e8967a" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M12.75 3.03v.568c0 .334.148.65.405.864l1.068.89c.442.369.535 1.01.216 1.49l-.51.766a2.25 2.25 0 01-1.161.886l-.143.048a1.107 1.107 0 00-.57 1.664c.369.555.169 1.307-.427 1.605L9 13.125l.423 1.059a.956.956 0 11-1.652.928l-.714-.093a1.125 1.125 0 00-1.906.172L4.5 15.75l-.612.153M12.75 3.031l.002-.004m0 0a8.955 8.955 0 00-4.943.834 8.974 8.974 0 004.943.834m4.943-.834a8.955 8.955 0 00-4.943-.834c2.687 0 5.18.948 7.161 2.664a8.974 8.974 0 014.943-.834z"/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
].map((benefit, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative text-center p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500 group"
|
||||
>
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-md border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
|
||||
{benefit.icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-[#1a1a1a] mb-3">{benefit.title}</h3>
|
||||
<p className="text-sm text-[#666666] leading-relaxed">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
{t("stayConnected")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">
|
||||
{t("joinCommunity")}
|
||||
</h2>
|
||||
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||
{t("newsletterText")}
|
||||
</p>
|
||||
<form className="flex flex-col sm:flex-row items-stretch justify-center max-w-md mx-auto gap-0">
|
||||
<input
|
||||
type="email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
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"
|
||||
>
|
||||
{t("subscribe")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer locale={locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
104
src/app/[locale]/products/[slug]/page.tsx
Normal file
104
src/app/[locale]/products/[slug]/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductDetail from "@/components/product/ProductDetail";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import { routing } from "@/i18n/routing";
|
||||
|
||||
interface ProductPageProps {
|
||||
params: Promise<{ locale: string; slug: string }>;
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const locales = routing.locales;
|
||||
const params: Array<{ locale: string; slug: string }> = [];
|
||||
|
||||
for (const locale of locales) {
|
||||
try {
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
const products = await getProducts(productLocale, 100);
|
||||
products.forEach((product: Product) => {
|
||||
params.push({ locale, slug: product.slug });
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps) {
|
||||
const { locale, slug } = await params;
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
const product = await getProductBySlug(slug, productLocale);
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
title: locale === "sr" ? "Proizvod nije pronađen" : "Product not found",
|
||||
};
|
||||
}
|
||||
|
||||
const localized = getLocalizedProduct(product, productLocale);
|
||||
|
||||
return {
|
||||
title: localized.name,
|
||||
description: localized.seoDescription || localized.description?.slice(0, 160),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
const { locale, slug } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations("Product");
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
const product = await getProductBySlug(slug, productLocale);
|
||||
|
||||
const basePath = locale === "sr" ? "" : `/${locale}`;
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
<main className="min-h-screen bg-white">
|
||||
<div className="pt-[180px] lg:pt-[200px] pb-20 text-center px-4">
|
||||
<h1 className="text-2xl font-medium mb-4">
|
||||
{t("notFound")}
|
||||
</h1>
|
||||
<p className="text-[#666666] mb-8">
|
||||
{t("notFoundDesc")}
|
||||
</p>
|
||||
<a
|
||||
href={`${basePath}/products`}
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{t("browseProducts")}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<Footer locale={locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
let relatedProducts: Product[] = [];
|
||||
try {
|
||||
const allProducts = await getProducts(productLocale, 8);
|
||||
relatedProducts = allProducts
|
||||
.filter((p: Product) => p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
} catch (e) {}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
<main className="min-h-screen bg-white">
|
||||
<ProductDetail
|
||||
product={product}
|
||||
relatedProducts={relatedProducts}
|
||||
locale={productLocale}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +1,64 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Products - ManoonOils",
|
||||
description: "Browse our collection of premium natural oils for hair and skin care.",
|
||||
};
|
||||
|
||||
interface ProductsPageProps {
|
||||
params: Promise<{ locale: string }>;
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductsPageProps) {
|
||||
const { locale } = await params;
|
||||
return {
|
||||
title: locale === "sr"
|
||||
? "Proizvodi - ManoonOils"
|
||||
: "Products - ManoonOils",
|
||||
description: locale === "sr"
|
||||
? "Pregledajte našu kolekciju premium prirodnih ulja za negu kose i kože."
|
||||
: "Browse our collection of premium natural oils for hair and skin care.",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
const { locale = "sr" } = await params;
|
||||
const products = await getProducts(locale.toUpperCase());
|
||||
const localeUpper = locale.toUpperCase();
|
||||
const { locale } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations("Products");
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
const products = await getProducts(productLocale);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<Header locale={locale} />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[140px] lg:pt-[160px]">
|
||||
<div className="pt-[72px] lg:pt-[72px]">
|
||||
<div className="border-b border-[#e5e5e5]">
|
||||
<div className="container py-8 md:py-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">
|
||||
{localeUpper === "EN" ? "Our Collection" : "Naša kolekcija"}
|
||||
{t("collection")}
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl font-medium">
|
||||
{localeUpper === "EN" ? "All Products" : "Svi Proizvodi"}
|
||||
{t("allProducts")}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Sort Dropdown */}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[#666666]">
|
||||
{products.length} {localeUpper === "EN" ? "products" : "proizvoda"}
|
||||
{t("productsCount", { count: products.length })}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<select
|
||||
<select
|
||||
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||
defaultValue="featured"
|
||||
>
|
||||
<option value="featured">
|
||||
{localeUpper === "EN" ? "Featured" : "Istaknuto"}
|
||||
</option>
|
||||
<option value="newest">
|
||||
{localeUpper === "EN" ? "Newest" : "Najnovije"}
|
||||
</option>
|
||||
<option value="price-low">
|
||||
{localeUpper === "EN" ? "Price: Low to High" : "Cena: Rastuće"}
|
||||
</option>
|
||||
<option value="price-high">
|
||||
{localeUpper === "EN" ? "Price: High to Low" : "Cena: Opadajuće"}
|
||||
</option>
|
||||
<option value="featured">{t("featured")}</option>
|
||||
<option value="newest">{t("newest")}</option>
|
||||
<option value="price-low">{t("priceLow")}</option>
|
||||
<option value="price-high">{t("priceHigh")}</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||
</div>
|
||||
@@ -67,28 +67,25 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-[#666666] mb-4">
|
||||
{localeUpper === "EN" ? "No products available" : "Nema dostupnih proizvoda"}
|
||||
{t("noProducts")}
|
||||
</p>
|
||||
<p className="text-sm text-[#999999]">
|
||||
{localeUpper === "EN"
|
||||
? "Please check back later for new arrivals."
|
||||
: "Molimo proverite ponovo kasnije za nove proizvode."}
|
||||
{t("checkBack")}
|
||||
</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}
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale={localeUpper}
|
||||
locale={productLocale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -97,10 +94,10 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
<Footer locale={locale} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
|
||||
export const metadata = {
|
||||
title: "About - ManoonOils",
|
||||
description: "Learn about ManoonOils - our story, mission, and commitment to natural beauty.",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
|
||||
{/* Hero Image */}
|
||||
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
||||
alt="Natural oils production"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<section className="py-16 md:py-24">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Introduction */}
|
||||
<div className="mb-16">
|
||||
<p className="text-xl md:text-2xl text-[#1a1a1a] leading-relaxed mb-8">
|
||||
ManoonOils was born from a passion for natural beauty and the belief
|
||||
that the best skincare comes from nature itself.
|
||||
</p>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
We believe in the power of natural ingredients. Every oil in our
|
||||
collection is carefully selected for its unique properties and
|
||||
benefits. From nourishing oils that restore hair vitality to serums
|
||||
that rejuvenate skin, we craft each product with love and attention
|
||||
to detail.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Values Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-16">
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Natural Ingredients</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
We use only the finest natural ingredients, sourced ethically and sustainably
|
||||
from trusted suppliers around the world.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Cruelty-Free</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
Our products are never tested on animals. We believe in beauty
|
||||
without compromise.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Sustainable Packaging</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
We use eco-friendly packaging materials and minimize waste
|
||||
throughout our production process.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 bg-[#f8f9fa]">
|
||||
<h3 className="text-lg font-medium mb-3">Handcrafted Quality</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">
|
||||
Every bottle is handcrafted in small batches to ensure
|
||||
the highest quality and freshness.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission */}
|
||||
<div className="text-center py-12 border-t border-b border-[#e5e5e5]">
|
||||
<span className="text-caption text-[#666666] mb-4 block">Our Mission</span>
|
||||
<blockquote className="text-2xl md:text-3xl font-medium tracking-tight">
|
||||
“To provide premium quality, natural products that enhance
|
||||
your daily beauty routine.”
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
{/* Story Section */}
|
||||
<div className="mt-16">
|
||||
<h2 className="text-2xl font-medium mb-6">Handmade with Love</h2>
|
||||
<p className="text-[#666666] leading-relaxed mb-6">
|
||||
Every bottle of ManoonOils is handcrafted with care. We small-batch
|
||||
produce our products to ensure the highest quality and freshness.
|
||||
When you use ManoonOils, you can feel confident that you're using
|
||||
something made with genuine care and expertise.
|
||||
</p>
|
||||
<p className="text-[#666666] leading-relaxed">
|
||||
Our journey began with a simple question: how can we create products
|
||||
that truly nurture both hair and skin? Today, we continue to innovate
|
||||
while staying true to our commitment to natural, effective beauty solutions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,494 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { formatPrice } from "@/lib/saleor";
|
||||
import { saleorClient } from "@/lib/saleor/client";
|
||||
import {
|
||||
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
CHECKOUT_COMPLETE,
|
||||
} from "@/lib/saleor/mutations/Checkout";
|
||||
import type { Checkout } from "@/types/saleor";
|
||||
|
||||
// GraphQL Response Types
|
||||
interface ShippingAddressUpdateResponse {
|
||||
checkoutShippingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface BillingAddressUpdateResponse {
|
||||
checkoutBillingAddressUpdate?: {
|
||||
checkout?: Checkout;
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CheckoutCompleteResponse {
|
||||
checkoutComplete?: {
|
||||
order?: { number: string };
|
||||
errors?: Array<{ message: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddressForm {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
streetAddress1: string;
|
||||
streetAddress2: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orderComplete, setOrderComplete] = useState(false);
|
||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||
|
||||
const [sameAsShipping, setSameAsShipping] = useState(true);
|
||||
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
const [billingAddress, setBillingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
|
||||
const lines = getLines();
|
||||
const total = getTotal();
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkout) {
|
||||
refreshCheckout();
|
||||
}
|
||||
}, [checkout, refreshCheckout]);
|
||||
|
||||
// Redirect if cart is empty
|
||||
useEffect(() => {
|
||||
if (lines.length === 0 && !orderComplete) {
|
||||
// Optionally redirect to cart or products
|
||||
// router.push("/products");
|
||||
}
|
||||
}, [lines, orderComplete, router]);
|
||||
|
||||
const handleShippingChange = (field: keyof AddressForm, value: string) => {
|
||||
setShippingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
if (sameAsShipping) {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBillingChange = (field: keyof AddressForm, value: string) => {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!checkout) {
|
||||
setError("No active checkout. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Update shipping address
|
||||
const shippingResult = await saleorClient.mutate<ShippingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
shippingAddress: {
|
||||
...shippingAddress,
|
||||
country: "RS", // Serbia
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
|
||||
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
// Update billing address
|
||||
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
|
||||
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
billingAddress: {
|
||||
...billingAddress,
|
||||
country: "RS",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
|
||||
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
// Complete checkout (creates order)
|
||||
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
|
||||
mutation: CHECKOUT_COMPLETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (completeResult.data?.checkoutComplete?.errors && completeResult.data.checkoutComplete.errors.length > 0) {
|
||||
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
|
||||
}
|
||||
|
||||
const order = completeResult.data?.checkoutComplete?.order;
|
||||
if (order) {
|
||||
setOrderNumber(order.number);
|
||||
setOrderComplete(true);
|
||||
} else {
|
||||
throw new Error("Failed to create order");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "An error occurred during checkout");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Order Success Page
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen">
|
||||
<section className="pt-[120px] pb-20 px-4">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-serif mb-2">Order Confirmed!</h1>
|
||||
<p className="text-foreground-muted">Thank you for your purchase.</p>
|
||||
</div>
|
||||
|
||||
{orderNumber && (
|
||||
<div className="bg-background-ice p-6 rounded-lg mb-6">
|
||||
<p className="text-sm text-foreground-muted mb-1">Order Number</p>
|
||||
<p className="text-2xl font-serif">#{orderNumber}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-foreground-muted mb-8">
|
||||
You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen">
|
||||
<section className="pt-[120px] pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 p-4 mb-6 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Checkout Form */}
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Shipping Address */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Shipping Address</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.firstName}
|
||||
onChange={(e) => handleShippingChange("firstName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.lastName}
|
||||
onChange={(e) => handleShippingChange("lastName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Street Address</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.streetAddress1}
|
||||
onChange={(e) => handleShippingChange("streetAddress1", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<input
|
||||
type="text"
|
||||
value={shippingAddress.streetAddress2}
|
||||
onChange={(e) => handleShippingChange("streetAddress2", e.target.value)}
|
||||
placeholder="Apartment, suite, etc. (optional)"
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">City</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.city}
|
||||
onChange={(e) => handleShippingChange("city", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Postal Code</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.postalCode}
|
||||
onChange={(e) => handleShippingChange("postalCode", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={shippingAddress.phone}
|
||||
onChange={(e) => handleShippingChange("phone", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Billing Address Toggle */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sameAsShipping}
|
||||
onChange={(e) => setSameAsShipping(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Billing address same as shipping</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Billing Address (if different) */}
|
||||
{!sameAsShipping && (
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Billing Address</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.firstName}
|
||||
onChange={(e) => handleBillingChange("firstName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.lastName}
|
||||
onChange={(e) => handleBillingChange("lastName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Street Address</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.streetAddress1}
|
||||
onChange={(e) => handleBillingChange("streetAddress1", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">City</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.city}
|
||||
onChange={(e) => handleBillingChange("city", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Postal Code</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.postalCode}
|
||||
onChange={(e) => handleBillingChange("postalCode", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={billingAddress.phone}
|
||||
onChange={(e) => handleBillingChange("phone", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Payment Method</h2>
|
||||
<div className="bg-background-ice p-4 rounded">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
checked
|
||||
readOnly
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Cash on Delivery (COD)</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-muted mt-2 ml-7">
|
||||
Pay when your order is delivered to your door.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || lines.length === 0}
|
||||
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? "Processing..." : `Complete Order - ${formatPrice(total)}`}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="bg-background-ice p-6 rounded-lg h-fit">
|
||||
<h2 className="text-xl font-serif mb-6">Order Summary</h2>
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<p className="text-foreground-muted">Your cart is empty</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4 mb-6">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex gap-4">
|
||||
<div className="w-16 h-16 bg-white relative flex-shrink-0">
|
||||
{line.variant.product.media[0]?.url && (
|
||||
<Image
|
||||
src={line.variant.product.media[0].url}
|
||||
alt={line.variant.product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-sm">{line.variant.product.name}</h3>
|
||||
<p className="text-foreground-muted text-sm">
|
||||
Qty: {line.quantity}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{formatPrice(line.totalPrice.gross.amount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4 space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-muted">Subtotal</span>
|
||||
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-muted">Shipping</span>
|
||||
<span>
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||
: "Calculated"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
|
||||
<span>Total</span>
|
||||
<span>{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { Mail, MapPin, Truck, Check } from "lucide-react";
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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'd love to hear from you.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||
{/* Contact Info */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-medium mb-6">Get in Touch</h2>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
We're here to help! Whether you have questions about our products,
|
||||
need assistance with an order, or just want to say hello, we'd love to hear from you.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Mail className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Email</h3>
|
||||
<p className="text-[#666666] text-sm">hello@manoonoils.com</p>
|
||||
<p className="text-[#999999] text-xs mt-1">We reply within 24 hours</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<Truck className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Shipping</h3>
|
||||
<p className="text-[#666666] text-sm">Free shipping over 3,000 RSD</p>
|
||||
<p className="text-[#999999] text-xs mt-1">Delivered within 2-5 business days</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-full bg-[#f8f9fa] flex items-center justify-center flex-shrink-0">
|
||||
<MapPin className="w-5 h-5 text-[#666666]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-1">Location</h3>
|
||||
<p className="text-[#666666] text-sm">Serbia</p>
|
||||
<p className="text-[#999999] text-xs mt-1">Shipping nationwide</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="bg-[#f8f9fa] p-8 md:p-10">
|
||||
{submitted ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
|
||||
<Check className="w-8 h-8 text-green-600" strokeWidth={1.5} />
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-2">Thank You!</h3>
|
||||
<p className="text-[#666666]">
|
||||
Your message has been sent. We'll get back to you soon.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-2">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-3 bg-white border border-[#e5e5e5] focus:outline-none focus:border-black transition-colors resize-none"
|
||||
placeholder="How can we help you?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-4 bg-black text-white text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Section */}
|
||||
<section className="py-16 md:py-24 border-t border-[#e5e5e5]">
|
||||
<div className="container">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h2 className="text-2xl font-medium text-center mb-12">
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
q: "How long does shipping take?",
|
||||
a: "Orders are typically delivered within 2-5 business days for domestic shipping. You'll receive a tracking number once your order ships."
|
||||
},
|
||||
{
|
||||
q: "Are your products 100% natural?",
|
||||
a: "Yes! All our oils are 100% natural, cold-pressed, and free from any additives, preservatives, or artificial fragrances."
|
||||
},
|
||||
{
|
||||
q: "What is your return policy?",
|
||||
a: "We accept returns within 14 days of delivery for unopened products. Please contact us if you have any issues with your order."
|
||||
},
|
||||
{
|
||||
q: "Do you offer wholesale?",
|
||||
a: "Yes, we offer wholesale pricing for bulk orders. Please contact us at hello@manoonoils.com for more information."
|
||||
}
|
||||
].map((faq, index) => (
|
||||
<div key={index} className="border-b border-[#e5e5e5] pb-6">
|
||||
<h3 className="font-medium mb-2">{faq.q}</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed">{faq.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// Re-export from main about page
|
||||
export { default, metadata } from "../../about/page";
|
||||
@@ -1,114 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
});
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen pt-16 md:pt-20">
|
||||
<Header />
|
||||
|
||||
<section className="py-20 px-4">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<h1 className="text-4xl md:text-5xl font-serif text-center mb-8">
|
||||
Contact Us
|
||||
</h1>
|
||||
|
||||
<p className="text-foreground-muted text-center mb-12">
|
||||
Have questions? We'd love to hear from you.
|
||||
</p>
|
||||
|
||||
{submitted ? (
|
||||
<div className="bg-green-50 text-green-700 p-6 text-center">
|
||||
<p className="text-lg">Thank you for your message!</p>
|
||||
<p className="mt-2">We'll get back to you soon.</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium mb-2">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
id="message"
|
||||
required
|
||||
rows={5}
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
|
||||
className="w-full px-4 py-3 border border-border focus:outline-none focus:border-foreground resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="mt-16 pt-8 border-t border-border/30">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Email</h3>
|
||||
<p className="text-foreground-muted">hello@manoonoils.com</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Shipping</h3>
|
||||
<p className="text-foreground-muted">Free over 3000 RSD</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-serif mb-2">Location</h3>
|
||||
<p className="text-foreground-muted">Serbia</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import HeroVideo from "@/components/home/HeroVideo";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
|
||||
export const metadata = {
|
||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
description:
|
||||
"Discover our premium collection of natural oils for hair and skin care. Handmade with love using only the finest ingredients.",
|
||||
};
|
||||
|
||||
export default async function Homepage() {
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts("EN");
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch products during build');
|
||||
}
|
||||
|
||||
const featuredProducts = products.slice(0, 4);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Hero Section with Video Background */}
|
||||
<HeroVideo />
|
||||
|
||||
{/* Main Content */}
|
||||
<div id="main-content">
|
||||
{/* Products Grid Section */}
|
||||
{featuredProducts.length > 0 && (
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="container">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Collection</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">Premium Natural Oils</h2>
|
||||
<p className="text-[#666666] max-w-xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{featuredProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} locale="EN" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* View All Link */}
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="/en/products"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
View All Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Brand Story Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||
<div className="container">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Our Story</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-6">Handmade with Love</h2>
|
||||
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||
Every bottle of ManoonOils is crafted with care using traditional
|
||||
methods passed down through generations. We source only the finest
|
||||
organic ingredients to bring you oils that nourish both hair and skin.
|
||||
</p>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
Our commitment to purity means no additives, no preservatives -
|
||||
just nature's goodness in its most potent form.
|
||||
</p>
|
||||
<a
|
||||
href="/en/about"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
||||
alt="Natural oils production"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="container">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">Why Choose Us</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">The Manoon Difference</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{[
|
||||
{
|
||||
title: "100% Natural",
|
||||
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||
},
|
||||
{
|
||||
title: "Handcrafted",
|
||||
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||
},
|
||||
{
|
||||
title: "Sustainable",
|
||||
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
|
||||
},
|
||||
].map((benefit, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
|
||||
<span className="text-2xl font-medium text-[#1a1a1a]">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
|
||||
<p className="text-[#666666]">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white mb-16">
|
||||
<div className="container">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">Stay Connected</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">Join Our Community</h2>
|
||||
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<form className="inline-flex flex-col sm:flex-row">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="w-64 sm:w-80 px-5 h-14 bg-white/10 border border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductDetail from "@/components/product/ProductDetail";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
interface ProductPageProps {
|
||||
params: Promise<{ slug: string }>;
|
||||
}
|
||||
|
||||
// Generate static params for all products
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const products = await getProducts("EN", 100);
|
||||
const params: Array<{ slug: string }> = [];
|
||||
|
||||
products.forEach((product: Product) => {
|
||||
// English slug (if translation exists)
|
||||
if (product.translation?.slug) {
|
||||
params.push({ slug: product.translation.slug });
|
||||
} else {
|
||||
params.push({ slug: product.slug });
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const product = await getProductBySlug(slug, "EN");
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
title: "Product Not Found",
|
||||
};
|
||||
}
|
||||
|
||||
const localized = getLocalizedProduct(product, "EN");
|
||||
|
||||
return {
|
||||
title: localized.name,
|
||||
description: localized.seoDescription || localized.description?.slice(0, 160),
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
const { slug } = await params;
|
||||
const product = await getProductBySlug(slug, "EN");
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<div className="pt-[120px] text-center px-4">
|
||||
<h1 className="text-2xl font-medium mb-4">Product not found</h1>
|
||||
<p className="text-[#666666] mb-8">
|
||||
The product you're looking for doesn't exist or has been removed.
|
||||
</p>
|
||||
<a
|
||||
href="/products"
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
Browse Products
|
||||
</a>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// Get related products
|
||||
let relatedProducts: Product[] = [];
|
||||
try {
|
||||
const allProducts = await getProducts("EN", 8);
|
||||
relatedProducts = allProducts
|
||||
.filter((p: Product) => p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
} catch (e) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
<Header />
|
||||
<ProductDetail
|
||||
product={product}
|
||||
relatedProducts={relatedProducts}
|
||||
locale="EN"
|
||||
/>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export const metadata = {
|
||||
title: "Products - ManoonOils",
|
||||
description: "Browse our collection of premium natural oils for hair and skin care.",
|
||||
};
|
||||
|
||||
export default async function ProductsPage() {
|
||||
const products = await getProducts("EN");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Page Header */}
|
||||
<div className="pt-[104px]">
|
||||
<div className="border-b border-[#e5e5e5]">
|
||||
<div className="container py-8 md:py-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-2 block">Our Collection</span>
|
||||
<h1 className="text-3xl md:text-4xl font-medium">All Products</h1>
|
||||
</div>
|
||||
|
||||
{/* Sort Dropdown */}
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[#666666]">{products.length} products</span>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="appearance-none bg-transparent border border-[#e5e5e5] pl-4 pr-10 py-2 text-sm focus:outline-none focus:border-black cursor-pointer"
|
||||
defaultValue="featured"
|
||||
>
|
||||
<option value="featured">Featured</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="price-low">Price: Low to High</option>
|
||||
<option value="price-high">Price: High to Low</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 pointer-events-none text-[#666666]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="py-12 md:py-16">
|
||||
<div className="container">
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-[#666666] mb-4">No products available</p>
|
||||
<p className="text-sm text-[#999999]">Please check back later for new arrivals.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{products.map((product, index) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
product={product}
|
||||
index={index}
|
||||
locale="EN"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -2,61 +2,11 @@
|
||||
|
||||
/* ============================================
|
||||
MANOONOILS DESIGN SYSTEM
|
||||
Inspired by premium skincare brands
|
||||
Tailwind 4 compatible - uses CSS layers
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--color-white: #ffffff;
|
||||
--color-background: #fafafa;
|
||||
--color-background-alt: #f5f5f5;
|
||||
--color-foreground: #1a1a1a;
|
||||
--color-foreground-muted: #666666;
|
||||
--color-foreground-subtle: #999999;
|
||||
|
||||
/* Accent Colors */
|
||||
--color-accent: #e8f0f5;
|
||||
--color-accent-dark: #a8c5d8;
|
||||
--color-accent-blue: #e8f0f5;
|
||||
--color-gold: #c9a962;
|
||||
--color-gold-light: #d4b978;
|
||||
|
||||
/* UI Colors */
|
||||
--color-border: #e5e5e5;
|
||||
--color-border-dark: #d1d1d1;
|
||||
--color-cta: #000000;
|
||||
--color-cta-hover: #333333;
|
||||
--color-overlay: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Spacing */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
--space-3xl: 64px;
|
||||
--space-4xl: 96px;
|
||||
--space-5xl: 128px;
|
||||
|
||||
/* Typography */
|
||||
--font-display: 'DM Sans', sans-serif;
|
||||
--font-body: 'Inter', sans-serif;
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 250ms ease;
|
||||
--transition-slow: 350ms ease;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* Colors - reference :root variables */
|
||||
/* Colors - reference CSS variables */
|
||||
--color-white: var(--color-white);
|
||||
--color-background: var(--color-background);
|
||||
--color-background-alt: var(--color-background-alt);
|
||||
@@ -79,6 +29,43 @@
|
||||
--font-body: var(--font-body);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CSS VARIABLES
|
||||
============================================ */
|
||||
|
||||
: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
|
||||
============================================ */
|
||||
@@ -98,307 +85,276 @@
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES
|
||||
BASE STYLES (in Tailwind base layer)
|
||||
============================================ */
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
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;
|
||||
@layer base {
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
TYPOGRAPHY
|
||||
COMPONENTS
|
||||
============================================ */
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.text-display {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.text-caption {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--color-foreground-muted);
|
||||
}
|
||||
|
||||
.text-subtle {
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITY CLASSES
|
||||
============================================ */
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@layer components {
|
||||
.container {
|
||||
padding-left: 32px;
|
||||
padding-right: 32px;
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.container {
|
||||
padding-left: 48px;
|
||||
padding-right: 48px;
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.container-narrow {
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.container-wide {
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
/* Section spacing */
|
||||
.section {
|
||||
padding-top: var(--space-4xl);
|
||||
padding-bottom: var(--space-4xl);
|
||||
}
|
||||
|
||||
.section-sm {
|
||||
padding-top: var(--space-2xl);
|
||||
padding-bottom: var(--space-2xl);
|
||||
}
|
||||
|
||||
/* Flex utilities */
|
||||
.flex-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
INTERACTIVE ELEMENTS
|
||||
UTILITIES
|
||||
============================================ */
|
||||
|
||||
/* Button Base */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 32px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-base);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--color-cta);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--color-cta-hover);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-foreground);
|
||||
border: 1px solid var(--color-border-dark);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--color-foreground);
|
||||
color: var(--color-white);
|
||||
border-color: var(--color-foreground);
|
||||
}
|
||||
|
||||
/* Link underline animation */
|
||||
.link-underline {
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link-underline::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 1px;
|
||||
background: currentColor;
|
||||
transition: width var(--transition-base);
|
||||
}
|
||||
|
||||
.link-underline:hover::after {
|
||||
width: 100%;
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORM ELEMENTS
|
||||
REDUCED MOTION
|
||||
============================================ */
|
||||
|
||||
input, textarea, select {
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-foreground);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SCROLLBAR
|
||||
============================================ */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-background-alt);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-dark);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-foreground-muted);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ANIMATIONS
|
||||
============================================ */
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn var(--transition-slow) forwards;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp var(--transition-slow) forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight var(--transition-slow) forwards;
|
||||
}
|
||||
|
||||
/* Marquee Animations */
|
||||
@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;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ACCESSIBILITY
|
||||
============================================ */
|
||||
|
||||
/* Focus visible styles */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-foreground);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
@@ -412,16 +368,3 @@ input:focus, textarea:focus, select:focus {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Screen reader only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "./globals.css";
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -7,7 +7,7 @@ export const metadata: Metadata = {
|
||||
default: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
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",
|
||||
openGraph: {
|
||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
@@ -17,16 +17,19 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
// Suppress extension-caused hydration warnings
|
||||
const suppressHydrationWarning = true;
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html suppressHydrationWarning>
|
||||
<body className="antialiased" suppressHydrationWarning>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
@@ -34,4 +37,4 @@ export default function RootLayout({
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
190
src/app/page.tsx
190
src/app/page.tsx
@@ -1,187 +1,5 @@
|
||||
import { getProducts } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import HeroVideo from "@/components/home/HeroVideo";
|
||||
import ProductCard from "@/components/product/ProductCard";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const metadata = {
|
||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||
description:
|
||||
"Discover our premium collection of natural oils for hair and skin care. Handmade with love using only the finest ingredients.",
|
||||
};
|
||||
|
||||
export default async function Homepage() {
|
||||
let products: any[] = [];
|
||||
try {
|
||||
products = await getProducts("SR");
|
||||
} catch (e) {
|
||||
// Fallback for build time when API is unavailable
|
||||
console.log('Failed to fetch products during build');
|
||||
}
|
||||
|
||||
const featuredProducts = products?.slice(0, 4) || [];
|
||||
const hasProducts = featuredProducts.length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
{/* Hero Section with Video Background */}
|
||||
<HeroVideo />
|
||||
|
||||
{/* Main Content */}
|
||||
<div id="main-content" className="scroll-mt-[72px] lg:scroll-mt-[72px]">
|
||||
{/* Products Grid Section */}
|
||||
{hasProducts && (
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Our Collection
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||
Premium Natural Oils
|
||||
</h2>
|
||||
<p className="text-[#666666] max-w-xl mx-auto">
|
||||
Cold-pressed, pure, and natural oils for your daily beauty routine
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Products Grid */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
{featuredProducts.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} locale="SR" />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* View All Link */}
|
||||
<div className="text-center mt-12">
|
||||
<a
|
||||
href="/products"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
View All Products
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Brand Story Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-[#f8f9fa]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Our Story
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-6">
|
||||
Handmade with Love
|
||||
</h2>
|
||||
<p className="text-[#666666] mb-6 leading-relaxed">
|
||||
Every bottle of ManoonOils is crafted with care using traditional
|
||||
methods passed down through generations. We source only the finest
|
||||
organic ingredients to bring you oils that nourish both hair and skin.
|
||||
</p>
|
||||
<p className="text-[#666666] mb-8 leading-relaxed">
|
||||
Our commitment to purity means no additives, no preservatives -
|
||||
just nature's goodness in its most potent form.
|
||||
</p>
|
||||
<a
|
||||
href="/about"
|
||||
className="inline-block text-sm uppercase tracking-[0.1em] border-b border-black pb-1 hover:text-[#666666] hover:border-[#666666] transition-colors"
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
||||
alt="Natural oils production"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Benefits Section */}
|
||||
<section className="py-24 px-4 sm:px-6 lg:px-8 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
Why Choose Us
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
The Manoon Difference
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-12">
|
||||
{[
|
||||
{
|
||||
title: "100% Natural",
|
||||
description: "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||
},
|
||||
{
|
||||
title: "Handcrafted",
|
||||
description: "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||
},
|
||||
{
|
||||
title: "Sustainable",
|
||||
description: "Ethically sourced ingredients and eco-friendly packaging for a better planet.",
|
||||
},
|
||||
].map((benefit, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-[#e8f0f5] flex items-center justify-center">
|
||||
<span className="text-2xl font-medium text-[#1a1a1a]">
|
||||
{String(index + 1).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-medium mb-3">{benefit.title}</h3>
|
||||
<p className="text-[#666666]">{benefit.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Newsletter Section */}
|
||||
<section className="py-28 lg:py-32 px-4 sm:px-6 lg:px-8 bg-[#1a1a1a] text-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-white/60 mb-3 block">
|
||||
Stay Connected
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6">
|
||||
Join Our Community
|
||||
</h2>
|
||||
<p className="text-white/70 mb-10 mx-auto text-lg">
|
||||
Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.
|
||||
</p>
|
||||
{/* Newsletter Form - Centered */}
|
||||
<form className="flex flex-col sm:flex-row items-center justify-center w-full sm:w-auto gap-0">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Enter your email"
|
||||
className="w-full sm:w-64 md:w-80 px-5 h-14 bg-white/10 border border-white/20 border-b-0 sm:border-b border-r-0 sm:border-r border-white/20 text-white placeholder:text-white/50 focus:border-white focus:outline-none transition-colors text-base text-center sm:text-left"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full sm:w-auto px-8 h-14 bg-white text-black text-sm uppercase tracking-[0.1em] font-medium hover:bg-white/90 transition-colors whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default function RootPage() {
|
||||
redirect("/sr");
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { getProductBySlug, getProducts, getLocalizedProduct } from "@/lib/saleor";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import ProductDetail from "@/components/product/ProductDetail";
|
||||
import type { Product } from "@/types/saleor";
|
||||
|
||||
interface ProductPageProps {
|
||||
params: Promise<{ slug: string; locale?: string }>;
|
||||
}
|
||||
|
||||
// Generate static params for all products
|
||||
export async function generateStaticParams() {
|
||||
try {
|
||||
const products = await getProducts("SR", 100);
|
||||
const params: Array<{ slug: string; locale: string }> = [];
|
||||
|
||||
products.forEach((product: Product) => {
|
||||
// Serbian slug
|
||||
params.push({ slug: product.slug, locale: "sr" });
|
||||
|
||||
// English slug (if translation exists)
|
||||
if (product.translation?.slug) {
|
||||
params.push({ slug: product.translation.slug, locale: "en" });
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: ProductPageProps) {
|
||||
const { slug, locale = "sr" } = await params;
|
||||
const product = await getProductBySlug(slug, locale.toUpperCase());
|
||||
|
||||
if (!product) {
|
||||
return {
|
||||
title: locale === "en" ? "Product Not Found" : "Proizvod nije pronađen",
|
||||
};
|
||||
}
|
||||
|
||||
const localized = getLocalizedProduct(product, locale.toUpperCase());
|
||||
|
||||
return {
|
||||
title: localized.name,
|
||||
description: localized.seoDescription || localized.description?.slice(0, 160),
|
||||
alternates: {
|
||||
canonical: `/products/${product.slug}`,
|
||||
languages: {
|
||||
"sr": `/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',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: ProductPageProps) {
|
||||
const { slug, locale = "sr" } = await params;
|
||||
const product = await getProductBySlug(slug, locale.toUpperCase());
|
||||
|
||||
if (!product) {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-white">
|
||||
<div className="pt-[180px] lg:pt-[200px] pb-20 text-center px-4">
|
||||
<h1 className="text-2xl font-medium mb-4">
|
||||
{locale === "en" ? "Product not found" : "Proizvod nije pronađen"}
|
||||
</h1>
|
||||
<p className="text-[#666666] mb-8">
|
||||
{locale === "en"
|
||||
? "The product you're looking for doesn't exist or has been removed."
|
||||
: "Proizvod koji tražite ne postoji ili je uklonjen."}
|
||||
</p>
|
||||
<a
|
||||
href="/products"
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{locale === "en" ? "Browse Products" : "Pregledaj proizvode"}
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine language based on which slug matched
|
||||
const isEnglishSlug = slug === product.translation?.slug;
|
||||
const currentLocale = isEnglishSlug ? "EN" : "SR";
|
||||
|
||||
// Get related products (same category or just other products)
|
||||
let relatedProducts: Product[] = [];
|
||||
try {
|
||||
const allProducts = await getProducts(currentLocale, 8);
|
||||
relatedProducts = allProducts
|
||||
.filter((p: Product) => p.id !== product.id)
|
||||
.slice(0, 4);
|
||||
} catch (e) {
|
||||
// Ignore error, just won't show related products
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-white">
|
||||
<ProductDetail
|
||||
product={product}
|
||||
relatedProducts={relatedProducts}
|
||||
locale={currentLocale}
|
||||
/>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { X, Minus, Plus, Trash2, ShoppingBag } from "lucide-react";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { formatPrice } from "@/lib/saleor";
|
||||
|
||||
export default function CartDrawer() {
|
||||
const {
|
||||
checkout,
|
||||
isOpen,
|
||||
const t = useTranslations("Cart");
|
||||
const locale = useLocale();
|
||||
const {
|
||||
checkout,
|
||||
isOpen,
|
||||
isLoading,
|
||||
error,
|
||||
closeCart,
|
||||
removeLine,
|
||||
updateLine,
|
||||
getTotal,
|
||||
closeCart,
|
||||
removeLine,
|
||||
updateLine,
|
||||
getTotal,
|
||||
getLineCount,
|
||||
getLines,
|
||||
initCheckout,
|
||||
@@ -27,13 +30,15 @@ export default function CartDrawer() {
|
||||
const lines = getLines();
|
||||
const total = getTotal();
|
||||
const lineCount = getLineCount();
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// Initialize checkout on mount
|
||||
useEffect(() => {
|
||||
initCheckout();
|
||||
}, [initCheckout]);
|
||||
if (!initialized) {
|
||||
initCheckout();
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [initialized]);
|
||||
|
||||
// Lock body scroll when cart is open
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
@@ -49,7 +54,6 @@ export default function CartDrawer() {
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50"
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -57,8 +61,7 @@ export default function CartDrawer() {
|
||||
exit={{ opacity: 0 }}
|
||||
onClick={closeCart}
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
|
||||
<motion.div
|
||||
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%" }}
|
||||
@@ -66,21 +69,19 @@ export default function CartDrawer() {
|
||||
exit={{ x: "100%" }}
|
||||
transition={{ type: "tween", duration: 0.3, ease: [0.4, 0, 0.2, 1] }}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-[#e5e5e5]">
|
||||
<h2 className="text-sm uppercase tracking-[0.1em] font-medium">
|
||||
Your Cart ({lineCount})
|
||||
{t("yourCart")} ({lineCount})
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeCart}
|
||||
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
aria-label="Close cart"
|
||||
aria-label={t("closeCart")}
|
||||
>
|
||||
<X className="w-5 h-5" strokeWidth={1.5} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.div
|
||||
@@ -91,41 +92,39 @@ export default function CartDrawer() {
|
||||
>
|
||||
<div className="p-4 bg-red-50 border-b border-red-100">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
<button
|
||||
<button
|
||||
onClick={clearError}
|
||||
className="text-red-600 text-xs underline mt-1 hover:no-underline"
|
||||
>
|
||||
Dismiss
|
||||
{t("dismiss")}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{lines.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-full px-6">
|
||||
<div className="w-16 h-16 rounded-full bg-[#f8f9fa] flex items-center justify-center mb-6">
|
||||
<ShoppingBag className="w-8 h-8 text-[#999999]" strokeWidth={1.5} />
|
||||
</div>
|
||||
<p className="text-[#666666] mb-2">Your cart is empty</p>
|
||||
<p className="text-[#666666] mb-2">{t("yourCartEmpty")}</p>
|
||||
<p className="text-sm text-[#999999] mb-8 text-center">
|
||||
Looks like you haven't added anything to your cart yet.
|
||||
{t("looksLikeEmpty")}
|
||||
</p>
|
||||
<Link
|
||||
href="/products"
|
||||
href={`/${locale}/products`}
|
||||
onClick={closeCart}
|
||||
className="inline-block px-8 py-3 bg-black text-white text-sm uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
Start Shopping
|
||||
{t("startShopping")}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-6 space-y-6">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex gap-4">
|
||||
{/* Product Image */}
|
||||
<div className="w-24 h-24 bg-[#f8f9fa] relative flex-shrink-0 overflow-hidden">
|
||||
{line.variant.product.media[0]?.url ? (
|
||||
<Image
|
||||
@@ -141,8 +140,7 @@ export default function CartDrawer() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-medium truncate">
|
||||
{line.variant.product.name}
|
||||
@@ -158,8 +156,7 @@ export default function CartDrawer() {
|
||||
line.variant.pricing?.price?.gross?.currency
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Quantity Controls */}
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<div className="flex items-center border border-[#e5e5e5]">
|
||||
<button
|
||||
@@ -180,13 +177,12 @@ export default function CartDrawer() {
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Remove Button */}
|
||||
|
||||
<button
|
||||
onClick={() => removeLine(line.id)}
|
||||
disabled={isLoading}
|
||||
className="p-2 text-[#999999] hover:text-red-500 transition-colors"
|
||||
aria-label="Remove item"
|
||||
aria-label={t("removeItem")}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" strokeWidth={1.5} />
|
||||
</button>
|
||||
@@ -198,65 +194,56 @@ export default function CartDrawer() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer with Checkout */}
|
||||
{lines.length > 0 && (
|
||||
<div className="border-t border-[#e5e5e5] bg-white">
|
||||
{/* Order Summary */}
|
||||
<div className="p-6 space-y-3">
|
||||
{/* Subtotal */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-[#666666]">Subtotal</span>
|
||||
<span className="text-[#666666]">{t("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]">{t("shipping")}</span>
|
||||
<span className="text-[#666666]">
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||
: "Calculated at checkout"
|
||||
: t("calculatedAtCheckout")
|
||||
}
|
||||
</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-sm uppercase tracking-[0.05em] font-medium">{t("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)}
|
||||
{t("freeShippingOver", { amount: formatPrice(5000) })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
<div className="px-6 pb-6 space-y-3">
|
||||
{/* Checkout Button */}
|
||||
<Link
|
||||
href="/checkout"
|
||||
href={`/${locale}/checkout`}
|
||||
onClick={closeCart}
|
||||
className="block w-full py-4 bg-black text-white text-center text-sm uppercase tracking-[0.1em] font-medium hover:bg-[#333333] transition-colors"
|
||||
>
|
||||
{isLoading ? "Processing..." : "Checkout"}
|
||||
{isLoading ? t("processing") : t("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
|
||||
{t("continueShopping")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,4 +253,4 @@ export default function CartDrawer() {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
}
|
||||
84
src/components/home/AsSeenIn.tsx
Normal file
84
src/components/home/AsSeenIn.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const mediaLogos = [
|
||||
{ name: "VOGUE", style: "serif" },
|
||||
{ name: "Allure", style: "sans" },
|
||||
{ name: "ELLE", style: "serif" },
|
||||
{ name: "COSMOPOLITAN", style: "serif" },
|
||||
{ name: "Bazaar", style: "serif" },
|
||||
{ name: "GLAMOUR", style: "serif" },
|
||||
{ name: "WOMEN'S HEALTH", style: "sans" },
|
||||
{ name: "Shape", style: "sans" },
|
||||
];
|
||||
|
||||
function LogoItem({ name }: { name: string }) {
|
||||
const isSerif = name === "VOGUE" || name === "ELLE" || name === "COSMOPOLITAN" || name === "Bazaar" || name === "GLAMOUR";
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center px-10 py-4 grayscale opacity-40 hover:grayscale-0 hover:opacity-100 transition-all duration-500 flex-shrink-0">
|
||||
<span
|
||||
className={`
|
||||
text-xl md:text-2xl tracking-[0.15em] text-white font-bold
|
||||
${isSerif ? 'font-serif italic' : 'font-sans uppercase'}
|
||||
`}
|
||||
style={{
|
||||
textShadow: '0 0 20px rgba(255,255,255,0.1)',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AsSeenIn() {
|
||||
const t = useTranslations("AsSeenIn");
|
||||
|
||||
return (
|
||||
<section className="py-12 bg-[#1a1a1a] overflow-hidden border-y border-white/10">
|
||||
<div className="container mx-auto px-4 mb-8">
|
||||
<motion.p
|
||||
className="text-center text-[10px] uppercase tracking-[0.4em] text-[#c9a962] font-bold"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
{t("title")}
|
||||
</motion.p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-32 bg-gradient-to-r from-[#1a1a1a] to-transparent z-10 pointer-events-none" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-[#1a1a1a] to-transparent z-10 pointer-events-none" />
|
||||
|
||||
<div className="flex overflow-hidden">
|
||||
<motion.div
|
||||
className="flex items-center gap-16"
|
||||
animate={{
|
||||
x: [0, -50 + "%"],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 30,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{mediaLogos.map((logo, index) => (
|
||||
<LogoItem key={`first-${index}`} name={logo.name} />
|
||||
))}
|
||||
{mediaLogos.map((logo, index) => (
|
||||
<LogoItem key={`second-${index}`} name={logo.name} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
231
src/components/home/BeforeAfterGallery.tsx
Normal file
231
src/components/home/BeforeAfterGallery.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useRef } from "react";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
|
||||
const results = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Facial Skin Transformation",
|
||||
beforeImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_2.webp",
|
||||
afterImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_2_1.webp",
|
||||
timeline: "4-6 Weeks",
|
||||
rating: 5,
|
||||
reviewCount: 2847,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Skin Radiance Transformation",
|
||||
beforeImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_3.webp",
|
||||
afterImg: "https://minio-api.nodecrew.me/saleor/marketing/use_case_3_1.webp",
|
||||
timeline: "6-8 Weeks",
|
||||
rating: 5,
|
||||
reviewCount: 1856,
|
||||
},
|
||||
];
|
||||
|
||||
function BeforeAfterSlider({ result }: { result: typeof results[0] }) {
|
||||
const t = useTranslations("BeforeAfterGallery");
|
||||
const [sliderPosition, setSliderPosition] = useState(50);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width) * 100;
|
||||
setSliderPosition(Math.max(0, Math.min(100, x)));
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = ((e.touches[0].clientX - rect.left) / rect.width) * 100;
|
||||
setSliderPosition(Math.max(0, Math.min(100, x)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative aspect-[4/3] rounded-2xl overflow-hidden shadow-2xl cursor-ew-resize select-none"
|
||||
onMouseMove={handleMouseMove}
|
||||
onTouchMove={handleTouchMove}
|
||||
>
|
||||
<img
|
||||
src={result.afterImg}
|
||||
alt="After"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{ width: `${sliderPosition}%` }}
|
||||
>
|
||||
<img
|
||||
src={result.beforeImg}
|
||||
alt="Before"
|
||||
className="absolute inset-0 h-full object-cover"
|
||||
style={{ width: `${100 / (sliderPosition / 100)}%`, maxWidth: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-1 bg-white shadow-lg cursor-ew-resize"
|
||||
style={{ left: `${sliderPosition}%`, transform: 'translateX(-50%)' }}
|
||||
>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10 h-10 bg-white rounded-full shadow-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l4-4 4 4m0 6l-4 4-4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-3 left-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
|
||||
{t("before")}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 bg-black/70 text-white px-3 py-1.5 rounded-full text-xs font-medium backdrop-blur-sm">
|
||||
{t("after")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 mt-4">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-xs font-medium">{result.timeline}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-xs text-[#666666]">({result.reviewCount.toLocaleString()})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-1.5 mt-2">
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span className="text-xs text-green-700 font-medium">{t("verified")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BeforeAfterGallery() {
|
||||
const t = useTranslations("BeforeAfterGallery");
|
||||
const locale = useLocale();
|
||||
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 (
|
||||
<section className="py-24 bg-[#faf9f7]">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
{t("realResults")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium mb-4">
|
||||
{t("seeTransformation")}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="hidden md:flex gap-6 max-w-6xl mx-auto">
|
||||
{results.map((result, index) => (
|
||||
<motion.div
|
||||
key={result.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className="flex-1 min-w-0"
|
||||
>
|
||||
<BeforeAfterSlider result={result} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<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>
|
||||
|
||||
<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 ${index + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="text-center mt-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
<a
|
||||
href={`/${locale}/products`}
|
||||
className="inline-block px-10 py-4 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333] transition-colors"
|
||||
>
|
||||
{t("startTransformation")}
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useLocale } from "next-intl";
|
||||
|
||||
export default function Hero() {
|
||||
const locale = useLocale();
|
||||
return (
|
||||
<section className="relative h-screen min-h-[600px] flex items-center justify-center overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-background-ice/50 to-background" />
|
||||
@@ -48,7 +50,7 @@ export default function Hero() {
|
||||
transition={{ duration: 0.8, delay: 0.8 }}
|
||||
>
|
||||
<Link
|
||||
href="/en/products"
|
||||
href={`/${locale}/products`}
|
||||
className="inline-block px-10 py-4 bg-foreground text-white text-lg tracking-wide hover:bg-accent-dark transition-colors duration-300"
|
||||
>
|
||||
Shop Now
|
||||
|
||||
@@ -2,9 +2,17 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
export default function HeroVideo() {
|
||||
interface HeroVideoProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
const t = useTranslations("Home.hero");
|
||||
const localePath = `/${locale}`;
|
||||
|
||||
const scrollToContent = () => {
|
||||
const element = document.getElementById("main-content");
|
||||
if (element) {
|
||||
@@ -13,85 +21,112 @@ export default function HeroVideo() {
|
||||
};
|
||||
|
||||
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) */}
|
||||
<section className="relative min-h-screen w-full overflow-hidden">
|
||||
{/* Background Image with Overlay */}
|
||||
<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 className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" />
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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 */}
|
||||
{/* Social Proof Micro */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 1.1 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex items-center justify-center gap-2 mb-6"
|
||||
>
|
||||
<div className="flex">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="text-sm text-white/80">
|
||||
{t("lovedBy")}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Heading - Outcome Focused */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight"
|
||||
>
|
||||
{t("transformHeadline")}
|
||||
<br />
|
||||
<span className="text-white/90">{t("withNaturalOils")}</span>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subtitle - Expands on how */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.7 }}
|
||||
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed"
|
||||
>
|
||||
{t("subtitleText")}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Button - Action verb + value */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.9 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4"
|
||||
>
|
||||
<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"
|
||||
href={`${localePath}/products`}
|
||||
className="inline-block px-10 py-4 bg-white text-black text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-white/90 transition-all duration-300 hover:scale-105 shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Shop Now
|
||||
{t("ctaButton")}
|
||||
</Link>
|
||||
<Link
|
||||
href={`${localePath}/about`}
|
||||
className="inline-block px-10 py-4 border border-white/50 text-white text-[13px] uppercase tracking-[0.15em] font-medium hover:bg-white/10 transition-all duration-300"
|
||||
>
|
||||
{t("learnStory")}
|
||||
</Link>
|
||||
</motion.div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.8 }}
|
||||
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span>{t("moneyBack")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||
</svg>
|
||||
<span>{t("freeShipping")}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
<span>{t("crueltyFree")}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
@@ -102,7 +137,7 @@ export default function HeroVideo() {
|
||||
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"
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer"
|
||||
aria-label="Scroll to content"
|
||||
>
|
||||
<motion.div
|
||||
|
||||
109
src/components/home/HowItWorks.tsx
Normal file
109
src/components/home/HowItWorks.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations, useLocale } from "next-intl";
|
||||
|
||||
export default function HowItWorks() {
|
||||
const t = useTranslations("HowItWorks");
|
||||
const locale = useLocale();
|
||||
const steps = t.raw("steps") as Array<{ title: string; description: string }>;
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-gradient-to-b from-white to-[#faf9f7]">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-20"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
|
||||
{t("title")}
|
||||
</span>
|
||||
<h2 className="text-4xl md:text-5xl font-medium text-[#1a1a1a]">
|
||||
{t("subtitle")}
|
||||
</h2>
|
||||
<div className="w-24 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-6 rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 lg:gap-16 max-w-6xl mx-auto">
|
||||
{steps.map((step, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="relative text-center group"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.15 }}
|
||||
>
|
||||
{index < steps.length - 1 && (
|
||||
<div className="hidden md:block absolute top-16 left-[55%] w-[90%] h-[2px]">
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-[#c9a962]/40 to-transparent rounded-full" />
|
||||
<motion.div
|
||||
className="absolute inset-y-0 left-0 w-2 bg-[#FFD700] rounded-full"
|
||||
initial={{ scaleX: 0 }}
|
||||
whileInView={{ scaleX: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.8, delay: 0.5 + index * 0.2 }}
|
||||
style={{ originX: 0 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500">
|
||||
<div className="absolute -top-5 left-1/2 -translate-x-1/2">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#c9a962] to-[#FFD700] flex items-center justify-center shadow-lg">
|
||||
<span className="text-white text-lg font-bold">0{index + 1}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-20 h-20 mx-auto mt-4 mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
|
||||
{index === 0 && (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007z" />
|
||||
</svg>
|
||||
)}
|
||||
{index === 1 && (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{index === 2 && (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="#FFD700">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-semibold text-[#1a1a1a] mb-3">{step.title}</h3>
|
||||
<p className="text-[#666666] text-sm leading-relaxed max-w-xs mx-auto">
|
||||
{step.description}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
className="text-center mt-20"
|
||||
initial={{ opacity: 0 }}
|
||||
whileInView={{ opacity: 1 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
<a
|
||||
href={`/${locale}/products`}
|
||||
className="group relative inline-flex items-center gap-3 px-12 py-5 bg-gradient-to-r from-[#1a1a1a] to-[#333333] text-white text-[13px] uppercase tracking-[0.2em] font-semibold hover:from-[#c9a962] hover:to-[#FFD700] transition-all duration-500 rounded-full shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<span>{t("startTransformation")}</span>
|
||||
<svg className="w-4 h-4 group-hover:translate-x-1 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.25 8.25L21 12m0 0l-3.75 3.75M21 12H3" />
|
||||
</svg>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { motion } from "framer-motion";
|
||||
import { Star, ShoppingBag } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useLocale } from "next-intl";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import { getProductPrice, getProductImage, formatPrice, parseDescription } from "@/lib/saleor";
|
||||
@@ -13,6 +14,7 @@ interface NewHeroProps {
|
||||
}
|
||||
|
||||
export default function NewHero({ featuredProduct }: NewHeroProps) {
|
||||
const locale = useLocale();
|
||||
const { addLine, openCart } = useSaleorCheckoutStore();
|
||||
|
||||
const handleAddToCart = async () => {
|
||||
@@ -150,13 +152,13 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<Link
|
||||
href="/products"
|
||||
href={`/${locale}/products`}
|
||||
className="inline-block bg-[#1A1A1A] text-white px-8 py-4 text-sm tracking-wide hover:bg-[#1A1A1A]/90 transition-colors"
|
||||
>
|
||||
Shop Collection
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
href={`/${locale}/about`}
|
||||
className="inline-block border border-[#1A1A1A] text-[#1A1A1A] px-8 py-4 text-sm tracking-wide hover:bg-[#1A1A1A] hover:text-white transition-colors"
|
||||
>
|
||||
Our Story
|
||||
@@ -168,7 +170,7 @@ export default function NewHero({ featuredProduct }: NewHeroProps) {
|
||||
{/* Mobile CTA */}
|
||||
<div className="lg:hidden relative z-10 px-6 pb-12">
|
||||
<Link
|
||||
href="/products"
|
||||
href={`/${locale}/products`}
|
||||
className="block w-full bg-[#1A1A1A] text-white text-center py-4 text-sm tracking-wide"
|
||||
>
|
||||
Shop Now
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
export default function NewsletterSection() {
|
||||
const t = useTranslations("Newsletter");
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "success" | "error">("idle");
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// TODO: Connect to newsletter service
|
||||
setStatus("success");
|
||||
setEmail("");
|
||||
};
|
||||
@@ -26,9 +27,7 @@ export default function NewsletterSection() {
|
||||
transition={{ duration: 0.6 }}
|
||||
className="font-serif italic text-4xl lg:text-5xl xl:text-[3.5rem] text-[#1A1A1A] tracking-tight leading-[1.1] mb-6"
|
||||
>
|
||||
Get 10% off your
|
||||
<br />
|
||||
first order
|
||||
{t("stayConnected")}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
@@ -38,8 +37,7 @@ export default function NewsletterSection() {
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-[#4A4A4A] mb-8"
|
||||
>
|
||||
Join the ManoonOils community and receive exclusive offers,
|
||||
skincare tips, and early access to new products.
|
||||
{t("newsletterText")}
|
||||
</motion.p>
|
||||
|
||||
<motion.form
|
||||
@@ -54,7 +52,7 @@ export default function NewsletterSection() {
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Enter your email"
|
||||
placeholder={t("emailPlaceholder")}
|
||||
required
|
||||
className="flex-1 px-4 py-3 border border-[#1A1A1A]/10 rounded-[4px] text-sm focus:outline-none focus:border-[#1A1A1A]/30 transition-colors"
|
||||
/>
|
||||
@@ -62,7 +60,7 @@ export default function NewsletterSection() {
|
||||
type="submit"
|
||||
className="inline-flex items-center justify-center gap-2 bg-[#1A1A1A] text-white px-6 py-3 text-sm font-medium hover:bg-[#1A1A1A]/90 transition-colors rounded-[4px]"
|
||||
>
|
||||
Subscribe
|
||||
{t("subscribe")}
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</motion.form>
|
||||
@@ -73,7 +71,7 @@ export default function NewsletterSection() {
|
||||
animate={{ opacity: 1 }}
|
||||
className="text-sm text-emerald-600 mt-4"
|
||||
>
|
||||
Thank you! Check your email for your discount code.
|
||||
Hvala vam! Proverite email za vaš kod za popust.
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
@@ -84,11 +82,10 @@ export default function NewsletterSection() {
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="text-xs text-[#4A4A4A]/60 mt-4"
|
||||
>
|
||||
By subscribing, you agree to our Privacy Policy. Unsubscribe
|
||||
anytime.
|
||||
Prijavom prihvatate našu Politiku privatnosti. Možete se odjaviti bilo kada.
|
||||
</motion.p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
70
src/components/home/ProblemSection.tsx
Normal file
70
src/components/home/ProblemSection.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function ProblemSection() {
|
||||
const t = useTranslations("ProblemSection");
|
||||
const problems = t.raw("problems") as Array<{ problem: string; description: string }>;
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="max-w-3xl mx-auto text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
|
||||
{t("title")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-medium mb-6 leading-tight text-[#1a1a1a]">
|
||||
{t("subtitle")}
|
||||
</h2>
|
||||
<p className="text-[#666666] text-lg max-w-xl mx-auto">
|
||||
{t("description")}
|
||||
</p>
|
||||
<div className="w-16 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-8 rounded-full" />
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto mt-16">
|
||||
{problems.map((item, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="relative text-center p-8 bg-white rounded-3xl shadow-lg border border-[#f0ede8] hover:shadow-2xl hover:border-[#c9a962]/30 transition-all duration-500 group"
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5 }}
|
||||
>
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-20 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] rounded-b-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
|
||||
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-md border border-[#e8e4dc] group-hover:border-[#c9a962]/50 transition-colors duration-300">
|
||||
{index === 0 && (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
|
||||
<path stroke="#c9a962" strokeLinecap="round" strokeLinejoin="round" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{index === 1 && (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
|
||||
<path stroke="#e8967a" strokeLinecap="round" strokeLinejoin="round" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)}
|
||||
{index === 2 && (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none" strokeWidth="1.5">
|
||||
<path stroke="#7eb89e" strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-3">{item.problem}</h3>
|
||||
<p className="text-sm text-[#666666] leading-relaxed">{item.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2,36 +2,20 @@
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { Star, Check } from "lucide-react";
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Sarah M.",
|
||||
skinType: "Dry, sensitive skin",
|
||||
text: "I've tried countless oils over the years, but ManoonOils is different. My skin has never felt this nourished and healthy. The argan oil is now a staple in my routine.",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "James K.",
|
||||
skinType: "Hair care enthusiast",
|
||||
text: "Finally found an oil that actually tames my frizz without making my hair greasy. The jojoba oil works wonders for my beard too. Highly recommend!",
|
||||
verified: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Emma L.",
|
||||
skinType: "Combination skin",
|
||||
text: "Was skeptical at first but after 3 weeks of using the rosehip oil, my skin texture has improved dramatically. The quality is unmatched.",
|
||||
verified: true,
|
||||
},
|
||||
];
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function TestimonialsSection() {
|
||||
const t = useTranslations("Testimonials");
|
||||
|
||||
const reviews = t.raw("reviews") as Array<{
|
||||
name: string;
|
||||
skinType: string;
|
||||
text: string;
|
||||
}>;
|
||||
|
||||
return (
|
||||
<section className="py-24 lg:py-32 bg-[#F0F7FA]">
|
||||
<div className="max-w-[1400px] mx-auto px-6">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
@@ -43,22 +27,20 @@ export default function TestimonialsSection() {
|
||||
Testimonials
|
||||
</span>
|
||||
<h2 className="font-serif italic text-4xl lg:text-5xl text-[#1A1A1A] tracking-tight">
|
||||
What our customers say
|
||||
{t("title")}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Testimonials Grid */}
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
{reviews.map((review, index) => (
|
||||
<motion.div
|
||||
key={testimonial.id}
|
||||
key={index}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className="bg-white rounded-[6px] border border-[#1A1A1A]/[0.06] p-9 flex flex-col"
|
||||
>
|
||||
{/* Stars */}
|
||||
<div className="flex gap-1 mb-5">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
@@ -68,28 +50,24 @@ export default function TestimonialsSection() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quote */}
|
||||
<p className="font-serif italic text-base lg:text-lg text-[#1A1A1A] leading-relaxed flex-1 mb-6">
|
||||
“{testimonial.text}”
|
||||
“{review.text}”
|
||||
</p>
|
||||
|
||||
{/* Author */}
|
||||
<div className="flex items-center justify-between pt-4 border-t border-[#1A1A1A]/[0.06]">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-[#1A1A1A]">
|
||||
{testimonial.name}
|
||||
{review.name}
|
||||
</p>
|
||||
<p className="text-xs text-[#4A4A4A]/70">
|
||||
{testimonial.skinType}
|
||||
{review.skinType}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{testimonial.verified && (
|
||||
<div className="inline-flex items-center gap-1 text-[10px] tracking-wider uppercase text-emerald-600 font-medium">
|
||||
<Check className="w-3 h-3" />
|
||||
Verified purchase
|
||||
</div>
|
||||
)}
|
||||
<div className="inline-flex items-center gap-1 text-[10px] tracking-wider uppercase text-emerald-600 font-medium">
|
||||
<Check className="w-3 h-3" />
|
||||
{t("verified")}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
118
src/components/home/TrustBadges.tsx
Normal file
118
src/components/home/TrustBadges.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function TrustBadges() {
|
||||
const t = useTranslations("TrustBadges");
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: 0 }}
|
||||
whileHover={{ y: -3 }}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
|
||||
<svg className="w-6 h-6 text-yellow-400" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
|
||||
4.9/5
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
|
||||
{t("averageRating")}
|
||||
</p>
|
||||
<p className="text-xs text-[#888888] mt-0.5">
|
||||
{t("basedOnReviews")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: 0.1 }}
|
||||
whileHover={{ y: -3 }}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
|
||||
50,000+
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
|
||||
{t("happyCustomers")}
|
||||
</p>
|
||||
<p className="text-xs text-[#888888] mt-0.5">
|
||||
{t("worldwide")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: 0.2 }}
|
||||
whileHover={{ y: -3 }}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#7eb89e" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
|
||||
100%
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
|
||||
{t("naturalIngredients")}
|
||||
</p>
|
||||
<p className="text-xs text-[#888888] mt-0.5">
|
||||
{t("noAdditives")}
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: 0.3 }}
|
||||
whileHover={{ y: -3 }}
|
||||
>
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
|
||||
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#e8967a" strokeWidth="1.5">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
|
||||
Free
|
||||
</p>
|
||||
<p className="text-sm font-semibold text-[#1a1a1a] mt-1">
|
||||
{t("freeShipping")}
|
||||
</p>
|
||||
<p className="text-xs text-[#888888] mt-0.5">
|
||||
{t("ordersOver")}
|
||||
</p>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Instagram, Facebook } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
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" },
|
||||
],
|
||||
};
|
||||
interface FooterProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
export default function Footer({ locale = "sr" }: FooterProps) {
|
||||
const t = useTranslations("Footer");
|
||||
const currentYear = new Date().getFullYear();
|
||||
const localePath = `/${locale}`;
|
||||
|
||||
const footerLinks = {
|
||||
shop: [
|
||||
{ label: t("allProducts"), href: `${localePath}/products` },
|
||||
{ label: t("hairCare"), href: `${localePath}/products` },
|
||||
{ label: t("skinCare"), href: `${localePath}/products` },
|
||||
{ label: t("giftSets"), href: `${localePath}/products` },
|
||||
],
|
||||
about: [
|
||||
{ label: t("ourStory"), href: `${localePath}/about` },
|
||||
{ label: t("process"), href: `${localePath}/about` },
|
||||
{ label: t("sustainability"), href: `${localePath}/about` },
|
||||
],
|
||||
help: [
|
||||
{ label: t("faq"), href: `${localePath}/contact` },
|
||||
{ label: t("shipping"), href: `${localePath}/contact` },
|
||||
{ label: t("returns"), href: `${localePath}/contact` },
|
||||
{ label: t("contactUs"), href: `${localePath}/contact` },
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-white border-t border-[#e5e5e5]">
|
||||
{/* Main Footer */}
|
||||
<div className="container py-16 lg:py-20">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-8">
|
||||
{/* Brand Column */}
|
||||
<div className="lg:col-span-4">
|
||||
<Link href="/" className="inline-block mb-6">
|
||||
<Link href={localePath} className="inline-block mb-6">
|
||||
<Image
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
@@ -42,9 +49,8 @@ export default function Footer() {
|
||||
/>
|
||||
</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.
|
||||
{t("brandDescription")}
|
||||
</p>
|
||||
{/* Social Links */}
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://instagram.com"
|
||||
@@ -67,13 +73,11 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Columns - All aligned at top */}
|
||||
<div className="lg:col-span-8">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-8">
|
||||
{/* Shop */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
Shop
|
||||
{t("shop")}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.shop.map((link) => (
|
||||
@@ -89,10 +93,9 @@ export default function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* About */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
About
|
||||
{t("about")}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.about.map((link) => (
|
||||
@@ -108,10 +111,9 @@ export default function Footer() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Help */}
|
||||
<div className="flex flex-col">
|
||||
<h4 className="text-xs uppercase tracking-[0.15em] font-medium mb-5 text-[#1a1a1a]">
|
||||
Help
|
||||
{t("help")}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{footerLinks.help.map((link) => (
|
||||
@@ -131,18 +133,15 @@ export default function Footer() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Bar */}
|
||||
<div className="border-t border-[#e5e5e5]">
|
||||
<div className="container py-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Copyright */}
|
||||
<p className="text-xs text-[#999999]">
|
||||
© {currentYear} ManoonOils. All rights reserved.
|
||||
© {currentYear} ManoonOils. {t("allRights")}
|
||||
</p>
|
||||
|
||||
{/* Payment Methods */}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-[#999999]">We accept:</span>
|
||||
<span className="text-xs text-[#999999]">{t("weAccept")}</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
|
||||
@@ -160,4 +159,4 @@ export default function Footer() {
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,29 +4,28 @@ import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { User, ShoppingBag, Menu, X } from "lucide-react";
|
||||
import CartDrawer from "@/components/cart/CartDrawer";
|
||||
|
||||
const navLinks = [
|
||||
{ href: "/products", label: "Products" },
|
||||
{ href: "/about", label: "About" },
|
||||
{ href: "/contact", label: "Contact" },
|
||||
];
|
||||
interface HeaderProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function Header() {
|
||||
export default function Header({ locale = "sr" }: HeaderProps) {
|
||||
const t = useTranslations("Header");
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
|
||||
|
||||
const itemCount = getLineCount();
|
||||
const localePath = `/${locale}`;
|
||||
|
||||
// Initialize checkout on mount
|
||||
useEffect(() => {
|
||||
initCheckout();
|
||||
}, [initCheckout]);
|
||||
|
||||
// Track scroll for header styling
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 50);
|
||||
@@ -35,7 +34,6 @@ export default function Header() {
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
// Lock body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
if (mobileMenuOpen) {
|
||||
document.body.style.overflow = "hidden";
|
||||
@@ -47,79 +45,78 @@ export default function Header() {
|
||||
};
|
||||
}, [mobileMenuOpen]);
|
||||
|
||||
const navLinks = [
|
||||
{ href: `${localePath}/products`, label: t("products") },
|
||||
{ href: `${localePath}/about`, label: t("about") },
|
||||
{ href: `${localePath}/contact`, label: t("contact") },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
scrolled
|
||||
? "bg-white/95 backdrop-blur-md shadow-sm"
|
||||
: "bg-transparent"
|
||||
: "bg-white/80 backdrop-blur-sm"
|
||||
}`}
|
||||
>
|
||||
<div className="container">
|
||||
<div className="flex items-center justify-between h-[72px]">
|
||||
{/* Mobile Menu Button */}
|
||||
<div className="relative flex items-center justify-between h-[72px] px-4 lg:px-6">
|
||||
<button
|
||||
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label={t("openMenu")}
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<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>
|
||||
|
||||
<Link href={localePath || "/"} 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>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="lg:hidden p-2 -ml-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
aria-label="Open menu"
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
||||
aria-label={t("account")}
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
<User className="w-5 h-5" strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
{/* Logo */}
|
||||
<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>
|
||||
|
||||
{/* Desktop Navigation - Centered */}
|
||||
<nav className="hidden lg:flex items-center gap-10 mx-auto">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-[13px] uppercase tracking-[0.05em] text-[#1a1a1a] hover:text-[#666666] transition-colors relative group"
|
||||
>
|
||||
{link.label}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-[1px] bg-current transition-all duration-300 group-hover:w-full" />
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Icons */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
||||
aria-label="Account"
|
||||
>
|
||||
<User className="w-5 h-5" strokeWidth={1.5} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
|
||||
onClick={toggleCart}
|
||||
aria-label="Open cart"
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
|
||||
{itemCount > 99 ? "99+" : itemCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="p-2 hover:bg-black/5 rounded-full transition-colors relative"
|
||||
onClick={toggleCart}
|
||||
aria-label={t("openCart")}
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||
{itemCount > 0 && (
|
||||
<span className="absolute -top-0.5 -right-0.5 bg-black text-white text-[10px] w-[18px] h-[18px] rounded-full flex items-center justify-center font-medium">
|
||||
{itemCount > 99 ? "99+" : itemCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{mobileMenuOpen && (
|
||||
<motion.div
|
||||
@@ -130,9 +127,8 @@ export default function Header() {
|
||||
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)}>
|
||||
<Link href={localePath || "/"} onClick={() => setMobileMenuOpen(false)}>
|
||||
<Image
|
||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||
alt="ManoonOils"
|
||||
@@ -144,13 +140,12 @@ export default function Header() {
|
||||
<button
|
||||
className="p-2 -mr-2 hover:bg-black/5 rounded-full transition-colors"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
aria-label="Close menu"
|
||||
aria-label={t("closeMenu")}
|
||||
>
|
||||
<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
|
||||
@@ -170,7 +165,6 @@ export default function Header() {
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Mobile Footer */}
|
||||
<div className="py-8 border-t border-[#e5e5e5]">
|
||||
<div className="flex items-center justify-between">
|
||||
<button
|
||||
@@ -181,13 +175,13 @@ export default function Header() {
|
||||
}}
|
||||
>
|
||||
<ShoppingBag className="w-5 h-5" strokeWidth={1.5} />
|
||||
Cart ({itemCount})
|
||||
{t("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
|
||||
{t("account")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,4 +193,4 @@ export default function Header() {
|
||||
<CartDrawer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
96
src/components/product/ProductBenefits.tsx
Normal file
96
src/components/product/ProductBenefits.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ProductBenefitsProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export default function ProductBenefits({ locale = "SR" }: ProductBenefitsProps) {
|
||||
const t = useTranslations("ProductBenefits");
|
||||
|
||||
const benefits = [
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-10 h-10" fill="none" viewBox="0 0 24 24" strokeWidth={1.5}>
|
||||
<path stroke="#c9a962" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
|
||||
<path stroke="#c9a962" strokeLinecap="round" strokeLinejoin="round" d="M15.75 10.5V6a3.75 3.75 0 10-7.5 0v4.5m11.356-1.993l1.263 12c.07.665-.45 1.243-1.119 1.243H4.25a1.125 1.125 0 01-1.12-1.243l1.264-12A1.125 1.125 0 015.513 7.5h12.974c.576 0 1.059.435 1.119 1.007zM8.625 10.5a.375.375 0 11-.75 0 .375.375 0 01.75 0zm7.5 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
),
|
||||
title: t("pureNatural"),
|
||||
description: t("pureNaturalDesc"),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#e8967a"/>
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
),
|
||||
title: t("crueltyFree"),
|
||||
description: t("crueltyFreeDesc"),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="#7eb89e"/>
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" stroke="#c9a962" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
),
|
||||
title: t("madeWithLove"),
|
||||
description: t("madeWithLoveDesc"),
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<svg className="w-10 h-10" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="#c9a962"/>
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="#b8944f" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
),
|
||||
title: t("visibleResults"),
|
||||
description: t("visibleResultsDesc"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-gradient-to-b from-white to-[#faf9f7]">
|
||||
<div className="container mx-auto px-4">
|
||||
<motion.div
|
||||
className="text-center mb-12"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#c9a962] mb-3 block font-medium">
|
||||
{t("whyChoose")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
{t("manoonDifference")}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 max-w-5xl mx-auto">
|
||||
{benefits.map((benefit, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
className="text-center p-6 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||
whileHover={{ y: -5 }}
|
||||
>
|
||||
<div className="w-20 h-20 mx-auto mb-5 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm border border-[#e8e4dc]">
|
||||
{benefit.icon}
|
||||
</div>
|
||||
<h3 className="text-base font-medium mb-2 text-[#1a1a1a]">{benefit.title}</h3>
|
||||
<p className="text-sm text-[#666666] leading-relaxed">{benefit.description}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Product } from "@/types/saleor";
|
||||
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
||||
|
||||
@@ -13,10 +14,12 @@ interface ProductCardProps {
|
||||
}
|
||||
|
||||
export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) {
|
||||
const t = useTranslations("ProductCard");
|
||||
const image = getProductImage(product);
|
||||
const price = getProductPrice(product);
|
||||
const localized = getLocalizedProduct(product, locale);
|
||||
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
|
||||
const urlLocale = locale === "SR" ? "sr" : "en";
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
@@ -25,57 +28,51 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||
>
|
||||
<Link href={`/products/${localized.slug}`} className="group block">
|
||||
{/* Image Container */}
|
||||
<div className="relative aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||
<Link href={`/${urlLocale}/products/${localized.slug}`} className="group block">
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||
{image ? (
|
||||
<Image
|
||||
<img
|
||||
src={image}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||
<span className="text-sm">No image</span>
|
||||
<span className="text-sm">{t("noImage")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Out of Stock Overlay */}
|
||||
|
||||
{!isAvailable && (
|
||||
<div className="absolute inset-0 bg-white/80 flex items-center justify-center">
|
||||
<span className="text-sm uppercase tracking-[0.1em] text-[#666666]">
|
||||
{locale === "EN" ? "Out of Stock" : "Nema na stanju"}
|
||||
{t("outOfStock")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hover Quick Add (optional) */}
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button
|
||||
<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"}
|
||||
{t("quickAdd")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
|
||||
<div className="text-center">
|
||||
<h3 className="text-[15px] font-medium text-[#1a1a1a] mb-1 group-hover:text-[#666666] transition-colors line-clamp-1">
|
||||
{localized.name}
|
||||
</h3>
|
||||
|
||||
|
||||
<p className="text-[14px] text-[#666666]">
|
||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
{price || t("contactForPrice")}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { ChevronDown, Star, Minus, Plus } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Product } from "@/types/saleor";
|
||||
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 ProductBenefits from "@/components/product/ProductBenefits";
|
||||
import ProductReviews from "@/components/product/ProductReviews";
|
||||
import AsSeenIn from "@/components/home/AsSeenIn";
|
||||
import TrustBadges from "@/components/home/TrustBadges";
|
||||
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
|
||||
import HowItWorks from "@/components/home/HowItWorks";
|
||||
import NewsletterSection from "@/components/home/NewsletterSection";
|
||||
|
||||
interface ProductDetailProps {
|
||||
product: Product;
|
||||
@@ -16,14 +24,13 @@ interface ProductDetailProps {
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
// Expandable Section Component
|
||||
function ExpandableSection({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
function ExpandableSection({
|
||||
title,
|
||||
children,
|
||||
defaultOpen = false
|
||||
}: {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
@@ -37,8 +44,8 @@ function ExpandableSection({
|
||||
<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' : ''}`}
|
||||
<ChevronDown
|
||||
className={`w-5 h-5 transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</button>
|
||||
@@ -61,7 +68,6 @@ function ExpandableSection({
|
||||
);
|
||||
}
|
||||
|
||||
// Star Rating Component
|
||||
function StarRating({ rating = 5, count = 0 }: { rating?: number; count?: number }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -69,34 +75,49 @@ function StarRating({ rating = 5, count = 0 }: { rating?: number; count?: number
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < rating ? 'fill-black text-black' : 'text-[#e5e5e5]'}`}
|
||||
className={`w-4 h-4 ${i < rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{count > 0 && (
|
||||
<span className="text-sm text-[#666666] ml-1">({count})</span>
|
||||
<span className="text-sm text-[#666666] ml-1">({count >= 1000 ? '1000+' : count})</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductDetail({ product, relatedProducts, locale = "SR" }: ProductDetailProps) {
|
||||
const t = useTranslations("ProductDetail");
|
||||
const tProduct = useTranslations("Product");
|
||||
const [selectedImage, setSelectedImage] = useState(0);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [urgencyIndex, setUrgencyIndex] = useState(0);
|
||||
const { addLine, openCart } = useSaleorCheckoutStore();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setUrgencyIndex(prev => (prev + 1) % 3);
|
||||
}, 3000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const urgencyMessages = [
|
||||
{ icon: "🚀", text: t("urgency1") },
|
||||
{ icon: "🛒", text: t("urgency2") },
|
||||
{ icon: "👀", text: t("urgency3") },
|
||||
];
|
||||
|
||||
const localized = getLocalizedProduct(product, locale);
|
||||
const variant = product.variants?.[0];
|
||||
|
||||
// Get all images from media
|
||||
const images = product.media?.length > 0
|
||||
|
||||
const images = product.media?.length > 0
|
||||
? product.media.filter(m => m.type === "IMAGE")
|
||||
: [{ id: "0", url: "/placeholder-product.jpg", alt: localized.name, type: "IMAGE" as const }];
|
||||
|
||||
const handleAddToCart = async () => {
|
||||
if (!variant?.id) return;
|
||||
|
||||
|
||||
setIsAdding(true);
|
||||
try {
|
||||
await addLine(variant.id, quantity);
|
||||
@@ -108,13 +129,13 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
|
||||
const isAvailable = variant?.quantityAvailable > 0;
|
||||
const price = getProductPrice(product);
|
||||
|
||||
// Extract short description (first sentence or first 100 chars)
|
||||
const shortDescription = localized.description
|
||||
const priceAmount = getProductPriceAmount(product);
|
||||
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
|
||||
|
||||
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",
|
||||
@@ -124,12 +145,11 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
return (
|
||||
<>
|
||||
<section className="min-h-screen" id="product-detail">
|
||||
{/* Breadcrumb - with proper top padding for fixed header */}
|
||||
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
|
||||
<div className="container py-5">
|
||||
<nav className="flex items-center gap-2 text-sm">
|
||||
<Link href="/" className="text-[#666666] hover:text-black transition-colors">
|
||||
{locale === "EN" ? "Home" : "Početna"}
|
||||
<Link href={`/${locale.toLowerCase()}`} className="text-[#666666] hover:text-black transition-colors">
|
||||
{t("home")}
|
||||
</Link>
|
||||
<span className="text-[#999999]">/</span>
|
||||
<span className="text-[#1a1a1a]">{localized.name}</span>
|
||||
@@ -137,17 +157,14 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Content */}
|
||||
<div className="container py-12 lg:py-16">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20">
|
||||
{/* Image Gallery - Left Side */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="flex gap-4"
|
||||
className="flex flex-col md:flex-row gap-4"
|
||||
>
|
||||
{/* Thumbnails - Vertical on Desktop, Hidden on Mobile */}
|
||||
{images.length > 1 && (
|
||||
<div className="hidden md:flex flex-col gap-3 w-20 flex-shrink-0">
|
||||
{images.map((image, index) => (
|
||||
@@ -155,79 +172,133 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
key={image.id}
|
||||
onClick={() => setSelectedImage(index)}
|
||||
className={`relative aspect-square w-full overflow-hidden border-2 transition-colors ${
|
||||
selectedImage === index
|
||||
? "border-black"
|
||||
selectedImage === index
|
||||
? "border-black"
|
||||
: "border-transparent hover:border-[#999999]"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="80px"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Image */}
|
||||
<div className="flex-1 relative aspect-square bg-[#f8f9fa] overflow-hidden">
|
||||
{images[selectedImage] && (
|
||||
<Image
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
/>
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
|
||||
<img
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Award Badge - Optional */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<div className="bg-black text-white text-[10px] uppercase tracking-[0.1em] px-3 py-1.5">
|
||||
{locale === "EN" ? "Bestseller" : "Najprodavanije"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Product Info - Right Side */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="lg:pl-8"
|
||||
>
|
||||
{/* Product Name */}
|
||||
<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>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
||||
{localized.name}
|
||||
</h1>
|
||||
|
||||
{/* Short Description */}
|
||||
<p className="text-[#666666] leading-relaxed mb-6">
|
||||
<p className="text-[#666666] leading-relaxed mb-4">
|
||||
{shortDescription}
|
||||
</p>
|
||||
|
||||
{/* Price & Rating */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<span className="text-3xl font-medium">
|
||||
{price || (locale === "EN" ? "Contact for price" : "Kontaktirajte za cenu")}
|
||||
<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>
|
||||
<StarRating rating={5} count={12} />
|
||||
<span className="text-red-600 text-sm font-medium">{t("stocksRunningOut")}</span>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{!originalPrice && (
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<span className="text-3xl font-medium">
|
||||
{price || tProduct("outOfStock")}
|
||||
</span>
|
||||
<StarRating rating={5} count={1000} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-[#e5e5e5] mb-8" />
|
||||
|
||||
{/* Size Selector */}
|
||||
{product.variants && product.variants.length > 1 && (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-sm uppercase tracking-[0.1em] font-medium">
|
||||
{locale === "EN" ? "Size" : "Veličina"}
|
||||
{t("size")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
@@ -247,10 +318,9 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
</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"}
|
||||
{t("qty")}
|
||||
</span>
|
||||
<div className="flex items-center border-2 border-[#1a1a1a]">
|
||||
<button
|
||||
@@ -270,44 +340,70 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
</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"
|
||||
className="w-full h-16 bg-black text-white text-[13px] uppercase tracking-[0.15em] font-semibold hover:bg-[#333333] active:bg-[#1a1a1a] transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed mb-6 hover:scale-[1.02] shadow-lg hover:shadow-xl"
|
||||
>
|
||||
{isAdding
|
||||
? (locale === "EN" ? "Adding..." : "Dodavanje...")
|
||||
: (locale === "EN" ? "Add to Cart — Free Shipping" : "Dodaj u korpu — Besplatna dostava")
|
||||
{isAdding
|
||||
? t("adding")
|
||||
: t("transformHairSkin")
|
||||
}
|
||||
</button>
|
||||
) : (
|
||||
<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"}
|
||||
{t("outOfStock")}
|
||||
</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>
|
||||
<div className="flex items-center justify-center gap-2 mb-6">
|
||||
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
||||
</svg>
|
||||
<p className="text-sm text-[#666666]">
|
||||
{t("freeShipping")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4 mb-8 p-4 bg-[#f8f9fa] rounded-lg">
|
||||
<div className="text-center">
|
||||
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<p className="text-xs text-[#666666]">
|
||||
{t("guarantee")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<p className="text-xs text-[#666666]">
|
||||
{t("secureCheckout")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<svg className="w-6 h-6 mx-auto mb-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-8m15.357 8H15" />
|
||||
</svg>
|
||||
<p className="text-xs text-[#666666]">
|
||||
{t("easyReturns")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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"}
|
||||
{t("benefits")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{benefits.map((benefit, index) => (
|
||||
<span
|
||||
<span
|
||||
key={index}
|
||||
className="px-4 py-2 text-sm border border-[#e5e5e5] text-[#666666]"
|
||||
>
|
||||
@@ -317,32 +413,20 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Sections */}
|
||||
<div>
|
||||
<ExpandableSection title={locale === "EN" ? "Description" : "Opis"}>
|
||||
<ExpandableSection title={t("description")}>
|
||||
<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 title={t("howToUse")}>
|
||||
<p>{t("howToUseText")}</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 title={t("ingredients")}>
|
||||
<p>{t("ingredientsText")}</p>
|
||||
</ExpandableSection>
|
||||
</div>
|
||||
|
||||
{/* SKU */}
|
||||
{variant?.sku && (
|
||||
<p className="text-xs text-[#999999] mt-8">
|
||||
SKU: {variant.sku}
|
||||
@@ -353,31 +437,45 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Related Products */}
|
||||
<ProductReviews locale={locale} productName={localized.name} />
|
||||
|
||||
<AsSeenIn />
|
||||
|
||||
<BeforeAfterGallery />
|
||||
|
||||
{relatedProducts && relatedProducts.length > 0 && (
|
||||
<section className="py-20 lg:py-28 bg-[#f8f9fa]">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="text-center mb-16">
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
{locale === "EN" ? "You May Also Like" : "Možda će vam se svideti"}
|
||||
{t("youMayAlsoLike")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
{locale === "EN" ? "Similar Products" : "Slični proizvodi"}
|
||||
{t("similarProducts")}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||
<div className="flex flex-wrap justify-center gap-6 lg:gap-8">
|
||||
{relatedProducts.filter(p => p && p.id).slice(0, 4).map((relatedProduct, index) => (
|
||||
<ProductCard
|
||||
key={relatedProduct.id}
|
||||
product={relatedProduct}
|
||||
index={index}
|
||||
locale={locale}
|
||||
/>
|
||||
<div key={relatedProduct.id} className="w-full sm:w-[calc(50%-12px)] lg:w-[calc(25%-18px)]">
|
||||
<ProductCard
|
||||
product={relatedProduct}
|
||||
index={index}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<ProductBenefits locale={locale} />
|
||||
|
||||
<TrustBadges />
|
||||
|
||||
<HowItWorks />
|
||||
|
||||
<NewsletterSection />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
134
src/components/product/ProductReviews.tsx
Normal file
134
src/components/product/ProductReviews.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface Review {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
text: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface ProductReviewsProps {
|
||||
locale?: string;
|
||||
productName?: string;
|
||||
}
|
||||
|
||||
function ReviewCard({ review }: { review: Review }) {
|
||||
return (
|
||||
<div className="flex-shrink-0 w-80 bg-white p-6 rounded-2xl shadow-sm border border-[#f0ede8] mx-3">
|
||||
<div className="flex items-center gap-1 mb-3">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[#444444] text-sm leading-relaxed mb-4">"{review.text}"</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#1a1a1a] flex items-center justify-center text-white text-sm font-medium">
|
||||
{review.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="text-sm font-medium">{review.name}</p>
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span className="text-xs text-green-700 font-medium">Verified</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#888888]">{review.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProductReviews(_props: ProductReviewsProps) {
|
||||
const t = useTranslations("ProductReviews");
|
||||
const reviews = t.raw("reviews") as Review[];
|
||||
|
||||
return (
|
||||
<section className="py-16 bg-[#faf9f7] overflow-hidden">
|
||||
<div className="container mx-auto px-4 mb-8">
|
||||
<motion.div
|
||||
className="text-center"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6 }}
|
||||
>
|
||||
<span className="text-xs uppercase tracking-[0.2em] text-[#666666] mb-3 block">
|
||||
{t("customerReviews")}
|
||||
</span>
|
||||
<h2 className="text-3xl md:text-4xl font-medium">
|
||||
{t("whatCustomersSay")}
|
||||
</h2>
|
||||
|
||||
<div className="flex items-center justify-center gap-4 mt-4">
|
||||
<span className="text-5xl font-bold text-[#1a1a1a]">4.9</span>
|
||||
<div>
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className="w-5 h-5 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-sm text-[#666666] mt-1">{t("basedOnReviews")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-0 bottom-0 w-20 bg-gradient-to-r from-[#faf9f7] to-transparent z-10 pointer-events-none" />
|
||||
<div className="absolute right-0 top-0 bottom-0 w-20 bg-gradient-to-l from-[#faf9f7] to-transparent z-10 pointer-events-none" />
|
||||
|
||||
<div className="flex overflow-hidden mb-4">
|
||||
<motion.div
|
||||
className="flex items-center gap-0"
|
||||
animate={{
|
||||
x: [0, -50 + "%"],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 120,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{[...reviews, ...reviews].map((review, index) => (
|
||||
<ReviewCard key={`first-${index}-${review.id}`} review={review} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="flex overflow-hidden">
|
||||
<motion.div
|
||||
className="flex items-center gap-0"
|
||||
animate={{
|
||||
x: [-50 + "%", 0],
|
||||
}}
|
||||
transition={{
|
||||
x: {
|
||||
repeat: Infinity,
|
||||
repeatType: "loop",
|
||||
duration: 120,
|
||||
ease: "linear",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{[...reviews.slice(25), ...reviews.slice(0, 25), ...reviews.slice(25), ...reviews.slice(0, 25)].map((review, index) => (
|
||||
<ReviewCard key={`second-${index}-${review.id}`} review={review} />
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
358
src/i18n/messages/de.json
Normal file
358
src/i18n/messages/de.json
Normal file
@@ -0,0 +1,358 @@
|
||||
{
|
||||
"Navigation": {
|
||||
"home": "Startseite",
|
||||
"products": "Produkte",
|
||||
"about": "Über uns",
|
||||
"contact": "Kontakt"
|
||||
},
|
||||
"Home": {
|
||||
"hero": {
|
||||
"title": "Premium Natürliche Öle",
|
||||
"subtitle": "Für Haar- und Hautpflege",
|
||||
"lovedBy": "Von 50.000+ Kunden weltweit geliebt",
|
||||
"transformHeadline": "Transformieren Sie Ihr Haar & Haut",
|
||||
"withNaturalOils": "mit 100% Natürlichen Ölen",
|
||||
"subtitleText": "Kaltgepresste, biologische Öle mit Liebe handgefertigt. Keine Zusatzstoffe, keine Konservierungsstoffe - nur die reinste Güte der Natur für Ihr tägliches Schönheitsritual.",
|
||||
"ctaButton": "Mein Haar & Haut transformieren",
|
||||
"learnStory": "Unsere Geschichte entdecken",
|
||||
"moneyBack": "30-Tage Geld-zurück",
|
||||
"freeShipping": "Kostenloser Versand über 3.000 RSD",
|
||||
"crueltyFree": "Tierversuchsfrei"
|
||||
},
|
||||
"collection": "Unsere Kollektion",
|
||||
"premiumOils": "Premium Natürliche Öle",
|
||||
"oilsDescription": "Kaltgepresste, reine und natürliche Öle für Ihre tägliche Schönheitsroutine",
|
||||
"viewAll": "Alle Produkte ansehen",
|
||||
"ourStory": "Unsere Geschichte",
|
||||
"handmadeWithLove": "Mit Liebe handgefertigt",
|
||||
"storyText1": "Jede Flasche ManoonOils wird mit Sorgfalt unter Verwendung traditioneller Methoden hergestellt, die von Generation zu Generation weitergegeben werden. Wir beziehen nur die feinsten biologischen Zutaten, um Ihnen Öle zu bringen, die Haar und Haut pflegen.",
|
||||
"storyText2": "Unser Engagement für Reinheit bedeutet keine Zusatzstoffe, keine Konservierungsstoffe - nur die Güte der Natur in ihrer potentesten Form.",
|
||||
"learnMore": "Mehr erfahren",
|
||||
"whyChooseUs": "Warum uns wählen",
|
||||
"manoonDifference": "Der Manoon Unterschied",
|
||||
"stayConnected": "Bleiben Sie verbunden",
|
||||
"joinCommunity": "Werden Sie Teil unserer Gemeinschaft",
|
||||
"newsletterText": "Abonnieren Sie, um exklusive Angebote, Schönheitstipps zu erhalten und als Erster über neue Produkte informiert zu werden.",
|
||||
"emailPlaceholder": "Geben Sie Ihre E-Mail ein",
|
||||
"subscribe": "Abonnieren"
|
||||
},
|
||||
"Benefits": {
|
||||
"natural": "100% Natürlich",
|
||||
"naturalDesc": "Reine, kaltgepresste Öle ohne Zusatzstoffe oder Konservierungsstoffe. Nur die Güte der Natur.",
|
||||
"handcrafted": "Handgefertigt",
|
||||
"handcraftedDesc": "Jede Charge wird sorgfältig von Hand zubereitet, um höchste Qualität zu gewährleisten.",
|
||||
"sustainable": "Nachhaltig",
|
||||
"sustainableDesc": "Ethnisch beschaffte Zutaten und umweltfreundliche Verpackungen für einen besseren Planeten."
|
||||
},
|
||||
"Products": {
|
||||
"collection": "Unsere Kollektion",
|
||||
"allProducts": "Alle Produkte",
|
||||
"productsCount": "{count} Produkte",
|
||||
"featured": "Empfohlen",
|
||||
"newest": "Neueste",
|
||||
"priceLow": "Preis: Aufsteigend",
|
||||
"priceHigh": "Preis: Absteigend",
|
||||
"noProducts": "Keine Produkte verfügbar",
|
||||
"checkBack": "Bitte schauen Sie später für neue Produkte vorbei."
|
||||
},
|
||||
"Product": {
|
||||
"addToCart": "In den Warenkorb",
|
||||
"outOfStock": "Nicht auf Lager",
|
||||
"details": "Details",
|
||||
"ingredients": "Zutaten",
|
||||
"usage": "Anwendung",
|
||||
"related": "Das könnte Ihnen auch gefallen",
|
||||
"notFound": "Produkt nicht gefunden",
|
||||
"notFoundDesc": "Das gesuchte Produkt existiert nicht oder wurde entfernt.",
|
||||
"browseProducts": "Produkte durchsuchen"
|
||||
},
|
||||
"Cart": {
|
||||
"title": "Ihr Warenkorb",
|
||||
"empty": "Ihr Warenkorb ist leer",
|
||||
"emptyDesc": "Es sieht so aus, als hätten Sie noch nichts in Ihren Warenkorb gelegt.",
|
||||
"continueShopping": "Weiter einkaufen",
|
||||
"checkout": "Zur Kasse",
|
||||
"subtotal": "Zwischensumme",
|
||||
"shipping": "Versand",
|
||||
"shippingCalc": "Wird an der Kasse berechnet",
|
||||
"total": "Gesamt",
|
||||
"freeShipping": "Kostenloser Versand bei Bestellungen über {amount}",
|
||||
"remove": "Entfernen",
|
||||
"processes": "Wird bearbeitet...",
|
||||
"cartEmpty": "Ihr Warenkorb ist leer"
|
||||
},
|
||||
"About": {
|
||||
"title": "Über uns",
|
||||
"subtitle": "Unsere Geschichte",
|
||||
"intro": "ManoonOils wurde aus einer Leidenschaft für natürliche Schönheit und dem Glauben geboren, dass die beste Hautpflege von der Natur selbst kommt.",
|
||||
"intro2": "Wir glauben an die Kraft natürlicher Inhaltsstoffe. Jedes Öl in unserer Kollektion wurde sorgfältig aufgrund seiner einzigartigen Eigenschaften und Vorteile ausgewählt. Von nährenden Ölen, die die Haarlebenskraft wiederherstellen, bis zu Seren, die die Haut verjüngen, stellen wir jedes Produkt mit Liebe und Liebe zum Detail her.",
|
||||
"naturalIngredients": "Natürliche Inhaltsstoffe",
|
||||
"naturalIngredientsDesc": "Wir verwenden nur die feinsten natürlichen Inhaltsstoffe, die ethisch und nachhaltig von vertrauenswürdigen Lieferanten weltweit beschafft werden.",
|
||||
"crueltyFree": "Tierversuchsfrei",
|
||||
"crueltyFreeDesc": "Unsere Produkte werden niemals an Tieren getestet. Wir glauben an Schönheit ohne Kompromisse.",
|
||||
"sustainablePackaging": "Nachhaltige Verpackung",
|
||||
"sustainablePackagingDesc": "Wir verwenden umweltfreundliche Verpackungsmaterialien und minimieren Abfall während unseres gesamten Produktionsprozesses.",
|
||||
"handcraftedQuality": "Handwerkliche Qualität",
|
||||
"handcraftedQualityDesc": "Jede Flasche wird in kleinen Chargen handgefertigt, um höchste Qualität und Frische zu gewährleisten.",
|
||||
"mission": "Unsere Mission",
|
||||
"missionQuote": "\"Hochwertige, natürliche Produkte anzubieten, die Ihre tägliche Schönheitsroutine verbessern.\"",
|
||||
"handmadeTitle": "Mit Liebe handgefertigt",
|
||||
"handmadeText1": "Jede Flasche ManoonOils wird mit Sorgfalt handgefertigt. Wir stellen unsere Produkte in kleinen Chargen her, um höchste Qualität und Frische zu gewährleisten. Wenn Sie ManoonOils verwenden, können Sie sicher sein, dass Sie etwas verwenden, das mit echter Sorgfalt und Fachwissen hergestellt wurde.",
|
||||
"handmadeText2": "Unsere Reise begann mit einer einfachen Frage: Wie können wir Produkte herstellen, die sowohl Haar als auch Haut wirklich pflegen? Heute innovieren wir weiter, während wir unserem Engagement für natürliche, effektive Schönheitslösungen treu bleiben."
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Kontakt",
|
||||
"subtitle": "Kontakt aufnehmen",
|
||||
"getInTouch": "Kontakt aufnehmen",
|
||||
"getInTouchDesc": "Wir sind hier um zu helfen! Ob Sie Fragen zu unseren Produkten haben, Hilfe mit einer Bestellung benötigen oder einfach Hallo sagen möchten - wir würden uns freuen, von Ihnen zu hören.",
|
||||
"email": "E-Mail",
|
||||
"emailReply": "Wir antworten innerhalb von 24 Stunden",
|
||||
"shippingTitle": "Versand",
|
||||
"freeShipping": "Kostenloser Versand über 3.000 RSD",
|
||||
"deliveryTime": "Geliefert innerhalb von 2-5 Werktagen",
|
||||
"location": "Standort",
|
||||
"locationDesc": "Serbien",
|
||||
"worldwideShipping": "Versand weltweit",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Ihr Name",
|
||||
"emailField": "E-Mail",
|
||||
"emailPlaceholder": "ihre@email.com",
|
||||
"message": "Nachricht",
|
||||
"messagePlaceholder": "Wie können wir Ihnen helfen?",
|
||||
"sendMessage": "Nachricht senden",
|
||||
"thankYou": "Vielen Dank!",
|
||||
"thankYouDesc": "Ihre Nachricht wurde gesendet. Wir werden uns in Kürze bei Ihnen melden.",
|
||||
"faqTitle": "Häufig gestellte Fragen",
|
||||
"faq1q": "Wie lange dauert der Versand?",
|
||||
"faq1a": "Bestellungen werden in der Regel innerhalb von 2-5 Werktagen für Inlandsversand geliefert. Sie erhalten eine Tracking-Nummer, sobald Ihre Bestellung versandt wurde.",
|
||||
"faq2q": "Sind Ihre Produkte 100% natürlich?",
|
||||
"faq2a": "Ja! Alle unsere Öle sind 100% natürlich, kaltgepresst und frei von jeglichen Zusatzstoffen, Konservierungsstoffen oder künstlichen Duftstoffen.",
|
||||
"faq3q": "Wie ist Ihre Rückgaberichtlinie?",
|
||||
"faq3a": "Wir akzeptieren Rücksendungen innerhalb von 14 Tagen nach Lieferung für ungeöffnete Produkte. Bitte kontaktieren Sie uns, wenn Sie Probleme mit Ihrer Bestellung haben.",
|
||||
"faq4q": "Bieten Sie Großhandel an?",
|
||||
"faq4a": "Ja, wir bieten Großhandelspreise für Bulk-Bestellungen. Bitte kontaktieren Sie uns unter hello@manoonoils.com für mehr Informationen."
|
||||
},
|
||||
"Footer": {
|
||||
"quickLinks": "Schnelle Links",
|
||||
"customerService": "Kundenservice",
|
||||
"contact": "Kontakt",
|
||||
"shipping": "Versand",
|
||||
"returns": "Rückgabe",
|
||||
"faq": "FAQ",
|
||||
"followUs": "Folgen Sie uns",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterDesc": "Abonnieren Sie unseren Newsletter für exklusive Angebote und Updates.",
|
||||
"copyright": "Alle Rechte vorbehalten.",
|
||||
"allRights": "Alle Rechte vorbehalten.",
|
||||
"shop": "Shop",
|
||||
"allProducts": "Alle Produkte",
|
||||
"hairCare": "Haarpflege",
|
||||
"skinCare": "Hautpflege",
|
||||
"giftSets": "Geschenksets",
|
||||
"about": "Über uns",
|
||||
"ourStory": "Unsere Geschichte",
|
||||
"process": "Prozess",
|
||||
"sustainability": "Nachhaltigkeit",
|
||||
"help": "Hilfe",
|
||||
"contactUs": "Kontaktieren Sie uns",
|
||||
"brandDescription": "Premium natürliche Öle für Haar- und Hautpflege. Handgefertigt mit Liebe unter Verwendung traditioneller Methoden.",
|
||||
"weAccept": "Wir akzeptieren:"
|
||||
},
|
||||
"Common": {
|
||||
"loading": "Laden...",
|
||||
"error": "Ein Fehler ist aufgetreten",
|
||||
"tryAgain": "Erneut versuchen",
|
||||
"close": "Schließen",
|
||||
"back": "Zurück",
|
||||
"next": "Weiter",
|
||||
"previous": "Vorherige",
|
||||
"search": "Suchen",
|
||||
"noResults": "Keine Ergebnisse gefunden"
|
||||
},
|
||||
"Testimonials": {
|
||||
"title": "Was unsere Kunden sagen",
|
||||
"verified": "Verifizierter Kauf",
|
||||
"reviews": [
|
||||
{
|
||||
"name": "Sarah M.",
|
||||
"skinType": "Trockene, empfindliche Haut",
|
||||
"text": "Ich habe im Laufe der Jahre unzählige Öle ausprobiert, aber ManoonOils ist anders. Meine Haut hat sich noch nie so genährt und gesund angefühlt. Das Arganöl ist jetzt ein Grundnahrungsmittel in meiner Routine."
|
||||
},
|
||||
{
|
||||
"name": "James K.",
|
||||
"skinType": "Haarpflege-Enthusiast",
|
||||
"text": "Endlich ein Öl gefunden, das meinen Frizz wirklich bändigt, ohne mein Haar fettig zu machen. Das Jojobaöl wirkt auch bei meinem Bart Wunder. Sehr empfehlenswert!"
|
||||
},
|
||||
{
|
||||
"name": "Emma L.",
|
||||
"skinType": "Mischhaut",
|
||||
"text": "War zuerst skeptisch, aber nach 3 Wochen Hagebuttenöl hat sich meine Hauttextur dramatisch verbessert. Die Qualität ist unübertroffen."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProductReviews": {
|
||||
"customerReviews": "Kundenbewertungen",
|
||||
"whatCustomersSay": "Was Kunden sagen",
|
||||
"basedOnReviews": "Basierend auf 1000+ Bewertungen",
|
||||
"reviews": [
|
||||
{ "id": 1, "name": "Ana M.", "location": "Belgrad", "text": "Manoon Anti-Age Serum hat meine Haut in nur 2 Wochen transformiert!", "rating": 5 },
|
||||
{ "id": 2, "name": "Milica P.", "location": "Novi Sad", "text": "Das beste Tageserum, das ich je verwendet habe. Meine Falten sind sichtbar reduziert.", "rating": 5 },
|
||||
{ "id": 3, "name": "Jelena K.", "location": "Belgrad", "text": "Manoon Nachtserum ist pure Magie. Aufwachen mit strahlender Haut jeden Morgen.", "rating": 5 },
|
||||
{ "id": 4, "name": "Stefan R.", "location": "Subotica", "text": "Das Anti-Age Set ist jeden Dinar wert. Meine Frau und ich benutzen es beide.", "rating": 5 },
|
||||
{ "id": 5, "name": "Marija T.", "location": "Kragujevac", "text": "Endlich ein Serum gefunden, das wirklich funktioniert! Manoon hält seine Versprechen.", "rating": 5 }
|
||||
]
|
||||
},
|
||||
"TrustBadges": {
|
||||
"averageRating": "Durchschnittliche Bewertung",
|
||||
"basedOnReviews": "Basierend auf 1000+ Bewertungen",
|
||||
"happyCustomers": "Zufriedene Kunden",
|
||||
"worldwide": "Weltweit",
|
||||
"naturalIngredients": "Natürliche Inhaltsstoffe",
|
||||
"noAdditives": "Keine Zusatzstoffe",
|
||||
"freeShipping": "Kostenloser Versand",
|
||||
"ordersOver": "Bestellungen über 3.000 RSD"
|
||||
},
|
||||
"ProblemSection": {
|
||||
"title": "Das Problem",
|
||||
"subtitle": "Müde von Haar- & Hautprodukten, die nicht liefern?",
|
||||
"description": "Sie verdienen mehr als Produkte voller aggressiver Chemikalien und leerer Versprechen",
|
||||
"problems": [
|
||||
{
|
||||
"problem": "Trockenes, beschädigtes Haar",
|
||||
"description": "Produkte hinterlassen Ihr Haar brüchig, frizzig und brechend trotz teurer Behandlungen"
|
||||
},
|
||||
{
|
||||
"problem": "Verwirrende Inhaltsstoffe",
|
||||
"description": "Sie können nicht aussprechen, was in Ihrer Hautpflege ist. Parabene, Sulfate, synthetische Duftstoffe - gefährliche Toxine"
|
||||
},
|
||||
{
|
||||
"problem": "Keine echten Ergebnisse",
|
||||
"description": "Unzählige Produkte versprechen Wunder, aber liefern nur leere Versprechen und verschwendetes Geld"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsSeenIn": {
|
||||
"title": "Wie gesehen in"
|
||||
},
|
||||
"BeforeAfterGallery": {
|
||||
"realResults": "Echte Ergebnisse",
|
||||
"seeTransformation": "Sehen Sie die Transformation",
|
||||
"startTransformation": "Starten Sie Ihre Transformation",
|
||||
"before": "VORHER",
|
||||
"after": "NACHHER",
|
||||
"verified": "Verifiziert",
|
||||
"timeline": "Nach {weeks}"
|
||||
},
|
||||
"HowItWorks": {
|
||||
"title": "Einfacher Prozess",
|
||||
"subtitle": "Wie ManoonOils funktioniert",
|
||||
"startTransformation": "Starten Sie Ihre Transformation",
|
||||
"steps": [
|
||||
{
|
||||
"title": "Wählen Sie Ihr Öl",
|
||||
"description": "Wählen Sie aus unserer Kollektion von reinen, kaltgepressten Ölen, die für Ihre spezifischen Haar- und Hautbedürfnisse formuliert sind."
|
||||
},
|
||||
{
|
||||
"title": "Täglich anwenden",
|
||||
"description": "Massieren Sie einige Tropfen in feuchtes Haar oder Haut. Unsere Öle ziehen sofort ein - nie fettig, immer pflegend."
|
||||
},
|
||||
{
|
||||
"title": "Ergebnisse sehen",
|
||||
"description": "Erleben Sie Transformation in 4-6 Wochen. Glänzenderes Haar, strahlende Haut und Selbstvertrauen, das strahlt."
|
||||
}
|
||||
]
|
||||
},
|
||||
"Header": {
|
||||
"products": "Produkte",
|
||||
"about": "Über uns",
|
||||
"contact": "Kontakt",
|
||||
"cart": "Warenkorb",
|
||||
"account": "Konto",
|
||||
"openMenu": "Menü öffnen",
|
||||
"closeMenu": "Menü schließen",
|
||||
"openCart": "Warenkorb öffnen"
|
||||
},
|
||||
"ProductCard": {
|
||||
"noImage": "Kein Bild",
|
||||
"outOfStock": "Nicht auf Lager",
|
||||
"quickAdd": "Schnell hinzufügen",
|
||||
"contactForPrice": "Preis anfragen"
|
||||
},
|
||||
"ProductDetail": {
|
||||
"home": "Startseite",
|
||||
"outOfStock": "Nicht auf Lager",
|
||||
"size": "Größe",
|
||||
"qty": "Menge",
|
||||
"adding": "Wird hinzugefügt...",
|
||||
"transformHairSkin": "Mein Haar & Haut transformieren",
|
||||
"freeShipping": "Kostenloser Versand bei Bestellungen über 3.000 RSD",
|
||||
"guarantee": "30-Tage-Garantie",
|
||||
"secureCheckout": "Sicheres Bezahlen",
|
||||
"easyReturns": "Einfache Rückgabe",
|
||||
"benefits": "Vorteile",
|
||||
"description": "Beschreibung",
|
||||
"howToUse": "Anwendung",
|
||||
"howToUseText": "Eine kleine Menge auf saubere, feuchte Haut oder Haare auftragen. Sanft einmassieren, bis es eingezogen ist. Täglich für beste Ergebnisse verwenden.",
|
||||
"ingredients": "Inhaltsstoffe",
|
||||
"ingredientsText": "100% Reines Natürliches Öl. Keine Zusatzstoffe, Konservierungsstoffe oder künstliche Duftstoffe.",
|
||||
"youMayAlsoLike": "Das könnte Ihnen auch gefallen",
|
||||
"similarProducts": "Ähnliche Produkte",
|
||||
"stocksRunningOut": "Vorräte gehen zur Neige!",
|
||||
"urgency1": "Beeilen Sie sich! 500+ Artikel in den letzten 3 Tagen verkauft!",
|
||||
"urgency2": "In den Warenkörben von 2,5K Menschen - kaufen Sie, bevor es weg ist!",
|
||||
"urgency3": "7.562 Personen haben sich dieses Produkt in den letzten 24 Stunden angesehen!"
|
||||
},
|
||||
"Newsletter": {
|
||||
"stayConnected": "Bleiben Sie verbunden",
|
||||
"joinCommunity": "Werden Sie Teil unserer Gemeinschaft",
|
||||
"newsletterText": "Abonnieren Sie, um exklusive Angebote, Schönheitstipps zu erhalten und als Erster über neue Produkte informiert zu werden.",
|
||||
"emailPlaceholder": "Geben Sie Ihre E-Mail ein",
|
||||
"subscribe": "Abonnieren"
|
||||
},
|
||||
"ProductBenefits": {
|
||||
"whyChoose": "Warum dieses Produkt wählen",
|
||||
"manoonDifference": "Der Manoon Unterschied",
|
||||
"pureNatural": "Rein & Natürlich",
|
||||
"pureNaturalDesc": "100% natürliche Inhaltsstoffe ohne Zusatzstoffe oder Konservierungsstoffe",
|
||||
"crueltyFree": "Tierversuchsfrei",
|
||||
"crueltyFreeDesc": "Nie an Tieren getestet, ethisch beschaffte Inhaltsstoffe",
|
||||
"madeWithLove": "Mit Liebe hergestellt",
|
||||
"madeWithLoveDesc": "In kleinen Chargen handgefertigt für maximale Qualität",
|
||||
"visibleResults": "Sichtbare Ergebnisse",
|
||||
"visibleResultsDesc": "Erkennbare Verbesserungen in 4-6 Wochen"
|
||||
},
|
||||
"Checkout": {
|
||||
"checkout": "Kasse",
|
||||
"shippingAddress": "Lieferadresse",
|
||||
"firstName": "Vorname",
|
||||
"lastName": "Nachname",
|
||||
"streetAddress": "Straße und Nummer",
|
||||
"streetAddressOptional": "Wohnung, Suite, etc. (optional)",
|
||||
"city": "Stadt",
|
||||
"postalCode": "Postleitzahl",
|
||||
"phone": "Telefon",
|
||||
"billingAddressSame": "Rechnungsadresse gleich Lieferadresse",
|
||||
"billingAddress": "Rechnungsadresse",
|
||||
"paymentMethod": "Zahlungsmethode",
|
||||
"cashOnDelivery": "Nachnahme (COD)",
|
||||
"cashOnDeliveryDesc": "Bezahlen Sie, wenn Ihre Bestellung an Ihre Tür geliefert wird.",
|
||||
"processing": "Wird bearbeitet...",
|
||||
"completeOrder": "Bestellung abschließen - {total}",
|
||||
"orderSummary": "Bestellübersicht",
|
||||
"qty": "Menge",
|
||||
"subtotal": "Zwischensumme",
|
||||
"shipping": "Versand",
|
||||
"calculated": "Berechnet",
|
||||
"total": "Gesamt",
|
||||
"yourCartEmpty": "Ihr Warenkorb ist leer",
|
||||
"continueShopping": "Weiter einkaufen",
|
||||
"errorNoCheckout": "Keine aktive Kasse. Bitte versuchen Sie es erneut.",
|
||||
"errorOccurred": "Ein Fehler ist during des Checkouts aufgetreten.",
|
||||
"errorCreatingOrder": "Bestellung konnte nicht erstellt werden.",
|
||||
"orderConfirmed": "Bestellung bestätigt!",
|
||||
"thankYou": "Vielen Dank für Ihren Einkauf.",
|
||||
"orderNumber": "Bestellnummer",
|
||||
"confirmationEmail": "Sie erhalten in Kürze eine Bestätigungs-E-Mail. Wir werden Sie kontaktieren, um Nachnahme zu arrangieren.",
|
||||
"continueShoppingBtn": "Weiter einkaufen"
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,52 @@
|
||||
"Home": {
|
||||
"hero": {
|
||||
"title": "Premium Natural Oils",
|
||||
"subtitle": "For hair and skin care"
|
||||
"subtitle": "For hair and skin care",
|
||||
"lovedBy": "Loved by 50,000+ customers worldwide",
|
||||
"transformHeadline": "Transform Your Hair & Skin",
|
||||
"withNaturalOils": "with 100% Natural Oils",
|
||||
"subtitleText": "Cold-pressed, organic oils handcrafted with love. No additives, no preservatives—just nature's purest goodness for your daily beauty ritual.",
|
||||
"ctaButton": "Transform My Hair & Skin",
|
||||
"learnStory": "Learn Our Story",
|
||||
"moneyBack": "30-Day Money Back",
|
||||
"freeShipping": "Free Shipping Over 3,000 RSD",
|
||||
"crueltyFree": "Cruelty Free"
|
||||
},
|
||||
"ticker": {
|
||||
"text": "Free shipping on orders over 3000 RSD • Natural ingredients • Cruelty-free • Handmade with love"
|
||||
},
|
||||
"products": {
|
||||
"title": "Our Products"
|
||||
},
|
||||
"bestsellers": {
|
||||
"title": "Best Sellers"
|
||||
},
|
||||
"story": {
|
||||
"title": "Our Story",
|
||||
"description": "ManoonOils was born from a passion for natural beauty..."
|
||||
},
|
||||
"newsletter": {
|
||||
"title": "Stay Updated",
|
||||
"placeholder": "Enter your email",
|
||||
"button": "Subscribe"
|
||||
}
|
||||
"collection": "Our Collection",
|
||||
"premiumOils": "Premium Natural Oils",
|
||||
"oilsDescription": "Cold-pressed, pure, and natural oils for your daily beauty routine",
|
||||
"viewAll": "View All Products",
|
||||
"ourStory": "Our Story",
|
||||
"handmadeWithLove": "Handmade with Love",
|
||||
"storyText1": "Every bottle of ManoonOils is crafted with care using traditional methods passed down through generations. We source only the finest organic ingredients to bring you oils that nourish both hair and skin.",
|
||||
"storyText2": "Our commitment to purity means no additives, no preservatives - just nature's goodness in its most potent form.",
|
||||
"learnMore": "Learn More",
|
||||
"whyChooseUs": "Why Choose Us",
|
||||
"manoonDifference": "The Manoon Difference",
|
||||
"stayConnected": "Stay Connected",
|
||||
"joinCommunity": "Join Our Community",
|
||||
"newsletterText": "Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"subscribe": "Subscribe"
|
||||
},
|
||||
"Benefits": {
|
||||
"natural": "100% Natural",
|
||||
"naturalDesc": "Pure, cold-pressed oils with no additives or preservatives. Just nature's goodness.",
|
||||
"handcrafted": "Handcrafted",
|
||||
"handcraftedDesc": "Each batch is carefully prepared by hand to ensure the highest quality.",
|
||||
"sustainable": "Sustainable",
|
||||
"sustainableDesc": "Ethically sourced ingredients and eco-friendly packaging for a better planet."
|
||||
},
|
||||
"Products": {
|
||||
"collection": "Our Collection",
|
||||
"allProducts": "All Products",
|
||||
"productsCount": "{count} products",
|
||||
"featured": "Featured",
|
||||
"newest": "Newest",
|
||||
"priceLow": "Price: Low to High",
|
||||
"priceHigh": "Price: High to Low",
|
||||
"noProducts": "No products available",
|
||||
"checkBack": "Please check back later for new arrivals."
|
||||
},
|
||||
"Product": {
|
||||
"addToCart": "Add to Cart",
|
||||
@@ -35,19 +61,366 @@
|
||||
"details": "Details",
|
||||
"ingredients": "Ingredients",
|
||||
"usage": "How to Use",
|
||||
"related": "You May Also Like"
|
||||
"related": "You May Also Like",
|
||||
"notFound": "Product not found",
|
||||
"notFoundDesc": "The product you're looking for doesn't exist or has been removed.",
|
||||
"browseProducts": "Browse Products"
|
||||
},
|
||||
"Cart": {
|
||||
"title": "Your Cart",
|
||||
"empty": "Your cart is empty",
|
||||
"emptyDesc": "Looks like you haven't added anything to your cart yet.",
|
||||
"continueShopping": "Continue Shopping",
|
||||
"checkout": "Checkout",
|
||||
"subtotal": "Subtotal",
|
||||
"remove": "Remove"
|
||||
"shipping": "Shipping",
|
||||
"shippingCalc": "Calculated at checkout",
|
||||
"total": "Total",
|
||||
"freeShipping": "Free shipping on orders over {amount}",
|
||||
"remove": "Remove",
|
||||
"processes": "Processing...",
|
||||
"cartEmpty": "Your cart is empty"
|
||||
},
|
||||
"About": {
|
||||
"title": "About Us",
|
||||
"subtitle": "Our Story",
|
||||
"intro": "ManoonOils was born from a passion for natural beauty and the belief that the best skincare comes from nature itself.",
|
||||
"intro2": "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.",
|
||||
"naturalIngredients": "Natural Ingredients",
|
||||
"naturalIngredientsDesc": "We use only the finest natural ingredients, sourced ethically and sustainably from trusted suppliers around the world.",
|
||||
"crueltyFree": "Cruelty-Free",
|
||||
"crueltyFreeDesc": "Our products are never tested on animals. We believe in beauty without compromise.",
|
||||
"sustainablePackaging": "Sustainable Packaging",
|
||||
"sustainablePackagingDesc": "We use eco-friendly packaging materials and minimize waste throughout our production process.",
|
||||
"handcraftedQuality": "Handcrafted Quality",
|
||||
"handcraftedQualityDesc": "Every bottle is handcrafted in small batches to ensure the highest quality and freshness.",
|
||||
"mission": "Our Mission",
|
||||
"missionQuote": "\"To provide premium quality, natural products that enhance your daily beauty routine.\"",
|
||||
"handmadeTitle": "Handmade with Love",
|
||||
"handmadeText1": "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.",
|
||||
"handmadeText2": "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."
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Contact",
|
||||
"subtitle": "Get in Touch",
|
||||
"getInTouch": "Get in Touch",
|
||||
"getInTouchDesc": "We're here to help! Whether you have questions about our products, need assistance with an order, or just want to say hello, we'd love to hear from you.",
|
||||
"email": "Email",
|
||||
"emailReply": "We reply within 24 hours",
|
||||
"shippingTitle": "Shipping",
|
||||
"freeShipping": "Free shipping over £50",
|
||||
"deliveryTime": "Delivered within 2-5 business days",
|
||||
"location": "Location",
|
||||
"locationDesc": "Serbia",
|
||||
"worldwideShipping": "Shipping worldwide",
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Your name",
|
||||
"emailField": "Email",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"message": "Message",
|
||||
"messagePlaceholder": "How can we help you?",
|
||||
"sendMessage": "Send Message",
|
||||
"thankYou": "Thank You!",
|
||||
"thankYouDesc": "Your message has been sent. We'll get back to you soon.",
|
||||
"faqTitle": "Frequently Asked Questions",
|
||||
"faq1q": "How long does shipping take?",
|
||||
"faq1a": "Orders are typically delivered within 2-5 business days for domestic shipping. You'll receive a tracking number once your order ships.",
|
||||
"faq2q": "Are your products 100% natural?",
|
||||
"faq2a": "Yes! All our oils are 100% natural, cold-pressed, and free from any additives, preservatives, or artificial fragrances.",
|
||||
"faq3q": "What is your return policy?",
|
||||
"faq3a": "We accept returns within 14 days of delivery for unopened products. Please contact us if you have any issues with your order.",
|
||||
"faq4q": "Do you offer wholesale?",
|
||||
"faq4a": "Yes, we offer wholesale pricing for bulk orders. Please contact us at hello@manoonoils.com for more information."
|
||||
},
|
||||
"Footer": {
|
||||
"quickLinks": "Quick Links",
|
||||
"customerService": "Customer Service",
|
||||
"copyright": "All rights reserved."
|
||||
"contact": "Contact",
|
||||
"shipping": "Shipping",
|
||||
"returns": "Returns",
|
||||
"faq": "FAQ",
|
||||
"followUs": "Follow Us",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterDesc": "Subscribe to our newsletter for exclusive offers and updates.",
|
||||
"copyright": "All rights reserved.",
|
||||
"allRights": "© {year} ManoonOils."
|
||||
},
|
||||
"Common": {
|
||||
"loading": "Loading...",
|
||||
"error": "An error occurred",
|
||||
"tryAgain": "Try again",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"search": "Search",
|
||||
"noResults": "No results found"
|
||||
},
|
||||
"Testimonials": {
|
||||
"title": "What our customers say",
|
||||
"verified": "Verified purchase",
|
||||
"reviews": [
|
||||
{
|
||||
"name": "Sarah M.",
|
||||
"skinType": "Dry, sensitive skin",
|
||||
"text": "I've tried countless oils over the years, but ManoonOils is different. My skin has never felt this nourished and healthy. The argan oil is now a staple in my routine."
|
||||
},
|
||||
{
|
||||
"name": "James K.",
|
||||
"skinType": "Hair care enthusiast",
|
||||
"text": "Finally found an oil that actually tames my frizz without making my hair greasy. The jojoba oil works wonders for my beard too. Highly recommend!"
|
||||
},
|
||||
{
|
||||
"name": "Emma L.",
|
||||
"skinType": "Combination skin",
|
||||
"text": "Was skeptical at first but after 3 weeks of using the rosehip oil, my skin texture has improved dramatically. The quality is unmatched."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProductReviews": {
|
||||
"customerReviews": "Customer Reviews",
|
||||
"whatCustomersSay": "What Customers Say",
|
||||
"basedOnReviews": "Based on 1000+ reviews",
|
||||
"reviews": [
|
||||
{ "id": 1, "name": "Ana M.", "location": "Belgrade", "text": "Manoon Anti-age Serum transformed my skin in just 2 weeks!", "rating": 5 },
|
||||
{ "id": 2, "name": "Milica P.", "location": "Novi Sad", "text": "The best day serum I've ever used. My wrinkles are visibly reduced.", "rating": 5 },
|
||||
{ "id": 3, "name": "Jelena K.", "location": "Belgrade", "text": "Manoon night serum is pure magic. Wake up with glowing skin every morning.", "rating": 5 },
|
||||
{ "id": 4, "name": "Stefan R.", "location": "Subotica", "text": "The Anti-age Set is worth every dinar. My wife and I both use it.", "rating": 5 },
|
||||
{ "id": 5, "name": "Marija T.", "location": "Kragujevac", "text": "Finally found a serum that actually works! Manoon delivers on its promises.", "rating": 5 },
|
||||
{ "id": 6, "name": "Nikola V.", "location": "Niš", "text": "My fine lines are disappearing. This day serum is incredible.", "rating": 5 },
|
||||
{ "id": 7, "name": "Ivana L.", "location": "Belgrade", "text": "Manoon morning glow serum smells divine and works even better.", "rating": 5 },
|
||||
{ "id": 8, "name": "Dejan M.", "location": "Novi Sad", "text": "The night serum has transformed my skincare routine completely.", "rating": 5 },
|
||||
{ "id": 9, "name": "Sanja B.", "location": "Kragujevac", "text": "My skin looks 10 years younger after using Manoon for a month.", "rating": 5 },
|
||||
{ "id": 10, "name": "Marko J.", "location": "Subotica", "text": "The anti-age set makes a perfect gift. My mother loves it!", "rating": 5 },
|
||||
{ "id": 11, "name": "Petra D.", "location": "Niš", "text": "The texture of Manoon serum is so luxurious. Worth every penny.", "rating": 5 },
|
||||
{ "id": 12, "name": "Luka G.", "location": "Belgrade", "text": "Day serum absorbs instantly. No greasy feeling at all!", "rating": 5 },
|
||||
{ "id": 13, "name": "Maja S.", "location": "Novi Sad", "text": "My esthetician asked what I'm using. Manoon is now my secret!", "rating": 5 },
|
||||
{ "id": 14, "name": "Vladimir P.", "location": "Kragujevac", "text": "The night serum works while I sleep. Wake up to visibly smoother skin.", "rating": 5 },
|
||||
{ "id": 15, "name": "Katarina N.", "location": "Subotica", "text": "The Anti-age Set arrived beautifully packaged. Perfect for gifting.", "rating": 5 },
|
||||
{ "id": 16, "name": "Bojan R.", "location": "Niš", "text": "Been using Manoon for 3 months. My wrinkles are noticeably reduced.", "rating": 5 },
|
||||
{ "id": 17, "name": "Tamara F.", "location": "Belgrade", "text": "The day serum provides the perfect base under makeup.", "rating": 5 },
|
||||
{ "id": 18, "name": "Aleksandar K.", "location": "Novi Sad", "text": "Finally a Serbian brand that competes with luxury international brands!", "rating": 5 },
|
||||
{ "id": 19, "name": "Natalia M.", "location": "Kragujevac", "text": "My sensitive skin loves Manoon. No irritation at all.", "rating": 5 },
|
||||
{ "id": 20, "name": "Filip T.", "location": "Subotica", "text": "The anti-age serum is lightweight yet incredibly effective.", "rating": 5 },
|
||||
{ "id": 21, "name": "Andrea L.", "location": "Niš", "text": "Manoon night serum is my evening ritual. Skin looks amazing!", "rating": 5 },
|
||||
{ "id": 22, "name": "Ognjen P.", "location": "Belgrade", "text": "My friends keep asking what changed in my skincare routine.", "rating": 5 },
|
||||
{ "id": 23, "name": "Mila J.", "location": "Novi Sad", "text": "The Anti-age Set includes everything you need. Great value!", "rating": 5 },
|
||||
{ "id": 24, "name": "Dragan S.", "location": "Kragujevac", "text": "Even my husband noticed the difference. He now uses the day serum too!", "rating": 5 },
|
||||
{ "id": 25, "name": "Jovana V.", "location": "Subotica", "text": "The morning glow serum gives the most beautiful luminosity.", "rating": 5 },
|
||||
{ "id": 26, "name": "Stefan M.", "location": "Niš", "text": "Manoon products are now essential in my daily routine.", "rating": 5 },
|
||||
{ "id": 27, "name": "Ana R.", "location": "Belgrade", "text": "The night serum helped clear my complexion. Skin looks so healthy!", "rating": 5 },
|
||||
{ "id": 28, "name": "Nenad L.", "location": "Novi Sad", "text": "Anti-aging results visible within weeks. Highly recommend Manoon!", "rating": 5 },
|
||||
{ "id": 29, "name": "Sofija D.", "location": "Kragujevac", "text": "The texture is divine. Feels like a luxury spa treatment at home.", "rating": 5 },
|
||||
{ "id": 30, "name": "Velibor K.", "location": "Subotica", "text": "My crow's feet have diminished significantly. Thank you Manoon!", "rating": 5 },
|
||||
{ "id": 31, "name": "Irena M.", "location": "Niš", "text": "The Anti-age Set makes the perfect birthday gift for my mother.", "rating": 5 },
|
||||
{ "id": 32, "name": "Radoslav P.", "location": "Belgrade", "text": "Professional quality serum at an honest price. Serbian excellence!", "rating": 5 },
|
||||
{ "id": 33, "name": "Jelena B.", "location": "Novi Sad", "text": "My skin has never been this hydrated. Day serum is amazing!", "rating": 5 },
|
||||
{ "id": 34, "name": "Dimitrije S.", "location": "Kragujevac", "text": "The night serum is worth its weight in gold. Pure luxury!", "rating": 5 },
|
||||
{ "id": 35, "name": "Minela G.", "location": "Subotica", "text": "Manoon lives up to the hype. My skin looks refreshed and young.", "rating": 5 },
|
||||
{ "id": 36, "name": "Zoran T.", "location": "Niš", "text": "I've tried many serums. Manoon is by far the most effective.", "rating": 5 },
|
||||
{ "id": 37, "name": "Mirjana F.", "location": "Belgrade", "text": "The Anti-age Set transformed my mother's skincare routine completely.", "rating": 5 },
|
||||
{ "id": 38, "name": "Ivan J.", "location": "Novi Sad", "text": "Fast-acting serum with real results. I recommend Manoon to everyone.", "rating": 5 },
|
||||
{ "id": 39, "name": "Kristina P.", "location": "Kragujevac", "text": "The morning glow serum gives such a beautiful dewy finish.", "rating": 5 },
|
||||
{ "id": 40, "name": "Bratislav L.", "location": "Subotica", "text": "Noticeable results in just 2 weeks. This serum is the real deal!", "rating": 5 },
|
||||
{ "id": 41, "name": "Zorica M.", "location": "Niš", "text": "The night serum erased years from my face. Absolutely miraculous!", "rating": 5 },
|
||||
{ "id": 42, "name": "Patrik N.", "location": "Belgrade", "text": "Premium quality Serbian skincare that rivals international luxury brands.", "rating": 5 },
|
||||
{ "id": 43, "name": "Simona K.", "location": "Novi Sad", "text": "Manoon Anti-age Serum is the best investment in my skin ever.", "rating": 5 },
|
||||
{ "id": 44, "name": "Mladen D.", "location": "Kragujevac", "text": "The day serum absorbs in seconds. No waiting around!", "rating": 5 },
|
||||
{ "id": 45, "name": "Ljiljana R.", "location": "Subotica", "text": "Gifting the Anti-age Set to my sisters. They loved it!", "rating": 5 },
|
||||
{ "id": 46, "name": "Tomislav V.", "location": "Niš", "text": "My wrinkles are visibly reduced after using Manoon for a month.", "rating": 5 },
|
||||
{ "id": 47, "name": "Emilija S.", "location": "Belgrade", "text": "The night serum leaves my skin so soft and renewed every morning.", "rating": 5 },
|
||||
{ "id": 48, "name": "Andrija P.", "location": "Novi Sad", "text": "Manoon day serum is perfect under sunscreen. Essential duo!", "rating": 5 },
|
||||
{ "id": 49, "name": "Miona L.", "location": "Kragujevac", "text": "My skin looks radiant and youthful. Couldn't be happier with Manoon!", "rating": 5 },
|
||||
{ "id": 50, "name": "Slavko M.", "location": "Subotica", "text": "The Anti-age Set delivers visible results. True Serbian quality!", "rating": 5 }
|
||||
]
|
||||
},
|
||||
"TrustBadges": {
|
||||
"averageRating": "Average Rating",
|
||||
"basedOnReviews": "Based on 1000+ reviews",
|
||||
"happyCustomers": "Happy Customers",
|
||||
"worldwide": "Worldwide",
|
||||
"naturalIngredients": "Natural Ingredients",
|
||||
"noAdditives": "No additives",
|
||||
"freeShipping": "Free Shipping",
|
||||
"ordersOver": "Orders over 3,000 RSD"
|
||||
},
|
||||
"ProblemSection": {
|
||||
"title": "The Problem",
|
||||
"subtitle": "Tired of Hair & Skin Products That Don't Deliver?",
|
||||
"description": "You deserve better than products filled with harsh chemicals and empty promises",
|
||||
"problems": [
|
||||
{
|
||||
"problem": "Dry, Damaged Hair",
|
||||
"description": "Products leave your hair brittle, frizzy, and breaking despite expensive treatments"
|
||||
},
|
||||
{
|
||||
"problem": "Confusing Ingredients",
|
||||
"description": "Can't pronounce what's in your skincare. parabens, sulfates, synthetic fragrances—dangerous toxins"
|
||||
},
|
||||
{
|
||||
"problem": "No Real Results",
|
||||
"description": "Countless products promise miracles but deliver nothing but empty promises and wasted money"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsSeenIn": {
|
||||
"title": "As Featured In"
|
||||
},
|
||||
"BeforeAfterGallery": {
|
||||
"realResults": "Real Results",
|
||||
"seeTransformation": "See the Transformation",
|
||||
"startTransformation": "Start Your Transformation",
|
||||
"before": "BEFORE",
|
||||
"after": "AFTER",
|
||||
"verified": "Verified",
|
||||
"timeline": "After {weeks}"
|
||||
},
|
||||
"HowItWorks": {
|
||||
"title": "Simple Process",
|
||||
"subtitle": "How ManoonOils Works",
|
||||
"startTransformation": "Start Your Transformation",
|
||||
"steps": [
|
||||
{
|
||||
"title": "Choose Your Oil",
|
||||
"description": "Select from our collection of pure, cold-pressed oils formulated for your specific hair and skin needs."
|
||||
},
|
||||
{
|
||||
"title": "Apply Daily",
|
||||
"description": "Massage a few drops into damp hair or skin. Our oils absorb instantly—never greasy, always nourishing."
|
||||
},
|
||||
{
|
||||
"title": "See Results",
|
||||
"description": "Experience transformation in 4-6 weeks. Shinier hair, radiant skin, and confidence that glows."
|
||||
}
|
||||
]
|
||||
},
|
||||
"Header": {
|
||||
"products": "Products",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"cart": "Cart",
|
||||
"account": "Account",
|
||||
"openMenu": "Open menu",
|
||||
"closeMenu": "Close menu",
|
||||
"openCart": "Open cart"
|
||||
},
|
||||
"Footer": {
|
||||
"shop": "Shop",
|
||||
"allProducts": "All Products",
|
||||
"hairCare": "Hair Care",
|
||||
"skinCare": "Skin Care",
|
||||
"giftSets": "Gift Sets",
|
||||
"about": "About",
|
||||
"ourStory": "Our Story",
|
||||
"process": "Process",
|
||||
"sustainability": "Sustainability",
|
||||
"help": "Help",
|
||||
"faq": "FAQ",
|
||||
"shipping": "Shipping",
|
||||
"returns": "Returns",
|
||||
"contactUs": "Contact Us",
|
||||
"brandDescription": "Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.",
|
||||
"weAccept": "We accept:",
|
||||
"allRights": "All rights reserved."
|
||||
},
|
||||
"ProductCard": {
|
||||
"noImage": "No image",
|
||||
"outOfStock": "Out of Stock",
|
||||
"quickAdd": "Quick Add",
|
||||
"contactForPrice": "Contact for price"
|
||||
},
|
||||
"ProductDetail": {
|
||||
"home": "Home",
|
||||
"outOfStock": "Out of Stock",
|
||||
"size": "Size",
|
||||
"qty": "Qty",
|
||||
"adding": "Adding...",
|
||||
"transformHairSkin": "Transform My Hair & Skin",
|
||||
"freeShipping": "Free shipping on orders over 3,000 RSD",
|
||||
"guarantee": "30-Day Guarantee",
|
||||
"secureCheckout": "Secure Checkout",
|
||||
"easyReturns": "Easy Returns",
|
||||
"benefits": "Benefits",
|
||||
"description": "Description",
|
||||
"howToUse": "How to Use",
|
||||
"howToUseText": "Apply a small amount to clean, damp hair or skin. Massage gently until absorbed. Use daily for best results.",
|
||||
"ingredients": "Ingredients",
|
||||
"ingredientsText": "100% Pure Natural Oil. No additives, preservatives, or artificial fragrances.",
|
||||
"youMayAlsoLike": "You May Also Like",
|
||||
"similarProducts": "Similar Products",
|
||||
"stocksRunningOut": "Stocks are running out!",
|
||||
"urgency1": "Hurry up! 500+ items sold in the last 3 days!",
|
||||
"urgency2": "In the carts of 2.5K people - buy before its gone!",
|
||||
"urgency3": "7,562 people viewed this product in the last 24 hours!"
|
||||
},
|
||||
"Newsletter": {
|
||||
"stayConnected": "Stay Connected",
|
||||
"joinCommunity": "Join Our Community",
|
||||
"newsletterText": "Subscribe to receive exclusive offers, beauty tips, and be the first to know about new products.",
|
||||
"emailPlaceholder": "Enter your email",
|
||||
"subscribe": "Subscribe"
|
||||
},
|
||||
"ProductBenefits": {
|
||||
"whyChoose": "Why Choose This Product",
|
||||
"manoonDifference": "The Manoon Difference",
|
||||
"pureNatural": "Pure & Natural",
|
||||
"pureNaturalDesc": "100% natural ingredients with no additives or preservatives",
|
||||
"crueltyFree": "Cruelty Free",
|
||||
"crueltyFreeDesc": "Never tested on animals, ethically sourced ingredients",
|
||||
"madeWithLove": "Made with Love",
|
||||
"madeWithLoveDesc": "Handcrafted in small batches for maximum quality",
|
||||
"visibleResults": "Visible Results",
|
||||
"visibleResultsDesc": "See noticeable improvements in 4-6 weeks"
|
||||
},
|
||||
"Cart": {
|
||||
"yourCart": "Your Cart",
|
||||
"closeCart": "Close cart",
|
||||
"dismiss": "Dismiss",
|
||||
"yourCartEmpty": "Your cart is empty",
|
||||
"looksLikeEmpty": "Looks like you haven't added anything to your cart yet.",
|
||||
"startShopping": "Start Shopping",
|
||||
"subtotal": "Subtotal",
|
||||
"shipping": "Shipping",
|
||||
"calculatedAtCheckout": "Calculated at checkout",
|
||||
"total": "Total",
|
||||
"freeShippingOver": "Free shipping on orders over {amount}",
|
||||
"processing": "Processing...",
|
||||
"checkout": "Checkout",
|
||||
"continueShopping": "Continue Shopping",
|
||||
"removeItem": "Remove item"
|
||||
},
|
||||
"Checkout": {
|
||||
"checkout": "Checkout",
|
||||
"shippingAddress": "Shipping Address",
|
||||
"firstName": "First Name",
|
||||
"lastName": "Last Name",
|
||||
"streetAddress": "Street Address",
|
||||
"streetAddressOptional": "Apartment, suite, etc. (optional)",
|
||||
"city": "City",
|
||||
"postalCode": "Postal Code",
|
||||
"phone": "Phone",
|
||||
"billingAddressSame": "Billing address same as shipping",
|
||||
"billingAddress": "Billing Address",
|
||||
"paymentMethod": "Payment Method",
|
||||
"cashOnDelivery": "Cash on Delivery (COD)",
|
||||
"cashOnDeliveryDesc": "Pay when your order is delivered to your door.",
|
||||
"processing": "Processing...",
|
||||
"completeOrder": "Complete Order - {total}",
|
||||
"orderSummary": "Order Summary",
|
||||
"qty": "Qty",
|
||||
"subtotal": "Subtotal",
|
||||
"shipping": "Shipping",
|
||||
"calculated": "Calculated",
|
||||
"total": "Total",
|
||||
"yourCartEmpty": "Your cart is empty",
|
||||
"continueShopping": "Continue Shopping",
|
||||
"errorNoCheckout": "No active checkout. Please try again.",
|
||||
"errorOccurred": "An error occurred during checkout.",
|
||||
"errorCreatingOrder": "Failed to create order.",
|
||||
"orderConfirmed": "Order Confirmed!",
|
||||
"thankYou": "Thank you for your purchase.",
|
||||
"orderNumber": "Order Number",
|
||||
"confirmationEmail": "You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery.",
|
||||
"continueShoppingBtn": "Continue Shopping"
|
||||
}
|
||||
}
|
||||
|
||||
358
src/i18n/messages/fr.json
Normal file
358
src/i18n/messages/fr.json
Normal file
@@ -0,0 +1,358 @@
|
||||
{
|
||||
"Navigation": {
|
||||
"home": "Accueil",
|
||||
"products": "Produits",
|
||||
"about": "À propos",
|
||||
"contact": "Contact"
|
||||
},
|
||||
"Home": {
|
||||
"hero": {
|
||||
"title": "Huiles Naturelles Premium",
|
||||
"subtitle": "Pour les soins capillaires et cutanés",
|
||||
"lovedBy": "Apprécié par 50 000+ clients dans le monde",
|
||||
"transformHeadline": "Transformez Vos Cheveux & Peau",
|
||||
"withNaturalOils": "avec des Huiles 100% Naturelles",
|
||||
"subtitleText": "Huiles biologiques cold-pressed, artisanales avec amour. Sans additifs, sans conservateurs - juste la pureté de la nature pour votre rituel beauté quotidien.",
|
||||
"ctaButton": "Transformer Mes Cheveux & Ma Peau",
|
||||
"learnStory": "Découvrir Notre Histoire",
|
||||
"moneyBack": "30 Jours Satisfait",
|
||||
"freeShipping": "Livraison Gratuite +3.000 RSD",
|
||||
"crueltyFree": "Cruelty Free"
|
||||
},
|
||||
"collection": "Notre Collection",
|
||||
"premiumOils": "Huiles Naturelles Premium",
|
||||
"oilsDescription": "Huiles cold-pressed, pures et naturelles pour votre routine beauté quotidienne",
|
||||
"viewAll": "Voir Tous Les Produits",
|
||||
"ourStory": "Notre Histoire",
|
||||
"handmadeWithLove": "Fait Main avec Amour",
|
||||
"storyText1": "Chaque flacon de ManoonOils est crafted avec soin en utilisant des méthodes traditionnelles transmises de génération en génération. Nous aprovisonnons uniquement les meilleurs ingrédients biologiques pour vous apporter des huiles qui nourrissent les cheveux et la peau.",
|
||||
"storyText2": "Notre engagement envers la pureté signifie aucun additif, aucun conservateur - juste la bonté de la nature dans sa forme la plus potente.",
|
||||
"learnMore": "En Savoir Plus",
|
||||
"whyChooseUs": "Pourquoi Nous Choisir",
|
||||
"manoonDifference": "La Différence Manoon",
|
||||
"stayConnected": "Restez Connectés",
|
||||
"joinCommunity": "Rejoignez Notre Communauté",
|
||||
"newsletterText": "Abonnez-vous pour recevoir des offres exclusives, des conseils beauté et être les premiers informés des nouveaux produits.",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"subscribe": "S'abonner"
|
||||
},
|
||||
"Benefits": {
|
||||
"natural": "100% Naturel",
|
||||
"naturalDesc": "Huiles pures cold-pressed sans additifs ni conservateurs. Juste la bonté de la nature.",
|
||||
"handcrafted": "Artisanal",
|
||||
"handcraftedDesc": "Chaque lot est soigneusement préparé à la main pour assurer la plus haute qualité.",
|
||||
"sustainable": "Durable",
|
||||
"sustainableDesc": "Ingrédients sourcés éthiquement et emballage écologique pour une meilleure planète."
|
||||
},
|
||||
"Products": {
|
||||
"collection": "Notre Collection",
|
||||
"allProducts": "Tous Les Produits",
|
||||
"productsCount": "{count} produits",
|
||||
"featured": "En Vedette",
|
||||
"newest": "Nouveautés",
|
||||
"priceLow": "Prix: Croissant",
|
||||
"priceHigh": "Prix: Décroissant",
|
||||
"noProducts": "Aucun produit disponible",
|
||||
"checkBack": "Veuillez vérifier plus tard pour les nouveaux arrivages."
|
||||
},
|
||||
"Product": {
|
||||
"addToCart": "Ajouter au Panier",
|
||||
"outOfStock": "Rupture de Stock",
|
||||
"details": "Détails",
|
||||
"ingredients": "Ingrédients",
|
||||
"usage": "Utilisation",
|
||||
"related": "Vous Aimerez Aussi",
|
||||
"notFound": "Produit non trouvé",
|
||||
"notFoundDesc": "Le produit que vous recherchez n'existe pas ou a été supprimé.",
|
||||
"browseProducts": "Parcourir les Produits"
|
||||
},
|
||||
"Cart": {
|
||||
"title": "Votre Panier",
|
||||
"empty": "Votre panier est vide",
|
||||
"emptyDesc": "Il semble que vous n'ayez pas encore ajouté d'articles à votre panier.",
|
||||
"continueShopping": "Continuer les Achats",
|
||||
"checkout": "Commander",
|
||||
"subtotal": "Sous-total",
|
||||
"shipping": "Livraison",
|
||||
"shippingCalc": "Calculé à la caisse",
|
||||
"total": "Total",
|
||||
"freeShipping": "Livraison gratuite sur les commandes de {amount}",
|
||||
"remove": "Supprimer",
|
||||
"processes": "En cours...",
|
||||
"cartEmpty": "Votre panier est vide"
|
||||
},
|
||||
"About": {
|
||||
"title": "À Propos",
|
||||
"subtitle": "Notre Histoire",
|
||||
"intro": "ManoonOils est né d'une passion pour la beauté naturelle et de la conviction que les meilleurs soins cutanés viennent de la nature elle-même.",
|
||||
"intro2": "Nous croyons au pouvoir des ingrédients naturels. Chaque huile de notre collection est soigneusement sélectionnée pour ses propriétés et bienfaits uniques. Des huiles nourrissantes qui restaurent la vitalité des cheveux aux sérums qui rajeunissent la peau, nous élaborons chaque produit avec amour et attention aux détails.",
|
||||
"naturalIngredients": "Ingrédients Naturels",
|
||||
"naturalIngredientsDesc": "Nous utilisons uniquement les meilleurs ingrédients naturels, sourcés de manière éthique et durable auprès de fournisseurs de confiance dans le monde entier.",
|
||||
"crueltyFree": "Cruelty Free",
|
||||
"crueltyFreeDesc": "Nos produits ne sont jamais testés sur les animaux. Nous croyons en la beauté sans compromis.",
|
||||
"sustainablePackaging": "Emballage Durable",
|
||||
"sustainablePackagingDesc": "Nous utilisons des matériaux d'emballage écologiques et minimisons les déchets tout au long de notre processus de production.",
|
||||
"handcraftedQuality": "Qualité Artisanale",
|
||||
"handcraftedQualityDesc": "Chaque flacon est fabriqué à la main en petites séries pour garantir la plus haute qualité et fraîcheur.",
|
||||
"mission": "Notre Mission",
|
||||
"missionQuote": "\"Fournir des produits premium de qualité, naturels qui améliorent votre routine beauté quotidienne.\"",
|
||||
"handmadeTitle": "Fait Main avec Amour",
|
||||
"handmadeText1": "Chaque flacon de ManoonOils est fabriqué à la main avec soin. Nous produisons nos produits en petites séries pour garantir la plus haute qualité et fraîcheur. Lorsque vous utilisez ManoonOils, vous pouvez être assuré d'utiliser quelque chose fabriqué avec un véritable souci et une expertise.",
|
||||
"handmadeText2": "Notre voyage a commencé par une question simple: comment pouvons-nous créer des produits qui truly nourrissent à la fois les cheveux et la peau? Aujourd'hui, nous continuons à innover tout en restant fidèles à notre engagement envers des solutions beauté naturelles et efficaces."
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Contact",
|
||||
"subtitle": "Contactez-nous",
|
||||
"getInTouch": "Contactez-nous",
|
||||
"getInTouchDesc": "Nous sommes là pour aider! Que vous ayez des questions sur nos produits, besoin d'aide avec une commande, ou simplement souhaitiez dire bonjour, nous aimerions avoir de vos nouvelles.",
|
||||
"email": "Email",
|
||||
"emailReply": "Nous répondons dans les 24 heures",
|
||||
"shippingTitle": "Livraison",
|
||||
"freeShipping": "Livraison gratuite +3.000 RSD",
|
||||
"deliveryTime": "Livré dans 2-5 jours ouvrables",
|
||||
"location": "Localisation",
|
||||
"locationDesc": "Serbie",
|
||||
"worldwideShipping": "Livraison dans le monde entier",
|
||||
"name": "Nom",
|
||||
"namePlaceholder": "Votre nom",
|
||||
"emailField": "Email",
|
||||
"emailPlaceholder": "votre@email.com",
|
||||
"message": "Message",
|
||||
"messagePlaceholder": "Comment pouvons-nous vous aider?",
|
||||
"sendMessage": "Envoyer le message",
|
||||
"thankYou": "Merci!",
|
||||
"thankYouDesc": "Votre message a été envoyé. Nous vous répondrons bientôt.",
|
||||
"faqTitle": "Questions Fréquemment Posées",
|
||||
"faq1q": "Combien de temps dure la livraison?",
|
||||
"faq1a": "Les commandes sont généralement livrées sous 2-5 jours ouvrables pour la livraison nationale. Vous recevrez un numéro de suivi dès que votre commande sera expédiée.",
|
||||
"faq2q": "Vos produits sont-ils 100% naturels?",
|
||||
"faq2a": "Oui! Toutes nos huiles sont 100% naturelles, cold-pressed et exemptes de tout additif, conservateur ou parfum artificiel.",
|
||||
"faq3q": "Quelle est votre politique de retour?",
|
||||
"faq3a": "Nous acceptons les retours dans les 14 jours suivant la livraison pour les produits non ouverts. Veuillez nous contacter si vous avez des problèmes avec votre commande.",
|
||||
"faq4q": "Offrez-vous des ventes en gros?",
|
||||
"faq4a": "Oui, nous offrons des prix de gros pour les commandes en grande quantité. Veuillez nous contacter à hello@manoonoils.com pour plus d'informations."
|
||||
},
|
||||
"Footer": {
|
||||
"quickLinks": "Liens Rapides",
|
||||
"customerService": "Service Client",
|
||||
"contact": "Contact",
|
||||
"shipping": "Livraison",
|
||||
"returns": "Retours",
|
||||
"faq": "FAQ",
|
||||
"followUs": "Suivez-nous",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterDesc": "Abonnez-vous à notre newsletter pour des offres exclusives et des mises à jour.",
|
||||
"copyright": "Tous droits réservés.",
|
||||
"allRights": "Tous droits réservés.",
|
||||
"shop": "Boutique",
|
||||
"allProducts": "Tous Les Produits",
|
||||
"hairCare": "Soins Capillaires",
|
||||
"skinCare": "Soins Cutanés",
|
||||
"giftSets": "Coffrets Cadeaux",
|
||||
"about": "À Propos",
|
||||
"ourStory": "Notre Histoire",
|
||||
"process": "Processus",
|
||||
"sustainability": "Durabilité",
|
||||
"help": "Aide",
|
||||
"contactUs": "Contactez-nous",
|
||||
"brandDescription": "Huiles naturelles premium pour les soins capillaires et cutanés. Fait main avec amour en utilisant des méthodes traditionnelles.",
|
||||
"weAccept": "Nous acceptons:"
|
||||
},
|
||||
"Common": {
|
||||
"loading": "Chargement...",
|
||||
"error": "Une erreur est survenue",
|
||||
"tryAgain": "Réessayer",
|
||||
"close": "Fermer",
|
||||
"back": "Retour",
|
||||
"next": "Suivant",
|
||||
"previous": "Précédent",
|
||||
"search": "Rechercher",
|
||||
"noResults": "Aucun résultat trouvé"
|
||||
},
|
||||
"Testimonials": {
|
||||
"title": "Ce que disent nos clients",
|
||||
"verified": "Achat vérifié",
|
||||
"reviews": [
|
||||
{
|
||||
"name": "Sarah M.",
|
||||
"skinType": "Peau sèche et sensible",
|
||||
"text": "J'ai essayé d'innombrables huiles au fil des ans, mais ManoonOils est différent. Ma peau n'a jamais été aussi nourrie et en bonne santé. L'huile d'argan est maintenant un élément essentiels de ma routine."
|
||||
},
|
||||
{
|
||||
"name": "James K.",
|
||||
"skinType": "Passionné de soins capillaires",
|
||||
"text": "Enfin trouvé une huile qui rassemble vraiment mes frisottis sans rendre mes cheveux gras. L'huile de jojoba fait des merveilles pour ma barbe aussi. Je recommande vivement!"
|
||||
},
|
||||
{
|
||||
"name": "Emma L.",
|
||||
"skinType": "Peau mixte",
|
||||
"text": "J'étais sceptique au début mais après 3 semaines d'utilisation de l'huile de rose musquée, la texture de ma peau s'est améliorée dramatiquement. La qualité est incomparable."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProductReviews": {
|
||||
"customerReviews": "Avis Clients",
|
||||
"whatCustomersSay": "Ce Que Disent Les Clients",
|
||||
"basedOnReviews": "Basé sur 1000+ avis",
|
||||
"reviews": [
|
||||
{ "id": 1, "name": "Ana M.", "location": "Belgrade", "text": "Le Sérum Anti-âge Manoon a transformé ma peau en seulement 2 semaines!", "rating": 5 },
|
||||
{ "id": 2, "name": "Milica P.", "location": "Novi Sad", "text": "Le meilleur sérum de jour que j'aie jamais utilisé. Mes rides sont visiblement réduites.", "rating": 5 },
|
||||
{ "id": 3, "name": "Jelena K.", "location": "Belgrade", "text": "Le sérum de nuit Manoon est de la magie pure. Réveillez-vous avec une peau qui rayonne chaque matin.", "rating": 5 },
|
||||
{ "id": 4, "name": "Stefan R.", "location": "Subotica", "text": "Le Set Anti-âge vaut chaque dinar. Ma femme et moi l'utilisons tous les deux.", "rating": 5 },
|
||||
{ "id": 5, "name": "Marija T.", "location": "Kragujevac", "text": "J'ai enfin trouvé un sérum qui fonctionne vraiment! Manoon tient ses promesses.", "rating": 5 }
|
||||
]
|
||||
},
|
||||
"TrustBadges": {
|
||||
"averageRating": "Note Moyenne",
|
||||
"basedOnReviews": "Basé sur 1000+ avis",
|
||||
"happyCustomers": "Clients Satisfaits",
|
||||
"worldwide": "Dans le Monde Entier",
|
||||
"naturalIngredients": "Ingrédients Naturels",
|
||||
"noAdditives": "Sans Additifs",
|
||||
"freeShipping": "Livraison Gratuite",
|
||||
"ordersOver": "Commandes +3.000 RSD"
|
||||
},
|
||||
"ProblemSection": {
|
||||
"title": "Le Problème",
|
||||
"subtitle": "Fatigué des Produits Capillaires & Cutanés Qui Ne Delivrent Pas?",
|
||||
"description": "Vous méritez mieux que des produits remplis de produits chimiques agressifs et de promesses vides",
|
||||
"problems": [
|
||||
{
|
||||
"problem": "Cheveux Secs et Endommagés",
|
||||
"description": "Les produits laissent vos cheveux cassants, crépus et se cassent malgré des traitements coûteux"
|
||||
},
|
||||
{
|
||||
"problem": "Ingrédients Déroutants",
|
||||
"description": "Vous ne pouvez pas prononcer ce qu'il y a dans vos soins cutanés. Parabènes, sulfates, parfums synthétiques - toxines dangereuses"
|
||||
},
|
||||
{
|
||||
"problem": "Aucun Vrai Résultat",
|
||||
"description": "D'innombrables produits promettent des miracles mais ne livrent que des promesses vides et de l'argent gaspillé"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsSeenIn": {
|
||||
"title": "Comme Vus Dans"
|
||||
},
|
||||
"BeforeAfterGallery": {
|
||||
"realResults": "Résultats Réels",
|
||||
"seeTransformation": "Voir la Transformation",
|
||||
"startTransformation": "Commencez Votre Transformation",
|
||||
"before": "AVANT",
|
||||
"after": "APRÈS",
|
||||
"verified": "Vérifié",
|
||||
"timeline": "Après {weeks}"
|
||||
},
|
||||
"HowItWorks": {
|
||||
"title": "Processus Simple",
|
||||
"subtitle": "Comment ManoonOils Fonctionne",
|
||||
"startTransformation": "Commencez Votre Transformation",
|
||||
"steps": [
|
||||
{
|
||||
"title": "Choisissez Votre Huile",
|
||||
"description": "Sélectionnez parmi notre collection d'huiles pures cold-pressed formulées pour vos besoins spécifiques en cheveux et peau."
|
||||
},
|
||||
{
|
||||
"title": "Appliquez Quotidiennement",
|
||||
"description": "Massez quelques gouttes dans des cheveux ou une peau humides. Nos huiles s'absorbent instantanément - jamais grasses, toujours nourrissantes."
|
||||
},
|
||||
{
|
||||
"title": "Voyez les Résultats",
|
||||
"description": "Vivez la transformation en 4-6 semaines. Cheveux plus brillants, peau radieuse et confiance qui rayonne."
|
||||
}
|
||||
]
|
||||
},
|
||||
"Header": {
|
||||
"products": "Produits",
|
||||
"about": "À Propos",
|
||||
"contact": "Contact",
|
||||
"cart": "Panier",
|
||||
"account": "Compte",
|
||||
"openMenu": "Ouvrir le menu",
|
||||
"closeMenu": "Fermer le menu",
|
||||
"openCart": "Ouvrir le panier"
|
||||
},
|
||||
"ProductCard": {
|
||||
"noImage": "Pas d'image",
|
||||
"outOfStock": "Rupture de Stock",
|
||||
"quickAdd": "Ajout Rapide",
|
||||
"contactForPrice": "Contacter pour le prix"
|
||||
},
|
||||
"ProductDetail": {
|
||||
"home": "Accueil",
|
||||
"outOfStock": "Rupture de Stock",
|
||||
"size": "Taille",
|
||||
"qty": "Qté",
|
||||
"adding": "Ajout en cours...",
|
||||
"transformHairSkin": "Transformer Mes Cheveux & Ma Peau",
|
||||
"freeShipping": "Livraison gratuite sur les commandes de +3.000 RSD",
|
||||
"guarantee": "Garantie 30 Jours",
|
||||
"secureCheckout": "Paiement Sécurisé",
|
||||
"easyReturns": "Retours Faciles",
|
||||
"benefits": "Bienfaits",
|
||||
"description": "Description",
|
||||
"howToUse": "Comment Utiliser",
|
||||
"howToUseText": "Appliquez une petite quantité sur des cheveux ou une peau propres et humides. Massez doucement jusqu'à absorption. Utilisez quotidiennement pour de meilleurs résultats.",
|
||||
"ingredients": "Ingrédients",
|
||||
"ingredientsText": "100% Huile Naturelle Pure. Aucun additif, conservateur ou parfum artificiel.",
|
||||
"youMayAlsoLike": "Vous Aimerez Aussi",
|
||||
"similarProducts": "Produits Similaires",
|
||||
"stocksRunningOut": "Les stocks s'épuisent!",
|
||||
"urgency1": "Dépêchez-vous! 500+ articles vendus ces 3 derniers jours!",
|
||||
"urgency2": "Dans les paniers de 2,5K personnes - achetez avant qu'il ne disparaisse!",
|
||||
"urgency3": "7 562 personnes ont vu ce produit ces dernières 24 heures!"
|
||||
},
|
||||
"Newsletter": {
|
||||
"stayConnected": "Restez Connectés",
|
||||
"joinCommunity": "Rejoignez Notre Communauté",
|
||||
"newsletterText": "Abonnez-vous pour recevoir des offres exclusives, des conseils beauté et être les premiers informés des nouveaux produits.",
|
||||
"emailPlaceholder": "Entrez votre email",
|
||||
"subscribe": "S'abonner"
|
||||
},
|
||||
"ProductBenefits": {
|
||||
"whyChoose": "Pourquoi Choisir Ce Produit",
|
||||
"manoonDifference": "La Différence Manoon",
|
||||
"pureNatural": "Pur & Naturel",
|
||||
"pureNaturalDesc": "Ingrédients 100% naturels sans additifs ni conservateurs",
|
||||
"crueltyFree": "Cruelty Free",
|
||||
"crueltyFreeDesc": "Jamais testé sur les animaux, ingrédients sourcés éthiquement",
|
||||
"madeWithLove": "Fait avec Amour",
|
||||
"madeWithLoveDesc": "Fabriqué à la main en petites séries pour une qualité maximale",
|
||||
"visibleResults": "Résultats Visibles",
|
||||
"visibleResultsDesc": "Des améliorations perceptibles en 4-6 semaines"
|
||||
},
|
||||
"Checkout": {
|
||||
"checkout": "Commande",
|
||||
"shippingAddress": "Adresse de Livraison",
|
||||
"firstName": "Prénom",
|
||||
"lastName": "Nom",
|
||||
"streetAddress": "Rue et Numéro",
|
||||
"streetAddressOptional": "Appartement, suite, etc. (optionnel)",
|
||||
"city": "Ville",
|
||||
"postalCode": "Code Postal",
|
||||
"phone": "Téléphone",
|
||||
"billingAddressSame": "L'adresse de facturation est la même que l'adresse de livraison",
|
||||
"billingAddress": "Adresse de Facturation",
|
||||
"paymentMethod": "Mode de Paiement",
|
||||
"cashOnDelivery": "Contre-remboursement (COD)",
|
||||
"cashOnDeliveryDesc": "Payez lorsque votre commande est livrée à votre porte.",
|
||||
"processing": "En cours...",
|
||||
"completeOrder": "Finaliser la Commande - {total}",
|
||||
"orderSummary": "Résumé de la Commande",
|
||||
"qty": "Qté",
|
||||
"subtotal": "Sous-total",
|
||||
"shipping": "Livraison",
|
||||
"calculated": "Calculé",
|
||||
"total": "Total",
|
||||
"yourCartEmpty": "Votre panier est vide",
|
||||
"continueShopping": "Continuer les Achats",
|
||||
"errorNoCheckout": "Pas de paiement actif. Veuillez réessayer.",
|
||||
"errorOccurred": "Une erreur s'est produite lors du paiement.",
|
||||
"errorCreatingOrder": "Échec de la création de la commande.",
|
||||
"orderConfirmed": "Commande Confirmée!",
|
||||
"thankYou": "Merci pour votre achat.",
|
||||
"orderNumber": "Numéro de Commande",
|
||||
"confirmationEmail": "Vous recevrez bientôt un email de confirmation. Nous vous contacterons pour organiser le paiement contre-remboursement.",
|
||||
"continueShoppingBtn": "Continuer les Achats"
|
||||
}
|
||||
}
|
||||
@@ -8,26 +8,52 @@
|
||||
"Home": {
|
||||
"hero": {
|
||||
"title": "Premium prirodna ulja",
|
||||
"subtitle": "Za negu kose i kože"
|
||||
"subtitle": "Za negu kose i kože",
|
||||
"lovedBy": "Omiljeno od 50.000+ kupaca širom sveta",
|
||||
"transformHeadline": "Transformiši kosu i kožu",
|
||||
"withNaturalOils": "sa 100% prirodnim uljima",
|
||||
"subtitleText": "Hladno ceđena, organska ulja ručno pravljena sa ljubavlju. Bez aditiva, bez konzervanasa - samo najčistija dobrobit prirode za vašu svakodnevnu lepotu.",
|
||||
"ctaButton": "Transformiši moju kosu i kožu",
|
||||
"learnStory": "Saznaj našu priču",
|
||||
"moneyBack": "Povrat novca 30 dana",
|
||||
"freeShipping": "Besplatna dostava preko 3.000 RSD",
|
||||
"crueltyFree": "Bez okrutnosti"
|
||||
},
|
||||
"ticker": {
|
||||
"text": "Besplatna dostava za porudžbine preko 3000 RSD • Prirodni sastojci • Bez okrutnosti • Ručno sa ljubavlju"
|
||||
},
|
||||
"products": {
|
||||
"title": "Naši proizvodi"
|
||||
},
|
||||
"bestsellers": {
|
||||
"title": "Najprodavaniji"
|
||||
},
|
||||
"story": {
|
||||
"title": "Naša priča",
|
||||
"description": "ManoonOils je rođen iz strasti za prirodnu lepotu..."
|
||||
},
|
||||
"newsletter": {
|
||||
"title": "Ostanite u toku",
|
||||
"placeholder": "Unesite vaš email",
|
||||
"button": "Pretplati se"
|
||||
}
|
||||
"collection": "Naša kolekcija",
|
||||
"premiumOils": "Premium prirodna ulja",
|
||||
"oilsDescription": "Hladno ceđena, čista i prirodna ulja za vašu svakodnevnuBeauty rutinu",
|
||||
"viewAll": "Pogledaj sve proizvode",
|
||||
"ourStory": "Naša priča",
|
||||
"handmadeWithLove": "Ručno sa ljubavlju",
|
||||
"storyText1": "Svaka boca ManoonOils je izrađena sa pažnjom koristeći tradicionalne metode koje se prenose kroz generacije. Nabavljamo samo najkvalitetnije organske sastojke da bismo vam doneli ulja koja neguju kosu i kožu.",
|
||||
"storyText2": "Naša posvećenost čistoći znači bez aditiva, bez konzervanasa - samo dobrobit prirode u njenom najpotentnijem obliku.",
|
||||
"learnMore": "Saznajte više",
|
||||
"whyChooseUs": "Zašto nas izabrati",
|
||||
"manoonDifference": "Manoon razlika",
|
||||
"stayConnected": "Ostanite povezani",
|
||||
"joinCommunity": "Pridružite se našoj zajednici",
|
||||
"newsletterText": "Pretplatite se da biste primali ekskluzivne ponude, Beauty savete i budite prvi koji ćete saznati za nove proizvode.",
|
||||
"emailPlaceholder": "Unesite vaš email",
|
||||
"subscribe": "Pretplatite se"
|
||||
},
|
||||
"Benefits": {
|
||||
"natural": "100% Prirodno",
|
||||
"naturalDesc": "Čista, hladno ceđena ulja bez aditiva ili konzervanasa. Samo dobrobit prirode.",
|
||||
"handcrafted": "Ručno pravljeno",
|
||||
"handcraftedDesc": "Svaka serija je pažljivo pripremljena ručno kako bi se osigurao najviši kvalitet.",
|
||||
"sustainable": "Održivo",
|
||||
"sustainableDesc": "Etički nabavljeni sastojci i ekološka ambalaža za bolju planetu."
|
||||
},
|
||||
"Products": {
|
||||
"collection": "Naša kolekcija",
|
||||
"allProducts": "Svi proizvodi",
|
||||
"productsCount": "{count} proizvoda",
|
||||
"featured": "Istaknuto",
|
||||
"newest": "Najnovije",
|
||||
"priceLow": "Cena: Rastuće",
|
||||
"priceHigh": "Cena: Opadajuće",
|
||||
"noProducts": "Nema dostupnih proizvoda",
|
||||
"checkBack": "Molimo proverite ponovo kasnije za nove proizvode."
|
||||
},
|
||||
"Product": {
|
||||
"addToCart": "Dodaj u korpu",
|
||||
@@ -35,19 +61,366 @@
|
||||
"details": "Detalji",
|
||||
"ingredients": "Sastojci",
|
||||
"usage": "Način upotrebe",
|
||||
"related": "Takođe će vam se svideti"
|
||||
"related": "Takođe će vam se svideti",
|
||||
"notFound": "Proizvod nije pronađen",
|
||||
"notFoundDesc": "Proizvod koji tražite ne postoji ili je uklonjen.",
|
||||
"browseProducts": "Pregledaj proizvode"
|
||||
},
|
||||
"Cart": {
|
||||
"title": "Vaša korpa",
|
||||
"empty": "Vaša korpa je prazna",
|
||||
"emptyDesc": "Izgleda da još uvek niste dodali ništa u korpu.",
|
||||
"continueShopping": "Nastavite kupovinu",
|
||||
"checkout": "Kupovina",
|
||||
"subtotal": "Ukupno",
|
||||
"remove": "Ukloni"
|
||||
"shipping": "Dostava",
|
||||
"shippingCalc": "Racunato pri kupovini",
|
||||
"total": "Ukupno",
|
||||
"freeShipping": "Besplatna dostava za porudžbine preko {amount}",
|
||||
"remove": "Ukloni",
|
||||
"processes": "Obrađuje se...",
|
||||
"cartEmpty": "Vaša korpa je prazna"
|
||||
},
|
||||
"About": {
|
||||
"title": "O nama",
|
||||
"subtitle": "Naša priča",
|
||||
"intro": "ManoonOils je rođen iz strasti za prirodnu lepotu i verovanja da najbolja nega kože dolazi od same prirode.",
|
||||
"intro2": "Verujemo u moć prirodnih sastojaka. Svako ulje u našoj kolekciji je pažljivo odabrano zbog svojih jedinstvenih svojstava i prednosti. Od hranljivih ulja koja obnavljaju vitalnost kose, do seruma koji podmlađuju kožu, svaki proizvod pravimo sa ljubavlju i pažnjom prema detaljima.",
|
||||
"naturalIngredients": "Prirodni sastojci",
|
||||
"naturalIngredientsDesc": "Koristimo samo najfinije prirodne sastojke, etički i održivo nabavljene od pouzdanih dobavljača širom sveta.",
|
||||
"crueltyFree": "Bez okrutnosti",
|
||||
"crueltyFreeDesc": "Naši proizvodi se nikada ne testiraju na životinjama. Verujemo u lepotu bez kompromisa.",
|
||||
"sustainablePackaging": "Održiva ambalaža",
|
||||
"sustainablePackagingDesc": "Koristimo ekološke materijale za pakovanje i minimizujemo otpad tokom celokupnog proizvodnog procesa.",
|
||||
"handcraftedQuality": "Ručna izrada",
|
||||
"handcraftedQualityDesc": "Svaka boca je ručno napravljena u malim serijama kako bi se osigurao najviši kvalitet i svežina.",
|
||||
"mission": "Naša misija",
|
||||
"missionQuote": "\"Da pružimo proizvode premium kvaliteta, prirodne proizvode koji unapređuju vašu svakodnevnuBeauty rutinu.\"",
|
||||
"handmadeTitle": "Ručno sa ljubavlju",
|
||||
"handmadeText1": "Svaka boca ManoonOils je ručno napravljena sa brigom. Proizvodimo proizvode u malim serijama kako bi osigurali najviši kvalitet i svežinu. Kada koristite ManoonOils, možete biti sigurni da koristite nešto napravljeno sa iskrenom brigom i stručnošću.",
|
||||
"handmadeText2": "Naše putovanje je počelo jednostavnim pitanjem: kako možemo stvoriti proizvode koji zaista neguju kosu i kožu? Danas nastavljamo da inoviramo dok ostajemo verni našoj posvećenosti prirodnim, efikasnim Beauty rešenjima."
|
||||
},
|
||||
"Contact": {
|
||||
"title": "Kontakt",
|
||||
"subtitle": "Stupite u kontakt",
|
||||
"getInTouch": "Stupite u kontakt",
|
||||
"getInTouchDesc": "Tu smo da pomognemo! Bilo da imate pitanja o našim proizvodima, trebate pomoć sa narudžbinom, ili jednostavno želite reći zdravo, voleli bismo da čujemo od vas.",
|
||||
"email": "Email",
|
||||
"emailReply": "Odgovaramo u roku od 24 sata",
|
||||
"shippingTitle": "Dostava",
|
||||
"freeShipping": "Besplatna dostava preko 3.000 RSD",
|
||||
"deliveryTime": "Isporučeno u roku od 2-5 radnih dana",
|
||||
"location": "Lokacija",
|
||||
"locationDesc": "Srbija",
|
||||
"worldwideShipping": "Isporuka širom sveta",
|
||||
"name": "Ime",
|
||||
"namePlaceholder": "Vaše ime",
|
||||
"emailField": "Email",
|
||||
"emailPlaceholder": "vas@email.com",
|
||||
"message": "Poruka",
|
||||
"messagePlaceholder": "Kako možemo da vam pomognemo?",
|
||||
"sendMessage": "Pošalji poruku",
|
||||
"thankYou": "Hvala vam!",
|
||||
"thankYouDesc": "Vaša poruka je poslata. Javićemo vam se uskoro.",
|
||||
"faqTitle": "Često postavljana pitanja",
|
||||
"faq1q": "Koliko dugo traje dostava?",
|
||||
"faq1a": "Narudžbe se obično isporučuju u roku od 2-5 radnih dana za domaću dostavu. Dobićete broj za praćenje čim vaša narudžbina bude poslata.",
|
||||
"faq2q": "Da li su vaši proizvodi 100% prirodni?",
|
||||
"faq2a": "Da! Sva naša ulja su 100% prirodna, hladno ceđena i bez ikakvih aditiva, konzervanasa ili veštačkih mirisa.",
|
||||
"faq3q": "Koja je vaša politika povrata?",
|
||||
"faq3a": "Prihvatamo povrate u roku od 14 dana od isporuke za neotvorene proizvode. Molimo kontaktirajte nas ako imate bilo kakvih problema sa narudžbinom.",
|
||||
"faq4q": "Da li nudite veleprodaju?",
|
||||
"faq4a": "Da, nudimo veleprodajne cene za narudžbine u velikim količinama. Molimo kontaktirajte nas na hello@manoonoils.com za više informacija."
|
||||
},
|
||||
"Footer": {
|
||||
"quickLinks": "Brze veze",
|
||||
"customerService": "Korisnička podrška",
|
||||
"copyright": "Sva prava zadržana."
|
||||
"contact": "Kontakt",
|
||||
"shipping": "Dostava",
|
||||
"returns": "Povrat",
|
||||
"faq": "Česta pitanja",
|
||||
"followUs": "Pratite nas",
|
||||
"newsletter": "Newsletter",
|
||||
"newsletterDesc": "Pretplatite se na naš newsletter za ekskluzivne ponude i novosti.",
|
||||
"copyright": "Sva prava zadržana.",
|
||||
"allRights": "© {year} ManoonOils."
|
||||
},
|
||||
"Common": {
|
||||
"loading": "Učitavanje...",
|
||||
"error": "Došlo je do greške",
|
||||
"tryAgain": "Pokušajte ponovo",
|
||||
"close": "Zatvori",
|
||||
"back": "Nazad",
|
||||
"next": "Sledeće",
|
||||
"previous": "Prethodno",
|
||||
"search": "Pretraga",
|
||||
"noResults": "Nema rezultata"
|
||||
},
|
||||
"Testimonials": {
|
||||
"title": "Šta naši kupci kažu",
|
||||
"verified": "Potvrđena kupovina",
|
||||
"reviews": [
|
||||
{
|
||||
"name": "Milica J.",
|
||||
"skinType": "Suva, osetljiva koža",
|
||||
"text": "Isprobala sam bezbroj ulja tokom godina, ali ManoonOils je drugačije. Moja koža nikad nije bila ovako negovana i zdrava. Arganovo ulje je sada osnovni deo moje rutine."
|
||||
},
|
||||
{
|
||||
"name": "Marko P.",
|
||||
"skinType": "Nega kose",
|
||||
"text": "Konačno sam pronašla ulje koje zaista obuzdava moju kosu bez que je čini masnom. Jojobino ulje čini čuda i za moju bradu. Toplo preporučujem!"
|
||||
},
|
||||
{
|
||||
"name": "Ana K.",
|
||||
"skinType": "Kombinovana koža",
|
||||
"text": "U početku sam bila skeptična, ali nakon 3 nedelje korišćenja ulja od šipka, tekstura moje kože se drastično poboljšala. Kvalitet je neuporediv."
|
||||
}
|
||||
]
|
||||
},
|
||||
"ProductReviews": {
|
||||
"customerReviews": "Ocene kupaca",
|
||||
"whatCustomersSay": "Šta kupci kažu",
|
||||
"basedOnReviews": "Na osnovu 1000+ recenzija",
|
||||
"reviews": [
|
||||
{ "id": 1, "name": "Ana M.", "location": "Beograd", "text": "Manoon Anti-age Serum je transformisao moju kožu za samo 2 nedelje!", "rating": 5 },
|
||||
{ "id": 2, "name": "Milica P.", "location": "Novi Sad", "text": "Najbolji dnevni serum koji sam ikada koristila. Moje bore su vidno smanjene.", "rating": 5 },
|
||||
{ "id": 3, "name": "Jelena K.", "location": "Beograd", "text": "Manoon noćni serum je čista magija. Probudite se sa blistavom kožom svako jutro.", "rating": 5 },
|
||||
{ "id": 4, "name": "Stefan R.", "location": "Subotica", "text": "Anti-age set vredi svaki dinar. Moja supruga i ja ga oboje koristimo.", "rating": 5 },
|
||||
{ "id": 5, "name": "Marija T.", "location": "Kragujevac", "text": "Konačno sam pronašla serum koji zaista deluje! Manoon ispunjava obećanja.", "rating": 5 },
|
||||
{ "id": 6, "name": "Nikola V.", "location": "Niš", "text": "Moje fine linije nestaju. Ovaj dnevni serum je neverovatan.", "rating": 5 },
|
||||
{ "id": 7, "name": "Ivana L.", "location": "Beograd", "text": "Manoon jutarnji serum miriše božanstveno i još bolje deluje.", "rating": 5 },
|
||||
{ "id": 8, "name": "Dejan M.", "location": "Novi Sad", "text": "Noćni serum je potpuno transformisao moju rutinu nege kože.", "rating": 5 },
|
||||
{ "id": 9, "name": "Sanja B.", "location": "Kragujevac", "text": "Moja koža izgleda 10 godina mlađe nakon mesec dana korišćenja Manoon-a.", "rating": 5 },
|
||||
{ "id": 10, "name": "Marko J.", "location": "Subotica", "text": "Anti-age set je savršen poklon. Moja majka ga obožava!", "rating": 5 },
|
||||
{ "id": 11, "name": "Petra D.", "location": "Niš", "text": "Tekstura Manoon seruma je toliko luksuzna. Vredi svaki dinar.", "rating": 5 },
|
||||
{ "id": 12, "name": "Luka G.", "location": "Beograd", "text": "Dnevni serum se upija momentalno. Nikakav masni osećaj!", "rating": 5 },
|
||||
{ "id": 13, "name": "Maja S.", "location": "Novi Sad", "text": "Moj kozmetičar je pitao šta koristim. Manoon je sada moja tajna!", "rating": 5 },
|
||||
{ "id": 14, "name": "Vladimir P.", "location": "Kragujevac", "text": "Noćni serum deluje dok spavam. Probudite se sa vidljivo glatkom kožom.", "rating": 5 },
|
||||
{ "id": 15, "name": "Katarina N.", "location": "Subotica", "text": "Anti-age set je stigao lepo upakovan. Savršen za poklone.", "rating": 5 },
|
||||
{ "id": 16, "name": "Bojan R.", "location": "Niš", "text": "Koristim Manoon 3 meseca. Moje bore su primetno smanjene.", "rating": 5 },
|
||||
{ "id": 17, "name": "Tamara F.", "location": "Beograd", "text": "Dnevni serum pruža savršenu bazu ispod šminke.", "rating": 5 },
|
||||
{ "id": 18, "name": "Aleksandar K.", "location": "Novi Sad", "text": "Konačno srpski brend koji se takmiči sa luksuznim međunarodnim brendovima!", "rating": 5 },
|
||||
{ "id": 19, "name": "Natalia M.", "location": "Kragujevac", "text": "Moja osetljiva koža obožava Manoon. Bez ikakve iritacije.", "rating": 5 },
|
||||
{ "id": 20, "name": "Filip T.", "location": "Subotica", "text": "Anti-age serum je lagan, a opet neverovatno efektan.", "rating": 5 },
|
||||
{ "id": 21, "name": "Andrea L.", "location": "Niš", "text": "Manoon noćni serum je moja večernja rutina. Koža izgleda neverovatno!", "rating": 5 },
|
||||
{ "id": 22, "name": "Ognjen P.", "location": "Beograd", "text": "Prijatelji neprestano pitaju šta sam promenio u rutini nege kože.", "rating": 5 },
|
||||
{ "id": 23, "name": "Mila J.", "location": "Novi Sad", "text": "Anti-age set sadrži sve što vam treba. Odlična vrednost!", "rating": 5 },
|
||||
{ "id": 24, "name": "Dragan S.", "location": "Kragujevac", "text": "Čak je i moj muž primetio razliku. Sada i on koristi dnevni serum!", "rating": 5 },
|
||||
{ "id": 25, "name": "Jovana V.", "location": "Subotica", "text": "Jutarnji serum za sjaj daje najlepšu luminoznost.", "rating": 5 },
|
||||
{ "id": 26, "name": "Stefan M.", "location": "Niš", "text": "Manoon proizvodi su sada neophodni u mojoj dnevnoj rutini.", "rating": 5 },
|
||||
{ "id": 27, "name": "Ana R.", "location": "Beograd", "text": "Noćni serum je pomogao da se pročisti ten. Koža izgleda tako zdravo!", "rating": 5 },
|
||||
{ "id": 28, "name": "Nenad L.", "location": "Novi Sad", "text": "Anti-aging rezultati vidljivi za par nedelja. Toplo preporučujem Manoon!", "rating": 5 },
|
||||
{ "id": 29, "name": "Sofija D.", "location": "Kragujevac", "text": "Tekstura je božanstvena. Oseća se kao luksuzni spa tretman kod kuće.", "rating": 5 },
|
||||
{ "id": 30, "name": "Velibor K.", "location": "Subotica", "text": "Moje bore oko očiju su se značajno smanjile. Hvala Manoon!", "rating": 5 },
|
||||
{ "id": 31, "name": "Irena M.", "location": "Niš", "text": "Anti-age set je savršen poklon za rođendan moje majke.", "rating": 5 },
|
||||
{ "id": 32, "name": "Radoslav P.", "location": "Beograd", "text": "Profesionalni kvalitet seruma po poštenoj ceni. Srpska izvrsnost!", "rating": 5 },
|
||||
{ "id": 33, "name": "Jelena B.", "location": "Novi Sad", "text": "Moja koža nikad nije bila ovako hidrirana. Dnevni serum je neverovatan!", "rating": 5 },
|
||||
{ "id": 34, "name": "Dimitrije S.", "location": "Kragujevac", "text": "Noćni serum vredi svog zlata. Čista luksuz!", "rating": 5 },
|
||||
{ "id": 35, "name": "Minela G.", "location": "Subotica", "text": "Manoon ispunjava očekivanja. Moja koža izgleda osveženo i mlado.", "rating": 5 },
|
||||
{ "id": 36, "name": "Zoran T.", "location": "Niš", "text": "Isprobao sam mnoge serume. Manoon je daleko najefektivniji.", "rating": 5 },
|
||||
{ "id": 37, "name": "Mirjana F.", "location": "Beograd", "text": "Anti-age set je potpuno transformisao rutinu nege kože moje majke.", "rating": 5 },
|
||||
{ "id": 38, "name": "Ivan J.", "location": "Novi Sad", "text": "Brzo delujući serum sa stvarnim rezultatima. Preporučujem Manoon svima.", "rating": 5 },
|
||||
{ "id": 39, "name": "Kristina P.", "location": "Kragujevac", "text": "Jutarnji serum za sjaj daje tako lep deve sjaj.", "rating": 5 },
|
||||
{ "id": 40, "name": "Bratislav L.", "location": "Subotica", "text": "Primetni rezultati za samo 2 nedelje. Ovaj serum je prava stvar!", "rating": 5 },
|
||||
{ "id": 41, "name": "Zorica M.", "location": "Niš", "text": "Noćni serum je izbrisao godine sa mog lica. Apsolutno čudesno!", "rating": 5 },
|
||||
{ "id": 42, "name": "Patrik N.", "location": "Beograd", "text": "Premium kvalitet srpske kozmetike koja se takmiči sa međunarodnim luksuznim brendovima.", "rating": 5 },
|
||||
{ "id": 43, "name": "Simona K.", "location": "Novi Sad", "text": "Manoon Anti-age Serum je najbolja investicija u moju kožu ikada.", "rating": 5 },
|
||||
{ "id": 44, "name": "Mladen D.", "location": "Kragujevac", "text": "Dnevni serum se upije za sekundu. Nema čekanja!", "rating": 5 },
|
||||
{ "id": 45, "name": "Ljiljana R.", "location": "Subotica", "text": "Poklanjam Anti-age set sestrama. Obožale su ga!", "rating": 5 },
|
||||
{ "id": 46, "name": "Tomislav V.", "location": "Niš", "text": "Moje bore su vidno smanjene nakon mesec dana korišćenja Manoon-a.", "rating": 5 },
|
||||
{ "id": 47, "name": "Emilija S.", "location": "Beograd", "text": "Noćni serum ostavlja moju kožu tako mekom i obnovljenom svako jutro.", "rating": 5 },
|
||||
{ "id": 48, "name": "Andrija P.", "location": "Novi Sad", "text": "Manoon dnevni serum je savršen ispod sunscreena. Neophodna kombinacija!", "rating": 5 },
|
||||
{ "id": 49, "name": "Miona L.", "location": "Kragujevac", "text": "Moja koža izgleda zračno i mlado. Ne mogu biti srećnija sa Manoon-om!", "rating": 5 },
|
||||
{ "id": 50, "name": "Slavko M.", "location": "Subotica", "text": "Anti-age set daje vidljive rezultate. Prava srpska kvaliteta!", "rating": 5 }
|
||||
]
|
||||
},
|
||||
"TrustBadges": {
|
||||
"averageRating": "Prosečna ocena",
|
||||
"basedOnReviews": "Na osnovu 1000+ recenzija",
|
||||
"happyCustomers": "Srećni kupci",
|
||||
"worldwide": "Širom sveta",
|
||||
"naturalIngredients": "Prirodni sastojci",
|
||||
"noAdditives": "Bez aditiva",
|
||||
"freeShipping": "Besplatna dostava",
|
||||
"ordersOver": "Porudžbine preko 3.000 RSD"
|
||||
},
|
||||
"ProblemSection": {
|
||||
"title": "Problem",
|
||||
"subtitle": "Zamareni proizvodima za kosu i kožu koji ne ispunjavaju obećanja?",
|
||||
"description": "Zaslužujete više od proizvoda punih grubih hemikalija i praznih obećanja",
|
||||
"problems": [
|
||||
{
|
||||
"problem": "Suva, oštećena kosa",
|
||||
"description": "Proizvodi ostavljaju vašu kosu krhkom, frizirajućom i lomljivom uprkos skupim tretmanima"
|
||||
},
|
||||
{
|
||||
"problem": "Zbunjeni sastojci",
|
||||
"description": "Ne možete izgovoriti šta je u vašoj nezi kože. Parabeni, sulfati, sintetički mirisi—opasni toksini"
|
||||
},
|
||||
{
|
||||
"problem": "Bez stvarnih rezultata",
|
||||
"description": "Bezbroj proizvoda obećava čuda ali isporučuju samo prazna obećanja i utrošen novac"
|
||||
}
|
||||
]
|
||||
},
|
||||
"AsSeenIn": {
|
||||
"title": "Kao što je viđeno u"
|
||||
},
|
||||
"BeforeAfterGallery": {
|
||||
"realResults": "Stvarni rezultati",
|
||||
"seeTransformation": "Pogledajte transformaciju",
|
||||
"startTransformation": "Započnite vašu transformaciju",
|
||||
"before": "PRE",
|
||||
"after": "POSLE",
|
||||
"verified": "Potvrđeno",
|
||||
"timeline": "Nakon {weeks}"
|
||||
},
|
||||
"HowItWorks": {
|
||||
"title": "Jednostavan proces",
|
||||
"subtitle": "Kako ManoonOils funkcioniše",
|
||||
"startTransformation": "Započnite vašu transformaciju",
|
||||
"steps": [
|
||||
{
|
||||
"title": "Izaberite vaše ulje",
|
||||
"description": "Izaberite iz naše kolekcije čistih, hladno ceđenih ulja formulisanih za vaše specifične potrebe kose i kože."
|
||||
},
|
||||
{
|
||||
"title": "Nanesite svakodnevno",
|
||||
"description": "Umasirajte nekoliko kapi u vlažnu kosu ili kožu. Naša ulja se momentalno upijaju—nikada masna, uvek negujuća."
|
||||
},
|
||||
{
|
||||
"title": "Vidite rezultate",
|
||||
"description": "Doživite transformaciju za 4-6 nedelja. Sjajnija kosa, zračna koža i samopouzdanje koje sija."
|
||||
}
|
||||
]
|
||||
},
|
||||
"Header": {
|
||||
"products": "Proizvodi",
|
||||
"about": "O nama",
|
||||
"contact": "Kontakt",
|
||||
"cart": "Korpa",
|
||||
"account": "Nalog",
|
||||
"openMenu": "Otvori meni",
|
||||
"closeMenu": "Zatvori meni",
|
||||
"openCart": "Otvori korpu"
|
||||
},
|
||||
"Footer": {
|
||||
"shop": "Prodavnica",
|
||||
"allProducts": "Svi proizvodi",
|
||||
"hairCare": "Nega kose",
|
||||
"skinCare": "Nega kože",
|
||||
"giftSets": "Poklon setovi",
|
||||
"about": "O nama",
|
||||
"ourStory": "Naša priča",
|
||||
"process": "Proces",
|
||||
"sustainability": "Održivost",
|
||||
"help": "Pomoć",
|
||||
"faq": "Česta pitanja",
|
||||
"shipping": "Dostava",
|
||||
"returns": "Povrat",
|
||||
"contactUs": "Kontaktirajte nas",
|
||||
"brandDescription": "Premium prirodna ulja za negu kose i kože. Ručno pravljena sa ljubavlju, korišćenjem tradicionalnih metoda.",
|
||||
"weAccept": "Prihvatamo:",
|
||||
"allRights": "Sva prava zadržana."
|
||||
},
|
||||
"ProductCard": {
|
||||
"noImage": "Nema slike",
|
||||
"outOfStock": "Nema na stanju",
|
||||
"quickAdd": "Brzo dodavanje",
|
||||
"contactForPrice": "Kontaktirajte za cenu"
|
||||
},
|
||||
"ProductDetail": {
|
||||
"home": "Početna",
|
||||
"outOfStock": "Nema na stanju",
|
||||
"size": "Veličina",
|
||||
"qty": "Kol",
|
||||
"adding": "Dodavanje...",
|
||||
"transformHairSkin": "Transformiši kosu i kožu",
|
||||
"freeShipping": "Besplatna dostava za porudžbine preko 3.000 RSD",
|
||||
"guarantee": "30-dnevna garancija",
|
||||
"secureCheckout": "Sigurno plaćanje",
|
||||
"easyReturns": "Lak povrat",
|
||||
"benefits": "Prednosti",
|
||||
"description": "Opis",
|
||||
"howToUse": "Kako koristiti",
|
||||
"howToUseText": "Nanesite malu količinu na čistu, vlažnu kosu ili kožu. Nežno masirajte dok se ne upije. Koristite svakodnevno za najbolje rezultate.",
|
||||
"ingredients": "Sastojci",
|
||||
"ingredientsText": "100% čisto prirodno ulje. Bez dodataka, konzervansa ili veštačkih mirisa.",
|
||||
"youMayAlsoLike": "Možda će vam se svideti",
|
||||
"similarProducts": "Slični proizvodi",
|
||||
"stocksRunningOut": "Zalihe se smanjuju!",
|
||||
"urgency1": "Požuri! 500+ proizvoda prodato u poslednja 3 dana!",
|
||||
"urgency2": "U korpama 2.5K ljudi - kupi pre nego što nestane!",
|
||||
"urgency3": "7.562 osobe su pogledale ovaj proizvod u poslednja 24 sata!"
|
||||
},
|
||||
"Newsletter": {
|
||||
"stayConnected": "Ostanite povezani",
|
||||
"joinCommunity": "Pridružite se našoj zajednici",
|
||||
"newsletterText": "Pretplatite se da biste primali ekskluzivne ponude, savete za negu i budite prvi koji ćete saznati za nove proizvode.",
|
||||
"emailPlaceholder": "Unesite vaš email",
|
||||
"subscribe": "Pretplatite se"
|
||||
},
|
||||
"ProductBenefits": {
|
||||
"whyChoose": "Zašto odabrati ovaj proizvod",
|
||||
"manoonDifference": "Manoon razlika",
|
||||
"pureNatural": "Čisto i prirodno",
|
||||
"pureNaturalDesc": "100% prirodni sastojci bez aditiva ili konzervansa",
|
||||
"crueltyFree": "Bez okrutnosti",
|
||||
"crueltyFreeDesc": "Nikada testirano na životinjama, etički nabavljeni sastojci",
|
||||
"madeWithLove": "Napravljeno sa ljubavlju",
|
||||
"madeWithLoveDesc": "Ručno pravljeno u malim serijama za maksimalni kvalitet",
|
||||
"visibleResults": "Vidljivi rezultati",
|
||||
"visibleResultsDesc": "Primetna poboljšanja za 4-6 nedelja"
|
||||
},
|
||||
"Cart": {
|
||||
"yourCart": "Vaša korpa",
|
||||
"closeCart": "Zatvori korpu",
|
||||
"dismiss": "Odbaci",
|
||||
"yourCartEmpty": "Vaša korpa je prazna",
|
||||
"looksLikeEmpty": "Izgleda da još uvek niste dodali ništa u korpu.",
|
||||
"startShopping": "Započni kupovinu",
|
||||
"subtotal": "Ukupno",
|
||||
"shipping": "Dostava",
|
||||
"calculatedAtCheckout": "Racunato pri kupovini",
|
||||
"total": "Ukupno",
|
||||
"freeShippingOver": "Besplatna dostava za porudžbine preko {amount}",
|
||||
"processing": "Obrađivanje...",
|
||||
"checkout": "Kupovina",
|
||||
"continueShopping": "Nastavi kupovinu",
|
||||
"removeItem": "Ukloni proizvod"
|
||||
},
|
||||
"Checkout": {
|
||||
"checkout": "Kupovina",
|
||||
"shippingAddress": "Adresa za dostavu",
|
||||
"firstName": "Ime",
|
||||
"lastName": "Prezime",
|
||||
"streetAddress": "Ulica i broj",
|
||||
"streetAddressOptional": "Stan, apartman, itd. (opciono)",
|
||||
"city": "Grad",
|
||||
"postalCode": "Poštanski broj",
|
||||
"phone": "Telefon",
|
||||
"billingAddressSame": "Adresa za naplatu je ista kao adresa za dostavu",
|
||||
"billingAddress": "Adresa za naplatu",
|
||||
"paymentMethod": "Način plaćanja",
|
||||
"cashOnDelivery": "Pouzećem (COD)",
|
||||
"cashOnDeliveryDesc": "Platite kada vam narudžbina bude isporučena na vrata.",
|
||||
"processing": "Obrađivanje...",
|
||||
"completeOrder": "Završi narudžbinu - {total}",
|
||||
"orderSummary": "Pregled narudžbine",
|
||||
"qty": "Kol",
|
||||
"subtotal": "Ukupno",
|
||||
"shipping": "Dostava",
|
||||
"calculated": "Po obračunu",
|
||||
"total": "Ukupno",
|
||||
"yourCartEmpty": "Vaša korpa je prazna",
|
||||
"continueShopping": "Nastavi kupovinu",
|
||||
"errorNoCheckout": "Nema aktivne korpe. Molimo pokušajte ponovo.",
|
||||
"errorOccurred": "Došlo je do greške prilikom kupovine.",
|
||||
"errorCreatingOrder": "Neuspešno kreiranje narudžbine.",
|
||||
"orderConfirmed": "Narudžbina potvrđena!",
|
||||
"thankYou": "Hvala vam na kupovini!",
|
||||
"orderNumber": "Broj narudžbine",
|
||||
"confirmationEmail": "Uскoro ćete primiti email potvrde. Kontaktiraćemo vas da dogovorimo pouzećem plaćanje.",
|
||||
"continueShoppingBtn": "Nastavi kupovinu"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { routing } from './routing';
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
let locale = await requestLocale;
|
||||
|
||||
if (!locale || !routing.locales.includes(locale as any)) {
|
||||
if (!locale || !routing.locales.includes(locale as typeof routing.locales[number])) {
|
||||
locale = routing.defaultLocale;
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ export default getRequestConfig(async ({ requestLocale }) => {
|
||||
locale,
|
||||
messages: (await import(`./messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineRouting } from 'next-intl/routing';
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
|
||||
export const routing = defineRouting({
|
||||
locales: ['sr', 'en'],
|
||||
defaultLocale: 'sr',
|
||||
localePrefix: 'as-needed'
|
||||
});
|
||||
locales: ["sr", "en", "de", "fr"],
|
||||
defaultLocale: "sr",
|
||||
localePrefix: "as-needed",
|
||||
});
|
||||
@@ -24,12 +24,8 @@ export const saleorClient = new ApolloClient({
|
||||
fields: {
|
||||
products: {
|
||||
keyArgs: ["channel", "filter"],
|
||||
merge(existing, incoming) {
|
||||
if (!existing) return incoming;
|
||||
return {
|
||||
...incoming,
|
||||
edges: [...existing.edges, ...incoming.edges],
|
||||
};
|
||||
merge(_existing, incoming) {
|
||||
return incoming;
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ export {
|
||||
getProducts,
|
||||
getProductBySlug,
|
||||
getProductPrice,
|
||||
getProductPriceAmount,
|
||||
getProductImage,
|
||||
isProductAvailable,
|
||||
formatPrice,
|
||||
|
||||
@@ -50,8 +50,8 @@ export const CHECKOUT_LINES_UPDATE = gql`
|
||||
`;
|
||||
|
||||
export const CHECKOUT_LINES_DELETE = gql`
|
||||
mutation CheckoutLinesDelete($checkoutId: ID!, $lineIds: [ID!]!) {
|
||||
checkoutLinesDelete(checkoutId: $checkoutId, lines: $lineIds) {
|
||||
mutation CheckoutLinesDelete($id: ID!, $linesIds: [ID!]!) {
|
||||
checkoutLinesDelete(id: $id, linesIds: $linesIds) {
|
||||
checkout {
|
||||
...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 {
|
||||
if (product.media && product.media.length > 0) {
|
||||
return product.media[0].url;
|
||||
@@ -88,7 +93,8 @@ export function formatPrice(amount: number, currency: string = "RSD"): string {
|
||||
return new Intl.NumberFormat("sr-RS", {
|
||||
style: "currency",
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
|
||||
@@ -221,8 +221,8 @@ export const useSaleorCheckoutStore = create<SaleorCheckoutStore>()(
|
||||
const { data } = await saleorClient.mutate<CheckoutLinesDeleteResponse>({
|
||||
mutation: CHECKOUT_LINES_DELETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
lineIds: [lineId],
|
||||
id: checkout.id,
|
||||
linesIds: [lineId],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user