PageSourceSearch

https://pathcreatives.netlify.app/previews/supabase-client.js

js pathcreatives.netlify.app collected 2026-10-03 11:14:48 UTC 18,004 bytes, 594 lines download raw bytes

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.