@stapel/listings-react 0.25.10 → 0.26.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.
- package/CHANGELOG.md +208 -0
- package/README.md +77 -9
- package/dist/api/generated/schema.d.ts +2 -2
- package/dist/api/generated/schema.d.ts.map +1 -1
- package/dist/default/CardBadges.d.ts.map +1 -1
- package/dist/default/CardBadges.js +3 -1
- package/dist/default/CardBadges.js.map +1 -1
- package/dist/default/ListingActions.d.ts +108 -0
- package/dist/default/ListingActions.d.ts.map +1 -0
- package/dist/default/ListingActions.js +29 -0
- package/dist/default/ListingActions.js.map +1 -0
- package/dist/default/ListingCard.d.ts.map +1 -1
- package/dist/default/ListingCard.js +15 -8
- package/dist/default/ListingCard.js.map +1 -1
- package/dist/default/ListingDetailPane.d.ts +76 -56
- package/dist/default/ListingDetailPane.d.ts.map +1 -1
- package/dist/default/ListingDetailPane.js +164 -18
- package/dist/default/ListingDetailPane.js.map +1 -1
- package/dist/default/ListingFeedCard.d.ts.map +1 -1
- package/dist/default/ListingFeedCard.js +2 -1
- package/dist/default/ListingFeedCard.js.map +1 -1
- package/dist/default/ListingPhoto.d.ts.map +1 -1
- package/dist/default/ListingPhoto.js +5 -2
- package/dist/default/ListingPhoto.js.map +1 -1
- package/dist/default/ListingSerpCard.d.ts.map +1 -1
- package/dist/default/ListingSerpCard.js +18 -4
- package/dist/default/ListingSerpCard.js.map +1 -1
- package/dist/default/MyListingsPane.d.ts.map +1 -1
- package/dist/default/MyListingsPane.js +45 -21
- package/dist/default/MyListingsPane.js.map +1 -1
- package/dist/default/ShareAction.d.ts +32 -0
- package/dist/default/ShareAction.d.ts.map +1 -0
- package/dist/default/ShareAction.js +116 -0
- package/dist/default/ShareAction.js.map +1 -0
- package/dist/default/actionRow.d.ts +58 -0
- package/dist/default/actionRow.d.ts.map +1 -0
- package/dist/default/actionRow.js +128 -0
- package/dist/default/actionRow.js.map +1 -0
- package/dist/default/cardGallery.d.ts +2 -0
- package/dist/default/cardGallery.d.ts.map +1 -1
- package/dist/default/cardGallery.js +31 -0
- package/dist/default/cardGallery.js.map +1 -1
- package/dist/default/favorite.d.ts +25 -0
- package/dist/default/favorite.d.ts.map +1 -1
- package/dist/default/favorite.js +29 -3
- package/dist/default/favorite.js.map +1 -1
- package/dist/default/icons.d.ts +19 -0
- package/dist/default/icons.d.ts.map +1 -1
- package/dist/default/icons.js +23 -0
- package/dist/default/icons.js.map +1 -1
- package/dist/default/index.d.ts +8 -1
- package/dist/default/index.d.ts.map +1 -1
- package/dist/default/index.js +6 -1
- package/dist/default/index.js.map +1 -1
- package/dist/default/notice.d.ts +14 -0
- package/dist/default/notice.d.ts.map +1 -0
- package/dist/default/notice.js +62 -0
- package/dist/default/notice.js.map +1 -0
- package/dist/headless/MyListings.d.ts +39 -18
- package/dist/headless/MyListings.d.ts.map +1 -1
- package/dist/headless/MyListings.js +41 -19
- package/dist/headless/MyListings.js.map +1 -1
- package/dist/headless/Share.d.ts +103 -0
- package/dist/headless/Share.d.ts.map +1 -0
- package/dist/headless/Share.js +190 -0
- package/dist/headless/Share.js.map +1 -0
- package/dist/i18n/es.d.ts.map +1 -1
- package/dist/i18n/es.js +21 -0
- package/dist/i18n/es.js.map +1 -1
- package/dist/i18n/generated/errors.es.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.es.gen.js +1 -1
- package/dist/i18n/generated/errors.gen.d.ts +18 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -1
- package/dist/i18n/generated/errors.gen.js +9 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -1
- package/dist/i18n/generated/errors.ru.gen.d.ts +1 -1
- package/dist/i18n/generated/errors.ru.gen.js +1 -1
- package/dist/i18n/keys.d.ts +45 -0
- package/dist/i18n/keys.d.ts.map +1 -1
- package/dist/i18n/keys.js +60 -0
- package/dist/i18n/keys.js.map +1 -1
- package/dist/i18n/ru.d.ts.map +1 -1
- package/dist/i18n/ru.js +21 -0
- package/dist/i18n/ru.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/model/cardBadges.d.ts +52 -3
- package/dist/model/cardBadges.d.ts.map +1 -1
- package/dist/model/cardBadges.js +74 -12
- package/dist/model/cardBadges.js.map +1 -1
- package/dist/model/mineSource.d.ts +9 -3
- package/dist/model/mineSource.d.ts.map +1 -1
- package/dist/model/mineSource.js.map +1 -1
- package/dist/model/status.d.ts +62 -13
- package/dist/model/status.d.ts.map +1 -1
- package/dist/model/status.js +69 -15
- package/dist/model/status.js.map +1 -1
- package/llms.txt +8 -3
- package/manifest.json +96 -4
- package/nav-manifest.json +1 -1
- package/package.json +10 -10
- package/src/analytics/generated/events.json +1 -1
- package/src/api/generated/schema.ts +2 -2
- package/src/default/CardBadges.tsx +3 -1
- package/src/default/ListingActions.tsx +196 -0
- package/src/default/ListingCard.tsx +79 -43
- package/src/default/ListingDetailPane.tsx +268 -81
- package/src/default/ListingFeedCard.tsx +2 -0
- package/src/default/ListingPhoto.tsx +28 -0
- package/src/default/ListingSerpCard.tsx +39 -12
- package/src/default/MyListingsPane.tsx +77 -44
- package/src/default/ShareAction.tsx +260 -0
- package/src/default/actionRow.ts +131 -0
- package/src/default/cardGallery.ts +31 -0
- package/src/default/favorite.tsx +68 -1
- package/src/default/icons.tsx +60 -0
- package/src/default/index.ts +23 -0
- package/src/default/notice.ts +68 -0
- package/src/headless/MyListings.tsx +89 -38
- package/src/headless/Share.tsx +278 -0
- package/src/i18n/es.ts +28 -0
- package/src/i18n/generated/errors.es.gen.ts +1 -1
- package/src/i18n/generated/errors.gen.ts +9 -0
- package/src/i18n/generated/errors.json +31 -0
- package/src/i18n/generated/errors.ru.gen.ts +1 -1
- package/src/i18n/keys.ts +63 -0
- package/src/i18n/ru.ts +28 -0
- package/src/index.ts +22 -0
- package/src/model/cardBadges.ts +124 -12
- package/src/model/mineSource.ts +9 -3
- package/src/model/status.ts +85 -18
package/src/index.ts
CHANGED
|
@@ -105,9 +105,12 @@ export type {
|
|
|
105
105
|
|
|
106
106
|
// ── model: the two axes ──────────────────────────────────────────────────────
|
|
107
107
|
export {
|
|
108
|
+
MY_LISTINGS_COUNTED_TABS,
|
|
109
|
+
MY_LISTINGS_REMOVED_TAB,
|
|
108
110
|
MY_LISTINGS_TABS,
|
|
109
111
|
MY_LISTINGS_TAB_STATUSES,
|
|
110
112
|
MY_LISTINGS_UNTABBED_STATUSES,
|
|
113
|
+
countedTabOf,
|
|
111
114
|
isPubliclyVisible,
|
|
112
115
|
lifecycleCaption,
|
|
113
116
|
listingStatusView,
|
|
@@ -119,6 +122,7 @@ export type {
|
|
|
119
122
|
ListingStatusTone,
|
|
120
123
|
ListingStatusView,
|
|
121
124
|
ModerationNotice,
|
|
125
|
+
MyListingsCountedTab,
|
|
122
126
|
MyListingsTab,
|
|
123
127
|
} from "./model/status.js";
|
|
124
128
|
export {
|
|
@@ -188,6 +192,7 @@ export type {
|
|
|
188
192
|
CardBadgeContract,
|
|
189
193
|
CardBadgePresentation,
|
|
190
194
|
CardBadgeRow,
|
|
195
|
+
CardBadgeStyle,
|
|
191
196
|
} from "./model/cardBadges.js";
|
|
192
197
|
|
|
193
198
|
// ── model: the engagement axis (already-seen / view count / the overlay) ─────
|
|
@@ -310,6 +315,23 @@ export type {
|
|
|
310
315
|
FavoritesBag,
|
|
311
316
|
UseFavoritesOptions,
|
|
312
317
|
} from "./headless/Favorites.js";
|
|
318
|
+
export {
|
|
319
|
+
SHARE_COPIED_MS,
|
|
320
|
+
SHARE_NETWORKS,
|
|
321
|
+
Share,
|
|
322
|
+
hasNativeShare,
|
|
323
|
+
resolveShareUrl,
|
|
324
|
+
shareLinks,
|
|
325
|
+
useShare,
|
|
326
|
+
} from "./headless/Share.js";
|
|
327
|
+
export type {
|
|
328
|
+
ShareBag,
|
|
329
|
+
ShareChannel,
|
|
330
|
+
ShareLink,
|
|
331
|
+
ShareNetwork,
|
|
332
|
+
ShareTarget,
|
|
333
|
+
UseShareOptions,
|
|
334
|
+
} from "./headless/Share.js";
|
|
313
335
|
export { useListingActions } from "./headless/ListingActions.js";
|
|
314
336
|
export type {
|
|
315
337
|
ListingActionsBag,
|
package/src/model/cardBadges.ts
CHANGED
|
@@ -56,6 +56,44 @@
|
|
|
56
56
|
* contract, and it is rendered exactly as it is rendered today — through
|
|
57
57
|
* `@stapel/attributes-react`'s `<FeatureBadges>`, off the stored DAO's own
|
|
58
58
|
* config. Nothing about this module is required for a card to draw.
|
|
59
|
+
*
|
|
60
|
+
* ── A CHIP IS NOT A LINE (D421) ───────────────────────────────────────────
|
|
61
|
+
*
|
|
62
|
+
* The four presentations produce a piece of text; where that text is PUT
|
|
63
|
+
* decides how it is read, and the two places this pair puts it are not the
|
|
64
|
+
* same place:
|
|
65
|
+
*
|
|
66
|
+
* a badge strip each element is a `<Tag>`, and the chip's own border says
|
|
67
|
+
* where one fact ends and the next begins;
|
|
68
|
+
* a spec line the elements are joined with " · ", so the only thing
|
|
69
|
+
* separating two facts is punctuation, and a SPACE inside
|
|
70
|
+
* one of them is not punctuation.
|
|
71
|
+
*
|
|
72
|
+
* Measured on a live feed, translated: "HONOR · Model 90 · 256 GB" — three
|
|
73
|
+
* facts, and the middle one reads as a value that begins with the word
|
|
74
|
+
* "Model", because in a run of values a caption joined by a space is
|
|
75
|
+
* indistinguishable from a two-word answer. So {@link CardBadgeStyle}
|
|
76
|
+
* `"line"` writes the same pair with a COLON, which is the punctuation that
|
|
77
|
+
* says "what follows is the answer to this". The chip keeps its space: "Floor 3" inside a border needs
|
|
78
|
+
* no help, and a colon there is the form-field look the 0.22 contract
|
|
79
|
+
* deliberately refused.
|
|
80
|
+
*
|
|
81
|
+
* ── …AND TWO AXES MUST NOT WEAR ONE CAPTION ───────────────────────────────
|
|
82
|
+
*
|
|
83
|
+
* The same feed: "5 fl. · 9 fl. · 54 m²" — the floor a flat is on and the
|
|
84
|
+
* number of floors in the building, printed as one number and its unit each,
|
|
85
|
+
* twice, with nothing saying which is which. The server presented both as
|
|
86
|
+
* `value_unit` and it was right about each of them ALONE; what it cannot see
|
|
87
|
+
* is that they are on one line together.
|
|
88
|
+
*
|
|
89
|
+
* That collision is a property of the SET, so it is resolved once, here, in
|
|
90
|
+
* {@link cardBadgeTexts}: elements printed without a caption that share a unit
|
|
91
|
+
* (or that print identical text) get their catalogue names back — the
|
|
92
|
+
* `name_value` shape the contract already defines, so a disambiguated element
|
|
93
|
+
* is spelled exactly as a server-captioned one. The rule refuses to act where
|
|
94
|
+
* it would not help: a group whose names are missing or not distinct is left
|
|
95
|
+
* alone rather than captioned with the same word twice, because a caption
|
|
96
|
+
* that does not tell two things apart is noise the reader still has to read.
|
|
59
97
|
*/
|
|
60
98
|
import type { ListingCardBadgeElement, ListingFeatureDao } from "../api/types.js";
|
|
61
99
|
|
|
@@ -180,6 +218,23 @@ function isTrue(raw: unknown): boolean {
|
|
|
180
218
|
return raw === true || raw === 1 || raw === "true";
|
|
181
219
|
}
|
|
182
220
|
|
|
221
|
+
/**
|
|
222
|
+
* WHERE the text is going to be put — see the module header.
|
|
223
|
+
*
|
|
224
|
+
* `"badge"` is a chip in a strip and is the default, so every existing call
|
|
225
|
+
* site keeps the bytes it had. `"line"` is one item in a " · "-separated run,
|
|
226
|
+
* where a caption needs punctuation to be read as one.
|
|
227
|
+
*/
|
|
228
|
+
export type CardBadgeStyle = "badge" | "line";
|
|
229
|
+
|
|
230
|
+
/** A caption and its answer, joined the way this surface separates them. */
|
|
231
|
+
function caption(name: string, body: string, style: CardBadgeStyle): string {
|
|
232
|
+
// A SPACE in a chip and a COLON in a line. "Floor 3" is a caption inside a
|
|
233
|
+
// border; "Floor: 3" is what the same pair has to become when the border is
|
|
234
|
+
// gone and the neighbours are a dot away.
|
|
235
|
+
return style === "line" ? `${name}: ${body}` : `${name} ${body}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
183
238
|
/**
|
|
184
239
|
* One badge element → the text a card prints, or `undefined` for an element
|
|
185
240
|
* that has nothing to say (a false boolean, a blank value).
|
|
@@ -189,7 +244,11 @@ function isTrue(raw: unknown): boolean {
|
|
|
189
244
|
* exists: three cards each re-deciding what a badge says is three places for
|
|
190
245
|
* "Brick · 3 · 9" to come back.
|
|
191
246
|
*/
|
|
192
|
-
export function cardBadgeText(
|
|
247
|
+
export function cardBadgeText(
|
|
248
|
+
row: CardBadgeRow,
|
|
249
|
+
locale?: string,
|
|
250
|
+
style: CardBadgeStyle = "badge"
|
|
251
|
+
): string | undefined {
|
|
193
252
|
const presentation = badgePresentation(row);
|
|
194
253
|
const name = text(row.name);
|
|
195
254
|
const unit = text(row.unit);
|
|
@@ -208,12 +267,11 @@ export function cardBadgeText(row: CardBadgeRow, locale?: string): string | unde
|
|
|
208
267
|
case "value_unit":
|
|
209
268
|
return withUnit;
|
|
210
269
|
case "name_value":
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
return name.length > 0 ? `${name} ${withUnit}` : withUnit;
|
|
270
|
+
// Joined by the SURFACE's own punctuation (D421): a space inside a
|
|
271
|
+
// chip, a colon inside a line. The unit rides along either way — a
|
|
272
|
+
// named row that dropped it reads "Mileage 20 000", which is the defect
|
|
273
|
+
// this contract exists to close.
|
|
274
|
+
return name.length > 0 ? caption(name, withUnit, style) : withUnit;
|
|
217
275
|
default:
|
|
218
276
|
// `value`, and an element the server left plain in a list that speaks
|
|
219
277
|
// the contract. The value alone, never the unit — the server said so.
|
|
@@ -221,17 +279,71 @@ export function cardBadgeText(row: CardBadgeRow, locale?: string): string | unde
|
|
|
221
279
|
}
|
|
222
280
|
}
|
|
223
281
|
|
|
224
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Does this element already print its own caption?
|
|
284
|
+
*
|
|
285
|
+
* `name` IS the caption, and a `name_value` with a name to print carries one.
|
|
286
|
+
* Everything else is a bare answer, and two bare answers are what can collide.
|
|
287
|
+
*/
|
|
288
|
+
function alreadyCaptioned(row: CardBadgeRow): boolean {
|
|
289
|
+
const presentation = badgePresentation(row);
|
|
290
|
+
if (presentation === "name") return true;
|
|
291
|
+
return presentation === "name_value" && text(row.name).length > 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* How a bare answer could be MISTAKEN for its neighbour: by measuring the
|
|
296
|
+
* same thing (one unit, two axes) or by reading identically.
|
|
297
|
+
*/
|
|
298
|
+
function ambiguityKey(row: CardBadgeRow, printed: string): string {
|
|
299
|
+
const unit = text(row.unit);
|
|
300
|
+
return unit.length > 0 ? `unit:${unit}` : `text:${printed}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Every element that has something to say, in the server's order, with two
|
|
305
|
+
* axes that would read alike told apart — see the module header (D421).
|
|
306
|
+
*/
|
|
225
307
|
export function cardBadgeTexts(
|
|
226
308
|
rows: readonly CardBadgeRow[],
|
|
227
|
-
locale?: string
|
|
309
|
+
locale?: string,
|
|
310
|
+
style: CardBadgeStyle = "badge"
|
|
228
311
|
): readonly { readonly slug: string; readonly text: string }[] {
|
|
229
312
|
const out: { slug: string; text: string }[] = [];
|
|
313
|
+
const printedRows: CardBadgeRow[] = [];
|
|
314
|
+
/** Ambiguity key → the positions in `out` that could be confused. */
|
|
315
|
+
const groups = new Map<string, number[]>();
|
|
316
|
+
|
|
230
317
|
for (const row of rows) {
|
|
231
|
-
const printed = cardBadgeText(row, locale);
|
|
232
|
-
if (printed
|
|
233
|
-
|
|
318
|
+
const printed = cardBadgeText(row, locale, style);
|
|
319
|
+
if (printed === undefined || typeof row.slug !== "string" || row.slug.length === 0) {
|
|
320
|
+
continue;
|
|
234
321
|
}
|
|
322
|
+
const at = out.length;
|
|
323
|
+
out.push({ slug: row.slug, text: printed });
|
|
324
|
+
printedRows.push(row);
|
|
325
|
+
if (alreadyCaptioned(row)) continue;
|
|
326
|
+
const key = ambiguityKey(row, printed);
|
|
327
|
+
const seen = groups.get(key);
|
|
328
|
+
if (seen === undefined) groups.set(key, [at]);
|
|
329
|
+
else seen.push(at);
|
|
235
330
|
}
|
|
331
|
+
|
|
332
|
+
for (const positions of groups.values()) {
|
|
333
|
+
if (positions.length < 2) continue;
|
|
334
|
+
const names = positions.map((at) => text(printedRows[at]?.name));
|
|
335
|
+
// Nothing to caption with, or one word for both axes: leave the line as
|
|
336
|
+
// the server wrote it rather than adding a caption that tells a reader
|
|
337
|
+
// nothing they did not already have.
|
|
338
|
+
if (names.some((name) => name.length === 0)) continue;
|
|
339
|
+
if (new Set(names).size !== names.length) continue;
|
|
340
|
+
positions.forEach((at, index) => {
|
|
341
|
+
const entry = out[at];
|
|
342
|
+
const name = names[index];
|
|
343
|
+
if (entry === undefined || name === undefined) return;
|
|
344
|
+
out[at] = { slug: entry.slug, text: caption(name, entry.text, style) };
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
236
348
|
return out;
|
|
237
349
|
}
|
package/src/model/mineSource.ts
CHANGED
|
@@ -32,12 +32,18 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { ListingsApi } from "../api/listingsApi.js";
|
|
34
34
|
import type { MyListingsParams, PaginatedMyListingCards } from "../api/types.js";
|
|
35
|
-
import type {
|
|
35
|
+
import type { MyListingsCountedTab } from "./status.js";
|
|
36
36
|
import { MY_LISTINGS_TAB_STATUSES } from "./status.js";
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* One page of the caller's own listings, however the host gets them.
|
|
40
|
+
*
|
|
41
|
+
* `tab` is one of the three COUNTED tabs and not the fourth: the removed tab
|
|
42
|
+
* (D407) reads takedowns off `?status=blocked` directly and unpaged, so a host
|
|
43
|
+
* source is never handed a tab it was not written for.
|
|
44
|
+
*/
|
|
39
45
|
export type MyListingsSource = (args: {
|
|
40
|
-
readonly tab:
|
|
46
|
+
readonly tab: MyListingsCountedTab;
|
|
41
47
|
readonly page: MyListingsParams;
|
|
42
48
|
readonly signal?: AbortSignal;
|
|
43
49
|
}) => Promise<PaginatedMyListingCards>;
|
package/src/model/status.ts
CHANGED
|
@@ -285,7 +285,8 @@ export function listingStatusView(
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
/**
|
|
288
|
-
* The
|
|
288
|
+
* The three tabs `my/counters` COUNTS, and the statuses each one folds
|
|
289
|
+
* together.
|
|
289
290
|
*
|
|
290
291
|
* The grouping is the SERVER's (`views.my_counters`), copied here so a tab's
|
|
291
292
|
* caption and its count cannot describe different sets: `active` includes
|
|
@@ -294,40 +295,106 @@ export function listingStatusView(
|
|
|
294
295
|
* working on. A client that grouped them its own way would show "3 active"
|
|
295
296
|
* over two rows.
|
|
296
297
|
*/
|
|
297
|
-
export const
|
|
298
|
+
export const MY_LISTINGS_COUNTED_TABS = ["active", "drafts", "archived"] as const;
|
|
298
299
|
|
|
299
|
-
|
|
300
|
+
/** One of the three tabs `MyCountersResponse` carries a number for. */
|
|
301
|
+
export type MyListingsCountedTab = (typeof MY_LISTINGS_COUNTED_TABS)[number];
|
|
300
302
|
|
|
301
|
-
|
|
302
|
-
Record<
|
|
303
|
+
const COUNTED_TAB_STATUSES: Readonly<
|
|
304
|
+
Record<MyListingsCountedTab, readonly ListingLifecycleStatus[]>
|
|
303
305
|
> = {
|
|
304
306
|
active: ["published", "pending"],
|
|
305
307
|
drafts: ["draft", "rejected"],
|
|
306
308
|
archived: ["archived", "paused", "expired", "sold"],
|
|
307
309
|
};
|
|
308
310
|
|
|
309
|
-
/** Which
|
|
310
|
-
*
|
|
311
|
-
|
|
312
|
-
export function tabOf(
|
|
311
|
+
/** Which of the three COUNTED tabs a status belongs to, or `undefined` for
|
|
312
|
+
* one the server's own counter groups nowhere — `blocked`, today. */
|
|
313
|
+
export function countedTabOf(
|
|
313
314
|
status: ListingLifecycleStatus
|
|
314
|
-
):
|
|
315
|
-
for (const tab of
|
|
316
|
-
if (
|
|
315
|
+
): MyListingsCountedTab | undefined {
|
|
316
|
+
for (const tab of MY_LISTINGS_COUNTED_TABS) {
|
|
317
|
+
if (COUNTED_TAB_STATUSES[tab].includes(status)) return tab;
|
|
317
318
|
}
|
|
318
319
|
return undefined;
|
|
319
320
|
}
|
|
320
321
|
|
|
321
322
|
/**
|
|
322
|
-
* The statuses
|
|
323
|
+
* The statuses the server's own counter groups nowhere — `blocked`, and
|
|
324
|
+
* nothing else today.
|
|
323
325
|
*
|
|
324
326
|
* DERIVED, not written down a second time: a status added upstream and left
|
|
325
327
|
* out of the counter groupings lands here automatically and gets shown, which
|
|
326
328
|
* is the opposite of what a hardcoded `["blocked"]` would do the day it goes
|
|
327
|
-
* stale.
|
|
328
|
-
* `default/MyListingsPane.tsx`) rather than folding them into one, because a
|
|
329
|
-
* tab's rows and its `my/counters` badge have to describe the same set — and
|
|
330
|
-
* `my/counters` counts a takedown in no tab at all.
|
|
329
|
+
* stale.
|
|
331
330
|
*/
|
|
332
331
|
export const MY_LISTINGS_UNTABBED_STATUSES: readonly ListingLifecycleStatus[] =
|
|
333
|
-
LISTING_STATUSES.filter((status) =>
|
|
332
|
+
LISTING_STATUSES.filter((status) => countedTabOf(status) === undefined);
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The fourth tab: the rows `my/counters` counts in NO tab at all.
|
|
336
|
+
*
|
|
337
|
+
* ── What was on screen (D407) ────────────────────────────────────────────
|
|
338
|
+
*
|
|
339
|
+
* A listing pulled by moderation showed "Taken down by a moderator" over a
|
|
340
|
+
* counter row reading "Active 0 · Drafts 0 · Archived 0", beside the active
|
|
341
|
+
* tab's own "nothing of yours is live". The object was on the page and in no
|
|
342
|
+
* tab and in no number — three statements, and the two loudest of them said
|
|
343
|
+
* the seller had nothing.
|
|
344
|
+
*
|
|
345
|
+
* The takedowns had a home before this (a block above the tabs) and that was
|
|
346
|
+
* the half that was wrong: a row outside the tab strip is a row the counters
|
|
347
|
+
* do not describe, and a person reads the counters. So they get a TAB, with a
|
|
348
|
+
* count, like every other state a listing can be in.
|
|
349
|
+
*
|
|
350
|
+
* ── Why a fourth tab and not the archive ─────────────────────────────────
|
|
351
|
+
*
|
|
352
|
+
* Folding `blocked` into `archived` is the other shape this could take, and
|
|
353
|
+
* it costs the count: `my/counters` has three integers and no fourth, so an
|
|
354
|
+
* archive tab holding takedowns would read the server's `archived` number —
|
|
355
|
+
* `0` — until the tab was opened and its rows could raise it. The fourth tab
|
|
356
|
+
* is counted from its OWN read (`?status=blocked`, unpaged), so the number is
|
|
357
|
+
* right while the seller is looking at a different tab, which is exactly the
|
|
358
|
+
* moment D407 was measured at.
|
|
359
|
+
*
|
|
360
|
+
* The archive keeps its meaning too, which is not nothing: "I put this away"
|
|
361
|
+
* and "a moderator took this down" are not the same sentence and a tab that
|
|
362
|
+
* said one over rows that meant the other would be the D407 defect wearing a
|
|
363
|
+
* label.
|
|
364
|
+
*/
|
|
365
|
+
export const MY_LISTINGS_REMOVED_TAB = "removed";
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Every tab the dashboard can show: the server's three, then the removed one.
|
|
369
|
+
*
|
|
370
|
+
* The removed tab is DRAWN only where there is something in it (see
|
|
371
|
+
* `headless/MyListings.tsx`) — an empty "Taken down" tab is a scare — but it
|
|
372
|
+
* is in this list unconditionally, because `?tab=removed` must parse and a
|
|
373
|
+
* host must be able to name it.
|
|
374
|
+
*/
|
|
375
|
+
export const MY_LISTINGS_TABS: readonly [
|
|
376
|
+
...typeof MY_LISTINGS_COUNTED_TABS,
|
|
377
|
+
typeof MY_LISTINGS_REMOVED_TAB,
|
|
378
|
+
] = [...MY_LISTINGS_COUNTED_TABS, MY_LISTINGS_REMOVED_TAB];
|
|
379
|
+
|
|
380
|
+
export type MyListingsTab = (typeof MY_LISTINGS_TABS)[number];
|
|
381
|
+
|
|
382
|
+
export const MY_LISTINGS_TAB_STATUSES: Readonly<
|
|
383
|
+
Record<MyListingsTab, readonly ListingLifecycleStatus[]>
|
|
384
|
+
> = {
|
|
385
|
+
...COUNTED_TAB_STATUSES,
|
|
386
|
+
[MY_LISTINGS_REMOVED_TAB]: MY_LISTINGS_UNTABBED_STATUSES,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/** Which tab a listing belongs to. Every status has one: the three counted
|
|
390
|
+
* groupings, and the removed tab for whatever they leave out. `undefined` is
|
|
391
|
+
* unreachable today and stays in the signature so a status this build has
|
|
392
|
+
* never heard of cannot be filed under a tab by accident. */
|
|
393
|
+
export function tabOf(
|
|
394
|
+
status: ListingLifecycleStatus
|
|
395
|
+
): MyListingsTab | undefined {
|
|
396
|
+
for (const tab of MY_LISTINGS_TABS) {
|
|
397
|
+
if (MY_LISTINGS_TAB_STATUSES[tab].includes(status)) return tab;
|
|
398
|
+
}
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|