Created centralized helpers: - src/lib/i18n/pageMetadata.ts: All page metadata (titles, descriptions, alt text) - src/lib/i18n/productText.ts: Product-specific translated text (shortDescription, benefits) - src/lib/i18n/metadata.ts: Helper functions for locale handling Updated all pages to use centralized metadata: - Homepage: Uses getPageMetadata for title, description, productionAlt - Products page: Uses getPageMetadata - Product detail: Uses getPageMetadata + getTranslatedShortDescription/getTranslatedBenefits - About page: Uses getPageMetadata ProductDetail component now uses: - getTranslatedShortDescription() instead of locale comparison - getTranslatedBenefits() instead of locale comparison All user-facing text now goes through translation files or centralized helpers. Adding a new language now requires only: 1. Add to SUPPORTED_LOCALES in locales.ts 2. Add LOCALE_CONFIG entry 3. Add entries to pageMetadata.ts and productText.ts 4. Add translation keys to message files
227 lines
11 KiB
TypeScript
227 lines
11 KiB
TypeScript
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";
|
|
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 }> }) {
|
|
const { locale } = await params;
|
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
const metadata = getPageMetadata(validLocale as Locale);
|
|
setRequestLocale(validLocale);
|
|
return {
|
|
title: metadata.home.title,
|
|
description: metadata.home.description,
|
|
};
|
|
}
|
|
|
|
export default async function Homepage({ params }: { params: Promise<{ locale: string }> }) {
|
|
const { locale } = await params;
|
|
const validLocale = isValidLocale(locale) ? locale : DEFAULT_LOCALE;
|
|
setRequestLocale(validLocale);
|
|
const t = await getTranslations("Home");
|
|
const tBenefits = await getTranslations("Benefits");
|
|
const metadata = getPageMetadata(validLocale as Locale);
|
|
|
|
const saleorLocale = getSaleorLocale(validLocale as Locale);
|
|
let products: any[] = [];
|
|
try {
|
|
products = await getProducts(saleorLocale);
|
|
} catch (e) {
|
|
console.log("Failed to fetch products during build");
|
|
}
|
|
|
|
const featuredProducts = products?.slice(0, 4) || [];
|
|
const hasProducts = featuredProducts.length > 0;
|
|
|
|
const basePath = `/${validLocale}`;
|
|
|
|
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={locale} />
|
|
))}
|
|
</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={metadata.home.productionAlt}
|
|
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} />
|
|
</>
|
|
);
|
|
} |