anatolian2/components/home/InteractiveMap.tsx
Temmuz Aslan 591d878ac6 Initial commit: The Anatolian Edit website
Next.js 14 website with standalone output configured for Docker deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 22:34:25 +03:00

298 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { motion, useScroll, useTransform, useInView } from "framer-motion";
import { useRef, useState } from "react";
import Image from "next/image";
import { SectionHeader } from "@/components/shared/SectionHeader";
/*
* Coordinates are percentages (0100) relative to the map image (2720×1568).
* Each point was placed by identifying the geographic feature on the
* istanbul_map.png silhouette:
*
* Karaköy — European shore just south of the Golden Horn mouth
* Moda İskelesi — tip of the Kadıköy/Moda peninsula (Asian side)
* Moda (coffee) — slightly inland on the Moda neighbourhood streets
* Fenerbahçe — the prominent cape/park jutting south into the Marmara
* Caddebostan — along the Asian Marmara coast, east of Fenerbahçe
* Bağdat Caddesi— runs parallel to the coast, slightly inland
* Suadiye — further east along the Marmara coast
*/
const waypoints = [
{
id: "karakoy",
name: "Karaköy Ferry Terminal",
description:
"Your journey begins here. Board the ferry with Turkish tea in hand.",
icon: "🚢",
x: 27,
y: 34,
side: "europe",
},
{
id: "moda",
name: "Moda İskelesi",
description:
"Step onto a 1917 pier that now houses a library and café over the sea.",
icon: "📚",
x: 42,
y: 56,
side: "asia",
},
{
id: "coffee",
name: "Moda Coffee District",
description:
"Third-wave coffee tasting in Istanbul's coolest neighbourhood.",
icon: "☕",
x: 48,
y: 53,
side: "asia",
},
{
id: "fenerbahce",
name: "Fenerbahçe Park",
description:
"Mediterranean-style marina with yachts, pine trees, and sea air.",
icon: "🌳",
x: 44,
y: 67,
side: "asia",
},
{
id: "caddebostan",
name: "Caddebostan Seafront",
description:
"The promenade where Istanbulites gather at sunset. Street food heaven.",
icon: "🌅",
x: 53,
y: 78,
side: "asia",
},
{
id: "bagdat",
name: "Bağdat Caddesi",
description:
"Istanbul's Champs-Élysées. 6km of designer boutiques and patisseries.",
icon: "🛍️",
x: 64,
y: 60,
side: "asia",
},
{
id: "suadiye",
name: "Suadiye",
description:
"Sunset dinner with the European skyline painted in gold. The grand finale.",
icon: "🍷",
x: 63,
y: 86,
side: "asia",
},
];
/* ------------------------------------------------------------------ */
/* SVG path that connects the waypoints via a smooth Bézier curve. */
/* The path crosses the Bosphorus water from Karaköy to Moda, then */
/* follows the Asian coastline south-east to Suadiye. */
/* ------------------------------------------------------------------ */
const ROUTE_PATH = [
"M 27 34", // Karaköy
"C 32 42, 38 52, 42 56", // ferry crossing → Moda İskelesi
"C 44 55, 46 54, 48 53", // walk to Moda coffee district
"C 47 57, 45 63, 44 67", // south to Fenerbahçe
"C 46 72, 50 76, 53 78", // east to Caddebostan
"C 57 76, 62 66, 64 60", // inland to Bağdat Caddesi
"C 64 68, 63 78, 63 86", // back to coast at Suadiye
].join(" ");
function WaypointPin({
waypoint,
index,
isActive,
onClick,
}: {
waypoint: (typeof waypoints)[0];
index: number;
isActive: boolean;
onClick: () => void;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
return (
<motion.button
ref={ref}
className="absolute z-10 -translate-x-1/2 -translate-y-1/2"
style={{ left: `${waypoint.x}%`, top: `${waypoint.y}%` }}
initial={{ opacity: 0, scale: 0 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{
delay: 0.4 + index * 0.18,
type: "spring" as const,
stiffness: 300,
}}
onClick={onClick}
aria-label={waypoint.name}
>
{/* Pulse ring behind pin */}
<span
className={`absolute inset-0 rounded-full ${
isActive ? "bg-coral-spritz/30 animate-ping" : ""
}`}
/>
{/* Pin circle */}
<motion.div
className={`relative w-9 h-9 md:w-11 md:h-11 rounded-full flex items-center justify-center text-sm md:text-lg shadow-lg cursor-pointer border-2 transition-colors ${
isActive
? "bg-coral-spritz text-white border-white scale-110"
: "bg-white text-deep-nazar border-white/80 hover:bg-bosphorus hover:text-white hover:border-bosphorus"
}`}
whileHover={{ scale: 1.2 }}
whileTap={{ scale: 0.9 }}
>
{waypoint.icon}
</motion.div>
{/* Step number label */}
<span className="absolute -top-1 -right-1 w-4 h-4 md:w-5 md:h-5 rounded-full bg-deep-nazar text-white text-[9px] md:text-[10px] font-bold flex items-center justify-center border border-white">
{index + 1}
</span>
</motion.button>
);
}
export function InteractiveMap() {
const sectionRef = useRef(null);
const [activeWaypoint, setActiveWaypoint] = useState<string | null>(null);
const { scrollYProgress } = useScroll({
target: sectionRef,
offset: ["start end", "end start"],
});
const pathLength = useTransform(scrollYProgress, [0.15, 0.55], [0, 1]);
return (
<section
id="the-route"
className="py-20 md:py-32 section-padding bg-warm-sand"
ref={sectionRef}
>
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="The Route"
title="Follow the Coastline"
subtitle="From a European pier to an Asian sunset. Here's every stop on our signature experience."
/>
{/* ---- Map Container ---- */}
<div
className="relative rounded-3xl overflow-hidden shadow-lg border border-sun-yolk/20"
onClick={() => setActiveWaypoint(null)}
>
{/* Map background image — the container inherits the image's
intrinsic aspect ratio so pins stay aligned at every size. */}
<Image
src="/images/istanbul_map.png"
alt="Illustrated map of Istanbul showing the Bosphorus strait, European side, and Asian side coastline"
width={2720}
height={1568}
className="block w-full h-auto"
priority
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 90vw, 1280px"
/>
{/* Animated route path overlay */}
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
viewBox="0 0 100 100"
preserveAspectRatio="none"
fill="none"
>
{/* Shadow/glow underneath */}
<motion.path
d={ROUTE_PATH}
stroke="rgba(30,58,138,0.15)"
strokeWidth="0.8"
strokeLinecap="round"
strokeLinejoin="round"
style={{ pathLength }}
fill="none"
/>
{/* Main dashed route line */}
<motion.path
d={ROUTE_PATH}
stroke="#1E3A8A"
strokeWidth="0.35"
strokeDasharray="0.8 0.5"
strokeLinecap="round"
strokeLinejoin="round"
style={{ pathLength }}
fill="none"
/>
</svg>
{/* Waypoint pins */}
{waypoints.map((wp, i) => (
<WaypointPin
key={wp.id}
waypoint={wp}
index={i}
isActive={activeWaypoint === wp.id}
onClick={() => {
setActiveWaypoint(activeWaypoint === wp.id ? null : wp.id);
}}
/>
))}
{/* Side labels */}
<div className="absolute left-[12%] top-[10%] md:top-[12%] text-deep-nazar/25 font-display font-bold text-[10px] md:text-base uppercase tracking-[0.25em] select-none pointer-events-none">
Europe
</div>
<div className="absolute right-[10%] top-[10%] md:top-[12%] text-deep-nazar/25 font-display font-bold text-[10px] md:text-base uppercase tracking-[0.25em] select-none pointer-events-none">
Asia
</div>
</div>
{/* Mobile: scrollable stop list below the map */}
<div className="mt-6 md:mt-8">
<div className="flex gap-3 overflow-x-auto pb-4 snap-x snap-mandatory scrollbar-hide md:grid md:grid-cols-4 lg:grid-cols-7 md:overflow-visible md:pb-0">
{waypoints.map((wp, i) => (
<motion.button
key={wp.id}
className={`flex-shrink-0 snap-start w-44 md:w-auto bg-white rounded-2xl p-4 shadow-sm text-left transition-all ${
activeWaypoint === wp.id
? "ring-2 ring-bosphorus shadow-md"
: "hover:shadow-md"
}`}
initial={{ opacity: 0, y: 10 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.08 }}
onClick={() =>
setActiveWaypoint(activeWaypoint === wp.id ? null : wp.id)
}
>
<div className="flex items-center gap-2 mb-2">
<span className="w-6 h-6 rounded-full bg-deep-nazar text-white text-[10px] font-bold flex items-center justify-center">
{i + 1}
</span>
<span className="text-lg">{wp.icon}</span>
</div>
<p className="font-display font-bold text-sm text-deep-nazar leading-tight">
{wp.name}
</p>
<p className="text-[11px] text-deep-nazar/50 mt-1 leading-relaxed line-clamp-2">
{wp.description}
</p>
</motion.button>
))}
</div>
</div>
</div>
</section>
);
}