290 lines
11 KiB
TypeScript
290 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
motion,
|
|
AnimatePresence,
|
|
useScroll,
|
|
useTransform,
|
|
useMotionValue,
|
|
useSpring,
|
|
} from "framer-motion";
|
|
import { useRef, useState, useEffect, useCallback } from "react";
|
|
import Header from "@/components/Header";
|
|
import type { Locale } from "@/i18n-config";
|
|
import dynamic from "next/dynamic";
|
|
|
|
const HomeClientBelowFold = dynamic(() => import('./HomeClientBelowFold'));
|
|
|
|
// ── EASING ──
|
|
const expo: [number, number, number, number] = [0.16, 1, 0.3, 1];
|
|
|
|
// ── SVG ICONS ──
|
|
const IconAI = () => (
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="3" y="3" width="18" height="18" rx="1" />
|
|
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
|
|
<circle cx="9" cy="9" r="1.5" fill="currentColor" />
|
|
<circle cx="15" cy="15" r="1.5" fill="currentColor" />
|
|
</svg>
|
|
);
|
|
const IconChain = () => (
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
|
<path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12" />
|
|
</svg>
|
|
);
|
|
const IconMobile = () => (
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="5" y="2" width="14" height="20" rx="2" />
|
|
<path d="M12 18h.01" />
|
|
</svg>
|
|
);
|
|
const IconWeb = () => (
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<rect x="3" y="4" width="18" height="16" rx="1" />
|
|
<path d="M3 9h18M8 6h.01M11 6h.01M14 6h.01" />
|
|
</svg>
|
|
);
|
|
const IconArrow = () => (
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
<polyline points="12 5 19 12 12 19" />
|
|
</svg>
|
|
);
|
|
const IconArrowUpRight = () => (
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
<line x1="7" y1="17" x2="17" y2="7" />
|
|
<polyline points="7 7 17 7 17 17" />
|
|
</svg>
|
|
);
|
|
const IconPlus = () => (
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
</svg>
|
|
);
|
|
const IconCheck = () => (
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#0A0A0A" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<polyline points="20 6 9 17 4 12" />
|
|
</svg>
|
|
);
|
|
|
|
// ── ANIMATED COUNTER ──
|
|
function Counter({ target, suffix = "" }: { target: number; suffix?: string }) {
|
|
const [count, setCount] = useState(0);
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const [started, setStarted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => { if (entry.isIntersecting) setStarted(true); },
|
|
{ threshold: 0.5 }
|
|
);
|
|
if (ref.current) observer.observe(ref.current);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!started) return;
|
|
let cur = 0;
|
|
const steps = 50;
|
|
const inc = target / steps;
|
|
const iv = setInterval(() => {
|
|
cur += inc;
|
|
if (cur >= target) { setCount(target); clearInterval(iv); }
|
|
else setCount(Math.floor(cur));
|
|
}, 1600 / steps);
|
|
return () => clearInterval(iv);
|
|
}, [started, target]);
|
|
|
|
return <span ref={ref}>{count.toLocaleString()}{suffix}</span>;
|
|
}
|
|
|
|
// ── MAGNETIC BUTTON ──
|
|
function MagneticBtn({ children, className, href }: { children: React.ReactNode; className?: string; href?: string }) {
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const x = useMotionValue(0);
|
|
const y = useMotionValue(0);
|
|
const sx = useSpring(x, { stiffness: 200, damping: 20 });
|
|
const sy = useSpring(y, { stiffness: 200, damping: 20 });
|
|
|
|
const handleMouse = useCallback((e: React.MouseEvent) => {
|
|
if (!ref.current) return;
|
|
const { left, top, width, height } = ref.current.getBoundingClientRect();
|
|
x.set((e.clientX - left - width / 2) * 0.3);
|
|
y.set((e.clientY - top - height / 2) * 0.3);
|
|
}, [x, y]);
|
|
|
|
const handleLeave = useCallback(() => { x.set(0); y.set(0); }, [x, y]);
|
|
|
|
const Tag = href ? "a" : "div";
|
|
return (
|
|
<motion.div ref={ref} style={{ x: sx, y: sy }} onMouseMove={handleMouse} onMouseLeave={handleLeave} className="inline-block">
|
|
<Tag href={href} className={className}>{children}</Tag>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
// ── MARQUEE TEXT ──
|
|
function Marquee({ text, reverse = false }: { text: string; reverse?: boolean }) {
|
|
const repeated = Array(8).fill(text).join(" · ");
|
|
return (
|
|
<div className="marquee-wrapper border-y border-[#C8C2B8] bg-[#EDE8E0] py-3 overflow-hidden">
|
|
<div className={reverse ? "marquee-track-reverse" : "marquee-track"}>
|
|
{[0, 1].map(i => (
|
|
<span key={i} className="font-mono text-[11px] tracking-[0.35em] uppercase text-[#A0998E] px-8 whitespace-nowrap flex-shrink-0">
|
|
{repeated}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── SPLIT TEXT ANIMATION ──
|
|
function SplitReveal({ text, className, delay = 0 }: { text: string; className?: string; delay?: number }) {
|
|
const words = text.split(" ");
|
|
return (
|
|
<span className={`inline-flex flex-wrap gap-x-[0.25em] ${className}`}>
|
|
{words.map((word, i) => (
|
|
<span key={i} className="overflow-hidden inline-block">
|
|
<motion.span
|
|
className="inline-block"
|
|
initial={{ y: "110%", rotate: 2 }}
|
|
whileInView={{ y: 0, rotate: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.75, ease: expo, delay: delay + i * 0.06 }}
|
|
>
|
|
{word}
|
|
</motion.span>
|
|
</span>
|
|
))}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
// ── SECTION LABEL ──
|
|
function Label({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<div className="w-4 h-4 bg-[#FFE600] flex-shrink-0" />
|
|
<span className="font-mono text-[10px] tracking-[0.3em] uppercase text-[#A0998E]">{children}</span>
|
|
<div className="flex-1 h-px bg-[#C8C2B8]" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ── MAIN COMPONENT ──
|
|
export default function HomeClient({ lang, dict }: { lang: Locale; dict: any }) {
|
|
const heroRef = useRef<HTMLDivElement>(null);
|
|
const { scrollYProgress } = useScroll({ target: heroRef, offset: ["start start", "end start"] });
|
|
const heroY = useTransform(scrollYProgress, [0, 1], ["0%", "25%"]);
|
|
const heroOpacity = useTransform(scrollYProgress, [0, 0.7], [1, 0]);
|
|
|
|
|
|
|
|
return (
|
|
<div className="bg-[#F4F0E8] text-[#0A0A0A] font-body selection:bg-[#FFE600] selection:text-[#0A0A0A]">
|
|
{/* Noise overlay */}
|
|
<div className="noise-overlay" />
|
|
|
|
<Header lang={lang} dict={dict.nav} />
|
|
|
|
{/* ──────────────── HERO ──────────────── */}
|
|
<section ref={heroRef} className="relative min-h-screen flex flex-col justify-end overflow-hidden border-b border-[#C8C2B8]">
|
|
|
|
{/* Dot grid bg */}
|
|
<motion.div
|
|
className="absolute inset-0 dot-grid opacity-60"
|
|
style={{ y: heroY, opacity: heroOpacity }}
|
|
/>
|
|
|
|
{/* Big watermark letters */}
|
|
<motion.div
|
|
className="absolute right-0 bottom-0 font-display font-black text-[22vw] leading-none text-[#E8E3DC] select-none pointer-events-none overflow-hidden"
|
|
style={{ y: useTransform(scrollYProgress, [0, 1], ["0%", "20%"]) }}
|
|
initial={{ opacity: 0, x: 40 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 1.2, delay: 0.5 }}
|
|
>
|
|
AT
|
|
</motion.div>
|
|
|
|
{/* Yellow right accent */}
|
|
<div className="absolute top-0 right-0 w-1 h-full bg-[#FFE600]" />
|
|
|
|
<div className="relative z-10 max-w-7xl mx-auto px-6 w-full pb-20 pt-40">
|
|
|
|
{/* Meta bar */}
|
|
<motion.div
|
|
className="flex items-center justify-between border-b border-[#C8C2B8] pb-6 mb-16"
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.1, duration: 0.8 }}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<span className="w-2 h-2 bg-[#00CC77] rounded-full animate-pulse" />
|
|
<span className="font-mono text-[10px] tracking-[0.25em] uppercase text-[#A0998E]">
|
|
{dict.hero.badge}
|
|
</span>
|
|
</div>
|
|
<span className="font-mono text-[10px] tracking-[0.2em] text-[#A0998E]">
|
|
EST. 2025 — MUĞLA
|
|
</span>
|
|
</motion.div>
|
|
|
|
{/* Headline */}
|
|
<div className="mb-12">
|
|
<h1 className="font-display font-black uppercase leading-[0.85] tracking-tight">
|
|
<motion.div className="overflow-hidden" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }}>
|
|
<motion.span
|
|
className="block text-[15vw] sm:text-[12vw] lg:text-[10vw] text-[#0A0A0A]"
|
|
initial={{ y: "105%" }}
|
|
animate={{ y: 0 }}
|
|
transition={{ delay: 0.2, duration: 0.9, ease: expo }}
|
|
>
|
|
{dict.hero.titleLine1}
|
|
</motion.span>
|
|
</motion.div>
|
|
<motion.div className="overflow-hidden" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.3 }}>
|
|
<motion.span
|
|
className="block text-[15vw] sm:text-[12vw] lg:text-[10vw] text-stroke"
|
|
initial={{ y: "105%" }}
|
|
animate={{ y: 0 }}
|
|
transition={{ delay: 0.35, duration: 0.9, ease: expo }}
|
|
>
|
|
{dict.hero.titleLine2}
|
|
</motion.span>
|
|
</motion.div>
|
|
</h1>
|
|
</div>
|
|
|
|
{/* Bottom bar */}
|
|
<motion.div
|
|
className="grid grid-cols-1 lg:grid-cols-12 gap-8 border-t border-[#C8C2B8] pt-10"
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: 0.6, duration: 0.8 }}
|
|
>
|
|
<div className="lg:col-span-5">
|
|
<p className="text-[#6A6460] text-base leading-relaxed max-w-md">
|
|
{dict.hero.desc}
|
|
</p>
|
|
</div>
|
|
<div className="lg:col-span-7 flex items-end justify-end gap-4 flex-wrap">
|
|
<MagneticBtn href={`/${lang}/contact`} className="btn-brutal btn-brutal-yellow inline-flex items-center gap-3 px-8 py-4 font-display font-black text-[13px] tracking-widest uppercase cursor-pointer">
|
|
{dict.hero.ctaQuote} <IconArrow />
|
|
</MagneticBtn>
|
|
<MagneticBtn href={`/${lang}/work`} className="btn-brutal inline-flex items-center gap-3 px-8 py-4 font-display font-black text-[13px] tracking-widest uppercase cursor-pointer">
|
|
{dict.hero.ctaWork}
|
|
</MagneticBtn>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</section>
|
|
|
|
<HomeClientBelowFold lang={lang} dict={dict} />
|
|
</div>
|
|
);
|
|
}
|