Compare commits
12 Commits
refactor/a
...
feature/hi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c9c091c46 | ||
|
|
27af03ba3a | ||
|
|
ad20ffe588 | ||
|
|
13301dca12 | ||
|
|
e57169a807 | ||
|
|
3697a5d8ea | ||
|
|
edd5c1582b | ||
|
|
dff78b28a5 | ||
|
|
b4905ce4ee | ||
|
|
e87c655a5b | ||
|
|
1c5ec1a271 | ||
|
|
8eb9f24b33 |
@@ -37,6 +37,9 @@ 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",
|
||||||
@@ -58,8 +61,16 @@ 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);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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";
|
||||||
|
|
||||||
@@ -67,10 +68,13 @@ 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">
|
||||||
<img
|
<Image
|
||||||
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}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
|
priority
|
||||||
|
className="object-cover"
|
||||||
|
sizes="100vw"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/20" />
|
<div className="absolute inset-0 bg-black/20" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ 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";
|
||||||
@@ -50,17 +49,11 @@ export default async function LocaleLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OpenPanelComponent
|
<AnalyticsProvider clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""} />
|
||||||
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="afterInteractive"
|
strategy="lazyOnload"
|
||||||
/>
|
/>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ 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";
|
||||||
|
|
||||||
@@ -157,10 +160,12 @@ 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">
|
||||||
<img
|
<Image
|
||||||
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}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ 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 {
|
||||||
|
|||||||
65
src/app/api/op/track/route.ts
Normal file
65
src/app/api/op/track/route.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
const OPENPANEL_API_URL = process.env.OPENPANEL_API_URL || "https://op.nodecrew.me/api";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.text();
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"openpanel-client-id": process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.OPENPANEL_CLIENT_SECRET) {
|
||||||
|
headers["openpanel-client-secret"] = process.env.OPENPANEL_CLIENT_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${OPENPANEL_API_URL}/track`, {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
return new NextResponse(data, {
|
||||||
|
status: response.status,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[OpenPanel Proxy] Error:", error);
|
||||||
|
return new NextResponse(JSON.stringify({ error: "Proxy error" }), {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const path = url.searchParams.get("path") || "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${OPENPANEL_API_URL}/track/${path}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"openpanel-client-id": process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.text();
|
||||||
|
return new NextResponse(data, {
|
||||||
|
status: response.status,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[OpenPanel Proxy] Error:", error);
|
||||||
|
return new NextResponse(JSON.stringify({ error: "Proxy error" }), {
|
||||||
|
status: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,8 +53,7 @@
|
|||||||
--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-display: 'DM Sans', sans-serif;
|
/* Font variables will be set by next/font in layout.tsx */
|
||||||
--font-body: 'Inter', sans-serif;
|
|
||||||
|
|
||||||
--transition-fast: 150ms ease;
|
--transition-fast: 150ms ease;
|
||||||
--transition-base: 250ms ease;
|
--transition-base: 250ms ease;
|
||||||
@@ -66,26 +65,9 @@
|
|||||||
--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 {
|
||||||
@@ -266,6 +248,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
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,9 +1,22 @@
|
|||||||
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 = {
|
||||||
@@ -39,7 +52,7 @@ export default async function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html suppressHydrationWarning>
|
<html suppressHydrationWarning className={`${dmSans.variable} ${inter.variable}`}>
|
||||||
<body className="antialiased" suppressHydrationWarning>
|
<body className="antialiased" suppressHydrationWarning>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
const mediaLogos = [
|
const mediaLogos = [
|
||||||
@@ -40,15 +39,9 @@ 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">
|
||||||
<motion.p
|
<p className="text-center text-[10px] uppercase tracking-[0.4em] text-[#c9a962] font-bold animate-fade-in">
|
||||||
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")}
|
||||||
</motion.p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -56,29 +49,30 @@ 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">
|
||||||
<motion.div
|
<div className="flex items-center gap-16 animate-marquee">
|
||||||
className="flex items-center gap-16"
|
{[...mediaLogos, ...mediaLogos].map((logo, index) => (
|
||||||
animate={{
|
<LogoItem key={`${logo.name}-${index}`} name={logo.name} />
|
||||||
x: [0, -50 + "%"],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
x: {
|
|
||||||
repeat: Infinity,
|
|
||||||
repeatType: "loop",
|
|
||||||
duration: 30,
|
|
||||||
ease: "linear",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mediaLogos.map((logo, index) => (
|
|
||||||
<LogoItem key={`first-${index}`} name={logo.name} />
|
|
||||||
))}
|
))}
|
||||||
{mediaLogos.map((logo, index) => (
|
</div>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,30 +23,23 @@ 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
|
<div className="absolute inset-0">
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
<Image
|
||||||
style={{
|
src="https://images.unsplash.com/photo-1608571423902-eed4a5ad8108?q=80&w=2574&auto=format&fit=crop"
|
||||||
backgroundImage: `url('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-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 */}
|
{/* Content - Visible immediately, animations are enhancements */}
|
||||||
<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">
|
||||||
<motion.div
|
<div className="max-w-4xl mx-auto animate-fadeSlideUp">
|
||||||
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 */}
|
||||||
<motion.div
|
<div className="flex items-center justify-center gap-2 mb-6 animate-fadeSlideUp" style={{ animationDelay: "0.1s" }}>
|
||||||
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">
|
||||||
@@ -57,36 +50,30 @@ 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>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Main Heading - Outcome Focused */}
|
{/* Main Heading */}
|
||||||
<motion.h1
|
<h1
|
||||||
initial={{ opacity: 0, y: 30 }}
|
className="text-4xl md:text-6xl lg:text-7xl font-medium mb-6 tracking-tight leading-tight animate-fadeSlideUp"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
style={{ animationDelay: "0.2s" }}
|
||||||
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>
|
||||||
</motion.h1>
|
</h1>
|
||||||
|
|
||||||
{/* Subtitle - Expands on how */}
|
{/* Subtitle */}
|
||||||
<motion.p
|
<p
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="text-lg md:text-xl text-white/80 mb-8 font-light max-w-2xl mx-auto leading-relaxed animate-fadeSlideUp"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
style={{ animationDelay: "0.3s" }}
|
||||||
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")}
|
||||||
</motion.p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Button - Action verb + value */}
|
{/* CTA Buttons */}
|
||||||
<motion.div
|
<div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-fadeSlideUp"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
style={{ animationDelay: "0.4s" }}
|
||||||
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`}
|
||||||
@@ -100,14 +87,12 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
|||||||
>
|
>
|
||||||
{t("learnStory")}
|
{t("learnStory")}
|
||||||
</Link>
|
</Link>
|
||||||
</motion.div>
|
</div>
|
||||||
|
|
||||||
{/* Trust Indicators */}
|
{/* Trust Indicators */}
|
||||||
<motion.div
|
<div
|
||||||
initial={{ opacity: 0 }}
|
className="flex flex-wrap items-center justify-center gap-6 mt-12 text-sm text-white/60 animate-fadeSlideUp"
|
||||||
animate={{ opacity: 1 }}
|
style={{ animationDelay: "0.5s" }}
|
||||||
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">
|
||||||
@@ -127,26 +112,21 @@ export default function HeroVideo({ locale = "sr" }: HeroVideoProps) {
|
|||||||
</svg>
|
</svg>
|
||||||
<span>{t("crueltyFree")}</span>
|
<span>{t("crueltyFree")}</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicator */}
|
{/* Scroll Indicator */}
|
||||||
<motion.button
|
<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"
|
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" }}
|
||||||
aria-label="Scroll to content"
|
aria-label="Scroll to content"
|
||||||
>
|
>
|
||||||
<motion.div
|
<div className="scroll-indicator">
|
||||||
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} />
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
"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 className="py-24 bg-gradient-to-b from-[#fefcfb] to-[#faf9f7]">
|
<section ref={sectionRef} 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">
|
||||||
<motion.div
|
<div className="max-w-3xl mx-auto text-center animate-on-scroll">
|
||||||
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>
|
||||||
@@ -27,18 +41,14 @@ 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" />
|
||||||
</motion.div>
|
</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) => (
|
||||||
<motion.div
|
<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"
|
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"
|
||||||
initial={{ opacity: 0, y: 30 }}
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
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" />
|
||||||
|
|
||||||
@@ -61,10 +71,29 @@ 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>
|
||||||
</motion.div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"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() {
|
||||||
@@ -9,21 +8,8 @@ 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">
|
||||||
<motion.div
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6">
|
||||||
className="grid grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-6"
|
<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" }}>
|
||||||
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" />
|
||||||
@@ -38,16 +24,9 @@ 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>
|
||||||
</motion.div>
|
</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" }}>
|
||||||
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" />
|
||||||
@@ -62,16 +41,9 @@ 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>
|
||||||
</motion.div>
|
</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" }}>
|
||||||
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" />
|
||||||
@@ -86,19 +58,12 @@ 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>
|
||||||
</motion.div>
|
</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" }}>
|
||||||
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 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" />
|
<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" />
|
||||||
</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">
|
||||||
@@ -110,9 +75,26 @@ 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>
|
||||||
</motion.div>
|
</div>
|
||||||
</motion.div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ 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 ? (
|
||||||
<img
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={localized.name}
|
alt={localized.name}
|
||||||
className="w-full h-full object-cover object-center transition-transform duration-700 ease-out group-hover:scale-105"
|
fill
|
||||||
loading="lazy"
|
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"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
<div className="absolute inset-0 flex items-center justify-center text-[#999999]">
|
||||||
@@ -52,7 +53,7 @@ export default function ProductCard({ product, index = 0, locale = "sr" }: Produ
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="absolute inset-x-0 bottom-0 p-4 translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
<div className="absolute inset-x-0 bottom-0 p-4 opacity-0 group-hover:opacity-100 transition-opacity 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) => {
|
||||||
|
|||||||
@@ -245,10 +245,12 @@ export default function ProductDetail({ product, relatedProducts, bundleProducts
|
|||||||
: "border-transparent hover:border-[#999999]"
|
: "border-transparent hover:border-[#999999]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<img
|
<Image
|
||||||
src={image.url}
|
src={image.url}
|
||||||
alt={image.alt || localized.name}
|
alt={image.alt || localized.name}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
sizes="100px"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -256,10 +258,13 @@ 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">
|
||||||
<img
|
<Image
|
||||||
src={images[selectedImage].url}
|
src={images[selectedImage].url}
|
||||||
alt={images[selectedImage].alt || localized.name}
|
alt={images[selectedImage].alt || localized.name}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
|
priority
|
||||||
|
className="object-cover"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{images.length > 1 && (
|
{images.length > 1 && (
|
||||||
@@ -307,17 +312,15 @@ 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"
|
||||||
>
|
>
|
||||||
<motion.div
|
<div className="min-h-[52px] flex items-center">
|
||||||
key={urgencyIndex}
|
<div
|
||||||
initial={{ opacity: 0, y: -10 }}
|
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"
|
||||||
animate={{ opacity: 1, y: 0 }}
|
key={urgencyIndex}
|
||||||
exit={{ opacity: 0, y: 10 }}
|
>
|
||||||
transition={{ duration: 0.3 }}
|
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
||||||
className="bg-white/80 backdrop-blur-sm text-[#1a1a1a] py-3 rounded-lg mb-4 text-sm font-medium text-left"
|
{urgencyMessages[urgencyIndex].text}
|
||||||
>
|
</div>
|
||||||
<span className="mr-2">{urgencyMessages[urgencyIndex].icon}</span>
|
</div>
|
||||||
{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}
|
||||||
|
|||||||
24
src/components/providers/AnalyticsProvider.tsx
Normal file
24
src/components/providers/AnalyticsProvider.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/middleware.ts
Normal file
35
src/middleware.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
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).*)",
|
||||||
|
],
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user