/* ==========================================================================
HSK-Vorlage – Öffnungszeiten (live, inkl. Feiertage des Bundeslands) & Kontaktformular
========================================================================== */
(function (w, d) {
'use strict';
/* ---------- Öffnungszeiten ---------- */
// Bundesland des Betriebs für die Feiertage:
// BW BY BE BB HB HH HE MV NI NW RP SL SN ST SH TH
var BUNDESLAND = 'BY';
// Minuten seit Mitternacht; Index = Wochentag (0 = Sonntag)
var SCHEDULE = {
0: [],
1: [[420, 720], [780, 990]],
2: [[420, 720], [780, 990]],
3: [[420, 720], [780, 990]],
4: [[420, 720], [780, 990]],
5: [[420, 690]],
6: []
};
var DAYS = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
function fmt(min) {
var h = Math.floor(min / 60), m = min % 60;
return h + ':' + (m < 10 ? '0' : '') + m;
}
function easter(y) {
var a = y % 19, b = Math.floor(y / 100), c = y % 100, dd = Math.floor(b / 4), e = b % 4;
var f = Math.floor((b + 8) / 25), g = Math.floor((b - f + 1) / 3);
var h = (19 * a + b - dd - g + 15) % 30, i = Math.floor(c / 4), k = c % 4;
var l = (32 + 2 * e + 2 * i - h - k) % 7, m = Math.floor((a + 11 * h + 22 * l) / 451);
var month = Math.floor((h + l - 7 * m + 114) / 31), day = ((h + l - 7 * m + 114) % 31) + 1;
return Date.UTC(y, month - 1, day);
}
// Gesetzliche Feiertage: bundesweit plus die des eingestellten Bundeslands.
// Nur landesweite Feiertage; Fronleichnam in Teilen von Sachsen/Thüringen und
// Mariä Himmelfahrt in Bayern gelten nicht überall (BY: hier mitgezählt, wie in
// Gemeinden mit überwiegend katholischer Bevölkerung).
var REGIONAL = {
dreikoenige: ['BW', 'BY', 'ST'],
frauentag: ['BE', 'MV'],
fronleichnam: ['BW', 'BY', 'HE', 'NW', 'RP', 'SL'],
mariae: ['BY', 'SL'],
kindertag: ['TH'],
reformation: ['BB', 'HB', 'HH', 'MV', 'NI', 'SN', 'ST', 'SH', 'TH'],
allerheiligen: ['BW', 'BY', 'NW', 'RP', 'SL'],
busstag: ['SN']
};
function inLand(key) { return REGIONAL[key].indexOf(BUNDESLAND) > -1; }
var holidayCache = {};
function holidays(y) {
if (holidayCache[y]) return holidayCache[y];
var DAY = 864e5, e = easter(y), map = {};
var add = function (t, name) { var dt = new Date(t); map[dt.getUTCMonth() + 1 + '-' + dt.getUTCDate()] = name; };
add(Date.UTC(y, 0, 1), 'Neujahr');
if (inLand('dreikoenige')) add(Date.UTC(y, 0, 6), 'Heilige Drei Könige');
if (inLand('frauentag')) add(Date.UTC(y, 2, 8), 'Internationaler Frauentag');
add(e - 2 * DAY, 'Karfreitag');
add(e + 1 * DAY, 'Ostermontag');
add(Date.UTC(y, 4, 1), 'Tag der Arbeit');
add(e + 39 * DAY, 'Christi Himmelfahrt');
add(e + 50 * DAY, 'Pfingstmontag');
if (inLand('fronleichnam')) add(e + 60 * DAY, 'Fronleichnam');
if (inLand('mariae')) add(Date.UTC(y, 7, 15), 'Mariä Himmelfahrt');
if (inLand('kindertag')) add(Date.UTC(y, 8, 20), 'Weltkindertag');
add(Date.UTC(y, 9, 3), 'Tag der Deutschen Einheit');
if (inLand('reformation')) add(Date.UTC(y, 9, 31), 'Reformationstag');
if (inLand('allerheiligen')) add(Date.UTC(y, 10, 1), 'Allerheiligen');
if (inLand('busstag')) {
// Mittwoch vor dem 23. November
var nov23 = Date.UTC(y, 10, 23);
add(nov23 - (((new Date(nov23).getUTCDay() + 4) % 7) || 7) * DAY, 'Buß- und Bettag');
}
add(Date.UTC(y, 11, 25), '1. Weihnachtstag');
add(Date.UTC(y, 11, 26), '2. Weihnachtstag');
return (holidayCache[y] = map);
}
function holidayName(y, m, day) { return holidays(y)[m + '-' + day] || null; }
function berlinNow() {
var now = new Date();
try {
var parts = new Intl.DateTimeFormat('de-DE', {
timeZone: 'Europe/Berlin', year: 'numeric', month: 'numeric', day: 'numeric',
hour: 'numeric', minute: 'numeric', hourCycle: 'h23'
}).formatToParts(now);
var get = function (t) { for (var i = 0; i < parts.length; i++) if (parts[i].type === t) return parseInt(parts[i].value, 10); return 0; };
var y = get('year'), mo = get('month'), da = get('day');
return { y: y, m: mo, d: da, mins: get('hour') * 60 + get('minute'), wd: new Date(Date.UTC(y, mo - 1, da)).getUTCDay() };
} catch (e) {
return { y: now.getFullYear(), m: now.getMonth() + 1, d: now.getDate(), mins: now.getHours() * 60 + now.getMinutes(), wd: now.getDay() };
}
}
function computeStatus() {
var n = berlinNow();
var hol = holidayName(n.y, n.m, n.d);
var slots = hol ? [] : SCHEDULE[n.wd];
var i;
for (i = 0; i < slots.length; i++) {
if (n.mins >= slots[i][0] && n.mins < slots[i][1]) {
return { state: 'open', text: 'Geöffnet bis ' + fmt(slots[i][1]) + '\u00a0Uhr', wd: n.wd };
}
}
if (slots.length > 1 && n.mins >= slots[0][1] && n.mins < slots[1][0]) {
return { state: 'break', text: 'Mittagspause bis ' + fmt(slots[1][0]) + '\u00a0Uhr', wd: n.wd };
}
if (slots.length && n.mins < slots[0][0]) {
return { state: 'closed', text: 'Öffnet heute um ' + fmt(slots[0][0]) + '\u00a0Uhr', wd: n.wd };
}
var base = Date.UTC(n.y, n.m - 1, n.d);
for (i = 1; i <= 14; i++) {
var dt = new Date(base + i * 864e5);
var wd = dt.getUTCDay();
var s = SCHEDULE[wd];
if (!s.length || holidayName(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate())) continue;
var when = i === 1 ? 'morgen' : (i < 7 ? DAYS[wd] : 'am ' + dt.getUTCDate() + '.' + (dt.getUTCMonth() + 1) + '.');
return { state: 'closed', text: (hol ? 'Feiertag · öffnet ' : 'Öffnet ') + when + ' um ' + fmt(s[0][0]) + '\u00a0Uhr', wd: n.wd };
}
return { state: 'closed', text: 'Geschlossen', wd: n.wd };
}
var statusEls = d.querySelectorAll('[data-status]');
var dayRows = d.querySelectorAll('[data-days]');
function renderStatus() {
var st = computeStatus();
Array.prototype.forEach.call(statusEls, function (el) {
el.classList.remove('is-open', 'is-break', 'is-closed');
el.classList.add('is-' + st.state);
var t = el.querySelector('.status__text');
if (t) t.textContent = st.text;
});
Array.prototype.forEach.call(dayRows, function (row) {
var days = row.getAttribute('data-days').split(',').map(Number);
row.classList.toggle('is-today', days.indexOf(st.wd) > -1);
});
}
renderStatus();
w.setInterval(renderStatus, 60000);
/* ---------- Kontaktformular ---------- */
var form = d.querySelector('[data-contact-form]');
if (!form) return;
var statusBox = form.querySelector('[data-form-status]');
var ts = form.querySelector('[data-ts]');
if (ts) ts.value = String(Date.now());
var RECIPIENT = 'kontakt@example.com';
var fields = {
name: form.querySelector('[name="name"]'),
email: form.querySelector('[name="email"]'),
message: form.querySelector('[name="message"]'),
privacy: form.querySelector('[name="privacy"]')
};
Object.keys(fields).forEach(function (k) {
var el = fields[k];
if (!el) return;
el.addEventListener(el.type === 'checkbox' ? 'change' : 'input', function () {
if (el.closest('.field').classList.contains('is-invalid')) validateField(k);
});
});
function validateField(k) {
var el = fields[k], ok = true;
if (k === 'email') ok = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(el.value.trim());
else if (k === 'privacy') ok = el.checked;
else if (k === 'message') ok = el.value.trim().length >= 5;
else ok = el.value.trim().length > 0;
el.closest('.field').classList.toggle('is-invalid', !ok);
el.setAttribute('aria-invalid', ok ? 'false' : 'true');
// Fehlertext nur verknüpfen, solange er sichtbar ist
if (ok) el.removeAttribute('aria-describedby'); else el.setAttribute('aria-describedby', 'e-' + k);
return ok;
}
function setStatus(type, html) {
statusBox.className = 'form__status' + (type ? ' is-' + type : '');
statusBox.innerHTML = html || '';
}
function selectTopic(topic) {
if (!topic) return;
var radios = form.querySelectorAll('[name="topic"]');
Array.prototype.forEach.call(radios, function (r) { r.checked = (r.value === topic); });
}
w.addEventListener('hsk:topic', function (e) { selectTopic(e.detail); });
w.addEventListener('hsk:prefill', function (e) {
var det = e.detail || {};
selectTopic(det.topic);
if (det.message && fields.message) {
fields.message.value = det.message;
if (fields.message.closest('.field').classList.contains('is-invalid')) validateField('message');
}
});
function buildMail(data) {
var topic = data.get('topic') || 'Allgemeine Anfrage';
var lines = [
'Anliegen: ' + topic,
'Name: ' + (data.get('name') || ''),
'E-Mail: ' + (data.get('email') || ''),
'Telefon: ' + (data.get('phone') || '–'),
'PLZ / Ort: ' + (data.get('place') || '–'),
'',
String(data.get('message') || '')
];
return 'mailto:' + RECIPIENT +
'?subject=' + encodeURIComponent('Anfrage über example.com – ' + topic) +
'&body=' + encodeURIComponent(lines.join('\n'));
}
form.addEventListener('submit', function (e) {
e.preventDefault();
var valid = ['name', 'email', 'message', 'privacy'].map(validateField);
if (valid.indexOf(false) > -1) {
var firstBad = form.querySelector('.is-invalid input, .is-invalid textarea');
if (firstBad) firstBad.focus();
setStatus('error', 'Bitte prüfen Sie die markierten Felder.');
return;
}
var data = new FormData(form);
form.classList.add('is-busy');
setStatus('', '');
var done = function (ok, serverValidation) {
form.classList.remove('is-busy');
if (ok) {
form.reset();
if (ts) ts.value = String(Date.now());
setStatus('success', 'Vielen Dank! Ihre Anfrage ist bei uns angekommen. Wir melden uns schnellstmöglich bei Ihnen.');
} else if (serverValidation) {
setStatus('error', 'Bitte prüfen Sie Ihre Angaben – einige Felder sind unvollständig.');
} else {
w.location.href = buildMail(data);
setStatus('info', 'Fast geschafft: Ihr E-Mail-Programm öffnet sich mit Ihrer vorbereiteten Anfrage – bitte dort nur noch auf „Senden“ klicken. Alternativ erreichen Sie uns unter 01234 567890.');
}
};
if (!/^https?:$/.test(w.location.protocol) || !w.fetch) { done(false); return; }
w.fetch(form.getAttribute('action'), { method: 'POST', body: data, headers: { Accept: 'application/json' } })
.then(function (res) {
return res.text().then(function (txt) {
var json = null;
try { json = JSON.parse(txt); } catch (err) { json = null; }
if (res.status === 422 && json) return done(false, true);
done(!!(res.ok && json && json.ok === true));
});
})
.catch(function () { done(false); });
});
})(window, document);