/**
* 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 =
'';
}
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 || '') +
'
' +
'
' +
'
' +
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, '"');
}
})();