const { useState, useEffect, useRef } = React;
const PALETTES = {
sand: { bg: "#f6f1e8", surface: "#fbf8f2", ink: "#1a1814", muted: "#6b6253", line: "#e3dccd", accent: "#8a6f3f", accentInk: "#fbf8f2" },
weiss: { bg: "#ffffff", surface: "#f7f7f5", ink: "#111111", muted: "#666666", line: "#e6e6e3", accent: "#1a1a1a", accentInk: "#ffffff" },
marine: { bg: "#eef2f6", surface: "#f7fafc", ink: "#0a1f33", muted: "#4a5d70", line: "#cfd9e3", accent: "#1e3a5f", accentInk: "#f7fafc" },
como: { bg: "#f5f1e8", surface: "#faf6ec", ink: "#1c2a1f", muted: "#5a6a55", line: "#dcd5c2", accent: "#2d4a32", accentInk: "#faf6ec" }
};
const TYPO = {
sans: { display: "'Inter', sans-serif", body: "'Inter', sans-serif" },
didone: { display: "'Playfair Display', 'Bodoni Moda', Georgia, serif", body: "'Inter', sans-serif" },
editorial: { display: "'Cormorant Garamond', Georgia, serif", body: "'Inter', sans-serif" }
};
const IMG = {
hero: "photos/villa-pool-bay.jpg",
villa1: "photos/villa-pool-loungers.jpg",
location: "photos/villa-pool-islands.jpg"
};
// Galerie-Sektionen mit echten Bildern. Innenräume + Suiten sind Platzhalter,
// bis Innen-Fotos hochgeladen werden.
const GALLERY = {
exterior: [
{ id: "ext-1", url: "photos/villa-aerial-front.jpg", aspect: "wide" },
{ id: "ext-2", url: "photos/villa-aerial-bay.jpg", aspect: "tall" },
{ id: "ext-3", url: "photos/villa-aerial-overview.jpg", aspect: "wide" },
{ id: "ext-4", url: "photos/villa-architecture.jpg", aspect: "square" },
{ id: "ext-5", url: "photos/villa-entrance.jpg", aspect: "square" }
],
pool: [
{ id: "pool-1", url: "photos/villa-pool-bay.jpg", aspect: "wide" },
{ id: "pool-2", url: "photos/villa-pool-loungers.jpg", aspect: "tall" },
{ id: "pool-3", url: "photos/villa-pool-islands.jpg", aspect: "tall" },
{ id: "pool-4", url: "photos/villa-pool-side.jpg", aspect: "wide" },
{ id: "pool-5", url: "photos/villa-pool-distant.jpg", aspect: "square" }
],
interior: [
{ id: "int-1", url: "photos/villa-living-pano.jpg", aspect: "wide" },
{ id: "int-2", url: "photos/villa-living-room.jpg", aspect: "square" },
{ id: "int-3", url: "photos/villa-staircase.jpg", aspect: "tall" },
{ id: "int-4", url: "photos/villa-kitchen-island.jpg", aspect: "wide" },
{ id: "int-5", url: "photos/villa-living-sea.jpg", aspect: "square" }
],
bedrooms: [
{ id: "bed-1", url: "photos/villa-bedroom-master.jpg", aspect: "wide" },
{ id: "bed-2", url: "photos/villa-bedroom-seaview.jpg", aspect: "tall" },
{ id: "bed-3", url: "photos/villa-bedroom-sea.jpg", aspect: "square" },
{ id: "bed-4", url: "photos/villa-bedroom-orange.jpg", aspect: "square" },
{ id: "bed-5", url: "photos/villa-hallway.jpg", aspect: "wide" }
],
details: [
{ id: "det-1", url: "photos/villa-bathroom-shower.jpg", aspect: "square" },
{ id: "det-2", url: "photos/villa-bath-detail.jpg", aspect: "tall" },
{ id: "det-3", url: "photos/villa-sofa-detail.jpg", aspect: "wide" },
{ id: "det-4", url: "photos/villa-dining-sea.jpg", aspect: "wide" },
{ id: "det-5", url: "photos/villa-swallows.jpg", aspect: "square" }
]
};
function App() {
const [t, setTweak] = useTweaks(window.TWEAK_DEFAULTS);
const [lang, setLang] = useState("en");
const [scrolled, setScrolled] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 60);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const dict = window.I18N[lang];
const palette = PALETTES[t.palette] || PALETTES.sand;
const typo = TYPO[t.typo] || TYPO.serif;
const cssVars = {
"--bg": palette.bg, "--surface": palette.surface, "--ink": palette.ink,
"--muted": palette.muted, "--line": palette.line, "--accent": palette.accent,
"--accent-ink": palette.accentInk,
"--font-display": typo.display, "--font-body": typo.body
};
const scrollTo = (id) => {
setMenuOpen(false);
const el = document.getElementById(id);
if (el) window.scrollTo({ top: el.offsetTop - 60, behavior: "smooth" });
};
useEffect(() => {
document.documentElement.dir = lang === "ar" ? "rtl" : "ltr";
document.documentElement.lang = lang;
}, [lang]);
return (
);
}
const LANGS = [
{ code: "en", cc: "gb", label: "English" },
{ code: "de", cc: "de", label: "Deutsch" },
{ code: "tr", cc: "tr", label: "Türkçe" },
{ code: "ru", cc: "ru", label: "Русский" },
{ code: "ar", cc: "sa", label: "العربية" },
{ code: "zh", cc: "cn", label: "中文" },
];
const Flag = ({ cc, alt }) => (
);
function LangSwitcher({ lang, setLang }) {
const [open, setOpen] = useState(false);
const ref = useRef(null);
useEffect(() => {
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", onDoc);
return () => document.removeEventListener("mousedown", onDoc);
}, []);
const current = LANGS.find(l => l.code === lang) || LANGS[0];
return (
{open && (
{LANGS.map(l => (
-
))}
)}
);
}
function Nav({ dict, lang, setLang, scrolled, scrollTo, menuOpen, setMenuOpen }) {
const items = [["villa","villa"],["features","features"],["location","location"],["gallery","gallery"],["prices","prices"],["contact","contact"]];
return (
);
}
function Hero({ dict, t, scrollTo }) {
const split = t.heroLayout === "split";
const [muted, setMuted] = useState(true);
return (
{dict.hero.eyebrow}
{dict.hero.title}
{dict.hero.subtitle}
);
}
function QuickFacts({ dict }) {
const items = [
{ v: "4", l: dict.quickFacts.bedrooms },
{ v: "8", l: dict.quickFacts.guests },
{ v: "400", l: dict.quickFacts.size, suf: "m²" },
{ v: "∞", l: dict.quickFacts.pool }
];
return (
{items.map((i, idx) => (
))}
);
}
function Villa({ dict }) {
return (
{dict.villa.eyebrow}
{dict.villa.title}
);
}
function Features({ dict, scrollTo }) {
// Map feature index -> gallery section anchor
const targets = ["gal-pool","gal-exterior","gal-exterior","gal-details","gal-exterior","gal-interior"];
const thumbs = [
"photos/villa-pool-bay.jpg",
"photos/villa-pool-loungers.jpg",
"photos/villa-entrance.jpg",
"photos/villa-bathroom-shower.jpg",
"https://images.unsplash.com/photo-1567899378494-47b22a2ae96a?w=800&q=80",
"https://images.unsplash.com/photo-1528207776546-365bb710ee93?w=800&q=80"
];
return (
{dict.features.eyebrow}
{dict.features.title}
);
}
function LocationHero({ dict }) {
return (
{dict.location.eyebrow}
{dict.location.title.toUpperCase()}
{dict.location.tagline}
);
}
function LocationDetail({ dict }) {
return (
{dict.location.lead}
{dict.location.address &&
{dict.location.address}
}
);
}
function LocBlock({ title, items, dist, wide }) {
return (
{title}
{items.map((it, i) => (
-
{dist ? (
<>
{it.label}
{it.value}
>
) : (
<>
{it.name}
{it.desc}
>
)}
))}
);
}
function Gallery({ dict }) {
const order = ["exterior","pool","interior","bedrooms","details"];
return (
{dict.gallery.eyebrow}
{dict.gallery.title}
{order.map(key => (
{dict.gallery.sections[key]}
{GALLERY[key].map(s => (
))}
))}
);
}
function Prices({ dict }) {
return (
{dict.prices.eyebrow}
{dict.prices.title}
{dict.prices.seasons.map((s, i) => (
))}
{dict.prices.note}
);
}
function ymd(d) {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
function DateRangePicker({ dict, lang, value, onChange }) {
const today = new Date(); today.setHours(0,0,0,0);
const todayStr = ymd(today);
const [open, setOpen] = useState(false);
const [hover, setHover] = useState(null);
const [view, setView] = useState(new Date(today.getFullYear(), today.getMonth(), 1));
const ref = useRef(null);
const { start, end, flex } = value;
const locale = lang === 'ar' ? 'ar' : lang;
useEffect(() => {
function onDocClick(e) {
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
}
if (open) document.addEventListener('mousedown', onDocClick);
return () => document.removeEventListener('mousedown', onDocClick);
}, [open]);
function pickDay(d) {
const ds = ymd(d);
if (!start || (start && end)) { onChange({ start: ds, end: null, flex }); return; }
if (ds <= start) { onChange({ start: ds, end: null, flex }); return; }
onChange({ start, end: ds, flex });
}
function setFlex(n) { onChange({ start, end, flex: n }); }
function reset() { onChange({ start: null, end: null, flex: 0 }); }
function fmt(s) {
if (!s) return null;
const d = new Date(s + 'T00:00:00');
return d.toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' });
}
const buttonText = start && end
? `${fmt(start)} → ${fmt(end)}`
: start ? `${fmt(start)} → …` : dict.contact.datesPlaceholder;
function buildMonth(viewDate) {
const y = viewDate.getFullYear(), m = viewDate.getMonth();
const first = new Date(y, m, 1);
const dow = (first.getDay() + 6) % 7;
const daysInMonth = new Date(y, m + 1, 0).getDate();
const cells = [];
for (let i = 0; i < dow; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(new Date(y, m, d));
return cells;
}
function weekdayLabels() {
const base = new Date(2024, 0, 1); // Monday
const out = [];
for (let i = 0; i < 7; i++) {
const d = new Date(base); d.setDate(base.getDate() + i);
out.push(d.toLocaleDateString(locale, { weekday: 'short' }));
}
return out;
}
function renderMonth(viewDate) {
const cells = buildMonth(viewDate);
const monthLabel = viewDate.toLocaleDateString(locale, { month: 'long', year: 'numeric' });
const wk = weekdayLabels();
return (
{monthLabel}
{wk.map((w,i)=>{w})}
{cells.map((d, i) => {
if (!d) return ;
const ds = ymd(d);
const past = ds < todayStr;
const isStart = ds === start;
const isEnd = ds === end;
const inRange = start && end && ds > start && ds < end;
const inHover = start && !end && hover && ds > start && ds <= hover;
const cls = ['drp__cell'];
if (past) cls.push('drp__cell--past');
if (isStart) cls.push('drp__cell--start');
if (isEnd) cls.push('drp__cell--end');
if (inRange || inHover) cls.push('drp__cell--in');
return (
);
})}
);
}
const nextView = new Date(view.getFullYear(), view.getMonth() + 1, 1);
const prevView = new Date(view.getFullYear(), view.getMonth() - 1, 1);
const canPrev = ymd(prevView) >= ymd(new Date(today.getFullYear(), today.getMonth(), 1));
const flexOpts = [0, 1, 2, 3, 7];
return (
{open && (
{renderMonth(view)}
{renderMonth(nextView)}
{dict.contact.flexLabel}:
{flexOpts.map(n => (
))}
)}
);
}
function Contact({ dict, lang, onSuccess, submitted }) {
const [sending, setSending] = useState(false);
const [error, setError] = useState(null);
const [dates, setDates] = useState({ start: null, end: null, flex: 0 });
async function handleSubmit(e) {
e.preventDefault();
if (sending) return;
if (!dates.start || !dates.end) {
setError(dict.contact.datesPlaceholder);
return;
}
setSending(true);
setError(null);
const form = e.target;
const data = new FormData(form);
data.set('checkin', dates.start);
data.set('checkout', dates.end);
data.set('flex', String(dates.flex));
try {
const res = await fetch("kontakt.php", { method: "POST", body: data });
let payload = null;
try { payload = await res.json(); } catch (_) {}
if (!res.ok || !payload || !payload.ok) {
throw new Error((payload && payload.error) || "send_failed");
}
onSuccess();
} catch (err) {
setError(dict.contact.error);
setSending(false);
}
}
const honeypotStyle = {
position: "absolute", left: "-10000px", top: "auto",
width: "1px", height: "1px", overflow: "hidden", opacity: 0
};
return (
);
}
function Footer({ dict }) {
return (
);
}
function StickyBar({ dict, scrollTo }) {
return (
Villa Yasmin
·
{dict.sticky.from}
);
}
function TweaksUI({ t, setTweak }) {
return (
setTweak("palette", v)} />
setTweak("heroLayout", v)} />
setTweak("typo", v)} />
setTweak("stickyBar", v)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render();