// ── Translations ──────────────────────────────────────────────────────────────
const TRANSLATIONS = {
de: {
nav: { zimmer:'Zimmer', fruehstueck:'Kaffeetheke', verfuegbarkeit:'Verfügbarkeit', galerie:'Galerie', ueber:'Über uns', kontakt:'Kontakt' },
hero: { slogan:'Wachau-Blick,\nWachauer Herz.', sub:'Zwei liebevoll geführte Doppelzimmer mit Panorama-Aussicht – mitten im UNESCO-Welterbe Wachau.', cta:'Jetzt anfragen', scroll:'Entdecken' },
zimmer: {
title:'Zimmer & Preise', sub:'Zwei frisch sanierte Doppelzimmer mit allem, was Sie brauchen.',
helene:{ name:'Zimmer Helene', desc:'Ruhiges Doppelzimmer mit Balkon und Blick ins Grüne. Frisch saniert, hell und gemütlich eingerichtet.' },
marille:{ name:'Zimmer Marille', desc:'Sonniges Doppelzimmer benannt nach der Wachauer Marille – warmer Charme und Balkon inklusive.' },
features:['ca. 20 m²','Balkon','Dusche & WC','WLAN','TV','Föhn','Mini-Kühlschrank','Ventilator'],
priceTitle:'Saisonale Preise', priceSub:'Kaffeetheke inklusive · Mindestaufenthalt Wochenende: 2 Nächte',
seasons:[
{ label:'Nebensaison', dates:'Dez – Feb', tier:'A', prices:{ '1-3': 94, '4+': 90, '7+': 87 }, single:{ '1-3': 53, '4+': 50, '7+': 46 }, color:'blue' },
{ label:'Vorsaison', dates:'Mai · Okt – Nov', tier:'B', prices:{ '1-3': 98, '4+': 95, '7+': 91 }, single:{ '1-3': 56, '4+': 53, '7+': 50 }, color:'green' },
{ label:'Hauptsaison', dates:'Jun – Sep', tier:'C', prices:{ '1-3': 105, '4+': 101, '7+': 98 }, single:{ '1-3': 58, '4+': 55, '7+': 52 }, color:'apricot' },
],
tierLabels:{ '1-3':'1 – 3 Nächte', '4+':'ab 4 Nächte', '7+':'ab 7 Nächte' },
extras:'Weitere Konditionen',
extrasItems:[
['Check-in', 'ab 15:00 Uhr'],
['Check-out', 'bis 11:00 Uhr'],
['Einzelnutzung', 'siehe Spalte rechts'],
['Zusatzbett pro Nacht', '€ 33 (1-3 N) · € 30 (4+ N) · € 26 (7+ N)'],
['Kinder bis 3 Jahre', 'kostenlos'],
['Kinder 4 – 17 Jahre', '€ 33 (1-3 N) · € 30 (4+ N) · € 26 (7+ N)'],
],
perNight:'/Nacht · 2 Personen',
perNightSingle:'Einzelnutzung',
note:'Preise pro Zimmer/Nacht inkl. Kaffeetheke · Ortstaxe nicht enthalten · Stand 2026'
},
fruehstueck:{ title:'Die Kaffeetheke', sub:'Kaffee und Tee – jederzeit zur Selbstbedienung.', text:'Auf der Galerie wartet unsere Kaffeetheke: frischer Kaffee aus dem Vollautomaten und eine Auswahl an Tee – jederzeit zur Selbstbedienung. Ein gemütlicher Moment zum Ankommen oder Ausklingen. Für ein echtes Wachauer Frühstücks- und Genusserlebnis empfehlen wir die Heurigen und Lokale der Umgebung – wir geben Ihnen gerne unsere Tipps.', tag:'Kaffeetheke inklusive' },
aussicht:{
title:'Aussicht & Umgebung', sub:'Mitten im UNESCO-Welterbe – draußen wartet die Wachau.',
tabs:['Veranstaltungen','Sehenswürdigkeiten','Sport & Freizeit'],
weather:'Wetter Spitz a. d. Donau', weatherSub:'Aktuell in Spitz an der Donau',
more:'Mehr Tipps & Ausflugsideen', moreLink:'https://www.spitz-wachau.at',
events:['Wachauer Weinfrühling','Klingende Rieden','Sonnwendfeier','Marillenkirtag','Wachau Marathon','Spitzer Weinherbst','Erntedankfest','Tage der offenen Kellertüren','Weingartenführungen'],
sehen:['Heurigenbesuche','Schifffahrtsmuseum im Schloss Erlahof','Spätgotische Kirche','Schloss zu Spitz','Ruine Hinterhaus','Tausendeimerberg','Rotes Tor','Rollfähre mit Camera obscura','Historischer Kaufmannsladen'],
sport:['Wander- & Rundwanderwege (Welterbesteig Wachau)','2 Tennisplätze','Freibad & Beachvolleyball','Fahrrad & Segwayverleih','Donauradweg & Radroute südliches Waldviertel','Segway Touren in der Wachau','Kinderspielplätze']
},
verfuegbarkeit:{
title:'Zimmer-Verfügbarkeit', sub:'Auf einen Blick – wann ist Ihr Wunschzimmer frei?',
helene:'Zimmer Helene', marille:'Zimmer Marille',
free:'Frei', booked:'Belegt', legend:'Legende',
cta:'Verfügbar? Jetzt anfragen',
months:['Januar','Februar','März','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'],
days:['Mo','Di','Mi','Do','Fr','Sa','So'],
adminBtn:'Admin', adminTitle:'Verfügbarkeit verwalten', adminHint:'Datum anklicken zum Umschalten · Änderungen werden sofort gespeichert',
pwLabel:'Passwort', pwBtn:'Anmelden', pwWrong:'Falsches Passwort', logout:'Abmelden'
},
galerie:{ title:'Galerie', sub:'Ein Blick ins Haus.', labels:['Eingang & Außenansicht','Zimmer Helene','Zimmer Marille','Balkon & Aussicht','Kaffeetheke','Wachau-Umgebung','Mini-Kühlschrank','Schrank & TV','Ventilator'] },
ueber:{ title:'Über uns', sub:'Zwei Brüder, ein Haus, eine Leidenschaft.', text:'Patrik und Daniel Sabata haben das Haus von ihrer Tante übernommen – in Ehren gehalten, was war, und mit frischem Wind, was kommt. Hier trifft Wachauer Gastfreundschaft auf modernes Konzept. Wir freuen uns auf Sie.', patrik:'Patrik Sabata', daniel:'Daniel Sabata' },
bewertungen:{ title:'Gästestimmen', sub:'Was unsere Gäste sagen.',
items:[]
},
kontakt:{ title:'Kontakt & Anfrage', sub:'Schreiben Sie uns – wir antworten rasch.', name:'Ihr Name', email:'E-Mail-Adresse', phone:'Telefon (optional)', street:'Straße & Hausnummer', zip:'PLZ', city:'Ort', country:'Land', arrival:'Anreise', departure:'Abreise', room:'Zimmer', roomOpts:['Bitte wählen','Zimmer Helene','Zimmer Marille','Egal / beide'], guests:'Anzahl Personen', message:'Nachricht (optional)', send:'Anfrage senden', sent:'Vielen Dank! Wir melden uns bald bei Ihnen.', direct:'Direkt erreichbar:' },
anfahrt:{ title:'Anfahrt', sub:'Siedlung Erlahof 47, 3620 Spitz an der Donau', parking:'Kostenlose Parkplätze direkt am Haus', train:'Bahnhof Spitz/Donau: ca. 2 km (Taxi verfügbar)', bike:'Donauradweg: direkt erreichbar · Fahrradabstellplatz vorhanden' },
footer:{ tagline:'Gemütlichkeit mit Aussicht – Wachau.', impressum:'Impressum', datenschutz:'Datenschutz', rights:'© 2025 P&D Schöne Aussicht · Patrik & Daniel Sabata', social:'Folgt bald auf:' }
},
en: {
nav: { zimmer:'Rooms', fruehstueck:'Kaffeetheke', verfuegbarkeit:'Availability', galerie:'Gallery', ueber:'About', kontakt:'Contact' },
hero: { slogan:'Wachau Views,\nWachau Heart.', sub:'Two lovingly run double rooms with panoramic views – in the heart of UNESCO World Heritage Wachau.', cta:'Enquire Now', scroll:'Discover' },
zimmer: {
title:'Rooms & Prices', sub:'Two newly renovated double rooms with everything you need.',
helene:{ name:'Room Helene', desc:'Quiet double room with balcony and garden views. Freshly renovated, bright and cosy.' },
marille:{ name:'Room Marille', desc:'Sunny double room named after the Wachau apricot – warm charm and a balcony included.' },
features:['approx. 20 m²','Balcony','Shower & WC','WiFi','TV','Hairdryer','Mini-fridge','Fan'],
priceTitle:'Seasonal Rates', priceSub:'Kaffeetheke included · Weekend minimum stay: 2 nights',
seasons:[
{ label:'Low Season', dates:'Dec – Feb', tier:'A', prices:{ '1-3': 94, '4+': 90, '7+': 87 }, single:{ '1-3': 53, '4+': 50, '7+': 46 }, color:'blue' },
{ label:'Shoulder Season', dates:'May · Oct – Nov', tier:'B', prices:{ '1-3': 98, '4+': 95, '7+': 91 }, single:{ '1-3': 56, '4+': 53, '7+': 50 }, color:'green' },
{ label:'High Season', dates:'Jun – Sep', tier:'C', prices:{ '1-3': 105, '4+': 101, '7+': 98 }, single:{ '1-3': 58, '4+': 55, '7+': 52 }, color:'apricot' },
],
tierLabels:{ '1-3':'1 – 3 nights', '4+':'4+ nights', '7+':'7+ nights' },
extras:'Additional terms',
extrasItems:[
['Check-in', 'from 3:00 pm'],
['Check-out', 'until 11:00 am'],
['Single occupancy', 'see right column'],
['Extra bed per night', '€ 33 (1-3 n) · € 30 (4+ n) · € 26 (7+ n)'],
['Children up to 3 yrs', 'free'],
['Children 4 – 17 yrs', '€ 33 (1-3 n) · € 30 (4+ n) · € 26 (7+ n)'],
],
perNight:'/night · 2 persons',
perNightSingle:'single occupancy',
note:'Prices per room/night incl. Kaffeetheke · City tax not included · As of 2026'
},
fruehstueck:{ title:'The Kaffeetheke', sub:'Coffee and tea – self-service, any time.', text:'On the landing you will find our Kaffeetheke: freshly brewed coffee and a selection of teas – self-service, whenever you like. A cosy spot to arrive or wind down. For a true Wachau breakfast and culinary experience, we recommend the Heurigen and spots nearby – just ask us for our tips.', tag:'Kaffeetheke included' },
aussicht:{
title:'Views & Surroundings', sub:'In the heart of UNESCO World Heritage – the Wachau awaits.',
tabs:['Events','Sights','Sport & Leisure'],
weather:'Weather Spitz a.d. Donau', weatherSub:'Currently in Spitz an der Donau',
more:'More tips & excursion ideas', moreLink:'https://www.spitz-wachau.at',
events:['Wachau Wine Spring','Klingende Rieden (vineyard concerts)','Midsummer Celebration','Apricot Festival','Wachau Marathon','Spitz Wine Autumn','Harvest Thanksgiving','Open Cellar Doors','Vineyard Tours'],
sehen:['Heuriger visits','Maritime Museum at Erlahof Castle','Late Gothic Church','Spitz Castle','Hinterhaus Ruins','Tausendeimerberg','Red Gate','Car Ferry with Camera Obscura','Historic Merchant Shop'],
sport:['Hiking & circular trails (Welterbesteig Wachau)','2 tennis courts','Open-air pool & beach volleyball','Bike & Segway rental','Danube Cycle Path & Southern Forest Quarter route','Segway tours in the Wachau','Children\'s playgrounds']
},
verfuegbarkeit:{
title:'Room Availability', sub:'At a glance – when is your preferred room free?',
helene:'Room Helene', marille:'Room Marille',
free:'Available', booked:'Booked', legend:'Legend',
cta:'Available? Enquire now',
months:['January','February','March','April','May','June','July','August','September','October','November','December'],
days:['Mo','Tu','We','Th','Fr','Sa','Su'],
adminBtn:'Admin', adminTitle:'Manage Availability', adminHint:'Click a date to toggle · Changes are saved immediately',
pwLabel:'Password', pwBtn:'Login', pwWrong:'Wrong password', logout:'Log out'
},
galerie:{ title:'Gallery', sub:'A look inside the house.', labels:['Entrance & Exterior','Room Helene','Room Marille','Balcony & View','Kaffeetheke','Wachau Surroundings','Mini-Fridge','Wardrobe & TV','Fan'] },
ueber:{ title:'About Us', sub:'Two brothers, one house, one passion.', text:'Patrik and Daniel Sabata took over the house from their aunt – with deep respect for what was, and fresh ideas for what is to come. Here, Wachau hospitality meets modern thinking. We look forward to welcoming you.', patrik:'Patrik Sabata', daniel:'Daniel Sabata' },
bewertungen:{ title:'Guest Reviews', sub:'What our guests say.',
items:[]
},
kontakt:{ title:'Contact & Enquiry', sub:'Write to us – we reply quickly.', name:'Your Name', email:'Email Address', phone:'Phone (optional)', street:'Street & No.', zip:'ZIP', city:'City', country:'Country', arrival:'Arrival', departure:'Departure', room:'Room', roomOpts:['Please select','Room Helene','Room Marille','Either / both'], guests:'Number of Guests', message:'Message (optional)', send:'Send Enquiry', sent:'Thank you! We will be in touch soon.', direct:'Reach us directly:' },
anfahrt:{ title:'Getting Here', sub:'Siedlung Erlahof 47, 3620 Spitz an der Donau', parking:'Free parking directly at the house', train:'Spitz/Donau station: approx. 2 km (taxi available)', bike:'Danube Cycle Path: directly accessible · Bicycle parking available' },
footer:{ tagline:'Cosiness with a view – Wachau.', impressum:'Imprint', datenschutz:'Privacy Policy', rights:'© 2025 P&D Schöne Aussicht · Patrik & Daniel Sabata', social:'Follow us soon on:' }
}
};
// ── Utilities ─────────────────────────────────────────────────────────────────
const useInView = (threshold = 0.1) => {
const ref = React.useRef();
const [inView, setInView] = React.useState(false);
React.useEffect(() => {
const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) setInView(true); }, { threshold });
if (ref.current) obs.observe(ref.current);
return () => obs.disconnect();
}, []);
return [ref, inView];
};
const FadeIn = ({ children, delay = 0, up = true }) => {
const [ref, inView] = useInView();
return (
{children}
);
};
const Img = ({ label, h = 220, bg = '#2d5a35' }) => (
{label}
);
const Stars = ({ n = 5 }) => {'★'.repeat(n)} ;
const SectionLabel = ({ children, light }) => (
{children}
);
const H2 = ({ children, light }) => (
{children}
);
const Sub = ({ children, light }) => (
{children}
);
// ── Logo ──────────────────────────────────────────────────────────────────────
const Logo = ({ white }) => {
const c = white ? '#fff' : 'var(--green)';
const c2 = white ? 'rgba(255,255,255,0.5)' : 'var(--muted)';
return (
);
};
// ── Header ────────────────────────────────────────────────────────────────────
const Header = ({ lang, setLang, t }) => {
const [scrolled, setScrolled] = React.useState(false);
const [open, setOpen] = React.useState(false);
React.useEffect(() => {
const fn = () => setScrolled(window.scrollY > 60);
window.addEventListener('scroll', fn);
return () => window.removeEventListener('scroll', fn);
}, []);
const go = (id) => { setOpen(false); document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }); };
const links = [['zimmer',t.nav.zimmer],['fruehstueck',t.nav.fruehstueck],['verfuegbarkeit',t.nav.verfuegbarkeit],['galerie',t.nav.galerie],['ueber',t.nav.ueber],['kontakt',t.nav.kontakt]];
const bg = scrolled || open ? 'rgba(248,246,241,0.97)' : 'transparent';
return (
window.scrollTo({ top: 0, behavior: 'smooth' })} style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0 }}>
{links.map(([id, label]) => (
go(id)} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 13, letterSpacing: '0.04em', color: scrolled ? 'var(--text)' : '#fff', fontWeight: 400, padding: 0, opacity: 0.9, transition: 'opacity 0.2s' }}
onMouseEnter={e => e.target.style.opacity = 0.55} onMouseLeave={e => e.target.style.opacity = 0.9}>{label}
))}
setLang(lang === 'de' ? 'en' : 'de')} style={{ background: 'none', border: `1px solid ${scrolled ? 'var(--border)' : 'rgba(255,255,255,0.35)'}`, borderRadius: 4, cursor: 'pointer', padding: '4px 10px', fontFamily: 'var(--font-body)', fontSize: 12, letterSpacing: '0.1em', color: scrolled ? 'var(--text)' : '#fff', fontWeight: 600 }}>
{lang === 'de' ? 'EN' : 'DE'}
go('kontakt')} style={{ background: 'var(--apricot)', border: 'none', borderRadius: 4, cursor: 'pointer', padding: '8px 18px', fontFamily: 'var(--font-body)', fontSize: 13, color: '#fff', fontWeight: 600 }}>
{t.hero.cta}
setOpen(!open)} className="mobile-menu-btn" style={{ display: 'none', background: 'none', border: 'none', cursor: 'pointer', padding: 8 }}>
{open && (
{links.map(([id, label]) => (
go(id)} style={{ display: 'block', background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 16, color: 'var(--text)', padding: '10px 0', width: '100%', textAlign: 'left' }}>{label}
))}
setLang(lang === 'de' ? 'en' : 'de')} style={{ background: 'none', border: '1px solid var(--border)', borderRadius: 4, cursor: 'pointer', padding: '6px 14px', fontSize: 13, fontFamily: 'var(--font-body)', color: 'var(--text)' }}>{lang === 'de' ? 'EN' : 'DE'}
go('kontakt')} style={{ background: 'var(--apricot)', border: 'none', borderRadius: 4, cursor: 'pointer', padding: '6px 18px', fontSize: 13, color: '#fff', fontFamily: 'var(--font-body)', fontWeight: 600 }}>{t.hero.cta}
)}
);
};
// ── Hero ──────────────────────────────────────────────────────────────────────
const Hero = ({ t }) => (
Spitz an der Donau · Wachau
{t.hero.slogan}
{t.hero.sub}
document.getElementById('kontakt')?.scrollIntoView({ behavior: 'smooth' })} style={{ background: 'var(--apricot)', border: 'none', borderRadius: 4, cursor: 'pointer', padding: '7px 16px', fontFamily: 'var(--font-body)', fontSize: 12, color: '#fff', fontWeight: 600, letterSpacing: '0.04em', transition: 'transform 0.2s' }}
onMouseEnter={e => e.currentTarget.style.transform = 'translateY(-2px)'} onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{t.hero.cta}
);
// ── Zimmer ────────────────────────────────────────────────────────────────────
const ZIMMER_PHOTOS = {
helene: [
{ src: 'photos/helene-1.jpeg', label: 'Zimmer Helene – Übersicht' },
{ src: 'photos/helene-2.jpeg', label: 'Zimmer Helene – Schlafbereich & Balkon' },
{ src: 'photos/helene-3.jpeg', label: 'Zimmer Helene – Bad mit Dusche' },
{ src: 'photos/helene-4.jpeg', label: 'Zimmer Helene – WC & Dachfenster' },
],
marille: [
{ src: 'photos/marille-1.jpeg', label: 'Zimmer Marille – Übersicht' },
{ src: 'photos/marille-2.jpeg', label: 'Zimmer Marille – Schlafbereich & Wachau-Blick' },
{ src: 'photos/marille-3.jpeg', label: 'Zimmer Marille – Bad mit Dusche' },
{ src: 'photos/marille-4.jpeg', label: 'Zimmer Marille – WC & Dachfenster' },
],
};
const ZimmerLightbox = ({ roomKey, roomName, onClose }) => {
const photos = ZIMMER_PHOTOS[roomKey];
const [idx, setIdx] = React.useState(0);
React.useEffect(() => {
document.body.style.overflow = 'hidden';
const fn = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowRight') setIdx(i => (i+1) % photos.length);
if (e.key === 'ArrowLeft') setIdx(i => (i-1+photos.length) % photos.length);
};
window.addEventListener('keydown', fn);
return () => { document.body.style.overflow = ''; window.removeEventListener('keydown', fn); };
}, []);
const colors = ['#2d5a35','#3a5a7a','#c47a2a'];
return (
×
{roomName}
e.stopPropagation()} style={{ maxWidth: 820, width: '100%', position: 'relative' }}>
{photos[idx].src
?
:
{photos[idx].label}
}
{ e.stopPropagation(); setIdx(i => (i-1+photos.length)%photos.length); }} style={{ position: 'absolute', left: -52, top: '50%', transform: 'translateY(-50%)', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%', width: 44, height: 44, cursor: 'pointer', color: '#fff', fontSize: 22, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>‹
{ e.stopPropagation(); setIdx(i => (i+1)%photos.length); }} style={{ position: 'absolute', right: -52, top: '50%', transform: 'translateY(-50%)', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%', width: 44, height: 44, cursor: 'pointer', color: '#fff', fontSize: 22, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>›
{/* Dots */}
{photos.map((_, i) => (
{ e.stopPropagation(); setIdx(i); }} style={{ width: i === idx ? 24 : 8, height: 8, borderRadius: 4, background: i === idx ? '#fff' : 'rgba(255,255,255,0.3)', border: 'none', cursor: 'pointer', padding: 0, transition: 'all 0.2s' }}/>
))}
{idx+1} / {photos.length}
);
};
// ── Zahlen-Counter ────────────────────────────────────────────────────────────
const useCountUp = (target, duration = 1600, start = false) => {
const [val, setVal] = React.useState(0);
React.useEffect(() => {
if (!start) return;
let raf;
const t0 = performance.now();
const tick = (now) => {
const p = Math.min(1, (now - t0) / duration);
const eased = 1 - Math.pow(1 - p, 3);
setVal(Math.round(target * eased));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [start, target]);
return val;
};
const Stat = ({ value, suffix, label, start, delay = 0 }) => {
const [go, setGo] = React.useState(false);
React.useEffect(() => {
if (start) { const id = setTimeout(() => setGo(true), delay); return () => clearTimeout(id); }
}, [start, delay]);
const n = useCountUp(value, 1600, go);
return (
);
};
const StatsBar = ({ lang }) => {
const ref = React.useRef(null);
const [start, setStart] = React.useState(false);
React.useEffect(() => {
const io = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) { setStart(true); io.disconnect(); }
}, { threshold: 0.4 });
if (ref.current) io.observe(ref.current);
return () => io.disconnect();
}, []);
const stats = lang === 'de'
? [{v:2,s:'',l:'Zimmer'},{v:20,s:' m²',l:'Wohnfläche'},{v:100,s:' %',l:'Wachau-Blick'},{v:2026,s:'',l:'Frisch saniert'}]
: [{v:2,s:'',l:'Rooms'},{v:20,s:' m²',l:'Living space'},{v:100,s:' %',l:'Wachau view'},{v:2026,s:'',l:'Newly renovated'}];
return (
);
};
// ── Preistabelle ──────────────────────────────────────────────────────────────
const PriceTable = ({ z, colorMap }) => {
const [tier, setTier] = React.useState('1-3');
const tiers = Object.keys(z.tierLabels);
return (
{z.priceTitle}
{z.priceSub}
{tiers.map((tk) => (
setTier(tk)}
style={{ fontFamily: 'var(--font-body)', fontSize: 12, fontWeight: 600, padding: '7px 12px', border: 'none', borderRadius: 4, cursor: 'pointer', background: tier === tk ? '#fff' : 'transparent', color: tier === tk ? 'var(--green)' : 'var(--muted)', boxShadow: tier === tk ? '0 1px 4px rgba(0,0,0,0.06)' : 'none', transition: 'all 0.15s', letterSpacing: '0.02em' }}>
{z.tierLabels[tk]}
))}
{z.seasons.map((s, i) => (
{s.label}
· {s.tier}
{s.dates}
€ {s.prices[tier]}
{z.perNight}
{z.perNightSingle}
€ {s.single[tier]}
))}
▸ {z.extras}
{z.extrasItems.map(([k, v], i) => (
{k}
{v}
))}
{z.note}
);
};
const Zimmer = ({ t }) => {
const z = t.zimmer;
const [lightbox, setLightbox] = React.useState(null); // 'helene' | 'marille' | null
const colorMap = { green: 'var(--green)', blue: 'var(--blue)', apricot: 'var(--apricot)' };
const roomKeys = ['helene', 'marille'];
return (
{lightbox && setLightbox(null)}/>}
{t.nav.zimmer} {z.title} {z.sub}
{[z.helene, z.marille].map((room, i) => (
setLightbox(roomKeys[i])} style={{ background: '#fff', borderRadius: 8, overflow: 'hidden', boxShadow: '0 2px 20px rgba(0,0,0,0.06)', transition: 'transform 0.3s, box-shadow 0.3s', cursor: 'pointer' }}
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)'; e.currentTarget.style.boxShadow = '0 8px 36px rgba(0,0,0,0.1)'; }}
onMouseLeave={e => { e.currentTarget.style.transform = 'none'; e.currentTarget.style.boxShadow = '0 2px 20px rgba(0,0,0,0.06)'; }}>
{ZIMMER_PHOTOS[roomKeys[i]][0].src
?
:
}
e.currentTarget.style.background = 'rgba(0,0,0,0.18)'}
onMouseLeave={e => e.currentTarget.style.background = 'rgba(0,0,0,0)'}>
Fotos ansehen
{room.name}
{room.desc}
{z.features.map((f, j) => {f} )}
))}
);
};
// ── Frühstück ─────────────────────────────────────────────────────────────────
const Fruehstueck = ({ t }) => {
const f = t.fruehstueck;
return (
{t.nav.fruehstueck}
{f.title}
{f.sub}
{f.text}
{f.tag}
);
};
// ── Aussicht ──────────────────────────────────────────────────────────────────
const Aussicht = ({ t, lang }) => {
const a = t.aussicht;
const [tab, setTab] = React.useState(0);
const lists = [a.events, a.sehen, a.sport];
const icons = [['🍷','🎵','🌅','🍑','🏃','🍂','🌾','🚪','🌿'], ['🍺','⚓','⛪','🏰','🏚️','🍇','🚪','⛴️','🏪'], ['🥾','🎾','🏊','🚲','🚴','🛴','🎪']];
return (
Wachau
{a.title}
{a.sub}
{/* Tabbed content */}
{a.tabs.map((label, i) => (
setTab(i)} style={{ flex: 1, fontFamily: 'var(--font-body)', fontSize: 12, fontWeight: tab === i ? 700 : 400, color: tab === i ? 'var(--green)' : 'var(--muted)', background: tab === i ? 'var(--green-light)' : 'transparent', border: 'none', padding: '13px 8px', cursor: 'pointer', borderBottom: tab === i ? '2px solid var(--green)' : '2px solid transparent', letterSpacing: '0.03em', transition: 'all 0.2s' }}>
{label}
))}
{lists[tab].map((item, i) => (
{icons[tab][i] || '·'}
{item}
))}
{a.more} ↗
{/* Map + weather */}
);
};
// ── Cookie-Consent System ────────────────────────────────────────────────────
const CONSENT_KEY = 'pd_consent_v2';
const CONSENT_VERSION = 2;
const ConsentContext = React.createContext({ necessary: true, external: false, set: () => {} });
const useConsent = () => React.useContext(ConsentContext);
const loadConsent = () => {
try {
const raw = JSON.parse(localStorage.getItem(CONSENT_KEY));
if (raw && raw.version === CONSENT_VERSION) return raw;
} catch {}
return null;
};
const ConsentProvider = ({ children, lang }) => {
const [consent, setConsent] = React.useState(() => loadConsent() || { necessary: true, external: false, decided: false, version: CONSENT_VERSION });
const [open, setOpen] = React.useState(() => !loadConsent());
const save = (next) => {
const data = { ...consent, ...next, decided: true, version: CONSENT_VERSION };
setConsent(data);
localStorage.setItem(CONSENT_KEY, JSON.stringify(data));
setOpen(false);
};
React.useEffect(() => {
window.__openConsent = () => setOpen(true);
return () => { delete window.__openConsent; };
}, []);
return (
setOpen(true) }}>
{children}
{open && setOpen(false)}/>}
);
};
const btnPrimary = { background: 'var(--green)', color: '#fff', border: 'none', borderRadius: 5, padding: '10px 20px', fontFamily: 'var(--font-body)', fontSize: 13, fontWeight: 600, cursor: 'pointer', letterSpacing: '0.02em' };
const btnSecondary = { background: 'none', border: '1px solid var(--border)', color: 'var(--text)', borderRadius: 5, padding: '10px 16px', fontFamily: 'var(--font-body)', fontSize: 13, fontWeight: 500, cursor: 'pointer' };
const CategoryRow = ({ title, desc, on, onChange, disabled, label }) => (
{disabled ? (
✓ {label}
) : (
onChange(!on)} role="switch" aria-checked={on} style={{ width: 42, height: 24, borderRadius: 12, background: on ? 'var(--green)' : '#ccc', border: 'none', position: 'relative', cursor: 'pointer', padding: 0, transition: 'background 0.2s', flexShrink: 0 }}>
)}
);
const ConsentBanner = ({ consent, onSave, lang, canDismiss, onClose }) => {
const [showDetails, setShowDetails] = React.useState(false);
const [external, setExternal] = React.useState(consent.external);
const txt = lang === 'de' ? {
title: 'Datenschutz-Einstellungen',
intro: 'Diese Website nutzt nur das technisch Notwendige. Für die interaktive Karte und Live-Wetter werden Daten an externe Anbieter (OpenStreetMap, Open-Meteo) übertragen – nur mit Ihrer Zustimmung.',
necessary: 'Notwendig', necessaryDesc: 'Speichert Ihre Spracheinstellungen und die Anzeige des Verfügbarkeitskalenders lokal in Ihrem Browser. Kein Tracking.',
always: 'Immer aktiv',
external: 'Externe Dienste', externalDesc: 'Karte (OpenStreetMap) und Live-Wetter (Open-Meteo). Beim Aktivieren wird Ihre IP-Adresse an diese Anbieter übertragen.',
acceptAll: 'Alle akzeptieren', rejectAll: 'Nur Notwendige', save: 'Auswahl speichern',
details: 'Einstellungen', backLink: 'Zurück', moreInfo: 'Datenschutzerklärung'
} : {
title: 'Privacy Settings',
intro: 'This site only uses what is technically necessary. The interactive map and live weather transmit data to external providers (OpenStreetMap, Open-Meteo) — only with your consent.',
necessary: 'Necessary', necessaryDesc: 'Stores your language settings and availability calendar locally in your browser. No tracking.',
always: 'Always active',
external: 'External services', externalDesc: 'Map (OpenStreetMap) and live weather (Open-Meteo). Enabling these transmits your IP address.',
acceptAll: 'Accept all', rejectAll: 'Necessary only', save: 'Save selection',
details: 'Customize', backLink: 'Back', moreInfo: 'Privacy policy'
};
return (
{canDismiss && (
×
)}
🍪
{txt.title}
{txt.intro}
{showDetails && (
)}
{!showDetails ? (
<>
setShowDetails(true)} style={btnSecondary}>{txt.details}
onSave({ external: false })} style={btnSecondary}>{txt.rejectAll}
onSave({ external: true })} style={btnPrimary}>{txt.acceptAll}
>
) : (
<>
setShowDetails(false)} style={btnSecondary}>← {txt.backLink}
onSave({ external })} style={btnPrimary}>{txt.save}
>
)}
);
};
// ── Consent-Wetter (Open-Meteo) ───────────────────────────────────────────────
const WEATHER_ICONS = { 0:'☀️', 1:'🌤️', 2:'⛅', 3:'☁️', 45:'🌫️', 48:'🌫️', 51:'🌦️', 53:'🌦️', 55:'🌦️', 61:'🌧️', 63:'🌧️', 65:'🌧️', 71:'🌨️', 73:'🌨️', 75:'❄️', 80:'🌦️', 81:'🌧️', 82:'⛈️', 95:'⛈️', 96:'⛈️', 99:'⛈️' };
const WEATHER_TEXT_DE = { 0:'Klar', 1:'Heiter', 2:'Wolkig', 3:'Bedeckt', 45:'Nebel', 48:'Nebel', 51:'Nieselregen', 53:'Nieselregen', 55:'Nieselregen', 61:'Leichter Regen', 63:'Regen', 65:'Starker Regen', 71:'Schneefall', 73:'Schneefall', 75:'Starker Schnee', 80:'Regenschauer', 81:'Regenschauer', 82:'Heftige Schauer', 95:'Gewitter', 96:'Gewitter', 99:'Gewitter' };
const WEATHER_TEXT_EN = { 0:'Clear', 1:'Mainly clear', 2:'Partly cloudy', 3:'Overcast', 45:'Foggy', 48:'Foggy', 51:'Drizzle', 53:'Drizzle', 55:'Drizzle', 61:'Light rain', 63:'Rain', 65:'Heavy rain', 71:'Snow', 73:'Snow', 75:'Heavy snow', 80:'Rain showers', 81:'Rain showers', 82:'Violent showers', 95:'Thunderstorm', 96:'Thunderstorm', 99:'Thunderstorm' };
const ConsentWeather = ({ a, lang }) => {
const consent = useConsent();
const allowed = consent.external;
const [state, setState] = React.useState('idle');
const [data, setData] = React.useState(null);
const load = async () => {
setState('loading');
try {
const res = await fetch('https://api.open-meteo.com/v1/forecast?latitude=48.3683&longitude=15.4136¤t=temperature_2m,weather_code,wind_speed_10m&timezone=Europe%2FVienna');
const j = await res.json();
setData(j.current); setState('loaded');
} catch { setState('error'); }
};
React.useEffect(() => { if (allowed && state === 'idle') load(); }, [allowed]);
if (state === 'loaded' && data) {
const code = data.weather_code;
const desc = (lang === 'de' ? WEATHER_TEXT_DE : WEATHER_TEXT_EN)[code] || '–';
return (
{WEATHER_ICONS[code] || '🌡️'}
{a.weather}
{Math.round(data.temperature_2m)} °C
{desc} · {Math.round(data.wind_speed_10m)} km/h
);
}
return (
🌤️
{a.weather}
{lang === 'de' ? 'Aktuelle Wetterdaten von Open-Meteo' : 'Current weather from Open-Meteo'}
{state === 'loading' ? '…' : (state === 'error' ? (lang === 'de' ? 'Erneut' : 'Retry') : (lang === 'de' ? 'Wetter anzeigen' : 'Show weather'))}
);
};
// ── Consent-Map (OpenStreetMap) ───────────────────────────────────────────────
const ConsentMap = ({ lang = 'de' }) => {
const consent = useConsent();
const [load, setLoad] = React.useState(false);
React.useEffect(() => { if (consent.external) setLoad(true); }, [consent.external]);
if (load) {
return (
);
}
return (
{lang === 'de' ? 'Beim Anzeigen der Karte werden Daten an OpenStreetMap übertragen.' : 'Loading the map transmits data to OpenStreetMap.'}
setLoad(true)} style={{ background: 'var(--green)', color: '#fff', border: 'none', borderRadius: 5, padding: '9px 18px', fontFamily: 'var(--font-body)', fontSize: 13, fontWeight: 600, cursor: 'pointer' }}>
{lang === 'de' ? 'Karte anzeigen' : 'Show map'}
);
};
// ── Verfügbarkeits-Kalender ───────────────────────────────────────────────────
const STORAGE_KEY = 'pd_availability_v1';
const loadData = () => {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch { return {}; }
};
const saveLocal = (d) => localStorage.setItem(STORAGE_KEY, JSON.stringify(d));
const API = 'verfuegbarkeit.php';
const toKey = (y, m, d) => `${y}-${String(m+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const MonthCalendar = ({ year, month, room, data, adminMode, onToggle, tV }) => {
const firstDay = new Date(year, month, 1).getDay(); // 0=Sun
const daysInMonth = new Date(year, month + 1, 0).getDate();
// shift so Mon=0
const offset = (firstDay + 6) % 7;
const cells = [];
for (let i = 0; i < offset; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
while (cells.length % 7 !== 0) cells.push(null);
const today = new Date();
const todayKey = toKey(today.getFullYear(), today.getMonth(), today.getDate());
return (
{tV.months[month]} {year}
{tV.days.map(d => (
{d}
))}
{cells.map((day, idx) => {
if (!day) return
;
const key = toKey(year, month, day);
const isBooked = !!(data[room] && data[room][key]);
const isPast = key < todayKey;
const isToday = key === todayKey;
let bg = '#f0f7f1', color = '#2d5a35', border = 'transparent';
if (isBooked) { bg = '#fdecea'; color = '#c0392b'; }
if (isPast) { bg = '#f5f5f5'; color = '#bbb'; }
if (isToday) { border = 'var(--apricot)'; }
return (
!isPast && adminMode && onToggle(room, key)}
style={{ aspectRatio: '1', display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 4, background: bg, color, fontFamily: 'var(--font-body)', fontSize: 11, fontWeight: isToday ? 700 : 400, border: `1.5px solid ${border}`, cursor: adminMode && !isPast ? 'pointer' : 'default', transition: 'transform 0.1s', userSelect: 'none' }}
onMouseEnter={e => { if (adminMode && !isPast) e.currentTarget.style.transform = 'scale(1.15)'; }}
onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{day}
);
})}
);
};
const Verfuegbarkeit = ({ t }) => {
const tV = t.verfuegbarkeit;
const [data, setData] = React.useState(loadData);
const [adminMode, setAdminMode] = React.useState(false);
const [showLogin, setShowLogin] = React.useState(false);
const [pw, setPw] = React.useState('');
const [pwError, setPwError] = React.useState(false);
const pwRef = React.useRef('');
// Belegung vom Server laden (für alle Besucher gleich)
React.useEffect(() => {
let alive = true;
fetch(API, { cache: 'no-store' })
.then(r => r.ok ? r.json() : {})
.then(d => { if (alive && d && typeof d === 'object') { setData(d); saveLocal(d); } })
.catch(() => {});
return () => { alive = false; };
}, []);
// Tick the clock so the calendar always shows the current month —
// even if the browser tab stays open across midnight or month boundaries.
const [tick, setTick] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 60 * 60 * 1000); // hourly check
return () => clearInterval(id);
}, []);
const now = new Date();
const months = Array.from({ length: 12 }, (_, i) => {
const d = new Date(now.getFullYear(), now.getMonth() + i, 1);
return { year: d.getFullYear(), month: d.getMonth() };
});
const toggle = (room, key) => {
setData(prev => {
const next = { ...prev, [room]: { ...(prev[room] || {}) } };
if (next[room][key]) delete next[room][key]; else next[room][key] = 1;
saveLocal(next);
// Auf dem Server speichern
fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pwRef.current, data: next })
}).then(res => { if (!res.ok) alert('Speichern am Server fehlgeschlagen (Status ' + res.status + '). Änderung wurde NICHT übernommen — bitte erneut versuchen.'); })
.catch(() => { alert('Speichern am Server fehlgeschlagen. Bitte Verbindung prüfen.'); });
return next;
});
};
const login = async () => {
try {
const res = await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: pw, action: 'login' })
});
if (res.ok) {
pwRef.current = pw;
setAdminMode(true); setShowLogin(false); setPw(''); setPwError(false);
} else { setPwError(true); }
} catch { setPwError(true); }
};
const logout = () => { setAdminMode(false); pwRef.current = ''; };
const rooms = ['helene','marille'];
const roomLabels = [tV.helene, tV.marille];
return (
{tV.title}
{tV.title}
{tV.sub}
{!adminMode ? (
setShowLogin(true)} style={{ background: 'none', border: '1px solid var(--border)', borderRadius: 4, cursor: 'pointer', padding: '6px 14px', fontFamily: 'var(--font-body)', fontSize: 11, color: 'var(--muted)', letterSpacing: '0.08em' }}>{tV.adminBtn}
) : (
✓ Admin
{tV.logout}
)}
{/* Login modal */}
{showLogin && (
setShowLogin(false)}>
e.stopPropagation()} style={{ background: '#fff', borderRadius: 10, padding: '36px 32px', width: '100%', maxWidth: 340, boxShadow: '0 20px 60px rgba(0,0,0,0.2)' }}>
{tV.adminTitle}
{tV.adminHint}
{ setPw(e.target.value); setPwError(false); }} onKeyDown={e => e.key === 'Enter' && login()} placeholder={tV.pwLabel} autoFocus
style={{ width: '100%', padding: '11px 14px', fontFamily: 'var(--font-body)', fontSize: 15, border: `1.5px solid ${pwError ? '#c0392b' : 'var(--border)'}`, borderRadius: 5, outline: 'none', marginBottom: 8 }}/>
{pwError &&
{tV.pwWrong}
}
{tV.pwBtn}
setShowLogin(false)} style={{ background: 'none', border: '1px solid var(--border)', borderRadius: 5, padding: '11px 16px', fontFamily: 'var(--font-body)', fontSize: 14, cursor: 'pointer', color: 'var(--muted)' }}>✕
)}
{adminMode && (
✏️ {tV.adminHint}
)}
{/* Legend */}
{rooms.map((room, ri) => (
{roomLabels[ri]}
{months.map(({ year, month }) => (
))}
))}
document.getElementById('kontakt')?.scrollIntoView({ behavior: 'smooth' })} style={{ background: 'var(--apricot)', border: 'none', borderRadius: 4, cursor: 'pointer', padding: '7px 16px', fontFamily: 'var(--font-body)', fontSize: 12, color: '#fff', fontWeight: 600, transition: 'opacity 0.2s' }}
onMouseEnter={e => e.target.style.opacity = 0.85} onMouseLeave={e => e.target.style.opacity = 1}>
{tV.cta}
);
};
// ── Galerie ───────────────────────────────────────────────────────────────────
const Galerie = ({ t }) => {
const g = t.galerie;
const [lb, setLb] = React.useState(null);
const colors = ['#2d5a35','#2e5f8a','#d4833a','#2d5a35','#2e5f8a','#d4833a'];
// Real photos keyed by gallery index (rest fall back to placeholders)
const galleryImgs = { 0: 'photos/eingang.jpeg', 1: 'photos/helene-1.jpeg', 2: 'photos/marille-1.jpeg', 3: 'photos/balkon-aussicht.jpeg', 4: 'photos/caffetheke-1.jpeg', 5: 'photos/umgebung.jpeg', 6: 'photos/ausstattung-kuehlschrank.jpg', 7: 'photos/ausstattung-schrank-tv.jpg', 8: 'photos/ausstattung-ventilator.jpg' };
React.useEffect(() => {
const fn = (e) => { if (lb !== null && e.key === 'ArrowRight') setLb(p => (p+1)%g.labels.length); if (lb !== null && e.key === 'ArrowLeft') setLb(p => (p-1+g.labels.length)%g.labels.length); if (e.key === 'Escape') setLb(null); };
window.addEventListener('keydown', fn); return () => window.removeEventListener('keydown', fn);
}, [lb, g.labels.length]);
return (
{t.nav.galerie} {g.title} {g.sub}
{g.labels.map((label, i) => (
setLb(i)} style={{ cursor: 'pointer', borderRadius: 6, overflow: 'hidden', transition: 'transform 0.25s', position: 'relative' }}
onMouseEnter={e => e.currentTarget.style.transform = 'scale(1.02)'}
onMouseLeave={e => e.currentTarget.style.transform = 'none'}>
{galleryImgs[i]
?
:
}
))}
{lb !== null && (
setLb(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.88)', zIndex: 200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
{ e.stopPropagation(); setLb(lb > 0 ? lb-1 : g.labels.length-1); }} style={{ position: 'fixed', left: 20, top: '50%', transform: 'translateY(-50%)', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%', width: 44, height: 44, cursor: 'pointer', color: '#fff', fontSize: 22, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>‹
e.stopPropagation()} style={{ maxWidth: 780, width: '100%' }}>
{galleryImgs[lb]
?
:
FOTO: {g.labels[lb]}
}
{g.labels[lb]} · {lb+1}/{g.labels.length}
{ e.stopPropagation(); setLb(lb < g.labels.length-1 ? lb+1 : 0); }} style={{ position: 'fixed', right: 20, top: '50%', transform: 'translateY(-50%)', background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%', width: 44, height: 44, cursor: 'pointer', color: '#fff', fontSize: 22, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>›
setLb(null)} style={{ position: 'fixed', top: 18, right: 18, background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%', width: 36, height: 36, cursor: 'pointer', color: '#fff', fontSize: 18, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>×
)}
);
};
// ── Über uns ──────────────────────────────────────────────────────────────────
const UeberUns = ({ t }) => {
const u = t.ueber;
return (
{u.patrik} & {u.daniel}
{t.nav.ueber}
{u.sub}
{u.text}
);
};
// ── Bewertungen ───────────────────────────────────────────────────────────────
const Bewertungen = ({ t }) => {
const b = t.bewertungen;
if (!b.items || b.items.length === 0) return null;
return (
{b.title}
{b.sub}
{b.items.map((item, i) => (
„{item.text}"
— {item.name}
))}
);
};
// ── Kontakt ───────────────────────────────────────────────────────────────────
const Kontakt = ({ t }) => {
const k = t.kontakt;
const [form, setForm] = React.useState({ name:'', email:'', phone:'', street:'', zip:'', city:'', country:'', arrival:'', departure:'', room:'', guests:'', message:'', website:'' });
const [sent, setSent] = React.useState(false);
const [errors, setErrors] = React.useState({});
const set = (key, val) => setForm(f => ({ ...f, [key]: val }));
const field = (key, label, type='text') => (
{label}{(key==='name'||key==='email')?' *':''}
set(key, e.target.value)}
style={{ fontFamily: 'var(--font-body)', fontSize: 15, padding: '11px 14px', border: `1.5px solid ${errors[key] ? '#c0392b' : 'var(--border)'}`, borderRadius: 5, background: '#fff', color: 'var(--text)', outline: 'none' }}
onFocus={e => e.target.style.borderColor='var(--green)'} onBlur={e => e.target.style.borderColor=errors[key]?'#c0392b':'var(--border)'}/>
{errors[key] && {errors[key]} }
);
const submit = async e => {
e.preventDefault();
const errs = {};
if (!form.name.trim()) errs.name = 'Pflichtfeld';
if (!form.email.includes('@')) errs.email = 'Ungültige E-Mail';
if (Object.keys(errs).length) { setErrors(errs); return; }
try {
const res = await fetch('kontakt.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify({
name: form.name,
email: form.email,
telefon: form.phone,
strasse: form.street,
plz: form.zip,
ort: form.city,
land: form.country,
anreise: form.arrival,
abreise: form.departure,
zimmer: form.room,
personen: form.guests,
nachricht: form.message,
website: form.website || ''
})
});
if (res.ok) { setSent(true); }
else { alert('Fehler beim Senden. Bitte versuchen Sie es erneut oder kontaktieren Sie uns direkt.'); }
} catch {
alert('Keine Verbindung. Bitte versuchen Sie es erneut.');
}
};
return (
{t.nav.kontakt}
{k.title}
{k.sub}
{sent ? (
) : (
)}
);
};
// ── Legal Modals ──────────────────────────────────────────────────────────────
const LegalModal = ({ title, onClose, children }) => {
React.useEffect(() => {
const fn = (e) => { if (e.key === 'Escape') onClose(); };
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', fn);
return () => { document.body.style.overflow = ''; window.removeEventListener('keydown', fn); };
}, []);
return (
e.stopPropagation()} style={{ background: '#fff', borderRadius: 10, width: '100%', maxWidth: 680, maxHeight: '85vh', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 80px rgba(0,0,0,0.25)' }}>
{title}
×
{children}
);
};
const ImpressumContent = () => (
Offenlegung gemäß § 5 E-Commerce-Gesetz (ECG) und § 25 Mediengesetz (MedienG)
Diensteanbieter / Medieninhaber
{[
['Name', 'Patrik Sabata und Daniel Sabata'],
['Anschrift', 'Siedlung Erlahof 47\n3620 Spitz an der Donau\nÖsterreich'],
['Telefon', '+43 699 16110244'],
['E-Mail', 'office@pd-schoeneaussicht.at'],
['Website', 'www.pd-schoeneaussicht.at'],
['Tätigkeit', 'Privatzimmervermietung\n(häusliche Nebenbeschäftigung gemäß § 2 Abs. 1 Z 9 GewO, keine gewerbliche Tätigkeit)'],
['Umsatzsteuer', 'Kleinunternehmerregelung gemäß § 6 Abs. 1 Z 27 UStG – keine UID-Nummer, keine Ausweisung der Umsatzsteuer'],
].map(([k, v], i) => (
{k}
{v}
))}
Anwendbare Rechtsvorschriften
Privatzimmervermietung im Rahmen der häuslichen Nebenbeschäftigung gemäß § 2 Abs. 1 Z 9 Gewerbeordnung (GewO 1994). Die Vermietung erfolgt im Privathaus an höchstens 10 Fremdenbetten und unterliegt nicht der Gewerbeordnung.
Aufsichtsbehörde
Bezirkshauptmannschaft Krems an der Donau, Drinkweldergasse 15, 3500 Krems
Online-Streitbeilegung
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit, die Sie unter ec.europa.eu/consumers/odr finden. Wir sind weder verpflichtet noch bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
Urheberrecht
Die Inhalte dieser Website (Texte, Bilder, Grafiken, Layout) sind urheberrechtlich geschützt. Die Vervielfältigung, Bearbeitung, Verbreitung oder Verwertung außerhalb der Grenzen des Urheberrechts bedarf der schriftlichen Zustimmung der Betreiber.
Haftungsausschluss
Die Inhalte dieser Website wurden mit größter Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann jedoch keine Gewähr übernommen werden. Für Inhalte externer Links übernehmen wir keine Haftung. Für den Inhalt verlinkter Seiten sind ausschließlich deren Betreiber verantwortlich.
);
const DatenschutzContent = () => (
Information gemäß Art. 13 Datenschutz-Grundverordnung (DSGVO)
Verantwortlich für die Verarbeitung personenbezogener Daten auf dieser Website im Sinne der DSGVO sind: Patrik Sabata und Daniel Sabata Siedlung Erlahof 47 3620 Spitz an der Donau, Österreich Telefon: +43 699 16110244 E-Mail: office@pd-schoeneaussicht.at
Der Schutz Ihrer personenbezogenen Daten ist uns ein wichtiges Anliegen. Wir verarbeiten Ihre Daten ausschließlich auf Grundlage der gesetzlichen Bestimmungen (DSGVO, DSG, TKG 2021). In dieser Datenschutzerklärung informieren wir Sie über die wichtigsten Aspekte der Datenverarbeitung im Rahmen unserer Website.
Beim Aufruf unserer Website werden durch unseren Hosting-Anbieter automatisch Informationen erfasst, die Ihr Browser übermittelt:
IP-Adresse
Datum und Uhrzeit des Zugriffs
Aufgerufene Seite / Datei
Übertragene Datenmenge
Browsertyp und Browserversion
Verwendetes Betriebssystem
Referrer-URL (zuvor besuchte Seite)
Zweck: Sicherstellung eines reibungslosen Verbindungsaufbaus, komfortable Nutzung unserer Website, Auswertung der Systemsicherheit und -stabilität.
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
Speicherdauer: Maximal 7 Tage, danach automatische Löschung.
Hosting-Anbieter: easyname GmbH, Canettistraße 5/10, 1100 Wien. Mit dem Anbieter besteht ein Auftragsverarbeitungsvertrag gemäß Art. 28 DSGVO.
Wenn Sie uns per E-Mail, Telefon oder über das Anfrageformular kontaktieren, werden die von Ihnen angegebenen Daten zur Bearbeitung Ihrer Anfrage und für mögliche Anschlussfragen gespeichert.
Erhobene Daten: Name, E-Mail-Adresse, Telefonnummer (optional), Anreise- und Abreisedatum, Anzahl der Personen, Ihre Nachricht.
Zweck: Bearbeitung Ihrer Anfrage und Vertragsanbahnung.
Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO (Vertragsanbahnung) sowie Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).
Übermittlung: Die Daten werden direkt an unsere E-Mail-Adresse office@pd-schoeneaussicht.at gesendet. Keine Speicherung in einer externen Datenbank.
Speicherdauer: So lange wie zur Bearbeitung erforderlich. Bei zustande gekommenen Buchungen gelten gesetzliche Aufbewahrungsfristen (z. B. 7 Jahre gemäß § 132 BAO).
Im Rahmen einer Buchung erheben wir zusätzlich die Daten, die zur Abwicklung des Beherbergungsvertrags und zur Erfüllung gesetzlicher Pflichten erforderlich sind (z. B. Meldepflicht gemäß Meldegesetz, Ortstaxe-Meldung an die Gemeinde Spitz).
Rechtsgrundlage: Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) und Art. 6 Abs. 1 lit. c DSGVO (rechtliche Verpflichtung).
Empfänger: Gemeinde Spitz (Ortstaxe), Steuerberatung, Finanzbehörden.
Speicherdauer: In der Regel 7 Jahre.
Zur Darstellung der Anfahrt binden wir eine Karte des Dienstes OpenStreetMap ein. Anbieter ist die OpenStreetMap Foundation, St John's Innovation Centre, Cowley Road, Cambridge, CB4 0WS, Vereinigtes Königreich.
Bei der Nutzung der Karte wird Ihre IP-Adresse an die Server von OpenStreetMap übermittelt. Die Karte wird erst nach Ihrer aktiven Zustimmung über den Consent-Banner geladen.
Rechtsgrundlage: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).
Datenschutz OSM: wiki.osmfoundation.org
Unsere Website verwendet ausschließlich technisch notwendige Cookies (z. B. zur Speicherung Ihrer Cookie-Einstellungen). Diese werden auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO gesetzt und benötigen keine Einwilligung.
Cookies, die nicht technisch notwendig sind (z. B. zur Einbindung von OpenStreetMap), werden nur nach Ihrer aktiven Zustimmung über unseren Consent-Banner geladen. Sie können Ihre Einwilligung jederzeit über den Link „Cookie-Einstellungen" im Footer widerrufen.
Die verwendeten Schriftarten werden datenschutzfreundlich über Bunny Fonts (BunnyWay d.o.o., Slowenien) bzw. lokal geladen. Eine Übermittlung Ihrer IP-Adresse an Google oder andere US-Drittanbieter findet nicht statt.
Wir verwenden auf unserer Website keine Tracking- oder Analyse-Tools (wie Google Analytics, Facebook Pixel oder Matomo). Es erfolgt keine Auswertung Ihres Nutzungsverhaltens.
Verlinkungen zu Social-Media-Profilen (z. B. Instagram, Facebook) sind einfache Hyperlinks – es werden keine Plugins eingebunden, die automatisch Daten an die Anbieter übermitteln. Eine Datenübertragung findet erst statt, wenn Sie aktiv auf einen Link klicken.
Sie haben gegenüber uns folgende Rechte:
Auskunft (Art. 15 DSGVO)
Berichtigung (Art. 16 DSGVO)
Löschung (Art. 17 DSGVO)
Einschränkung der Verarbeitung (Art. 18 DSGVO)
Datenübertragbarkeit (Art. 20 DSGVO)
Widerspruch (Art. 21 DSGVO)
Widerruf erteilter Einwilligungen (Art. 7 Abs. 3 DSGVO)
Zur Wahrnehmung Ihrer Rechte wenden Sie sich bitte formlos an office@pd-schoeneaussicht.at .
Sie haben das Recht, sich bei der zuständigen Aufsichtsbehörde zu beschweren:
Österreichische Datenschutzbehörde
Barichgasse 40–42, 1030 Wien
Telefon: +43 1 52 152-0 · E-Mail: dsb@dsb.gv.at
Web: www.dsb.gv.at
Wir setzen aktuelle technische und organisatorische Maßnahmen ein, um Ihre personenbezogenen Daten gegen Manipulation, Verlust, Zerstörung oder unbefugten Zugriff zu schützen. Die Website wird über eine verschlüsselte SSL/TLS-Verbindung übertragen.
Durch die Weiterentwicklung unserer Website oder geänderte gesetzliche Vorgaben kann es notwendig werden, diese Datenschutzerklärung anzupassen. Die jeweils aktuelle Fassung kann jederzeit auf unserer Website abgerufen werden.
Stand: Mai 2026
);
// ── AGB Content ───────────────────────────────────────────────────────────────
const AGBContent = () => (
Allgemeine Geschäftsbedingungen für die Hotellerie (AGBH) – Fassung vom 01.01.2016, in der jeweils geltenden Fassung. Auszug der wesentlichen Bestimmungen.
Diese AGB regeln das Vertragsverhältnis zwischen dem Beherberger (P&D Schöne Aussicht) und dem Vertragspartner/Gast. Sondervereinbarungen sind möglich und gegenüber diesen AGB vorrangig.
Beherberger: Natürliche oder juristische Person, die Gäste gegen Entgelt beherbergt.
Gast: Natürliche Person, die Beherbergung in Anspruch nimmt.
Vertragspartner: Person, die als Gast oder für einen Gast den Beherbergungsvertrag abschließt.
Der Beherbergungsvertrag kommt durch die Annahme der Bestellung des Vertragspartners durch den Beherberger zustande. Der Beherberger ist berechtigt, eine Anzahlung zu verlangen. Die Anzahlung ist spätestens 60 Tage vor Beherbergung zu leisten und gilt als Teilzahlung auf das vereinbarte Entgelt.
Check-in: ab 15:00 Uhr am Ankunftstag (sofern keine andere Bezugszeit vereinbart).
Check-out: bis 11:00 Uhr am Abreisetag. Bei verspätetem Auszug kann ein weiterer Tag verrechnet werden.
Bei Rücktritt durch den Vertragspartner fallen folgende Stornogebühren an:
Bis 2 Monate vor Anreise: kostenfreie Stornierung
Ab 3 Monaten bis 1 Monat vor Anreise: 40 %
Ab 1 Monat bis 1 Woche vor Anreise: 70 %
Ab 1 Woche vor Anreise: 90 %
Bei unvorhersehbaren außergewöhnlichen Umständen (z. B. extremer Schneefall, Hochwasser), die die Anreise unmöglich machen, entfällt die Entgeltzahlungspflicht.
Der Beherberger kann eine adäquate Ersatzunterkunft gleicher Qualität zur Verfügung stellen, wenn dies zumutbar und sachlich gerechtfertigt ist. Mehraufwendungen trägt der Beherberger.
Der Vertragspartner erwirbt das Recht auf den üblichen Gebrauch der gemieteten Räume und Einrichtungen. Er ist verpflichtet, spätestens zur Abreise das vereinbarte Entgelt zu bezahlen und haftet für Schäden, die er, der Gast oder seine Begleitung verursachen.
Bei Zahlungsverzug stehen dem Beherberger das gesetzliche Zurückbehaltungs- und Pfandrecht (§§ 970c, 1101 ABGB) zu. Der Beherberger ist verpflichtet, die vereinbarten Leistungen in seinem Standard entsprechenden Umfang zu erbringen.
Der Beherberger haftet gemäß §§ 970 ff ABGB für eingebrachte Sachen, sofern diese ihm oder befugten Personen übergeben wurden. Die Haftung ist auf die jeweils gültige gesetzliche Höchstgrenze beschränkt. Für Kostbarkeiten, Geld und Wertpapiere haftet der Beherberger nur bis € 550,– (sofern nicht anders vereinbart). Die Haftung für leichte Fahrlässigkeit ist gegenüber Konsumenten ausgeschlossen (außer Personenschäden), gegenüber Unternehmern auch für grobe Fahrlässigkeit.
Tiere dürfen nur nach vorheriger Zustimmung des Beherbergers mitgebracht werden. In unserem Haus sind keine Haustiere erlaubt.
Eine Verlängerung des Aufenthalts ist nur mit Zustimmung des Beherbergers möglich. Bei höherer Gewalt (z. B. unmöglicher Abreise) verlängert sich der Vertrag automatisch; der Preis kann dann auf Nebensaisontarif reduziert sein.
Bei vorzeitiger Abreise ist das volle vereinbarte Entgelt zu zahlen, abzüglich der Ersparnis des Beherbergers (sofern die Räume anderweitig vermietet werden konnten). Der Beherberger kann aus wichtigen Gründen (z. B. grob ungehöriges Verhalten, ansteckende Krankheit, Zahlungsverzug) den Vertrag mit sofortiger Wirkung auflösen.
Bei Erkrankung sorgt der Beherberger auf Wunsch oder bei Gefahr in Verzug für ärztliche Betreuung auf Kosten des Gastes. Bei Todesfall haftet der Rechtsnachfolger für offene Kosten (Arzt, Krankentransport, Desinfektion, Zimmerwiederherstellung etc.).
Erfüllungsort: Sitz des Beherbergers (3620 Spitz an der Donau).
Rechtswahl: Österreichisches formelles und materielles Recht unter Ausschluss des UN-Kaufrechts und der Verweisungsnormen des Internationalen Privatrechts.
Gerichtsstand: Für Konsumenten gelten die zwingenden Konsumentenschutzbestimmungen (Wohnsitz des Verbrauchers).
Nichtraucherhaus – Rauchen nur im Freien
Keine Haustiere
Ruhezeiten: 22:00 – 07:00 Uhr
Kostenlose Parkplätze und Fahrradabstellplatz vorhanden
Mindestaufenthalt am Wochenende: 2 Nächte
Vollständige AGBH 2016 in der jeweils geltenden Fassung. Auf Anfrage übermitteln wir Ihnen gerne den vollständigen Text per E-Mail.
Stand: Mai 2026
);
const Section = ({ title, children }) => (
);
// ── Footer ────────────────────────────────────────────────────────────────────
const Footer = ({ t, lang = 'de' }) => {
const f = t.footer;
const [modal, setModal] = React.useState(null); // 'impressum' | 'datenschutz' | null
return (
<>
{modal === 'impressum' && (
setModal(null)}>
)}
{modal === 'datenschutz' && (
setModal(null)}>
)}
{modal === 'agb' && (
setModal(null)}>
)}
Kontakt
office@pd-schoeneaussicht.at +43 699 16110244
{f.rights}
setModal('impressum')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 12, color: 'rgba(255,255,255,0.4)', textDecoration: 'underline', padding: 0 }}>{f.impressum}
setModal('datenschutz')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 12, color: 'rgba(255,255,255,0.4)', textDecoration: 'underline', padding: 0 }}>{f.datenschutz}
setModal('agb')} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 12, color: 'rgba(255,255,255,0.4)', textDecoration: 'underline', padding: 0 }}>{lang === 'de' ? 'AGB' : 'Terms'}
window.__openConsent && window.__openConsent()} style={{ background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-body)', fontSize: 12, color: 'rgba(255,255,255,0.4)', textDecoration: 'underline', padding: 0 }}>{lang === 'de' ? 'Cookie-Einstellungen' : 'Cookie Settings'}
>
);
};
// ── App ───────────────────────────────────────────────────────────────────────
const App = () => {
const [lang, setLang] = React.useState('de');
const t = TRANSLATIONS[lang];
return (
);
};
ReactDOM.createRoot(document.getElementById('root')).render( );