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>
This commit is contained in:
Temmuz Aslan 2026-02-15 22:34:25 +03:00
commit 591d878ac6
59 changed files with 10167 additions and 0 deletions

7
.dockerignore Normal file
View file

@ -0,0 +1,7 @@
node_modules
.next
.vercel
.claude
.git
imagesforyou
server.md

3
.eslintrc.json Normal file
View file

@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

41
.gitignore vendored Normal file
View file

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# project-specific
imagesforyou/
.claude/
server.md

36
Dockerfile Normal file
View file

@ -0,0 +1,36 @@
FROM node:18-alpine AS base
# Stage 1: Install dependencies
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# Stage 2: Build the application
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production runner
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

36
README.md Normal file
View file

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

128
app/about/page.tsx Normal file
View file

@ -0,0 +1,128 @@
import { Metadata } from "next";
import Image from "next/image";
import { generatePageMetadata } from "@/lib/metadata";
import { generateBreadcrumbSchema } from "@/lib/structured-data";
import { Button } from "@/components/shared/Button";
import { FinalCTA } from "@/components/home/FinalCTA";
export const metadata: Metadata = generatePageMetadata({
title: "About Us — The Anatolian Edit",
description:
"We're not guides. We're editors. We edit out the noise and show you only the good parts of Istanbul's Asian side.",
path: "/about",
});
export default function AboutPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "About", url: "https://theanatolianedit.com/about" },
])),
}}
/>
<section className="pt-28 md:pt-36 pb-16 section-padding">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div>
<p className="text-bosphorus font-semibold text-sm tracking-widest uppercase mb-3">
Our Story
</p>
<h1 className="font-display text-4xl md:text-5xl font-bold text-deep-nazar leading-tight mb-6">
We Started Because <span className="text-bosphorus italic">Nobody Else Would</span>
</h1>
<div className="space-y-4 text-deep-nazar/70 leading-relaxed">
<p>
Every year, 17 million tourists visit Istanbul. They see the Hagia Sophia.
They cruise the Bosphorus. They haggle at the Grand Bazaar. And then they leave.
</p>
<p>
Almost none of them cross the water.
</p>
<p>
We grew up on the Asian side. We ate breakfast in Kadıköy, studied in Moda,
had our first dates on Bağdat Caddesi, and watched a thousand sunsets from
Caddebostan. We knew, with absolute certainty, that we were living in the
best part of one of the world&apos;s greatest cities.
</p>
<p>
But nobody was telling that story.
</p>
<p>
So we started The Anatolian Edit. Not a tour company an editorial service
for the city we love. We edit out the noise, the crowds, the clichés, and
the tourist traps. We show you only the good parts.
</p>
<p className="font-display font-semibold text-deep-nazar text-lg italic">
We&apos;re not guides. We&apos;re editors. And the Asian side is our best work.
</p>
</div>
<div className="mt-8">
<Button href="/experiences" variant="primary" size="lg">
See Our Experiences
</Button>
</div>
</div>
<div className="relative">
<div className="relative aspect-[4/5] rounded-3xl overflow-hidden">
<Image
src="/images/manifesto_coastline.jpg"
alt="The Anatolian Edit team on the waterfront of Istanbul's Asian side"
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
<div className="absolute -bottom-4 -right-4 bg-sun-yolk text-deep-nazar rounded-2xl px-5 py-3 font-display font-bold text-sm shadow-lg rotate-[3deg]">
Based in Kadıköy since day one
</div>
</div>
</div>
</div>
</section>
{/* Philosophy */}
<section className="py-16 md:py-24 section-padding bg-warm-sand">
<div className="max-w-4xl mx-auto text-center">
<h2 className="font-display text-3xl md:text-4xl font-bold text-deep-nazar mb-12">
How We Think About This
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
title: "Small by Design",
text: "Max 8 guests per experience. If you wanted a crowd, you'd stay on the European side.",
},
{
title: "Local, Always",
text: "Every restaurant, café, and stop on our routes is a place we go ourselves. No kickbacks. No tourist menus.",
},
{
title: "Editors, Not Guides",
text: "We don't read from scripts. We share stories, adapt to the group, and make sure every moment earns its place.",
},
].map((item, i) => (
<div key={i} className="bg-white rounded-3xl p-8 shadow-sm">
<h3 className="font-display text-xl font-bold text-deep-nazar mb-3">
{item.title}
</h3>
<p className="text-deep-nazar/60 text-sm leading-relaxed">
{item.text}
</p>
</div>
))}
</div>
</div>
</section>
<FinalCTA />
</>
);
}

129
app/blog/[slug]/page.tsx Normal file
View file

@ -0,0 +1,129 @@
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { blogPosts } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateBreadcrumbSchema } from "@/lib/structured-data";
import { Button } from "@/components/shared/Button";
interface BlogPostPageProps {
params: { slug: string };
}
export async function generateStaticParams() {
return blogPosts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
const post = blogPosts.find((p) => p.slug === params.slug);
if (!post) return {};
return generatePageMetadata({
title: post.title,
description: post.excerpt,
path: `/blog/${post.slug}`,
});
}
export default function BlogPostPage({ params }: BlogPostPageProps) {
const post = blogPosts.find((p) => p.slug === params.slug);
if (!post) notFound();
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "The Edit", url: "https://theanatolianedit.com/blog" },
{ name: post.title, url: `https://theanatolianedit.com/blog/${post.slug}` },
])),
}}
/>
<article className="pt-28 md:pt-36 pb-16">
<div className="max-w-3xl mx-auto px-5 sm:px-8">
<div className="mb-8">
<Link
href="/blog"
className="text-bosphorus text-sm font-semibold hover:underline"
>
&larr; Back to The Edit
</Link>
</div>
<span className="inline-block bg-bosphorus/10 text-bosphorus text-xs font-semibold px-3 py-1.5 rounded-full mb-4">
{post.category}
</span>
<h1 className="font-display text-3xl md:text-4xl lg:text-5xl font-bold text-deep-nazar leading-tight mb-4">
{post.title}
</h1>
<div className="flex items-center gap-4 text-sm text-deep-nazar/50 mb-8">
<span>
{new Date(post.date).toLocaleDateString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})}
</span>
<span>&middot;</span>
<span>{post.readTime}</span>
</div>
<div className="relative aspect-[16/9] rounded-3xl overflow-hidden mb-12">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 800px"
priority
/>
</div>
{/* Placeholder article content */}
<div className="prose prose-lg max-w-none text-deep-nazar/80">
<p className="text-xl leading-relaxed">
{post.excerpt}
</p>
<p>
This is a placeholder article. In production, each blog post would contain
2,0003,000 words of original, SEO-optimized content written in The Anatolian
Edit&apos;s signature voice warm, specific, and sensory.
</p>
<h2 className="font-display text-2xl font-bold text-deep-nazar">
Why This Matters for Your Trip
</h2>
<p>
The Asian side of Istanbul isn&apos;t an alternative to the European side it&apos;s
the complement. While the historic peninsula gives you the monuments and the
museums, the Asian side gives you the life. The breakfasts, the sunsets, the
boulevards, the neighbourhoods where Istanbul actually lives.
</p>
<p>
Want to experience it yourself?{" "}
<Link href="/experiences/the-other-side" className="text-bosphorus font-semibold hover:underline">
Our signature experience
</Link>{" "}
covers the best of the Asian coastline in a single afternoon.
</p>
</div>
<div className="mt-12 pt-8 border-t border-deep-nazar/10 text-center">
<p className="text-deep-nazar/60 mb-4">Ready to see the Asian side for yourself?</p>
<Button href="/experiences" variant="coral" size="lg">
Explore Our Experiences
</Button>
</div>
</div>
</article>
</>
);
}

79
app/blog/page.tsx Normal file
View file

@ -0,0 +1,79 @@
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { blogPosts } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateBreadcrumbSchema } from "@/lib/structured-data";
import { SectionHeader } from "@/components/shared/SectionHeader";
export const metadata: Metadata = generatePageMetadata({
title: "The Edit — Stories from Istanbul's Other Side",
description:
"Neighbourhood guides, food stories, cultural deep-dives, and insider tips for Istanbul's Asian side. Written by locals who live it.",
path: "/blog",
});
export default function BlogPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "The Edit", url: "https://theanatolianedit.com/blog" },
])),
}}
/>
<section className="pt-28 md:pt-36 pb-16 section-padding">
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="The Edit"
title="Stories from Istanbul's Other Side"
subtitle="Neighbourhood guides, food stories, and the local knowledge that doesn't make the guidebooks."
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{blogPosts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="group"
>
<article className="bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-shadow h-full flex flex-col">
<div className="relative aspect-[16/10] overflow-hidden">
<Image
src={post.image}
alt={post.title}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
<div className="absolute top-4 left-4">
<span className="bg-bosphorus text-white text-xs font-semibold px-3 py-1.5 rounded-full">
{post.category}
</span>
</div>
</div>
<div className="p-6 flex flex-col flex-1">
<h2 className="font-display text-xl font-bold text-deep-nazar mb-3 group-hover:text-bosphorus transition-colors">
{post.title}
</h2>
<p className="text-deep-nazar/60 text-sm leading-relaxed mb-4 flex-1">
{post.excerpt}
</p>
<div className="flex items-center justify-between text-xs text-deep-nazar/40">
<span>{new Date(post.date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span>
<span>{post.readTime}</span>
</div>
</div>
</article>
</Link>
))}
</div>
</div>
</section>
</>
);
}

110
app/contact/page.tsx Normal file
View file

@ -0,0 +1,110 @@
import { Metadata } from "next";
import { generatePageMetadata } from "@/lib/metadata";
import { generateBreadcrumbSchema } from "@/lib/structured-data";
import { EMAIL, WHATSAPP_NUMBER, WHATSAPP_MESSAGE, INSTAGRAM } from "@/lib/constants";
import { Button } from "@/components/shared/Button";
export const metadata: Metadata = generatePageMetadata({
title: "Contact Us — The Anatolian Edit",
description:
"Get in touch about booking a private experience, custom group tours, or any questions about Istanbul's Asian side.",
path: "/contact",
});
export default function ContactPage() {
const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER.replace("+", "")}?text=${encodeURIComponent(WHATSAPP_MESSAGE)}`;
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "Contact", url: "https://theanatolianedit.com/contact" },
])),
}}
/>
<section className="pt-28 md:pt-36 pb-16 section-padding">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<p className="text-bosphorus font-semibold text-sm tracking-widest uppercase mb-3">
Get in Touch
</p>
<h1 className="font-display text-4xl md:text-5xl font-bold text-deep-nazar leading-tight mb-4">
Let&apos;s Plan Your <span className="text-bosphorus italic">Edit</span>
</h1>
<p className="text-deep-nazar/70 text-lg max-w-xl mx-auto">
Questions about our experiences, private bookings, or just want to say hello?
We&apos;d love to hear from you.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-12">
<a
href={whatsappUrl}
target="_blank"
rel="noopener noreferrer"
className="bg-white rounded-3xl p-8 shadow-sm hover:shadow-md transition-shadow text-center group"
>
<div className="w-14 h-14 rounded-full bg-[#25D366]/10 flex items-center justify-center mx-auto mb-4 group-hover:bg-[#25D366]/20 transition-colors">
<svg className="w-7 h-7 text-[#25D366]" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z" />
</svg>
</div>
<h3 className="font-display font-bold text-deep-nazar mb-1">WhatsApp</h3>
<p className="text-deep-nazar/50 text-sm">Fastest way to reach us</p>
</a>
<a
href={`mailto:${EMAIL}`}
className="bg-white rounded-3xl p-8 shadow-sm hover:shadow-md transition-shadow text-center group"
>
<div className="w-14 h-14 rounded-full bg-bosphorus/10 flex items-center justify-center mx-auto mb-4 group-hover:bg-bosphorus/20 transition-colors">
<svg className="w-7 h-7 text-bosphorus" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="font-display font-bold text-deep-nazar mb-1">Email</h3>
<p className="text-deep-nazar/50 text-sm">{EMAIL}</p>
</a>
<a
href={`https://instagram.com/${INSTAGRAM}`}
target="_blank"
rel="noopener noreferrer"
className="bg-white rounded-3xl p-8 shadow-sm hover:shadow-md transition-shadow text-center group"
>
<div className="w-14 h-14 rounded-full bg-coral-spritz/10 flex items-center justify-center mx-auto mb-4 group-hover:bg-coral-spritz/20 transition-colors">
<svg className="w-7 h-7 text-coral-spritz" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z" />
</svg>
</div>
<h3 className="font-display font-bold text-deep-nazar mb-1">Instagram</h3>
<p className="text-deep-nazar/50 text-sm">@{INSTAGRAM}</p>
</a>
</div>
{/* Private Experiences inquiry */}
<div className="bg-warm-sand rounded-3xl p-8 md:p-12 text-center">
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-4">
Interested in a Private Experience?
</h2>
<p className="text-deep-nazar/70 text-lg mb-6 max-w-xl mx-auto">
Every experience is available as a private edition for couples, families,
or groups of up to 12. Tell us what you&apos;re looking for and we&apos;ll craft
the perfect day.
</p>
<Button href={whatsappUrl} external variant="coral" size="lg">
Chat With Us on WhatsApp
</Button>
<p className="text-deep-nazar/40 text-sm mt-3">
Starting from &euro;250 for private groups
</p>
</div>
</div>
</section>
</>
);
}

View file

@ -0,0 +1,76 @@
import { Metadata } from "next";
import { experiences } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateTouristTripSchema, generateBreadcrumbSchema } from "@/lib/structured-data";
import { ExperienceHero } from "@/components/experiences/ExperienceHero";
import { Itinerary } from "@/components/experiences/Itinerary";
import { BookingWidget } from "@/components/experiences/BookingWidget";
import { Testimonials } from "@/components/home/Testimonials";
import { FinalCTA } from "@/components/home/FinalCTA";
const experience = experiences.find((e) => e.slug === "after-dark")!;
export const metadata: Metadata = generatePageMetadata({
title: "After Dark — The Kadıköy Meyhane Evening",
description: "Rakı, meze, live music, and the most honest night out in Istanbul. A guided evening through Kadıköy's legendary bar and meyhane scene.",
path: "/experiences/after-dark",
});
export default function AfterDarkPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateTouristTripSchema(experience)) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "Experiences", url: "https://theanatolianedit.com/experiences" },
{ name: "After Dark", url: "https://theanatolianedit.com/experiences/after-dark" },
])),
}}
/>
<ExperienceHero experience={experience} />
<section className="py-12 section-padding">
<div className="max-w-4xl mx-auto">
<div className="flex flex-wrap justify-center gap-4">
{experience.highlights.map((h, i) => (
<div key={i} className="inline-flex items-center gap-2 bg-warm-sand rounded-full px-5 py-3 text-sm text-deep-nazar font-medium">
<span className="text-lg">{h.icon}</span>
{h.text}
</div>
))}
</div>
</div>
</section>
<section className="py-12 section-padding bg-warm-sand">
<div className="max-w-3xl mx-auto">
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-8 text-center">What&apos;s Included</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{experience.includes.map((item, i) => (
<div key={i} className="flex items-start gap-3 bg-white rounded-2xl p-4">
<span className="text-bosphorus mt-0.5">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</span>
<span className="text-deep-nazar/80 text-sm">{item}</span>
</div>
))}
</div>
</div>
</section>
{experience.itinerary && <Itinerary stops={experience.itinerary} />}
<BookingWidget experience={experience} />
<Testimonials />
<FinalCTA />
</>
);
}

View file

@ -0,0 +1,76 @@
import { Metadata } from "next";
import { experiences } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateTouristTripSchema, generateBreadcrumbSchema } from "@/lib/structured-data";
import { ExperienceHero } from "@/components/experiences/ExperienceHero";
import { Itinerary } from "@/components/experiences/Itinerary";
import { BookingWidget } from "@/components/experiences/BookingWidget";
import { Testimonials } from "@/components/home/Testimonials";
import { FinalCTA } from "@/components/home/FinalCTA";
const experience = experiences.find((e) => e.slug === "first-light")!;
export const metadata: Metadata = generatePageMetadata({
title: "First Light — The Asian Side Breakfast Experience",
description: "Istanbul's breakfast culture is legendary. The best of it happens on the Asian side. Serpme kahvaltı, Kadıköy market, Turkish coffee ritual.",
path: "/experiences/first-light",
});
export default function FirstLightPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateTouristTripSchema(experience)) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "Experiences", url: "https://theanatolianedit.com/experiences" },
{ name: "First Light", url: "https://theanatolianedit.com/experiences/first-light" },
])),
}}
/>
<ExperienceHero experience={experience} />
<section className="py-12 section-padding">
<div className="max-w-4xl mx-auto">
<div className="flex flex-wrap justify-center gap-4">
{experience.highlights.map((h, i) => (
<div key={i} className="inline-flex items-center gap-2 bg-warm-sand rounded-full px-5 py-3 text-sm text-deep-nazar font-medium">
<span className="text-lg">{h.icon}</span>
{h.text}
</div>
))}
</div>
</div>
</section>
<section className="py-12 section-padding bg-warm-sand">
<div className="max-w-3xl mx-auto">
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-8 text-center">What&apos;s Included</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{experience.includes.map((item, i) => (
<div key={i} className="flex items-start gap-3 bg-white rounded-2xl p-4">
<span className="text-bosphorus mt-0.5">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</span>
<span className="text-deep-nazar/80 text-sm">{item}</span>
</div>
))}
</div>
</div>
</section>
{experience.itinerary && <Itinerary stops={experience.itinerary} />}
<BookingWidget experience={experience} />
<Testimonials />
<FinalCTA />
</>
);
}

65
app/experiences/page.tsx Normal file
View file

@ -0,0 +1,65 @@
import { Metadata } from "next";
import { experiences } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateBreadcrumbSchema } from "@/lib/structured-data";
import { ExperienceCard } from "@/components/experiences/ExperienceCard";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { Button } from "@/components/shared/Button";
import { FinalCTA } from "@/components/home/FinalCTA";
export const metadata: Metadata = generatePageMetadata({
title: "Experiences — Curated Tours on Istanbul's Asian Side",
description:
"Choose your edit: half-day coastal tours, legendary Turkish breakfasts, meyhane evenings, or the full all-day immersion. Small groups, max 8 people.",
path: "/experiences",
});
export default function ExperiencesPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "Experiences", url: "https://theanatolianedit.com/experiences" },
])),
}}
/>
<section className="pt-28 md:pt-36 pb-16 section-padding">
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="Our Experiences"
title="Choose Your Edit"
subtitle="Every experience is handcrafted, small-group, and designed around the places we'd take our own friends."
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{experiences.map((exp, i) => (
<ExperienceCard key={exp.slug} experience={exp} index={i} />
))}
</div>
<div className="mt-16 bg-gradient-to-r from-deep-nazar to-deep-nazar/90 rounded-3xl p-8 md:p-12 text-white text-center">
<h3 className="font-display text-2xl md:text-3xl font-bold mb-3">
Private Editions
</h3>
<p className="text-white/80 text-lg mb-6 max-w-2xl mx-auto">
Want The Anatolian Edit all to yourself? Every experience is
available as a private edition for couples, families, or groups of up to 12.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button href="/contact" variant="coral" size="lg">
Inquire About Private Experiences
</Button>
<span className="text-white/60 text-sm">Starting from &euro;250</span>
</div>
</div>
</div>
</section>
<FinalCTA />
</>
);
}

View file

@ -0,0 +1,125 @@
import { Metadata } from "next";
import { experiences } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateTouristTripSchema, generateBreadcrumbSchema } from "@/lib/structured-data";
import { ExperienceHero } from "@/components/experiences/ExperienceHero";
import { Itinerary } from "@/components/experiences/Itinerary";
import { BookingWidget } from "@/components/experiences/BookingWidget";
import { PhotoGallery } from "@/components/experiences/PhotoGallery";
import { Testimonials } from "@/components/home/Testimonials";
import { FinalCTA } from "@/components/home/FinalCTA";
const experience = experiences.find((e) => e.slug === "the-other-side")!;
export const metadata: Metadata = generatePageMetadata({
title: "The Other Side — Signature Asian Side Experience",
description: experience.hook,
path: "/experiences/the-other-side",
});
export default function TheOtherSidePage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateTouristTripSchema(experience)),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "Experiences", url: "https://theanatolianedit.com/experiences" },
{ name: "The Other Side", url: "https://theanatolianedit.com/experiences/the-other-side" },
])
),
}}
/>
<ExperienceHero experience={experience} />
{/* Highlights */}
<section className="py-12 section-padding">
<div className="max-w-4xl mx-auto">
<div className="flex flex-wrap justify-center gap-4">
{experience.highlights.map((h, i) => (
<div
key={i}
className="inline-flex items-center gap-2 bg-warm-sand rounded-full px-5 py-3 text-sm text-deep-nazar font-medium"
>
<span className="text-lg">{h.icon}</span>
{h.text}
</div>
))}
</div>
</div>
</section>
{/* What's Included */}
<section className="py-12 section-padding bg-warm-sand">
<div className="max-w-3xl mx-auto">
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-8 text-center">
What&apos;s Included
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{experience.includes.map((item, i) => (
<div key={i} className="flex items-start gap-3 bg-white rounded-2xl p-4">
<span className="text-bosphorus mt-0.5">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</span>
<span className="text-deep-nazar/80 text-sm">{item}</span>
</div>
))}
</div>
</div>
</section>
{experience.itinerary && <Itinerary stops={experience.itinerary} />}
<PhotoGallery />
<BookingWidget experience={experience} />
<Testimonials />
{/* You Might Also Like */}
<section className="py-16 section-padding bg-warm-sand">
<div className="max-w-7xl mx-auto text-center">
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-8">
You Might Also Like
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{experiences
.filter((e) => e.slug !== "the-other-side")
.slice(0, 3)
.map((exp) => (
<a
key={exp.slug}
href={`/experiences/${exp.slug}`}
className="bg-white rounded-2xl p-6 shadow-sm hover:shadow-md transition-shadow text-left"
>
<p className="text-bosphorus font-semibold text-xs tracking-widest uppercase mb-1">
{exp.tagline}
</p>
<h3 className="font-display text-lg font-bold text-deep-nazar mb-2">
{exp.name}
</h3>
<p className="text-deep-nazar/60 text-sm mb-3">{exp.hook}</p>
<span className="text-coral-spritz font-semibold text-sm">
From &euro;{exp.price} &rarr;
</span>
</a>
))}
</div>
</div>
</section>
<FinalCTA />
</>
);
}

38
app/faq/page.tsx Normal file
View file

@ -0,0 +1,38 @@
import { Metadata } from "next";
import { faqs } from "@/lib/constants";
import { generatePageMetadata } from "@/lib/metadata";
import { generateFAQSchema, generateBreadcrumbSchema } from "@/lib/structured-data";
import { HomeFAQ } from "@/components/home/HomeFAQ";
import { FinalCTA } from "@/components/home/FinalCTA";
export const metadata: Metadata = generatePageMetadata({
title: "FAQ — Istanbul Asian Side Tours",
description:
"Answers to the most common questions about visiting Istanbul's Asian side, Kadıköy, Bağdat Caddesi, and our curated experiences.",
path: "/faq",
});
export default function FAQPage() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(generateFAQSchema(faqs)) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateBreadcrumbSchema([
{ name: "Home", url: "https://theanatolianedit.com" },
{ name: "FAQ", url: "https://theanatolianedit.com/faq" },
])),
}}
/>
<div className="pt-20 md:pt-28">
<HomeFAQ />
</div>
<FinalCTA />
</>
);
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

39
app/globals.css Normal file
View file

@ -0,0 +1,39 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@apply bg-aegean-white text-deep-nazar;
}
::selection {
@apply bg-bosphorus/20 text-deep-nazar;
}
}
@layer components {
.glass-card {
@apply bg-white/70 backdrop-blur-md border border-bosphorus/10 rounded-3xl;
}
.section-padding {
@apply px-5 sm:px-8 md:px-12 lg:px-20 xl:px-32;
}
.container-wide {
@apply max-w-7xl mx-auto w-full;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}

78
app/layout.tsx Normal file
View file

@ -0,0 +1,78 @@
import type { Metadata } from "next";
import { Syne, Plus_Jakarta_Sans } from "next/font/google";
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
import { WhatsAppButton } from "@/components/shared/WhatsAppButton";
import { CookieConsent } from "@/components/shared/CookieConsent";
import { generateOrganizationSchema, generateLocalBusinessSchema } from "@/lib/structured-data";
const syne = Syne({
subsets: ["latin"],
variable: "--font-syne",
display: "swap",
});
const jakarta = Plus_Jakarta_Sans({
subsets: ["latin"],
variable: "--font-jakarta",
display: "swap",
});
export const metadata: Metadata = {
title: "The Anatolian Edit | Istanbul's Asian Side, Curated for You",
description:
"Curated experiences on Istanbul's Asian side. Small-group tours through Kadıköy, Moda, Bağdat Caddesi and the coastline that 90% of visitors never see.",
metadataBase: new URL("https://theanatolianedit.com"),
openGraph: {
title: "The Anatolian Edit | Istanbul's Asian Side, Curated for You",
description:
"Curated experiences on Istanbul's Asian side. Small-group tours through Kadıköy, Moda, Bağdat Caddesi and the coastline that 90% of visitors never see.",
url: "https://theanatolianedit.com",
siteName: "The Anatolian Edit",
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: "The Anatolian Edit",
description:
"Curated experiences on Istanbul's Asian side.",
},
robots: {
index: true,
follow: true,
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className={`${syne.variable} ${jakarta.variable}`}>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateOrganizationSchema()),
}}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateLocalBusinessSchema()),
}}
/>
</head>
<body className="font-body text-deep-nazar bg-aegean-white antialiased">
<Header />
<main>{children}</main>
<Footer />
<WhatsAppButton />
<CookieConsent />
</body>
</html>
);
}

31
app/not-found.tsx Normal file
View file

@ -0,0 +1,31 @@
import { Button } from "@/components/shared/Button";
export default function NotFound() {
return (
<section className="min-h-screen flex items-center justify-center section-padding">
<div className="text-center max-w-lg">
<div className="w-24 h-24 rounded-full bg-bosphorus/10 flex items-center justify-center mx-auto mb-8">
<span className="text-4xl">🚢</span>
</div>
<h1 className="font-display text-4xl md:text-5xl font-bold text-deep-nazar mb-4">
Wrong Side of the Water
</h1>
<p className="text-deep-nazar/60 text-lg mb-8">
Looks like you&apos;ve drifted off course. This page doesn&apos;t exist
but Istanbul&apos;s Asian side definitely does.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button href="/" variant="primary" size="lg">
Back to Shore
</Button>
<Button href="/experiences" variant="outline" size="lg">
Explore Experiences
</Button>
</div>
</div>
</section>
);
}

27
app/page.tsx Normal file
View file

@ -0,0 +1,27 @@
import { Hero } from "@/components/home/Hero";
import { Manifesto } from "@/components/home/Manifesto";
import { ExperienceGrid } from "@/components/home/ExperienceGrid";
import { InteractiveMap } from "@/components/home/InteractiveMap";
import { WhyAsianSide } from "@/components/home/WhyAsianSide";
import { HowItWorks } from "@/components/home/HowItWorks";
import { Testimonials } from "@/components/home/Testimonials";
import { HomeFAQ } from "@/components/home/HomeFAQ";
import { FinalCTA } from "@/components/home/FinalCTA";
import { StickyBookingBar } from "@/components/layout/StickyBookingBar";
export default function HomePage() {
return (
<>
<Hero />
<Manifesto />
<ExperienceGrid />
<InteractiveMap />
<WhyAsianSide />
<HowItWorks />
<Testimonials />
<HomeFAQ />
<FinalCTA />
<StickyBookingBar />
</>
);
}

12
app/robots.ts Normal file
View file

@ -0,0 +1,12 @@
import { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
disallow: ["/api/"],
},
sitemap: "https://theanatolianedit.com/sitemap.xml",
};
}

31
app/sitemap.ts Normal file
View file

@ -0,0 +1,31 @@
import { MetadataRoute } from "next";
import { experiences, blogPosts } from "@/lib/constants";
const SITE_URL = "https://theanatolianedit.com";
export default function sitemap(): MetadataRoute.Sitemap {
const staticPages = [
{ url: SITE_URL, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 1.0 },
{ url: `${SITE_URL}/experiences`, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 0.9 },
{ url: `${SITE_URL}/about`, lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.7 },
{ url: `${SITE_URL}/blog`, lastModified: new Date(), changeFrequency: "weekly" as const, priority: 0.8 },
{ url: `${SITE_URL}/faq`, lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.7 },
{ url: `${SITE_URL}/contact`, lastModified: new Date(), changeFrequency: "monthly" as const, priority: 0.6 },
];
const experiencePages = experiences.map((exp) => ({
url: `${SITE_URL}/experiences/${exp.slug}`,
lastModified: new Date(),
changeFrequency: "weekly" as const,
priority: 0.9,
}));
const blogPages = blogPosts.map((post) => ({
url: `${SITE_URL}/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: "monthly" as const,
priority: 0.7,
}));
return [...staticPages, ...experiencePages, ...blogPages];
}

View file

@ -0,0 +1,140 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import { Experience, WHATSAPP_NUMBER } from "@/lib/constants";
import { Button } from "@/components/shared/Button";
interface BookingWidgetProps {
experience: Experience;
}
export function BookingWidget({ experience }: BookingWidgetProps) {
const [guests, setGuests] = useState(2);
const [selectedDate, setSelectedDate] = useState("");
const total = experience.price * guests;
const dates = Array.from({ length: 7 }, (_, i) => {
const d = new Date();
d.setDate(d.getDate() + i + 2);
return d.toISOString().split("T")[0];
});
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
return d.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
});
};
const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER.replace("+", "")}?text=${encodeURIComponent(
`Hi! I'd like to book "${experience.name}" for ${guests} guest(s)${selectedDate ? ` on ${formatDate(selectedDate)}` : ""}. Can you help?`
)}`;
return (
<section id="booking" className="py-16 md:py-24 section-padding bg-warm-sand">
<div className="max-w-2xl mx-auto">
<motion.div
className="bg-white rounded-3xl p-8 md:p-10 shadow-lg"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h2 className="font-display text-2xl md:text-3xl font-bold text-deep-nazar mb-2 text-center">
Book {experience.name}
</h2>
<p className="text-deep-nazar/60 text-center mb-8">
Secure your spot free cancellation up to 48 hours before
</p>
<div className="space-y-6">
{/* Date selection */}
<div>
<label className="block text-sm font-semibold text-deep-nazar mb-3">
Select a Date
</label>
<div className="flex flex-wrap gap-2">
{dates.map((date) => (
<button
key={date}
onClick={() => setSelectedDate(date)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedDate === date
? "bg-bosphorus text-white"
: "bg-deep-nazar/5 text-deep-nazar/70 hover:bg-bosphorus/10"
}`}
>
{formatDate(date)}
</button>
))}
</div>
</div>
{/* Guest count */}
<div>
<label className="block text-sm font-semibold text-deep-nazar mb-3">
Number of Guests
</label>
<div className="flex items-center gap-4">
<button
onClick={() => setGuests(Math.max(1, guests - 1))}
className="w-10 h-10 rounded-full bg-deep-nazar/5 flex items-center justify-center text-deep-nazar hover:bg-deep-nazar/10 transition-colors"
>
-
</button>
<span className="text-2xl font-bold text-deep-nazar w-8 text-center">
{guests}
</span>
<button
onClick={() => setGuests(Math.min(8, guests + 1))}
className="w-10 h-10 rounded-full bg-deep-nazar/5 flex items-center justify-center text-deep-nazar hover:bg-deep-nazar/10 transition-colors"
>
+
</button>
<span className="text-sm text-deep-nazar/50">
Max 8 per group
</span>
</div>
</div>
{/* Price summary */}
<div className="bg-warm-sand rounded-2xl p-5">
<div className="flex justify-between items-center mb-2">
<span className="text-deep-nazar/70">
&euro;{experience.price} &times; {guests} guest{guests > 1 ? "s" : ""}
</span>
<span className="font-semibold text-deep-nazar">
&euro;{total}
</span>
</div>
<div className="flex justify-between items-center pt-3 border-t border-deep-nazar/10">
<span className="font-bold text-deep-nazar">Total</span>
<span className="text-2xl font-bold text-deep-nazar">
&euro;{total}
</span>
</div>
</div>
{/* CTA */}
<div className="space-y-3">
<Button
href={whatsappUrl}
external
variant="coral"
size="lg"
className="w-full"
>
Reserve Now via WhatsApp
</Button>
<p className="text-center text-xs text-deep-nazar/40">
Instant confirmation &middot; Pay securely online &middot; Free cancellation up to 48h before
</p>
</div>
</div>
</motion.div>
</div>
</section>
);
}

View file

@ -0,0 +1,66 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { Experience } from "@/lib/constants";
interface ExperienceCardProps {
experience: Experience;
index?: number;
}
export function ExperienceCard({ experience, index = 0 }: ExperienceCardProps) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: index * 0.1, duration: 0.6 }}
>
<Link href={`/experiences/${experience.slug}`} className="group block h-full">
<div className="bg-white rounded-3xl overflow-hidden shadow-md hover:shadow-lg transition-all h-full flex flex-col">
<div className="relative aspect-[4/3] overflow-hidden">
<Image
src={experience.image}
alt={`${experience.name}${experience.hook}`}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
<div className="p-6 flex flex-col flex-1">
<p className="text-bosphorus font-semibold text-xs tracking-widest uppercase mb-1">
{experience.tagline}
</p>
<h3 className="font-display text-xl font-bold text-deep-nazar mb-2">
{experience.name}
</h3>
<p className="text-deep-nazar/70 text-sm leading-relaxed mb-4 flex-1">
{experience.hook}
</p>
<div className="flex items-center gap-4 mb-4 text-xs text-deep-nazar/60">
<span>{experience.duration}</span>
<span>{experience.groupSize}</span>
</div>
<div className="flex items-center justify-between pt-4 border-t border-deep-nazar/10">
<div>
<span className="text-xs text-deep-nazar/60">From </span>
<span className="text-xl font-bold text-deep-nazar">
&euro;{experience.price}
</span>
</div>
<span className="text-coral-spritz font-semibold text-sm group-hover:underline">
Book Now &rarr;
</span>
</div>
</div>
</div>
</Link>
</motion.div>
);
}

View file

@ -0,0 +1,73 @@
"use client";
import { motion } from "framer-motion";
import Image from "next/image";
import { Experience } from "@/lib/constants";
import { Button } from "@/components/shared/Button";
interface ExperienceHeroProps {
experience: Experience;
}
export function ExperienceHero({ experience }: ExperienceHeroProps) {
return (
<section className="relative pt-20 md:pt-24">
<div className="relative h-[50vh] md:h-[60vh] overflow-hidden">
<Image
src={experience.image}
alt={`${experience.name}${experience.tagline}`}
fill
className="object-cover"
priority
sizes="100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-deep-nazar/70 via-deep-nazar/20 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-12">
<div className="max-w-7xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<p className="text-bosphorus font-semibold text-sm tracking-widest uppercase mb-3">
{experience.tagline}
</p>
<h1 className="font-display text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-4">
{experience.name}
</h1>
<p className="text-white/80 text-lg md:text-xl max-w-2xl mb-6">
{experience.hook}
</p>
<div className="flex flex-wrap items-center gap-4 text-white/70 text-sm mb-6">
<span className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{experience.duration}
</span>
<span className="flex items-center gap-1.5">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{experience.groupSize}
</span>
<span className="text-sun-yolk font-semibold">&#9733; 4.9/5</span>
</div>
<div className="flex flex-wrap gap-4 items-center">
<Button href="#booking" variant="coral" size="lg">
Book Now From &euro;{experience.price}
</Button>
<span className="text-white/50 text-sm">
Free cancellation up to 48h before
</span>
</div>
</motion.div>
</div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,76 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import { ItineraryStop } from "@/lib/constants";
interface ItineraryProps {
stops: ItineraryStop[];
}
function ItineraryStopItem({
stop,
index,
isLast,
}: {
stop: ItineraryStop;
index: number;
isLast: boolean;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
return (
<motion.div
ref={ref}
className="relative flex gap-6 md:gap-8"
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
{/* Timeline */}
<div className="flex flex-col items-center">
<div className="w-12 h-12 rounded-full bg-bosphorus/10 border-2 border-bosphorus flex items-center justify-center text-bosphorus font-display font-bold text-xs flex-shrink-0">
{stop.time.replace(" PM", "").replace(" AM", "").replace("~", "")}
</div>
{!isLast && (
<div className="w-0.5 flex-1 bg-bosphorus/20 mt-2" />
)}
</div>
{/* Content */}
<div className="pb-10">
<span className="text-bosphorus font-semibold text-sm">{stop.time}</span>
<h3 className="font-display text-xl font-bold text-deep-nazar mt-1 mb-3">
{stop.title}
</h3>
<p className="text-deep-nazar/70 leading-relaxed">
{stop.description}
</p>
</div>
</motion.div>
);
}
export function Itinerary({ stops }: ItineraryProps) {
return (
<section className="py-16 md:py-24 section-padding">
<div className="max-w-3xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold text-deep-nazar mb-12 text-center">
Your Itinerary
</h2>
<div>
{stops.map((stop, i) => (
<ItineraryStopItem
key={i}
stop={stop}
index={i}
isLast={i === stops.length - 1}
/>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,119 @@
"use client";
import { useState } from "react";
import Image from "next/image";
import { motion, AnimatePresence } from "framer-motion";
const galleryImages = [
{
src: "/images/hero_main.jpg",
alt: "Friends walking along a sun-drenched boulevard on Istanbul's Asian side",
},
{
src: "/images/manifesto_coastline.jpg",
alt: "Locals relaxing on the waterfront with the Marmara Sea and Princes' Islands beyond",
},
{
src: "/images/first_light.jpg",
alt: "Traditional Turkish breakfast spread with eggs, cheese, simit, and tea",
},
{
src: "/images/the_other_side.jpg",
alt: "Aerial view of Fenerbahçe Park, Kalamış Marina, and the Asian side coastline",
},
];
export function PhotoGallery() {
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
return (
<section className="py-16 md:py-24 section-padding">
<div className="max-w-7xl mx-auto">
<h2 className="font-display text-3xl md:text-4xl font-bold text-deep-nazar mb-12 text-center">
Through the Lens
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{galleryImages.map((img, i) => (
<motion.button
key={i}
className={`relative overflow-hidden rounded-2xl ${
i === 0 ? "col-span-2 row-span-2 aspect-square" : "aspect-[4/3]"
}`}
whileHover={{ scale: 0.98 }}
onClick={() => setLightboxIndex(i)}
>
<Image
src={img.src}
alt={img.alt}
fill
className="object-cover hover:scale-110 transition-transform duration-700"
sizes={i === 0 ? "(max-width: 768px) 100vw, 66vw" : "(max-width: 768px) 50vw, 33vw"}
/>
</motion.button>
))}
</div>
</div>
{/* Lightbox */}
<AnimatePresence>
{lightboxIndex !== null && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
onClick={() => setLightboxIndex(null)}
>
<button
className="absolute top-4 right-4 text-white/70 hover:text-white text-3xl z-10"
onClick={() => setLightboxIndex(null)}
aria-label="Close lightbox"
>
&times;
</button>
<button
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white text-3xl z-10"
onClick={(e) => {
e.stopPropagation();
setLightboxIndex((lightboxIndex - 1 + galleryImages.length) % galleryImages.length);
}}
aria-label="Previous image"
>
&#8249;
</button>
<button
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white text-3xl z-10"
onClick={(e) => {
e.stopPropagation();
setLightboxIndex((lightboxIndex + 1) % galleryImages.length);
}}
aria-label="Next image"
>
&#8250;
</button>
<motion.div
key={lightboxIndex}
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="relative w-full max-w-4xl aspect-[3/2]"
onClick={(e) => e.stopPropagation()}
>
<Image
src={galleryImages[lightboxIndex].src}
alt={galleryImages[lightboxIndex].alt}
fill
className="object-contain"
sizes="100vw"
/>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</section>
);
}

View file

@ -0,0 +1,219 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
import Link from "next/link";
import { experiences } from "@/lib/constants";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { Button } from "@/components/shared/Button";
function ExperienceCard({
experience,
index,
featured = false,
}: {
experience: (typeof experiences)[0];
index: number;
featured?: boolean;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
if (featured) {
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.1, duration: 0.6 }}
className="col-span-full"
>
<Link href={`/experiences/${experience.slug}`} className="group block">
<div className="relative bg-white rounded-3xl overflow-hidden shadow-lg hover:shadow-xl transition-shadow">
<div className="grid grid-cols-1 lg:grid-cols-2">
<div className="relative aspect-[4/3] lg:aspect-auto">
<Image
src={experience.image}
alt={`${experience.name}${experience.hook}`}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
<div
className="absolute -top-1 -left-1 bg-sun-yolk text-deep-nazar px-5 py-2 rounded-br-2xl rounded-tl-3xl text-xs font-bold uppercase tracking-wide"
style={{ boxShadow: "0 4px 14px rgba(0,0,0,0.15), 0 1px 4px rgba(0,0,0,0.1)" }}
>
Popular Sells out 3 days in advance
</div>
</div>
<div className="p-8 lg:p-12 flex flex-col justify-center">
<p className="text-bosphorus font-semibold text-sm tracking-widest uppercase mb-2">
{experience.tagline}
</p>
<h3 className="font-display text-3xl md:text-4xl font-bold text-deep-nazar mb-4">
{experience.name}
</h3>
<p className="text-deep-nazar/70 text-lg leading-relaxed mb-6">
{experience.hook}
</p>
<div className="flex flex-wrap gap-2.5 mb-6">
{experience.highlights.map((h, i) => (
<span
key={i}
className="inline-flex items-center gap-1.5 bg-warm-sand rounded-full px-3 py-1.5 text-sm text-deep-nazar"
>
<span>{h.icon}</span>
{h.text}
</span>
))}
</div>
<div className="flex items-center justify-between pt-6 border-t border-deep-nazar/10">
<div className="flex flex-col">
<div className="flex items-center gap-4 text-sm text-deep-nazar/50 mb-1">
<span className="inline-flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{experience.duration}
</span>
<span className="inline-flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
{experience.groupSize}
</span>
</div>
<div>
<span className="text-sm text-deep-nazar/60">From</span>
<span className="text-3xl font-bold text-deep-nazar ml-2">
{experience.price}
</span>
<span className="text-deep-nazar/60">/person</span>
</div>
</div>
<span className="px-6 py-3 bg-coral-spritz text-white font-semibold rounded-full shadow-lg shadow-coral-spritz/25 group-hover:shadow-xl transition-shadow">
Book Now
</span>
</div>
</div>
</div>
</div>
</Link>
</motion.div>
);
}
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: index * 0.1, duration: 0.6 }}
>
<Link href={`/experiences/${experience.slug}`} className="group block h-full">
<div className="bg-white rounded-3xl overflow-hidden shadow-md hover:shadow-lg transition-all h-full flex flex-col">
<div className="relative aspect-[4/3] overflow-hidden">
<Image
src={experience.image}
alt={`${experience.name}${experience.hook}`}
fill
className="object-cover group-hover:scale-105 transition-transform duration-700"
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>
</div>
<div className="p-6 flex flex-col flex-1">
<p className="text-bosphorus font-semibold text-xs tracking-widest uppercase mb-1">
{experience.tagline}
</p>
<h3 className="font-display text-xl font-bold text-deep-nazar mb-2">
{experience.name}
</h3>
<p className="text-deep-nazar/70 text-sm leading-relaxed mb-4">
{experience.hook}
</p>
<div className="flex flex-wrap gap-2 mb-4">
{experience.highlights.slice(0, 3).map((h, i) => (
<span
key={i}
className="inline-flex items-center gap-1 bg-warm-sand rounded-full px-2.5 py-1 text-xs text-deep-nazar"
>
<span>{h.icon}</span>
{h.text}
</span>
))}
</div>
<div className="flex items-center gap-4 mb-4 text-xs text-deep-nazar/50">
<span className="inline-flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{experience.duration}
</span>
<span className="inline-flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
{experience.groupSize}
</span>
</div>
<div className="flex items-center justify-between pt-4 border-t border-deep-nazar/10 mt-auto">
<div>
<span className="text-xs text-deep-nazar/60">From </span>
<span className="text-xl font-bold text-deep-nazar">
{experience.price}
</span>
</div>
<span className="px-4 py-2 border-2 border-coral-spritz text-coral-spritz font-semibold text-sm rounded-full group-hover:bg-coral-spritz group-hover:text-white transition-colors">
Book Now
</span>
</div>
</div>
</div>
</Link>
</motion.div>
);
}
export function ExperienceGrid() {
const featured = experiences.find((e) => e.featured);
const others = experiences.filter((e) => !e.featured);
return (
<section id="experiences" className="py-20 md:py-32 section-padding">
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="Experiences"
title="Choose Your Edit"
subtitle="Every experience is handcrafted, small-group, and designed around the places we'd take our own friends."
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{featured && <ExperienceCard experience={featured} index={0} featured />}
{others.map((exp, i) => (
<ExperienceCard key={exp.slug} experience={exp} index={i + 1} />
))}
</div>
<motion.div
className="mt-16 bg-gradient-to-r from-deep-nazar to-deep-nazar/90 rounded-3xl p-8 md:p-12 text-white text-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<h3 className="font-display text-2xl md:text-3xl font-bold mb-3">
Private Editions
</h3>
<p className="text-white/80 text-lg mb-6 max-w-2xl mx-auto">
Want The Anatolian Edit all to yourself? Every experience is
available as a private edition for couples, families, or groups of up
to 12.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Button href="/contact" variant="coral" size="lg">
Inquire About Private Experiences
</Button>
<span className="text-white/60 text-sm">Starting from 250</span>
</div>
</motion.div>
</div>
</section>
);
}

View file

@ -0,0 +1,64 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
import { Button } from "@/components/shared/Button";
export function FinalCTA() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section className="relative py-24 md:py-36 overflow-hidden" ref={ref}>
<div className="absolute inset-0">
<Image
src="/images/manifesto_coastline.jpg"
alt="Locals enjoying the sun on Istanbul's Asian side waterfront with the sea and islands beyond"
fill
className="object-cover"
sizes="100vw"
/>
<div className="absolute inset-0 bg-gradient-to-t from-deep-nazar/80 via-deep-nazar/50 to-deep-nazar/30" />
</div>
<div className="relative z-10 max-w-3xl mx-auto text-center px-5 sm:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
>
<h2 className="font-display text-3xl sm:text-4xl md:text-5xl font-bold text-white leading-tight mb-6">
Your Istanbul Trip Already Has a Hagia Sophia Day.{" "}
<span className="text-sun-yolk italic">
Now Give It an Anatolian Edit.
</span>
</h2>
<p className="text-white/80 text-lg md:text-xl mb-4">
Small groups. Limited dates. The kind of day you&apos;ll talk about for
years.
</p>
<div className="inline-flex items-center gap-2 bg-coral-spritz/20 backdrop-blur-sm rounded-full px-5 py-2 mb-8">
<span className="w-2 h-2 rounded-full bg-coral-spritz animate-pulse" />
<span className="text-white text-sm font-medium">
Only 3 spots remaining for this week
</span>
</div>
<div className="flex flex-col items-center gap-4">
<Button href="/experiences/the-other-side" variant="coral" size="lg">
Book Your Experience
</Button>
<p className="text-white/50 text-sm">
Free cancellation up to 48 hours before &middot; Instant
confirmation &middot; Pay securely online
</p>
</div>
</motion.div>
</div>
</section>
);
}

110
components/home/Hero.tsx Normal file
View file

@ -0,0 +1,110 @@
"use client";
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
import { Button } from "@/components/shared/Button";
export function Hero() {
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const scale = useTransform(scrollYProgress, [0, 1], [1.1, 1]);
const opacity = useTransform(scrollYProgress, [0, 0.5], [1, 0]);
const y = useTransform(scrollYProgress, [0, 1], [0, 100]);
return (
<section ref={ref} className="relative h-screen min-h-[700px] overflow-hidden">
<motion.div className="absolute inset-0" style={{ scale }}>
<Image
src="/images/hero_main.jpg"
alt="Friends walking along a sun-drenched tree-lined boulevard on Istanbul's Asian side"
fill
className="object-cover"
priority
sizes="100vw"
/>
{/* Gradient scrim: 30% at top → 60% at bottom for text readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-black/25 to-black/60" />
{/* Extra top band so nav is always legible */}
<div className="absolute inset-x-0 top-0 h-28 bg-gradient-to-b from-black/40 to-transparent" />
</motion.div>
<motion.div
className="relative z-10 h-full flex flex-col justify-center items-center text-center px-5 sm:px-8"
style={{ opacity, y }}
>
{/* Glassmorphism container */}
<div className="bg-black/[0.45] backdrop-blur-2xl border border-white/[0.05] rounded-3xl px-5 py-8 sm:px-12 sm:py-14 max-w-[92%] sm:max-w-3xl shadow-2xl">
<motion.p
className="text-bosphorus font-semibold text-xs sm:text-sm tracking-[0.2em] uppercase mb-4 sm:mb-6"
style={{ textShadow: "0 1px 8px rgba(0,0,0,0.4)" }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
Istanbul&apos;s Asian Side &middot; Curated Experiences
</motion.p>
<motion.h1
className="font-display text-3xl sm:text-5xl md:text-6xl lg:text-7xl font-bold text-white max-w-4xl leading-[1.1] mb-4 sm:mb-6"
style={{ textShadow: "0 2px 10px rgba(0,0,0,0.7)" }}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
>
The Side They Never{" "}
<span className="text-sun-yolk italic">Show You</span>
</motion.h1>
<motion.p
className="text-white font-medium text-sm sm:text-lg md:text-xl max-w-2xl mx-auto mb-6 sm:mb-10 leading-relaxed"
style={{ textShadow: "0 1px 8px rgba(0,0,0,0.5)" }}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
>
While 17 million tourists crowd the same five monuments, an entire
coastline of world-class dining, sun-drenched boulevards, and the
city&apos;s best-kept secrets waits across the water. We take you there.
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 justify-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8 }}
>
<Button href="#experiences" variant="coral" size="lg">
Explore Our Experiences
</Button>
<Button href="#the-route" variant="outline" size="lg" className="border-white text-white hover:bg-white hover:text-deep-nazar">
Watch the Crossing
</Button>
</motion.div>
</div>
</motion.div>
<motion.div
className="absolute bottom-0 left-0 right-0 z-10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.2 }}
>
<div className="bg-white/90 backdrop-blur-sm py-4 px-5 sm:px-8">
<div className="max-w-5xl mx-auto flex flex-wrap justify-center gap-x-6 gap-y-2 text-sm text-deep-nazar/70">
<span className="flex items-center gap-1.5">
<span className="text-sun-yolk">&#9733;</span> 4.9/5 from 200+ guests
</span>
<span className="hidden sm:inline text-deep-nazar/30">|</span>
<span>Featured in TimeOut Istanbul</span>
<span className="hidden sm:inline text-deep-nazar/30">|</span>
<span>Small groups, max 8 people</span>
</div>
</div>
</motion.div>
</section>
);
}

102
components/home/HomeFAQ.tsx Normal file
View file

@ -0,0 +1,102 @@
"use client";
import { motion, useInView, AnimatePresence } from "framer-motion";
import { useRef, useState } from "react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { faqs } from "@/lib/constants";
import { generateFAQSchema } from "@/lib/structured-data";
function FAQItem({
faq,
index,
isOpen,
onToggle,
}: {
faq: (typeof faqs)[0];
index: number;
isOpen: boolean;
onToggle: () => void;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
return (
<motion.div
ref={ref}
className="border-b border-deep-nazar/10 last:border-0"
initial={{ opacity: 0, y: 10 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: index * 0.05 }}
>
<button
onClick={onToggle}
className="w-full py-5 flex items-center justify-between text-left group"
aria-expanded={isOpen}
>
<h3 className="font-display font-semibold text-base md:text-lg text-deep-nazar pr-8 group-hover:text-bosphorus transition-colors">
{faq.question}
</h3>
<motion.span
className="flex-shrink-0 w-8 h-8 rounded-full bg-bosphorus/10 flex items-center justify-center text-bosphorus"
animate={{ rotate: isOpen ? 45 : 0 }}
transition={{ duration: 0.2 }}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</motion.span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
>
<p className="pb-5 text-deep-nazar/70 leading-relaxed text-sm md:text-base">
{faq.answer}
</p>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
export function HomeFAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(0);
return (
<section className="py-20 md:py-32 section-padding bg-warm-sand">
<div className="max-w-3xl mx-auto">
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(generateFAQSchema(faqs)),
}}
/>
<SectionHeader
eyebrow="FAQ"
title="Got Questions? Good."
subtitle="We answer the ones that actually matter."
/>
<div className="bg-white rounded-3xl p-6 md:p-8 shadow-sm">
{faqs.map((faq, i) => (
<FAQItem
key={i}
faq={faq}
index={i}
isOpen={openIndex === i}
onToggle={() => setOpenIndex(openIndex === i ? null : i)}
/>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,77 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import { SectionHeader } from "@/components/shared/SectionHeader";
const steps = [
{
number: "01",
title: "Choose Your Experience",
description:
"Pick the afternoon coastal tour, the breakfast deep-dive, or go all in with the full-day edit.",
icon: "🎯",
},
{
number: "02",
title: "We Handle Everything",
description:
"Ferry tickets, reservations, insider access, the best table at sunset. You just show up.",
icon: "✨",
},
{
number: "03",
title: "Cross Over",
description:
"Meet your guide at the ferry. By the time you step off the boat, you're not a tourist anymore. You're a guest.",
icon: "🚢",
},
];
export function HowItWorks() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section className="py-20 md:py-32 section-padding bg-warm-sand" ref={ref}>
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="How It Works"
title="Three Steps to the Other Side"
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
{steps.map((step, i) => (
<motion.div
key={i}
className="relative text-center"
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: i * 0.2, duration: 0.6 }}
>
{i < steps.length - 1 && (
<div className="hidden md:block absolute top-12 left-[60%] w-[80%] h-[2px] bg-gradient-to-r from-bosphorus/30 to-transparent" />
)}
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-white shadow-lg mb-6 text-4xl">
{step.icon}
</div>
<div className="text-bosphorus font-display font-bold text-sm tracking-widest mb-2">
{step.number}
</div>
<h3 className="font-display text-xl font-bold text-deep-nazar mb-3">
{step.title}
</h3>
<p className="text-deep-nazar/60 text-sm leading-relaxed max-w-xs mx-auto">
{step.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,298 @@
"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>
);
}

View file

@ -0,0 +1,122 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import Image from "next/image";
const lines = [
{ text: "You've already planned the Hagia Sophia.", delay: 0, check: true },
{ text: "You've booked the Bosphorus cruise.", delay: 0.1, check: true },
{ text: "You've saved the Grand Bazaar food tour.", delay: 0.2, check: true },
{ text: "", delay: 0 },
{ text: "And you'll love all of it. Millions do.", delay: 0.4 },
{ text: "", delay: 0 },
{ text: "But here's what nobody tells you:", highlight: true, delay: 0.6 },
];
const facts = [
{
bold: "Istanbul's best neighbourhood for breakfast?",
text: " It's on the Asian side.",
},
{
bold: "The boulevard that rivals the Champs-Élysées?",
text: " Asian side.",
},
{
bold: "The sunset that makes the European skyline look like a painting?",
text: " You can only see it from the Asian side.",
},
{
bold: "The restaurants where Istanbul's own elite spend their weekends?",
text: " All on the Asian side.",
},
];
export function Manifesto() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section className="py-20 md:py-32 section-padding bg-warm-sand" ref={ref}>
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center">
<div className="space-y-6">
<div className="space-y-1">
{lines.map((line, i) =>
line.text === "" ? (
<div key={i} className="h-4" />
) : (
<motion.p
key={i}
className={`text-lg md:text-xl leading-relaxed ${
line.highlight
? "text-coral-spritz font-display font-bold text-2xl md:text-3xl mt-4"
: "text-deep-nazar/80"
}`}
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ delay: line.delay, duration: 0.5 }}
>
{line.check && (
<span className="text-coral-spritz mr-2 font-semibold">&#10003;</span>
)}
{line.text}
</motion.p>
)
)}
</div>
<div className="space-y-4 mt-8">
{facts.map((fact, i) => (
<motion.p
key={i}
className="text-base md:text-lg leading-relaxed text-deep-nazar/80"
initial={{ opacity: 0, x: -20 }}
animate={isInView ? { opacity: 1, x: 0 } : {}}
transition={{ delay: 0.8 + i * 0.15, duration: 0.5 }}
>
<strong className="text-deep-nazar">{fact.bold}</strong>
{fact.text}
</motion.p>
))}
</div>
<motion.div
className="pt-6"
initial={{ opacity: 0 }}
animate={isInView ? { opacity: 1 } : {}}
transition={{ delay: 1.6 }}
>
<p className="text-lg md:text-xl text-deep-nazar font-display font-semibold leading-relaxed">
The Anatolian Edit exists for a simple reason: the best day of
your Istanbul trip shouldn&apos;t be the one you already planned. It
should be the one you almost missed.
</p>
</motion.div>
</div>
<motion.div
className="relative"
initial={{ opacity: 0, scale: 0.95 }}
animate={isInView ? { opacity: 1, scale: 1 } : {}}
transition={{ delay: 0.5, duration: 0.8 }}
>
<div className="relative aspect-[4/5] rounded-3xl overflow-hidden">
<Image
src="/images/manifesto_coastline.jpg"
alt="Locals relaxing on the green waterfront of Istanbul's Asian side with the Marmara Sea and Princes' Islands in the background"
fill
className="object-cover"
sizes="(max-width: 1024px) 100vw, 50vw"
/>
</div>
<div className="absolute -bottom-4 -left-4 bg-sun-yolk text-deep-nazar rounded-2xl px-5 py-3 font-display font-bold text-sm rotate-[-4deg]" style={{ boxShadow: "0 6px 20px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12)" }}>
90% of tourists never see this side
</div>
</motion.div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,85 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef, useState } from "react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { testimonials } from "@/lib/constants";
function TestimonialCard({
testimonial,
index,
}: {
testimonial: (typeof testimonials)[0];
index: number;
}) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-50px" });
return (
<motion.div
ref={ref}
className="bg-white rounded-3xl p-8 shadow-sm border border-deep-nazar/5 flex flex-col"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: index * 0.1, duration: 0.5 }}
>
<div className="flex gap-1 mb-4">
{Array.from({ length: testimonial.rating }).map((_, i) => (
<span key={i} className="text-sun-yolk text-lg">
&#9733;
</span>
))}
</div>
<p className="text-deep-nazar/80 text-base leading-relaxed flex-1 mb-6 italic">
&ldquo;{testimonial.quote}&rdquo;
</p>
<div className="flex items-center gap-3 pt-4 border-t border-deep-nazar/5">
<div className="w-10 h-10 rounded-full bg-bosphorus/10 flex items-center justify-center text-lg">
{testimonial.flag}
</div>
<div>
<p className="font-semibold text-sm text-deep-nazar">
{testimonial.name}
</p>
<p className="text-xs text-deep-nazar/50">{testimonial.type}</p>
</div>
</div>
</motion.div>
);
}
export function Testimonials() {
const [showAll, setShowAll] = useState(false);
const displayed = showAll ? testimonials : testimonials.slice(0, 3);
return (
<section className="py-20 md:py-32 section-padding">
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="Guest Stories"
title="Don't Take Our Word for It"
subtitle="200+ guests. 4.9 average rating. Here's what they say."
/>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{displayed.map((t, i) => (
<TestimonialCard key={i} testimonial={t} index={i} />
))}
</div>
{!showAll && testimonials.length > 3 && (
<div className="text-center mt-8">
<button
onClick={() => setShowAll(true)}
className="text-bosphorus font-semibold text-sm hover:underline"
>
Show all {testimonials.length} reviews &rarr;
</button>
</div>
)}
</div>
</section>
);
}

View file

@ -0,0 +1,88 @@
"use client";
import { motion, useInView } from "framer-motion";
import { useRef } from "react";
import { SectionHeader } from "@/components/shared/SectionHeader";
import { AnimatedCounter } from "@/components/shared/AnimatedCounter";
import { pressQuotes } from "@/lib/constants";
const stats = [
{
title: "Where Istanbulites Actually Go",
stat: "1 in 3",
description:
"Istanbul residents lives on the Asian side. On weekends, thousands more cross over for breakfast, shopping, and the best sunsets in the city.",
},
{
title: "Named One of the World's 50 Coolest Neighbourhoods",
stat: "Top 50",
description:
"Kadıköy was named by Time Out as one of the 50 coolest neighbourhoods on Earth. You've probably never heard of it.",
},
{
title: "Zero Tourist Crowds",
stat: "50,000",
description:
"While Sultanahmet processes 50,000 visitors a day, you'll share Moda's waterfront with students, artists, and cats. Mostly cats.",
},
];
export function WhyAsianSide() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<section className="py-20 md:py-32 section-padding" ref={ref}>
<div className="max-w-7xl mx-auto">
<SectionHeader
eyebrow="Why the Asian Side?"
title="The Side Istanbul Loves Most"
subtitle="Don't take our word for it. Take Istanbul's."
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mb-16">
{stats.map((stat, i) => (
<motion.div
key={i}
className="bg-white rounded-3xl p-8 shadow-sm border border-deep-nazar/5 text-center"
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: i * 0.15, duration: 0.6 }}
>
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-bosphorus/10 text-bosphorus font-display font-bold text-lg mb-4">
{stat.stat === "50,000" ? (
<AnimatedCounter target={50000} suffix="" />
) : (
stat.stat
)}
</div>
<h3 className="font-display text-lg font-bold text-deep-nazar mb-3">
{stat.title}
</h3>
<p className="text-deep-nazar/60 text-sm leading-relaxed">
{stat.description}
</p>
</motion.div>
))}
</div>
{/* Press marquee */}
<div className="overflow-hidden py-6 border-y border-deep-nazar/10">
<div className="animate-marquee flex whitespace-nowrap">
{[...pressQuotes, ...pressQuotes].map((press, i) => (
<span
key={i}
className="mx-8 text-deep-nazar/40 text-sm font-medium inline-flex items-center gap-2"
>
<span className="italic">&ldquo;{press.quote}&rdquo;</span>
<span className="text-bosphorus font-semibold">
{press.source}
</span>
</span>
))}
</div>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,132 @@
import Link from "next/link";
import { SITE_NAME, EMAIL, INSTAGRAM, WHATSAPP_NUMBER, WHATSAPP_MESSAGE } from "@/lib/constants";
import { NewsletterSignup } from "@/components/shared/NewsletterSignup";
const experienceLinks = [
{ href: "/experiences/the-other-side", label: "The Other Side" },
{ href: "/experiences/first-light", label: "First Light" },
{ href: "/experiences/after-dark", label: "After Dark" },
];
const companyLinks = [
{ href: "/about", label: "About Us" },
{ href: "/blog", label: "The Edit (Blog)" },
{ href: "/faq", label: "FAQ" },
{ href: "/contact", label: "Contact" },
];
export function Footer() {
const whatsappUrl = `https://wa.me/${WHATSAPP_NUMBER.replace("+", "")}?text=${encodeURIComponent(WHATSAPP_MESSAGE)}`;
return (
<footer className="bg-deep-nazar text-white">
<div className="max-w-7xl mx-auto px-5 sm:px-8 py-16 md:py-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-16">
<div className="lg:col-span-1">
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-8 rounded-full bg-bosphorus flex items-center justify-center">
<div className="w-4 h-4 rounded-full bg-white flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-bosphorus" />
</div>
</div>
<span className="font-display font-bold text-lg">{SITE_NAME}</span>
</div>
<p className="text-white/60 text-sm mb-6 leading-relaxed">
Istanbul&apos;s Asian Side, Edited for You.
<br />
Based in Kadıköy, Istanbul on the right side of the Bosphorus.
</p>
<div className="flex gap-4">
<a
href={`https://instagram.com/${INSTAGRAM}`}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center hover:bg-bosphorus transition-colors"
aria-label="Instagram"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z" />
</svg>
</a>
<a
href={whatsappUrl}
target="_blank"
rel="noopener noreferrer"
className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center hover:bg-[#25D366] transition-colors"
aria-label="WhatsApp"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
</svg>
</a>
<a
href={`mailto:${EMAIL}`}
className="w-10 h-10 rounded-full bg-white/10 flex items-center justify-center hover:bg-bosphorus transition-colors"
aria-label="Email"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</a>
</div>
</div>
<div>
<h3 className="font-display font-bold text-sm uppercase tracking-widest mb-4 text-bosphorus">
Experiences
</h3>
<ul className="space-y-3">
{experienceLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} className="text-white/60 hover:text-white text-sm transition-colors">
{link.label}
</Link>
</li>
))}
<li>
<Link href="/contact" className="text-white/60 hover:text-white text-sm transition-colors">
Private Editions
</Link>
</li>
</ul>
</div>
<div>
<h3 className="font-display font-bold text-sm uppercase tracking-widest mb-4 text-bosphorus">
Company
</h3>
<ul className="space-y-3">
{companyLinks.map((link) => (
<li key={link.href}>
<Link href={link.href} className="text-white/60 hover:text-white text-sm transition-colors">
{link.label}
</Link>
</li>
))}
</ul>
</div>
<div>
<h3 className="font-display font-bold text-sm uppercase tracking-widest mb-4 text-bosphorus">
Get the Insider Edit
</h3>
<p className="text-white/60 text-sm mb-4">
Tips, secret spots, and early access to new experiences.
</p>
<NewsletterSignup />
</div>
</div>
<div className="border-t border-white/10 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-white/40 text-xs">
&copy; {new Date().getFullYear()} {SITE_NAME}. All rights reserved. TURSAB License #14285
</p>
<div className="flex gap-6 text-xs text-white/40">
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
</div>
</div>
</div>
</footer>
);
}

View file

@ -0,0 +1,103 @@
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import { motion, AnimatePresence } from "framer-motion";
import { MobileNav } from "./MobileNav";
const navLinks = [
{ href: "/experiences", label: "Experiences" },
{ href: "/about", label: "About" },
{ href: "/blog", label: "The Edit" },
{ href: "/faq", label: "FAQ" },
{ href: "/contact", label: "Contact" },
];
export function Header() {
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<>
<motion.header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-white/90 backdrop-blur-md shadow-sm"
: "bg-transparent"
}`}
initial={{ y: -100 }}
animate={{ y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="max-w-7xl mx-auto px-5 sm:px-8 flex items-center justify-between h-16 md:h-20">
<Link href="/" className="flex items-center gap-2 group">
<div className="w-8 h-8 rounded-full bg-bosphorus flex items-center justify-center">
<div className="w-4 h-4 rounded-full bg-deep-nazar flex items-center justify-center">
<div className="w-2 h-2 rounded-full bg-aegean-white" />
</div>
</div>
<span
className={`font-display font-bold text-lg transition-colors duration-300 ${
scrolled
? "text-deep-nazar"
: "text-white"
}`}
style={scrolled ? {} : { textShadow: "0 1px 8px rgba(0,0,0,0.5)" }}
>
The Anatolian Edit
</span>
</Link>
<nav className="hidden md:flex items-center gap-8">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className={`text-sm font-semibold transition-colors relative group ${
scrolled
? "text-deep-nazar/70 hover:text-bosphorus"
: "text-white/90 hover:text-white"
}`}
style={scrolled ? {} : { textShadow: "0 1px 6px rgba(0,0,0,0.5)" }}
>
{link.label}
<span className={`absolute -bottom-1 left-0 w-0 h-0.5 transition-all group-hover:w-full ${
scrolled ? "bg-bosphorus" : "bg-white"
}`} />
</Link>
))}
<Link
href="/experiences/the-other-side"
className="px-5 py-2.5 bg-coral-spritz text-white text-sm font-semibold rounded-full hover:bg-coral-spritz/90 transition-colors shadow-md shadow-coral-spritz/25"
>
Book Now
</Link>
</nav>
<button
onClick={() => setMobileOpen(true)}
className={`md:hidden p-2 transition-colors duration-300 ${
scrolled ? "text-deep-nazar" : "text-white"
}`}
style={scrolled ? {} : { filter: "drop-shadow(0 1px 4px rgba(0,0,0,0.5))" }}
aria-label="Open menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</motion.header>
<AnimatePresence>
{mobileOpen && <MobileNav onClose={() => setMobileOpen(false)} />}
</AnimatePresence>
</>
);
}

View file

@ -0,0 +1,78 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
const navLinks = [
{ href: "/experiences", label: "Experiences" },
{ href: "/experiences/the-other-side", label: "The Other Side", featured: true },
{ href: "/experiences/first-light", label: "First Light" },
{ href: "/experiences/after-dark", label: "After Dark" },
{ href: "/about", label: "About" },
{ href: "/blog", label: "The Edit" },
{ href: "/faq", label: "FAQ" },
{ href: "/contact", label: "Contact" },
];
interface MobileNavProps {
onClose: () => void;
}
export function MobileNav({ onClose }: MobileNavProps) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 bg-white"
>
<div className="flex flex-col h-full p-6">
<div className="flex items-center justify-between mb-12">
<span className="font-display font-bold text-lg text-deep-nazar">
The Anatolian Edit
</span>
<button
onClick={onClose}
className="p-2 text-deep-nazar"
aria-label="Close menu"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="flex flex-col gap-1 flex-1">
{navLinks.map((link, i) => (
<motion.div
key={link.href}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.05 }}
>
<Link
href={link.href}
onClick={onClose}
className={`block py-3 text-2xl font-display font-bold transition-colors ${
link.featured
? "text-bosphorus"
: "text-deep-nazar hover:text-bosphorus"
}`}
>
{link.label}
</Link>
</motion.div>
))}
</nav>
<Link
href="/experiences/the-other-side"
onClick={onClose}
className="w-full py-4 bg-coral-spritz text-white text-center text-lg font-semibold rounded-full shadow-lg"
>
Book Your Experience
</Link>
</div>
</motion.div>
);
}

View file

@ -0,0 +1,45 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Link from "next/link";
export function StickyBookingBar() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const handleScroll = () => {
setVisible(window.scrollY > 600);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-0 left-0 right-0 z-40 md:hidden bg-white/95 backdrop-blur-md border-t border-deep-nazar/10 px-5 py-3"
>
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-xs text-deep-nazar/60">From</p>
<p className="text-lg font-bold text-deep-nazar">
&euro;89<span className="text-sm font-normal text-deep-nazar/60">/person</span>
</p>
</div>
<Link
href="/experiences/the-other-side"
className="px-6 py-3 bg-coral-spritz text-white font-semibold rounded-full shadow-lg shadow-coral-spritz/25 text-sm"
>
Book Now
</Link>
</div>
</motion.div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,46 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useInView } from "framer-motion";
interface AnimatedCounterProps {
target: number;
suffix?: string;
prefix?: string;
duration?: number;
}
export function AnimatedCounter({
target,
suffix = "",
prefix = "",
duration = 2000,
}: AnimatedCounterProps) {
const [count, setCount] = useState(0);
const ref = useRef(null);
const isInView = useInView(ref, { once: true });
const hasAnimated = useRef(false);
useEffect(() => {
if (isInView && !hasAnimated.current) {
hasAnimated.current = true;
const startTime = Date.now();
const timer = setInterval(() => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setCount(Math.floor(eased * target));
if (progress >= 1) clearInterval(timer);
}, 16);
return () => clearInterval(timer);
}
}, [isInView, target, duration]);
return (
<span ref={ref}>
{prefix}
{count}
{suffix}
</span>
);
}

View file

@ -0,0 +1,74 @@
"use client";
import { motion } from "framer-motion";
import Link from "next/link";
interface ButtonProps {
children: React.ReactNode;
href?: string;
onClick?: () => void;
variant?: "primary" | "secondary" | "coral" | "outline";
size?: "sm" | "md" | "lg";
className?: string;
external?: boolean;
}
const variants = {
primary: "bg-bosphorus text-white hover:bg-bosphorus/90 shadow-lg shadow-bosphorus/25",
secondary: "bg-deep-nazar text-white hover:bg-deep-nazar/90",
coral: "bg-coral-spritz text-white hover:bg-coral-spritz/90 shadow-lg shadow-coral-spritz/25",
outline: "border-2 border-deep-nazar text-deep-nazar hover:bg-deep-nazar hover:text-white",
};
const sizes = {
sm: "px-5 py-2.5 text-sm",
md: "px-7 py-3.5 text-base",
lg: "px-9 py-4 text-lg",
};
export function Button({
children,
href,
onClick,
variant = "primary",
size = "md",
className = "",
external = false,
}: ButtonProps) {
const baseClasses = `inline-flex items-center justify-center font-semibold rounded-full transition-all duration-300 ${variants[variant]} ${sizes[size]} ${className}`;
const motionProps = {
whileHover: { scale: 1.03, y: -1 },
whileTap: { scale: 0.98 },
transition: { type: "spring" as const, stiffness: 400, damping: 17 },
};
if (href) {
if (external) {
return (
<motion.a
href={href}
target="_blank"
rel="noopener noreferrer"
className={baseClasses}
{...motionProps}
>
{children}
</motion.a>
);
}
return (
<Link href={href} className="inline-block">
<motion.span className={baseClasses} {...motionProps}>
{children}
</motion.span>
</Link>
);
}
return (
<motion.button onClick={onClick} className={baseClasses} {...motionProps}>
{children}
</motion.button>
);
}

View file

@ -0,0 +1,57 @@
"use client";
import { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
export function CookieConsent() {
const [show, setShow] = useState(false);
useEffect(() => {
const consent = localStorage.getItem("cookie-consent");
if (!consent) {
const timer = setTimeout(() => setShow(true), 3000);
return () => clearTimeout(timer);
}
}, []);
const accept = () => {
localStorage.setItem("cookie-consent", "accepted");
setShow(false);
};
const decline = () => {
localStorage.setItem("cookie-consent", "declined");
setShow(false);
};
return (
<AnimatePresence>
{show && (
<motion.div
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
className="fixed bottom-20 left-4 right-4 md:left-6 md:right-auto md:max-w-md z-40 bg-white rounded-2xl shadow-xl border border-deep-nazar/10 p-5"
>
<p className="text-sm text-deep-nazar/80 mb-4">
We use cookies to make your experience better. By continuing to visit this site you agree to our use of cookies.
</p>
<div className="flex gap-3">
<button
onClick={accept}
className="px-5 py-2 bg-bosphorus text-white text-sm font-semibold rounded-full hover:bg-bosphorus/90 transition-colors"
>
Accept
</button>
<button
onClick={decline}
className="px-5 py-2 text-deep-nazar/60 text-sm font-semibold rounded-full hover:bg-deep-nazar/5 transition-colors"
>
Decline
</button>
</div>
</motion.div>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,38 @@
"use client";
import { useState } from "react";
import { Button } from "./Button";
export function NewsletterSignup() {
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setSubmitted(true);
};
if (submitted) {
return (
<div className="text-center py-4">
<p className="text-bosphorus font-semibold">You&apos;re in! Check your inbox for the insider edit.</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="flex gap-3 max-w-md">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Your email"
required
className="flex-1 px-5 py-3 rounded-full border border-deep-nazar/20 bg-white text-deep-nazar placeholder:text-deep-nazar/40 focus:outline-none focus:border-bosphorus focus:ring-2 focus:ring-bosphorus/20 transition-all"
/>
<Button variant="primary" size="sm">
Subscribe
</Button>
</form>
);
}

View file

@ -0,0 +1,33 @@
interface SectionHeaderProps {
eyebrow?: string;
title: string;
subtitle?: string;
centered?: boolean;
className?: string;
}
export function SectionHeader({
eyebrow,
title,
subtitle,
centered = true,
className = "",
}: SectionHeaderProps) {
return (
<div className={`${centered ? "text-center" : ""} mb-12 md:mb-16 ${className}`}>
{eyebrow && (
<p className="text-bosphorus font-semibold text-sm tracking-widest uppercase mb-3">
{eyebrow}
</p>
)}
<h2 className="font-display text-3xl sm:text-4xl md:text-5xl font-bold text-deep-nazar leading-tight">
{title}
</h2>
{subtitle && (
<p className="mt-4 text-lg text-deep-nazar/70 max-w-2xl mx-auto leading-relaxed">
{subtitle}
</p>
)}
</div>
);
}

View file

@ -0,0 +1,27 @@
"use client";
import { motion } from "framer-motion";
import { WHATSAPP_NUMBER, WHATSAPP_MESSAGE } from "@/lib/constants";
export function WhatsAppButton() {
const url = `https://wa.me/${WHATSAPP_NUMBER.replace("+", "")}?text=${encodeURIComponent(WHATSAPP_MESSAGE)}`;
return (
<motion.a
href={url}
target="_blank"
rel="noopener noreferrer"
className="fixed bottom-24 md:bottom-6 right-6 z-40 bg-[#25D366] text-white w-14 h-14 rounded-full flex items-center justify-center shadow-lg shadow-[#25D366]/30 hover:shadow-xl transition-shadow"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 2 }}
aria-label="Chat on WhatsApp"
>
<svg viewBox="0 0 24 24" className="w-7 h-7 fill-current">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z" />
</svg>
</motion.a>
);
}

387
lib/constants.ts Normal file
View file

@ -0,0 +1,387 @@
export const SITE_NAME = "The Anatolian Edit";
export const SITE_URL = "https://theanatolianedit.com";
export const SITE_DESCRIPTION =
"Curated experiences on Istanbul's Asian side. Small-group tours through Kadıköy, Moda, Bağdat Caddesi and the coastline that 90% of visitors never see.";
export const WHATSAPP_NUMBER = "+905551234567";
export const WHATSAPP_MESSAGE =
"Hi! I'm interested in The Anatolian Edit experiences. Can you help me plan?";
export const EMAIL = "hello@theanatolianedit.com";
export const INSTAGRAM = "theanatolianedit";
export interface Experience {
slug: string;
name: string;
tagline: string;
hook: string;
duration: string;
groupSize: string;
price: number;
currency: string;
image: string;
highlights: { icon: string; text: string }[];
includes: string[];
itinerary?: ItineraryStop[];
featured?: boolean;
}
export interface ItineraryStop {
time: string;
title: string;
description: string;
}
export const experiences: Experience[] = [
{
slug: "the-other-side",
name: "The Other Side",
tagline: "Our Signature Half-Day Experience",
hook: "A perfect afternoon on Istanbul's best-kept coastline — from a historic pier with a library to sunset drinks on the Riviera you didn't know Istanbul had.",
duration: "56 hours",
groupSize: "Max 8 guests",
price: 89,
currency: "EUR",
image: "/images/the_other_side.jpg",
featured: true,
highlights: [
{ icon: "🚢", text: "Ferry ride included" },
{ icon: "☕", text: "Third-wave coffee tasting" },
{ icon: "🌅", text: "Sunset over the European skyline" },
{ icon: "🍽️", text: "Full dinner included" },
{ icon: "📸", text: "Photo spots you won't find on Google" },
{ icon: "👥", text: "Max 8 guests" },
],
includes: [
"Ferry crossing with guide",
"Specialty coffee at Moda",
"Guided walk through Fenerbahçe Park & Kalamış Marina",
"Caddebostan seafront & street food tastings",
"Bağdat Caddesi boulevard stroll",
"Sunset dinner at a curated Suadiye restaurant (food included)",
],
itinerary: [
{
time: "3:00 PM",
title: "The Crossing",
description:
"Meet your guide at Karaköy ferry terminal. Board the ferry together — Turkish tea in hand, European skyline shrinking behind you. Your guide briefs you on what's ahead as seagulls trace the boat's wake. Twenty minutes later, you step onto the historic Moda İskelesi — a 1917 pier that now houses a library and café jutting into the Marmara Sea. Welcome to the other side.",
},
{
time: "3:30 PM",
title: "Moda: Istanbul's Best-Kept Secret",
description:
"Wander the tree-lined streets where artists, musicians, and writers have lived for over a century. Stop at one of Moda's legendary third-wave coffee shops for your first tasting. Your guide doesn't lecture — they share stories. Why this neighbourhood is called Istanbul's Brooklyn. Why the cats here are famous. Why you'll want to move here by the end of the day.",
},
{
time: "4:15 PM",
title: "Fenerbahçe Park & Kalamış Marina",
description:
"A surprise for every first-time visitor: Istanbul has a Mediterranean-style marina with bobbing yachts, pine trees, and sea air. Stroll through Fenerbahçe Park to Kalamış Bay. Pause for a cold drink at a waterside café. This is the Istanbul that doesn't make the guidebooks.",
},
{
time: "5:00 PM",
title: "Caddebostan Seafront",
description:
"The long coastal promenade where Istanbulites jog, cycle, and gather at sunset. Sample Istanbul's most Instagram-famous street food at a local favourite. Feel the energy shift — this isn't tourist Istanbul. This is Saturday-afternoon Istanbul.",
},
{
time: "5:45 PM",
title: "Bağdat Caddesi: The Boulevard",
description:
"Istanbul's answer to the Champs-Élysées. A 6-kilometre luxury boulevard lined with designer boutiques, concept cafés, and century-old patisseries. Your guide tells the story: from Ottoman military road to the most prestigious address on the Asian side. Browse Turkish designer brands you won't find in Europe. Or just people-watch with an ice cream.",
},
{
time: "6:30 PM",
title: "Suadiye: Sunset & Dinner",
description:
"The finale. As the sun drops toward the European skyline — painting Hagia Sophia and the Blue Mosque in gold — you'll sit down to a curated dinner at one of Suadiye's finest restaurants. Meze, fresh fish, Turkish wine, conversation. This isn't a \"dinner stop.\" It's the reason the whole day was building to this moment.",
},
{
time: "~8:30 PM",
title: "End",
description:
"Your guide helps you arrange the return — ferry, taxi, or Marmaray train. You leave with a full stomach, a camera full of photos no other tourist has, and the knowledge that you've seen the Istanbul that Istanbul loves.",
},
],
},
{
slug: "first-light",
name: "First Light",
tagline: "The Asian Side Breakfast Experience",
hook: "Istanbul's breakfast culture is legendary. The best of it happens on this side of the water.",
duration: "3 hours",
groupSize: "Max 8 guests",
price: 59,
currency: "EUR",
image: "/images/first_light.jpg",
highlights: [
{ icon: "🥚", text: "Serpme kahvaltı spread" },
{ icon: "🛒", text: "Kadıköy market walk" },
{ icon: "☕", text: "Turkish coffee ritual" },
{ icon: "🏘️", text: "Moda neighbourhood stroll" },
],
includes: [
"Serpme kahvaltı (spread breakfast) at a local favourite",
"Market walk through Kadıköy Çarşı",
"Turkish coffee ritual at a charcoal-roasted coffee house",
"Moda neighbourhood walk",
],
itinerary: [
{
time: "9:00 AM",
title: "Meet at Kadıköy",
description:
"Your guide meets you at the Kadıköy ferry dock. The morning light hits different here — softer, warmer, with the smell of simit and sea salt in the air.",
},
{
time: "9:15 AM",
title: "The Breakfast Spread",
description:
"Sit down to a full serpme kahvaltı — the legendary Turkish breakfast spread. Dozens of small plates: cheeses from the Black Sea, honey from the Aegean, eggs cooked to order, fresh-baked bread, and tea that never stops flowing.",
},
{
time: "10:30 AM",
title: "Kadıköy Market",
description:
"Walk through the bustling Kadıköy Çarşı where fishmongers shout their catch and spice vendors offer tastes of everything. Your guide knows every stall worth stopping at.",
},
{
time: "11:15 AM",
title: "Turkish Coffee Ritual",
description:
"End at a charcoal-roasted coffee house where the beans are ground to order. Learn how to read your fortune in the grounds — a tradition that's been going on for 500 years.",
},
],
},
{
slug: "after-dark",
name: "After Dark",
tagline: "The Kadıköy Meyhane Evening",
hook: "Rakı, meze, live music, and the most honest night out in Istanbul.",
duration: "3 hours",
groupSize: "Max 8 guests",
price: 79,
currency: "EUR",
image: "https://images.unsplash.com/photo-1551632436-cbf8dd35adfa?w=800&q=80",
highlights: [
{ icon: "🥂", text: "Rakı pairings" },
{ icon: "🎵", text: "Live fasıl music" },
{ icon: "🍢", text: "Meze feast" },
{ icon: "🚢", text: "Ferry ride back under the lights" },
],
includes: [
"Meyhane dinner with rakı pairings",
"Kadıköy Barlar Sokağı (bar street) guided crawl",
"Live fasıl music",
"Ferry ride back under the city lights",
],
itinerary: [
{
time: "7:00 PM",
title: "Meet at Kadıköy",
description:
"The neighbourhood comes alive at night. Your guide meets you where the energy is highest — right in the heart of Kadıköy's buzzing bar district.",
},
{
time: "7:15 PM",
title: "The Meyhane",
description:
"Sit down at a proper meyhane — the Turkish tavern. Meze arrives in waves: octopus, hummus, stuffed vine leaves, grilled halloumi. Then the rakı. Your guide teaches you the ritual: the water, the ice, the toast. Şerefe.",
},
{
time: "8:30 PM",
title: "Barlar Sokağı",
description:
"Kadıköy's bar street is loud, colourful, and completely free of tourists. Hop between live music venues, craft beer bars, and rooftop spots with views of the Marmara.",
},
{
time: "9:30 PM",
title: "The Night Ferry",
description:
"The most cinematic ending to any night in Istanbul. Board the ferry back to Europe as the city skyline glitters across the water. Turkish tea in hand, city lights reflected in the Bosphorus.",
},
],
},
];
export interface Testimonial {
quote: string;
name: string;
flag: string;
type: string;
rating: number;
}
export const testimonials: Testimonial[] = [
{
quote:
"We almost didn't do this. We thought — why would we leave the European side? It ended up being the single best day of our entire trip. The sunset from Caddebostan with the Hagia Sophia skyline across the water... I still can't believe that view isn't in every guidebook.",
name: "Sofia M.",
flag: "🇮🇹",
type: "Couple",
rating: 5,
},
{
quote:
"Our guide wasn't a guide — he was a friend who happened to know everything. By the third stop, we'd completely forgotten we were on a 'tour.' It just felt like an incredible day with a local who genuinely loved showing us his city.",
name: "James & Hannah R.",
flag: "🇬🇧",
type: "Couple",
rating: 5,
},
{
quote:
"As a woman traveling alone, I was nervous about going off the main tourist area. This was the opposite of what I feared. I felt completely safe, completely welcomed, and completely blown away. The meyhane dinner was the highlight of my Istanbul trip.",
name: "Amira K.",
flag: "🇦🇪",
type: "Solo",
rating: 5,
},
{
quote:
"We have two kids under 10 and every other Istanbul tour was designed for adults. This was different — our guide adapted everything. The kids loved the ferry ride, the ice cream on Bağdat Caddesi, and the park. We loved the food and the pace. Everyone was happy.",
name: "The Petersons",
flag: "🇺🇸",
type: "Family",
rating: 5,
},
{
quote:
"Third time in Istanbul. First time on the Asian side. I'm genuinely annoyed at myself for missing this on my first two trips. Moda alone is worth the ferry ride. The whole experience felt like someone opened a door I didn't know existed.",
name: "Marcus W.",
flag: "🇩🇪",
type: "Solo",
rating: 5,
},
{
quote:
"The content I got from this tour outperformed everything I shot on the European side. Moda, the waterfront, the sunset — every spot was perfect for photos. My guide even knew the best angles. Posted a reel that got 4x my usual views.",
name: "Yuki T.",
flag: "🇯🇵",
type: "Friends",
rating: 5,
},
];
export interface FAQ {
question: string;
answer: string;
}
export const faqs: FAQ[] = [
{
question: "Is the Asian side of Istanbul worth visiting?",
answer:
"Yes — Istanbul's Asian side is absolutely worth visiting and is increasingly recommended by travel experts as a must-see. Neighbourhoods like Kadıköy (named one of the world's 50 coolest by TimeOut), Moda, and the Bağdat Caddesi boulevard offer world-class dining, stunning Marmara Sea views, and a completely crowd-free alternative to the tourist-heavy European side. Most visitors spend all their time in Sultanahmet, but locals overwhelmingly prefer the Asian side for its lifestyle, food scene, and waterfront culture.",
},
{
question: "How do I get to the Asian side of Istanbul?",
answer:
"Getting to the Asian side is easy and scenic. The most popular way is by ferry from Karaköy or Eminönü — the ride takes about 20 minutes and costs less than €1. You can also take the Marmaray metro tunnel under the Bosphorus (5 minutes) or drive across the bridges. On all our experiences, we include the ferry crossing with your guide, so you don't need to worry about logistics.",
},
{
question: "What is there to do on Istanbul's Asian side?",
answer:
"The Asian side offers a different pace of Istanbul — think waterfront promenades, world-class breakfast culture, Istanbul's best shopping boulevard (Bağdat Caddesi), vibrant nightlife in Kadıköy, stunning parks along the Marmara Sea, and the city's most photogenic sunset views. It's where Istanbulites go on weekends for brunch, shopping, and coastal walks. Our experiences cover the highlights so you don't miss the best parts.",
},
{
question: "Is Kadıköy safe for tourists?",
answer:
"Kadıköy is one of the safest and most welcoming neighbourhoods in all of Istanbul. It's a residential area beloved by artists, students, and families. The streets are well-lit, busy with locals at all hours, and have a relaxed, progressive atmosphere. Solo travellers, including women, consistently tell us they feel more comfortable here than in the busier tourist districts on the European side.",
},
{
question: "What is Bağdat Caddesi?",
answer:
"Bağdat Caddesi (Baghdad Avenue) is Istanbul's most prestigious shopping boulevard — a 6-kilometre stretch on the Asian side lined with designer boutiques, concept stores, historic patisseries, and upscale cafés. Think of it as Istanbul's answer to the Champs-Élysées. It runs parallel to the Marmara Sea coast and is a favourite weekend destination for Istanbul's locals. Our signature experience includes a guided stroll along the boulevard.",
},
{
question: "Can I visit the Asian side of Istanbul in half a day?",
answer:
"Absolutely. A half-day is the perfect amount of time to experience the Asian side's highlights. Our signature experience, 'The Other Side,' runs from 3 PM to around 8:30 PM — covering the ferry crossing, Moda's coffee scene, the coastal promenade, Bağdat Caddesi, and a sunset dinner in Suadiye. You'll see more in those 56 hours than most visitors see in days.",
},
{
question: "Is the Asian side good for families?",
answer:
"The Asian side is fantastic for families. The waterfront parks, wide promenades, and relaxed pace make it much more family-friendly than the crowded tourist districts. Kids love the ferry ride, the ice cream shops on Bağdat Caddesi, and running around Fenerbahçe Park. Our guides adapt every experience for families, adjusting the pace and stops to keep everyone happy.",
},
{
question:
"What's the difference between the European and Asian side of Istanbul?",
answer:
"The European side has Istanbul's famous historical monuments (Hagia Sophia, Blue Mosque, Grand Bazaar) and the busy tourist infrastructure. The Asian side is where Istanbul actually lives — it's more residential, greener, calmer, and focused on lifestyle: great food, beautiful coastline, and a local energy you won't find in Sultanahmet. Think of it as the difference between visiting Times Square and exploring Brooklyn.",
},
{
question: "Where do locals go in Istanbul?",
answer:
"Locals overwhelmingly head to the Asian side on weekends. Kadıköy for breakfast and nightlife, Moda for coffee and sunset walks, Bağdat Caddesi for shopping, and the Caddebostan waterfront for jogging and socialising. One in three Istanbul residents lives on the Asian side, and thousands more cross over every weekend. Our experiences take you to exactly these spots.",
},
{
question: "What is the best sunset spot in Istanbul?",
answer:
"The best sunset in Istanbul is from the Asian side looking west toward Europe. From the Caddebostan or Suadiye waterfront, you watch the sun drop behind the silhouette of Hagia Sophia, the Blue Mosque, and the Topkapı Palace — a skyline you can't see from the European side itself. It's the signature moment of our 'The Other Side' experience, and it's worth the entire trip.",
},
];
export const pressQuotes = [
{ quote: "The real Istanbul", source: "Lonely Planet" },
{ quote: "Skip the European side on day two", source: "Condé Nast Traveler" },
{ quote: "Kadıköy is Istanbul's Brooklyn", source: "TimeOut" },
{ quote: "Where the locals actually go", source: "The Guardian" },
{ quote: "The Asian side is Istanbul's best secret", source: "AFAR" },
];
export const currencyRates: Record<string, number> = {
EUR: 1,
USD: 1.08,
GBP: 0.86,
TRY: 34.5,
};
export const currencySymbols: Record<string, string> = {
EUR: "€",
USD: "$",
GBP: "£",
TRY: "₺",
};
export interface BlogPost {
slug: string;
title: string;
excerpt: string;
category: string;
image: string;
date: string;
readTime: string;
}
export const blogPosts: BlogPost[] = [
{
slug: "ultimate-guide-kadikoy",
title: "The Insider's Guide to Kadıköy: Everything You Need to Know",
excerpt:
"Kadıköy is not a tourist neighbourhood. It's where Istanbul eats, drinks, shops, and lives. Here's how to spend a perfect day in the neighbourhood TimeOut called one of the 50 coolest on Earth.",
category: "Neighbourhood Guides",
image: "/images/manifesto_coastline.jpg",
date: "2024-12-15",
readTime: "8 min read",
},
{
slug: "turkish-breakfast-guide",
title: "Turkish Breakfast: Why It's the Best Meal You'll Ever Have",
excerpt:
"Forget continental breakfast. Turkish kahvaltı is a marathon of flavour — dozens of small plates, endless tea, and a ritual that can last three hours. Here's where to find the best on the Asian side.",
category: "Food & Culture",
image: "/images/first_light.jpg",
date: "2024-11-28",
readTime: "6 min read",
},
{
slug: "best-sunset-spots-istanbul",
title: "5 Sunset Spots in Istanbul That Will Ruin Every Other Sunset for You",
excerpt:
"Istanbul does sunsets differently. When the light hits the minarets and the Bosphorus turns gold, you understand why empires fought over this city. Here are the five best places to watch it happen.",
category: "Best Of",
image: "/images/the_other_side.jpg",
date: "2024-11-10",
readTime: "5 min read",
},
];

61
lib/metadata.ts Normal file
View file

@ -0,0 +1,61 @@
import { Metadata } from "next";
const SITE_URL = "https://theanatolianedit.com";
const SITE_NAME = "The Anatolian Edit";
export function generatePageMetadata({
title,
description,
path = "",
image = "/og/default.jpg",
}: {
title: string;
description: string;
path?: string;
image?: string;
}): Metadata {
const url = `${SITE_URL}${path}`;
const fullTitle = `${title} | ${SITE_NAME}`;
return {
title: fullTitle,
description,
metadataBase: new URL(SITE_URL),
alternates: {
canonical: url,
},
openGraph: {
title: fullTitle,
description,
url,
siteName: SITE_NAME,
images: [
{
url: image,
width: 1200,
height: 630,
alt: title,
},
],
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
title: fullTitle,
description,
images: [image],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
};
}

162
lib/structured-data.ts Normal file
View file

@ -0,0 +1,162 @@
import { Experience } from "./constants";
export function generateOrganizationSchema() {
return {
"@context": "https://schema.org",
"@type": "TravelAgency",
name: "The Anatolian Edit",
description: "Curated experiences on Istanbul's Asian side",
url: "https://theanatolianedit.com",
logo: "https://theanatolianedit.com/icons/logo.svg",
areaServed: {
"@type": "City",
name: "Istanbul",
addressCountry: "TR",
},
address: {
"@type": "PostalAddress",
streetAddress: "Caferağa Mahallesi",
addressLocality: "Kadıköy",
addressRegion: "Istanbul",
postalCode: "34710",
addressCountry: "TR",
},
geo: {
"@type": "GeoCoordinates",
latitude: 40.9833,
longitude: 29.0333,
},
telephone: "+905551234567",
email: "hello@theanatolianedit.com",
priceRange: "€€",
openingHoursSpecification: {
"@type": "OpeningHoursSpecification",
dayOfWeek: [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday",
],
opens: "08:00",
closes: "22:00",
},
paymentAccepted: "Credit Card, Cash",
aggregateRating: {
"@type": "AggregateRating",
ratingValue: "4.9",
reviewCount: "200",
bestRating: "5",
},
sameAs: ["https://www.instagram.com/theanatolianedit"],
};
}
export function generateLocalBusinessSchema() {
return {
"@context": "https://schema.org",
"@type": "LocalBusiness",
name: "The Anatolian Edit",
description: "Curated tour experiences on Istanbul's Asian side",
url: "https://theanatolianedit.com",
address: {
"@type": "PostalAddress",
streetAddress: "Caferağa Mahallesi",
addressLocality: "Kadıköy",
addressRegion: "Istanbul",
postalCode: "34710",
addressCountry: "TR",
},
geo: {
"@type": "GeoCoordinates",
latitude: 40.9833,
longitude: 29.0333,
},
telephone: "+905551234567",
priceRange: "€€",
openingHoursSpecification: {
"@type": "OpeningHoursSpecification",
dayOfWeek: [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday",
],
opens: "08:00",
closes: "22:00",
},
};
}
export function generateTouristTripSchema(experience: Experience) {
return {
"@context": "https://schema.org",
"@type": "TouristTrip",
name: `${experience.name}${experience.tagline}`,
description: experience.hook,
touristType: "Cultural tourism",
itinerary: experience.itinerary
? {
"@type": "ItemList",
itemListElement: experience.itinerary.map((stop, index) => ({
"@type": "ListItem",
position: index + 1,
name: `${stop.time}${stop.title}`,
description: stop.description,
})),
}
: undefined,
offers: {
"@type": "Offer",
price: experience.price.toString(),
priceCurrency: experience.currency,
availability: "https://schema.org/InStock",
validFrom: "2024-01-01",
},
provider: {
"@type": "TravelAgency",
name: "The Anatolian Edit",
url: "https://theanatolianedit.com",
},
};
}
export function generateFAQSchema(faqs: { question: string; answer: string }[]) {
return {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: faqs.map((faq) => ({
"@type": "Question",
name: faq.question,
acceptedAnswer: {
"@type": "Answer",
text: faq.answer,
},
})),
};
}
export function generateBreadcrumbSchema(
items: { name: string; url: string }[]
) {
return {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: items.map((item, index) => ({
"@type": "ListItem",
position: index + 1,
name: item.name,
item: item.url,
})),
};
}
export function generateAggregateRatingSchema() {
return {
"@context": "https://schema.org",
"@type": "Product",
name: "The Anatolian Edit Experiences",
aggregateRating: {
"@type": "AggregateRating",
ratingValue: "4.9",
reviewCount: "200",
bestRating: "5",
worstRating: "1",
},
};
}

14
next.config.mjs Normal file
View file

@ -0,0 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
};
export default nextConfig;

5998
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "anatolian2-temp",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"framer-motion": "^12.34.0",
"next": "14.2.35",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.35",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

BIN
public/images/hero_main.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

45
tailwind.config.ts Normal file
View file

@ -0,0 +1,45 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
'bosphorus': '#06B6D4',
'deep-nazar': '#1E3A8A',
'aegean-white': '#FFFFFF',
'sun-yolk': '#FACC15',
'warm-sand': '#FEF3C7',
'coral-spritz': '#FB7185',
},
fontFamily: {
display: ['var(--font-syne)', 'system-ui', 'sans-serif'],
body: ['var(--font-jakarta)', 'system-ui', 'sans-serif'],
},
borderRadius: {
'4xl': '2rem',
'5xl': '2.5rem',
},
keyframes: {
'marquee': {
'0%': { transform: 'translateX(0%)' },
'100%': { transform: 'translateX(-50%)' },
},
'fade-up': {
'0%': { opacity: '0', transform: 'translateY(20px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
animation: {
'marquee': 'marquee 30s linear infinite',
'fade-up': 'fade-up 0.6s ease-out forwards',
},
},
},
plugins: [],
};
export default config;

26
tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}