diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..288e979 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,55 @@ +# 1. Base image +FROM node:20-alpine AS base + +# 2. Dependencies +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# Install dependencies based on the preferred package manager +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + + +# 3. Builder +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Environment variables must be present at build time for Next.js +# Coolify will provide these, but we can set defaults +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN npm run build + +# 4. Runner +FROM base AS runner +WORKDIR /app + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +COPY --from=builder /app/public ./public + +# Set the correct permission for prerender cache +RUN mkdir .next +RUN chown nextjs:nodejs .next + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +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 +# set hostname to localhost +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx new file mode 100644 index 0000000..62decc3 --- /dev/null +++ b/app/[locale]/contact/page.tsx @@ -0,0 +1,101 @@ +import { useTranslations } from 'next-intl'; + +export default function ContactPage() { + const t = useTranslations('Contact'); + return ( +
+
+
+ {/* Editorial Column */} +
+ {t('reservation_concierge')} +

+ {t('title1')}
+ {t('title2')} +

+
+ +
+
+

{t('headquarters')}

+

+ {t('hq_address1')}
+ {t('hq_address2')} +

+
+ +
+
+

{t('direct_line')}

+

+90 (252) 316 12 34

+
+
+

{t('digital_mail')}

+

atelier@salmakis.com

+
+
+ +
+ +
+
+
+ + {/* Minimalist Form Column */} +
+

{t('form_title')}

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+
+
+
+
+ ); +} + diff --git a/app/[locale]/fleet/[slug]/page.tsx b/app/[locale]/fleet/[slug]/page.tsx new file mode 100644 index 0000000..b1c37ff --- /dev/null +++ b/app/[locale]/fleet/[slug]/page.tsx @@ -0,0 +1,481 @@ +'use client'; + +import { yachts } from "../../../data/yachts"; +import { notFound } from "next/navigation"; +import { Link } from "@/i18n/routing"; +import { motion } from "framer-motion"; +import { use, useState, useCallback, useEffect } from "react"; +import { AnimatePresence } from "framer-motion"; +import { CldImage } from "next-cloudinary"; +import { useTranslations, useLocale } from "next-intl"; + +interface PageProps { + params: Promise<{ slug: string }>; +} + +export default function YachtPage({ params }: PageProps) { + const { slug } = use(params); + const yacht = yachts.find((y) => y.slug === slug); + const [lightboxIndex, setLightboxIndex] = useState(null); + const t = useTranslations('FleetDetail'); + const locale = useLocale(); + + const openLightbox = (index: number) => setLightboxIndex(index); + const closeLightbox = () => setLightboxIndex(null); + + const goNext = useCallback(() => { + if (lightboxIndex !== null && yacht) { + setLightboxIndex((lightboxIndex + 1) % yacht.gallery.length); + } + }, [lightboxIndex, yacht]); + + const goPrev = useCallback(() => { + if (lightboxIndex !== null && yacht) { + setLightboxIndex((lightboxIndex - 1 + yacht.gallery.length) % yacht.gallery.length); + } + }, [lightboxIndex, yacht]); + + useEffect(() => { + const handleKey = (e: KeyboardEvent) => { + if (lightboxIndex === null) return; + if (e.key === 'Escape') closeLightbox(); + if (e.key === 'ArrowRight') goNext(); + if (e.key === 'ArrowLeft') goPrev(); + }; + window.addEventListener('keydown', handleKey); + return () => window.removeEventListener('keydown', handleKey); + }, [lightboxIndex, goNext, goPrev]); + + if (!yacht) { + notFound(); + } + + const fadeInUp = { + hidden: { opacity: 0, y: 40 }, + visible: { opacity: 1, y: 0, transition: { duration: 0.8, ease: "easeOut" } } + }; + + const staggerContainer = { + hidden: { opacity: 0 }, + visible: { opacity: 1, transition: { staggerChildren: 0.15 } } + }; + + return ( +
+ {/* Editorial Hero Section */} +
+ + + + +
+ + + The Fleet Collection + + + {yacht.name} + + + {yacht.tagline} + + + + + Explore Specifications +
+
+
+
+ + {/* Technical Atelier Bar */} +
+
+
+
+ straighten + {t('length')} + {yacht.length} +
+
+ groups + {t('guests')} + {yacht.guests} +
+
+ bed + {t('cabins')} + {yacht.cabins} +
+
+ support_agent + {t('crew')} + {yacht.crew} +
+
+ speed + {t('speed')} + {yacht.speed} +
+
+
+
+ + {/* Editorial Content Section */} +
+ + {/* Intro Block - Full Width Centered */} + + + {t('design_engineering')} + + + {t('sanctuary_title1')}
+ {t('sanctuary_title2')} +
+ + + +
+ {t('builder')} + {yacht.builder} +
+
+
+ {t('year_refit')} + {yacht.year} {yacht.refitYear && `(${yacht.refitYear})`} +
+
+ + + {locale === 'tr' && yacht.description_tr ? yacht.description_tr : yacht.description} + + + + + {t('inquire_btn')} + + +
+ + {/* Specifications Grid - Modern Cards */} +
+ + {/* 1. Construction & Design */} + {(yacht.construction || yacht.furniture) && ( + + + 01 +

{t('design_construction').replace(/0\d\s\/\s/, '')}

+
+
+ +
+ {yacht.construction && Object.entries(yacht.construction).map(([key, value]) => ( + + + {key.replace(/([A-Z])/g, ' $1').trim()} + + + {value} + + + ))} +
+ + +
+ )} + + {/* 2. Equipment on Board */} + {yacht.equipment && yacht.equipment.length > 0 && ( + + + 02 +

{t('tech_equipment').replace(/0\d\s\/\s/, '')}

+
+
+ +
+ {yacht.equipment.map((item, idx) => ( + +
+ check +
+ {item} +
+ ))} +
+
+ )} + + {/* 3. Tenders & Toys */} + {yacht.watersports && yacht.watersports.length > 0 && ( + + + 03 +

{t('water_toys').replace(/0\d\s\/\s/, '')}

+
+
+ +
+ {yacht.watersports.map((item, idx) => ( + +
+ anchor + {item} +
+
+ ))} +
+
+ )} + +
+
+ + {/* Atelier Gallery Section */} + {yacht.gallery && yacht.gallery.length > 0 && ( +
+
+ {t('atelier_experience')} +

+ {t('gallery')} +

+
+ + + {yacht.gallery.map((imgUrl, index) => { + const isLarge = index === 0; + return ( + openLightbox(index)} + className={`relative overflow-hidden group cursor-pointer ${isLarge ? 'md:col-span-2 lg:col-span-2 row-span-2' : 'col-span-1 row-span-1'}`} + > + +
+ zoom_in +
+
+ ); + })} +
+
+ )} + + {/* Charter Rates Section */} + {yacht.prices && yacht.prices.length > 0 && ( +
+
+ RATES +
+ + +
+ + Charter Investment + + + {t('rates')} + + + {t('rates_desc')} + +
+ +
+ {yacht.prices.map((p) => ( + + + {p.month} + + + {p.price} + + + ))} +
+ + +

+ * {t('apa_note')}
+ * {t('vat_note')} +

+
+
+
+ )} + + {/* Editorial Navigation */} +
+ + + {t('journey_title')} + + + {t('ready_title')} + + + + {t('start_inquiry_btn')} + + + {t('explore_btn')} + + + +
+ + {/* Lightbox Overlay */} + + {lightboxIndex !== null && yacht.gallery && ( + + {/* Close Button */} + + + {/* Image Counter */} +
+ {lightboxIndex + 1} / {yacht.gallery.length} +
+ + {/* Previous Arrow */} + + + {/* Next Arrow */} + + + {/* Main Image */} + e.stopPropagation()} + > + + +
+ )} +
+
+ ); +} + diff --git a/app/[locale]/fleet/page.tsx b/app/[locale]/fleet/page.tsx new file mode 100644 index 0000000..0d9b6b1 --- /dev/null +++ b/app/[locale]/fleet/page.tsx @@ -0,0 +1,76 @@ +import { yachts } from "../../data/yachts"; +import { Link } from "@/i18n/routing"; +import { useTranslations, useLocale } from "next-intl"; +import { CldImage } from "next-cloudinary"; + +export default function FleetPage() { + const t = useTranslations('FleetList'); + const locale = useLocale(); + return ( +
+
+
+
+ {t('collection')} +

+ {t('title1')}
+ {t('title2')} +

+
+

+ {t('description')} +

+
+
+
+ {t('bg_text')} +
+
+
+ +
+ {yachts.map((yacht, index) => ( +
+
+ +
+
+
+ {t('masterpiece')}{index + 1} +

{yacht.name}

+

+ {locale === 'tr' && yacht.description_tr ? yacht.description_tr : yacht.description} +

+
+
+ straighten + {t('length')} + {yacht.length} +
+
+ groups + {t('guests')} + {yacht.guests} {t('guests_suffix')} +
+
+ + {t('discover')} + +
+
+
+
+ ))} +
+
+
+ ); +} + diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..22458be --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,55 @@ +import type { Metadata } from "next"; +import { Inter, Oswald } from "next/font/google"; +import "../globals.css"; +import Navbar from "../components/Navbar"; +import Footer from "../components/Footer"; +import {NextIntlClientProvider} from 'next-intl'; +import {getMessages} from 'next-intl/server'; + +const inter = Inter({ + variable: "--font-inter", + subsets: ["latin"], +}); + +const oswald = Oswald({ + variable: "--font-oswald", + subsets: ["latin", "latin-ext"], +}); + +export const metadata: Metadata = { + title: "Salmakis Yachting | Luxury Yacht Charters in Bodrum & Aegean", + description: "Experience the pinnacle of Aegean luxury with Salmakis Yachting. Premium yacht charters since 1980.", + keywords: ["yacht charter", "bodrum", "luxury yacht", "meira", "salmakis yachting"], +}; + +export default async function RootLayout({ + children, + params +}: { + children: React.ReactNode; + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const messages = await getMessages(); + + return ( + + + + + + + +
+ {children} +
+