Files

313 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* PMPNZNG Shop — Client-JS (Vanilla, kein Framework)
* Cart · Passwort-Gate · Toasts · Galerie · Größenwahl · Mobile-Menü · Insta · Exit-Intent
*
* Produktdaten kommen aus dem Astro-Layout (window.__PRODUCTS__),
* damit es nur EINE Quelle der Wahrheit gibt (src/data/products.ts).
* Später ersetzt Medusa sowohl die Render-Daten als auch den Checkout.
*/
'use strict';
const PRODUCTS = Array.isArray(window.__PRODUCTS__) ? window.__PRODUCTS__ : [];
/* ===================== PASSWORT-GATE ===================== */
const PASS_KEY = 'pmpnzng_auth';
const PASS_WORD = null; /* Passwortschutz entfernt */
function initPasswordGate() {
if (sessionStorage.getItem(PASS_KEY) === '1') return;
const overlay = document.createElement('div');
overlay.id = 'pw-overlay';
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', 'Passwortschutz');
overlay.innerHTML = `
<div class="pw-box">
<div class="pw-logo">PMPN<strong>Z</strong>NG</div>
<div class="pw-sub">Vorschau · tgasolutions-shop.de</div>
<p class="pw-desc">Diese Seite ist passwortgeschützt.<br>Bitte gib das Passwort ein, um fortzufahren.</p>
<form class="pw-form" id="pw-form" novalidate>
<input type="password" id="pw-input" class="pw-input" placeholder="Passwort eingeben" autocomplete="current-password" aria-label="Passwort" required>
<button type="submit" class="pw-btn">Eintreten</button>
</form>
<p class="pw-error" id="pw-error" aria-live="polite"></p>
</div>`;
document.body.appendChild(overlay);
document.body.style.overflow = 'hidden';
setTimeout(() => document.getElementById('pw-input')?.focus(), 100);
document.getElementById('pw-form').addEventListener('submit', (e) => {
e.preventDefault();
const val = document.getElementById('pw-input').value;
if (val === PASS_WORD) {
sessionStorage.setItem(PASS_KEY, '1');
overlay.style.transition = 'opacity 0.4s ease';
overlay.style.opacity = '0';
setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 400);
} else {
document.getElementById('pw-error').textContent = 'Falsches Passwort. Bitte versuche es erneut.';
const input = document.getElementById('pw-input');
input.value = ''; input.focus();
const box = overlay.querySelector('.pw-box');
box.classList.add('pw-shake');
setTimeout(() => box.classList.remove('pw-shake'), 500);
}
});
}
/* ===================== CART ===================== */
const CART_KEY = 'pmpnzng_cart';
const getCart = () => { try { return JSON.parse(localStorage.getItem(CART_KEY)) || []; } catch { return []; } };
const saveCart = (cart) => { localStorage.setItem(CART_KEY, JSON.stringify(cart)); updateCartUI(); };
function addToCart(productId, size, qty = 1) {
const cart = getCart();
const product = PRODUCTS.find((p) => p.id === productId);
if (!product) return;
const key = `${productId}_${size}`;
const existing = cart.find((i) => i.key === key);
if (existing) existing.qty += qty;
else cart.push({ key, productId, size, qty, name: product.name, price: product.price, image: product.cardImage });
saveCart(cart);
showToast(`${product.shortName}" wurde in den Warenkorb gelegt.`);
}
const removeFromCart = (key) => saveCart(getCart().filter((i) => i.key !== key));
function updateQty(key, delta) {
const cart = getCart();
const item = cart.find((i) => i.key === key);
if (item) { item.qty = Math.max(1, item.qty + delta); saveCart(cart); }
}
const cartTotal = () => getCart().reduce((s, i) => s + i.price * i.qty, 0);
const cartCount = () => getCart().reduce((s, i) => s + i.qty, 0);
function updateCartUI() {
const count = cartCount();
document.querySelectorAll('.js-cart-count').forEach((el) => {
el.textContent = count;
el.style.display = count > 0 ? 'inline-flex' : 'none';
});
document.querySelectorAll('.js-cart-total').forEach((el) => { el.textContent = formatPrice(cartTotal()); });
}
/* ===================== TOASTS ===================== */
function showToast(message, type = 'success', duration = 3500) {
let c = document.getElementById('toast-container');
if (!c) { c = document.createElement('div'); c.id = 'toast-container'; c.setAttribute('aria-live', 'polite'); document.body.appendChild(c); }
const t = document.createElement('div');
t.className = `toast toast-${type}`;
t.setAttribute('role', 'status');
t.innerHTML = `<span class="toast-icon">${type === 'success' ? '✓' : '!'}</span><span class="toast-msg">${message}</span>`;
c.appendChild(t);
requestAnimationFrame(() => requestAnimationFrame(() => t.classList.add('toast-visible')));
setTimeout(() => { t.classList.remove('toast-visible'); setTimeout(() => t.remove(), 350); }, duration);
}
/* ===================== GALERIE ===================== */
function initGallery() {
const main = document.getElementById('gallery-main');
const thumbs = document.querySelectorAll('.gallery-thumb');
if (!main || !thumbs.length) return;
thumbs.forEach((thumb, i) => {
thumb.addEventListener('click', () => {
main.src = thumb.dataset.src || thumb.src;
main.alt = thumb.alt;
thumbs.forEach((t) => t.classList.remove('active'));
thumb.classList.add('active');
});
if (i === 0) thumb.classList.add('active');
});
}
/* ===================== GRÖSSENWAHL ===================== */
function initSizeSelector() {
const btns = document.querySelectorAll('.size-btn');
const input = document.getElementById('selected-size');
const addBtn = document.getElementById('add-to-cart-btn');
if (!btns.length) return;
btns.forEach((btn) => {
btn.addEventListener('click', () => {
btns.forEach((b) => b.classList.remove('active'));
btn.classList.add('active');
if (input) input.value = btn.dataset.size;
if (addBtn) { addBtn.disabled = false; addBtn.textContent = 'In den Warenkorb'; }
});
});
}
/* ===================== ADD TO CART ===================== */
function initAddToCart() {
const form = document.getElementById('product-form');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
const productId = form.dataset.productId;
const sizeInput = document.getElementById('selected-size');
const size = sizeInput ? sizeInput.value : 'One Size';
if (!size) { showToast('Bitte wähle zuerst eine Größe.', 'warning'); return; }
addToCart(productId, size);
});
}
document.querySelectorAll('.js-quick-add').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
addToCart(btn.dataset.productId, btn.dataset.size || 'One Size');
});
});
}
/* ===================== WARENKORB-SEITE ===================== */
function renderCartPage() {
const container = document.getElementById('cart-items');
const emptyMsg = document.getElementById('cart-empty');
const summary = document.getElementById('cart-summary');
if (!container) return;
const cart = getCart();
if (cart.length === 0) {
container.innerHTML = '';
if (emptyMsg) emptyMsg.style.display = 'block';
if (summary) summary.style.display = 'none';
return;
}
if (emptyMsg) emptyMsg.style.display = 'none';
if (summary) summary.style.display = 'block';
container.innerHTML = cart.map((item) => `
<div class="cart-item" data-key="${item.key}">
<div class="cart-item-img"><img src="${item.image}" alt="${item.name}" width="80" height="100" loading="lazy"></div>
<div class="cart-item-info">
<h3 class="cart-item-name">${item.name}</h3>
<span class="cart-item-size">Größe: ${item.size}</span>
<div class="cart-item-qty">
<button class="qty-btn" data-key="${item.key}" data-delta="-1" aria-label="Weniger"></button>
<span class="qty-val">${item.qty}</span>
<button class="qty-btn" data-key="${item.key}" data-delta="1" aria-label="Mehr">+</button>
</div>
</div>
<div class="cart-item-price">
<span>${formatPrice(item.price * item.qty)}</span>
<button class="cart-remove" data-key="${item.key}" aria-label="${item.name} entfernen">Entfernen</button>
</div>
</div>`).join('');
container.querySelectorAll('.qty-btn').forEach((btn) => btn.addEventListener('click', () => { updateQty(btn.dataset.key, parseInt(btn.dataset.delta)); renderCartPage(); }));
container.querySelectorAll('.cart-remove').forEach((btn) => btn.addEventListener('click', () => { removeFromCart(btn.dataset.key); renderCartPage(); showToast('Artikel entfernt.', 'info'); }));
document.querySelectorAll('.js-cart-subtotal').forEach((el) => { el.textContent = formatPrice(cartTotal()); });
}
/* ===================== CHECKOUT (simuliert — später Stripe via Medusa) ===================== */
function initCheckout() {
const form = document.getElementById('checkout-form');
if (!form) return;
const summaryContainer = document.getElementById('checkout-order-items');
if (summaryContainer) {
const cart = getCart();
summaryContainer.innerHTML = cart.map((i) => `<div class="checkout-item"><span>${i.name} (${i.size}) × ${i.qty}</span><span>${formatPrice(i.price * i.qty)}</span></div>`).join('');
document.querySelectorAll('.js-checkout-total').forEach((el) => { el.textContent = formatPrice(cartTotal() + (cartTotal() >= 50 ? 0 : 4.9)); });
document.querySelectorAll('.js-checkout-shipping').forEach((el) => { el.textContent = cartTotal() >= 50 ? 'Kostenlos' : formatPrice(4.9); });
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const cart = getCart();
if (!cart.length) { showToast('Dein Warenkorb ist leer.', 'warning'); return; }
const fd = new FormData(form);
const btn = form.querySelector('.checkout-submit-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Wird verarbeitet …'; }
try {
const res = await fetch('/api/checkout', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart, contact: Object.fromEntries(fd.entries()) }),
});
const data = await res.json();
if (data.url) { localStorage.removeItem(CART_KEY); window.location.href = data.url; return; }
throw new Error(data.error || 'Fehler');
} catch (err) {
showToast('Checkout fehlgeschlagen. Bitte erneut versuchen.', 'warning');
if (btn) { btn.disabled = false; btn.textContent = 'Jetzt bestellen →'; }
}
});
}
/* ===================== INSTAGRAM-FEED ===================== */
const INSTA_POSTS = [
{ img: '/product-images/insta/insta-2.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DVMOZMGiDto/' },
{ img: '/product-images/insta/insta-3.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DVDX5KQiB8Z/' },
{ img: '/product-images/insta/insta-4.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DU_br0OCE35/' },
{ img: '/product-images/insta/insta-1.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DVgqeRQiNnD/' },
{ img: '/product-images/insta/insta-5.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DVVazsxiIB8/' },
{ img: '/product-images/insta/insta-6.jpg', url: 'https://www.instagram.com/tgasolutions/reel/DVDX5KQiB8Z/' },
];
function initInstaFeed() {
const grid = document.getElementById('insta-grid');
if (!grid || !INSTA_POSTS.length) return;
grid.innerHTML = '';
grid.className = 'insta-grid';
grid.setAttribute('role', 'list');
INSTA_POSTS.forEach((post) => {
const tile = document.createElement('a');
tile.className = 'insta-post';
tile.href = post.url; tile.target = '_blank'; tile.rel = 'noopener noreferrer';
tile.setAttribute('role', 'listitem');
tile.setAttribute('aria-label', '@tgasolutions auf Instagram');
tile.innerHTML = `<img src="${post.img}" alt="@tgasolutions" loading="lazy">
<div class="insta-post-overlay">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="5" ry="5"/><circle cx="12" cy="12" r="4.5"/><circle cx="17.5" cy="6.5" r="1.2" fill="white" stroke="none"/></svg>
<span class="insta-post-tag">@tgasolutions</span>
</div>`;
grid.appendChild(tile);
});
}
/* ===================== MOBILE-MENÜ ===================== */
function initMobileMenu() {
const toggle = document.getElementById('menu-toggle');
const nav = document.getElementById('main-nav');
if (!toggle || !nav) return;
toggle.addEventListener('click', () => {
const open = nav.classList.toggle('nav-open');
toggle.setAttribute('aria-expanded', open);
document.body.style.overflow = open ? 'hidden' : '';
});
nav.querySelectorAll('a').forEach((a) => a.addEventListener('click', () => {
nav.classList.remove('nav-open');
toggle.setAttribute('aria-expanded', 'false');
document.body.style.overflow = '';
}));
}
/* ===================== STICKY MOBILE ADD-TO-CART ===================== */
function initStickyMobileATC() {
const mainBtn = document.getElementById('add-to-cart-btn');
if (!mainBtn || window.innerWidth > 900) return;
const nameEl = document.querySelector('[class*="pdp-name"]') || document.querySelector('h1');
const productName = nameEl ? nameEl.textContent.trim().split('\n')[0].trim() : 'Produkt';
const bar = document.createElement('div');
bar.id = 'sticky-atc';
bar.setAttribute('aria-hidden', 'true');
bar.innerHTML = `<span class="sticky-atc-name">${productName.substring(0, 40)}</span><button class="sticky-atc-btn" id="sticky-atc-btn">In den Warenkorb</button>`;
document.body.appendChild(bar);
const observer = new IntersectionObserver(([entry]) => {
bar.classList.toggle('sticky-atc-visible', !entry.isIntersecting);
bar.setAttribute('aria-hidden', entry.isIntersecting ? 'true' : 'false');
}, { threshold: 0, rootMargin: '0px 0px -60px 0px' });
observer.observe(mainBtn);
document.getElementById('sticky-atc-btn').addEventListener('click', () => {
if (!mainBtn.disabled) mainBtn.click();
else { mainBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); mainBtn.focus(); }
});
}
/* ===================== UTIL ===================== */
function formatPrice(num) {
return num.toLocaleString('de-DE', { style: 'currency', currency: 'EUR' });
}
/* ===================== INIT ===================== */
document.addEventListener('DOMContentLoaded', () => {
updateCartUI();
initGallery();
initSizeSelector();
initAddToCart();
initMobileMenu();
if (document.getElementById('cart-items')) renderCartPage();
if (document.getElementById('checkout-form')) initCheckout();
if (document.body.dataset.page === 'product') initStickyMobileATC();
if (document.getElementById('insta-grid')) initInstaFeed();
});