feat(publish): Publish preview before Woo draft POST

After Review → Continue to Publish, show a glanceable confirmation:
ordered photo thumbs, SKU/title/price, taxonomy + dims, short and long
descriptions (plain text). Back to Review edits; Publish posts the draft.
Reuses slab/taxonomy/photos only — no invented fields.
This commit is contained in:
grokbot_ty
2026-09-26 11:47:18 -07:00
parent 8f8e0132e6
commit 73e28180c8
6 changed files with 542 additions and 23 deletions
@@ -0,0 +1,133 @@
/**
* Pure builders for the Publish preview confirmation screen.
* Resolves taxonomy ids → display names; never invents fields beyond the slab.
*/
import { decodeName } from '../decodeName';
import type { PhotoRow, Slab, Taxonomy, TaxAttribute, TaxCategory, TaxTag } from '../../api/slabs';
export interface PreviewPhoto {
id: string;
url: string;
role: 'calibration' | 'overlay' | 'batch';
/** Leftmost (seq order) = Woo cover. */
primary: boolean;
}
export interface PreviewFact {
label: string;
value: string;
}
export interface PublishPreviewModel {
photos: PreviewPhoto[];
sku: string;
title: string;
price: string;
facts: PreviewFact[];
shortDescription: string;
description: string;
}
const fmt1 = (v: number) => {
const s = v.toFixed(1);
return s.endsWith('.0') ? s.slice(0, -2) : s;
};
const fmt2 = (v: number) => v.toFixed(2);
function catName(cats: TaxCategory[], id: number | null | undefined): string {
if (id == null) return '—';
const c = cats.find((x) => x.id === id);
return c ? decodeName(c.name) : `#${id}`;
}
function termName(attrs: TaxAttribute[], id: number | null | undefined): string {
if (id == null) return '—';
for (const a of attrs) {
const t = a.terms.find((x) => x.id === id);
if (t) return decodeName(t.name);
}
return `#${id}`;
}
function attrTermsByName(attrs: TaxAttribute[], name: string): { id: number; name: string }[] {
const attr = attrs.find((a) => a.name.toLowerCase() === name.toLowerCase());
return (attr?.terms ?? []).map((t) => ({ id: t.id, name: decodeName(t.name) }));
}
function namesForIds(
ids: number[],
lookup: (id: number) => string | null,
): string {
if (!ids.length) return '—';
return ids.map((id) => lookup(id) ?? `#${id}`).join(', ');
}
function tagName(tags: TaxTag[], id: number): string | null {
const t = tags.find((x) => x.id === id);
return t ? decodeName(t.name) : null;
}
/** Widths as "min–max (avg)" inches, matching Review dims language. */
export function formatWidths(slab: Pick<Slab, 'width_min_in' | 'width_max_in' | 'width_avg_in'>): string {
return `${fmt1(slab.width_min_in)}–${fmt1(slab.width_max_in)}″ (avg ${fmt1(slab.width_avg_in)}″)`;
}
export function buildPublishPreviewModel(
slab: Slab,
tax: Taxonomy | null,
photos: PhotoRow[],
urlFor: (photoId: string, kind: 'original' | 'processed') => string,
): PublishPreviewModel {
const cats = tax?.categories ?? [];
const attrs = tax?.attributes ?? [];
const tags = tax?.tags ?? [];
const figureTerms = attrTermsByName(attrs, 'figure');
const figureLookup = (id: number) => {
const t = figureTerms.find((x) => x.id === id);
return t ? t.name : null;
};
const ordered = [...photos].sort((a, b) => a.seq - b.seq);
const previewPhotos: PreviewPhoto[] = ordered.map((p, i) => ({
id: p.id,
url: urlFor(p.id, p.has_processed ? 'processed' : 'original'),
role: p.role ?? 'batch',
primary: i === 0,
}));
const woodNames = namesForIds(slab.wood_category_ids ?? [], (id) => {
const c = cats.find((x) => x.id === id);
return c ? decodeName(c.name) : null;
});
const facts: PreviewFact[] = [
{ label: 'Species', value: catName(cats, slab.species_id) },
{ label: 'Wood categories', value: woodNames },
{ label: 'Edge', value: termName(attrs, slab.edge_type_term_id) },
{
label: 'Figure',
value: namesForIds(slab.figure_term_ids ?? [], figureLookup),
},
{ label: 'Grade', value: termName(attrs, slab.grade_term_id) },
{ label: 'Moisture', value: termName(attrs, slab.moisture_term_id) },
{
label: 'Features',
value: namesForIds(slab.feat_tag_ids ?? [], (id) => tagName(tags, id)),
},
{ label: 'Length', value: `${fmt1(slab.length_in)}″` },
{ label: 'Widths', value: formatWidths(slab) },
{ label: 'Thickness', value: `${fmt1(slab.thickness_in)}″` },
{ label: 'Bdft', value: fmt2(slab.bdft) },
];
return {
photos: previewPhotos,
sku: slab.sku || '—',
title: slab.title || '—',
price: slab.price != null ? `$${slab.price.toFixed(2)}` : '—',
facts,
shortDescription: (slab.short_description ?? '').trim() || '—',
description: (slab.description ?? '').trim() || '—',
};
}