@asteby/storefront-react 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ // src/fitment/fitment.ts
4
+ var DEFAULT_FITMENT_CONFIG = {
5
+ tagPrefix: "fit:",
6
+ universalTag: "fit:universal",
7
+ stateTagPrefix: "fit-state:",
8
+ compatAttribute: "compatibility",
9
+ rowSeparator: ";",
10
+ cellSeparator: "|"
11
+ };
12
+ function fitmentConfig(overrides = {}) {
13
+ const tagPrefix = overrides.tagPrefix ?? DEFAULT_FITMENT_CONFIG.tagPrefix;
14
+ return {
15
+ ...DEFAULT_FITMENT_CONFIG,
16
+ ...overrides,
17
+ tagPrefix,
18
+ universalTag: overrides.universalTag ?? `${tagPrefix}universal`
19
+ };
20
+ }
21
+ var EMPTY_DRAFT = { make: "", year: "", model: "", engine: "" };
22
+ var isNum = (v) => typeof v === "number" && Number.isFinite(v);
23
+ var isText = (v) => typeof v === "string" && v.length > 0;
24
+ function makeMeta(m, aliases) {
25
+ const raw = typeof m.priority === "string" ? m.priority.toLowerCase() : "";
26
+ const priority = aliases[raw] ?? raw;
27
+ const out = {};
28
+ if (priority === "high" || priority === "medium" || priority === "low") out.priority = priority;
29
+ if (isText(m.kind)) out.kind = m.kind;
30
+ for (const k of ["refs", "observed_from", "observed_to", "typical_from", "typical_to"]) {
31
+ if (isNum(m[k])) out[k] = m[k];
32
+ }
33
+ return out;
34
+ }
35
+ function labelFromId(id) {
36
+ return id.split("-").filter(Boolean).join(" ").toUpperCase();
37
+ }
38
+ function parseCatalog(raw, opts = {}) {
39
+ let makesRaw = raw?.makes;
40
+ if (typeof makesRaw === "string") {
41
+ try {
42
+ makesRaw = JSON.parse(makesRaw);
43
+ } catch {
44
+ makesRaw = [];
45
+ }
46
+ }
47
+ const aliases = opts.priorityAliases ?? {};
48
+ const makes = [];
49
+ for (const m of Array.isArray(makesRaw) ? makesRaw : []) {
50
+ if (!m || !isText(m.id) || !isText(m.label) || !Array.isArray(m.models)) continue;
51
+ const models = [];
52
+ for (const entry of m.models) {
53
+ const mo = Array.isArray(entry) ? { id: entry[0], from: entry[1], to: entry[2], engines: entry[3] } : entry;
54
+ if (!mo || !isText(mo.id) || !isNum(mo.from) || !isNum(mo.to) || !Array.isArray(mo.engines)) continue;
55
+ const engines = mo.engines.map((e) => typeof e === "string" ? { id: e, label: labelFromId(e) } : e).filter((e) => e && isText(e.id)).map((e) => ({
56
+ id: e.id,
57
+ label: isText(e.label) ? e.label : labelFromId(e.id),
58
+ ...isNum(e.from) ? { from: e.from } : {},
59
+ ...isNum(e.to) ? { to: e.to } : {}
60
+ }));
61
+ if (engines.length) models.push({ id: mo.id, label: isText(mo.label) ? mo.label : labelFromId(mo.id), from: mo.from, to: mo.to, engines });
62
+ }
63
+ if (models.length || m.models.length === 0) makes.push({ id: m.id, label: m.label, models, ...makeMeta(m, aliases) });
64
+ }
65
+ return { makes };
66
+ }
67
+ function vehicleKey(v) {
68
+ return `${v.make}/${v.model}/${v.year}/${v.engine}`;
69
+ }
70
+ function fitTag(key, config = DEFAULT_FITMENT_CONFIG) {
71
+ return `${config.tagPrefix}${key}`;
72
+ }
73
+ var engineCovers = (e, model, year) => year >= (e.from ?? model.from) && year <= (e.to ?? model.to);
74
+ var modelCovers = (m, year) => year == null || year >= m.from && year <= m.to;
75
+ function num(v) {
76
+ const n = Number(v);
77
+ return v !== "" && Number.isInteger(n) ? n : null;
78
+ }
79
+ function resolveVehicle(catalog, key) {
80
+ if (!key) return null;
81
+ const [makeId, modelId, yearText, engineId] = key.split("/");
82
+ const year = num(yearText ?? "");
83
+ const make = catalog.makes.find((m) => m.id === makeId);
84
+ const model = make?.models.find((m) => m.id === modelId);
85
+ if (!make || !model || year == null || !modelCovers(model, year)) return null;
86
+ const engine = model.engines.find((e) => e.id === engineId && engineCovers(e, model, year));
87
+ if (!engine) return null;
88
+ return {
89
+ key: vehicleKey({ make: make.id, model: model.id, year, engine: engine.id }),
90
+ make: { id: make.id, label: make.label },
91
+ model: { id: model.id, label: model.label },
92
+ year,
93
+ engine: { id: engine.id, label: engine.label }
94
+ };
95
+ }
96
+ function vehicleName(v) {
97
+ return `${v.make.label} ${v.model.label} ${v.year}`;
98
+ }
99
+ function vehicleLongName(v) {
100
+ return `${vehicleName(v)} \xB7 ${v.engine.label}`;
101
+ }
102
+ function modelsOf(catalog, draft) {
103
+ const year = num(draft.year);
104
+ return catalog.makes.filter((m) => !draft.make || m.id === draft.make).flatMap((make) => make.models.filter((m) => modelCovers(m, year)).map((model) => ({ make, model })));
105
+ }
106
+ function makeOptions(catalog) {
107
+ return catalog.makes.filter((m) => m.models.length > 0).map((m) => ({ value: m.id, label: m.label }));
108
+ }
109
+ function yearOptions(catalog, draft) {
110
+ const models = catalog.makes.filter((m) => !draft.make || m.id === draft.make).flatMap((m) => m.models).filter((m) => !draft.model || m.id === draft.model);
111
+ if (models.length === 0) return [];
112
+ const from = Math.min(...models.map((m) => m.from));
113
+ const to = Math.max(...models.map((m) => m.to));
114
+ const years = [];
115
+ for (let y = to; y >= from; y--) years.push({ value: String(y), label: String(y) });
116
+ return years;
117
+ }
118
+ function modelOptions(catalog, draft) {
119
+ return modelsOf(catalog, draft).map(({ make, model }) => ({
120
+ value: draft.make ? model.id : `${make.id}/${model.id}`,
121
+ label: draft.make ? model.label : `${make.label} ${model.label}`
122
+ }));
123
+ }
124
+ function splitModelValue(value) {
125
+ const i = value.indexOf("/");
126
+ return i === -1 ? { model: value } : { make: value.slice(0, i), model: value.slice(i + 1) };
127
+ }
128
+ function engineOptions(catalog, draft) {
129
+ const year = num(draft.year);
130
+ const seen = /* @__PURE__ */ new Map();
131
+ for (const { model } of modelsOf(catalog, draft).filter(({ model: model2 }) => !draft.model || model2.id === draft.model)) {
132
+ for (const e of model.engines) {
133
+ if (year == null || engineCovers(e, model, year)) seen.set(e.id, e.label);
134
+ }
135
+ }
136
+ return [...seen].map(([value, label]) => ({ value, label }));
137
+ }
138
+ function reconcileDraft(catalog, draft) {
139
+ let next = { ...draft };
140
+ if (next.year && !yearOptions(catalog, { make: next.make, model: "" }).some((o) => o.value === next.year)) next = { ...next, year: "" };
141
+ if (next.model && !next.make) {
142
+ const owners = catalog.makes.filter((m) => m.models.some((mo) => mo.id === next.model));
143
+ const [owner] = owners;
144
+ if (owners.length === 1 && owner) next = { ...next, make: owner.id };
145
+ }
146
+ if (next.model && !modelOptions(catalog, next).some((o) => o.value === next.model)) next = { ...next, model: "" };
147
+ if (next.engine && !engineOptions(catalog, next).some((o) => o.value === next.engine)) next = { ...next, engine: "" };
148
+ return next;
149
+ }
150
+ function draftFromVehicle(v) {
151
+ return v ? { make: v.make.id, year: String(v.year), model: v.model.id, engine: v.engine.id } : EMPTY_DRAFT;
152
+ }
153
+ function vehicleFromDraft(catalog, draft) {
154
+ return resolveVehicle(catalog, vehicleKey({ make: draft.make, model: draft.model, year: draft.year, engine: draft.engine }));
155
+ }
156
+ function popularMakes(catalog) {
157
+ return catalog.makes.filter((m) => m.priority === "high" && m.models.length > 0).sort((a, b) => (b.refs ?? 0) - (a.refs ?? 0));
158
+ }
159
+ function popularVehicles(catalog, limit = 6) {
160
+ const out = [];
161
+ for (const make of popularMakes(catalog)) {
162
+ if (out.length >= limit) break;
163
+ const model = make.models[0];
164
+ if (!model) continue;
165
+ const mid = make.typical_from != null && make.typical_to != null ? Math.round((make.typical_from + make.typical_to) / 2) : model.to;
166
+ const year = Math.min(model.to, Math.max(model.from, mid));
167
+ const engine = model.engines.find((e) => engineCovers(e, model, year));
168
+ if (!engine) continue;
169
+ out.push({ key: vehicleKey({ make: make.id, model: model.id, year, engine: engine.id }), label: `${make.label} ${model.label} ${year}` });
170
+ }
171
+ return out;
172
+ }
173
+ function fitStatus(product, vehicle, config = DEFAULT_FITMENT_CONFIG) {
174
+ const fits = product.tags.filter((t) => t.startsWith(config.tagPrefix));
175
+ if (fits.length === 0) return "unknown";
176
+ if (fits.includes(config.universalTag)) return "universal";
177
+ if (!vehicle) return "unknown";
178
+ return fits.includes(fitTag(vehicle.key, config)) ? "fits" : "no";
179
+ }
180
+ function fitFilterTags(vehicle, config = DEFAULT_FITMENT_CONFIG) {
181
+ return [fitTag(vehicle.key, config), config.universalTag];
182
+ }
183
+ function compatRows(product, config = DEFAULT_FITMENT_CONFIG) {
184
+ const raw = product.attributes[config.compatAttribute];
185
+ if (typeof raw !== "string" || !raw) return [];
186
+ return raw.split(config.rowSeparator).map((row) => row.split(config.cellSeparator).map((c) => c.trim())).flatMap((c) => {
187
+ const [make, model, years, engine, note] = c;
188
+ return c.length >= 4 && make && model ? [{ make, model, years: years ?? "", engine: engine ?? "", note: note || null }] : [];
189
+ });
190
+ }
191
+ function rowCoversYear(years, year) {
192
+ const [from, to] = years.split(/[–-]/).map((s) => Number(s.trim()));
193
+ if (from === void 0 || !Number.isFinite(from)) return false;
194
+ return year >= from && year <= (to !== void 0 && Number.isFinite(to) ? to : from);
195
+ }
196
+ function withFit(product, vehicle, config = DEFAULT_FITMENT_CONFIG) {
197
+ const state = fitStatus(product, vehicle, config);
198
+ return { ...product, tags: [...product.tags.filter((t) => !t.startsWith(config.tagPrefix)), `${config.stateTagPrefix}${state}`] };
199
+ }
200
+ function fitState(product, config = DEFAULT_FITMENT_CONFIG) {
201
+ const tag = product.tags.find((t) => t.startsWith(config.stateTagPrefix));
202
+ const value = tag?.slice(config.stateTagPrefix.length);
203
+ return value === "fits" || value === "universal" || value === "no" ? value : "unknown";
204
+ }
205
+ function safeRedirect(target, fallback = "/") {
206
+ return typeof target === "string" && target.startsWith("/") && !target.startsWith("//") && !target.includes("\\") ? target : fallback;
207
+ }
208
+ function withoutQueryParam(path, param) {
209
+ const url = new URL(path, "http://local.invalid");
210
+ url.searchParams.delete(param);
211
+ return `${url.pathname}${url.search}`;
212
+ }
213
+
214
+ exports.DEFAULT_FITMENT_CONFIG = DEFAULT_FITMENT_CONFIG;
215
+ exports.EMPTY_DRAFT = EMPTY_DRAFT;
216
+ exports.compatRows = compatRows;
217
+ exports.draftFromVehicle = draftFromVehicle;
218
+ exports.engineOptions = engineOptions;
219
+ exports.fitFilterTags = fitFilterTags;
220
+ exports.fitState = fitState;
221
+ exports.fitStatus = fitStatus;
222
+ exports.fitTag = fitTag;
223
+ exports.fitmentConfig = fitmentConfig;
224
+ exports.makeOptions = makeOptions;
225
+ exports.modelOptions = modelOptions;
226
+ exports.parseCatalog = parseCatalog;
227
+ exports.popularMakes = popularMakes;
228
+ exports.popularVehicles = popularVehicles;
229
+ exports.reconcileDraft = reconcileDraft;
230
+ exports.resolveVehicle = resolveVehicle;
231
+ exports.rowCoversYear = rowCoversYear;
232
+ exports.safeRedirect = safeRedirect;
233
+ exports.splitModelValue = splitModelValue;
234
+ exports.vehicleFromDraft = vehicleFromDraft;
235
+ exports.vehicleKey = vehicleKey;
236
+ exports.vehicleLongName = vehicleLongName;
237
+ exports.vehicleName = vehicleName;
238
+ exports.withFit = withFit;
239
+ exports.withoutQueryParam = withoutQueryParam;
240
+ exports.yearOptions = yearOptions;
241
+ //# sourceMappingURL=fitment.cjs.map
242
+ //# sourceMappingURL=fitment.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fitment/fitment.ts"],"names":["model"],"mappings":"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","file":"fitment.cjs","sourcesContent":["// Vehicle fitment (\"Mi vehículo\"): a make → model → year → engine catalog, vehicle keys, the\n// cascading selector options and the fit state of a product. Pure functions with no UI, no\n// storage and no copy: every tag prefix, attribute name and legacy spelling is injectable through\n// `FitmentConfig`, so any store that sells parts can use it. Nothing here decides on its own\n// whether a part fits a vehicle: it only compares what the backend (OPS) published.\n\nexport type VehicleEngine = { id: string; label: string; from?: number; to?: number }\nexport type VehicleModel = { id: string; label: string; from: number; to: number; engines: VehicleEngine[] }\nexport type MakePriority = 'high' | 'medium' | 'low'\n/**\n * A make of the catalog. `models` may be empty while the catalog is being imported: the make is\n * declared but it does not show in the selector until it has models.\n */\nexport type VehicleMake = {\n id: string\n label: string\n models: VehicleModel[]\n priority?: MakePriority\n /** Free-form segment of the make (e.g. light / heavy duty). Passed through as published. */\n kind?: string\n refs?: number\n observed_from?: number\n observed_to?: number\n typical_from?: number\n typical_to?: number\n}\nexport type VehicleCatalog = { makes: VehicleMake[] }\n\n/** Partial selection of the selector (all text, as it arrives from a form). */\nexport type VehicleDraft = { make: string; year: string; model: string; engine: string }\n\n/** A complete vehicle resolved against the catalog (with labels to display). */\nexport type Vehicle = {\n key: string\n make: { id: string; label: string }\n model: { id: string; label: string }\n year: number\n engine: { id: string; label: string }\n}\n\nexport type Option = { value: string; label: string }\n\n/** Fit state of a product for a vehicle. */\nexport type Fit = 'fits' | 'universal' | 'no' | 'unknown'\n\nexport type FitmentConfig = {\n /** Prefix of the tags that publish compatibility: `<prefix><vehicle key>`. Default `fit:`. */\n tagPrefix: string\n /** Tag that means \"fits everything\". Default `<tagPrefix>universal`. */\n universalTag: string\n /** Prefix of the single resolved-state tag written by `withFit`. Default `fit-state:`. */\n stateTagPrefix: string\n /** Product attribute holding the compatibility table. Default `compatibility`. */\n compatAttribute: string\n /** Separators of the compact compatibility table. */\n rowSeparator: string\n cellSeparator: string\n}\n\nexport const DEFAULT_FITMENT_CONFIG: FitmentConfig = {\n tagPrefix: 'fit:',\n universalTag: 'fit:universal',\n stateTagPrefix: 'fit-state:',\n compatAttribute: 'compatibility',\n rowSeparator: ';',\n cellSeparator: '|',\n}\n\n/** Merges overrides; an unset `universalTag` follows a custom `tagPrefix`. */\nexport function fitmentConfig(overrides: Partial<FitmentConfig> = {}): FitmentConfig {\n const tagPrefix = overrides.tagPrefix ?? DEFAULT_FITMENT_CONFIG.tagPrefix\n return {\n ...DEFAULT_FITMENT_CONFIG,\n ...overrides,\n tagPrefix,\n universalTag: overrides.universalTag ?? `${tagPrefix}universal`,\n }\n}\n\nexport const EMPTY_DRAFT: VehicleDraft = { make: '', year: '', model: '', engine: '' }\n\nconst isNum = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v)\nconst isText = (v: unknown): v is string => typeof v === 'string' && v.length > 0\n\nexport type ParseCatalogOptions = {\n /** Maps the backend's spelling of a priority to the canonical one (e.g. `{ alta: 'high' }`). English values always work. */\n priorityAliases?: Record<string, MakePriority>\n}\n\nfunction makeMeta(m: Record<string, unknown>, aliases: Record<string, MakePriority>): Partial<VehicleMake> {\n const raw = typeof m.priority === 'string' ? m.priority.toLowerCase() : ''\n const priority = aliases[raw] ?? raw\n const out: Partial<VehicleMake> = {}\n if (priority === 'high' || priority === 'medium' || priority === 'low') out.priority = priority\n if (isText(m.kind)) out.kind = m.kind\n for (const k of ['refs', 'observed_from', 'observed_to', 'typical_from', 'typical_to'] as const) {\n if (isNum(m[k])) out[k] = m[k] as number\n }\n return out\n}\n\n/** \"np300-frontier\" → \"NP300 FRONTIER\"; \"1.9-tdi\" → \"1.9 TDI\". */\nfunction labelFromId(id: string): string {\n return id.split('-').filter(Boolean).join(' ').toUpperCase()\n}\n\n/**\n * Reads the vehicle catalog from the props of a content section; drops whatever has the wrong shape.\n * Accepts the compact format (a real catalog weighs tens of KB and OPS caps each write): a model may\n * be a tuple `[id, from, to, engines]`, omit `label` (derived from the id) and list engines as ids.\n * Props stored as text (`makes` as a JSON string) are parsed.\n */\nexport function parseCatalog(raw: unknown, opts: ParseCatalogOptions = {}): VehicleCatalog {\n let makesRaw = (raw as { makes?: unknown } | null | undefined)?.makes\n if (typeof makesRaw === 'string') {\n try {\n makesRaw = JSON.parse(makesRaw)\n } catch {\n makesRaw = []\n }\n }\n const aliases = opts.priorityAliases ?? {}\n const makes: VehicleMake[] = []\n for (const m of Array.isArray(makesRaw) ? makesRaw : []) {\n if (!m || !isText(m.id) || !isText(m.label) || !Array.isArray(m.models)) continue\n const models: VehicleModel[] = []\n for (const entry of m.models) {\n const mo = Array.isArray(entry) ? { id: entry[0], from: entry[1], to: entry[2], engines: entry[3] } : entry\n if (!mo || !isText(mo.id) || !isNum(mo.from) || !isNum(mo.to) || !Array.isArray(mo.engines)) continue\n const engines: VehicleEngine[] = mo.engines\n .map((e: unknown) => (typeof e === 'string' ? { id: e, label: labelFromId(e) } : e))\n .filter((e: VehicleEngine) => e && isText(e.id))\n .map((e: VehicleEngine) => ({\n id: e.id,\n label: isText(e.label) ? e.label : labelFromId(e.id),\n ...(isNum(e.from) ? { from: e.from } : {}),\n ...(isNum(e.to) ? { to: e.to } : {}),\n }))\n if (engines.length) models.push({ id: mo.id, label: isText(mo.label) ? mo.label : labelFromId(mo.id), from: mo.from, to: mo.to, engines })\n }\n // A make declared without models (waiting for the import) is kept; one with only invalid models is dropped.\n if (models.length || m.models.length === 0) makes.push({ id: m.id, label: m.label, models, ...makeMeta(m, aliases) })\n }\n return { makes }\n}\n\nexport function vehicleKey(v: { make: string; model: string; year: number | string; engine: string }): string {\n return `${v.make}/${v.model}/${v.year}/${v.engine}`\n}\n\n/** Tag that marks a part as fitting the vehicle with this key. */\nexport function fitTag(key: string, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): string {\n return `${config.tagPrefix}${key}`\n}\n\nconst engineCovers = (e: VehicleEngine, model: VehicleModel, year: number) =>\n year >= (e.from ?? model.from) && year <= (e.to ?? model.to)\n\nconst modelCovers = (m: VehicleModel, year: number | null) => year == null || (year >= m.from && year <= m.to)\n\nfunction num(v: string): number | null {\n const n = Number(v)\n return v !== '' && Number.isInteger(n) ? n : null\n}\n\n/** Resolves a `make/model/year/engine` key against the catalog; null when it does not exist. */\nexport function resolveVehicle(catalog: VehicleCatalog, key: string | null | undefined): Vehicle | null {\n if (!key) return null\n const [makeId, modelId, yearText, engineId] = key.split('/')\n const year = num(yearText ?? '')\n const make = catalog.makes.find((m) => m.id === makeId)\n const model = make?.models.find((m) => m.id === modelId)\n if (!make || !model || year == null || !modelCovers(model, year)) return null\n const engine = model.engines.find((e) => e.id === engineId && engineCovers(e, model, year))\n if (!engine) return null\n return {\n key: vehicleKey({ make: make.id, model: model.id, year, engine: engine.id }),\n make: { id: make.id, label: make.label },\n model: { id: model.id, label: model.label },\n year,\n engine: { id: engine.id, label: engine.label },\n }\n}\n\n/** Short name for chips and badges: \"Make Model 2018\". */\nexport function vehicleName(v: Vehicle): string {\n return `${v.make.label} ${v.model.label} ${v.year}`\n}\n\n/** With engine: \"Make Model 2018 · 1.6 L\". */\nexport function vehicleLongName(v: Vehicle): string {\n return `${vehicleName(v)} · ${v.engine.label}`\n}\n\n// --- Cascade make → year → model → engine ------------------------------------------------------\n// Every list filters by what is already chosen, in any order. With nothing chosen they show\n// everything (so the form also works without JavaScript).\n\nfunction modelsOf(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'year'>): { make: VehicleMake; model: VehicleModel }[] {\n const year = num(draft.year)\n return catalog.makes\n .filter((m) => !draft.make || m.id === draft.make)\n .flatMap((make) => make.models.filter((m) => modelCovers(m, year)).map((model) => ({ make, model })))\n}\n\nexport function makeOptions(catalog: VehicleCatalog): Option[] {\n return catalog.makes.filter((m) => m.models.length > 0).map((m) => ({ value: m.id, label: m.label }))\n}\n\nexport function yearOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'model'>): Option[] {\n const models = catalog.makes\n .filter((m) => !draft.make || m.id === draft.make)\n .flatMap((m) => m.models)\n .filter((m) => !draft.model || m.id === draft.model)\n if (models.length === 0) return []\n const from = Math.min(...models.map((m) => m.from))\n const to = Math.max(...models.map((m) => m.to))\n const years: Option[] = []\n for (let y = to; y >= from; y--) years.push({ value: String(y), label: String(y) })\n return years\n}\n\n/**\n * Model options. Model ids repeat across makes (`pick-up` exists in several): with no make chosen\n * the value is `make/model` (unique) and `splitModelValue` sets the make when it is picked; with a\n * make chosen the value is the model id.\n */\nexport function modelOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'year'>): Option[] {\n return modelsOf(catalog, draft).map(({ make, model }) => ({\n value: draft.make ? model.id : `${make.id}/${model.id}`,\n label: draft.make ? model.label : `${make.label} ${model.label}`,\n }))\n}\n\n/** `make/model` (value of the make-less list) → { make, model }; a bare id returns only the model. */\nexport function splitModelValue(value: string): { make?: string; model: string } {\n const i = value.indexOf('/')\n return i === -1 ? { model: value } : { make: value.slice(0, i), model: value.slice(i + 1) }\n}\n\nexport function engineOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'year' | 'model'>): Option[] {\n const year = num(draft.year)\n const seen = new Map<string, string>()\n for (const { model } of modelsOf(catalog, draft).filter(({ model }) => !draft.model || model.id === draft.model)) {\n for (const e of model.engines) {\n if (year == null || engineCovers(e, model, year)) seen.set(e.id, e.label)\n }\n }\n return [...seen].map(([value, label]) => ({ value, label }))\n}\n\n/** Drops what was chosen and is no longer valid after changing another field. */\nexport function reconcileDraft(catalog: VehicleCatalog, draft: VehicleDraft): VehicleDraft {\n let next = { ...draft }\n if (next.year && !yearOptions(catalog, { make: next.make, model: '' }).some((o) => o.value === next.year)) next = { ...next, year: '' }\n // A bare model id sets the make only when it belongs to a single one; repeated ids are ambiguous and dropped.\n if (next.model && !next.make) {\n const owners = catalog.makes.filter((m) => m.models.some((mo) => mo.id === next.model))\n const [owner] = owners\n if (owners.length === 1 && owner) next = { ...next, make: owner.id }\n }\n if (next.model && !modelOptions(catalog, next).some((o) => o.value === next.model)) next = { ...next, model: '' }\n if (next.engine && !engineOptions(catalog, next).some((o) => o.value === next.engine)) next = { ...next, engine: '' }\n return next\n}\n\nexport function draftFromVehicle(v: Vehicle | null): VehicleDraft {\n return v ? { make: v.make.id, year: String(v.year), model: v.model.id, engine: v.engine.id } : EMPTY_DRAFT\n}\n\n/** Complete vehicle from the form, validated against the catalog. */\nexport function vehicleFromDraft(catalog: VehicleCatalog, draft: VehicleDraft): Vehicle | null {\n return resolveVehicle(catalog, vehicleKey({ make: draft.make, model: draft.model, year: draft.year, engine: draft.engine }))\n}\n\n// --- Popular vehicles --------------------------------------------------------------------------\n// They come from the data: makes with `high` priority in the catalog, most references first.\n\nexport function popularMakes(catalog: VehicleCatalog): VehicleMake[] {\n return catalog.makes.filter((m) => m.priority === 'high' && m.models.length > 0).sort((a, b) => (b.refs ?? 0) - (a.refs ?? 0))\n}\n\n/**\n * One shortcut per popular make: its first model in a typical year (the middle of the make's typical\n * range, clamped to the model's range) with the first engine that covers that year.\n */\nexport function popularVehicles(catalog: VehicleCatalog, limit = 6): { key: string; label: string }[] {\n const out: { key: string; label: string }[] = []\n for (const make of popularMakes(catalog)) {\n if (out.length >= limit) break\n const model = make.models[0]\n if (!model) continue\n const mid = make.typical_from != null && make.typical_to != null ? Math.round((make.typical_from + make.typical_to) / 2) : model.to\n const year = Math.min(model.to, Math.max(model.from, mid))\n const engine = model.engines.find((e) => engineCovers(e, model, year))\n if (!engine) continue\n out.push({ key: vehicleKey({ make: make.id, model: model.id, year, engine: engine.id }), label: `${make.label} ${model.label} ${year}` })\n }\n return out\n}\n\n// --- Fitment -----------------------------------------------------------------------------------\n\ntype WithTags = { tags: readonly string[] }\ntype WithAttributes = { attributes: Record<string, unknown> }\n\n/**\n * Does the part fit the vehicle? Only compares what the backend published: a `<prefix><key>` tag\n * (fits), the universal tag (fits everything) or, when the part has other fit tags, no fit. With no\n * fitment data nothing is asserted (\"unknown\").\n */\nexport function fitStatus(product: WithTags, vehicle: { key: string } | null, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): Fit {\n const fits = product.tags.filter((t) => t.startsWith(config.tagPrefix))\n if (fits.length === 0) return 'unknown'\n if (fits.includes(config.universalTag)) return 'universal'\n if (!vehicle) return 'unknown'\n return fits.includes(fitTag(vehicle.key, config)) ? 'fits' : 'no'\n}\n\n/** Values for a `filter.tag` query that lists only what fits (the API takes them as \"any of\"). */\nexport function fitFilterTags(vehicle: { key: string }, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): string[] {\n return [fitTag(vehicle.key, config), config.universalTag]\n}\n\nexport type CompatRow = { make: string; model: string; years: string; engine: string; note: string | null }\n\n/** Compatibility table: the attribute holds \"Make|Model|Years|Engine|Note;Make|…\". */\nexport function compatRows(product: WithAttributes, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): CompatRow[] {\n const raw = product.attributes[config.compatAttribute]\n if (typeof raw !== 'string' || !raw) return []\n return raw\n .split(config.rowSeparator)\n .map((row) => row.split(config.cellSeparator).map((c) => c.trim()))\n .flatMap((c): CompatRow[] => {\n const [make, model, years, engine, note] = c\n return c.length >= 4 && make && model ? [{ make, model, years: years ?? '', engine: engine ?? '', note: note || null }] : []\n })\n}\n\n/** \"2012-2019\" contains the year (a bare \"2016\" too). */\nexport function rowCoversYear(years: string, year: number): boolean {\n const [from, to] = years.split(/[–-]/).map((s) => Number(s.trim()))\n if (from === undefined || !Number.isFinite(from)) return false\n return year >= from && year <= (to !== undefined && Number.isFinite(to) ? to : from)\n}\n\n/**\n * The backend publishes one tag per compatible year and engine (hundreds per part). Loaders replace\n * them with ONE `<stateTagPrefix><state>` tag, resolved for the chosen vehicle, so they are not\n * serialized into the page.\n */\nexport function withFit<T extends WithTags>(product: T, vehicle: { key: string } | null, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): T {\n const state = fitStatus(product, vehicle, config)\n return { ...product, tags: [...product.tags.filter((t) => !t.startsWith(config.tagPrefix)), `${config.stateTagPrefix}${state}`] }\n}\n\n/** Fit state already resolved by `withFit` (\"unknown\" if the product did not go through it). */\nexport function fitState(product: WithTags, config: FitmentConfig = DEFAULT_FITMENT_CONFIG): Fit {\n const tag = product.tags.find((t) => t.startsWith(config.stateTagPrefix))\n const value = tag?.slice(config.stateTagPrefix.length)\n return value === 'fits' || value === 'universal' || value === 'no' ? value : 'unknown'\n}\n\n/** Validates a redirect target: only relative paths of the own site. */\nexport function safeRedirect(target: string | null | undefined, fallback = '/'): string {\n return typeof target === 'string' && target.startsWith('/') && !target.startsWith('//') && !target.includes('\\\\') ? target : fallback\n}\n\n/** Removes the selector's error flag (default `?vehicle=invalid`) from a path so it can be returned to clean. */\nexport function withoutQueryParam(path: string, param: string): string {\n const url = new URL(path, 'http://local.invalid')\n url.searchParams.delete(param)\n return `${url.pathname}${url.search}`\n}\n"]}
@@ -0,0 +1,179 @@
1
+ type VehicleEngine = {
2
+ id: string;
3
+ label: string;
4
+ from?: number;
5
+ to?: number;
6
+ };
7
+ type VehicleModel = {
8
+ id: string;
9
+ label: string;
10
+ from: number;
11
+ to: number;
12
+ engines: VehicleEngine[];
13
+ };
14
+ type MakePriority = 'high' | 'medium' | 'low';
15
+ /**
16
+ * A make of the catalog. `models` may be empty while the catalog is being imported: the make is
17
+ * declared but it does not show in the selector until it has models.
18
+ */
19
+ type VehicleMake = {
20
+ id: string;
21
+ label: string;
22
+ models: VehicleModel[];
23
+ priority?: MakePriority;
24
+ /** Free-form segment of the make (e.g. light / heavy duty). Passed through as published. */
25
+ kind?: string;
26
+ refs?: number;
27
+ observed_from?: number;
28
+ observed_to?: number;
29
+ typical_from?: number;
30
+ typical_to?: number;
31
+ };
32
+ type VehicleCatalog = {
33
+ makes: VehicleMake[];
34
+ };
35
+ /** Partial selection of the selector (all text, as it arrives from a form). */
36
+ type VehicleDraft = {
37
+ make: string;
38
+ year: string;
39
+ model: string;
40
+ engine: string;
41
+ };
42
+ /** A complete vehicle resolved against the catalog (with labels to display). */
43
+ type Vehicle = {
44
+ key: string;
45
+ make: {
46
+ id: string;
47
+ label: string;
48
+ };
49
+ model: {
50
+ id: string;
51
+ label: string;
52
+ };
53
+ year: number;
54
+ engine: {
55
+ id: string;
56
+ label: string;
57
+ };
58
+ };
59
+ type Option = {
60
+ value: string;
61
+ label: string;
62
+ };
63
+ /** Fit state of a product for a vehicle. */
64
+ type Fit = 'fits' | 'universal' | 'no' | 'unknown';
65
+ type FitmentConfig = {
66
+ /** Prefix of the tags that publish compatibility: `<prefix><vehicle key>`. Default `fit:`. */
67
+ tagPrefix: string;
68
+ /** Tag that means "fits everything". Default `<tagPrefix>universal`. */
69
+ universalTag: string;
70
+ /** Prefix of the single resolved-state tag written by `withFit`. Default `fit-state:`. */
71
+ stateTagPrefix: string;
72
+ /** Product attribute holding the compatibility table. Default `compatibility`. */
73
+ compatAttribute: string;
74
+ /** Separators of the compact compatibility table. */
75
+ rowSeparator: string;
76
+ cellSeparator: string;
77
+ };
78
+ declare const DEFAULT_FITMENT_CONFIG: FitmentConfig;
79
+ /** Merges overrides; an unset `universalTag` follows a custom `tagPrefix`. */
80
+ declare function fitmentConfig(overrides?: Partial<FitmentConfig>): FitmentConfig;
81
+ declare const EMPTY_DRAFT: VehicleDraft;
82
+ type ParseCatalogOptions = {
83
+ /** Maps the backend's spelling of a priority to the canonical one (e.g. `{ alta: 'high' }`). English values always work. */
84
+ priorityAliases?: Record<string, MakePriority>;
85
+ };
86
+ /**
87
+ * Reads the vehicle catalog from the props of a content section; drops whatever has the wrong shape.
88
+ * Accepts the compact format (a real catalog weighs tens of KB and OPS caps each write): a model may
89
+ * be a tuple `[id, from, to, engines]`, omit `label` (derived from the id) and list engines as ids.
90
+ * Props stored as text (`makes` as a JSON string) are parsed.
91
+ */
92
+ declare function parseCatalog(raw: unknown, opts?: ParseCatalogOptions): VehicleCatalog;
93
+ declare function vehicleKey(v: {
94
+ make: string;
95
+ model: string;
96
+ year: number | string;
97
+ engine: string;
98
+ }): string;
99
+ /** Tag that marks a part as fitting the vehicle with this key. */
100
+ declare function fitTag(key: string, config?: FitmentConfig): string;
101
+ /** Resolves a `make/model/year/engine` key against the catalog; null when it does not exist. */
102
+ declare function resolveVehicle(catalog: VehicleCatalog, key: string | null | undefined): Vehicle | null;
103
+ /** Short name for chips and badges: "Make Model 2018". */
104
+ declare function vehicleName(v: Vehicle): string;
105
+ /** With engine: "Make Model 2018 · 1.6 L". */
106
+ declare function vehicleLongName(v: Vehicle): string;
107
+ declare function makeOptions(catalog: VehicleCatalog): Option[];
108
+ declare function yearOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'model'>): Option[];
109
+ /**
110
+ * Model options. Model ids repeat across makes (`pick-up` exists in several): with no make chosen
111
+ * the value is `make/model` (unique) and `splitModelValue` sets the make when it is picked; with a
112
+ * make chosen the value is the model id.
113
+ */
114
+ declare function modelOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'year'>): Option[];
115
+ /** `make/model` (value of the make-less list) → { make, model }; a bare id returns only the model. */
116
+ declare function splitModelValue(value: string): {
117
+ make?: string;
118
+ model: string;
119
+ };
120
+ declare function engineOptions(catalog: VehicleCatalog, draft: Pick<VehicleDraft, 'make' | 'year' | 'model'>): Option[];
121
+ /** Drops what was chosen and is no longer valid after changing another field. */
122
+ declare function reconcileDraft(catalog: VehicleCatalog, draft: VehicleDraft): VehicleDraft;
123
+ declare function draftFromVehicle(v: Vehicle | null): VehicleDraft;
124
+ /** Complete vehicle from the form, validated against the catalog. */
125
+ declare function vehicleFromDraft(catalog: VehicleCatalog, draft: VehicleDraft): Vehicle | null;
126
+ declare function popularMakes(catalog: VehicleCatalog): VehicleMake[];
127
+ /**
128
+ * One shortcut per popular make: its first model in a typical year (the middle of the make's typical
129
+ * range, clamped to the model's range) with the first engine that covers that year.
130
+ */
131
+ declare function popularVehicles(catalog: VehicleCatalog, limit?: number): {
132
+ key: string;
133
+ label: string;
134
+ }[];
135
+ type WithTags = {
136
+ tags: readonly string[];
137
+ };
138
+ type WithAttributes = {
139
+ attributes: Record<string, unknown>;
140
+ };
141
+ /**
142
+ * Does the part fit the vehicle? Only compares what the backend published: a `<prefix><key>` tag
143
+ * (fits), the universal tag (fits everything) or, when the part has other fit tags, no fit. With no
144
+ * fitment data nothing is asserted ("unknown").
145
+ */
146
+ declare function fitStatus(product: WithTags, vehicle: {
147
+ key: string;
148
+ } | null, config?: FitmentConfig): Fit;
149
+ /** Values for a `filter.tag` query that lists only what fits (the API takes them as "any of"). */
150
+ declare function fitFilterTags(vehicle: {
151
+ key: string;
152
+ }, config?: FitmentConfig): string[];
153
+ type CompatRow = {
154
+ make: string;
155
+ model: string;
156
+ years: string;
157
+ engine: string;
158
+ note: string | null;
159
+ };
160
+ /** Compatibility table: the attribute holds "Make|Model|Years|Engine|Note;Make|…". */
161
+ declare function compatRows(product: WithAttributes, config?: FitmentConfig): CompatRow[];
162
+ /** "2012-2019" contains the year (a bare "2016" too). */
163
+ declare function rowCoversYear(years: string, year: number): boolean;
164
+ /**
165
+ * The backend publishes one tag per compatible year and engine (hundreds per part). Loaders replace
166
+ * them with ONE `<stateTagPrefix><state>` tag, resolved for the chosen vehicle, so they are not
167
+ * serialized into the page.
168
+ */
169
+ declare function withFit<T extends WithTags>(product: T, vehicle: {
170
+ key: string;
171
+ } | null, config?: FitmentConfig): T;
172
+ /** Fit state already resolved by `withFit` ("unknown" if the product did not go through it). */
173
+ declare function fitState(product: WithTags, config?: FitmentConfig): Fit;
174
+ /** Validates a redirect target: only relative paths of the own site. */
175
+ declare function safeRedirect(target: string | null | undefined, fallback?: string): string;
176
+ /** Removes the selector's error flag (default `?vehicle=invalid`) from a path so it can be returned to clean. */
177
+ declare function withoutQueryParam(path: string, param: string): string;
178
+
179
+ export { type CompatRow, DEFAULT_FITMENT_CONFIG, EMPTY_DRAFT, type Fit, type FitmentConfig, type MakePriority, type Option, type ParseCatalogOptions, type Vehicle, type VehicleCatalog, type VehicleDraft, type VehicleEngine, type VehicleMake, type VehicleModel, compatRows, draftFromVehicle, engineOptions, fitFilterTags, fitState, fitStatus, fitTag, fitmentConfig, makeOptions, modelOptions, parseCatalog, popularMakes, popularVehicles, reconcileDraft, resolveVehicle, rowCoversYear, safeRedirect, splitModelValue, vehicleFromDraft, vehicleKey, vehicleLongName, vehicleName, withFit, withoutQueryParam, yearOptions };