/** * Snuzz Creator Marketplace * Shopify Liquid Section + Supabase (client-side) */ (function () { 'use strict'; const roots = document.querySelectorAll('[data-creator-marketplace]'); if (!roots.length) return; roots.forEach(initRoot); function initRoot(root) { const config = { supabaseUrl: root.dataset.supabaseUrl, supabaseAnonKey: root.dataset.supabaseAnonKey, marketplaceVariantId: root.dataset.marketplaceVariantId, currency: root.dataset.currency || 'EUR', mode: root.dataset.mode || 'portal', creatorSlug: root.dataset.creatorSlug || '', shopifyCustomerId: root.dataset.shopifyCustomerId || '', }; if (!config.supabaseUrl || !config.supabaseAnonKey) { showMessage(root, 'Supabase ist nicht konfiguriert. Bitte Theme-Einstellungen prüfen.', 'error'); return; } loadSupabase(config.supabaseUrl, config.supabaseAnonKey) .then(function (client) { return createApp(root, client, config).init(); }) .catch(function (err) { showMessage(root, 'Supabase konnte nicht geladen werden: ' + err.message, 'error'); }); } function loadSupabase(url, key) { return new Promise(function (resolve, reject) { if (window.supabase && window.supabase.createClient) { resolve(window.supabase.createClient(url, key)); return; } const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.min.js'; script.onload = function () { if (!window.supabase) { reject(new Error('Supabase SDK nicht verfügbar')); return; } resolve(window.supabase.createClient(url, key)); }; script.onerror = function () { reject(new Error('Supabase SDK konnte nicht geladen werden')); }; document.head.appendChild(script); }); } function createApp(root, client, config) { let session = null; let creator = null; let shopifyMode = false; async function fetchCreator(userId) { const { data, error } = await client.from('creators').select('*').eq('id', userId).maybeSingle(); if (error) return null; return data; } function shopifyPayload() { return config.shopifyCustomerId ? { shopify_customer_id: parseInt(config.shopifyCustomerId, 10) } : {}; } async function loadDashboardData() { if (shopifyMode || !session) { const { data, error } = await client.functions.invoke('creator-dashboard', { body: shopifyPayload(), }); if (error || !data || !data.success) { throw new Error((data && data.error) || error?.message || 'Dashboard konnte nicht geladen werden'); } return data; } const creatorData = await fetchCreator(session.user.id); if (!creatorData) throw new Error('Creator-Profil nicht gefunden'); const [stats, products, sales, payouts] = await Promise.all([ client.from('creator_dashboard_stats').select('*').eq('creator_id', creatorData.id).maybeSingle(), client.from('creator_products').select('*').eq('creator_id', creatorData.id).order('created_at', { ascending: false }), client.from('sales').select('*').eq('creator_id', creatorData.id).order('created_at', { ascending: false }).limit(20), client.from('payouts').select('*').eq('creator_id', creatorData.id).order('created_at', { ascending: false }).limit(20), ]); return { creator: creatorData, stats: stats.data, products: products.data || [], sales: sales.data || [], payouts: payouts.data || [], }; } async function renderPortal() { const authEl = root.querySelector('[data-panel="auth"]'); const dashboardEl = root.querySelector('[data-panel="dashboard"]'); shopifyMode = !session && !!config.shopifyCustomerId; if (!session && !shopifyMode) { togglePanels(root, 'auth'); if (authEl) authEl.classList.remove('creator-marketplace__hidden'); if (dashboardEl) dashboardEl.classList.add('creator-marketplace__hidden'); return; } try { const dashboard = await loadDashboardData(); creator = dashboard.creator; if (!creator) { showMessage(root, 'Creator-Profil nicht gefunden.', 'error'); return; } if (authEl) authEl.classList.add('creator-marketplace__hidden'); if (dashboardEl) dashboardEl.classList.remove('creator-marketplace__hidden'); togglePanels(root, 'dashboard'); renderCreatorStatus(root, creator); renderStatsFromData(root, dashboard.stats); renderProductsTableFromData(root, dashboard.products); renderSalesTableFromData(root, dashboard.sales); renderPayoutsTableFromData(root, dashboard.payouts); } catch (err) { showMessage(root, err.message || 'Dashboard Fehler', 'error'); } } async function renderMarketplace() { const grid = root.querySelector('[data-marketplace-grid]'); if (!grid) return; grid.innerHTML = '

Lade Produkte…

'; const { data: products, error } = await client .from('creator_products') .select('*, creators!inner(display_name, slug, status)') .eq('status', 'published') .eq('creators.status', 'approved') .order('created_at', { ascending: false }); if (error) { grid.innerHTML = '

Fehler beim Laden.

'; return; } if (!products.length) { grid.innerHTML = '

Noch keine Creator-Produkte verfügbar.

'; return; } grid.innerHTML = products.map(renderProductCard).join(''); bindAddToCart(root, config, products); } async function renderCreatorShop(slug) { const header = root.querySelector('[data-creator-header]'); const grid = root.querySelector('[data-marketplace-grid]'); if (!grid) return; const { data: creatorData, error: creatorError } = await client .from('creators') .select('*') .eq('slug', slug) .eq('status', 'approved') .maybeSingle(); if (creatorError || !creatorData) { grid.innerHTML = '

Creator nicht gefunden.

'; return; } if (header) { header.innerHTML = '
' + (creatorData.avatar_url ? '' : '') + '

' + escapeHtml(creatorData.display_name) + '

' + (creatorData.bio ? '

' + escapeHtml(creatorData.bio) + '

' : '') + '
'; } const { data: products, error } = await client .from('creator_products') .select('*') .eq('creator_id', creatorData.id) .eq('status', 'published') .order('created_at', { ascending: false }); if (error || !products.length) { grid.innerHTML = '

Dieser Creator hat noch keine Produkte.

'; return; } const enriched = products.map(function (p) { return Object.assign({}, p, { creators: creatorData }); }); grid.innerHTML = enriched.map(renderProductCard).join(''); bindAddToCart(root, config, enriched); } function bindForms() { const loginForm = root.querySelector('[data-form="login"]'); const registerForm = root.querySelector('[data-form="register"]'); const logoutBtn = root.querySelector('[data-action="logout"]'); const productForm = root.querySelector('[data-form="product"]'); const profileForm = root.querySelector('[data-form="profile"]'); if (loginForm) { loginForm.addEventListener('submit', async function (e) { e.preventDefault(); clearMessage(root); const email = loginForm.querySelector('[name="email"]').value.trim(); const password = loginForm.querySelector('[name="password"]').value; const { error } = await client.auth.signInWithPassword({ email, password }); if (error) showMessage(root, error.message, 'error'); }); } if (registerForm) { registerForm.addEventListener('submit', async function (e) { e.preventDefault(); clearMessage(root); const displayName = registerForm.querySelector('[name="display_name"]').value.trim(); const email = registerForm.querySelector('[name="email"]').value.trim(); const password = registerForm.querySelector('[name="password"]').value; const { error } = await client.auth.signUp({ email, password, options: { data: { display_name: displayName } }, }); if (error) { showMessage(root, error.message, 'error'); } else { showMessage(root, 'Registrierung erfolgreich! Bitte E-Mail bestätigen und einloggen.', 'success'); } }); } if (logoutBtn) { logoutBtn.addEventListener('click', async function () { await client.auth.signOut(); }); } if (profileForm) { profileForm.addEventListener('submit', async function (e) { e.preventDefault(); if (!creator) return; clearMessage(root); const commissionRate = parseFloat(profileForm.querySelector('[name="commission_rate"]').value); const bio = profileForm.querySelector('[name="bio"]').value.trim(); const displayName = profileForm.querySelector('[name="display_name"]').value.trim(); const { error } = await client .from('creators') .update({ display_name: displayName, bio: bio, commission_rate: commissionRate, }) .eq('id', creator.id); if (error) { showMessage(root, error.message, 'error'); } else { showMessage(root, 'Profil gespeichert.', 'success'); creator = await fetchCreator(creator.id); renderCreatorStatus(root, creator); } }); } if (productForm) { productForm.addEventListener('submit', async function (e) { e.preventDefault(); if (!creator) { showFormFeedback(productForm, 'Creator-Profil nicht geladen. Bitte Seite neu laden.', 'error'); return; } clearMessage(root); if (creator.status !== 'approved') { const msg = 'Dein Account muss erst freigeschaltet werden. Bitte den Shop-Betreiber kontaktieren.'; showMessage(root, msg, 'info'); showFormFeedback(productForm, msg, 'error'); return; } const submitBtn = productForm.querySelector('[data-product-submit]'); const payload = Object.assign({ title: productForm.querySelector('[name="title"]').value.trim(), description: productForm.querySelector('[name="description"]').value.trim(), price: parseFloat(productForm.querySelector('[name="price"]').value), image_url: productForm.querySelector('[name="image_url"]').value.trim() || null, inventory_quantity: parseInt(productForm.querySelector('[name="inventory_quantity"]').value, 10) || 0, status: productForm.querySelector('[name="status"]').value, }, shopifyPayload()); if (!payload.title) { showFormFeedback(productForm, 'Bitte einen Titel eingeben.', 'error'); return; } if (!payload.price || payload.price <= 0) { showFormFeedback(productForm, 'Bitte einen gültigen Preis eingeben.', 'error'); return; } if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Speichert…'; } showFormFeedback(productForm, '', ''); try { const { data, error } = await client.functions.invoke('create-shopify-product', { body: payload, }); let response = data; if (error) { const detail = await readFunctionError(error); if (detail) { throw new Error(detail); } if (data && (data.error || data.success === false)) { response = data; } else { throw new Error(error.message || 'Edge Function Fehler'); } } if (!response || response.success === false) { const msg = response && response.error ? (response.partial ? 'In Supabase gespeichert, Shopify-Sync fehlgeschlagen: ' + response.error : response.error) : 'Unbekannter Fehler beim Speichern'; showMessage(root, msg, response && response.partial ? 'info' : 'error'); showFormFeedback(productForm, msg, 'error'); if (response && response.partial) { await renderProductsTable(root, client, creator.id); } return; } const successMsg = 'Produkt erfolgreich angelegt!'; showMessage(root, successMsg, 'success'); showFormFeedback(productForm, successMsg, 'success'); productForm.reset(); await renderPortal(); } catch (err) { const msg = err && err.message ? err.message : 'Unbekannter Fehler beim Speichern'; showMessage(root, msg, 'error'); showFormFeedback(productForm, msg, 'error'); } finally { if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Produkt speichern'; } } }); } } return { init: async function () { if (!config.creatorSlug) { const params = new URLSearchParams(window.location.search); config.creatorSlug = params.get('creator') || ''; } bindTabs(root); bindForms(); const { data } = await client.auth.getSession(); session = data.session; if (config.mode === 'portal') { await renderPortal(); } else if (config.mode === 'marketplace') { await renderMarketplace(); } else if (config.mode === 'creator-shop') { await renderCreatorShop(config.creatorSlug); } client.auth.onAuthStateChange(async function (_event, newSession) { session = newSession; if (config.mode === 'portal') { await renderPortal(); } }); }, }; } function bindTabs(root) { root.querySelectorAll('[data-tab]').forEach(function (tab) { tab.addEventListener('click', function () { togglePanels(root, tab.dataset.tab); }); }); } function togglePanels(root, activeTab) { root.querySelectorAll('[data-tab]').forEach(function (tab) { tab.classList.toggle('is-active', tab.dataset.tab === activeTab); }); root.querySelectorAll('[data-panel]').forEach(function (panel) { const panelName = panel.dataset.panel; if (panelName === 'auth' || panelName === 'dashboard') return; panel.classList.toggle('is-active', panelName === activeTab); }); } function renderCreatorStatus(root, creator) { const el = root.querySelector('[data-creator-status]'); const profileForm = root.querySelector('[data-form="profile"]'); if (!el) return; const statusLabels = { pending: 'Wartet auf Freigabe', approved: 'Aktiv', rejected: 'Abgelehnt', suspended: 'Gesperrt', }; let shopLinks = '

Dein Shop: /pages/creator-shop?creator=' + encodeURIComponent(creator.slug) + '

'; if (creator.shopify_collection_handle) { shopLinks += '

Shopify Kollektion: /collections/' + encodeURIComponent(creator.shopify_collection_handle) + '

'; } el.innerHTML = '

Status: ' + (statusLabels[creator.status] || creator.status) + '

' + shopLinks; if (profileForm) { profileForm.querySelector('[name="display_name"]').value = creator.display_name || ''; profileForm.querySelector('[name="bio"]').value = creator.bio || ''; profileForm.querySelector('[name="commission_rate"]').value = creator.commission_rate || 70; } } function renderStatsFromData(root, data) { const statsEl = root.querySelector('[data-stats]'); if (!statsEl) return; statsEl.innerHTML = '
' + statCard(formatMoney(data ? data.pending_earnings : 0), 'Offene Auszahlung') + statCard(formatMoney(data ? data.paid_earnings : 0), 'Bereits ausgezahlt') + statCard(String(data ? data.total_sales : 0), 'Verkäufe gesamt') + '
'; } function renderProductsTableFromData(root, data) { const table = root.querySelector('[data-products-table] tbody'); if (!table) return; if (!data.length) { table.innerHTML = 'Noch keine Produkte'; return; } table.innerHTML = data.map(function (p) { const shopifyStatus = p.shopify_sync_status === 'synced' ? '✓ Shopify' : p.shopify_sync_status === 'failed' ? '✗ Fehler' : '…'; return ( '' + escapeHtml(p.title) + '' + formatMoney(p.price) + '' + '' + escapeHtml(p.status) + '' + shopifyStatus + '' + p.inventory_quantity + '' ); }).join(''); } function renderSalesTableFromData(root, data) { const table = root.querySelector('[data-sales-table] tbody'); if (!table) return; if (!data.length) { table.innerHTML = 'Noch keine Verkäufe'; return; } table.innerHTML = data.map(function (s) { return ( '' + escapeHtml(s.shopify_order_name || String(s.shopify_order_id)) + '' + '' + escapeHtml(s.product_title) + '' + formatMoney(s.gross_amount) + '' + '' + formatMoney(s.creator_earnings) + '' + escapeHtml(s.payout_status) + '' ); }).join(''); } function renderPayoutsTableFromData(root, data) { const table = root.querySelector('[data-payouts-table] tbody'); if (!table) return; if (!data.length) { table.innerHTML = 'Noch keine Auszahlungen'; return; } table.innerHTML = data.map(function (p) { return ( '' + formatMoney(p.amount) + '' + escapeHtml(p.status) + '' + '' + new Date(p.created_at).toLocaleDateString('de-DE') + '' ); }).join(''); } async function renderStats(root, client, creatorId) { const statsEl = root.querySelector('[data-stats]'); if (!statsEl) return; const { data, error } = await client .from('creator_dashboard_stats') .select('*') .eq('creator_id', creatorId) .maybeSingle(); if (error) { statsEl.innerHTML = ''; return; } statsEl.innerHTML = '
' + statCard(formatMoney(data ? data.pending_earnings : 0), 'Offene Auszahlung') + statCard(formatMoney(data ? data.paid_earnings : 0), 'Bereits ausgezahlt') + statCard(String(data ? data.total_sales : 0), 'Verkäufe gesamt') + '
'; } function statCard(value, label) { return ( '
' + '
' + value + '
' + '
' + label + '
' ); } async function renderProductsTable(root, client, creatorId) { const table = root.querySelector('[data-products-table] tbody'); if (!table) return; const { data, error } = await client .from('creator_products') .select('*') .eq('creator_id', creatorId) .order('created_at', { ascending: false }); if (error) { table.innerHTML = 'Fehler beim Laden'; return; } if (!data.length) { table.innerHTML = 'Noch keine Produkte'; return; } table.innerHTML = data .map(function (p) { const shopifyStatus = p.shopify_sync_status === 'synced' ? '✓ Shopify' : p.shopify_sync_status === 'failed' ? '✗ Fehler' : '…'; return ( '' + '' + escapeHtml(p.title) + '' + '' + formatMoney(p.price) + '' + '' + escapeHtml(p.status) + '' + '' + shopifyStatus + '' + '' + p.inventory_quantity + '' + '' ); }) .join(''); } async function renderSalesTable(root, client, creatorId) { const table = root.querySelector('[data-sales-table] tbody'); if (!table) return; const { data, error } = await client .from('sales') .select('*') .eq('creator_id', creatorId) .order('created_at', { ascending: false }) .limit(20); if (error || !data.length) { table.innerHTML = 'Noch keine Verkäufe'; return; } table.innerHTML = data .map(function (s) { return ( '' + '' + escapeHtml(s.shopify_order_name || String(s.shopify_order_id)) + '' + '' + escapeHtml(s.product_title) + '' + '' + formatMoney(s.gross_amount) + '' + '' + formatMoney(s.creator_earnings) + '' + '' + escapeHtml(s.payout_status) + '' + '' ); }) .join(''); } async function renderPayoutsTable(root, client, creatorId) { const table = root.querySelector('[data-payouts-table] tbody'); if (!table) return; const { data, error } = await client .from('payouts') .select('*') .eq('creator_id', creatorId) .order('created_at', { ascending: false }) .limit(20); if (error || !data.length) { table.innerHTML = 'Noch keine Auszahlungen'; return; } table.innerHTML = data .map(function (p) { return ( '' + '' + formatMoney(p.amount) + '' + '' + escapeHtml(p.status) + '' + '' + new Date(p.created_at).toLocaleDateString('de-DE') + '' + '' ); }) .join(''); } function renderProductCard(product) { const creator = product.creators || {}; const productLink = product.shopify_handle ? '/products/' + encodeURIComponent(product.shopify_handle) : null; return ( '
' + (product.image_url ? '' : '
') + '
' + '

' + escapeHtml(creator.display_name || '') + '

' + '

' + (productLink ? '' + escapeHtml(product.title) + '' : escapeHtml(product.title)) + '

' + '

' + formatMoney(product.price) + '

' + '' + '
' ); } function bindAddToCart(root, config, products) { root.querySelectorAll('[data-add-to-cart]').forEach(function (btn) { btn.addEventListener('click', async function () { const productId = btn.dataset.addToCart; const product = products.find(function (p) { return p.id === productId; }); if (!product) return; const useShopifyVariant = product.shopify_variant_id; const fallbackVariantId = config.marketplaceVariantId; if (!useShopifyVariant && !fallbackVariantId) { alert('Checkout ist nicht konfiguriert.'); return; } const cartItem = useShopifyVariant ? { id: parseInt(product.shopify_variant_id, 10), quantity: 1, properties: { _listing_id: product.id, _creator_id: product.creator_id, Creator: (product.creators && product.creators.display_name) || '', }, } : { id: parseInt(fallbackVariantId, 10), quantity: Math.max(1, Math.round(parseFloat(product.price))), properties: { _listing_id: product.id, _creator_id: product.creator_id, Produkt: product.title, Creator: (product.creators && product.creators.display_name) || '', }, }; try { const res = await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [cartItem] }), }); if (!res.ok) throw new Error('Warenkorb-Fehler'); window.location.href = '/cart'; } catch (_err) { alert('Produkt konnte nicht in den Warenkorb gelegt werden.'); } }); }); } async function readFunctionError(error) { try { if (error && error.context) { if (typeof error.context.json === 'function') { const body = await error.context.json(); if (body && body.error) return body.error; } if (typeof error.context.text === 'function') { const text = await error.context.text(); if (text) { try { const body = JSON.parse(text); if (body && body.error) return body.error; } catch (_e) { return text; } } } } } catch (_e) { /* ignore */ } return null; } function showFormFeedback(form, text, type) { const el = form.querySelector('[data-product-feedback]'); if (!el) return; el.textContent = text; el.className = 'creator-marketplace__form-feedback' + (type === 'error' ? ' is-error' : type === 'success' ? ' is-success' : ''); } function showMessage(root, text, type) { let el = root.querySelector('[data-message]'); if (!el) { el = document.createElement('div'); el.setAttribute('data-message', ''); root.prepend(el); } el.className = 'creator-marketplace__message creator-marketplace__message--' + type; el.textContent = text; } function clearMessage(root) { const el = root.querySelector('[data-message]'); if (el) el.remove(); } function formatMoney(amount) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', }).format(Number(amount) || 0); } function escapeHtml(str) { return String(str || '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } })();