Compare commits

..
Author SHA1 Message Date
Unchained 8a8bbd1b71 fix(analytics): create actual route handler for OpenPanel track endpoint
Build and Deploy / build (push) Has been cancelled
The standalone build doesn't support rewrites - need actual route handler to proxy OpenPanel API requests.
2026-03-31 08:18:20 +02:00
Unchained b476807a43 fix(analytics): add catch-all rewrite for OpenPanel /track paths
Build and Deploy / build (push) Has been cancelled
2026-03-31 08:15:36 +02:00
Unchained 7f5c05d995 fix(analytics): add backward compatibility aliases for old method names
Build and Deploy / build (push) Has been cancelled
The new AnalyticsTracker had different method names than the old useAnalytics hook. Added aliases like trackCheckoutStarted -> checkoutStarted etc. to maintain backward compatibility with existing components.
2026-03-31 08:09:20 +02:00
Unchained ede696ab2a fix(analytics): move legacy analytics.ts to legacy.ts to allow new abstraction to be used
Build and Deploy / build (push) Has been cancelled
The old analytics.ts was taking imports to resolve to it instead of the new analytics/ directory. Now analytics/ has index.ts which exports the new useAnalytics hook with the provider pattern.
2026-03-31 08:02:55 +02:00
17 changed files with 227 additions and 301 deletions
-11
View File
@@ -37,9 +37,6 @@ const nextConfig: NextConfig = {
]; ];
}, },
images: { images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
remotePatterns: [ remotePatterns: [
{ {
protocol: "https", protocol: "https",
@@ -61,16 +58,8 @@ const nextConfig: NextConfig = {
hostname: "**.saleor.cloud", hostname: "**.saleor.cloud",
pathname: "/**", pathname: "/**",
}, },
{
protocol: "https",
hostname: "images.unsplash.com",
pathname: "/**",
},
], ],
}, },
experimental: {
optimizePackageImports: ["lucide-react", "framer-motion", "clsx", "motion"],
},
}; };
export default withNextIntl(nextConfig); export default withNextIntl(nextConfig);
+2 -6
View File
@@ -5,7 +5,6 @@ import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales"; import { isValidLocale, DEFAULT_LOCALE, type Locale } from "@/lib/i18n/locales";
import { getPageKeywords } from "@/lib/seo/keywords"; import { getPageKeywords } from "@/lib/seo/keywords";
import { Metadata } from "next"; import { Metadata } from "next";
import Image from "next/image";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
@@ -68,13 +67,10 @@ export default async function AboutPage({ params }: AboutPageProps) {
</div> </div>
<div className="relative h-[400px] md:h-[500px] overflow-hidden"> <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" src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2000&auto=format&fit=crop"
alt={metadata.about.productionAlt} alt={metadata.about.productionAlt}
fill className="w-full h-full object-cover"
priority
className="object-cover"
sizes="100vw"
/> />
<div className="absolute inset-0 bg-black/20" /> <div className="absolute inset-0 bg-black/20" />
</div> </div>
+10 -3
View File
@@ -2,9 +2,10 @@ 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 { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales"; import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
import { OpenPanelComponent } from "@openpanel/nextjs";
import Script from "next/script"; import Script from "next/script";
import AnalyticsProvider from "@/components/providers/AnalyticsProvider";
// Rybbit configuration
const RYBBIT_SITE_ID = process.env.NEXT_PUBLIC_RYBBIT_SITE_ID || "1"; const RYBBIT_SITE_ID = process.env.NEXT_PUBLIC_RYBBIT_SITE_ID || "1";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
@@ -49,11 +50,17 @@ export default async function LocaleLayout({
return ( return (
<> <>
<AnalyticsProvider clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""} /> <OpenPanelComponent
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
trackScreenViews={true}
trackOutgoingLinks={true}
apiUrl="/api/op"
scriptUrl="/api/op1"
/>
<Script <Script
src="/api/script.js" src="/api/script.js"
data-site-id={RYBBIT_SITE_ID} data-site-id={RYBBIT_SITE_ID}
strategy="lazyOnload" strategy="afterInteractive"
/> />
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
{children} {children}
+2 -7
View File
@@ -14,9 +14,6 @@ import { getPageMetadata } from "@/lib/i18n/pageMetadata";
import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales"; import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/lib/i18n/locales";
import { getPageKeywords, getBrandKeywords } from "@/lib/seo/keywords"; import { getPageKeywords, getBrandKeywords } from "@/lib/seo/keywords";
import { Metadata } from "next"; import { Metadata } from "next";
import Image from "next/image";
export const revalidate = 3600;
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com"; 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> </a>
</div> </div>
<div className="relative aspect-[4/3] bg-[#e8f0f5] rounded-lg overflow-hidden"> <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" src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=800&auto=format&fit=crop"
alt={metadata.home.productionAlt} alt={metadata.home.productionAlt}
fill className="w-full h-full object-cover"
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw"
/> />
</div> </div>
</div> </div>
-2
View File
@@ -9,8 +9,6 @@ import { isValidLocale, DEFAULT_LOCALE, getSaleorLocale, type Locale } from "@/l
import { getPageKeywords } from "@/lib/seo/keywords"; import { getPageKeywords } from "@/lib/seo/keywords";
import { Metadata } from "next"; import { Metadata } from "next";
export const revalidate = 3600;
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
interface ProductsPageProps { interface ProductsPageProps {
+20 -34
View File
@@ -53,7 +53,8 @@
--color-cta-hover: #333333; --color-cta-hover: #333333;
--color-overlay: rgba(0, 0, 0, 0.4); --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-fast: 150ms ease;
--transition-base: 250ms ease; --transition-base: 250ms ease;
@@ -65,9 +66,26 @@
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1); --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) BASE STYLES (in Tailwind base layer)
Fonts loaded via next/font in layout.tsx
============================================ */ ============================================ */
@layer base { @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 UTILITIES
============================================ */ ============================================ */
+1 -14
View File
@@ -1,22 +1,9 @@
import "./globals.css"; import "./globals.css";
import type { Metadata, Viewport } from "next"; import type { Metadata, Viewport } from "next";
import { DM_Sans, Inter } from "next/font/google";
import ErrorBoundary from "@/components/providers/ErrorBoundary"; import ErrorBoundary from "@/components/providers/ErrorBoundary";
import { SUPPORTED_LOCALES } from "@/lib/i18n/locales"; import { SUPPORTED_LOCALES } from "@/lib/i18n/locales";
import { OrganizationSchema } from "@/components/seo"; 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"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://manoonoils.com";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -52,7 +39,7 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html suppressHydrationWarning className={`${dmSans.variable} ${inter.variable}`}> <html suppressHydrationWarning>
<body className="antialiased" suppressHydrationWarning> <body className="antialiased" suppressHydrationWarning>
<ErrorBoundary> <ErrorBoundary>
{children} {children}
+30 -24
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
const mediaLogos = [ const mediaLogos = [
@@ -39,9 +40,15 @@ export default function AsSeenIn() {
return ( return (
<section className="py-12 bg-[#1a1a1a] overflow-hidden border-y border-white/10"> <section className="py-12 bg-[#1a1a1a] overflow-hidden border-y border-white/10">
<div className="container mx-auto px-4 mb-8"> <div className="container mx-auto px-4 mb-8">
<p className="text-center text-[10px] uppercase tracking-[0.4em] text-[#c9a962] font-bold animate-fade-in"> <motion.p
className="text-center text-[10px] uppercase tracking-[0.4em] text-[#c9a962] font-bold"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
{t("title")} {t("title")}
</p> </motion.p>
</div> </div>
<div className="relative"> <div className="relative">
@@ -49,30 +56,29 @@ export default function AsSeenIn() {
<div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-[#1a1a1a] to-transparent z-10 pointer-events-none" /> <div className="absolute right-0 top-0 bottom-0 w-32 bg-gradient-to-l from-[#1a1a1a] to-transparent z-10 pointer-events-none" />
<div className="flex overflow-hidden"> <div className="flex overflow-hidden">
<div className="flex items-center gap-16 animate-marquee"> <motion.div
{[...mediaLogos, ...mediaLogos].map((logo, index) => ( className="flex items-center gap-16"
<LogoItem key={`${logo.name}-${index}`} name={logo.name} /> animate={{
x: [0, -50 + "%"],
}}
transition={{
x: {
repeat: Infinity,
repeatType: "loop",
duration: 30,
ease: "linear",
},
}}
>
{mediaLogos.map((logo, index) => (
<LogoItem key={`first-${index}`} name={logo.name} />
))} ))}
</div> {mediaLogos.map((logo, index) => (
<LogoItem key={`second-${index}`} name={logo.name} />
))}
</motion.div>
</div> </div>
</div> </div>
<style>{`
@keyframes marquee {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 30s linear infinite;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fade-in {
animation: fade-in 0.6s ease-out forwards;
}
`}</style>
</section> </section>
); );
} }
+61 -41
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "framer-motion";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
@@ -23,23 +23,30 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
return ( return (
<section className="relative min-h-screen w-full overflow-hidden"> <section className="relative min-h-screen w-full overflow-hidden">
{/* Background Image with Overlay */} {/* Background Image with Overlay */}
<div className="absolute inset-0"> <div
<Image className="absolute inset-0 bg-cover bg-center bg-no-repeat"
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop" style={{
alt="" backgroundImage: `url('https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop')`,
fill }}
priority >
className="object-cover"
sizes="100vw"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" /> <div className="absolute inset-0 bg-gradient-to-b from-black/50 via-black/40 to-black/70" />
</div> </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="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 */} {/* 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"> <div className="flex">
{[1, 2, 3, 4, 5].map((star) => ( {[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"> <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"> <span className="text-sm text-white/80">
{t("lovedBy")} {t("lovedBy")}
</span> </span>
</div> </motion.div>
{/* Main Heading */} {/* Main Heading - Outcome Focused */}
<h1 <motion.h1
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight animate-fadeSlideUp" initial={{ opacity: 0, y: 30 }}
style={{ animationDelay: "0.2s" }} 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")} {t("transformHeadline")}
<br /> <br />
<span className="text-white/90">{t("withNaturalOils")}</span> <span className="text-white/90">{t("withNaturalOils")}</span>
</h1> </motion.h1>
{/* Subtitle */} {/* Subtitle - Expands on how */}
<p <motion.p
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed animate-fadeSlideUp" initial={{ opacity: 0, y: 20 }}
style={{ animationDelay: "0.3s" }} 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")} {t("subtitleText")}
</p> </motion.p>
{/* CTA Buttons */} {/* CTA Button - Action verb + value */}
<div <motion.div
className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fadeSlideUp" initial={{ opacity: 0, y: 20 }}
style={{ animationDelay: "0.4s" }} 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 <Link
href={`${localePath}/products`} href={`${localePath}/products`}
@@ -87,12 +100,14 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
> >
{t("learnStory")} {t("learnStory")}
</Link> </Link>
</div> </motion.div>
{/* Trust Indicators */} {/* Trust Indicators */}
<div <motion.div
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60 animate-fadeSlideUp" initial={{ opacity: 0 }}
style={{ animationDelay: "0.5s" }} 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"> <div className="flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> </svg>
<span>{t("crueltyFree")}</span> <span>{t("crueltyFree")}</span>
</div> </div>
</div> </motion.div>
</div> </motion.div>
</div> </div>
{/* Scroll Indicator */} {/* Scroll Indicator */}
<button <motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.5, duration: 0.8 }}
onClick={scrollToContent} 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" className="absolute bottom-10 left-1/2 -translate-x-1/2 text-white/60 hover:text-white transition-colors cursor-pointer"
style={{ animationDelay: "1.5s", animationFillMode: "forwards" }}
aria-label="Scroll to content" 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} /> <ChevronDown className="w-6 h-6" strokeWidth={1.5} />
</div> </motion.div>
</button> </motion.button>
</section> </section>
); );
} }
+19 -48
View File
@@ -1,36 +1,22 @@
"use client"; "use client";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useEffect, useRef } from "react";
export default function ProblemSection() { export default function ProblemSection() {
const t = useTranslations("ProblemSection"); const t = useTranslations("ProblemSection");
const problems = t.raw("problems") as Array<{ problem: string; description: string }>; const problems = t.raw("problems") as Array<{ problem: string; description: string }>;
const sectionRef = useRef<HTMLElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("animate-visible");
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
const animatedElements = sectionRef.current?.querySelectorAll(".animate-on-scroll");
animatedElements?.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, []);
return ( return (
<section ref={sectionRef} className="py-24 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]"> <section className="py-24 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="max-w-3xl mx-auto text-center animate-on-scroll"> <motion.div
className="max-w-3xl mx-auto text-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium"> <span className="text-xs uppercase tracking-[0.3em] text-[#c9a962] mb-4 block font-medium">
{t("title")} {t("title")}
</span> </span>
@@ -41,14 +27,18 @@ export default function ProblemSection() {
{t("description")} {t("description")}
</p> </p>
<div className="w-16 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-8 rounded-full" /> <div className="w-16 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] mx-auto mt-8 rounded-full" />
</div> </motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto mt-16"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto mt-16">
{problems.map((item, index) => ( {problems.map((item, index) => (
<div <motion.div
key={index} 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 animate-on-scroll" 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"
style={{ animationDelay: `${index * 100}ms` }} initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
whileHover={{ y: -5 }}
> >
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-20 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] rounded-b-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" /> <div className="absolute top-0 left-1/2 -translate-x-1/2 w-20 h-1 bg-gradient-to-r from-[#c9a962] to-[#FFD700] rounded-b-full opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
@@ -71,29 +61,10 @@ export default function ProblemSection() {
</div> </div>
<h3 className="text-lg font-semibold text-[#1a1a1a] mb-3">{item.problem}</h3> <h3 className="text-lg font-semibold text-[#1a1a1a] mb-3">{item.problem}</h3>
<p className="text-sm text-[#666666] leading-relaxed">{item.description}</p> <p className="text-sm text-[#666666] leading-relaxed">{item.description}</p>
</div> </motion.div>
))} ))}
</div> </div>
</div> </div>
<style>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-on-scroll {
opacity: 0;
}
.animate-on-scroll.animate-visible {
animation: fadeInUp 0.5s ease-out forwards;
}
`}</style>
</section> </section>
); );
} }
+47 -29
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
export default function TrustBadges() { export default function TrustBadges() {
@@ -8,8 +9,21 @@ export default function TrustBadges() {
return ( return (
<section className="py-16 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]"> <section className="py-16 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"> <motion.div
<div className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300 animate-fadeSlideUp" style={{ animationDelay: "0s" }}> className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<motion.div
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0 }}
whileHover={{ y: -3 }}
>
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]"> <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
<svg className="w-6 h-6 text-yellow-400" viewBox="0 0 24 24" fill="currentColor"> <svg className="w-6 h-6 text-yellow-400" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /> <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
@@ -24,9 +38,16 @@ export default function TrustBadges() {
<p className="text-xs text-[#888888] mt-0.5"> <p className="text-xs text-[#888888] mt-0.5">
{t("basedOnReviews")} {t("basedOnReviews")}
</p> </p>
</div> </motion.div>
<div className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300 animate-fadeSlideUp" style={{ animationDelay: "0.1s" }}> <motion.div
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.1 }}
whileHover={{ y: -3 }}
>
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]"> <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5"> <svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#c9a962" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
@@ -41,9 +62,16 @@ export default function TrustBadges() {
<p className="text-xs text-[#888888] mt-0.5"> <p className="text-xs text-[#888888] mt-0.5">
{t("worldwide")} {t("worldwide")}
</p> </p>
</div> </motion.div>
<div className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300 animate-fadeSlideUp" style={{ animationDelay: "0.2s" }}> <motion.div
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.2 }}
whileHover={{ y: -3 }}
>
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]"> <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#7eb89e" strokeWidth="1.5"> <svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#7eb89e" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" />
@@ -58,12 +86,19 @@ export default function TrustBadges() {
<p className="text-xs text-[#888888] mt-0.5"> <p className="text-xs text-[#888888] mt-0.5">
{t("noAdditives")} {t("noAdditives")}
</p> </p>
</div> </motion.div>
<div className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300 animate-fadeSlideUp" style={{ animationDelay: "0.3s" }}> <motion.div
className="flex flex-col items-center text-center p-5 bg-white rounded-2xl shadow-md border border-[#f0ede8] hover:shadow-xl hover:border-[#c9a962]/30 transition-all duration-300"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.3 }}
whileHover={{ y: -3 }}
>
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]"> <div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#faf9f7] to-[#f5f0e8] flex items-center justify-center shadow-sm mb-4 border border-[#e8e4dc]">
<svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#e8967a" strokeWidth="1.5"> <svg className="w-6 h-6" viewBox="0 0 24 24" fill="none" stroke="#e8967a" strokeWidth="1.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0v1.875c0 .621-.504 1.125-1.125 1.125H4.125A1.125 1.125 0 013 16.875v-1.875m12-9.375v-6.75m0 4.5v-4.5m0 0h-12" /> <path strokeLinecap="round" strokeLinejoin="round" d="M8.25 18.75a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 01-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m3 0h1.125c.621 0 1.129-.504 1.09-1.124a17.902 17.902 0 00-3.213-9.193 2.056 2.056 0 00-1.58-.86H14.25M16.5 18.75h-2.25m0-11.177v-.958c0-.568-.422-1.048-.987-1.106a48.554 48.554 0 00-10.026 0 1.106 1.106 0 00-.987 1.106v7.635m12-6.677v6.677m0 4.5v-4.5m0 0h-12" />
</svg> </svg>
</div> </div>
<p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight"> <p className="text-2xl lg:text-3xl font-bold bg-gradient-to-r from-[#1a1a1a] to-[#4a4a4a] bg-clip-text text-transparent tracking-tight">
@@ -75,26 +110,9 @@ export default function TrustBadges() {
<p className="text-xs text-[#888888] mt-0.5"> <p className="text-xs text-[#888888] mt-0.5">
{t("ordersOver")} {t("ordersOver")}
</p> </p>
</div> </motion.div>
</div> </motion.div>
</div> </div>
<style>{`
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeSlideUp {
opacity: 0;
animation: fadeInUp 0.5s ease-out forwards;
}
`}</style>
</section> </section>
); );
} }
+4 -5
View File
@@ -32,12 +32,11 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
<Link href={`/${locale}/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 ? (
<Image <img
src={image} src={image}
alt={localized.name} alt={localized.name}
fill className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
className="object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105" loading="lazy"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
/> />
) : ( ) : (
<div className="absolute inset-0 flex items-center justify-center text-[#999999]"> <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>
)} )}
<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 <button
className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors" className="w-full py-3 bg-black text-white text-xs uppercase tracking-[0.1em] hover:bg-[#333333] transition-colors"
onClick={(e) => { onClick={(e) => {
+15 -18
View File
@@ -245,12 +245,10 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
: "border-transparent hover:border-[#999999]" : "border-transparent hover:border-[#999999]"
}`} }`}
> >
<Image <img
src={image.url} src={image.url}
alt={image.alt || localized.name} alt={image.alt || localized.name}
fill className="w-full h-full object-cover"
className="object-cover"
sizes="100px"
/> />
</button> </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"> <div className="relative w-full aspect-square bg-[#f8f9fa] overflow-hidden flex-1">
<Image <img
src={images[selectedImage].url} src={images[selectedImage].url}
alt={images[selectedImage].alt || localized.name} alt={images[selectedImage].alt || localized.name}
fill className="w-full h-full object-cover"
priority
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw"
/> />
{images.length > 1 && ( {images.length > 1 && (
@@ -312,15 +307,17 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
transition={{ duration: 0.6, delay: 0.2 }} transition={{ duration: 0.6, delay: 0.2 }}
className="lg:pl-8" className="lg:pl-8"
> >
<div className="min-h-[52px] flex items-center"> <motion.div
<div key={urgencyIndex}
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" initial={{ opacity: 0, y: -10 }}
key={urgencyIndex} animate={{ opacity: 1, y: 0 }}
> exit={{ opacity: 0, y: 10 }}
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span> transition={{ duration: 0.3 }}
{urgencyMessages[urgencyIndex].text} className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
</div> >
</div> <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"> <h1 className="text-3xl md:text-4xl font-medium mb-4 tracking-tight">
{localized.name} {localized.name}
@@ -1,24 +0,0 @@
"use client";
import dynamic from "next/dynamic";
const OpenPanelComponent = dynamic(
() => import("@openpanel/nextjs").then((mod) => mod.OpenPanelComponent),
{ ssr: false }
);
interface AnalyticsProviderProps {
clientId: string;
}
export default function AnalyticsProvider({ clientId }: AnalyticsProviderProps) {
return (
<OpenPanelComponent
clientId={clientId}
trackScreenViews={true}
trackOutgoingLinks={true}
apiUrl="/api/op"
scriptUrl="/api/op1"
/>
);
}
@@ -100,6 +100,22 @@ export class AnalyticsTracker {
newsletterSignedUp(email: string, source: string) { newsletterSignedUp(email: string, source: string) {
this.track({ type: "newsletter_signup", email, source }); 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; let trackerInstance: AnalyticsTracker | null = null;
-35
View File
@@ -1,35 +0,0 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(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).*)",
],
};