1/** 2 * Supabase client for Path Creatives static previews. 3 * Requires @supabase/supabase-js (CDN) and previews/config.local.js 4 */ 5(function (global) { 6 let client = null; 7 let ready = false; 8 9 function getConfig() { 10 return global.PathCreativesConfig || null; 11 } 12 13 function hasValidConfig() { 14 const cfg = getConfig(); 15 return Boolean( 16 cfg?.supabaseUrl && 17 cfg?.supabaseAnonKey && 18 !String(cfg.supabaseAnonKey).includes('YOUR_') 19 ); 20 } 21 22 async function init() { 23 if (ready) return true; 24 if (!hasValidConfig() || !global.supabase?.createClient) return false; 25 26 const cfg = getConfig(); 27 client = global.supabase.createClient(cfg.supabaseUrl, cfg.supabaseAnonKey); 28 ready = true; 29 return true; 30 } 31 32 function isReady() { 33 return ready; 34 } 35 36 function getClient() { 37 return client; 38 } 39 40 function mapProjectRow(row) { 41 if (!row) return null; 42 return { 43 slug: row.slug, 44 name: row.name, 45 company: row.company, 46 offer: row.offer, 47 offerSlug: row.offer_slug, 48 sourceUrl: row.source_url, 49 status: row.status, 50 brief: row.brief || null, 51 briefApproved: Boolean(row.brief_approved), 52 briefSummary: row.brief_summary, 53 briefStrategy: row.brief_strategy, 54 updatedAt: row.updated_at || null 55 }; 56 } 57 58 function mapDesignRow(slug, row) { 59 if (!row) return null; 60 return { 61 projectSlug: slug, 62 colors: Array.isArray(row.colors) ? row.colors : [], 63 logos: Array.isArray(row.logos) ? row.logos : [], 64 pageImagery: Array.isArray(row.page_imagery) ? row.page_imagery : [], 65 scrapeMeta: row.scrape_meta && typeof row.scrape_meta === 'object' ? row.scrape_meta : {}, 66 backgrounds: Array.isArray(row.backgrounds) ? row.backgrounds : [], 67 assetsApproved: Boolean(row.assets_approved), 68 imageryApproved: Boolean(row.imagery_approved), 69 selectedBg: Number.isFinite(row.selected_bg) ? row.selected_bg : 0, 70 backgroundPrompt: row.background_prompt || '', 71 customLogoDataUrl: row.custom_logo_data_url || null, 72 updatedAt: row.updated_at || null 73 }; 74 } 75 76 function mapGenerateRow(slug, row) { 77 if (!row) return null; 78 return { 79 projectSlug: slug, 80 creatives: Array.isArray(row.creatives) ? row.creatives : [], 81 selectedIndex: Number.isFinite(row.selected_index) ? row.selected_index : 0, 82 approved: Boolean(row.approved), 83 updatedAt: row.updated_at || null 84 }; 85 } 86 87 function mapWizardProgressRow(slug, row) { 88 if (!row) return null; 89 return { 90 projectSlug: slug, 91 currentStep: Number.isFinite(row.current_step) ? row.current_step : 1, 92 maxCompletedStep: Number.isFinite(row.max_completed_step) ? row.max_completed_step : 0, 93 urlEnteredAt: row.url_entered_at || null, 94 briefApprovedAt: row.brief_approved_at || null, 95 assetsApprovedAt: row.assets_approved_at || null, 96 imageryApprovedAt: row.imagery_approved_at || null, 97 generateApprovedAt: row.generate_approved_at || null, 98 lastStepEnteredAt: row.last_step_entered_at || null, 99 stepMeta: row.step_meta && typeof row.step_meta === 'object' ? row.step_meta : {}, 100 updatedAt: row.updated_at || null 101 }; 102 } 103 104 function mapHistoryRow(slug, row) { 105 if (!row) return null; 106 return { 107 projectSlug: slug, 108 displayDate: row.display_date || null, 109 sortDate: Number.isFinite(row.sort_date) ? row.sort_date : 0, 110 thumbnails: Array.isArray(row.thumbnails) ? row.thumbnails : [], 111 colors: Array.isArray(row.colors) ? row.colors : [], 112 logoUrl: row.logo_url || null, 113 creatives: Array.isArray(row.creatives) ? row.creatives : [], 114 creativeCount: Number.isFinite(row.creative_count) ? row.creative_count : 4, 115 insights: Number.isFinite(row.insights) ? row.insights : 0, 116 publishedAt: row.published_at || null, 117 snapshot: row.snapshot && typeof row.snapshot === 'object' ? row.snapshot : {}, 118 updatedAt: row.updated_at || null 119 }; 120 } 121 122 function mapHistoryListRow(row) { 123 if (!row) return null; 124 const displayDate = row.display_date 125 ? new Date(`${row.display_date}T12:00:00`).toLocaleDateString('en-US', { 126 month: 'short', 127 day: 'numeric', 128 year: 'numeric' 129 }) 130 : row.updated_at 131 ? new Date(row.updated_at).toLocaleDateString('en-US', { 132 month: 'short', 133 day: 'numeric', 134 year: 'numeric' 135 }) 136 : ''; 137 138 return { 139 slug: row.slug, 140 name: row.name, 141 company: row.company, 142 offer: row.offer, 143 offerSlug: row.offer_slug, 144 sourceUrl: row.source_url, 145 status: row.status || 'draft', 146 briefSummary: row.brief_summary, 147 briefStrategy: row.brief_strategy, 148 creativeCount: row.history_creative_count ?? row.creative_count ?? 4, 149 insights: row.history_insights ?? row.insights ?? 0, 150 date: displayDate, 151 sortDate: row.sort_date || 0, 152 thumbnails: Array.isArray(row.thumbnails) ? row.thumbnails : [], 153 colors: Array.isArray(row.colors) ? row.colors : [], 154 logo: row.logo_url || null, 155 creatives: Array.isArray(row.creatives) ? row.creatives : [], 156 wizardCurrentStep: row.wizard_current_step ?? 1, 157 wizardMaxCompletedStep: row.wizard_max_completed_step ?? 0, 158 publishedAt: row.published_at || null, 159 snapshot: row.snapshot && typeof row.snapshot === 'object' ? row.snapshot : {} 160 }; 161 } 162 163 async function upsertProject(project) { 164 if (!ready) await init(); 165 if (!client || !project?.slug) return false;
166 167 const { error } = await client.from('projects').upsert({ 168 slug: project.slug, 169 name: project.name || project.company || project.slug, 170 company: project.company || null, 171 offer: project.offer || null, 172 offer_slug: project.offerSlug || null, 173 source_url: project.sourceUrl || null, 174 status: project.status || 'draft', 175 brief: project.brief || null, 176 brief_approved: Boolean(project.briefApproved), 177 brief_summary: project.briefSummary || null, 178 brief_strategy: project.briefStrategy || null, 179 updated_at: new Date().toISOString() 180 }); 181 182 if (error) { 183 console.warn('[PathCreativesSupabase] upsertProject failed', error.message); 184 return false; 185 } 186 return true; 187 } 188 189 async function loadProject(slug) { 190 if (!ready) await init(); 191 if (!client) return null; 192 193 const { data, error } = await client 194 .from('projects') 195 .select('*') 196 .eq('slug', slug) 197 .maybeSingle(); 198 199 if (error) { 200 console.warn('[PathCreativesSupabase] loadProject failed', error.message); 201 return null; 202 } 203 204 return mapProjectRow(data); 205 } 206 207 async function loadDesign(slug) { 208 if (!ready) await init(); 209 if (!client) return null; 210 211 const { data, error } = await client 212 .from('project_design') 213 .select('*') 214 .eq('project_slug', slug) 215 .maybeSingle(); 216 217 if (error) { 218 console.warn('[PathCreativesSupabase] loadDesign failed', error.message); 219 return null; 220 } 221 222 return hydrateDesign(mapDesignRow(slug, data)); 223 } 224 225 async function hydrateDesign(mapped) { 226 if (!mapped || !global.PathCreativesStorage) return mapped; 227 return { 228 ...mapped, 229 backgrounds: global.PathCreativesStorage.hydrateBackgrounds(mapped.backgrounds), 230 customLogoDataUrl: global.PathCreativesStorage.resolveAssetUrl( 231 getClient(), 232 { src: mapped.customLogoDataUrl } 233 ) || mapped.customLogoDataUrl 234 }; 235 } 236 237 async function saveDesign(slug, state) { 238 if (!ready) await init(); 239 if (!client) return false; 240 241 let prepared = state; 242 if (global.PathCreativesStorage) { 243 try { 244 prepared = await global.PathCreativesStorage.prepareDesignForRemote(slug, state); 245 } catch (err) { 246 console.warn('[PathCreativesSupabase] saveDesign storage prep failed', err.message); 247 } 248 } 249 250 const { error } = await client.from('project_design').upsert({ 251 project_slug: slug, 252 colors: prepared.colors || [], 253 logos: prepared.logos || [], 254 page_imagery: prepared.pageImagery || [], 255 scrape_meta: prepared.scrapeMeta || {}, 256 backgrounds: prepared.backgrounds || [], 257 assets_approved: Boolean(prepared.assetsApproved), 258 imagery_approved: Boolean(prepared.imageryApproved), 259 selected_bg: Number.isFinite(prepared.selectedBg) ? prepared.selectedBg : 0, 260 background_prompt: prepared.backgroundPrompt || '', 261 custom_logo_data_url: prepared.customLogoDataUrl || null, 262 updated_at: new Date().toISOString() 263 }); 264 265 if (error) { 266 console.warn('[PathCreativesSupabase] saveDesign failed', error.message); 267 return false; 268 } 269 270 return true; 271 } 272 273 async function saveBrandKit(slug, brandKit) { 274 if (!ready) await init(); 275 if (!client) return false; 276 277 const { error } = await client.from('project_design').upsert({ 278 project_slug: slug, 279 colors: brandKit.colors || [], 280 logos: brandKit.logos || [], 281 page_imagery: brandKit.pageImagery || [], 282 scrape_meta: brandKit.scrapeMeta || {}, 283 updated_at: new Date().toISOString() 284 }); 285 286 if (error) { 287 console.warn('[PathCreativesSupabase] saveBrandKit failed', error.message); 288 return false; 289 } 290 291 return true; 292 } 293 294 async function loadGenerate(slug) { 295 if (!ready) await init(); 296 if (!client) return null; 297 298 const { data, error } = await client 299 .from('project_generate') 300 .select('*') 301 .eq('project_slug', slug) 302 .maybeSingle(); 303 304 if (error) { 305 console.warn('[PathCreativesSupabase] loadGenerate failed', error.message); 306 return null; 307 } 308 309 const mapped = mapGenerateRow(slug, data); 310 if (mapped && global.PathCreativesStorage) { 311 mapped.creatives = global.PathCreativesStorage.hydrateCreatives(mapped.creatives); 312 } 313 return mapped; 314 } 315 316 async function saveGenerate(slug, state) { 317 if (!ready) await init(); 318 if (!client) return false;
319 320 let creatives = state.creatives || []; 321 if (global.PathCreativesStorage && state.approved) { 322 try { 323 creatives = await global.PathCreativesStorage.prepareCreatives( 324 slug, 325 creatives, 326 state.offerSlug || 'offer' 327 ); 328 } catch (err) { 329 console.warn('[PathCreativesSupabase] saveGenerate storage prep failed', err.message); 330 } 331 } 332 333 const { error } = await client.from('project_generate').upsert({ 334 project_slug: slug, 335 creatives, 336 selected_index: state.selectedIndex, 337 approved: state.approved, 338 updated_at: new Date().toISOString() 339 }); 340 341 if (error) { 342 console.warn('[PathCreativesSupabase] saveGenerate failed', error.message); 343 return false; 344 } 345 346 return true; 347 } 348 349 async function listProjects() { 350 if (!ready) await init(); 351 if (!client) return null; 352 353 const { data, error } = await client 354 .from('projects') 355 .select('*') 356 .order('updated_at', { ascending: false }); 357 358 if (error) { 359 console.warn('[PathCreativesSupabase] listProjects failed', error.message); 360 return null; 361 } 362 363 return data; 364 } 365 366 async function loadWizardProgress(slug) { 367 if (!ready) await init(); 368 if (!client) return null; 369 370 const { data, error } = await client 371 .from('project_wizard_progress') 372 .select('*') 373 .eq('project_slug', slug) 374 .maybeSingle(); 375 376 if (error) { 377 console.warn('[PathCreativesSupabase] loadWizardProgress failed', error.message); 378 return null; 379 } 380 381 return mapWizardProgressRow(slug, data); 382 } 383 384 async function saveWizardProgress(slug, state) { 385 if (!ready) await init(); 386 if (!client) return false; 387 388 const { error } = await client.from('project_wizard_progress').upsert({ 389 project_slug: slug, 390 current_step: Number.isFinite(state.currentStep) ? state.currentStep : 1, 391 max_completed_step: Number.isFinite(state.maxCompletedStep) ? state.maxCompletedStep : 0, 392 url_entered_at: state.urlEnteredAt || null, 393 brief_approved_at: state.briefApprovedAt || null, 394 assets_approved_at: state.assetsApprovedAt || null, 395 imagery_approved_at: state.imageryApprovedAt || null, 396 generate_approved_at: state.generateApprovedAt || null, 397 last_step_entered_at: state.lastStepEnteredAt || new Date().toISOString(), 398 step_meta: state.stepMeta || {}, 399 updated_at: new Date().toISOString() 400 }); 401 402 if (error) { 403 console.warn('[PathCreativesSupabase] saveWizardProgress failed', error.message); 404 return false; 405 } 406 407 return true; 408 } 409 410 async function logWizardActivity(slug, { step, action, detail }) { 411 if (!ready) await init(); 412 if (!client || !action) return false; 413 414 const { error } = await client.from('wizard_activity_log').insert({ 415 project_slug: slug, 416 step: Number.isFinite(step) ? step : null, 417 action, 418 detail: detail || {} 419 }); 420 421 if (error) { 422 console.warn('[PathCreativesSupabase] logWizardActivity failed', error.message); 423 return false; 424 } 425 426 return true; 427 } 428 429 async function listWizardActivity(slug, limit = 50) { 430 if (!ready) await init(); 431 if (!client) return null; 432 433 const { data, error } = await client 434 .from('wizard_activity_log') 435 .select('*') 436 .eq('project_slug', slug) 437 .order('created_at', { ascending: false }) 438 .limit(limit); 439 440 if (error) { 441 console.warn('[PathCreativesSupabase] listWizardActivity failed', error.message); 442 return null; 443 } 444 445 return data; 446 } 447 448 async function loadHistory(slug) { 449 if (!ready) await init(); 450 if (!client) return null; 451 452 const { data, error } = await client 453 .from('project_history') 454 .select('*') 455 .eq('project_slug', slug) 456 .maybeSingle(); 457 458 if (error) { 459 console.warn('[PathCreativesSupabase] loadHistory failed', error.message); 460 return null; 461 } 462 463 const mapped = mapHistoryRow(slug, data); 464 if (mapped && global.PathCreativesStorage) { 465 mapped.creatives = global.PathCreativesStorage.hydrateCreatives(mapped.creatives); 466 mapped.thumbnails = (mapped.thumbnails || []).map((thumb) => 467 global.PathCreativesStorage.resolveAssetUrl(getClient(), { src: thumb }) || thumb 468 ); 469 mapped.logoUrl = global.PathCreativesStorage.resolveAssetUrl(getClient(), { src: mapped.logoUrl }) 470 || mapped.logoUrl; 471 } 472 return mapped; 473 } 474 475 async function saveHistory(slug, entry) { 476 if (!ready) await init(); 477 if (!client) return false;
478 479 let prepared = entry; 480 if (global.PathCreativesStorage) { 481 try { 482 prepared = await global.PathCreativesStorage.prepareHistoryForRemote(slug, entry); 483 } catch (err) { 484 console.warn('[PathCreativesSupabase] saveHistory storage prep failed', err.message); 485 } 486 } 487 488 const now = new Date(); 489 const sortDate = prepared.sortDate 490 || Number(now.toISOString().slice(0, 10).replace(/-/g, '')); 491 492 const { error } = await client.from('project_history').upsert({ 493 project_slug: slug, 494 display_date: prepared.displayDate || now.toISOString().slice(0, 10), 495 sort_date: sortDate, 496 thumbnails: prepared.thumbnails || [], 497 colors: prepared.colors || [], 498 logo_url: prepared.logoUrl || null, 499 creatives: prepared.creatives || [], 500 creative_count: Number.isFinite(prepared.creativeCount) ? prepared.creativeCount : 4, 501 insights: Number.isFinite(prepared.insights) ? prepared.insights : 0, 502 published_at: prepared.publishedAt || (prepared.status === 'delivered' ? now.toISOString() : null), 503 snapshot: prepared.snapshot || {}, 504 updated_at: now.toISOString() 505 }); 506 507 if (error) { 508 console.warn('[PathCreativesSupabase] saveHistory failed', error.message); 509 return false; 510 } 511 512 return true; 513 } 514 515 async function listHistoryProjects() { 516 if (!ready) await init(); 517 if (!client) return null; 518 519 const { data, error } = await client 520 .from('history_project_list') 521 .select('*') 522 .order('sort_date', { ascending: false, nullsFirst: false }) 523 .order('updated_at', { ascending: false }); 524 525 if (error) { 526 console.warn('[PathCreativesSupabase] listHistoryProjects failed', error.message); 527 return null; 528 } 529 530 return (data || []).map(mapHistoryListRow).filter(Boolean); 531 } 532 533 /** 534 * Record stepper navigation / approval in one call. 535 * step: 1=Brief, 2=Assets, 3=Design, 4=Compile 536 */ 537 async function recordWizardStep(slug, step, action, detail = {}) { 538 const existing = (await loadWizardProgress(slug)) || { 539 projectSlug: slug, 540 currentStep: 1, 541 maxCompletedStep: 0, 542 stepMeta: {} 543 }; 544 545 const now = new Date().toISOString(); 546 const next = { 547 ...existing, 548 currentStep: step, 549 lastStepEnteredAt: now, 550 stepMeta: { ...existing.stepMeta, ...(detail.stepMeta || {}) } 551 }; 552 553 if (action === 'step_approved') { 554 next.maxCompletedStep = Math.max(existing.maxCompletedStep || 0, step); 555 if (step === 1) next.briefApprovedAt = now; 556 if (step === 2) next.assetsApprovedAt = now; 557 if (step === 3) next.imageryApprovedAt = now; 558 if (step === 4) next.generateApprovedAt = now; 559 } 560 561 if (action === 'url_entered' || step === 1) { 562 if (!next.urlEnteredAt) next.urlEnteredAt = now; 563 } 564 565 await Promise.all([ 566 saveWizardProgress(slug, next), 567 logWizardActivity(slug, { step, action, detail }) 568 ]); 569 570 return next; 571 } 572 573 global.PathCreativesSupabase = { 574 init, 575 isReady, 576 getClient, 577 upsertProject, 578 loadProject, 579 saveBrandKit, 580 loadDesign, 581 saveDesign, 582 loadGenerate, 583 saveGenerate, 584 listProjects, 585 loadWizardProgress, 586 saveWizardProgress, 587 logWizardActivity, 588 listWizardActivity, 589 recordWizardStep, 590 loadHistory, 591 saveHistory, 592 listHistoryProjects 593 }; 594})(typeof window !== 'undefined' ? window : globalThis);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.