feat: implement locale-aware routing with [locale] dynamic segments
Some checks failed
Build and Deploy / build (push) Has been cancelled
Some checks failed
Build and Deploy / build (push) Has been cancelled
WARNING: This change breaks existing SEO URLs for Serbian locale. Changes: - Migrated from separate locale folders (src/app/en/, src/app/de/, etc.) to [locale] dynamic segments (src/app/[locale]/) - Serbian is now at /sr/ instead of / (root) - English at /en/, German at /de/, French at /fr/ - All components updated to generate locale-aware links - Root / now redirects to /sr (307 temporary redirect) SEO Impact: - Previously indexed Serbian URLs (/, /products, /about, /contact) will now return 404 or redirect to /sr/* URLs - This is a breaking change for SEO - Serbian pages should ideally remain at root (/) with only non-default locales getting prefix - Consider implementing 301 redirects from old URLs to maintain search engine rankings Technical Notes: - next-intl v4 with [locale] structure requires ALL locales to have the prefix (cannot have default locale at root) - Alternative approach would be separate folder structure per locale
This commit is contained in:
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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
103
src/app/[locale]/products/page.tsx
Normal file
103
src/app/[locale]/products/page.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
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";
|
||||
|
||||
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 } = await params;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations("Products");
|
||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
||||
const products = await getProducts(productLocale);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header locale={locale} />
|
||||
|
||||
<main className="min-h-screen bg-white">
|
||||
<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">
|
||||
{t("collection")}
|
||||
</span>
|
||||
<h1 className="text-3xl md:text-4xl font-medium">
|
||||
{t("allProducts")}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-[#666666]">
|
||||
{t("productsCount", { count: products.length })}
|
||||
</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">{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{t("noProducts")}
|
||||
</p>
|
||||
<p className="text-sm text-[#999999]">
|
||||
{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}
|
||||
index={index}
|
||||
locale={productLocale}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div className="pt-16">
|
||||
<Footer locale={locale} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user