@mohasinac/appkit 3.5.2 → 3.5.4
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/dist/features/homepage/components/CustomCardsSection.js +1 -1
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.d.ts +1 -1
- package/dist/features/seller/components/CategoryInlineSelect.js +1 -1
- package/dist/react/hooks/useBulkSelection.js +12 -3
- package/dist/react/hooks/useGesture.js +48 -4
- package/dist/react/hooks/useLongPress.d.ts +1 -0
- package/dist/react/hooks/useLongPress.js +5 -0
- package/dist/react/hooks/usePullToRefresh.js +12 -1
- package/dist/react/hooks/useRealtimeEvent.js +18 -1
- package/dist/react/hooks/useSwipe.js +14 -0
- package/dist/seed/addresses-seed-data.js +8 -99
- package/dist/seed/categories-seed-data.js +103 -2082
- package/dist/seed/homepage-sections-seed-data.js +9 -9
- package/dist/seed/products-art-seed-data.d.ts +1 -10
- package/dist/seed/products-art-seed-data.js +13 -122
- package/dist/seed/products-auctions-seed-data.js +39 -747
- package/dist/seed/products-classifieds-seed-data.d.ts +1 -9
- package/dist/seed/products-classifieds-seed-data.js +13 -195
- package/dist/seed/products-digital-codes-seed-data.d.ts +1 -9
- package/dist/seed/products-digital-codes-seed-data.js +13 -202
- package/dist/seed/products-live-items-seed-data.d.ts +1 -12
- package/dist/seed/products-live-items-seed-data.js +13 -188
- package/dist/seed/products-preorders-seed-data.js +18 -149
- package/dist/seed/products-prize-draws-seed-data.d.ts +1 -16
- package/dist/seed/products-prize-draws-seed-data.js +13 -385
- package/dist/seed/products-standard-seed-data.js +152 -1331
- package/dist/seed/products-stickers-seed-data.d.ts +1 -10
- package/dist/seed/products-stickers-seed-data.js +13 -122
- package/dist/seed/site-settings-seed-data.js +106 -108
- package/dist/seed/store-addresses-seed-data.js +14 -46
- package/dist/seed/stores-seed-data.js +13 -205
- package/dist/styles.css +19 -18
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -5
- package/dist/ui/components/BulkActionBar.style.css +4 -4
- package/dist/ui/components/Button.js +52 -22
- package/dist/ui/components/Button.style.css +14 -13
- package/dist/ui/components/DateInput.js +1 -0
- package/dist/ui/components/DynamicBgDiv.js +8 -6
- package/dist/ui/components/FormField.js +1 -1
- package/dist/ui/components/HorizontalScroller.js +35 -16
- package/dist/ui/components/Iframe.d.ts +8 -1
- package/dist/ui/components/Layout.d.ts +4 -4
- package/dist/ui/components/Layout.js +8 -8
- package/dist/ui/components/ListingToolbar.js +4 -4
- package/dist/ui/components/Motion.js +4 -1
- package/dist/ui/components/OtpInput.js +1 -1
- package/dist/ui/components/PaginatedSelect.js +29 -3
- package/dist/ui/components/Pagination.js +10 -5
- package/dist/ui/components/RichTextEditor.js +64 -3
- package/dist/ui/components/Select.js +1 -0
- package/dist/ui/components/Semantic.d.ts +10 -10
- package/dist/ui/components/Semantic.js +25 -25
- package/dist/ui/components/SideDrawer.js +13 -11
- package/dist/ui/components/SideModal.js +9 -7
- package/dist/ui/components/SlottedListingView.d.ts +13 -2
- package/dist/ui/components/StickyToolbar.js +9 -5
- package/dist/ui/components/TagInput.js +15 -2
- package/dist/ui/components/Textarea.js +1 -0
- package/dist/ui/components/Toggle.js +8 -1
- package/dist/ui/components/UnsavedChangesModal.js +12 -1
- package/dist/ui/forms/ColorPickerField.js +7 -0
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/utils/id-generators.d.ts +7 -4
- package/dist/utils/id-generators.js +53 -31
- package/dist/utils/number.formatter.js +19 -6
- package/dist/utils/string.formatter.js +4 -1
- package/package.json +1 -1
|
@@ -74,7 +74,8 @@ export function generateOrderId(input) {
|
|
|
74
74
|
export function generateFAQId(input) {
|
|
75
75
|
if (input.customId?.trim())
|
|
76
76
|
return input.customId.trim();
|
|
77
|
-
|
|
77
|
+
const questionSlug = slugify(input.question).substring(0, 50).replace(/-+$/, "");
|
|
78
|
+
return `faq-${slugify(input.category)}-${questionSlug}`;
|
|
78
79
|
}
|
|
79
80
|
// ─── Coupon ───────────────────────────────────────────────────────────────────
|
|
80
81
|
export function generateCouponId(code, customId) {
|
|
@@ -427,53 +428,74 @@ export function generateMediaFilename(ctx) {
|
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
/**
|
|
430
|
-
* W1-51 —
|
|
431
|
-
*
|
|
431
|
+
* W1-51 (regex fixed 2026-08-15 — see bug notes below) — validate that a
|
|
432
|
+
* filename matches the canonical pattern produced by
|
|
433
|
+
* {@link generateMediaFilename}. Called from /api/media/sign before a
|
|
432
434
|
* signed-URL is issued so malformed names can never enter storage.
|
|
433
435
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
+
* The generator functions above put the type word (image/video/logo/
|
|
437
|
+
* banner/avatar) at the END of the slug portion, e.g.
|
|
438
|
+
* `product-{name}-{category}-{store}-image-{n}.ext`, NOT as a literal
|
|
439
|
+
* compound prefix like `product-image-{...}`. The previous version of this
|
|
440
|
+
* validator anchored on the compound-prefix shape and rejected real output
|
|
441
|
+
* from every generator except invoice/payout-doc/shipping-proof/
|
|
442
|
+
* refund-proof/catalogue-image — causing /api/media/sign to 500 for
|
|
443
|
+
* product/review/auction/preorder/store/brand/blog/event/category/user/
|
|
444
|
+
* carousel/rich-text uploads. Each pattern below mirrors one generator
|
|
445
|
+
* function's actual output shape. blog-cover/blog-content-image/
|
|
446
|
+
* blog-additional-image all dispatch to the same generator as blog-image
|
|
447
|
+
* (and the four event-* variants likewise share one shape), so one pattern
|
|
448
|
+
* validates all sub-types of that family.
|
|
436
449
|
*
|
|
437
450
|
* Returns true when the filename is acceptable.
|
|
438
451
|
*/
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"
|
|
444
|
-
"
|
|
445
|
-
"
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
"
|
|
450
|
-
"
|
|
451
|
-
"
|
|
452
|
-
"
|
|
453
|
-
"
|
|
452
|
+
const SLUG_PART = "[a-z0-9]+(?:-[a-z0-9]+)*";
|
|
453
|
+
const EXT_PART = "[a-z0-9]+";
|
|
454
|
+
const DATE_PART = "\\d{8}";
|
|
455
|
+
const MEDIA_FILENAME_PATTERNS = [
|
|
456
|
+
{ context: "product-image", pattern: new RegExp(`^product-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
457
|
+
{ context: "product-video", pattern: new RegExp(`^product-${SLUG_PART}-video-\\d+\\.${EXT_PART}$`) },
|
|
458
|
+
{ context: "review-image", pattern: new RegExp(`^review-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
459
|
+
{ context: "review-video", pattern: new RegExp(`^review-${SLUG_PART}-video-1\\.${EXT_PART}$`) },
|
|
460
|
+
{ context: "auction-image", pattern: new RegExp(`^auction-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
461
|
+
{ context: "preorder-image", pattern: new RegExp(`^preorder-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
462
|
+
{ context: "store-logo", pattern: new RegExp(`^store-${SLUG_PART}-logo\\.${EXT_PART}$`) },
|
|
463
|
+
{ context: "store-banner", pattern: new RegExp(`^store-${SLUG_PART}-banner\\.${EXT_PART}$`) },
|
|
464
|
+
{ context: "brand-logo", pattern: new RegExp(`^brand-${SLUG_PART}-logo\\.${EXT_PART}$`) },
|
|
465
|
+
{ context: "brand-banner", pattern: new RegExp(`^brand-${SLUG_PART}-banner\\.${EXT_PART}$`) },
|
|
466
|
+
{ context: "blog-image", pattern: new RegExp(`^blog-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
467
|
+
{ context: "event-image", pattern: new RegExp(`^event-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
468
|
+
{ context: "rich-text-image", pattern: new RegExp(`^rich-text-${SLUG_PART}-image-\\d+\\.${EXT_PART}$`) },
|
|
469
|
+
{ context: "category-image", pattern: new RegExp(`^category-${SLUG_PART}-image\\.${EXT_PART}$`) },
|
|
470
|
+
{ context: "user-avatar", pattern: new RegExp(`^user-${SLUG_PART}-avatar\\.${EXT_PART}$`) },
|
|
471
|
+
{ context: "carousel-image", pattern: new RegExp(`^carousel-${SLUG_PART}-image\\.${EXT_PART}$`) },
|
|
472
|
+
{ context: "catalogue-image", pattern: new RegExp(`^catalogue-image-${SLUG_PART}-\\d+\\.${EXT_PART}$`) },
|
|
473
|
+
{ context: "invoice", pattern: new RegExp(`^invoice-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
|
|
474
|
+
{ context: "payout-doc", pattern: new RegExp(`^payout-doc-${SLUG_PART}-${DATE_PART}\\.pdf$`) },
|
|
475
|
+
{ context: "shipping-proof", pattern: new RegExp(`^shipping-proof-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
476
|
+
{ context: "refund-proof", pattern: new RegExp(`^refund-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
477
|
+
{ context: "payment-proof", pattern: new RegExp(`^payment-proof-${SLUG_PART}-${SLUG_PART}-${DATE_PART}\\.${EXT_PART}$`) },
|
|
454
478
|
];
|
|
455
|
-
const MEDIA_FILENAME_REGEX = new RegExp(`^(?:${MEDIA_FILENAME_PREFIXES.join("|")})-[a-z0-9]([a-z0-9-]*[a-z0-9])?(?:-\\d+)?\\.[a-z0-9]+$`);
|
|
456
479
|
export function validateMediaFilename(filename) {
|
|
457
480
|
if (!filename || filename.length > 256)
|
|
458
481
|
return false;
|
|
459
482
|
if (filename.includes("..") || filename.includes("/") || filename.includes("\\"))
|
|
460
483
|
return false;
|
|
461
|
-
return
|
|
484
|
+
return MEDIA_FILENAME_PATTERNS.some(({ pattern }) => pattern.test(filename));
|
|
462
485
|
}
|
|
463
486
|
/**
|
|
464
|
-
* Derives the `MediaFilenameContext["type"]` a filename was
|
|
465
|
-
* (e.g. "catalogue-image") by matching against the same
|
|
466
|
-
* {@link validateMediaFilename} uses
|
|
467
|
-
*
|
|
487
|
+
* Derives the `MediaFilenameContext["type"]` family a filename was
|
|
488
|
+
* generated under (e.g. "catalogue-image") by matching against the same
|
|
489
|
+
* shape patterns {@link validateMediaFilename} uses. Sibling context types
|
|
490
|
+
* that share one generator (e.g. blog-cover/blog-content-image/
|
|
491
|
+
* blog-additional-image all produce the same shape as blog-image) resolve
|
|
492
|
+
* to that shared family name — the dispatcher itself can't distinguish them
|
|
493
|
+
* from the filename alone, since it never encoded the sub-type into the name.
|
|
468
494
|
* Used at /api/media/finalize time to stamp `MediaAssetDocument.contextType`
|
|
469
495
|
* without changing the finalize request contract.
|
|
470
496
|
*/
|
|
471
497
|
export function deriveContextTypeFromFilename(filename) {
|
|
472
|
-
|
|
473
|
-
const matches = MEDIA_FILENAME_PREFIXES.filter((prefix) => stem.startsWith(`${prefix}-`) || stem === prefix);
|
|
474
|
-
if (matches.length === 0)
|
|
475
|
-
return undefined;
|
|
476
|
-
return matches.reduce((longest, candidate) => (candidate.length > longest.length ? candidate : longest));
|
|
498
|
+
return MEDIA_FILENAME_PATTERNS.find(({ pattern }) => pattern.test(filename))?.context;
|
|
477
499
|
}
|
|
478
500
|
/**
|
|
479
501
|
* Generates a scannable barcode identifier for a product listing.
|
|
@@ -40,10 +40,20 @@ export function formatFileSize(bytes) {
|
|
|
40
40
|
export function formatCompactNumber(num) {
|
|
41
41
|
if (num < 1000)
|
|
42
42
|
return num.toString();
|
|
43
|
-
if (num < 1000000)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if (num < 1000000) {
|
|
44
|
+
const kValue = (num / 1000).toFixed(1);
|
|
45
|
+
// Rounding can push a value right up to the next unit's boundary
|
|
46
|
+
// (e.g. 999950 -> "1000.0K") — escalate instead of displaying "1000.0K".
|
|
47
|
+
if (kValue === "1000.0")
|
|
48
|
+
return `${(num / 1000000).toFixed(1)}M`;
|
|
49
|
+
return `${kValue}K`;
|
|
50
|
+
}
|
|
51
|
+
if (num < 1000000000) {
|
|
52
|
+
const mValue = (num / 1000000).toFixed(1);
|
|
53
|
+
if (mValue === "1000.0")
|
|
54
|
+
return `${(num / 1000000000).toFixed(1)}B`;
|
|
55
|
+
return `${mValue}M`;
|
|
56
|
+
}
|
|
47
57
|
return `${(num / 1000000000).toFixed(1)}B`;
|
|
48
58
|
}
|
|
49
59
|
export function formatDecimal(num, decimals = 2) {
|
|
@@ -73,7 +83,10 @@ export function parseFormattedNumber(str) {
|
|
|
73
83
|
}
|
|
74
84
|
else if (lastDotIndex > -1 && lastCommaIndex === -1) {
|
|
75
85
|
const afterDot = cleaned.substring(lastDotIndex + 1);
|
|
76
|
-
|
|
86
|
+
// A single separator followed by exactly 3 digits is the standard
|
|
87
|
+
// thousands-grouping width (e.g. "3.500" -> 3500), not a decimal — only
|
|
88
|
+
// 1-2 trailing digits are treated as a genuine decimal fraction.
|
|
89
|
+
const isThousands = dotCount > 1 || afterDot.length >= 3;
|
|
77
90
|
if (isThousands)
|
|
78
91
|
cleaned = cleaned.replace(/\./g, "");
|
|
79
92
|
decimalSeparator = ".";
|
|
@@ -81,7 +94,7 @@ export function parseFormattedNumber(str) {
|
|
|
81
94
|
}
|
|
82
95
|
else if (lastCommaIndex > -1 && lastDotIndex === -1) {
|
|
83
96
|
const afterComma = cleaned.substring(lastCommaIndex + 1);
|
|
84
|
-
const commaIsThousands = commaCount > 1 || afterComma.length
|
|
97
|
+
const commaIsThousands = commaCount > 1 || afterComma.length >= 3;
|
|
85
98
|
if (commaIsThousands) {
|
|
86
99
|
cleaned = cleaned.replace(/,/g, "");
|
|
87
100
|
decimalSeparator = ".";
|
|
@@ -76,7 +76,10 @@ function applyMark(text, mark) {
|
|
|
76
76
|
}
|
|
77
77
|
if (mark.type === "link") {
|
|
78
78
|
const rawHref = String(mark.attrs?.href ?? "#").trim();
|
|
79
|
-
|
|
79
|
+
// `\/(?!\/)` allows a single leading slash (same-site relative path) but
|
|
80
|
+
// rejects `//host/...` protocol-relative URLs, which browsers resolve to
|
|
81
|
+
// an arbitrary external origin despite "looking" like a local path.
|
|
82
|
+
const safe = /^(https?:\/\/|mailto:|\/(?!\/)|#)/i.test(rawHref) ? rawHref : "#";
|
|
80
83
|
const href = safe.replace(/[&"<>]/g, (c) => ({ "&": "&", '"': """, "<": "<", ">": ">" })[c] ?? c);
|
|
81
84
|
return `<a href="${href}" rel="noopener noreferrer">${text}</a>`;
|
|
82
85
|
}
|