@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
|
@@ -64,7 +64,7 @@ export function CustomCardsSection(config) {
|
|
|
64
64
|
}
|
|
65
65
|
// Row layout: horizontal scroller
|
|
66
66
|
if (layout === "row") {
|
|
67
|
-
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, loop: true,
|
|
67
|
+
return (_jsx(Section, { padding: "y-3xl", surface: "muted", children: _jsxs(Div, { className: CLS_CONTAINER, children: [_jsx(SectionHeader, { title: title }), _jsx(HorizontalScroller, { gap: 16, showArrows: true, snapToItems: true, loop: true, items: cards, keyExtractor: (card) => card.id, renderItem: (card) => (_jsx(Div, { className: "w-72 flex-shrink-0", children: _jsx(CardItem, { card: card }) })) })] }) }));
|
|
68
68
|
}
|
|
69
69
|
// Masonry layout: CSS columns
|
|
70
70
|
if (layout === "masonry") {
|
|
@@ -41,5 +41,5 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
|
|
|
41
41
|
headingClass,
|
|
42
42
|
]
|
|
43
43
|
.filter(Boolean)
|
|
44
|
-
.join(" "), children: title }), headingVariant === "editorial" && (_jsxs(Row, { align: "center", justify: "center", gap: "sm", textSize: "xs", color: "faint", className: "mt-1 select-none", "aria-hidden": "true", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { size: "xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `${descVariant} mt-2`, size: "base", children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop:
|
|
44
|
+
.join(" "), children: title }), headingVariant === "editorial" && (_jsxs(Row, { align: "center", justify: "center", gap: "sm", textSize: "xs", color: "faint", className: "mt-1 select-none", "aria-hidden": "true", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { size: "xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `${descVariant} mt-2`, size: "base", children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop: true, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx(Row, { className: "mt-6", justify: "start", children: _jsx(TextLink, { rounded: "lg", paddingX: "xl", paddingY: "sm", href: viewMoreHref, className: `inline-flex items-[center] gap-[0.375rem] border transition-colors ${useLightText ? "border-white/40 text-white hover:bg-[rgba(255,255,255,0.1)]" : "border-[var(--appkit-color-border)] text-[var(--appkit-color-text-muted)] hover:bg-[var(--appkit-color-surface-elevated)]"}`, size: "sm", weight: "medium", children: viewMoreLabel }) }))] })] }));
|
|
45
45
|
}
|
|
@@ -4,7 +4,7 @@ export interface CategoryInlineSelectProps {
|
|
|
4
4
|
onChange: (v: string) => void;
|
|
5
5
|
placeholder?: string;
|
|
6
6
|
disabled?: boolean;
|
|
7
|
-
/** Show the "+ Create new category" option. Defaults to
|
|
7
|
+
/** Show the "+ Create new category" option. Defaults to true — sellers can create categories inline. */
|
|
8
8
|
allowCreate?: boolean;
|
|
9
9
|
}
|
|
10
10
|
export declare function CategoryInlineSelect({ value, onChange, placeholder, disabled, allowCreate, }: CategoryInlineSelectProps): React.JSX.Element;
|
|
@@ -35,7 +35,7 @@ async function loadPublicCategoryOptions(query, _page) {
|
|
|
35
35
|
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
36
|
return { items, hasMore: false, nextPage: 2 };
|
|
37
37
|
}
|
|
38
|
-
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate =
|
|
38
|
+
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = true, }) {
|
|
39
39
|
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
40
|
return (_jsx(PaginatedSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
41
41
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
@@ -19,7 +19,12 @@ export function useBulkSelection({ items, keyExtractor, maxSelection = BULK_MAX_
|
|
|
19
19
|
}, [maxSelection]);
|
|
20
20
|
const toggleAll = useCallback(() => {
|
|
21
21
|
setSelectedSet((prev) => {
|
|
22
|
-
|
|
22
|
+
// Compare actual membership, not just set size — `prev` can hold IDs
|
|
23
|
+
// from a page that's no longer rendered (e.g. after paginating), in
|
|
24
|
+
// which case the sizes coincidentally matching would wrongly report
|
|
25
|
+
// "all selected" and CLEAR the selection instead of selecting the
|
|
26
|
+
// currently-visible items.
|
|
27
|
+
const allSelected = allIds.length > 0 && allIds.every((id) => prev.has(id));
|
|
23
28
|
if (allSelected)
|
|
24
29
|
return new Set();
|
|
25
30
|
return new Set(allIds.slice(0, maxSelection));
|
|
@@ -33,8 +38,12 @@ export function useBulkSelection({ items, keyExtractor, maxSelection = BULK_MAX_
|
|
|
33
38
|
const selectedIds = useMemo(() => Array.from(selectedSet), [selectedSet]);
|
|
34
39
|
const selectedCount = selectedSet.size;
|
|
35
40
|
const isSelecting = selectedCount > 0;
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
// Membership-based, not cardinality-based — `selectedSet` can hold IDs
|
|
42
|
+
// from items that are no longer in `allIds` (e.g. the previous page),
|
|
43
|
+
// so comparing sizes alone can report "all selected" when none of the
|
|
44
|
+
// *currently rendered* rows are actually selected.
|
|
45
|
+
const isAllSelected = allIds.length > 0 && allIds.every((id) => selectedSet.has(id));
|
|
46
|
+
const isIndeterminate = !isAllSelected && allIds.some((id) => selectedSet.has(id));
|
|
38
47
|
return {
|
|
39
48
|
selectedIds,
|
|
40
49
|
selectedIdSet: selectedSet,
|
|
@@ -28,6 +28,12 @@ export function useGesture(ref, options = {}) {
|
|
|
28
28
|
const lastTapRef = useRef(0);
|
|
29
29
|
const initialPinchDistanceRef = useRef(0);
|
|
30
30
|
const initialRotationRef = useRef(0);
|
|
31
|
+
// Real devices lift two fingers in separate touchend events, so by the
|
|
32
|
+
// final touchend `e.changedTouches` only ever has one entry — tracking
|
|
33
|
+
// the last-known two-finger positions here lets us compute the true
|
|
34
|
+
// final pinch distance / rotation angle instead of pairing a touch with
|
|
35
|
+
// itself (which always yields 0).
|
|
36
|
+
const lastTwoTouchesRef = useRef(null);
|
|
31
37
|
// Store callbacks in refs to avoid event listener churn
|
|
32
38
|
const onTapRef = useRef(onTap);
|
|
33
39
|
const onDoubleTapRef = useRef(onDoubleTap);
|
|
@@ -67,6 +73,14 @@ export function useGesture(ref, options = {}) {
|
|
|
67
73
|
const dy = touch1.clientY - touch2.clientY;
|
|
68
74
|
return Math.atan2(dy, dx) * (180 / Math.PI);
|
|
69
75
|
};
|
|
76
|
+
const captureTwoTouches = (t1, t2) => {
|
|
77
|
+
lastTwoTouchesRef.current = {
|
|
78
|
+
x1: t1.clientX,
|
|
79
|
+
y1: t1.clientY,
|
|
80
|
+
x2: t2.clientX,
|
|
81
|
+
y2: t2.clientY,
|
|
82
|
+
};
|
|
83
|
+
};
|
|
70
84
|
const handleTouchStart = (e) => {
|
|
71
85
|
if (preventDefault)
|
|
72
86
|
e.preventDefault();
|
|
@@ -79,6 +93,7 @@ export function useGesture(ref, options = {}) {
|
|
|
79
93
|
if (e.touches.length === 2) {
|
|
80
94
|
initialPinchDistanceRef.current = getDistance(e.touches[0], e.touches[1]);
|
|
81
95
|
initialRotationRef.current = getAngle(e.touches[0], e.touches[1]);
|
|
96
|
+
captureTwoTouches(e.touches[0], e.touches[1]);
|
|
82
97
|
}
|
|
83
98
|
};
|
|
84
99
|
const handleTouchMove = (e) => {
|
|
@@ -91,6 +106,7 @@ export function useGesture(ref, options = {}) {
|
|
|
91
106
|
onRotatingRef.current)) {
|
|
92
107
|
if (preventDefault)
|
|
93
108
|
e.preventDefault();
|
|
109
|
+
captureTwoTouches(e.touches[0], e.touches[1]);
|
|
94
110
|
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
|
95
111
|
const currentAngle = getAngle(e.touches[0], e.touches[1]);
|
|
96
112
|
if (initialPinchDistanceRef.current > 0) {
|
|
@@ -122,16 +138,40 @@ export function useGesture(ref, options = {}) {
|
|
|
122
138
|
}
|
|
123
139
|
}
|
|
124
140
|
if (e.touches.length === 0 && initialPinchDistanceRef.current > 0) {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
// `e.changedTouches` only has the LAST-lifted finger at this point
|
|
142
|
+
// (the other one already ended in a prior touchend event), so pair
|
|
143
|
+
// it against the last-known two-finger positions instead of
|
|
144
|
+
// self-pairing it (which always yields a distance of 0).
|
|
145
|
+
const last = lastTwoTouchesRef.current;
|
|
146
|
+
if (last) {
|
|
147
|
+
const currentDistance = getDistance({ clientX: last.x1, clientY: last.y1 }, { clientX: last.x2, clientY: last.y2 });
|
|
148
|
+
onPinchRef.current?.(currentDistance / initialPinchDistanceRef.current, currentDistance);
|
|
149
|
+
}
|
|
127
150
|
initialPinchDistanceRef.current = 0;
|
|
128
151
|
}
|
|
129
152
|
if (e.touches.length === 0 && initialRotationRef.current !== 0) {
|
|
130
|
-
const
|
|
131
|
-
|
|
153
|
+
const last = lastTwoTouchesRef.current;
|
|
154
|
+
if (last) {
|
|
155
|
+
const currentAngle = getAngle({ clientX: last.x1, clientY: last.y1 }, { clientX: last.x2, clientY: last.y2 });
|
|
156
|
+
onRotateRef.current?.(currentAngle - initialRotationRef.current);
|
|
157
|
+
}
|
|
132
158
|
initialRotationRef.current = 0;
|
|
133
159
|
}
|
|
160
|
+
if (e.touches.length === 0) {
|
|
161
|
+
lastTwoTouchesRef.current = null;
|
|
162
|
+
}
|
|
163
|
+
touchStartRef.current = null;
|
|
164
|
+
};
|
|
165
|
+
const handleTouchCancel = () => {
|
|
166
|
+
// OS/browser gesture takeover interrupted the touch sequence — reset
|
|
167
|
+
// all tracked state without firing any callback, matching the "quick
|
|
168
|
+
// taps don't fire" contract. Without this, stale pinch/rotation refs
|
|
169
|
+
// survive into the next unrelated tap and spuriously fire onPinch /
|
|
170
|
+
// onRotate for it.
|
|
134
171
|
touchStartRef.current = null;
|
|
172
|
+
initialPinchDistanceRef.current = 0;
|
|
173
|
+
initialRotationRef.current = 0;
|
|
174
|
+
lastTwoTouchesRef.current = null;
|
|
135
175
|
};
|
|
136
176
|
const handleMouseDown = (e) => {
|
|
137
177
|
if (preventDefault)
|
|
@@ -168,12 +208,16 @@ export function useGesture(ref, options = {}) {
|
|
|
168
208
|
element.addEventListener("touchend", handleTouchEnd, {
|
|
169
209
|
passive: !preventDefault,
|
|
170
210
|
});
|
|
211
|
+
element.addEventListener("touchcancel", handleTouchCancel, {
|
|
212
|
+
passive: true,
|
|
213
|
+
});
|
|
171
214
|
element.addEventListener("mousedown", handleMouseDown);
|
|
172
215
|
element.addEventListener("mouseup", handleMouseUp);
|
|
173
216
|
return () => {
|
|
174
217
|
element.removeEventListener("touchstart", handleTouchStart);
|
|
175
218
|
element.removeEventListener("touchmove", handleTouchMove);
|
|
176
219
|
element.removeEventListener("touchend", handleTouchEnd);
|
|
220
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
177
221
|
element.removeEventListener("mousedown", handleMouseDown);
|
|
178
222
|
element.removeEventListener("mouseup", handleMouseUp);
|
|
179
223
|
};
|
|
@@ -32,5 +32,10 @@ export function useLongPress(callback, ms = 500) {
|
|
|
32
32
|
onMouseLeave: cancel,
|
|
33
33
|
onTouchStart: start,
|
|
34
34
|
onTouchEnd: cancel,
|
|
35
|
+
// Browsers fire touchcancel (not touchend) when a touch is reinterpreted
|
|
36
|
+
// as a scroll — common for cards in a scrollable list. Without this the
|
|
37
|
+
// pending timer survives and can fire the callback after what was really
|
|
38
|
+
// just a scroll, contradicting the "quick taps do NOT fire" contract.
|
|
39
|
+
onTouchCancel: cancel,
|
|
35
40
|
};
|
|
36
41
|
}
|
|
@@ -65,6 +65,15 @@ export function usePullToRefresh(onRefresh, options = {}) {
|
|
|
65
65
|
await onRefreshRef.current();
|
|
66
66
|
}
|
|
67
67
|
}, []);
|
|
68
|
+
const handleTouchCancel = useCallback(() => {
|
|
69
|
+
// Interrupted pull (OS/browser gesture takeover) — reset without
|
|
70
|
+
// triggering onRefresh. Without this, isPulling/progress stay frozen
|
|
71
|
+
// mid-animation since only touchend resets them.
|
|
72
|
+
setIsPulling(false);
|
|
73
|
+
setProgress(0);
|
|
74
|
+
startYRef.current = null;
|
|
75
|
+
thresholdReachedRef.current = false;
|
|
76
|
+
}, []);
|
|
68
77
|
useEffect(() => {
|
|
69
78
|
const el = containerRef.current;
|
|
70
79
|
if (!el)
|
|
@@ -72,11 +81,13 @@ export function usePullToRefresh(onRefresh, options = {}) {
|
|
|
72
81
|
el.addEventListener("touchstart", handleTouchStart, { passive: true });
|
|
73
82
|
el.addEventListener("touchmove", handleTouchMove, { passive: true });
|
|
74
83
|
el.addEventListener("touchend", handleTouchEnd);
|
|
84
|
+
el.addEventListener("touchcancel", handleTouchCancel, { passive: true });
|
|
75
85
|
return () => {
|
|
76
86
|
el.removeEventListener("touchstart", handleTouchStart);
|
|
77
87
|
el.removeEventListener("touchmove", handleTouchMove);
|
|
78
88
|
el.removeEventListener("touchend", handleTouchEnd);
|
|
89
|
+
el.removeEventListener("touchcancel", handleTouchCancel);
|
|
79
90
|
};
|
|
80
|
-
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
91
|
+
}, [handleTouchStart, handleTouchMove, handleTouchEnd, handleTouchCancel]);
|
|
81
92
|
return { containerRef, isPulling, progress };
|
|
82
93
|
}
|
|
@@ -32,6 +32,13 @@ const DEFAULT_MESSAGES = {
|
|
|
32
32
|
};
|
|
33
33
|
export function useRealtimeEvent(config) {
|
|
34
34
|
const configRef = useRef(config);
|
|
35
|
+
// Re-sync every render — callers commonly pass a fresh config object
|
|
36
|
+
// (new extractData/messages/onLogError) each render, and subscribe()/
|
|
37
|
+
// cleanup() only ever read configRef.current, so without this the very
|
|
38
|
+
// first render's config would be captured forever.
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
configRef.current = config;
|
|
41
|
+
});
|
|
35
42
|
const [status, setStatus] = useState(RealtimeEventStatus.IDLE);
|
|
36
43
|
const [error, setError] = useState(null);
|
|
37
44
|
const [data, setData] = useState(null);
|
|
@@ -71,13 +78,23 @@ export function useRealtimeEvent(config) {
|
|
|
71
78
|
catch (authErr) {
|
|
72
79
|
void normalizeError(authErr);
|
|
73
80
|
onLogError?.(`useRealtimeEvent[${type}]: custom token sign-in failed`, authErr);
|
|
81
|
+
// A newer subscribe() call for a different eventId may have already
|
|
82
|
+
// superseded this one (and possibly already fully subscribed) while
|
|
83
|
+
// this sign-in was still in flight. Do NOT call cleanup() here —
|
|
84
|
+
// cleanup() tears down whatever is *currently* registered in the
|
|
85
|
+
// shared refs, which by now may be the newer call's live
|
|
86
|
+
// subscription, not anything this stale call ever set up itself.
|
|
87
|
+
if (eventIdRef.current !== eventId) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
74
90
|
setError(msg.tokenFailure);
|
|
75
91
|
setStatus(RealtimeEventStatus.FAILED);
|
|
76
92
|
cleanup();
|
|
77
93
|
return;
|
|
78
94
|
}
|
|
79
95
|
if (eventIdRef.current !== eventId) {
|
|
80
|
-
|
|
96
|
+
// Same reasoning as the catch branch above — don't tear down a
|
|
97
|
+
// newer call's already-live subscription via the shared refs.
|
|
81
98
|
return;
|
|
82
99
|
}
|
|
83
100
|
setStatus(RealtimeEventStatus.PENDING);
|
|
@@ -118,6 +118,16 @@ export function useSwipe(ref, options = {}) {
|
|
|
118
118
|
isSwiping.current = false;
|
|
119
119
|
onSwipeEndRef.current?.();
|
|
120
120
|
};
|
|
121
|
+
const handleTouchCancel = () => {
|
|
122
|
+
// OS/browser gesture takeover interrupted the touch — without this,
|
|
123
|
+
// isSwiping/touchStartRef stay stuck and onSwipeEnd (which callers
|
|
124
|
+
// rely on to clear a "dragging" UI flag) never fires.
|
|
125
|
+
if (!isSwiping.current)
|
|
126
|
+
return;
|
|
127
|
+
touchStartRef.current = null;
|
|
128
|
+
isSwiping.current = false;
|
|
129
|
+
onSwipeEndRef.current?.();
|
|
130
|
+
};
|
|
121
131
|
const handleMouseDown = (e) => {
|
|
122
132
|
if (preventDefault)
|
|
123
133
|
e.preventDefault();
|
|
@@ -149,6 +159,9 @@ export function useSwipe(ref, options = {}) {
|
|
|
149
159
|
element.addEventListener("touchend", handleTouchEnd, {
|
|
150
160
|
passive: !preventDefault,
|
|
151
161
|
});
|
|
162
|
+
element.addEventListener("touchcancel", handleTouchCancel, {
|
|
163
|
+
passive: true,
|
|
164
|
+
});
|
|
152
165
|
element.addEventListener("mousedown", handleMouseDown);
|
|
153
166
|
document.addEventListener("mousemove", handleMouseMove);
|
|
154
167
|
document.addEventListener("mouseup", handleMouseUp);
|
|
@@ -156,6 +169,7 @@ export function useSwipe(ref, options = {}) {
|
|
|
156
169
|
element.removeEventListener("touchstart", handleTouchStart);
|
|
157
170
|
element.removeEventListener("touchmove", handleTouchMove);
|
|
158
171
|
element.removeEventListener("touchend", handleTouchEnd);
|
|
172
|
+
element.removeEventListener("touchcancel", handleTouchCancel);
|
|
159
173
|
element.removeEventListener("mousedown", handleMouseDown);
|
|
160
174
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
161
175
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* WHY: Seeds
|
|
3
|
-
*
|
|
2
|
+
* WHY: Seeds a minimal set of buyer delivery addresses for the demo catalog. Store pickup
|
|
3
|
+
* addresses live entirely in store-addresses-seed-data.ts (merged in at the API route
|
|
4
|
+
* level) — this file only carries ownerType:"user" entries now, to avoid seeding two
|
|
5
|
+
* overlapping sources of store address data.
|
|
6
|
+
* WHAT: Exports 3 user addresses. Top-level collection (SB-UNI-A) with ownerType discriminator.
|
|
7
|
+
* PII encrypted via HMAC blind indices (emailIndex, phoneIndex). Composite indexes
|
|
8
|
+
* (ownerType, ownerId, createdAt desc) + (ownerType, ownerId, isDefault).
|
|
4
9
|
*
|
|
5
10
|
* EXPORTS:
|
|
6
|
-
* addressesSeedData — Array of
|
|
11
|
+
* addressesSeedData — Array of 3 user address documents
|
|
7
12
|
*
|
|
8
13
|
* @tag domain:addresses,shipping
|
|
9
14
|
* @tag layer:seed
|
|
@@ -15,9 +20,6 @@
|
|
|
15
20
|
const NOW = new Date();
|
|
16
21
|
const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
|
|
17
22
|
const _rawAddressesSeedData = [
|
|
18
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
// User Addresses — Buyer delivery addresses
|
|
20
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
21
23
|
// Yugi Muto — Home
|
|
22
24
|
{
|
|
23
25
|
id: "addr-yugi-home",
|
|
@@ -36,24 +38,6 @@ const _rawAddressesSeedData = [
|
|
|
36
38
|
createdAt: daysAgo(180),
|
|
37
39
|
updatedAt: daysAgo(1),
|
|
38
40
|
},
|
|
39
|
-
// Yugi Muto — Grandpa's Card Shop
|
|
40
|
-
{
|
|
41
|
-
id: "addr-yugi-shop",
|
|
42
|
-
ownerType: "user",
|
|
43
|
-
ownerId: "user-yugi-muto",
|
|
44
|
-
label: "Grandpa's Card Shop",
|
|
45
|
-
fullName: "Yugi Muto",
|
|
46
|
-
phone: "+91-99999-10002",
|
|
47
|
-
addressLine1: "45 Trading Card Plaza",
|
|
48
|
-
addressLine2: "Domino City Center",
|
|
49
|
-
city: "Domino City",
|
|
50
|
-
state: "Tokyo",
|
|
51
|
-
postalCode: "110-0002",
|
|
52
|
-
country: "Japan",
|
|
53
|
-
isDefault: false,
|
|
54
|
-
createdAt: daysAgo(150),
|
|
55
|
-
updatedAt: daysAgo(30),
|
|
56
|
-
},
|
|
57
41
|
// Seto Kaiba — Kaiba Mansion
|
|
58
42
|
{
|
|
59
43
|
id: "addr-kaiba-mansion",
|
|
@@ -72,24 +56,6 @@ const _rawAddressesSeedData = [
|
|
|
72
56
|
createdAt: daysAgo(200),
|
|
73
57
|
updatedAt: daysAgo(10),
|
|
74
58
|
},
|
|
75
|
-
// Seto Kaiba — Kaiba Land Office
|
|
76
|
-
{
|
|
77
|
-
id: "addr-kaiba-land",
|
|
78
|
-
ownerType: "user",
|
|
79
|
-
ownerId: "user-seto-kaiba",
|
|
80
|
-
label: "Kaiba Land",
|
|
81
|
-
fullName: "Seto Kaiba",
|
|
82
|
-
phone: "+91-99999-20002",
|
|
83
|
-
addressLine1: "500 Kaiba Land Boulevard",
|
|
84
|
-
addressLine2: "Tokyo Tower District",
|
|
85
|
-
city: "Tokyo",
|
|
86
|
-
state: "Tokyo",
|
|
87
|
-
postalCode: "105-0001",
|
|
88
|
-
country: "Japan",
|
|
89
|
-
isDefault: false,
|
|
90
|
-
createdAt: daysAgo(180),
|
|
91
|
-
updatedAt: daysAgo(20),
|
|
92
|
-
},
|
|
93
59
|
// Admin (LetItRip) — HQ
|
|
94
60
|
{
|
|
95
61
|
id: "addr-letitrip-hq",
|
|
@@ -108,62 +74,5 @@ const _rawAddressesSeedData = [
|
|
|
108
74
|
createdAt: daysAgo(365),
|
|
109
75
|
updatedAt: daysAgo(5),
|
|
110
76
|
},
|
|
111
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
112
|
-
// Store Addresses — Pickup / fulfillment locations
|
|
113
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
114
|
-
// Kaiba Corp Card Vault — HQ (fulfillment)
|
|
115
|
-
{
|
|
116
|
-
id: "addr-kaiba-corp-hq",
|
|
117
|
-
ownerType: "store",
|
|
118
|
-
ownerId: "store-kaiba-corp-cards",
|
|
119
|
-
label: "Kaiba Corp HQ",
|
|
120
|
-
fullName: "Kaiba Corp Card Vault",
|
|
121
|
-
phone: "+91-99999-40001",
|
|
122
|
-
addressLine1: "2000 Kaiba Corp Tower",
|
|
123
|
-
addressLine2: "Domino Business District",
|
|
124
|
-
city: "Domino City",
|
|
125
|
-
state: "Tokyo",
|
|
126
|
-
postalCode: "110-0030",
|
|
127
|
-
country: "Japan",
|
|
128
|
-
isDefault: true,
|
|
129
|
-
createdAt: daysAgo(360),
|
|
130
|
-
updatedAt: daysAgo(2),
|
|
131
|
-
},
|
|
132
|
-
// Kaiba Corp Card Vault — Warehouse (backup fulfillment)
|
|
133
|
-
{
|
|
134
|
-
id: "addr-kaiba-corp-warehouse",
|
|
135
|
-
ownerType: "store",
|
|
136
|
-
ownerId: "store-kaiba-corp-cards",
|
|
137
|
-
label: "Kaiba Land Warehouse",
|
|
138
|
-
fullName: "Kaiba Land Fulfillment Center",
|
|
139
|
-
phone: "+91-99999-40002",
|
|
140
|
-
addressLine1: "300 Kaiba Land Logistics",
|
|
141
|
-
addressLine2: "Tokyo Harbor Zone",
|
|
142
|
-
city: "Tokyo",
|
|
143
|
-
state: "Tokyo",
|
|
144
|
-
postalCode: "135-0064",
|
|
145
|
-
country: "Japan",
|
|
146
|
-
isDefault: false,
|
|
147
|
-
createdAt: daysAgo(300),
|
|
148
|
-
updatedAt: daysAgo(15),
|
|
149
|
-
},
|
|
150
|
-
// LetItRip Official — Fulfillment
|
|
151
|
-
{
|
|
152
|
-
id: "addr-letitrip-fulfillment",
|
|
153
|
-
ownerType: "store",
|
|
154
|
-
ownerId: "store-letitrip-official",
|
|
155
|
-
label: "LetItRip Fulfillment",
|
|
156
|
-
fullName: "LetItRip Fulfillment Center",
|
|
157
|
-
phone: "+91-99999-50001",
|
|
158
|
-
addressLine1: "200 Logistics Lane",
|
|
159
|
-
addressLine2: "Mumbai Warehouse District",
|
|
160
|
-
city: "Mumbai",
|
|
161
|
-
state: "Maharashtra",
|
|
162
|
-
postalCode: "400086",
|
|
163
|
-
country: "India",
|
|
164
|
-
isDefault: true,
|
|
165
|
-
createdAt: daysAgo(365),
|
|
166
|
-
updatedAt: daysAgo(1),
|
|
167
|
-
},
|
|
168
77
|
];
|
|
169
78
|
export const addressesSeedData = _rawAddressesSeedData;
|