Files
SlabUploader/frontend/src/create/publish/publishPreviewModel.ts
T
grokbot_ty 73e28180c8 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.
2026-09-26 11:47:18 -07:00

134 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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() || '—',
};
}