Merge dev into master: resolve middleware conflict (use dev version with full locale detection)
Some checks failed
Build and Deploy / build (push) Has been cancelled
Some checks failed
Build and Deploy / build (push) Has been cancelled
This commit is contained in:
@@ -1,40 +1,45 @@
|
|||||||
import createMiddleware from "next-intl/middleware";
|
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { routing } from "./src/i18n/routing";
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, LOCALE_COOKIE, getPathWithoutLocale, buildLocalePath, isValidLocale } from "@/lib/i18n/locales";
|
||||||
|
import type { Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
const oldSerbianPaths = ["", "products", "about", "contact", "checkout"];
|
const OLD_SERBIAN_PATHS = ["products", "about", "contact", "checkout"];
|
||||||
|
|
||||||
|
function detectLocale(cookieLocale: string | undefined, acceptLanguage: string): Locale {
|
||||||
|
if (cookieLocale && isValidLocale(cookieLocale)) {
|
||||||
|
return cookieLocale;
|
||||||
|
}
|
||||||
|
if (acceptLanguage.includes("en")) {
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
export default function middleware(request: NextRequest) {
|
export default function middleware(request: NextRequest) {
|
||||||
const pathname = request.nextUrl.pathname;
|
const pathname = request.nextUrl.pathname;
|
||||||
|
const cookieLocale = request.cookies.get(LOCALE_COOKIE)?.value;
|
||||||
|
const acceptLanguage = request.headers.get("accept-language") || "";
|
||||||
|
|
||||||
const isOldSerbianPath = oldSerbianPaths.some((path) => {
|
if (pathname === "/" || pathname === "") {
|
||||||
if (path === "") {
|
const locale = detectLocale(cookieLocale, acceptLanguage);
|
||||||
return pathname === "/";
|
|
||||||
}
|
|
||||||
return pathname === `/${path}` || pathname.startsWith(`/${path}/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasLocalePrefix = routing.locales.some(
|
|
||||||
(locale) => pathname === `/${locale}` || pathname.startsWith(`/${locale}/`)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isOldSerbianPath && !hasLocalePrefix) {
|
|
||||||
const newPathname = pathname === "/"
|
|
||||||
? "/sr"
|
|
||||||
: `/sr${pathname}`;
|
|
||||||
|
|
||||||
const url = request.nextUrl.clone();
|
const url = request.nextUrl.clone();
|
||||||
url.pathname = newPathname;
|
url.pathname = buildLocalePath(locale, "/");
|
||||||
|
|
||||||
return NextResponse.redirect(url, 301);
|
return NextResponse.redirect(url, 301);
|
||||||
}
|
}
|
||||||
|
|
||||||
const intlMiddleware = createMiddleware({
|
const isOldSerbianPath = OLD_SERBIAN_PATHS.some(
|
||||||
...routing,
|
(path) => pathname === `/${path}` || pathname.startsWith(`/${path}/`)
|
||||||
});
|
);
|
||||||
|
|
||||||
return intlMiddleware(request);
|
if (isOldSerbianPath) {
|
||||||
|
const locale = detectLocale(cookieLocale, acceptLanguage);
|
||||||
|
const newPath = buildLocalePath(locale, pathname);
|
||||||
|
const url = request.nextUrl.clone();
|
||||||
|
url.pathname = newPath;
|
||||||
|
return NextResponse.redirect(url, 301);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1,6 +1,8 @@
|
|||||||
import { getTranslations, setRequestLocale } from "next-intl/server";
|
import { getTranslations, setRequestLocale } from "next-intl/server";
|
||||||
import Header from "@/components/layout/Header";
|
import Header from "@/components/layout/Header";
|
||||||
import Footer from "@/components/layout/Footer";
|
import Footer from "@/components/layout/Footer";
|
||||||
|
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||||
|
import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
interface AboutPageProps {
|
interface AboutPageProps {
|
||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
@@ -8,19 +10,19 @@ interface AboutPageProps {
|
|||||||
|
|
||||||
export async function generateMetadata({ params }: AboutPageProps) {
|
export async function generateMetadata({ params }: AboutPageProps) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
return {
|
return {
|
||||||
title: locale === "sr"
|
title: metadata.about.title,
|
||||||
? "O nama - ManoonOils"
|
description: metadata.about.description,
|
||||||
: "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) {
|
export default async function AboutPage({ params }: AboutPageProps) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
|
setRequestLocale(validLocale);
|
||||||
const t = await getTranslations("About");
|
const t = await getTranslations("About");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +45,7 @@ export default async function AboutPage({ params }: AboutPageProps) {
|
|||||||
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
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"}
|
alt={metadata.productionAlt}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/20" />
|
<div className="absolute inset-0 bg-black/20" />
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export default function CheckoutPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header locale={locale} />
|
||||||
<main className="min-h-screen">
|
<main className="min-h-screen">
|
||||||
<section className="pt-[120px] pb-20 px-4">
|
<section className="pt-[120px] pb-20 px-4">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
@@ -377,7 +377,7 @@ export default function CheckoutPage() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div className="pt-16">
|
<div className="pt-16">
|
||||||
<Footer />
|
<Footer locale={locale} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,35 @@
|
|||||||
|
import { Metadata } from "next";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages, setRequestLocale } from "next-intl/server";
|
import { getMessages, setRequestLocale } from "next-intl/server";
|
||||||
import { routing } from "@/i18n/routing";
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return routing.locales.map((locale) => ({ locale }));
|
return SUPPORTED_LOCALES.map((locale) => ({ locale }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const { locale } = await params;
|
||||||
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
const localePrefix = validLocale === DEFAULT_LOCALE ? "" : `/${validLocale}`;
|
||||||
|
|
||||||
|
const languages: Record<string, string> = {};
|
||||||
|
for (const loc of SUPPORTED_LOCALES) {
|
||||||
|
const prefix = loc === DEFAULT_LOCALE ? "" : `/${loc}`;
|
||||||
|
languages[loc] = `${baseUrl}${prefix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
alternates: {
|
||||||
|
canonical: `${baseUrl}${localePrefix}`,
|
||||||
|
languages,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function LocaleLayout({
|
export default async function LocaleLayout({
|
||||||
|
|||||||
@@ -10,30 +10,32 @@ import ProductReviews from "@/components/product/ProductReviews";
|
|||||||
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
|
import BeforeAfterGallery from "@/components/home/BeforeAfterGallery";
|
||||||
import ProblemSection from "@/components/home/ProblemSection";
|
import ProblemSection from "@/components/home/ProblemSection";
|
||||||
import HowItWorks from "@/components/home/HowItWorks";
|
import HowItWorks from "@/components/home/HowItWorks";
|
||||||
|
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||||
|
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
|
setRequestLocale(validLocale);
|
||||||
return {
|
return {
|
||||||
title: locale === "sr"
|
title: metadata.home.title,
|
||||||
? "ManoonOils - Premium prirodna ulja za negu kose i kože"
|
description: metadata.home.description,
|
||||||
: "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 }> }) {
|
export default async function Homepage({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
setRequestLocale(validLocale);
|
||||||
const t = await getTranslations("Home");
|
const t = await getTranslations("Home");
|
||||||
const tBenefits = await getTranslations("Benefits");
|
const tBenefits = await getTranslations("Benefits");
|
||||||
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
|
|
||||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
const saleorLocale = getSaleorLocale(validLocale as Locale);
|
||||||
let products: any[] = [];
|
let products: any[] = [];
|
||||||
try {
|
try {
|
||||||
products = await getProducts(productLocale);
|
products = await getProducts(saleorLocale);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Failed to fetch products during build");
|
console.log("Failed to fetch products during build");
|
||||||
}
|
}
|
||||||
@@ -41,7 +43,7 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
|
|||||||
const featuredProducts = products?.slice(0, 4) || [];
|
const featuredProducts = products?.slice(0, 4) || [];
|
||||||
const hasProducts = featuredProducts.length > 0;
|
const hasProducts = featuredProducts.length > 0;
|
||||||
|
|
||||||
const basePath = `/${locale}`;
|
const basePath = `/${validLocale}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -78,7 +80,7 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
|
||||||
{featuredProducts.map((product, index) => (
|
{featuredProducts.map((product, index) => (
|
||||||
<ProductCard key={product.id} product={product} index={index} locale={productLocale} />
|
<ProductCard key={product.id} product={product} index={index} locale={locale} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
|
|||||||
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||||
<img
|
<img
|
||||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
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"}
|
alt={metadata.home.productionAlt}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,14 +206,14 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder={t("emailPlaceholder")}
|
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"
|
className="flex-1 min-w-0 px-5 !h-16 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
|
<button
|
||||||
type="submit"
|
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"
|
className="px-8 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")}
|
{t("subscribe")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Footer from "@/components/layout/Footer";
|
|||||||
import ProductDetail from "@/components/product/ProductDetail";
|
import ProductDetail from "@/components/product/ProductDetail";
|
||||||
import type { Product } from "@/types/saleor";
|
import type { Product } from "@/types/saleor";
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
|
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||||
|
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
interface ProductPageProps {
|
interface ProductPageProps {
|
||||||
params: Promise<{ locale: string; slug: string }>;
|
params: Promise<{ locale: string; slug: string }>;
|
||||||
@@ -16,8 +18,8 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
for (const locale of locales) {
|
for (const locale of locales) {
|
||||||
try {
|
try {
|
||||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
const saleorLocale = locale === "sr" ? "SR" : "EN";
|
||||||
const products = await getProducts(productLocale, 100);
|
const products = await getProducts(saleorLocale, 100);
|
||||||
products.forEach((product: Product) => {
|
products.forEach((product: Product) => {
|
||||||
params.push({ locale, slug: product.slug });
|
params.push({ locale, slug: product.slug });
|
||||||
});
|
});
|
||||||
@@ -29,16 +31,18 @@ export async function generateStaticParams() {
|
|||||||
|
|
||||||
export async function generateMetadata({ params }: ProductPageProps) {
|
export async function generateMetadata({ params }: ProductPageProps) {
|
||||||
const { locale, slug } = await params;
|
const { locale, slug } = await params;
|
||||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
const product = await getProductBySlug(slug, productLocale);
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
|
const saleorLocale = validLocale === "sr" ? "SR" : "EN";
|
||||||
|
const product = await getProductBySlug(slug, saleorLocale);
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return {
|
return {
|
||||||
title: locale === "sr" ? "Proizvod nije pronađen" : "Product not found",
|
title: metadata.productNotFound,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const localized = getLocalizedProduct(product, productLocale);
|
const localized = getLocalizedProduct(product, saleorLocale);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: localized.name,
|
title: localized.name,
|
||||||
@@ -48,12 +52,13 @@ export async function generateMetadata({ params }: ProductPageProps) {
|
|||||||
|
|
||||||
export default async function ProductPage({ params }: ProductPageProps) {
|
export default async function ProductPage({ params }: ProductPageProps) {
|
||||||
const { locale, slug } = await params;
|
const { locale, slug } = await params;
|
||||||
setRequestLocale(locale);
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
setRequestLocale(validLocale);
|
||||||
const t = await getTranslations("Product");
|
const t = await getTranslations("Product");
|
||||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
const saleorLocale = getSaleorLocale(validLocale as Locale);
|
||||||
const product = await getProductBySlug(slug, productLocale);
|
const product = await getProductBySlug(slug, saleorLocale);
|
||||||
|
|
||||||
const basePath = locale === "sr" ? "" : `/${locale}`;
|
const basePath = `/${validLocale}`;
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
return (
|
return (
|
||||||
@@ -95,10 +100,10 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
<ProductDetail
|
<ProductDetail
|
||||||
product={product}
|
product={product}
|
||||||
relatedProducts={relatedProducts}
|
relatedProducts={relatedProducts}
|
||||||
locale={productLocale}
|
locale={locale}
|
||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer locale={locale} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ import Header from "@/components/layout/Header";
|
|||||||
import Footer from "@/components/layout/Footer";
|
import Footer from "@/components/layout/Footer";
|
||||||
import ProductCard from "@/components/product/ProductCard";
|
import ProductCard from "@/components/product/ProductCard";
|
||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||||
|
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
interface ProductsPageProps {
|
interface ProductsPageProps {
|
||||||
params: Promise<{ locale: string }>;
|
params: Promise<{ locale: string }>;
|
||||||
@@ -11,22 +13,21 @@ interface ProductsPageProps {
|
|||||||
|
|
||||||
export async function generateMetadata({ params }: ProductsPageProps) {
|
export async function generateMetadata({ params }: ProductsPageProps) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
const metadata = getPageMetadata(validLocale as Locale);
|
||||||
return {
|
return {
|
||||||
title: locale === "sr"
|
title: metadata.products.title,
|
||||||
? "Proizvodi - ManoonOils"
|
description: metadata.products.description,
|
||||||
: "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) {
|
export default async function ProductsPage({ params }: ProductsPageProps) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
setRequestLocale(locale);
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
||||||
|
setRequestLocale(validLocale);
|
||||||
const t = await getTranslations("Products");
|
const t = await getTranslations("Products");
|
||||||
const productLocale = locale === "sr" ? "SR" : "EN";
|
const saleorLocale = getSaleorLocale(validLocale as Locale);
|
||||||
const products = await getProducts(productLocale);
|
const products = await getProducts(saleorLocale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
BIN
src/app/icon.png
Normal file
BIN
src/app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -1,6 +1,9 @@
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
||||||
|
import { SUPPORTED_LOCALES } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: {
|
title: {
|
||||||
@@ -9,6 +12,12 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
description: "Discover our premium collection of natural oils for hair and skin care.",
|
description: "Discover our premium collection of natural oils for hair and skin care.",
|
||||||
robots: "index, follow",
|
robots: "index, follow",
|
||||||
|
alternates: {
|
||||||
|
canonical: baseUrl,
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? baseUrl : `${baseUrl}/${locale}`])
|
||||||
|
),
|
||||||
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||||
description: "Discover our premium collection of natural oils for hair and skin care.",
|
description: "Discover our premium collection of natural oils for hair and skin care.",
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { cookies, headers } from "next/headers";
|
||||||
|
|
||||||
export default function RootPage() {
|
export default async function RootPage() {
|
||||||
redirect("/sr");
|
const headersList = await headers();
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const acceptLanguage = headersList.get("accept-language") || "";
|
||||||
|
const cookieLocale = cookieStore.get("NEXT_LOCALE")?.value;
|
||||||
|
|
||||||
|
let locale = "sr";
|
||||||
|
if (cookieLocale && ["sr", "en", "de", "fr"].includes(cookieLocale)) {
|
||||||
|
locale = cookieLocale;
|
||||||
|
} else if (acceptLanguage.includes("en")) {
|
||||||
|
locale = "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(`/${locale}`);
|
||||||
}
|
}
|
||||||
@@ -1,48 +1,107 @@
|
|||||||
import { MetadataRoute } from "next";
|
import { MetadataRoute } from "next";
|
||||||
import { getProducts } from "@/lib/saleor";
|
import { getProducts } from "@/lib/saleor";
|
||||||
|
import { SUPPORTED_LOCALES, type Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
|
|
||||||
|
|
||||||
|
interface SitemapEntry {
|
||||||
|
url: string;
|
||||||
|
lastModified: Date;
|
||||||
|
changeFrequency: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
||||||
|
priority: number;
|
||||||
|
alternates?: {
|
||||||
|
languages?: Record<string, string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<SitemapEntry[]> {
|
||||||
let products: any[] = [];
|
let products: any[] = [];
|
||||||
try {
|
try {
|
||||||
products = await getProducts("SR", 100);
|
products = await getProducts("SR", 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to fetch products for sitemap during build');
|
console.log("Failed to fetch products for sitemap during build");
|
||||||
}
|
}
|
||||||
|
|
||||||
const productUrls = products.map((product) => ({
|
const staticPages: SitemapEntry[] = [
|
||||||
url: `${baseUrl}/products/${product.slug}`,
|
|
||||||
lastModified: new Date(),
|
|
||||||
changeFrequency: "weekly" as const,
|
|
||||||
priority: 0.8,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
{
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "daily",
|
changeFrequency: "daily",
|
||||||
priority: 1,
|
priority: 1,
|
||||||
|
alternates: {
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? baseUrl : `${baseUrl}/${locale}`])
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${baseUrl}/products`,
|
url: `${baseUrl}/products`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "daily",
|
changeFrequency: "daily",
|
||||||
priority: 0.9,
|
priority: 0.9,
|
||||||
|
alternates: {
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/products` : `${baseUrl}/${locale}/products`])
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${baseUrl}/about`,
|
url: `${baseUrl}/about`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "monthly",
|
changeFrequency: "monthly",
|
||||||
priority: 0.6,
|
priority: 0.6,
|
||||||
|
alternates: {
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/about` : `${baseUrl}/${locale}/about`])
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: `${baseUrl}/contact`,
|
url: `${baseUrl}/contact`,
|
||||||
lastModified: new Date(),
|
lastModified: new Date(),
|
||||||
changeFrequency: "monthly",
|
changeFrequency: "monthly",
|
||||||
priority: 0.6,
|
priority: 0.6,
|
||||||
|
alternates: {
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/contact` : `${baseUrl}/${locale}/contact`])
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: `${baseUrl}/checkout`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
changeFrequency: "monthly",
|
||||||
|
priority: 0.5,
|
||||||
|
alternates: {
|
||||||
|
languages: Object.fromEntries(
|
||||||
|
SUPPORTED_LOCALES.map((locale) => [locale, locale === "sr" ? `${baseUrl}/checkout` : `${baseUrl}/${locale}/checkout`])
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
...productUrls,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const productUrls: SitemapEntry[] = [];
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
const hreflangs: Record<string, string> = {};
|
||||||
|
for (const locale of SUPPORTED_LOCALES) {
|
||||||
|
const path = locale === "sr" ? `/products/${product.slug}` : `/${locale}/products/${product.slug}`;
|
||||||
|
hreflangs[locale] = `${baseUrl}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const locale of SUPPORTED_LOCALES) {
|
||||||
|
const localePrefix = locale === "sr" ? "" : `/${locale}`;
|
||||||
|
productUrls.push({
|
||||||
|
url: `${baseUrl}${localePrefix}/products/${product.slug}`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
changeFrequency: "weekly",
|
||||||
|
priority: 0.8,
|
||||||
|
alternates: {
|
||||||
|
languages: hreflangs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...staticPages, ...productUrls];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default function NewsletterSection() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder={t("emailPlaceholder")}
|
placeholder={t("emailPlaceholder")}
|
||||||
required
|
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"
|
className="flex-1 px-4 py-4 h-14 border border-[#1A1A1A]/10 rounded-[4px] text-base focus:outline-none focus:border-[#1A1A1A]/30 transition-colors"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface ProductShowcaseProps {
|
|||||||
locale?: string;
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductShowcase({ products, locale = "SR" }: ProductShowcaseProps) {
|
export default function ProductShowcase({ products, locale = "sr" }: ProductShowcaseProps) {
|
||||||
if (!products || products.length === 0) return null;
|
if (!products || products.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -140,6 +140,18 @@ export default function Footer({ locale = "sr" }: FooterProps) {
|
|||||||
© {currentYear} ManoonOils. {t("allRights")}
|
© {currentYear} ManoonOils. {t("allRights")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p className="text-xs text-[#999999]">
|
||||||
|
<strong>{t("madeWith")} ❤️ by{" "}
|
||||||
|
<a
|
||||||
|
href="https://nodecrew.me"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-[#c9a962] hover:text-[#b8944f] transition-colors"
|
||||||
|
>
|
||||||
|
Nodecrew
|
||||||
|
</a></strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-xs text-[#999999]">{t("weAccept")}</span>
|
<span className="text-xs text-[#999999]">{t("weAccept")}</span>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||||
import { User, ShoppingBag, Menu, X } from "lucide-react";
|
import { User, ShoppingBag, Menu, X, Globe } from "lucide-react";
|
||||||
import CartDrawer from "@/components/cart/CartDrawer";
|
import CartDrawer from "@/components/cart/CartDrawer";
|
||||||
|
import { SUPPORTED_LOCALES, LOCALE_COOKIE, LOCALE_CONFIG, isValidLocale, getPathWithoutLocale, buildLocalePath } from "@/lib/i18n/locales";
|
||||||
|
import type { Locale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
@@ -15,12 +18,41 @@ interface HeaderProps {
|
|||||||
|
|
||||||
export default function Header({ locale = "sr" }: HeaderProps) {
|
export default function Header({ locale = "sr" }: HeaderProps) {
|
||||||
const t = useTranslations("Header");
|
const t = useTranslations("Header");
|
||||||
|
const pathname = usePathname();
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const [langDropdownOpen, setLangDropdownOpen] = useState(false);
|
||||||
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
|
const { getLineCount, toggleCart, initCheckout } = useSaleorCheckoutStore();
|
||||||
|
|
||||||
const itemCount = getLineCount();
|
const itemCount = getLineCount();
|
||||||
const localePath = `/${locale}`;
|
const currentLocale = isValidLocale(locale) ? LOCALE_CONFIG[locale] : LOCALE_CONFIG.sr;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setLangDropdownOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const switchLocale = (newLocale: string) => {
|
||||||
|
if (newLocale === locale) {
|
||||||
|
setLangDropdownOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidLocale(newLocale)) {
|
||||||
|
setLangDropdownOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.cookie = `${LOCALE_COOKIE}=${newLocale}; path=/; max-age=31536000`;
|
||||||
|
const pathWithoutLocale = getPathWithoutLocale(pathname);
|
||||||
|
const newPath = buildLocalePath(newLocale as Locale, pathWithoutLocale);
|
||||||
|
window.location.replace(newPath);
|
||||||
|
setLangDropdownOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initCheckout();
|
initCheckout();
|
||||||
@@ -46,9 +78,9 @@ export default function Header({ locale = "sr" }: HeaderProps) {
|
|||||||
}, [mobileMenuOpen]);
|
}, [mobileMenuOpen]);
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ href: `${localePath}/products`, label: t("products") },
|
{ href: `/${locale}/products`, label: t("products") },
|
||||||
{ href: `${localePath}/about`, label: t("about") },
|
{ href: `/${locale}/about`, label: t("about") },
|
||||||
{ href: `${localePath}/contact`, label: t("contact") },
|
{ href: `/${locale}/contact`, label: t("contact") },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,7 +114,7 @@ export default function Header({ locale = "sr" }: HeaderProps) {
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<Link href={localePath || "/"} className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
|
<Link href={`/${locale}`} className="flex-shrink-0 lg:absolute lg:left-1/2 lg:-translate-x-1/2">
|
||||||
<Image
|
<Image
|
||||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||||
alt="ManoonOils"
|
alt="ManoonOils"
|
||||||
@@ -94,6 +126,40 @@ export default function Header({ locale = "sr" }: HeaderProps) {
|
|||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
<div ref={dropdownRef} className="relative">
|
||||||
|
<button
|
||||||
|
className="p-2 hover:bg-black/5 rounded-full transition-colors flex items-center gap-1"
|
||||||
|
onClick={() => setLangDropdownOpen(!langDropdownOpen)}
|
||||||
|
aria-label="Select language"
|
||||||
|
>
|
||||||
|
<Globe className="w-5 h-5" strokeWidth={1.5} />
|
||||||
|
<span className="text-sm">{currentLocale.flag}</span>
|
||||||
|
</button>
|
||||||
|
<AnimatePresence>
|
||||||
|
{langDropdownOpen && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -10 }}
|
||||||
|
className="absolute right-0 top-full mt-1 bg-white border border-[#e5e5e5] shadow-lg rounded-md overflow-hidden z-50"
|
||||||
|
>
|
||||||
|
{SUPPORTED_LOCALES.map((loc) => (
|
||||||
|
<button
|
||||||
|
key={loc}
|
||||||
|
onClick={() => switchLocale(loc)}
|
||||||
|
className={`flex items-center gap-2 px-4 py-2 text-sm hover:bg-black/5 transition-colors w-full text-left ${
|
||||||
|
loc === locale ? "bg-black/5 font-medium" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{LOCALE_CONFIG[loc].flag}</span>
|
||||||
|
<span>{LOCALE_CONFIG[loc].label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
className="p-2 hover:bg-black/5 rounded-full transition-colors hidden sm:block"
|
||||||
aria-label={t("account")}
|
aria-label={t("account")}
|
||||||
@@ -128,7 +194,7 @@ export default function Header({ locale = "sr" }: HeaderProps) {
|
|||||||
>
|
>
|
||||||
<div className="container h-full flex flex-col">
|
<div className="container h-full flex flex-col">
|
||||||
<div className="flex items-center justify-between h-[72px]">
|
<div className="flex items-center justify-between h-[72px]">
|
||||||
<Link href={localePath || "/"} onClick={() => setMobileMenuOpen(false)}>
|
<Link href={`/${locale}`} onClick={() => setMobileMenuOpen(false)}>
|
||||||
<Image
|
<Image
|
||||||
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
src="https://minio-api.nodecrew.me/manoon-media/2024/09/cropped-manoon-logo_256x-1-1.png"
|
||||||
alt="ManoonOils"
|
alt="ManoonOils"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ interface ProductBenefitsProps {
|
|||||||
locale?: string;
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductBenefits({ locale = "SR" }: ProductBenefitsProps) {
|
export default function ProductBenefits({ locale = "sr" }: ProductBenefitsProps) {
|
||||||
const t = useTranslations("ProductBenefits");
|
const t = useTranslations("ProductBenefits");
|
||||||
|
|
||||||
const benefits = [
|
const benefits = [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Link from "next/link";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import type { Product } from "@/types/saleor";
|
import type { Product } from "@/types/saleor";
|
||||||
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
import { getProductPrice, getProductImage, getLocalizedProduct } from "@/lib/saleor";
|
||||||
|
import { isValidLocale, getSaleorLocale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
interface ProductCardProps {
|
interface ProductCardProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
@@ -13,13 +14,13 @@ interface ProductCardProps {
|
|||||||
locale?: string;
|
locale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductCard({ product, index = 0, locale = "SR" }: ProductCardProps) {
|
export default function ProductCard({ product, index = 0, locale = "sr" }: ProductCardProps) {
|
||||||
const t = useTranslations("ProductCard");
|
const t = useTranslations("ProductCard");
|
||||||
const image = getProductImage(product);
|
const image = getProductImage(product);
|
||||||
const price = getProductPrice(product);
|
const price = getProductPrice(product);
|
||||||
const localized = getLocalizedProduct(product, locale);
|
const saleorLocale = isValidLocale(locale) ? getSaleorLocale(locale) : "SR";
|
||||||
|
const localized = getLocalizedProduct(product, saleorLocale);
|
||||||
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
|
const isAvailable = product.variants?.[0]?.quantityAvailable > 0;
|
||||||
const urlLocale = locale === "SR" ? "sr" : "en";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -28,7 +29,7 @@ export default function ProductCard({ product, index = 0, locale = "SR" }: Produ
|
|||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ duration: 0.5, delay: index * 0.1 }}
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
>
|
>
|
||||||
<Link href={`/${urlLocale}/products/${localized.slug}`} className="group block">
|
<Link href={`/${locale}/products/${localized.slug}`} className="group block">
|
||||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||||
{image ? (
|
{image ? (
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { useTranslations } from "next-intl";
|
|||||||
import type { Product } from "@/types/saleor";
|
import type { Product } from "@/types/saleor";
|
||||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||||
import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor";
|
import { getProductPrice, getProductPriceAmount, getLocalizedProduct, formatPrice } from "@/lib/saleor";
|
||||||
|
import { getTranslatedShortDescription, getTranslatedBenefits } from "@/lib/i18n/productText";
|
||||||
|
import { isValidLocale } from "@/lib/i18n/locales";
|
||||||
import ProductCard from "@/components/product/ProductCard";
|
import ProductCard from "@/components/product/ProductCard";
|
||||||
import ProductBenefits from "@/components/product/ProductBenefits";
|
import ProductBenefits from "@/components/product/ProductBenefits";
|
||||||
import ProductReviews from "@/components/product/ProductReviews";
|
import ProductReviews from "@/components/product/ProductReviews";
|
||||||
@@ -86,7 +88,7 @@ function StarRating({ rating = 5, count = 0 }: { rating?: number; count?: number
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductDetail({ product, relatedProducts, locale = "SR" }: ProductDetailProps) {
|
export default function ProductDetail({ product, relatedProducts, locale = "sr" }: ProductDetailProps) {
|
||||||
const t = useTranslations("ProductDetail");
|
const t = useTranslations("ProductDetail");
|
||||||
const tProduct = useTranslations("Product");
|
const tProduct = useTranslations("Product");
|
||||||
const [selectedImage, setSelectedImage] = useState(0);
|
const [selectedImage, setSelectedImage] = useState(0);
|
||||||
@@ -94,6 +96,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
const [urgencyIndex, setUrgencyIndex] = useState(0);
|
const [urgencyIndex, setUrgencyIndex] = useState(0);
|
||||||
const { addLine, openCart } = useSaleorCheckoutStore();
|
const { addLine, openCart } = useSaleorCheckoutStore();
|
||||||
|
const validLocale = isValidLocale(locale) ? locale : "sr";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -132,15 +135,10 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
const priceAmount = getProductPriceAmount(product);
|
const priceAmount = getProductPriceAmount(product);
|
||||||
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
|
const originalPrice = priceAmount > 0 ? formatPrice(Math.round(priceAmount * 1.30)) : null;
|
||||||
|
|
||||||
const shortDescription = localized.description
|
const shortDescription = getTranslatedShortDescription(localized.description, validLocale);
|
||||||
? localized.description.split('.')[0] + '.'
|
|
||||||
: locale === "EN" ? "Premium natural oil for your beauty routine." : "Premium prirodno ulje za vašu rutinu lepote.";
|
|
||||||
|
|
||||||
const benefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',') || [
|
const metadataBenefits = product.metadata?.find(m => m.key === "benefits")?.value?.split(',');
|
||||||
locale === "EN" ? "Natural" : "Prirodno",
|
const benefits = getTranslatedBenefits(metadataBenefits, validLocale);
|
||||||
locale === "EN" ? "Organic" : "Organsko",
|
|
||||||
locale === "EN" ? "Cruelty-free" : "Bez okrutnosti",
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -148,7 +146,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
|
<div className="border-b border-[#e5e5e5] pt-[72px] lg:pt-[72px]">
|
||||||
<div className="container py-5">
|
<div className="container py-5">
|
||||||
<nav className="flex items-center gap-2 text-sm">
|
<nav className="flex items-center gap-2 text-sm">
|
||||||
<Link href={`/${locale.toLowerCase()}`} className="text-[#666666] hover:text-black transition-colors">
|
<Link href={`/${validLocale}`} className="text-[#666666] hover:text-black transition-colors">
|
||||||
{t("home")}
|
{t("home")}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-[#999999]">/</span>
|
<span className="text-[#999999]">/</span>
|
||||||
@@ -469,7 +467,7 @@ export default function ProductDetail({ product, relatedProducts, locale = "SR"
|
|||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProductBenefits locale={locale} />
|
<ProductBenefits key={locale} locale={locale} />
|
||||||
|
|
||||||
<TrustBadges />
|
<TrustBadges />
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages } from "next-intl/server";
|
import { getMessages } from "next-intl/server";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { SUPPORTED_LOCALES, isValidLocale } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
export default async function LocaleProvider({
|
export default async function LocaleProvider({
|
||||||
children,
|
children,
|
||||||
@@ -11,8 +12,7 @@ export default async function LocaleProvider({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
locale: string;
|
locale: string;
|
||||||
}) {
|
}) {
|
||||||
const locales = ["en", "sr"];
|
if (!isValidLocale(locale)) notFound();
|
||||||
if (!locales.includes(locale)) notFound();
|
|
||||||
|
|
||||||
const messages = await getMessages();
|
const messages = await getMessages();
|
||||||
|
|
||||||
|
|||||||
@@ -68,18 +68,27 @@
|
|||||||
},
|
},
|
||||||
"Cart": {
|
"Cart": {
|
||||||
"title": "Ihr Warenkorb",
|
"title": "Ihr Warenkorb",
|
||||||
|
"yourCart": "Ihr Warenkorb",
|
||||||
|
"closeCart": "Warenkorb schließen",
|
||||||
"empty": "Ihr Warenkorb ist leer",
|
"empty": "Ihr Warenkorb ist leer",
|
||||||
"emptyDesc": "Es sieht so aus, als hätten Sie noch nichts in Ihren Warenkorb gelegt.",
|
"emptyDesc": "Es sieht so aus, als hätten Sie noch nichts in Ihren Warenkorb gelegt.",
|
||||||
"continueShopping": "Weiter einkaufen",
|
"continueShopping": "Weiter einkaufen",
|
||||||
|
"startShopping": "Einkauf starten",
|
||||||
"checkout": "Zur Kasse",
|
"checkout": "Zur Kasse",
|
||||||
"subtotal": "Zwischensumme",
|
"subtotal": "Zwischensumme",
|
||||||
"shipping": "Versand",
|
"shipping": "Versand",
|
||||||
"shippingCalc": "Wird an der Kasse berechnet",
|
"shippingCalc": "Wird an der Kasse berechnet",
|
||||||
|
"calculatedAtCheckout": "Wird an der Kasse berechnet",
|
||||||
|
"dismiss": "Schließen",
|
||||||
|
"yourCartEmpty": "Ihr Warenkorb ist leer",
|
||||||
|
"looksLikeEmpty": "Es sieht so aus, als hätten Sie nichts hinzugefügt",
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
"freeShipping": "Kostenloser Versand bei Bestellungen über {amount}",
|
"freeShipping": "Kostenloser Versand bei Bestellungen über {amount}",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
|
"removeItem": "Artikel entfernen",
|
||||||
"processes": "Wird bearbeitet...",
|
"processes": "Wird bearbeitet...",
|
||||||
"cartEmpty": "Ihr Warenkorb ist leer"
|
"cartEmpty": "Ihr Warenkorb ist leer",
|
||||||
|
"qty": "Menge"
|
||||||
},
|
},
|
||||||
"About": {
|
"About": {
|
||||||
"title": "Über uns",
|
"title": "Über uns",
|
||||||
@@ -156,7 +165,8 @@
|
|||||||
"help": "Hilfe",
|
"help": "Hilfe",
|
||||||
"contactUs": "Kontaktieren Sie uns",
|
"contactUs": "Kontaktieren Sie uns",
|
||||||
"brandDescription": "Premium natürliche Öle für Haar- und Hautpflege. Handgefertigt mit Liebe unter Verwendung traditioneller Methoden.",
|
"brandDescription": "Premium natürliche Öle für Haar- und Hautpflege. Handgefertigt mit Liebe unter Verwendung traditioneller Methoden.",
|
||||||
"weAccept": "Wir akzeptieren:"
|
"weAccept": "Wir akzeptieren:",
|
||||||
|
"madeWith": "Erstellt mit"
|
||||||
},
|
},
|
||||||
"Common": {
|
"Common": {
|
||||||
"loading": "Laden...",
|
"loading": "Laden...",
|
||||||
|
|||||||
@@ -132,19 +132,6 @@
|
|||||||
"faq4q": "Do you offer wholesale?",
|
"faq4q": "Do you offer wholesale?",
|
||||||
"faq4a": "Yes, we offer wholesale pricing for bulk orders. Please contact us at hello@manoonoils.com for more information."
|
"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",
|
|
||||||
"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": {
|
"Common": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"error": "An error occurred",
|
"error": "An error occurred",
|
||||||
@@ -321,7 +308,8 @@
|
|||||||
"contactUs": "Contact Us",
|
"contactUs": "Contact Us",
|
||||||
"brandDescription": "Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.",
|
"brandDescription": "Premium natural oils for hair and skin care. Handcrafted with love using traditional methods.",
|
||||||
"weAccept": "We accept:",
|
"weAccept": "We accept:",
|
||||||
"allRights": "All rights reserved."
|
"allRights": "All rights reserved.",
|
||||||
|
"madeWith": "Made with"
|
||||||
},
|
},
|
||||||
"ProductCard": {
|
"ProductCard": {
|
||||||
"noImage": "No image",
|
"noImage": "No image",
|
||||||
|
|||||||
@@ -68,18 +68,27 @@
|
|||||||
},
|
},
|
||||||
"Cart": {
|
"Cart": {
|
||||||
"title": "Votre Panier",
|
"title": "Votre Panier",
|
||||||
|
"yourCart": "Votre Panier",
|
||||||
|
"closeCart": "Fermer le panier",
|
||||||
"empty": "Votre panier est vide",
|
"empty": "Votre panier est vide",
|
||||||
"emptyDesc": "Il semble que vous n'ayez pas encore ajouté d'articles à votre panier.",
|
"emptyDesc": "Il semble que vous n'ayez pas encore ajouté d'articles à votre panier.",
|
||||||
"continueShopping": "Continuer les Achats",
|
"continueShopping": "Continuer les Achats",
|
||||||
|
"startShopping": "Commencer vos Achats",
|
||||||
"checkout": "Commander",
|
"checkout": "Commander",
|
||||||
"subtotal": "Sous-total",
|
"subtotal": "Sous-total",
|
||||||
"shipping": "Livraison",
|
"shipping": "Livraison",
|
||||||
"shippingCalc": "Calculé à la caisse",
|
"shippingCalc": "Calculé à la caisse",
|
||||||
|
"calculatedAtCheckout": "Calculé à la caisse",
|
||||||
|
"dismiss": "Fermer",
|
||||||
|
"yourCartEmpty": "Votre panier est vide",
|
||||||
|
"looksLikeEmpty": "On dirait que vous n'avez rien ajouté",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"freeShipping": "Livraison gratuite sur les commandes de {amount}",
|
"freeShipping": "Livraison gratuite sur les commandes de {amount}",
|
||||||
"remove": "Supprimer",
|
"remove": "Supprimer",
|
||||||
|
"removeItem": "Supprimer l'article",
|
||||||
"processes": "En cours...",
|
"processes": "En cours...",
|
||||||
"cartEmpty": "Votre panier est vide"
|
"cartEmpty": "Votre panier est vide",
|
||||||
|
"qty": "Qté"
|
||||||
},
|
},
|
||||||
"About": {
|
"About": {
|
||||||
"title": "À Propos",
|
"title": "À Propos",
|
||||||
@@ -156,7 +165,8 @@
|
|||||||
"help": "Aide",
|
"help": "Aide",
|
||||||
"contactUs": "Contactez-nous",
|
"contactUs": "Contactez-nous",
|
||||||
"brandDescription": "Huiles naturelles premium pour les soins capillaires et cutanés. Fait main avec amour en utilisant des méthodes traditionnelles.",
|
"brandDescription": "Huiles naturelles premium pour les soins capillaires et cutanés. Fait main avec amour en utilisant des méthodes traditionnelles.",
|
||||||
"weAccept": "Nous acceptons:"
|
"weAccept": "Nous acceptons:",
|
||||||
|
"madeWith": "Fait avec"
|
||||||
},
|
},
|
||||||
"Common": {
|
"Common": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
|
|||||||
@@ -132,19 +132,6 @@
|
|||||||
"faq4q": "Da li nudite veleprodaju?",
|
"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."
|
"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",
|
|
||||||
"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": {
|
"Common": {
|
||||||
"loading": "Učitavanje...",
|
"loading": "Učitavanje...",
|
||||||
"error": "Došlo je do greške",
|
"error": "Došlo je do greške",
|
||||||
@@ -321,7 +308,8 @@
|
|||||||
"contactUs": "Kontaktirajte nas",
|
"contactUs": "Kontaktirajte nas",
|
||||||
"brandDescription": "Premium prirodna ulja za negu kose i kože. Ručno pravljena sa ljubavlju, korišćenjem tradicionalnih metoda.",
|
"brandDescription": "Premium prirodna ulja za negu kose i kože. Ručno pravljena sa ljubavlju, korišćenjem tradicionalnih metoda.",
|
||||||
"weAccept": "Prihvatamo:",
|
"weAccept": "Prihvatamo:",
|
||||||
"allRights": "Sva prava zadržana."
|
"allRights": "Sva prava zadržana.",
|
||||||
|
"madeWith": "Napravljeno sa"
|
||||||
},
|
},
|
||||||
"ProductCard": {
|
"ProductCard": {
|
||||||
"noImage": "Nema slike",
|
"noImage": "Nema slike",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { defineRouting } from "next-intl/routing";
|
import { defineRouting } from "next-intl/routing";
|
||||||
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from "@/lib/i18n/locales";
|
||||||
|
|
||||||
export const routing = defineRouting({
|
export const routing = defineRouting({
|
||||||
locales: ["sr", "en", "de", "fr"],
|
locales: SUPPORTED_LOCALES,
|
||||||
defaultLocale: "sr",
|
defaultLocale: DEFAULT_LOCALE,
|
||||||
localePrefix: "as-needed",
|
localePrefix: "as-needed",
|
||||||
});
|
});
|
||||||
37
src/lib/i18n/locales.ts
Normal file
37
src/lib/i18n/locales.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
export const SUPPORTED_LOCALES = ["sr", "en", "de", "fr"] as const;
|
||||||
|
export type Locale = (typeof SUPPORTED_LOCALES)[number];
|
||||||
|
|
||||||
|
export const DEFAULT_LOCALE: Locale = "sr";
|
||||||
|
|
||||||
|
export const LOCALE_COOKIE = "NEXT_LOCALE";
|
||||||
|
|
||||||
|
export const LOCALE_CONFIG: Record<Locale, { label: string; flag: string; saleorLocale: string }> = {
|
||||||
|
sr: { label: "Srpski", flag: "🇷🇸", saleorLocale: "SR" },
|
||||||
|
en: { label: "English", flag: "🇬🇧", saleorLocale: "EN" },
|
||||||
|
de: { label: "Deutsch", flag: "🇩🇪", saleorLocale: "EN" },
|
||||||
|
fr: { label: "Français", flag: "🇫🇷", saleorLocale: "EN" },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isValidLocale(locale: string): locale is Locale {
|
||||||
|
return SUPPORTED_LOCALES.includes(locale as Locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSaleorLocale(locale: Locale): string {
|
||||||
|
return LOCALE_CONFIG[locale].saleorLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocaleFromPath(pathname: string): string {
|
||||||
|
const pattern = SUPPORTED_LOCALES.join("|");
|
||||||
|
const match = pathname.match(new RegExp(`^\\/(${pattern})`));
|
||||||
|
return match ? match[1] : DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPathWithoutLocale(pathname: string): string {
|
||||||
|
const pattern = SUPPORTED_LOCALES.join("|");
|
||||||
|
return pathname.replace(new RegExp(`^\\/(${pattern})`), "") || "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLocalePath(locale: Locale, path: string): string {
|
||||||
|
const pathPart = path === "/" ? "" : path;
|
||||||
|
return `/${locale}${pathPart}`;
|
||||||
|
}
|
||||||
37
src/lib/i18n/metadata.ts
Normal file
37
src/lib/i18n/metadata.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { DEFAULT_LOCALE, LOCALE_CONFIG, SUPPORTED_LOCALES, type Locale } from "./locales";
|
||||||
|
|
||||||
|
export function getSaleorLocale(locale: Locale): string {
|
||||||
|
return LOCALE_CONFIG[locale].saleorLocale;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocaleLabel(locale: Locale): string {
|
||||||
|
return LOCALE_CONFIG[locale].label;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDefaultLocale(locale: string): boolean {
|
||||||
|
return locale === DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocaleFromParams(params: { locale: string }): Locale {
|
||||||
|
const { locale } = params;
|
||||||
|
if (SUPPORTED_LOCALES.includes(locale as Locale)) {
|
||||||
|
return locale as Locale;
|
||||||
|
}
|
||||||
|
return DEFAULT_LOCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProductLocale(locale: Locale): string {
|
||||||
|
return getSaleorLocale(locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildHreflangAlternates(baseUrl: string): Record<string, string> {
|
||||||
|
const alternates: Record<string, string> = {};
|
||||||
|
for (const loc of SUPPORTED_LOCALES) {
|
||||||
|
if (loc === DEFAULT_LOCALE) {
|
||||||
|
alternates[loc] = baseUrl;
|
||||||
|
} else {
|
||||||
|
alternates[loc] = `${baseUrl}/${loc}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alternates;
|
||||||
|
}
|
||||||
98
src/lib/i18n/pageMetadata.ts
Normal file
98
src/lib/i18n/pageMetadata.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import type { Locale } from "./locales";
|
||||||
|
|
||||||
|
const PAGE_METADATA: Record<Locale, {
|
||||||
|
home: { title: string; description: string; productionAlt: string };
|
||||||
|
products: { title: string; description: string };
|
||||||
|
productNotFound: string;
|
||||||
|
about: { title: string; description: string; productionAlt: string };
|
||||||
|
contact: { title: string; description: string };
|
||||||
|
}> = {
|
||||||
|
sr: {
|
||||||
|
home: {
|
||||||
|
title: "ManoonOils - Premium prirodna ulja za negu kose i kože",
|
||||||
|
description: "Otkrijte našu premium kolekciju prirodnih ulja za negu kose i kože.",
|
||||||
|
productionAlt: "Proizvodnja prirodnih ulja",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Proizvodi - ManoonOils",
|
||||||
|
description: "Pregledajte našu kolekciju premium prirodnih ulja za negu kose i kože.",
|
||||||
|
},
|
||||||
|
productNotFound: "Proizvod nije pronađen",
|
||||||
|
about: {
|
||||||
|
title: "O nama - ManoonOils",
|
||||||
|
description: "Saznajte više o ManoonOils - naša priča, misija i posvećenost prirodnoj lepoti.",
|
||||||
|
productionAlt: "Proizvodnja prirodnih ulja",
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Kontakt - ManoonOils",
|
||||||
|
description: "Kontaktirajte nas za sva pitanja o proizvodima, narudžbinama ili saradnji.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
home: {
|
||||||
|
title: "ManoonOils - Premium Natural Oils for Hair & Skin",
|
||||||
|
description: "Discover our premium collection of natural oils for hair and skin care.",
|
||||||
|
productionAlt: "Natural oils production",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Products - ManoonOils",
|
||||||
|
description: "Browse our collection of premium natural oils for hair and skin care.",
|
||||||
|
},
|
||||||
|
productNotFound: "Product not found",
|
||||||
|
about: {
|
||||||
|
title: "About - ManoonOils",
|
||||||
|
description: "Learn more about ManoonOils - our story, mission, and commitment to natural beauty.",
|
||||||
|
productionAlt: "Natural oils production",
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Contact - ManoonOils",
|
||||||
|
description: "Contact us for any questions about products, orders, or collaborations.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
home: {
|
||||||
|
title: "ManoonOils - Premium natürliche Öle für Haar & Haut",
|
||||||
|
description: "Entdecken Sie unsere Premium-Kollektion natürlicher Öle für Haar- und Hautpflege.",
|
||||||
|
productionAlt: "Natürliche Ölproduktion",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Produkte - ManoonOils",
|
||||||
|
description: "Durchsuchen Sie unsere Kollektion premium natürlicher Öle für Haar- und Hautpflege.",
|
||||||
|
},
|
||||||
|
productNotFound: "Produkt nicht gefunden",
|
||||||
|
about: {
|
||||||
|
title: "Über uns - ManoonOils",
|
||||||
|
description: "Erfahren Sie mehr über ManoonOils und unsere Mission, premium natürliche Produkte anzubieten.",
|
||||||
|
productionAlt: "Natürliche Ölproduktion",
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Kontakt - ManoonOils",
|
||||||
|
description: "Kontaktieren Sie uns für Fragen zu Produkten, Bestellungen oder Zusammenarbeit.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
home: {
|
||||||
|
title: "ManoonOils - Huiles Naturelles Premium pour Cheveux & Peau",
|
||||||
|
description: "Découvrez notre collection premium d'huiles naturelles pour les soins capillaires et cutanés.",
|
||||||
|
productionAlt: "Production d'huiles naturelles",
|
||||||
|
},
|
||||||
|
products: {
|
||||||
|
title: "Produits - ManoonOils",
|
||||||
|
description: "Parcourez notre collection d'huiles naturelles premium pour les soins capillaires et cutanés.",
|
||||||
|
},
|
||||||
|
productNotFound: "Produit non trouvé",
|
||||||
|
about: {
|
||||||
|
title: "À propos - ManoonOils",
|
||||||
|
description: "En savoir plus sur ManoonOils et notre mission de fournir des produits naturels premium.",
|
||||||
|
productionAlt: "Production d'huiles naturelles",
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
title: "Contact - ManoonOils",
|
||||||
|
description: "Contactez-nous pour toute question sur les produits, commandes ou collaborations.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPageMetadata(locale: Locale) {
|
||||||
|
return PAGE_METADATA[locale] || PAGE_METADATA.en;
|
||||||
|
}
|
||||||
57
src/lib/i18n/productText.ts
Normal file
57
src/lib/i18n/productText.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { Locale } from "./locales";
|
||||||
|
|
||||||
|
const PRODUCT_TEXT: Record<Locale, {
|
||||||
|
defaultShortDescription: string;
|
||||||
|
defaultBenefits: string[];
|
||||||
|
}> = {
|
||||||
|
sr: {
|
||||||
|
defaultShortDescription: "Premium prirodno ulje za vašu rutinu lepote.",
|
||||||
|
defaultBenefits: ["Prirodno", "Organsko", "Bez okrutnosti"],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
defaultShortDescription: "Premium natural oil for your beauty routine.",
|
||||||
|
defaultBenefits: ["Natural", "Organic", "Cruelty-free"],
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
defaultShortDescription: "Premium natürliches Öl für Ihre Schönheitsroutine.",
|
||||||
|
defaultBenefits: ["Natürlich", "Bio", "Tierversuchsfrei"],
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
defaultShortDescription: "Huile naturelle premium pour votre routine beauté.",
|
||||||
|
defaultBenefits: ["Naturel", "Bio", "Sans cruauté"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getProductDefaults(locale: Locale) {
|
||||||
|
return PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslatedBenefits(
|
||||||
|
metadataBenefits: string[] | undefined,
|
||||||
|
locale: Locale
|
||||||
|
): string[] {
|
||||||
|
const defaults = PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
|
||||||
|
|
||||||
|
if (!metadataBenefits || metadataBenefits.length === 0) {
|
||||||
|
return defaults.defaultBenefits;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadataBenefits.map((benefit, index) => {
|
||||||
|
const trimmed = benefit.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return defaults.defaultBenefits[index] || trimmed;
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTranslatedShortDescription(
|
||||||
|
description: string | undefined,
|
||||||
|
locale: Locale
|
||||||
|
): string {
|
||||||
|
if (description && description.trim()) {
|
||||||
|
return description.split('.')[0] + '.';
|
||||||
|
}
|
||||||
|
const defaults = PRODUCT_TEXT[locale] || PRODUCT_TEXT.en;
|
||||||
|
return defaults.defaultShortDescription;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user