feat: complete v2 website relaunch (dark elegance, 50/50 hero, trattoria admin, gutschein shop, enzino bot, i18n)

This commit is contained in:
leonardo committed 2026-09-16 21:35:23 +00:00
1 parent a90af21a8c
commit 4758017563
27 files changed
+3958 -292

No files matched your search

+287
View File
@@ -0,0 +1,287 @@
---
import {
ENZINO_INITIAL_GREETING_DE,
ENZINO_INITIAL_GREETING_EN,
ENZINO_QUICK_QUESTIONS_DE,
ENZINO_QUICK_QUESTIONS_EN
} from '../data/enzinoPrompt';
interface Props {
lang?: 'de' | 'en';
}
const { lang = 'de' } = Astro.props;
const initialGreeting = lang === 'de' ? ENZINO_INITIAL_GREETING_DE : ENZINO_INITIAL_GREETING_EN;
const quickQuestions = lang === 'de' ? ENZINO_QUICK_QUESTIONS_DE : ENZINO_QUICK_QUESTIONS_EN;
---
<div id="enzino-widget" class="fixed bottom-6 right-6 z-50 font-sans" data-lang={lang}>
<!-- Floating Launcher Button -->
<button
id="enzino-launcher"
class="relative group flex items-center gap-3 bg-[#24211D] border border-[#D4A373]/30 hover:border-[#D4A373] text-[#E6D5C3] px-4 py-3 rounded-full shadow-2xl transition-all duration-300 hover:scale-105"
aria-label="Enzino Concierge Chat öffnen"
>
<div class="relative w-10 h-10 rounded-full bg-gradient-to-br from-[#C85A32] to-[#D4A373] p-0.5 flex items-center justify-center shadow-inner">
<div class="w-full h-full rounded-full bg-[#1A1816] flex items-center justify-center text-lg font-serif font-bold text-[#D4A373]">
🇮🇹
</div>
<span class="absolute -top-0.5 -right-0.5 w-3 h-3 bg-emerald-500 rounded-full border-2 border-[#1A1816] animate-pulse"></span>
</div>
<div class="text-left hidden sm:block">
<div class="text-xs font-serif font-bold text-[#D4A373] tracking-wide">Enzino Concierge</div>
<div class="text-[10px] text-[#A3978B]">Online • Frag mich was!</div>
</div>
</button>
<!-- Chat Dialog Window -->
<div
id="enzino-chat-window"
class="hidden fixed bottom-6 right-6 w-[calc(100vw-3rem)] sm:w-[380px] max-h-[580px] h-[85vh] bg-[#24211D] border border-[#D4A373]/30 rounded-3xl shadow-2xl flex-col overflow-hidden transition-all duration-300 z-50"
>
<!-- Header -->
<div class="bg-[#1A1816] border-b border-[#D4A373]/20 p-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="relative w-10 h-10 rounded-full bg-gradient-to-br from-[#C85A32] to-[#D4A373] p-0.5 flex items-center justify-center">
<div class="w-full h-full rounded-full bg-[#24211D] flex items-center justify-center text-lg">
👨‍🍳
</div>
<span class="absolute bottom-0 right-0 w-2.5 h-2.5 bg-emerald-500 rounded-full border border-[#1A1816]"></span>
</div>
<div>
<h3 class="font-serif text-sm font-bold text-[#E6D5C3]">Enzino da Enzo</h3>
<p class="text-[10px] text-[#D4A373]">Digitaler Host & Concierge</p>
</div>
</div>
<div class="flex items-center gap-2">
<!-- Session Disable Button -->
<button
id="enzino-disable-btn"
title={lang === 'de' ? 'Enzino für diese Session deaktivieren' : 'Disable Enzino for this session'}
class="p-1.5 text-[#A3978B] hover:text-amber-400 rounded-lg hover:bg-[#2A2622] text-xs transition-colors"
>
🚫
</button>
<!-- Close Button -->
<button
id="enzino-close-btn"
class="p-1.5 text-[#A3978B] hover:text-white rounded-lg hover:bg-[#2A2622] transition-colors"
aria-label="Chat schließen"
>
✕
</button>
</div>
</div>
<!-- Messages Body -->
<div id="enzino-messages" class="flex-1 p-4 overflow-y-auto space-y-3.5 text-xs text-[#E6D5C3]">
<!-- Initial Bot Message -->
<div class="flex gap-2.5 items-start">
<div class="w-7 h-7 rounded-full bg-[#C85A32]/20 border border-[#C85A32]/40 flex items-center justify-center text-xs shrink-0">
🇮🇹
</div>
<div class="bg-[#2E2A25] border border-[#D4A373]/15 p-3 rounded-2xl rounded-tl-none max-w-[85%] leading-relaxed shadow-sm">
{initialGreeting}
</div>
</div>
<!-- Quick Chips -->
<div id="enzino-chips" class="flex flex-wrap gap-1.5 pt-2 pl-9">
{quickQuestions.map((q) => (
<button
class="enzino-chip text-[11px] bg-[#1A1816] hover:bg-[#C85A32]/20 text-[#D4A373] border border-[#D4A373]/30 hover:border-[#D4A373] px-3 py-1.5 rounded-full transition-all text-left"
data-question={q}
>
{q}
</button>
))}
</div>
</div>
<!-- Typing Indicator (hidden by default) -->
<div id="enzino-typing" class="hidden px-4 py-2 text-[11px] text-[#D4A373] italic flex items-center gap-2 bg-[#1A1816]/50">
<span class="w-2 h-2 bg-[#D4A373] rounded-full animate-ping"></span>
<span>Enzino tippt...</span>
</div>
<!-- Input Footer -->
<form id="enzino-form" class="bg-[#1A1816] border-t border-[#D4A373]/20 p-3 flex gap-2">
<input
type="text"
id="enzino-input"
placeholder={lang === 'de' ? 'Stelle Enzino eine Frage...' : 'Ask Enzino a question...'}
class="flex-1 bg-[#24211D] border border-[#D4A373]/20 focus:border-[#D4A373] text-[#E6D5C3] placeholder-[#A3978B] text-xs rounded-xl px-3 py-2.5 outline-none transition-all"
autocomplete="off"
/>
<button
type="submit"
class="bg-[#C85A32] hover:bg-[#b04d2a] text-white px-3.5 py-2.5 rounded-xl text-xs font-bold transition-all shrink-0"
>
➔
</button>
</form>
</div>
</div>
<script>
function initEnzino() {
const launcher = document.getElementById('enzino-launcher');
const windowEl = document.getElementById('enzino-chat-window');
const closeBtn = document.getElementById('enzino-close-btn');
const disableBtn = document.getElementById('enzino-disable-btn');
const form = document.getElementById('enzino-form') as HTMLFormElement;
const input = document.getElementById('enzino-input') as HTMLInputElement;
const messagesEl = document.getElementById('enzino-messages');
const typingEl = document.getElementById('enzino-typing');
const widget = document.getElementById('enzino-widget');
if (!launcher || !windowEl || !closeBtn || !messagesEl) return;
// Check if user disabled Enzino for session
if (sessionStorage.getItem('enzino_disabled') === 'true') {
if (widget) widget.style.display = 'none';
return;
}
const lang = widget?.getAttribute('data-lang') || 'de';
// Toggle Chat
launcher.addEventListener('click', () => {
windowEl.classList.remove('hidden');
windowEl.classList.add('flex');
launcher.classList.add('hidden');
if (input) input.focus();
});
closeBtn.addEventListener('click', () => {
windowEl.classList.add('hidden');
windowEl.classList.remove('flex');
launcher.classList.remove('hidden');
});
// Disable for session
if (disableBtn) {
disableBtn.addEventListener('click', () => {
if (confirm(lang === 'de' ? 'Enzino für diese Browser-Sitzung ausblenden?' : 'Disable Enzino for this session?')) {
sessionStorage.setItem('enzino_disabled', 'true');
if (widget) widget.style.display = 'none';
}
});
}
// Quick chips click
document.querySelectorAll('.enzino-chip').forEach((chip) => {
chip.addEventListener('click', (e) => {
const q = (e.currentTarget as HTMLElement).getAttribute('data-question');
if (q) {
handleUserQuery(q);
}
});
});
// Form submit
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
const text = input.value.trim();
if (text) {
input.value = '';
handleUserQuery(text);
}
});
}
function appendUserMessage(text: string) {
const div = document.createElement('div');
div.className = 'flex justify-end';
div.innerHTML = `
<div class="bg-[#2D5A43] text-[#E6D5C3] p-3 rounded-2xl rounded-tr-none max-w-[85%] leading-relaxed shadow-sm">
${escapeHtml(text)}
</div>
`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function appendBotMessage(text: string) {
const div = document.createElement('div');
div.className = 'flex gap-2.5 items-start';
div.innerHTML = `
<div class="w-7 h-7 rounded-full bg-[#C85A32]/20 border border-[#C85A32]/40 flex items-center justify-center text-xs shrink-0">
🇮🇹
</div>
<div class="bg-[#2E2A25] border border-[#D4A373]/15 p-3 rounded-2xl rounded-tl-none max-w-[85%] leading-relaxed shadow-sm">
${text}
</div>
`;
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function escapeHtml(str: string) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function handleUserQuery(text: string) {
appendUserMessage(text);
// Show typing
if (typingEl) typingEl.classList.remove('hidden');
setTimeout(() => {
if (typingEl) typingEl.classList.add('hidden');
const reply = generateEnzinoReply(text, lang);
appendBotMessage(reply);
}, 700);
}
function generateEnzinoReply(query: string, language: string): string {
const q = query.toLowerCase();
if (q.includes('check-in') || q.includes('checkin') || q.includes('anreise') || q.includes('nuki') || q.includes('schlüssel') || q.includes('key')) {
return language === 'de'
? "<strong>Perfetto!</strong> Der Check-in verläuft 100% kontaktlos und digital. Du erhältst vor deiner Anreise deinen persönlichen Nuki-PIN-Code per WhatsApp oder E-Mail. Check-in ist ab <strong>15:00 Uhr</strong> möglich. Bei Fragen erreichst du Alja direkt über <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a>."
: "<strong>Perfetto!</strong> Check-in is 100% digital & contactless. You'll receive your personal Nuki PIN code via WhatsApp or email prior to arrival. Check-in starts at <strong>3:00 PM</strong>. For queries, text Alja directly via <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a>.";
}
if (q.includes('tisch') || q.includes('trattoria') || q.includes('essen') || q.includes('pasta') || q.includes('reservier') || q.includes('restaurant') || q.includes('speisekarte')) {
return language === 'de'
? "<strong>Mamma Mia!</strong> In Alessandras Trattoria wartet ehrliche sizilianische Küche auf dich! Öffnungszeiten: Di–So 17:00–22:30 Uhr (Montag Ruhetag). Hotelgäste genießen bevorzugte Tischreservierungen! Für Reservierungen rufe direkt unser Trattoria-Festnetz an: <a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>."
: "<strong>Mamma Mia!</strong> Honest Sicilian cuisine awaits you at Alessandra's Trattoria! Hours: Tue–Sun 5:00 PM–10:30 PM (Mon closed). Apartment guests get priority table reservations! Call our Trattoria landline: <a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>.";
}
if (q.includes('bestpreis') || q.includes('preis') || q.includes('rabatt') || q.includes('booking') || q.includes('direkt')) {
return language === 'de'
? "<strong>Bellissimo!</strong> Wenn du direkt auf unserer Website buchst, sparst du dir <strong>15% OTA-Gebühren</strong> gegenüber Booking.com oder Airbnb. Wir garantieren dir den besten verfügbaren Preis!"
: "<strong>Bellissimo!</strong> Booking directly on our website saves you <strong>15% in OTA commissions</strong> compared to Booking.com or Airbnb. We guarantee the best available rate!";
}
if (q.includes('eviivo') || q.includes('gästebuch') || q.includes('guest') || q.includes('directory') || q.includes('mappe')) {
return language === 'de'
? "<strong>Prego!</strong> In unserem <a href='https://casa-daenzo.de/eviivo-guest' target='_blank' class='text-[#D4A373] underline'>Eviivo Digital Guest Directory</a> findest du alle Details zu deinem Aufenthalt, WLAN-Passwort und Tipps für Goslar. Logge dich einfach mit deiner Buchungsnummer und deinem Nachnamen ein!"
: "<strong>Prego!</strong> In our <a href='https://casa-daenzo.de/eviivo-guest' target='_blank' class='text-[#D4A373] underline'>Eviivo Digital Guest Directory</a> you'll find all details about your stay, Wi-Fi password, and local tips. Simply log in with your booking number and last name!";
}
if (q.includes('kontakt') || q.includes('telefon') || q.includes('nummer') || q.includes('whatsapp') || q.includes('anja') || q.includes('alja')) {
return language === 'de'
? "<strong>Hier ist unsere Kontakttrennung:</strong><br/>• <strong>Apartments & Buchungen:</strong> Alja per <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a><br/>• <strong>Trattoria & Tische:</strong> Festnetz <a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>"
: "<strong>Here is our contact separation:</strong><br/>• <strong>Apartments & Bookings:</strong> Alja via <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a><br/>• <strong>Trattoria & Tables:</strong> Landline <a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>";
}
if (q.includes('parken') || q.includes('parking') || q.includes('auto') || q.includes('garagen')) {
return language === 'de'
? "<strong>Molto bene!</strong> Zum Ausladen deines Gepäcks kannst du direkt vor das Haus (Bäckerstraße 18) fahren. Öffentliche Parkplätze & das Parkhaus 'Kaiserpassage' liegen nur 2-3 Gehminuten entfernt."
: "<strong>Molto bene!</strong> You can stop directly in front of the building (Bäckerstraße 18) to unload luggage. Public parking and 'Kaiserpassage' parking garage are just 2-3 minutes walk away.";
}
return language === 'de'
? "<strong>Grazie für deine Frage!</strong> Bei allen Anliegen zu den Apartments schreib Alja per <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a>. Für Tischreservierungen rufe die Trattoria an (<a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>)!"
: "<strong>Grazie for your question!</strong> For apartment inquiries, message Alja via <a href='https://wa.me/4915207483196' target='_blank' class='text-[#D4A373] underline'>WhatsApp (+49 1520 7483196)</a>. For table reservations, call the Trattoria (<a href='tel:+49532123456' class='text-[#D4A373] underline'>+49 5321 23456</a>)!";
}
}
// Run on page load and view transitions
document.addEventListener('DOMContentLoaded', initEnzino);
document.addEventListener('astro:page-load', initEnzino);
</script>