diff --git a/README.md b/README.md index e215bc4..1ff4eca 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,65 @@ -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). +# Kozmos Beach & More + +**"Denizin Ormana Kıyısında" / "Where the Sea Meets the Forest"** + +Kozmos Beach & More is a modern, responsive, and bilingual (Turkish & English) corporate website built with Next.js 16 (App Router). It features a mobile-first design, a seamless single-page scrolling experience, and dedicated pages like Accommodation, all wrapped in a boho-chic, Aegean-inspired aesthetic. + +## Features + +- **Next.js 16 (App Router):** Fast, SEO-friendly, and modern architecture. +- **i18n Support:** Fully bilingual (TR/EN) using `next-intl`. +- **Styling:** Tailwind CSS v4 with custom gradients, colors, and an elegant typography system (Marcellus). +- **Animations:** Smooth micro-interactions and scroll animations powered by `framer-motion`. +- **Responsive Design:** Mobile-first approach, ensuring a flawless experience across all devices. +- **Lightbox Gallery:** Custom-built image gallery with a full-screen lightbox feature. +- **PWA & SEO Ready:** Pre-configured `manifest.ts`, `robots.ts`, `sitemap.ts`, and metadata. + +## Tech Stack + +- **Framework:** [Next.js](https://nextjs.org/) +- **Language:** [TypeScript](https://www.typescriptlang.org/) +- **Styling:** [Tailwind CSS v4](https://tailwindcss.com/) +- **Animations:** [Framer Motion](https://www.framer.com/motion/) +- **Icons:** [Lucide React](https://lucide.dev/) +- **Internationalization:** [next-intl](https://next-intl-docs.vercel.app/) + +## Environment Variables + +To run the project locally, create a `.env.local` file with the following variables: + +```env +# Contact number for WhatsApp reservation buttons +NEXT_PUBLIC_WHATSAPP_NUMBER="905000000000" + +# URL for the QR menu (if applicable) +NEXT_PUBLIC_MENU_URL="https://qr.kozmosbeach.com" +``` ## Getting Started -First, run the development server: +1. **Install dependencies:** + ```bash + npm install + ``` -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +2. **Run the development server:** + ```bash + npm run dev + ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +3. **Build for production:** + ```bash + npm run build + npm run start + ``` -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## Development Commands -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. +- `npm run dev`: Starts the development server. +- `npm run build`: Builds the app for production. +- `npm run start`: Runs the built app in production mode. +- `npm run lint`: Runs ESLint. -## 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. +*Created by [ayris.tech](https://ayris.tech)* diff --git a/app/[locale]/accommodation/page.tsx b/app/[locale]/accommodation/page.tsx new file mode 100644 index 0000000..742e4bf --- /dev/null +++ b/app/[locale]/accommodation/page.tsx @@ -0,0 +1,24 @@ +import Navbar from '@/components/Navbar'; +import Accommodation from '@/components/Accommodation'; +import Footer from '@/components/Footer'; +import WhatsAppButton from '@/components/WhatsAppButton'; + +export default async function AccommodationPage({ + params +}: { + params: Promise<{ locale: string }> +}) { + const { locale } = await params; + + return ( +
+ + {/* Spacer to push content below fixed navbar */} +
+ +
+
+ ); +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..bed88d9 --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,47 @@ +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages } from 'next-intl/server'; +import { notFound } from 'next/navigation'; +import { routing } from '@/i18n/routing'; +import { Marcellus } from 'next/font/google'; +import '../globals.css'; + +const marcellus = Marcellus({ + subsets: ['latin'], + variable: '--font-marcellus', + weight: ['400'], +}); + +export const metadata = { + title: 'Kozmos Beach & More', + description: 'Denizin Ormana Kıyısında — Akyaka, Gökova', +}; + +export function generateStaticParams() { + return routing.locales.map((locale) => ({ locale })); +} + +export default async function LocaleLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + if (!routing.locales.includes(locale as any)) { + notFound(); + } + + const messages = await getMessages(); + + return ( + + + + {children} + + + + ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..93ab098 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,35 @@ +import Navbar from '@/components/Navbar'; +import Hero from '@/components/Hero'; +import About from '@/components/About'; +import Accommodation from '@/components/Accommodation'; +import Beach from '@/components/Beach'; +import Dining from '@/components/Dining'; +import Events from '@/components/Events'; +import Gallery from '@/components/Gallery'; +import Contact from '@/components/Contact'; +import Footer from '@/components/Footer'; +import WhatsAppButton from '@/components/WhatsAppButton'; + +export default async function Page({ + params +}: { + params: Promise<{ locale: string }> +}) { + const { locale } = await params; + + return ( +
+ + + + + + + + + +
+ ); +} diff --git a/app/globals.css b/app/globals.css index a2dc41e..545e0db 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,157 @@ @import "tailwindcss"; -:root { - --background: #ffffff; - --foreground: #171717; +@theme { + --color-sand: #FFF8EE; + --color-sandy: #F5E6CC; + --color-coral: #FF5A36; + --color-coral-light: #FF7A5A; + --color-amber: #FFA827; + --color-gold: #FFD166; + --color-aqua: #00BFA5; + --color-cyan: #00E5FF; + --color-midnight: #080D18; + --color-deep: #0E1B30; + --color-navy: #1A2F4A; + + /* backward compat */ + --color-cream: #FFF8EE; + --color-beige: #F5E6CC; + --color-turquoise: #00BFA5; + --color-forest: #0E1B30; + --color-dark-brown: #080D18; + + --font-heading: var(--font-marcellus); + --font-body: var(--font-marcellus); } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} +@layer base { + body { + @apply bg-sand text-midnight font-body antialiased; + } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; + h1, h2, h3, h4, h5, h6 { + @apply font-heading; } } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +html { + scroll-behavior: smooth; +} + +/* ── CSS custom property for animated border ──── */ +@property --border-angle { + syntax: ''; + initial-value: 0deg; + inherits: false; +} + +/* ── Animated conic border classes ─────────────── */ +.border-spin-cyan { + background: + linear-gradient(#0E1B30, #0E1B30) padding-box, + conic-gradient(from var(--border-angle), #00E5FF 0deg, transparent 70deg, transparent 290deg, #00E5FF 360deg) border-box; + border: 1px solid transparent; + animation: border-rotate 3s linear infinite; +} + +.border-spin-coral { + background: + linear-gradient(#0E1B30, #0E1B30) padding-box, + conic-gradient(from var(--border-angle), #FF5A36 0deg, transparent 70deg, transparent 290deg, #FF5A36 360deg) border-box; + border: 1px solid transparent; + animation: border-rotate 4s linear infinite; +} + +.border-spin-amber { + background: + linear-gradient(#0E1B30, #0E1B30) padding-box, + conic-gradient(from var(--border-angle), #FFA827 0deg, transparent 70deg, transparent 290deg, #FFA827 360deg) border-box; + border: 1px solid transparent; + animation: border-rotate 5s linear infinite; +} + +/* ── Keyframes ─────────────────────────────────── */ + +@keyframes shimmer { + 0% { background-position: -400% center; } + 100% { background-position: 400% center; } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-16px); } +} + +@keyframes border-rotate { + to { --border-angle: 360deg; } +} + +@keyframes eq-bar { + 0%, 100% { transform: scaleY(0.3); } + 50% { transform: scaleY(1); } +} + +@keyframes grain { + 0%, 100% { transform: translate(0, 0 ); } + 10% { transform: translate(-5%, -5% ); } + 20% { transform: translate(-10%, 5% ); } + 30% { transform: translate(5%, -10%); } + 40% { transform: translate(-5%, 15%); } + 50% { transform: translate(-10%, 5%); } + 60% { transform: translate(15%, 0% ); } + 70% { transform: translate(0%, 10%); } + 80% { transform: translate(-15%, 0% ); } + 90% { transform: translate(10%, 5%); } +} + +/* ── Utility classes ───────────────────────────── */ + +.shimmer-text { + background: linear-gradient( + 90deg, + #ffffff 0%, + #ffffff 35%, + #FFD166 45%, + #FF5A36 50%, + #FFD166 55%, + #ffffff 65%, + #ffffff 100% + ); + background-size: 400% auto; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: shimmer 4s linear infinite; +} + +.gradient-text { + background: linear-gradient(135deg, #FF5A36, #FFA827, #00BFA5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gradient-text-warm { + background: linear-gradient(135deg, #FF5A36, #FFA827); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.gradient-text-cool { + background: linear-gradient(135deg, #00E5FF, #00BFA5); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.grain-overlay::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.045; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.75' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + animation: grain 8s steps(10) infinite; } diff --git a/app/layout.tsx b/app/layout.tsx deleted file mode 100644 index 976eb90..0000000 --- a/app/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; - -const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], -}); - -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], -}); - -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - -export default function RootLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( - - {children} - - ); -} diff --git a/app/manifest.ts b/app/manifest.ts new file mode 100644 index 0000000..ebd47e7 --- /dev/null +++ b/app/manifest.ts @@ -0,0 +1,25 @@ +import { MetadataRoute } from 'next'; + +export default function manifest(): MetadataRoute.Manifest { + return { + name: 'Kozmos Beach & More', + short_name: 'Kozmos', + description: 'Where the Sea Meets the Forest', + start_url: '/', + display: 'standalone', + background_color: '#F5F0E8', + theme_color: '#4AAFA8', + icons: [ + { + src: '/icon-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/icon-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + ], + }; +} diff --git a/app/page.tsx b/app/page.tsx deleted file mode 100644 index 3f36f7c..0000000 --- a/app/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import Image from "next/image"; - -export default function Home() { - return ( -
-
- Next.js logo -
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

-
-
- - Vercel logomark - Deploy Now - - - Documentation - -
-
-
- ); -} diff --git a/app/robots.ts b/app/robots.ts new file mode 100644 index 0000000..765f1ff --- /dev/null +++ b/app/robots.ts @@ -0,0 +1,11 @@ +import { MetadataRoute } from 'next'; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: '*', + allow: '/', + }, + sitemap: 'https://kozmosbeach.com/sitemap.xml', + }; +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 0000000..25d1083 --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,20 @@ +import { MetadataRoute } from 'next'; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://kozmosbeach.com'; + + return [ + { + url: `${baseUrl}/tr`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 1, + }, + { + url: `${baseUrl}/en`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 1, + }, + ]; +} diff --git a/components/About.tsx b/components/About.tsx new file mode 100644 index 0000000..714652e --- /dev/null +++ b/components/About.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { motion } from 'framer-motion'; +import { Waves, Leaf, Music } from 'lucide-react'; +import Image from 'next/image'; + +const features = [ + { icon: Waves, titleKey: 'card1_title', descKey: 'card1_desc', color: 'text-aqua', bg: 'bg-aqua/10' }, + { icon: Leaf, titleKey: 'card2_title', descKey: 'card2_desc', color: 'text-amber', bg: 'bg-amber/10' }, + { icon: Music, titleKey: 'card3_title', descKey: 'card3_desc', color: 'text-coral', bg: 'bg-coral/10' }, +]; + +export default function About() { + const t = useTranslations('About'); + + return ( +
+
+
+ + {/* Left content */} + +
+ + — Beach & More + +

+ {t('title')} +

+

{t('description')}

+
+ +
+ {features.map(({ icon: Icon, titleKey, descKey, color, bg }, i) => ( + +
+ +
+
+

{t(titleKey)}

+

{t(descKey)}

+
+
+ ))} +
+
+ + {/* Right image */} + + {/* Glow ring */} +
+
+ Kozmos atmosferi +
+
+ + +
+
+
+ ); +} diff --git a/components/Accommodation.tsx b/components/Accommodation.tsx new file mode 100644 index 0000000..db8c927 --- /dev/null +++ b/components/Accommodation.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { motion } from 'framer-motion'; +import Image from 'next/image'; +import { Users, Check, Sparkles } from 'lucide-react'; + +export default function Accommodation() { + const t = useTranslations('Accommodation'); + + const openWhatsApp = () => { + const phone = process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '905000000000'; + window.open(`https://wa.me/${phone}`, '_blank'); + }; + + const rooms = [ + { + id: 'standard', + image: 'https://picsum.photos/600/400?random=3', + capacity: 2, + features: t.raw('rooms.standard.features') as string[], + featured: false, + gradient: 'linear-gradient(135deg, #00BFA5, #00E5FF)', + }, + { + id: 'sea_view', + image: 'https://picsum.photos/600/400?random=4', + capacity: 2, + features: t.raw('rooms.sea_view.features') as string[], + featured: true, + gradient: 'linear-gradient(135deg, #FF5A36, #FFA827)', + }, + { + id: 'garden_suite', + image: 'https://picsum.photos/600/400?random=5', + capacity: 4, + features: t.raw('rooms.garden_suite.features') as string[], + featured: false, + gradient: 'linear-gradient(135deg, #00BFA5, #00E5FF)', + }, + ]; + + return ( +
+
+ +
+ + — Konaklama + + + {t('title')} + + + {t('description')} + +
+ +
+ {rooms.map((room, index) => ( + + {/* Featured badge */} + {room.featured && ( +
+ + FEATURED +
+ )} + + {/* Image */} +
+ {t(`rooms.${room.id}.name`)} +
+
+ + {/* Content */} +
+

+ {t(`rooms.${room.id}.name`)} +

+ +
+ + + {t('capacity')} {room.capacity} {t('person')} + +
+ +
    + {room.features.map((feature, i) => ( +
  • + + {feature} +
  • + ))} +
+ + +
+ + ))} +
+
+
+ ); +} diff --git a/components/Beach.tsx b/components/Beach.tsx new file mode 100644 index 0000000..bd6a1ee --- /dev/null +++ b/components/Beach.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { motion } from 'framer-motion'; +import Image from 'next/image'; +import { Sun } from 'lucide-react'; + +export default function Beach() { + const t = useTranslations('Beach'); + + const openWhatsApp = () => { + const phone = process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '905000000000'; + window.open( + `https://wa.me/${phone}?text=Merhaba,%20şezlong%20rezervasyonu%20yaptırmak%20istiyorum.`, + '_blank', + ); + }; + + const stats = [ + { value: '∞', label: 'Şezlong' }, + { value: '7/7', label: 'Açık' }, + { value: 'VIP', label: 'Cabana' }, + ]; + + return ( +
+
+
+ + {/* Left masonry photos */} +
+ +
+ Plaj +
+
+ Şezlong +
+
+ + +
+ Deniz +
+
+ Koy +
+
+
+ + {/* Right content */} + +
+ + Beach Club +
+ +

+ {t('title')} +

+ +

{t('description')}

+ + {/* Stats */} +
+ {stats.map(({ value, label }) => ( +
+
{value}
+
{label}
+
+ ))} +
+ + {/* Info & CTA */} +
+

{t('reservation_info')}

+ +
+
+ +
+
+ + {/* Wave → midnight */} +
+ + + +
+
+ ); +} diff --git a/components/Contact.tsx b/components/Contact.tsx new file mode 100644 index 0000000..bc616ec --- /dev/null +++ b/components/Contact.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useTranslations } from 'next-intl'; +import { motion } from 'framer-motion'; +import { MapPin, Phone, Send } from 'lucide-react'; +import { Instagram } from '@/components/InstagramIcon'; +import { useState } from 'react'; + +export default function Contact() { + const t = useTranslations('Contact'); + const [formData, setFormData] = useState({ name: '', date: '', guests: '', message: '' }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const phone = process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '905000000000'; + const text = `*Yeni Rezervasyon Talebi*%0A%0A*Ad Soyad:* ${formData.name}%0A*Tarih:* ${formData.date}%0A*Kişi Sayısı:* ${formData.guests}%0A*Mesaj:* ${formData.message}`; + window.open(`https://wa.me/${phone}?text=${text}`, '_blank'); + }; + + const handleChange = (e: React.ChangeEvent) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const contactItems = [ + { + Icon: MapPin, + label: 'Adres', + value: t('address'), + color: 'text-coral', + bg: 'bg-coral/10', + }, + { + Icon: Phone, + label: 'Telefon / WhatsApp', + value: `+${process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '90 500 000 00 00'}`, + color: 'text-aqua', + bg: 'bg-aqua/10', + }, + { + Icon: Instagram, + label: 'Instagram', + value: '@kozmosbeach', + href: 'https://www.instagram.com/kozmosbeach/', + color: 'text-amber', + bg: 'bg-amber/10', + }, + ]; + + const inputClass = + 'w-full bg-sand border border-sandy rounded-xl px-4 py-3 text-sm focus:outline-none focus:border-coral focus:ring-1 focus:ring-coral transition-all placeholder:text-midnight/30'; + + return ( +
+
+ + + + — Bize Ulaşın + +

+ {t('title')} +

+
+ +
+ + {/* Left: info + form */} + + {/* Contact info */} +
+ {contactItems.map(({ Icon, label, value, href, color, bg }) => ( +
+
+ +
+
+

{label}

+ {href ? ( + + {value} + + ) : ( +

{value}

+ )} +
+
+ ))} +
+ + {/* Form */} +
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ +