@m3000/market 0.0.1 → 0.0.2

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.
Files changed (79) hide show
  1. package/dist/styles.css +2 -0
  2. package/dist/tokens.css +2 -0
  3. package/package.json +10 -10
  4. package/src/components/blocks/auction/Auction.tsx +0 -74
  5. package/src/components/blocks/auction/AuctionArtwork.tsx +0 -4
  6. package/src/components/blocks/auction/AuctionBidForm.tsx +0 -138
  7. package/src/components/blocks/auction/AuctionBidInput.tsx +0 -166
  8. package/src/components/blocks/auction/AuctionContext.tsx +0 -401
  9. package/src/components/blocks/auction/AuctionInfo.tsx +0 -36
  10. package/src/components/blocks/auction/AuctionLayout.tsx +0 -200
  11. package/src/components/blocks/auction/AuctionRankings.tsx +0 -435
  12. package/src/components/blocks/auction/AuctionStatusTag.tsx +0 -98
  13. package/src/components/blocks/auction/AuctionSuggestedBids.tsx +0 -203
  14. package/src/components/blocks/auction/AuctionYourBidCard.tsx +0 -125
  15. package/src/components/blocks/auction/AuctionYourBids.tsx +0 -61
  16. package/src/components/blocks/auction/index.ts +0 -42
  17. package/src/components/blocks/index.ts +0 -1
  18. package/src/components/index.ts +0 -2
  19. package/src/components/primitives/Button.tsx +0 -183
  20. package/src/components/primitives/Drawer.tsx +0 -125
  21. package/src/components/primitives/Feedback.tsx +0 -185
  22. package/src/components/primitives/MorphDialog.tsx +0 -160
  23. package/src/components/primitives/Price.tsx +0 -394
  24. package/src/components/primitives/PriceInput.tsx +0 -48
  25. package/src/components/primitives/Receipt.tsx +0 -711
  26. package/src/components/primitives/Scale.tsx +0 -287
  27. package/src/components/primitives/Separator.tsx +0 -87
  28. package/src/components/primitives/Skeleton.tsx +0 -33
  29. package/src/components/primitives/SteppedInput.tsx +0 -313
  30. package/src/components/primitives/Tabs.tsx +0 -161
  31. package/src/components/primitives/Tag.tsx +0 -48
  32. package/src/components/primitives/Text.tsx +0 -102
  33. package/src/components/primitives/countdown/Countdown.tsx +0 -43
  34. package/src/components/primitives/countdown/index.ts +0 -2
  35. package/src/components/primitives/framed-image/FramedImage.tsx +0 -51
  36. package/src/components/primitives/framed-image/index.ts +0 -1
  37. package/src/components/primitives/index.ts +0 -42
  38. package/src/components/primitives/ranked-list/RankedList.tsx +0 -9
  39. package/src/components/primitives/ranked-list/Ranking.tsx +0 -454
  40. package/src/components/primitives/ranked-list/index.ts +0 -8
  41. package/src/hooks/index.ts +0 -1
  42. package/src/hooks/useCountdown.ts +0 -91
  43. package/src/index.ts +0 -130
  44. package/src/lib/cn.ts +0 -81
  45. package/src/lib/index.ts +0 -2
  46. package/src/lib/motion.ts +0 -55
  47. package/src/public/lea-83-time-walk.png +0 -0
  48. package/src/public/lea-83-time-walk.webp +0 -0
  49. package/src/stories/Auction.stories.tsx +0 -658
  50. package/src/stories/AuctionLayout.stories.tsx +0 -313
  51. package/src/stories/AuctionStatusTag.stories.tsx +0 -166
  52. package/src/stories/AuctionYourBidCard.stories.tsx +0 -257
  53. package/src/stories/Button.stories.tsx +0 -306
  54. package/src/stories/Countdown.stories.tsx +0 -158
  55. package/src/stories/Feedback.stories.tsx +0 -80
  56. package/src/stories/FramedImage.stories.tsx +0 -46
  57. package/src/stories/MorphDialog.stories.tsx +0 -88
  58. package/src/stories/Price.stories.tsx +0 -292
  59. package/src/stories/RankedList.stories.tsx +0 -190
  60. package/src/stories/Receipt.stories.tsx +0 -221
  61. package/src/stories/Scale.stories.tsx +0 -578
  62. package/src/stories/Separator.stories.tsx +0 -188
  63. package/src/stories/Skeleton.stories.tsx +0 -138
  64. package/src/stories/SteppedInput.stories.tsx +0 -321
  65. package/src/stories/Tabs.stories.tsx +0 -215
  66. package/src/stories/Tag.stories.tsx +0 -138
  67. package/src/stories/Text.stories.tsx +0 -245
  68. package/src/styles/globals.css +0 -39
  69. package/src/styles/index.css +0 -4
  70. package/src/styles/theme/animation.css +0 -11
  71. package/src/styles/theme/color.css +0 -185
  72. package/src/styles/theme/index.css +0 -3
  73. package/src/styles/theme/typography.css +0 -3
  74. package/src/styles/utility.css +0 -8
  75. package/src/types/index.ts +0 -149
  76. package/src/utils/format.ts +0 -130
  77. package/src/utils/index.ts +0 -16
  78. package/src/utils/rank-utils.ts +0 -131
  79. package/src/utils/tick-validation.ts +0 -65
@@ -1,435 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useRef } from "react";
4
- import type { GroupItemContextValue } from "@/components";
5
- import { Ranking } from "@/components";
6
- import { Button, Separator, Skeleton, Text } from "@/components/primitives";
7
- import { cn } from "@/lib";
8
- import type { AuctionBid, AuctionUserBid } from "@/types";
9
- import { formatShortRelative, getProjectedRankForPrice } from "@/utils";
10
- import { useAuctionContext } from "./AuctionContext";
11
-
12
- export interface AuctionRankingsProps {
13
- className?: string;
14
- renderBidRow?: (
15
- bid: AuctionBid,
16
- context: GroupItemContextValue<AuctionBid> & { isOutbid: boolean },
17
- ) => React.ReactNode;
18
- }
19
-
20
- function BidRow({
21
- bid,
22
- rank,
23
- isOutbid,
24
- isUserBid,
25
- onTopUp,
26
- isAuctionEnded,
27
- formatPrice,
28
- currencySymbol,
29
- }: {
30
- bid: AuctionBid;
31
- rank: number;
32
- isOutbid: boolean;
33
- isUserBid: boolean;
34
- onTopUp?: () => void;
35
- isAuctionEnded: boolean;
36
- formatPrice: (priceValue: bigint) => string;
37
- currencySymbol: string;
38
- }): React.ReactElement {
39
- const timeShort = formatShortRelative(bid.createdAt);
40
-
41
- return (
42
- <div
43
- className={cn(
44
- "flex items-center justify-between gap-2 px-6 py-2",
45
- isOutbid && "opacity-50",
46
- isUserBid && "shadow-[inset_2px_0_0_0_var(--color-primary)]",
47
- )}
48
- >
49
- <div className="flex min-w-0 items-center gap-3">
50
- <Text color="tertiary" className="w-8 shrink-0" size="1">
51
- #{rank}
52
- </Text>
53
- <span className="flex min-w-0 items-center gap-3">
54
- {bid.bidder.avatarUrl && (
55
- // biome-ignore lint/performance/noImgElement: shared UI stays framework-agnostic.
56
- <img
57
- src={bid.bidder.avatarUrl}
58
- alt=""
59
- className="size-6 shrink-0 rounded-full object-cover"
60
- />
61
- )}
62
- <Text className="truncate">
63
- {isUserBid
64
- ? "You"
65
- : bid.bidder.name || `${bid.bidder.id.slice(0, 6)}...`}
66
- </Text>
67
- </span>
68
- </div>
69
- <div className="flex shrink-0 items-center gap-4">
70
- {isUserBid && !isAuctionEnded && (
71
- <Button size="xs" color="secondary" onClick={onTopUp}>
72
- Top up
73
- </Button>
74
- )}
75
- <Text>
76
- {formatPrice(bid.price)} {currencySymbol}
77
- </Text>
78
- <Text
79
- size="1"
80
- color="tertiary"
81
- className="min-w-9 text-right tabular-nums"
82
- suppressHydrationWarning
83
- >
84
- {timeShort}
85
- </Text>
86
- </div>
87
- </div>
88
- );
89
- }
90
-
91
- function BidPreviewRow({
92
- price,
93
- rank,
94
- onCancel,
95
- formatPrice,
96
- currencySymbol,
97
- previewRef,
98
- }: {
99
- price: bigint;
100
- rank: number;
101
- onCancel: () => void;
102
- formatPrice: (priceValue: bigint) => string;
103
- currencySymbol: string;
104
- previewRef?: React.RefObject<HTMLDivElement | null>;
105
- }): React.ReactElement {
106
- return (
107
- <div ref={previewRef} className="relative">
108
- <div className="absolute inset-0 animate-[pulse_2s_ease-in-out_infinite] bg-success/10" />
109
- <div className="relative flex items-center justify-between gap-2 px-6 py-2">
110
- <div className="flex min-w-0 items-center gap-3">
111
- <Text color="tertiary" className="w-8 shrink-0" size="1">
112
- #{rank}
113
- </Text>
114
- <span className="flex min-w-0 items-center gap-3">
115
- <Text color="secondary">New bid preview</Text>
116
- </span>
117
- </div>
118
- <div className="flex shrink-0 items-center gap-4">
119
- <Button size="xs" color="tertiary" onClick={onCancel}>
120
- Cancel
121
- </Button>
122
- <Text weight="medium" tabularNums>
123
- {formatPrice(price)} {currencySymbol}
124
- </Text>
125
- <span className="min-w-9"></span>
126
- </div>
127
- </div>
128
- </div>
129
- );
130
- }
131
-
132
- function TopUpPreviewRow({
133
- price,
134
- rank,
135
- onCancel,
136
- formatPrice,
137
- currencySymbol,
138
- }: {
139
- price: bigint;
140
- rank: number;
141
- onCancel: () => void;
142
- formatPrice: (priceValue: bigint) => string;
143
- currencySymbol: string;
144
- }): React.ReactElement {
145
- return (
146
- <div className="relative shadow-[inset_2px_0_0_0_var(--color-success)]">
147
- <div className="absolute inset-0 animate-[pulse_2s_ease-in-out_infinite] bg-success/10" />
148
- <div className="relative flex items-center justify-between gap-2 px-6 py-2">
149
- <div className="flex min-w-0 items-center gap-3">
150
- <Text color="tertiary" className="w-8 shrink-0" size="1">
151
- #{rank}
152
- </Text>
153
- <span className="flex min-w-0 items-center gap-3">
154
- <Text color="secondary">Top-up preview</Text>
155
- </span>
156
- </div>
157
- <div className="flex shrink-0 items-center gap-4">
158
- <Button size="xs" color="tertiary" onClick={onCancel}>
159
- Cancel
160
- </Button>
161
- <Text weight="medium" tabularNums>
162
- {formatPrice(price)} {currencySymbol}
163
- </Text>
164
- <span className="min-w-9"></span>
165
- </div>
166
- </div>
167
- </div>
168
- );
169
- }
170
-
171
- function RankingsSkeleton(): React.ReactElement {
172
- return (
173
- <div>
174
- <div className="flex items-center justify-between gap-2 px-6 py-3">
175
- <Text render={<p />} size="3">
176
- <Skeleton>username</Skeleton>
177
- </Text>
178
- <Text render={<p />} size="3">
179
- <Skeleton>0.00420 USD 11m</Skeleton>
180
- </Text>
181
- </div>
182
- <Separator orientation="horizontal" />
183
- </div>
184
- );
185
- }
186
-
187
- export { RankingsSkeleton };
188
-
189
- export function AuctionRankings({
190
- className,
191
- renderBidRow,
192
- }: AuctionRankingsProps): React.ReactElement {
193
- const {
194
- mergedForRank,
195
- maxTotalItems,
196
- formatPrice,
197
- currencySymbol,
198
- bidValue,
199
- setBidValue,
200
- minBidValue,
201
- userBids,
202
- lockedBid,
203
- setLockedBid,
204
- showBidPreview,
205
- isAuctionEnded,
206
- cancelBidding,
207
- isBiddingActive,
208
- } = useAuctionContext();
209
-
210
- const userBidMap = useMemo(() => {
211
- const map = new Map<string, AuctionUserBid>();
212
- for (const ub of userBids) {
213
- map.set(ub.id, ub);
214
- }
215
- return map;
216
- }, [userBids]);
217
-
218
- const previewRef = useRef<HTMLDivElement>(null);
219
- const scrollContainerRef = useRef<HTMLDivElement>(null);
220
-
221
- const showPreview = useMemo(() => {
222
- if (!showBidPreview) return false;
223
- if (lockedBid !== null) return false;
224
- return bidValue >= minBidValue;
225
- }, [showBidPreview, bidValue, minBidValue, lockedBid]);
226
-
227
- const previewIndex = useMemo(() => {
228
- if (!showPreview) return 0;
229
- const result = getProjectedRankForPrice(
230
- bidValue,
231
- mergedForRank,
232
- maxTotalItems,
233
- );
234
- return result.rank ? result.rank - 1 : 0;
235
- }, [showPreview, bidValue, mergedForRank, maxTotalItems]);
236
-
237
- const prevBiddingActiveRef = useRef(false);
238
-
239
- useEffect(() => {
240
- const justStartedBidding = isBiddingActive && !prevBiddingActiveRef.current;
241
- prevBiddingActiveRef.current = isBiddingActive;
242
-
243
- if (!showPreview) return;
244
-
245
- const scrollToPreview = () => {
246
- if (previewRef.current && scrollContainerRef.current) {
247
- const container = scrollContainerRef.current;
248
- const preview = previewRef.current;
249
- const stickyDivider = container.querySelector<HTMLElement>(
250
- "[data-ranking-group-divider]",
251
- );
252
- const stickyOffset = stickyDivider?.offsetHeight ?? 0;
253
-
254
- const containerRect = container.getBoundingClientRect();
255
- const previewRect = preview.getBoundingClientRect();
256
- const visibleTop = containerRect.top + stickyOffset;
257
- const visibleBottom = containerRect.bottom;
258
-
259
- const isAbove = previewRect.top < visibleTop;
260
- const isBelow = previewRect.bottom > visibleBottom;
261
-
262
- if (justStartedBidding || isAbove || isBelow) {
263
- let targetScrollTop = container.scrollTop;
264
-
265
- if (justStartedBidding) {
266
- const visibleHeight = container.clientHeight - stickyOffset;
267
- targetScrollTop +=
268
- previewRect.top -
269
- visibleTop -
270
- (visibleHeight - previewRect.height) / 2;
271
- } else if (isAbove) {
272
- targetScrollTop += previewRect.top - visibleTop;
273
- } else if (isBelow) {
274
- targetScrollTop += previewRect.bottom - visibleBottom;
275
- }
276
-
277
- container.scrollTo({
278
- top: Math.max(0, targetScrollTop),
279
- behavior: "auto",
280
- });
281
- }
282
- }
283
- };
284
-
285
- const frameId = requestAnimationFrame(() => {
286
- requestAnimationFrame(scrollToPreview);
287
- });
288
-
289
- return () => cancelAnimationFrame(frameId);
290
- }, [showPreview, isBiddingActive]);
291
-
292
- const { lockedBidId, lockedBidOriginalIndex } = useMemo(() => {
293
- if (lockedBid === null)
294
- return { lockedBidId: null, lockedBidOriginalIndex: null };
295
- const lockedUserBid = userBids.find(
296
- (ub) => ub.globalBidId === lockedBid.bidId,
297
- );
298
- const bidId = lockedUserBid?.id ?? null;
299
- const originalIndex =
300
- bidId !== null ? mergedForRank.findIndex((b) => b.id === bidId) : null;
301
- return { lockedBidId: bidId, lockedBidOriginalIndex: originalIndex };
302
- }, [lockedBid, userBids, mergedForRank]);
303
-
304
- const allBids: AuctionBid[] = useMemo(
305
- () =>
306
- mergedForRank
307
- .filter((b) => b.id !== lockedBidId)
308
- .map((b) => ({
309
- id: b.id,
310
- price: BigInt(b.price),
311
- createdAt: new Date(b.created_at),
312
- bidder: b.bidder ?? { id: b.id, name: "Unknown" },
313
- })),
314
- [mergedForRank, lockedBidId],
315
- );
316
-
317
- const topUpPreviewIndex = useMemo(() => {
318
- if (lockedBid === null || lockedBidId === null) return null;
319
-
320
- const bidsWithoutLocked = mergedForRank.filter((b) => b.id !== lockedBidId);
321
-
322
- const result = getProjectedRankForPrice(
323
- bidValue,
324
- bidsWithoutLocked,
325
- maxTotalItems,
326
- );
327
- return result.rank ? result.rank - 1 : 0;
328
- }, [lockedBid, lockedBidId, bidValue, mergedForRank, maxTotalItems]);
329
-
330
- return (
331
- <div
332
- ref={scrollContainerRef}
333
- className={cn("min-h-0 flex-1 overflow-y-auto", className)}
334
- >
335
- <Ranking.Root
336
- items={allBids}
337
- getKey={(bid) => bid.id}
338
- boundaries={[maxTotalItems]}
339
- labels={["Winning Bids", "Outbid"]}
340
- >
341
- {showPreview && (
342
- <Ranking.Slot slotKey="preview" atIndex={previewIndex}>
343
- {(context) => (
344
- <>
345
- <BidPreviewRow
346
- price={bidValue}
347
- rank={context.rank}
348
- onCancel={cancelBidding}
349
- formatPrice={formatPrice}
350
- currencySymbol={currencySymbol}
351
- previewRef={previewRef}
352
- />
353
- {!context.isLastInGroup && (
354
- <Separator orientation="horizontal" />
355
- )}
356
- </>
357
- )}
358
- </Ranking.Slot>
359
- )}
360
- {lockedBid !== null && topUpPreviewIndex !== null && (
361
- <Ranking.Slot slotKey="topup-preview" atIndex={topUpPreviewIndex}>
362
- {(context) => (
363
- <>
364
- <TopUpPreviewRow
365
- price={bidValue}
366
- rank={context.rank}
367
- onCancel={cancelBidding}
368
- formatPrice={formatPrice}
369
- currencySymbol={currencySymbol}
370
- />
371
- {!context.isLastInGroup && (
372
- <Separator orientation="horizontal" />
373
- )}
374
- </>
375
- )}
376
- </Ranking.Slot>
377
- )}
378
- <Ranking.Empty>
379
- <Text color="tertiary">No activity</Text>
380
- </Ranking.Empty>
381
- <Ranking.Group>
382
- <Ranking.GroupDivider className="rounded-xl" />
383
- <Ranking.GroupItem>
384
- <Ranking.GroupItemValue>
385
- {(bid: AuctionBid, context) => {
386
- const isOutbid = context.groupIndex === 1;
387
- const isBelowLockedBid =
388
- lockedBid !== null &&
389
- lockedBidOriginalIndex !== null &&
390
- lockedBidOriginalIndex >= 0 &&
391
- context.globalIndex >= lockedBidOriginalIndex;
392
- const extendedContext = { ...context, isOutbid };
393
-
394
- const userBid = userBidMap.get(bid.id);
395
- const isUserBid = !!userBid;
396
-
397
- const handleTopUp = () => {
398
- if (userBid) {
399
- setLockedBid({
400
- bidId: userBid.globalBidId,
401
- priceValue: userBid.price,
402
- });
403
- setBidValue(userBid.price);
404
- }
405
- };
406
-
407
- return (
408
- <>
409
- {renderBidRow ? (
410
- renderBidRow(bid, extendedContext)
411
- ) : (
412
- <BidRow
413
- bid={bid}
414
- rank={context.globalIndex + 1}
415
- isOutbid={isOutbid || isBelowLockedBid}
416
- isUserBid={isUserBid}
417
- onTopUp={handleTopUp}
418
- isAuctionEnded={isAuctionEnded}
419
- formatPrice={formatPrice}
420
- currencySymbol={currencySymbol}
421
- />
422
- )}
423
- {!context.isLastInGroup && (
424
- <Separator orientation="horizontal" />
425
- )}
426
- </>
427
- );
428
- }}
429
- </Ranking.GroupItemValue>
430
- </Ranking.GroupItem>
431
- </Ranking.Group>
432
- </Ranking.Root>
433
- </div>
434
- );
435
- }
@@ -1,98 +0,0 @@
1
- "use client";
2
-
3
- import { Tag, Text } from "@/components/primitives";
4
- import { cn } from "@/lib";
5
- import { formatDateTime } from "@/utils";
6
- import { Countdown } from "../../primitives/countdown/Countdown";
7
-
8
- type AuctionState = "upcoming" | "live" | "closed";
9
-
10
- function getAuctionState(
11
- opensAt: Date | null,
12
- endsAt: Date | null,
13
- ): AuctionState {
14
- if (!opensAt || !endsAt) return "live";
15
- const now = Date.now();
16
- const open = opensAt.getTime();
17
- const end = endsAt.getTime();
18
- if (now < open) return "upcoming";
19
- if (now > end) return "closed";
20
- return "live";
21
- }
22
-
23
- const transparentTagClassName = "bg-transparent border-transparent p-0";
24
-
25
- export interface AuctionStatusTagProps {
26
- opensAt: Date | null;
27
- endsAt: Date | null;
28
- background?: "filled" | "transparent";
29
- showCountdown?: boolean;
30
- }
31
-
32
- export function AuctionStatusTag({
33
- opensAt,
34
- endsAt,
35
- background = "filled",
36
- showCountdown = false,
37
- }: AuctionStatusTagProps): React.ReactElement | null {
38
- const state = getAuctionState(opensAt, endsAt);
39
- const countdownTo =
40
- state === "live" ? endsAt : state === "upcoming" ? opensAt : null;
41
-
42
- const tagClassName = cn(
43
- "inline-flex items-center gap-1.5 p-2",
44
- background === "transparent" && transparentTagClassName,
45
- );
46
-
47
- if (state === "live" && endsAt) {
48
- return (
49
- <Tag className={tagClassName} aria-label="Auction is live">
50
- <span className="relative flex size-2 shrink-0" aria-hidden>
51
- <span className="absolute inline-flex size-full animate-ping rounded-full bg-success opacity-75" />
52
- <span className="relative inline-flex size-2 rounded-full bg-success" />
53
- </span>
54
- <Text size="1" suppressHydrationWarning>
55
- {showCountdown && countdownTo ? (
56
- <>
57
- Live - <Countdown to={countdownTo} />
58
- </>
59
- ) : (
60
- <>Live - Until {formatDateTime(endsAt)}</>
61
- )}
62
- </Text>
63
- </Tag>
64
- );
65
- }
66
-
67
- if (state === "upcoming" && opensAt) {
68
- return (
69
- <Tag className={tagClassName}>
70
- <Text size="1" suppressHydrationWarning>
71
- {showCountdown && countdownTo ? (
72
- <>
73
- Opens in <Countdown to={countdownTo} />
74
- </>
75
- ) : (
76
- <>Opens {formatDateTime(opensAt)}</>
77
- )}
78
- </Text>
79
- </Tag>
80
- );
81
- }
82
-
83
- if (state === "closed" && endsAt) {
84
- return (
85
- <Tag className={tagClassName}>
86
- <div
87
- className="size-2.5 shrink-0 rounded-full bg-muted-foreground"
88
- aria-hidden
89
- />
90
- <Text suppressHydrationWarning>
91
- Closed - ended {formatDateTime(endsAt)}
92
- </Text>
93
- </Tag>
94
- );
95
- }
96
-
97
- return null;
98
- }