Ready for production deployment with Dockerfile and i18n support

This commit is contained in:
2026-04-13 12:57:52 +03:00
parent 8346812507
commit b30376aa1d
32 changed files with 1078 additions and 117 deletions

55
Dockerfile Normal file
View File

@@ -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"]

31
app/[lang]/layout.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { Oswald } from "next/font/google";
import "../globals.css";
import { getDictionary } from "../dictionaries";
const oswald = Oswald({
subsets: ["latin", "latin-ext"],
variable: "--font-oswald",
});
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const resolvedParams = await params;
return {
title: "Salmakis Group | Luxury Resort, Villas & Yachting",
description: "Salmakis Group Gateway",
};
}
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: Promise<{ lang: string }>;
}>) {
const resolvedParams = await params;
return (
<html lang={resolvedParams.lang} className={`${oswald.variable}`}>
<body>{children}</body>
</html>
);
}

18
app/[lang]/page.tsx Normal file
View File

@@ -0,0 +1,18 @@
import HeroSplit from "../components/HeroSplit";
import AboutLegend from "../components/AboutLegend";
import Footer from "../components/Footer";
import { getDictionary } from "../dictionaries";
export default async function Home({ params }: { params: Promise<{ lang: string }> }) {
const resolvedParams = await params;
const lang = resolvedParams.lang as "en" | "tr";
const dict = await getDictionary(lang);
return (
<main>
<HeroSplit dict={dict} currentLang={lang} />
<AboutLegend dict={dict} />
<Footer dict={dict} />
</main>
);
}

View File

@@ -0,0 +1,130 @@
.aboutSection {
padding: 8rem 2rem;
background-color: var(--primary-white);
color: var(--text-dark);
text-align: center;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.sinceBadge {
font-size: 0.85rem;
font-weight: 500;
letter-spacing: 0.2em;
text-transform: uppercase;
color: var(--gold);
margin-bottom: 2rem;
}
.heading {
font-size: 2.5rem;
letter-spacing: 0.05em;
margin-bottom: 3rem;
color: var(--navy);
}
.content {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.paragraph {
font-size: 1.1rem;
line-height: 1.8;
color: #555;
}
.bottomBadge {
margin-top: 3rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.yachtingTitle {
font-family: var(--font-heading);
font-size: 1.8rem;
color: var(--navy);
letter-spacing: 0.1em;
}
/* Legend Section */
.legendSection {
position: relative;
height: 80vh;
width: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.parallaxBg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('/2.jpg');
background-attachment: fixed;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
}
.glassCard {
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
padding: 4rem;
max-width: 600px;
text-align: center;
color: var(--primary-white);
border-radius: 8px;
}
.legendTitle {
font-size: 2.2rem;
margin-bottom: 1.5rem;
letter-spacing: 0.05em;
font-weight: 300;
}
.legendText {
font-size: 1.05rem;
line-height: 1.8;
margin-bottom: 2rem;
font-weight: 300;
}
.goldLine {
width: 60px;
height: 2px;
background-color: var(--gold);
margin: 0 auto;
}
@media (max-width: 768px) {
.aboutSection {
padding: 5rem 1.5rem;
}
.glassCard {
padding: 2rem;
margin: 1rem;
}
.parallaxBg {
background-attachment: scroll;
}
}

View File

@@ -0,0 +1,38 @@
import styles from "./AboutLegend.module.css";
export default function AboutLegend({ dict }: { dict: any }) {
return (
<>
<section className={styles.aboutSection} id="about">
<div className={styles.container}>
<h2 className={styles.heading}>{dict.about.title}</h2>
<div className={styles.content}>
<p className={styles.paragraph}>{dict.about.p1}</p>
<p className={styles.paragraph}>{dict.about.p2}</p>
</div>
<div className={styles.bottomBadge}>
<h3 className={styles.yachtingTitle}>SALMAKIS</h3>
<p className={styles.sinceBadge}>{dict.about.since}</p>
</div>
</div>
</section>
<section className={styles.legendSection} id="legend">
{/* Parallax / minimal illustration container */}
<div className={styles.parallaxBg}>
<div className={styles.glassCard}>
<h3 className={styles.legendTitle}>{dict.about.company}</h3>
<div className={styles.legendText}>
<p><strong>SALMAKIS TURIZM YATIRIM VE TICARET ANONIM SIRKETI</strong></p>
<p>Adres: Kumbahce Mahallesi Icmeler Caddesi No: 28/1 Bodrum / MUGLA / TURKEY</p>
<p>Vergi Dairesi: BODRUM</p>
<p>Vergi No: 741 003 6900</p>
<p>Mersis No: 0741 0036 9000 0011</p>
</div>
<div className={styles.goldLine}></div>
</div>
</div>
</section>
</>
);
}

View File

@@ -0,0 +1,124 @@
.footer {
background-color: var(--text-dark);
color: var(--primary-white);
padding: 6rem 2rem 2rem 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 4rem;
}
.brandCol {
text-align: center;
}
.logo {
font-family: var(--font-heading);
font-size: 2rem;
letter-spacing: 0.2em;
margin-bottom: 0.5rem;
color: var(--gold);
}
.tagline {
font-size: 0.9rem;
letter-spacing: 0.1em;
opacity: 0.6;
text-transform: uppercase;
margin-bottom: 1.5rem;
}
.generalContact {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
font-size: 0.95rem;
}
.contactText {
opacity: 0.8;
}
.gridContainer {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
text-align: center;
}
.colTitle {
font-family: var(--font-heading);
font-size: 1.25rem;
color: var(--sand-beige);
margin-bottom: 1rem;
letter-spacing: 0.05em;
}
.address {
font-size: 0.95rem;
opacity: 0.8;
margin-bottom: 0.5rem;
}
.contactLink {
display: block;
font-size: 0.95rem;
opacity: 0.8;
margin-bottom: 0.25rem;
transition: opacity 0.3s ease;
}
.contactLink:hover {
opacity: 1;
color: var(--gold);
}
.bottomBar {
max-width: 1200px;
margin: 4rem auto 0 auto;
padding-top: 2rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
}
.copyright {
margin: 0;
}
.signature {
letter-spacing: 0.05em;
}
.ayrisLink {
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
transition: color 0.3s ease;
text-decoration: underline;
text-underline-offset: 4px;
}
.ayrisLink:hover {
color: var(--gold);
}
@media (max-width: 768px) {
.gridContainer {
grid-template-columns: 1fr;
gap: 3rem;
}
.bottomBar {
flex-direction: column;
gap: 1rem;
text-align: center;
}
}

69
app/components/Footer.tsx Normal file
View File

@@ -0,0 +1,69 @@
import styles from "./Footer.module.css";
export default function Footer({ dict }: { dict: any }) {
return (
<footer className={styles.footer} id="contact">
<div className={styles.container}>
{/* Brand Column */}
<div className={styles.brandCol}>
<h2 className={styles.logo}>SALMAKIS</h2>
<p className={styles.tagline}>{dict.footer.tagline}</p>
<div className={styles.generalContact}>
<a href="tel:+902523166506" className={styles.contactLink}>+90 252 316 65 06</a>
<span className={styles.contactText}>08:00 20:00</span>
<a href="mailto:salmakis@salmakis.com.tr" className={styles.contactLink}>salmakis@salmakis.com.tr</a>
</div>
</div>
{/* Divisions Column */}
<div className={styles.gridContainer}>
<div className={styles.col}>
<h4 className={styles.colTitle}>Salmakis Resort</h4>
<p className={styles.address}>
Bardakci Koyu<br/>
BODRUM/MUĞLA/TURKEY
</p>
<a href="tel:+902523166506" className={styles.contactLink}>+90 252 316 65 06</a>
<a href="tel:+902523166507" className={styles.contactLink}>+90 252 316 65 07</a>
<a href="tel:+902523166511" className={styles.contactLink}>+90 252 316 65 11</a>
<a href="mailto:salmakis@salmakis.com.tr" className={styles.contactLink}>salmakis@salmakis.com.tr</a>
</div>
<div className={styles.col}>
<h4 className={styles.colTitle}>Salmakis Villas</h4>
<p className={styles.address}>
Bademlik Mevkii<br/>
Kume Evleri No 24<br/>
Bodrum/MUGLA/TURKEY
</p>
<a href="tel:+902523162738" className={styles.contactLink}>+90 252 316 27 38</a>
<a href="tel:+902523162877" className={styles.contactLink}>+90 252 316 28 77</a>
<a href="tel:+905327317804" className={styles.contactLink}>+90 532 731 78 04</a>
<a href="tel:+902523162737" className={styles.contactLink}>+90 252 316 27 37</a>
<a href="mailto:info@salmakisvillas.com" className={styles.contactLink}>info@salmakisvillas.com</a>
</div>
<div className={styles.col}>
<h4 className={styles.colTitle}>Salmakis Yachting</h4>
<p className={styles.address}>
Kumbahce Mah. Icmeler Cad.<br/>
No 28/1<br/>
BODRUM/MUGLA/TURKEY
</p>
<a href="tel:+902523162738" className={styles.contactLink}>+90 252 316 27 38</a>
<a href="tel:+902523162877" className={styles.contactLink}>+90 252 316 28 77</a>
<a href="tel:+902523162737" className={styles.contactLink}>+90 252 316 27 37</a>
<a href="mailto:info@salmakisyachting.com" className={styles.contactLink}>info@salmakisyachting.com</a>
</div>
</div>
</div>
<div className={styles.bottomBar}>
<p className={styles.copyright}>&copy; {new Date().getFullYear()} {dict.footer.copyright}</p>
<p className={styles.signature}>
{dict.footer.created_by} <a href="https://ayris.tech" target="_blank" rel="noreferrer" className={styles.ayrisLink}>AYRISTECH</a>
</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,255 @@
.heroContainer {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: var(--text-dark);
}
.navOverlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 2rem 4rem;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
color: var(--primary-white);
}
.logo {
font-family: var(--font-heading);
font-size: 1.5rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.navLinks {
display: flex;
align-items: center;
gap: 1rem;
}
.langBtn {
background: none;
border: none;
color: var(--primary-white);
font-family: var(--font-text);
font-size: 0.9rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.langBtn:hover {
opacity: 1;
}
.divider {
width: 1px;
height: 16px;
background-color: rgba(255, 255, 255, 0.3);
}
.contactIcon {
margin-left: 1rem;
opacity: 0.8;
transition: opacity 0.3s ease;
}
.contactIcon:hover {
opacity: 1;
}
/* Split Pane Layout */
.splitWrapper {
display: flex;
width: 100%;
height: 100%;
}
.splitPane {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: flex 0.6s cubic-bezier(0.25, 1, 0.5, 1);
cursor: pointer;
}
.paneBg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
transform: scale(1.05);
transition: transform 10s ease;
filter: grayscale(30%);
overflow: hidden;
}
.videoIfrm {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100vw;
height: 56.25vw; /* 16:9 aspect ratio target */
min-height: 100vh;
min-width: 177.77vh; /* 16:9 aspect ratio target */
pointer-events: none;
border: none;
}
.videoPlaceholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
z-index: 2;
transition: opacity 0.4s ease;
pointer-events: none;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(0,0,0,0.6) 100%);
transition: background 0.4s ease;
}
.paneContent {
position: relative;
z-index: 10;
text-align: center;
color: var(--primary-white);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transform: translateY(20px);
transition: transform 0.6s cubic-bezier(0.25, 1, 0.5, 1);
}
.title {
font-size: 3rem;
font-weight: 300;
letter-spacing: 0.1em;
margin-bottom: 0.5rem;
text-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1rem;
font-weight: 300;
letter-spacing: 0.15em;
text-transform: uppercase;
opacity: 0.8;
margin-bottom: 2rem;
}
.exploreBtn {
background: transparent;
color: var(--primary-white);
border: 1px solid var(--primary-white);
padding: 0.75rem 2rem;
font-size: 0.9rem;
font-family: var(--font-text);
letter-spacing: 0.1em;
cursor: pointer;
opacity: 0;
transform: translateY(10px);
transition: all 0.4s ease;
}
.socialsWrapper {
display: flex;
gap: 1.25rem;
margin-top: 1.5rem;
opacity: 0;
transform: translateY(10px);
transition: all 0.4s ease;
transition-delay: 0.1s; /* appears slightly after the button */
}
.socialLink {
color: var(--primary-white);
transition: color 0.3s ease, transform 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}
.socialLink:hover {
color: var(--gold);
transform: translateY(-2px);
}
/* Hover States */
.splitWrapper:hover .splitPane {
flex: 0.5;
}
.splitWrapper .splitPane:hover {
flex: 3;
}
.splitPane:hover .videoPlaceholder {
opacity: 0;
}
.splitPane:hover .paneBg {
transform: scale(1.0);
filter: grayscale(0%);
}
.splitPane:hover .overlay {
background: linear-gradient(to bottom, rgba(0,0,0,0.1) 0%, rgba(0,0,0,0.4) 100%);
}
.splitPane:hover .paneContent {
transform: translateY(0);
}
.splitPane:hover .exploreBtn {
opacity: 1;
transform: translateY(0);
background: var(--primary-white);
color: var(--text-dark);
}
.splitPane:hover .socialsWrapper {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 768px) {
.splitWrapper {
flex-direction: column;
}
.splitPane:hover {
flex: 2;
}
.navOverlay {
padding: 1rem 2rem;
}
.title {
font-size: 2rem;
}
}

View File

@@ -0,0 +1,125 @@
"use client";
import styles from "./HeroSplit.module.css";
import Link from "next/link";
import { heroSectionsData } from "../data/hero-sections";
export default function HeroSplit({ dict, currentLang }: { dict: any; currentLang: string }) {
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>, videoId?: string) => {
if (videoId) {
const iframe = e.currentTarget.querySelector('iframe');
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
}
}
};
const handleMouseLeave = (e: React.MouseEvent<HTMLDivElement>, videoId?: string) => {
if (videoId) {
const iframe = e.currentTarget.querySelector('iframe');
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
}
}
};
const mapDataToDict = heroSectionsData.map(item => {
// @ts-ignore
const translation = dict.sections[item.key] || {};
return {
...item,
title: translation.title || item.title,
subtitle: translation.subtitle || item.subtitle
}
});
return (
<section className={styles.heroContainer}>
<nav className={styles.navOverlay}>
<div className={styles.logo}>SALMAKIS</div>
<div className={styles.navLinks}>
<Link href="/tr" className={styles.langBtn} style={{ opacity: currentLang === 'tr' ? 1 : 0.5 }}>TR</Link>
<span className={styles.divider}></span>
<Link href="/en" className={styles.langBtn} style={{ opacity: currentLang === 'en' ? 1 : 0.5 }}>EN</Link>
<Link href="#contact" className={styles.contactIcon}>
<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</Link>
</div>
</nav>
<div className={styles.splitWrapper}>
{mapDataToDict.map((section, index) => (
<div
key={index}
className={styles.splitPane}
onMouseEnter={(e) => handleMouseEnter(e, section.videoId)}
onMouseLeave={(e) => handleMouseLeave(e, section.videoId)}
>
{section.videoId ? (
<div className={styles.paneBg}>
{/* The background overlay thumbnail ensuring video loads smoothly behind */}
<div style={{ backgroundImage: `url(${section.bgUrl})`, position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', backgroundSize: 'cover', backgroundPosition: 'center', zIndex: 0 }}></div>
<iframe
className={styles.videoIfrm}
src={`https://www.youtube.com/embed/${section.videoId}?autoplay=0&mute=1&controls=0&loop=1&playlist=${section.videoId}&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1&vq=hd1080`}
allow="autoplay; encrypted-media"
frameBorder="0"
></iframe>
{/* The background overlay thumbnail ensuring YouTube UI is hidden until hover */}
<div
className={styles.videoPlaceholder}
style={{ backgroundImage: `url(${section.bgUrl})` }}
></div>
</div>
) : (
<div
className={styles.paneBg}
style={{ backgroundImage: `url(${section.bgUrl})` }}
></div>
)}
<div className={styles.overlay}></div>
<div className={styles.paneContent}>
<h2 className={styles.title}>{section.title}</h2>
<p className={styles.subtitle}>{section.subtitle}</p>
<a href={section.link} target={section.link.startsWith('#') ? '_self' : '_blank'} rel="noreferrer" className={styles.exploreBtn}>
{dict.nav.discover}
</a>
<div className={styles.socialsWrapper}>
{section.socials.instagram && (
<a href={section.socials.instagram} target="_blank" rel="noreferrer" className={styles.socialLink}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</a>
)}
{section.socials.twitter && (
<a href={section.socials.twitter} target="_blank" rel="noreferrer" className={styles.socialLink}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path>
</svg>
</a>
)}
{section.socials.facebook && (
<a href={section.socials.facebook} target="_blank" rel="noreferrer" className={styles.socialLink}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</a>
)}
</div>
</div>
</div>
))}
</div>
</section>
);
}

36
app/data/hero-sections.ts Normal file
View File

@@ -0,0 +1,36 @@
export const heroSectionsData = [
{
title: "Resort",
subtitle: "A Timeless Escape",
bgUrl: "/1.jpg",
link: "https://www.salmakishotel.com/",
socials: {
instagram: "https://www.facebook.com/SalmakisResortSpa",
twitter: "https://x.com/SalmakisRS",
facebook: "https://www.instagram.com/salmakisspafitness/",
}
},
{
title: "Villas",
subtitle: "Private Luxury Reflections",
bgUrl: "/SU-3.jpg",
link: "https://www.salmakisvillas.com/",
socials: {
instagram: "https://www.facebook.com/salmakisvillas/",
twitter: "https://x.com/salmakisvillas",
facebook: "https://facebook.com/salmakisvillas",
}
},
{
title: "Yachting",
subtitle: "Aegean Elegance",
bgUrl: "/MEIRA-2000x1333.jpg",
videoId: "0k4s7X8EgYI",
link: "https://www.salmakisyachting.com/",
socials: {
instagram: "https://www.instagram.com/salmakisyachting/",
twitter: "https://x.com/SalmakisYacht",
facebook: "https://www.facebook.com/salmakis.yachting/",
}
},
];

31
app/dictionaries/en.json Normal file
View File

@@ -0,0 +1,31 @@
{
"nav": {
"discover": "Discover"
},
"about": {
"since": "Since 1980",
"title": "THE SALMAKIS LEGEND",
"p1": "In ancient times, in the bay known today as Bardakçı, there were a pristine lake and a beautiful nymph both named Salmakis. One day, while bathing, she saw Hermaphroditus, the son of Hermes and Aphrodite, and fell deeply in love with him.",
"p2": "However, Hermaphroditus rejected her. Devastated, Salmakis prayed to the gods to unite them forever. Taking pity on her, the gods merged their bodies into one. From that day on, Salmakis and Hermaphroditus lived eternally in a single body possessing dual nature.",
"company": "COMPANY INFORMATION"
},
"footer": {
"tagline": "A Heritage of Excellence Since 1980.",
"copyright": "Salmakis Group. All rights reserved.",
"created_by": "CREATED BY"
},
"sections": {
"resort": {
"title": "Resort",
"subtitle": "A Timeless Escape"
},
"villas": {
"title": "Villas",
"subtitle": "Private Luxury Reflections"
},
"yachting": {
"title": "Yachting",
"subtitle": "Aegean Elegance"
}
}
}

View File

@@ -0,0 +1,6 @@
export const getDictionary = async (locale: 'en' | 'tr') => {
if (locale === 'en') {
return import('./en.json').then((module) => module.default);
}
return import('./tr.json').then((module) => module.default);
};

31
app/dictionaries/tr.json Normal file
View File

@@ -0,0 +1,31 @@
{
"nav": {
"discover": "Keşfet"
},
"about": {
"since": "Since 1980",
"title": "SALMAKİS EFSANESİ",
"p1": "Eski zamanlarda, bugün Bardakçı olarak bilinen koyda, adı Salmakis olan bir göl varmış. Bu gölde kendisiyle aynı ismi taşıyan Salmakis adından güzel bir peri yaşarmış. Bir gün, Salmakis gölde yıkanırken Hermes ve Afroditin oğlu Hermafroditi görmüş ve ona aşık olmuş.",
"p2": "Ne var ki, Hermafrodit onu reddetmiş. Salmakis çok üzülmüş ve onları birleştirmeleri için tanrılara yalvarmış. Salmakise acıyan tanrılar, onları tek bir vücutta birleştirmiş. O günden sonra Salmakis ve Hermafrodit çift cinsiyete sahip tek bir vücutta yaşayıp gitmişler.",
"company": "ŞİRKET BİLGİLERİ"
},
"footer": {
"tagline": "A Heritage of Excellence Since 1980.",
"copyright": "Salmakis Group. Tüm hakları saklıdır.",
"created_by": "CREATED BY"
},
"sections": {
"resort": {
"title": "Resort",
"subtitle": "Zamansız Bir Kaçış"
},
"villas": {
"title": "Villas",
"subtitle": "Özel Lüks Yansımalar"
},
"yachting": {
"title": "Yachting",
"subtitle": "Ege Zarafeti"
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,26 +1,46 @@
@import "tailwindcss";
:root { :root {
--background: #ffffff; --primary-white: #ffffff;
--foreground: #171717; --turquoise: #008080;
--sand-beige: #d7c4a3;
--gold: #d4af37;
--navy: #000080;
--text-dark: #1a1a1a;
--text-light: #fdfdfd;
--font-heading: var(--font-oswald), sans-serif;
--font-text: var(--font-oswald), sans-serif;
} }
@theme inline { * {
--color-background: var(--background); box-sizing: border-box;
--color-foreground: var(--foreground); margin: 0;
--font-sans: var(--font-geist-sans); padding: 0;
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} }
body { body {
background: var(--background); font-family: var(--font-text);
color: var(--foreground); color: var(--text-dark);
font-family: Arial, Helvetica, sans-serif; background-color: var(--primary-white);
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 400;
}
a {
text-decoration: none;
color: inherit;
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 1s ease-out forwards;
} }

View File

@@ -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 (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
</html>
);
}

View File

@@ -1,65 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
}

55
docs/prd.md Normal file
View File

@@ -0,0 +1,55 @@
BÖLÜM 1: Ürün Gereksinim Dokümanı (PRD)
Proje Adı: Salmakis Group Kurumsal Gateway Sitesi Modernizasyonu
Sürüm: 1.0 (Taslak)
Tarih: 24 Mayıs 2024
Hedef Kitle: Global ve Yerel Yatırımcılar, Lüks Turizm Müşterileri, İş Ortakları.
1. Yönetici Özeti (Executive Summary)
Mevcut salmakis.com.tr sitesi, grubun üç ana iş kolunu (Resort, Villas, Yachting) barındıran tek sayfalık bir yönlendirme sitesidir. Ancak, 2019 yılından kalma tasarımı, düşük görsel kalitesi ve zayıf kullanıcı deneyimi nedeniyle grubun 1980'den gelen köklü ve lüks marka imajını yansıtmamaktadır.
Bu projenin amacı; siteyi "Dijital Prestij Vitrini" olarak yeniden tasarlamak, ziyaretçileri etkilemek ve alt markalara (Resort, Villas, Yachting) geçişi pürüzsüz ve davetkar hale getirmektir.
2. Proje Hedefleri (Goals)
Marka İmajını Güçlendirmek: Grubun lüks, köklü (Since 1980) ve Bodrum kökenli kimliğini vurgulamak.
Kullanıcı Deneyimini (UX) Mükemmelleştirmek: Karmaşadan uzak, sezgisel ve etkileşimli bir yönlendirme sağlamak.
Görsel Mükemmeliyet: Yüksek çözünürlüklü medya kullanarak ziyaretçide anında hayranlık uyandırmak.
Teknik Performans: Mobil öncelikli (Mobile-First), Google PageSpeed skorları 90+ olan, ultra hızlı bir site oluşturmak.
3. Kullanıcı Hikayeleri (User Stories)
Bir Müşteri Olarak: Salmakis Group sitesine girdiğimde, grubun kalitesini hissetmek ve Otel mi, Villa mı yoksa Tekne mi kiralayacağıma saniyeler içinde karar verip ilgili siteye gitmek istiyorum.
Bir İş Ortağı Olarak: Grubun ana sitesini ziyaret ettiğimde, 1980'den beri faal olan güvenilir bir holding imajı görmek istiyorum.
4. Fonksiyonel Gereksinimler (Functional Requirements)
4.1. Ana Sayfa Yapısı (Gateway)
Bölünmüş Ekran (Split Screen) veya İnteraktif Slider: Ekran, üç iş kolunu temsil eden üç ana dikey bölüme ayrılmalıdır (Resort, Villas, Yachting).
Dinamik Arka Plan: Her bölümün arkasında, o iş koluna ait yüksek kaliteli (4K) ağır çekim videolar (Cinemagraph) sessizce oynamalıdır.
Hover Etkileşimi: Kullanıcı bir bölümün (örneğin Yachting) üzerine geldiğinde, o bölüm genişlemeli, görsel daha belirginleşmeli ve "Keşfet" butonu ortaya çıkmalıdır.
Minimal Navigasyon: Sadece Dil Seçimi (TR/EN) ve Kurumsal İletişim için minimal ikonlar bulunmalıdır.
4.2. İçerik Bölümleri (Scroll ile Erişilen)
Since 1980 & Hakkımızda: Ana görselin hemen altında, grubun köklü geçmişini anlatan minimal, prestijli bir metin bölümü.
Salmakis Efsanesi (İnteraktif): Efsane, düz metin yerine, kaydırma (scroll) ile tetiklenen parallax efektleri veya özel minimal illüstrasyonlarla anlatılmalıdır.
Footer (İletişim): Tüm alt şirketlerin iletişim bilgilerinin modern bir hiyerarşiyle sunulduğu, harita entegrasyonlu minimal bir footer.
4.3. Teknik Gereksinimler
Altyapı: Modern JavaScript framework'leri (örneğin Next.js veya Nuxt.js) ile Server-Side Rendering (SSR) yapısı.
Performans: Görseller WebP/AVIF formatında olmalı, videolar lazy-load edilmelidir.
Responsive: Masaüstü, tablet ve mobil cihazlarda kusursuz görüntüleme (özellikle mobil hover deneyimi dokunmatik öncelikli kurgulanmalı).
5. Tasarım İlkeleri (Design Principles)
Lüks Minimalizm: Az içerik, çok boşluk (whitespaces), devasa görseller.
Bodrum Renkleri: Saf beyaz, turkuaz mavisi, kum beji ve prestij için altın/lacivert detaylar.
Tipografi: Serif (başlıklar için, örneğin Playfair Display) ve Sans-Serif (metinler için, örneğin Montserrat) fontların modern kombinasyonu.

33
middleware.ts Normal file
View File

@@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const locales = ['tr', 'en'];
const defaultLocale = 'tr';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Exclude static files, public folder items, API routes, next internals
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
pathname.includes('.') ||
pathname === '/favicon.ico'
) {
return NextResponse.next();
}
const pathnameHasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (pathnameHasLocale) return NextResponse.next();
// Redirect if there is no locale
request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
};

View File

@@ -1,6 +1,7 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone',
/* config options here */ /* config options here */
}; };

BIN
public/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

BIN
public/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
public/MEIRA-2000x1333.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
public/SU-3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

BIN
public/favicon_io.zip Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

BIN
public/logo_m.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB