- One-Page-Site nach Design-Handoff: Hero mit scroll-gesteuertem Bus, Stats, Leistungen, Foto-Band, Flotte, interaktive MapLibre-Karte, Karriere, Footer - Vollständige Handy-Optimierung: Hamburger-Menü, responsive Grids, touch-freundliche Karte - Bus-Foto lokal in assets/ Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
261 lines
9.8 KiB
JavaScript
261 lines
9.8 KiB
JavaScript
/* =========================================================
|
|
Unger Reisen — behaviour
|
|
- render data-driven sections (stats / services / fleet)
|
|
- scroll-driven bus across the hero
|
|
- nav solidify on scroll
|
|
- reveal-on-scroll (IntersectionObserver)
|
|
- interactive MapLibre map (two locations + route)
|
|
========================================================= */
|
|
(function () {
|
|
'use strict';
|
|
|
|
var ACCENT = '#C8102E';
|
|
var reduceMotion = window.matchMedia &&
|
|
window.matchMedia('(prefers-reduced-motion:reduce)').matches;
|
|
|
|
/* ---------- Data ---------- */
|
|
var stats = [
|
|
{ big: '2', label: 'Standorte in Bayern' },
|
|
{ big: '60+', label: 'Sitzplätze pro Reisebus' },
|
|
{ big: '24/7', label: 'Für Sie erreichbar' },
|
|
{ big: '∞', label: 'Ziele in ganz Europa' }
|
|
];
|
|
|
|
var services = [
|
|
{ no: '01', title: 'Busanmietung', desc: 'Moderne Reisebusse mit Fahrer — flexibel für einzelne Fahrten oder mehrtägige Touren.' },
|
|
{ no: '02', title: 'Firmenreisen', desc: 'Betriebsausflüge, Tagungen und Events. Ihr Team sicher und entspannt am Ziel.' },
|
|
{ no: '03', title: 'Vereinsfahrten', desc: 'Ausflüge, Feste und Sportevents — komfortabel für die ganze Gruppe.' },
|
|
{ no: '04', title: 'Transfers', desc: 'Flughafen-, Hotel- und Event-Transfers. Pünktlich und rund um die Uhr planbar.' }
|
|
];
|
|
|
|
var fleet = [
|
|
{ name: 'Reisebus', seats: '60', desc: 'Der Große für lange Strecken und große Gruppen — voll ausgestattet.', tags: ['Klimaanlage', 'WC', 'Bordküche', 'WLAN'] },
|
|
{ name: 'Midibus', seats: '35', desc: 'Wendig und komfortabel — ideal für mittlere Gruppen und Tagestouren.', tags: ['Klimaanlage', 'Komfortbestuhlung'] },
|
|
{ name: 'Kleinbus', seats: '19', desc: 'Persönlich und flexibel für kleine Gruppen, Transfers und Sonderfahrten.', tags: ['Flexibel', 'Wendig'] }
|
|
];
|
|
|
|
function el(tag, cls, html) {
|
|
var n = document.createElement(tag);
|
|
if (cls) n.className = cls;
|
|
if (html != null) n.innerHTML = html;
|
|
return n;
|
|
}
|
|
|
|
/* ---------- Render sections ---------- */
|
|
function renderStats() {
|
|
var grid = document.getElementById('stats-grid');
|
|
if (!grid) return;
|
|
stats.forEach(function (s) {
|
|
var wrap = el('div', null);
|
|
wrap.setAttribute('data-reveal', '');
|
|
wrap.appendChild(el('div', 'display stat__big', s.big));
|
|
wrap.appendChild(el('div', 'stat__label', s.label));
|
|
grid.appendChild(wrap);
|
|
});
|
|
}
|
|
|
|
function renderServices() {
|
|
var grid = document.getElementById('services-grid');
|
|
if (!grid) return;
|
|
services.forEach(function (s) {
|
|
var card = el('div', 'service');
|
|
card.setAttribute('data-reveal', '');
|
|
card.appendChild(el('div', 'display service__no', s.no));
|
|
card.appendChild(el('h3', 'service__title', s.title));
|
|
card.appendChild(el('p', 'service__desc', s.desc));
|
|
grid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function renderFleet() {
|
|
var grid = document.getElementById('fleet-grid');
|
|
if (!grid) return;
|
|
fleet.forEach(function (f) {
|
|
var card = el('div', 'fleetCard');
|
|
card.setAttribute('data-reveal', '');
|
|
|
|
var head = el('div', 'fleetCard__head');
|
|
head.appendChild(el('h3', 'display fleetCard__name', f.name));
|
|
var seats = el('div', 'fleetCard__seats',
|
|
'<span class="fleetCard__seatsWord">bis</span>' +
|
|
'<span class="display fleetCard__seatsNum">' + f.seats + '</span>' +
|
|
'<span class="fleetCard__seatsWord">Sitze</span>');
|
|
head.appendChild(seats);
|
|
card.appendChild(head);
|
|
|
|
var body = el('div', 'fleetCard__body');
|
|
body.appendChild(el('p', 'fleetCard__desc', f.desc));
|
|
var tags = el('div', 'fleetCard__tags');
|
|
f.tags.forEach(function (t) { tags.appendChild(el('span', 'tag', t)); });
|
|
body.appendChild(tags);
|
|
card.appendChild(body);
|
|
|
|
grid.appendChild(card);
|
|
});
|
|
}
|
|
|
|
/* ---------- Nav solidify on scroll ---------- */
|
|
function initNav() {
|
|
var nav = document.getElementById('nav');
|
|
if (!nav) return;
|
|
var onScroll = function () {
|
|
nav.classList.toggle('is-scrolled', window.scrollY > 40);
|
|
};
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
onScroll();
|
|
}
|
|
|
|
/* ---------- Mobile menu ---------- */
|
|
function initMobileMenu() {
|
|
var burger = document.getElementById('nav-burger');
|
|
var menu = document.getElementById('mobile-menu');
|
|
if (!burger || !menu) return;
|
|
|
|
function setOpen(open) {
|
|
menu.classList.toggle('is-open', open);
|
|
burger.classList.toggle('is-open', open);
|
|
burger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
burger.setAttribute('aria-label', open ? 'Menü schließen' : 'Menü öffnen');
|
|
document.body.style.overflow = open ? 'hidden' : '';
|
|
}
|
|
|
|
burger.addEventListener('click', function () {
|
|
setOpen(!menu.classList.contains('is-open'));
|
|
});
|
|
menu.querySelectorAll('a').forEach(function (a) {
|
|
a.addEventListener('click', function () { setOpen(false); });
|
|
});
|
|
window.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') setOpen(false);
|
|
});
|
|
// Close if we grow past the mobile breakpoint.
|
|
window.addEventListener('resize', function () {
|
|
if (window.innerWidth > 680) setOpen(false);
|
|
});
|
|
}
|
|
|
|
/* ---------- Scroll-driven bus ---------- */
|
|
function initBus() {
|
|
var bus = document.getElementById('bus');
|
|
var hero = document.getElementById('top');
|
|
if (!bus || !hero) return;
|
|
|
|
function tick() {
|
|
var hp = Math.min(1, Math.max(0, window.scrollY / (hero.offsetHeight * 0.9)));
|
|
var scene = bus.parentNode;
|
|
var sceneW = scene ? scene.offsetWidth : window.innerWidth;
|
|
var busW = bus.offsetWidth || 400;
|
|
var minX = -busW * 0.30;
|
|
var maxX = sceneW - busW * 0.70;
|
|
var x = minX + (maxX - minX) * hp;
|
|
var bob = reduceMotion ? 0 : Math.sin(Date.now() / 380) * 4;
|
|
bus.style.transform = 'translate(' + x.toFixed(1) + 'px,' + bob.toFixed(1) + 'px)';
|
|
requestAnimationFrame(tick);
|
|
}
|
|
requestAnimationFrame(tick);
|
|
}
|
|
|
|
/* ---------- Reveal on scroll ---------- */
|
|
function initReveal() {
|
|
var els = Array.prototype.slice.call(document.querySelectorAll('[data-reveal]'));
|
|
if ('IntersectionObserver' in window) {
|
|
var io = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (e) {
|
|
if (e.isIntersecting) {
|
|
e.target.classList.add('is-visible');
|
|
io.unobserve(e.target);
|
|
}
|
|
});
|
|
}, { threshold: 0.12 });
|
|
els.forEach(function (n) { io.observe(n); });
|
|
} else {
|
|
els.forEach(function (n) { n.classList.add('is-visible'); });
|
|
}
|
|
// Safety fallback: reveal everything after 2.5s regardless.
|
|
setTimeout(function () {
|
|
els.forEach(function (n) { n.classList.add('is-visible'); });
|
|
}, 2500);
|
|
}
|
|
|
|
/* ---------- Interactive map ---------- */
|
|
function initMap() {
|
|
var HOME = [12.0517, 48.4703]; // Buch am Erlbach / Vatersdorf
|
|
var BRANCH = [12.1508, 48.5449]; // Landshut
|
|
var container = document.getElementById('unger-map');
|
|
if (!container || typeof maplibregl === 'undefined') return;
|
|
|
|
function makePin(fill, ring, title, sub) {
|
|
var wrap = el('div', 'pin');
|
|
wrap.appendChild(el('div', 'pin__label',
|
|
'<div class="pin__title">' + title + '</div>' +
|
|
'<div class="pin__sub">' + sub + '</div>'));
|
|
var dot = el('div', 'pin__dot');
|
|
dot.style.background = fill;
|
|
dot.style.border = '3px solid ' + ring;
|
|
dot.style.boxShadow = '0 0 0 5px ' + ACCENT + '40, 0 2px 6px rgba(0,0,0,.55)';
|
|
wrap.appendChild(dot);
|
|
return wrap;
|
|
}
|
|
|
|
var map = new maplibregl.Map({
|
|
container: container,
|
|
style: 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json',
|
|
center: [(HOME[0] + BRANCH[0]) / 2, (HOME[1] + BRANCH[1]) / 2],
|
|
zoom: 10.4,
|
|
attributionControl: { compact: true }
|
|
});
|
|
map.scrollZoom.disable();
|
|
map.addControl(new maplibregl.NavigationControl({ showCompass: true, visualizePitch: false }), 'top-right');
|
|
|
|
// On phones, keep the map static so a one-finger drag scrolls the page
|
|
// instead of getting trapped panning the map.
|
|
if (window.matchMedia && window.matchMedia('(max-width:680px)').matches) {
|
|
map.dragPan.disable();
|
|
if (map.touchZoomRotate) map.touchZoomRotate.disable();
|
|
if (map.touchPitch) map.touchPitch.disable();
|
|
}
|
|
|
|
new maplibregl.Marker({ element: makePin(ACCENT, '#fff', 'Landshut', 'Niederlassung'), anchor: 'bottom' })
|
|
.setLngLat(BRANCH).addTo(map);
|
|
new maplibregl.Marker({ element: makePin('#fff', ACCENT, 'Buch a. Erlbach', 'Hauptsitz'), anchor: 'bottom' })
|
|
.setLngLat(HOME).addTo(map);
|
|
|
|
map.on('load', function () {
|
|
map.addSource('unger-route', {
|
|
type: 'geojson',
|
|
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: [HOME, BRANCH] } }
|
|
});
|
|
map.addLayer({
|
|
id: 'unger-route-casing', type: 'line', source: 'unger-route',
|
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
paint: { 'line-color': ACCENT, 'line-width': 9, 'line-opacity': 0.18 }
|
|
});
|
|
map.addLayer({
|
|
id: 'unger-route-line', type: 'line', source: 'unger-route',
|
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
paint: { 'line-color': ACCENT, 'line-width': 3, 'line-dasharray': [1.6, 1.4] }
|
|
});
|
|
map.fitBounds([HOME, BRANCH], { padding: { top: 96, bottom: 70, left: 70, right: 70 }, duration: 0, maxZoom: 12 });
|
|
setTimeout(function () { try { map.resize(); } catch (e) {} }, 60);
|
|
});
|
|
}
|
|
|
|
/* ---------- Boot ---------- */
|
|
function init() {
|
|
renderStats();
|
|
renderServices();
|
|
renderFleet();
|
|
initNav();
|
|
initMobileMenu();
|
|
initBus();
|
|
initReveal();
|
|
initMap();
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
})();
|