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:
commit
591d878ac6
59 changed files with 10167 additions and 0 deletions
7
.dockerignore
Normal file
7
.dockerignore
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.next
|
||||
.vercel
|
||||
.claude
|
||||
.git
|
||||
imagesforyou
|
||||
server.md
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal 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
36
Dockerfile
Normal 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
36
README.md
Normal 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
128
app/about/page.tsx
Normal 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'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're not guides. We'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
129
app/blog/[slug]/page.tsx
Normal 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"
|
||||
>
|
||||
← 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>·</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,000–3,000 words of original, SEO-optimized content written in The Anatolian
|
||||
Edit'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't an alternative to the European side — it'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
79
app/blog/page.tsx
Normal 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
110
app/contact/page.tsx
Normal 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'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'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're looking for and we'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 €250 for private groups
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
76
app/experiences/after-dark/page.tsx
Normal file
76
app/experiences/after-dark/page.tsx
Normal 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'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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
76
app/experiences/first-light/page.tsx
Normal file
76
app/experiences/first-light/page.tsx
Normal 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'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
65
app/experiences/page.tsx
Normal 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 €250</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<FinalCTA />
|
||||
</>
|
||||
);
|
||||
}
|
||||
125
app/experiences/the-other-side/page.tsx
Normal file
125
app/experiences/the-other-side/page.tsx
Normal 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'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 €{exp.price} →
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<FinalCTA />
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
app/faq/page.tsx
Normal file
38
app/faq/page.tsx
Normal 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
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
39
app/globals.css
Normal file
39
app/globals.css
Normal 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
78
app/layout.tsx
Normal 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
31
app/not-found.tsx
Normal 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've drifted off course. This page doesn't exist —
|
||||
but Istanbul'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
27
app/page.tsx
Normal 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
12
app/robots.ts
Normal 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
31
app/sitemap.ts
Normal 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];
|
||||
}
|
||||
140
components/experiences/BookingWidget.tsx
Normal file
140
components/experiences/BookingWidget.tsx
Normal 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">
|
||||
€{experience.price} × {guests} guest{guests > 1 ? "s" : ""}
|
||||
</span>
|
||||
<span className="font-semibold text-deep-nazar">
|
||||
€{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">
|
||||
€{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 · Pay securely online · Free cancellation up to 48h before
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
66
components/experiences/ExperienceCard.tsx
Normal file
66
components/experiences/ExperienceCard.tsx
Normal 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">
|
||||
€{experience.price}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-coral-spritz font-semibold text-sm group-hover:underline">
|
||||
Book Now →
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
73
components/experiences/ExperienceHero.tsx
Normal file
73
components/experiences/ExperienceHero.tsx
Normal 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">★ 4.9/5</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<Button href="#booking" variant="coral" size="lg">
|
||||
Book Now — From €{experience.price}
|
||||
</Button>
|
||||
<span className="text-white/50 text-sm">
|
||||
Free cancellation up to 48h before
|
||||
</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
76
components/experiences/Itinerary.tsx
Normal file
76
components/experiences/Itinerary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
119
components/experiences/PhotoGallery.tsx
Normal file
119
components/experiences/PhotoGallery.tsx
Normal 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"
|
||||
>
|
||||
×
|
||||
</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"
|
||||
>
|
||||
‹
|
||||
</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"
|
||||
>
|
||||
›
|
||||
</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>
|
||||
);
|
||||
}
|
||||
219
components/home/ExperienceGrid.tsx
Normal file
219
components/home/ExperienceGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
components/home/FinalCTA.tsx
Normal file
64
components/home/FinalCTA.tsx
Normal 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'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 · Instant
|
||||
confirmation · Pay securely online
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
110
components/home/Hero.tsx
Normal file
110
components/home/Hero.tsx
Normal 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's Asian Side · 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'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">★</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
102
components/home/HomeFAQ.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
77
components/home/HowItWorks.tsx
Normal file
77
components/home/HowItWorks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
298
components/home/InteractiveMap.tsx
Normal file
298
components/home/InteractiveMap.tsx
Normal 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 (0–100) 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>
|
||||
);
|
||||
}
|
||||
122
components/home/Manifesto.tsx
Normal file
122
components/home/Manifesto.tsx
Normal 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">✓</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'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>
|
||||
);
|
||||
}
|
||||
85
components/home/Testimonials.tsx
Normal file
85
components/home/Testimonials.tsx
Normal 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">
|
||||
★
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-deep-nazar/80 text-base leading-relaxed flex-1 mb-6 italic">
|
||||
“{testimonial.quote}”
|
||||
</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 →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
88
components/home/WhyAsianSide.tsx
Normal file
88
components/home/WhyAsianSide.tsx
Normal 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">“{press.quote}”</span>
|
||||
<span className="text-bosphorus font-semibold">
|
||||
— {press.source}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
132
components/layout/Footer.tsx
Normal file
132
components/layout/Footer.tsx
Normal 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'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">
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
103
components/layout/Header.tsx
Normal file
103
components/layout/Header.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
78
components/layout/MobileNav.tsx
Normal file
78
components/layout/MobileNav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
components/layout/StickyBookingBar.tsx
Normal file
45
components/layout/StickyBookingBar.tsx
Normal 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">
|
||||
€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>
|
||||
);
|
||||
}
|
||||
46
components/shared/AnimatedCounter.tsx
Normal file
46
components/shared/AnimatedCounter.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
74
components/shared/Button.tsx
Normal file
74
components/shared/Button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
components/shared/CookieConsent.tsx
Normal file
57
components/shared/CookieConsent.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
38
components/shared/NewsletterSignup.tsx
Normal file
38
components/shared/NewsletterSignup.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
33
components/shared/SectionHeader.tsx
Normal file
33
components/shared/SectionHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
components/shared/WhatsAppButton.tsx
Normal file
27
components/shared/WhatsAppButton.tsx
Normal 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
387
lib/constants.ts
Normal 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: "5–6 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 5–6 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
61
lib/metadata.ts
Normal 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
162
lib/structured-data.ts
Normal 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
14
next.config.mjs
Normal 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
5998
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal 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
8
postcss.config.mjs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
BIN
public/images/first_light.jpg
Normal file
BIN
public/images/first_light.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
BIN
public/images/hero_main.jpg
Normal file
BIN
public/images/hero_main.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 544 KiB |
BIN
public/images/istanbul_map.png
Normal file
BIN
public/images/istanbul_map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9 MiB |
BIN
public/images/manifesto_coastline.jpg
Normal file
BIN
public/images/manifesto_coastline.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 497 KiB |
BIN
public/images/the_other_side.jpg
Normal file
BIN
public/images/the_other_side.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 409 KiB |
45
tailwind.config.ts
Normal file
45
tailwind.config.ts
Normal 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
26
tsconfig.json
Normal 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"]
|
||||
}
|
||||
Loading…
Reference in a new issue