Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8bbd1b71 | ||
|
|
b476807a43 | ||
|
|
7f5c05d995 | ||
|
|
ede696ab2a |
@@ -37,9 +37,6 @@ const nextConfig: NextConfig = {
|
||||
];
|
||||
},
|
||||
images: {
|
||||
formats: ["image/avif", "image/webp"],
|
||||
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "https",
|
||||
@@ -61,16 +58,8 @@ const nextConfig: NextConfig = {
|
||||
hostname: "**.saleor.cloud",
|
||||
pathname: "/**",
|
||||
},
|
||||
{
|
||||
protocol: "https",
|
||||
hostname: "images.unsplash.com",
|
||||
pathname: "/**",
|
||||
},
|
||||
],
|
||||
},
|
||||
experimental: {
|
||||
optimizePackageImports: ["lucide-react", "framer-motion"],
|
||||
},
|
||||
};
|
||||
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||
import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales";
|
||||
import { getPageKeywords } from "@/lib/seo/keywords";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
@@ -68,13 +67,10 @@ export default async function AboutPage({ params }: AboutPageProps) {
|
||||
</div>
|
||||
|
||||
<div className="relative h-[400px] md:h-[500px] overflow-hidden">
|
||||
<Image
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
|
||||
alt={metadata.about.productionAlt}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="100vw"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20" />
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default async function LocaleLayout({
|
||||
<Script
|
||||
src="/api/script.js"
|
||||
data-site-id={RYBBIT_SITE_ID}
|
||||
strategy="lazyOnload"
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
{children}
|
||||
|
||||
@@ -14,9 +14,6 @@ import { getPageMetadata } from "@/lib/i18n/pageMetadata";
|
||||
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
|
||||
import { getPageKeywords, getBrandKeywords } from "@/lib/seo/keywords";
|
||||
import { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
@@ -160,12 +157,10 @@ export default async function Homepage({ params }: { params: Promise<{ locale: s
|
||||
</a>
|
||||
</div>
|
||||
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden">
|
||||
<Image
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
|
||||
alt={metadata.home.productionAlt}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,6 @@ import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/l
|
||||
import { getPageKeywords } from "@/lib/seo/keywords";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
interface ProductsPageProps {
|
||||
|
||||
+20
-34
@@ -53,7 +53,8 @@
|
||||
--color-cta-hover: #333333;
|
||||
--color-overlay: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Font variables will be set by next/font in layout.tsx */
|
||||
--font-display: 'DM Sans', sans-serif;
|
||||
--font-body: 'Inter', sans-serif;
|
||||
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-base: 250ms ease;
|
||||
@@ -65,9 +66,26 @@
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FONT IMPORTS
|
||||
============================================ */
|
||||
|
||||
@font-face {
|
||||
font-family: 'DM Sans';
|
||||
src: url('https://fonts.gstatic.com/s/dmsans/v15/rP2tp2ywxg089UriI5-g4vlH9VoD8CmcqZG40F9JadbnoEwAopxhS2f3ZGMZpg.woff2') format('woff2');
|
||||
font-weight: 400 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('https://fonts.gstatic.com/s/inter/v18/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfAZ9hjp-Ek-_EeA.woff2') format('woff2');
|
||||
font-weight: 400 700;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BASE STYLES (in Tailwind base layer)
|
||||
Fonts loaded via next/font in layout.tsx
|
||||
============================================ */
|
||||
|
||||
@layer base {
|
||||
@@ -248,38 +266,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SCROLL INDICATOR ANIMATION
|
||||
============================================ */
|
||||
|
||||
@keyframes scrollBounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(8px); }
|
||||
}
|
||||
|
||||
.scroll-indicator {
|
||||
animation: scrollBounce 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FADE SLIDE UP ANIMATION
|
||||
============================================ */
|
||||
|
||||
@keyframes fadeSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fadeSlideUp {
|
||||
animation: fadeSlideUp 0.6s ease-out both;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITIES
|
||||
============================================ */
|
||||
|
||||
+1
-14
@@ -1,22 +1,9 @@
|
||||
import "./globals.css";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { DM_Sans, Inter } from "next/font/google";
|
||||
import ErrorBoundary from "@/components/providers/ErrorBoundary";
|
||||
import { SUPPORTED_LOCALES } from "@/lib/i18n/locales";
|
||||
import { OrganizationSchema } from "@/components/seo";
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-display",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-body",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -52,7 +39,7 @@ export default async function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html suppressHydrationWarning className={`${dmSans.variable} ${inter.variable}`}>
|
||||
<html suppressHydrationWarning>
|
||||
<body className="antialiased" suppressHydrationWarning>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
@@ -23,23 +23,30 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
return (
|
||||
<section className="relative min-h-screen w-full overflow-hidden">
|
||||
{/* Background Image with Overlay */}
|
||||
<div className="absolute inset-0">
|
||||
<Image
|
||||
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop"
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="100vw"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop')`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" />
|
||||
</div>
|
||||
|
||||
{/* Content - Visible immediately, animations are enhancements */}
|
||||
{/* Content */}
|
||||
<div className="relative z-10 min-h-screen flex flex-col items-center justify-center text-center text-white px-4 py-20">
|
||||
<div className="max-w-4xl mx-auto animate-fadeSlideUp">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
className="max-w-4xl mx-auto"
|
||||
>
|
||||
{/* Social Proof Micro */}
|
||||
<div className="flex items-center justify-center gap-2 mb-6 animate-fadeSlideUp" style={{ animationDelay: "0.1s" }}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
className="flex items-center justify-center gap-2 mb-6"
|
||||
>
|
||||
<div className="flex">
|
||||
{[1, 2, 3, 4, 5].map((star) => (
|
||||
<svg key={star} className="w-4 h-4 fill-yellow-400 text-yellow-400" viewBox="0 0 24 24">
|
||||
@@ -50,30 +57,36 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
<span className="text-sm text-white/80">
|
||||
{t("lovedBy")}
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h1
|
||||
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight animate-fadeSlideUp"
|
||||
style={{ animationDelay: "0.2s" }}
|
||||
{/* Main Heading - Outcome Focused */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight"
|
||||
>
|
||||
{t("transformHeadline")}
|
||||
<br />
|
||||
<span className="text-white/90">{t("withNaturalOils")}</span>
|
||||
</h1>
|
||||
</motion.h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed animate-fadeSlideUp"
|
||||
style={{ animationDelay: "0.3s" }}
|
||||
{/* Subtitle - Expands on how */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.7 }}
|
||||
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed"
|
||||
>
|
||||
{t("subtitleText")}
|
||||
</p>
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<div
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fadeSlideUp"
|
||||
style={{ animationDelay: "0.4s" }}
|
||||
{/* CTA Button - Action verb + value */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.9 }}
|
||||
className="flex flex-col sm:flex-row items-center justify-center gap-4"
|
||||
>
|
||||
<Link
|
||||
href={`${localePath}/products`}
|
||||
@@ -87,12 +100,14 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
>
|
||||
{t("learnStory")}
|
||||
</Link>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60 animate-fadeSlideUp"
|
||||
style={{ animationDelay: "0.5s" }}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.8 }}
|
||||
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -112,21 +127,26 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
||||
</svg>
|
||||
<span>{t("crueltyFree")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<button
|
||||
<motion.button
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.5, duration: 0.8 }}
|
||||
onClick={scrollToContent}
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer opacity-0 animate-fade-in"
|
||||
style={{ animationDelay: "1.5s", animationFillMode: "forwards" }}
|
||||
className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer"
|
||||
aria-label="Scroll to content"
|
||||
>
|
||||
<div className="scroll-indicator">
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ repeat: Infinity, duration: 1.5, ease: "easeInOut" }}
|
||||
>
|
||||
<ChevronDown className="w-6 h-6" strokeWidth={1.5} />
|
||||
</div>
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,12 +32,11 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
|
||||
<Link href={`/${locale}/products/${localized.slug}`} className="group block">
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden mb-4">
|
||||
{image ? (
|
||||
<Image
|
||||
<img
|
||||
src={image}
|
||||
alt={localized.name}
|
||||
fill
|
||||
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||
@@ -53,7 +52,7 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<button
|
||||
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -245,12 +245,10 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
: "border-transparent hover:border-[#999999]"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
<img
|
||||
src={image.url}
|
||||
alt={image.alt || localized.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="100px"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
@@ -258,13 +256,10 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
)}
|
||||
|
||||
<div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
|
||||
<Image
|
||||
<img
|
||||
src={images[selectedImage].url}
|
||||
alt={images[selectedImage].alt || localized.name}
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
|
||||
{images.length > 1 && (
|
||||
@@ -312,15 +307,17 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
className="lg:pl-8"
|
||||
>
|
||||
<div className="min-h-[52px] flex items-center">
|
||||
<div
|
||||
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 px-4 rounded-lg mb-4 text-sm font-medium text-left w-full"
|
||||
key={urgencyIndex}
|
||||
>
|
||||
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||
{urgencyMessages[urgencyIndex].text}
|
||||
</div>
|
||||
</div>
|
||||
<motion.div
|
||||
key={urgencyIndex}
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
|
||||
>
|
||||
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||
{urgencyMessages[urgencyIndex].text}
|
||||
</motion.div>
|
||||
|
||||
<h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
|
||||
{localized.name}
|
||||
|
||||
@@ -100,6 +100,22 @@ export class AnalyticsTracker {
|
||||
newsletterSignedUp(email: string, source: string) {
|
||||
this.track({ type: "newsletter_signup", email, source });
|
||||
}
|
||||
|
||||
// Backward compatibility aliases (old method names)
|
||||
trackProductView = this.productViewed;
|
||||
trackAddToCart = this.addToCart;
|
||||
trackRemoveFromCart = this.removeFromCart;
|
||||
trackCartView = this.cartViewed;
|
||||
trackCheckoutStarted = this.checkoutStarted;
|
||||
trackCheckoutStep = this.checkoutStep;
|
||||
trackOrderCompleted = this.orderCompleted;
|
||||
trackSearch = this.searchPerformed;
|
||||
trackExternalLink = this.externalLinkClicked;
|
||||
trackWishlistAdd = this.wishlistAdded;
|
||||
trackUserLogin = this.userLoggedIn;
|
||||
trackUserRegister = this.userRegistered;
|
||||
trackNewsletterSignup = this.newsletterSignedUp;
|
||||
identifyUser = this.identify;
|
||||
}
|
||||
|
||||
let trackerInstance: AnalyticsTracker | null = null;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export function proxy(request: NextRequest) {
|
||||
const response = NextResponse.next();
|
||||
|
||||
const url = request.nextUrl.pathname;
|
||||
|
||||
if (
|
||||
url.startsWith("/sr") ||
|
||||
url.startsWith("/en") ||
|
||||
url.startsWith("/de") ||
|
||||
url.startsWith("/fr") ||
|
||||
url === "/"
|
||||
) {
|
||||
if (
|
||||
!url.includes("/checkout") &&
|
||||
!url.includes("/cart") &&
|
||||
!url.includes("/api/")
|
||||
) {
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"public, max-age=3600, stale-while-revalidate=86400"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/((?!_next/static|_next/image|favicon.ico|icon.png|robots.txt|sitemap.xml).*)",
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user