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:
@@ -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() || '—',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user