TGA-Shop Demo: Storefront (kein Passwort, Umami, Stripe-Checkout mit Demo-Fallback) + Shopify-Style-Admin; Datenschicht sqlite|directus

This commit is contained in:
2026-06-16 08:41:43 +00:00
commit 4f9a2cf512
77 changed files with 11485 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" fill="#0d0d0d"/>
<text x="16" y="23" font-family="Arial Black, Arial, sans-serif" font-size="20" font-weight="900" text-anchor="middle" fill="#e01c2e">Z</text>
</svg>

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

+312
View File
@@ -0,0 +1,312 @@
/**
* 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();
});