@m3000/market 0.0.1
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/LICENSE +21 -0
- package/dist/components/blocks/auction/Auction.d.ts +49 -0
- package/dist/components/blocks/auction/Auction.js +44 -0
- package/dist/components/blocks/auction/AuctionBidForm.d.ts +11 -0
- package/dist/components/blocks/auction/AuctionBidForm.js +88 -0
- package/dist/components/blocks/auction/AuctionBidInput.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionBidInput.js +99 -0
- package/dist/components/blocks/auction/AuctionContext.d.ts +71 -0
- package/dist/components/blocks/auction/AuctionContext.js +228 -0
- package/dist/components/blocks/auction/AuctionInfo.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionInfo.js +37 -0
- package/dist/components/blocks/auction/AuctionLayout.d.ts +63 -0
- package/dist/components/blocks/auction/AuctionLayout.js +80 -0
- package/dist/components/blocks/auction/AuctionRankings.d.ts +16 -0
- package/dist/components/blocks/auction/AuctionRankings.js +334 -0
- package/dist/components/blocks/auction/AuctionStatusTag.d.ts +15 -0
- package/dist/components/blocks/auction/AuctionStatusTag.js +60 -0
- package/dist/components/blocks/auction/AuctionSuggestedBids.d.ts +38 -0
- package/dist/components/blocks/auction/AuctionSuggestedBids.js +116 -0
- package/dist/components/blocks/auction/AuctionYourBidCard.d.ts +27 -0
- package/dist/components/blocks/auction/AuctionYourBidCard.js +94 -0
- package/dist/components/blocks/auction/AuctionYourBids.d.ts +9 -0
- package/dist/components/blocks/auction/AuctionYourBids.js +49 -0
- package/dist/components/blocks/auction/index.d.ts +12 -0
- package/dist/components/blocks/index.d.ts +12 -0
- package/dist/components/index.d.ts +28 -0
- package/dist/components/primitives/Button.d.ts +31 -0
- package/dist/components/primitives/Button.js +117 -0
- package/dist/components/primitives/Drawer.d.ts +43 -0
- package/dist/components/primitives/Drawer.js +51 -0
- package/dist/components/primitives/Feedback.d.ts +28 -0
- package/dist/components/primitives/Feedback.js +147 -0
- package/dist/components/primitives/MorphDialog.d.ts +39 -0
- package/dist/components/primitives/MorphDialog.js +87 -0
- package/dist/components/primitives/Price.d.ts +84 -0
- package/dist/components/primitives/Price.js +255 -0
- package/dist/components/primitives/PriceInput.d.ts +33 -0
- package/dist/components/primitives/PriceInput.js +25 -0
- package/dist/components/primitives/Receipt.d.ts +164 -0
- package/dist/components/primitives/Receipt.js +344 -0
- package/dist/components/primitives/Scale.d.ts +67 -0
- package/dist/components/primitives/Scale.js +132 -0
- package/dist/components/primitives/Separator.d.ts +22 -0
- package/dist/components/primitives/Separator.js +62 -0
- package/dist/components/primitives/Skeleton.d.ts +14 -0
- package/dist/components/primitives/Skeleton.js +20 -0
- package/dist/components/primitives/SteppedInput.d.ts +94 -0
- package/dist/components/primitives/SteppedInput.js +154 -0
- package/dist/components/primitives/Tabs.d.ts +37 -0
- package/dist/components/primitives/Tabs.js +99 -0
- package/dist/components/primitives/Tag.d.ts +24 -0
- package/dist/components/primitives/Tag.js +22 -0
- package/dist/components/primitives/Text.d.ts +32 -0
- package/dist/components/primitives/Text.js +65 -0
- package/dist/components/primitives/countdown/Countdown.d.ts +24 -0
- package/dist/components/primitives/countdown/Countdown.js +22 -0
- package/dist/components/primitives/framed-image/FramedImage.d.ts +13 -0
- package/dist/components/primitives/framed-image/FramedImage.js +37 -0
- package/dist/components/primitives/index.d.ts +17 -0
- package/dist/components/primitives/ranked-list/Ranking.d.ts +117 -0
- package/dist/components/primitives/ranked-list/Ranking.js +219 -0
- package/dist/components/primitives/ranked-list/index.d.ts +1 -0
- package/dist/hooks/useCountdown.d.ts +20 -0
- package/dist/hooks/useCountdown.js +75 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +36 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +75 -0
- package/dist/lib/motion.d.ts +19 -0
- package/dist/lib/motion.js +43 -0
- package/dist/types/index.d.ts +120 -0
- package/dist/utils/format.d.ts +38 -0
- package/dist/utils/format.js +103 -0
- package/dist/utils/rank-utils.d.ts +34 -0
- package/dist/utils/rank-utils.js +80 -0
- package/dist/utils/tick-validation.d.ts +22 -0
- package/dist/utils/tick-validation.js +40 -0
- package/package.json +92 -0
- package/src/components/blocks/auction/Auction.tsx +74 -0
- package/src/components/blocks/auction/AuctionArtwork.tsx +4 -0
- package/src/components/blocks/auction/AuctionBidForm.tsx +138 -0
- package/src/components/blocks/auction/AuctionBidInput.tsx +166 -0
- package/src/components/blocks/auction/AuctionContext.tsx +401 -0
- package/src/components/blocks/auction/AuctionInfo.tsx +36 -0
- package/src/components/blocks/auction/AuctionLayout.tsx +200 -0
- package/src/components/blocks/auction/AuctionRankings.tsx +435 -0
- package/src/components/blocks/auction/AuctionStatusTag.tsx +98 -0
- package/src/components/blocks/auction/AuctionSuggestedBids.tsx +203 -0
- package/src/components/blocks/auction/AuctionYourBidCard.tsx +125 -0
- package/src/components/blocks/auction/AuctionYourBids.tsx +61 -0
- package/src/components/blocks/auction/index.ts +42 -0
- package/src/components/blocks/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/components/primitives/Button.tsx +183 -0
- package/src/components/primitives/Drawer.tsx +125 -0
- package/src/components/primitives/Feedback.tsx +185 -0
- package/src/components/primitives/MorphDialog.tsx +160 -0
- package/src/components/primitives/Price.tsx +394 -0
- package/src/components/primitives/PriceInput.tsx +48 -0
- package/src/components/primitives/Receipt.tsx +711 -0
- package/src/components/primitives/Scale.tsx +287 -0
- package/src/components/primitives/Separator.tsx +87 -0
- package/src/components/primitives/Skeleton.tsx +33 -0
- package/src/components/primitives/SteppedInput.tsx +313 -0
- package/src/components/primitives/Tabs.tsx +161 -0
- package/src/components/primitives/Tag.tsx +48 -0
- package/src/components/primitives/Text.tsx +102 -0
- package/src/components/primitives/countdown/Countdown.tsx +43 -0
- package/src/components/primitives/countdown/index.ts +2 -0
- package/src/components/primitives/framed-image/FramedImage.tsx +51 -0
- package/src/components/primitives/framed-image/index.ts +1 -0
- package/src/components/primitives/index.ts +42 -0
- package/src/components/primitives/ranked-list/RankedList.tsx +9 -0
- package/src/components/primitives/ranked-list/Ranking.tsx +454 -0
- package/src/components/primitives/ranked-list/index.ts +8 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useCountdown.ts +91 -0
- package/src/index.ts +130 -0
- package/src/lib/cn.ts +81 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/motion.ts +55 -0
- package/src/public/lea-83-time-walk.png +0 -0
- package/src/public/lea-83-time-walk.webp +0 -0
- package/src/stories/Auction.stories.tsx +658 -0
- package/src/stories/AuctionLayout.stories.tsx +313 -0
- package/src/stories/AuctionStatusTag.stories.tsx +166 -0
- package/src/stories/AuctionYourBidCard.stories.tsx +257 -0
- package/src/stories/Button.stories.tsx +306 -0
- package/src/stories/Countdown.stories.tsx +158 -0
- package/src/stories/Feedback.stories.tsx +80 -0
- package/src/stories/FramedImage.stories.tsx +46 -0
- package/src/stories/MorphDialog.stories.tsx +88 -0
- package/src/stories/Price.stories.tsx +292 -0
- package/src/stories/RankedList.stories.tsx +190 -0
- package/src/stories/Receipt.stories.tsx +221 -0
- package/src/stories/Scale.stories.tsx +578 -0
- package/src/stories/Separator.stories.tsx +188 -0
- package/src/stories/Skeleton.stories.tsx +138 -0
- package/src/stories/SteppedInput.stories.tsx +321 -0
- package/src/stories/Tabs.stories.tsx +215 -0
- package/src/stories/Tag.stories.tsx +138 -0
- package/src/stories/Text.stories.tsx +245 -0
- package/src/styles/globals.css +39 -0
- package/src/styles/index.css +4 -0
- package/src/styles/theme/animation.css +11 -0
- package/src/styles/theme/color.css +185 -0
- package/src/styles/theme/index.css +3 -0
- package/src/styles/theme/typography.css +3 -0
- package/src/styles/utility.css +8 -0
- package/src/types/index.ts +149 -0
- package/src/utils/format.ts +130 -0
- package/src/utils/index.ts +16 -0
- package/src/utils/rank-utils.ts +131 -0
- package/src/utils/tick-validation.ts +65 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
@theme {
|
|
2
|
+
/* ============================================================
|
|
3
|
+
PRIMITIVE SCALE - based on Radix UI grey scale
|
|
4
|
+
Light mode values (used directly) / Dark mode has separate scale
|
|
5
|
+
============================================================ */
|
|
6
|
+
|
|
7
|
+
/* Light mode primitives (Radix gray 1-12) */
|
|
8
|
+
--color-grey-1: #fcfcfc;
|
|
9
|
+
--color-grey-2: #f9f9f9;
|
|
10
|
+
--color-grey-3: #f0f0f0;
|
|
11
|
+
--color-grey-4: #e8e8e8;
|
|
12
|
+
--color-grey-5: #e0e0e0;
|
|
13
|
+
--color-grey-6: #d9d9d9;
|
|
14
|
+
--color-grey-7: #cecece;
|
|
15
|
+
--color-grey-8: #bbbbbb;
|
|
16
|
+
--color-grey-9: #8d8d8d;
|
|
17
|
+
--color-grey-10: #838383;
|
|
18
|
+
--color-grey-11: #646464;
|
|
19
|
+
--color-grey-12: #202020;
|
|
20
|
+
|
|
21
|
+
--color-white: #ffffff;
|
|
22
|
+
--color-black: #111111;
|
|
23
|
+
--color-current: currentColor;
|
|
24
|
+
--color-transparent: transparent;
|
|
25
|
+
|
|
26
|
+
/* ============================================================
|
|
27
|
+
SEMANTIC TOKENS (shadcn naming) - light mode defaults
|
|
28
|
+
Using Radix scale semantics:
|
|
29
|
+
1-2: backgrounds, 3-5: component bg, 6-8: borders, 11-12: text
|
|
30
|
+
============================================================ */
|
|
31
|
+
|
|
32
|
+
/* Page backgrounds */
|
|
33
|
+
--color-background: var(--color-white);
|
|
34
|
+
--color-foreground: var(--color-grey-12);
|
|
35
|
+
|
|
36
|
+
/* Muted surfaces (cards, secondary backgrounds) */
|
|
37
|
+
--color-muted: var(--color-grey-2);
|
|
38
|
+
--color-muted-foreground: var(--color-grey-11);
|
|
39
|
+
|
|
40
|
+
/* Card surfaces */
|
|
41
|
+
--color-card: var(--color-white);
|
|
42
|
+
--color-card-foreground: var(--color-grey-12);
|
|
43
|
+
|
|
44
|
+
/* Popover surfaces */
|
|
45
|
+
--color-popover: var(--color-white);
|
|
46
|
+
--color-popover-foreground: var(--color-grey-12);
|
|
47
|
+
|
|
48
|
+
/* Primary actions (solid buttons) */
|
|
49
|
+
--color-primary: var(--color-grey-12);
|
|
50
|
+
--color-primary-hover: var(--color-grey-11);
|
|
51
|
+
--color-primary-foreground: var(--color-white);
|
|
52
|
+
|
|
53
|
+
/* Secondary actions */
|
|
54
|
+
--color-secondary: var(--color-grey-3);
|
|
55
|
+
--color-secondary-hover: var(--color-grey-4);
|
|
56
|
+
--color-secondary-foreground: var(--color-grey-12);
|
|
57
|
+
|
|
58
|
+
/* Accent (interactive element backgrounds) */
|
|
59
|
+
--color-accent: var(--color-grey-3);
|
|
60
|
+
--color-accent-hover: var(--color-grey-4);
|
|
61
|
+
--color-accent-active: var(--color-grey-5);
|
|
62
|
+
--color-accent-foreground: var(--color-grey-12);
|
|
63
|
+
|
|
64
|
+
/* Borders - using Radix steps 6-7 for subtle borders */
|
|
65
|
+
--color-border: var(--color-grey-6);
|
|
66
|
+
--color-input: var(--color-grey-7);
|
|
67
|
+
--color-ring: var(--color-grey-12);
|
|
68
|
+
|
|
69
|
+
/* Disabled states */
|
|
70
|
+
--color-disabled: var(--color-grey-3);
|
|
71
|
+
--color-disabled-foreground: var(--color-grey-9);
|
|
72
|
+
|
|
73
|
+
/* Destructive/Error */
|
|
74
|
+
--color-destructive: #e5484d;
|
|
75
|
+
--color-destructive-hover: #dc3d43;
|
|
76
|
+
--color-destructive-foreground: var(--color-white);
|
|
77
|
+
--color-destructive-muted: #fff8f8;
|
|
78
|
+
--color-destructive-muted-foreground: #cd2b31;
|
|
79
|
+
|
|
80
|
+
/* Success */
|
|
81
|
+
--color-success: #30a46c;
|
|
82
|
+
--color-success-hover: #2b9a64;
|
|
83
|
+
--color-success-foreground: var(--color-white);
|
|
84
|
+
--color-success-muted: #e9f9ee;
|
|
85
|
+
--color-success-muted-foreground: #18794e;
|
|
86
|
+
|
|
87
|
+
/* Warning/Alert */
|
|
88
|
+
--color-warning: #f5d90a;
|
|
89
|
+
--color-warning-hover: #e5c800;
|
|
90
|
+
--color-warning-foreground: var(--color-grey-12);
|
|
91
|
+
--color-warning-muted: #fefce9;
|
|
92
|
+
--color-warning-muted-foreground: #946800;
|
|
93
|
+
|
|
94
|
+
/* Separator (subtle dividers) */
|
|
95
|
+
--color-separator: var(--color-grey-3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ============================================================
|
|
99
|
+
DARK MODE - using Radix dark grey scale for proper layering
|
|
100
|
+
============================================================ */
|
|
101
|
+
@layer base {
|
|
102
|
+
@variant dark {
|
|
103
|
+
/* Dark mode primitives (Radix gray-dark 1-12)
|
|
104
|
+
These override the light primitives contextually */
|
|
105
|
+
--color-grey-1: #111111;
|
|
106
|
+
--color-grey-2: #191919;
|
|
107
|
+
--color-grey-3: #222222;
|
|
108
|
+
--color-grey-4: #2a2a2a;
|
|
109
|
+
--color-grey-5: #313131;
|
|
110
|
+
--color-grey-6: #3a3a3a;
|
|
111
|
+
--color-grey-7: #484848;
|
|
112
|
+
--color-grey-8: #606060;
|
|
113
|
+
--color-grey-9: #6e6e6e;
|
|
114
|
+
--color-grey-10: #7b7b7b;
|
|
115
|
+
--color-grey-11: #b4b4b4;
|
|
116
|
+
--color-grey-12: #eeeeee;
|
|
117
|
+
|
|
118
|
+
--color-black: #111111;
|
|
119
|
+
|
|
120
|
+
/* Page backgrounds */
|
|
121
|
+
--color-background: var(--color-grey-1);
|
|
122
|
+
--color-foreground: var(--color-grey-12);
|
|
123
|
+
|
|
124
|
+
/* Muted surfaces - step 2 for subtle elevation */
|
|
125
|
+
--color-muted: var(--color-grey-2);
|
|
126
|
+
--color-muted-foreground: var(--color-grey-11);
|
|
127
|
+
|
|
128
|
+
/* Card surfaces */
|
|
129
|
+
--color-card: var(--color-grey-2);
|
|
130
|
+
--color-card-foreground: var(--color-grey-12);
|
|
131
|
+
|
|
132
|
+
/* Popover surfaces */
|
|
133
|
+
--color-popover: var(--color-grey-2);
|
|
134
|
+
--color-popover-foreground: var(--color-grey-12);
|
|
135
|
+
|
|
136
|
+
/* Primary actions */
|
|
137
|
+
--color-primary: var(--color-grey-12);
|
|
138
|
+
--color-primary-hover: var(--color-white);
|
|
139
|
+
--color-primary-foreground: var(--color-grey-1);
|
|
140
|
+
|
|
141
|
+
/* Secondary actions - steps 3-4 */
|
|
142
|
+
--color-secondary: var(--color-grey-3);
|
|
143
|
+
--color-secondary-hover: var(--color-grey-4);
|
|
144
|
+
--color-secondary-foreground: var(--color-grey-12);
|
|
145
|
+
|
|
146
|
+
/* Accent - steps 3-5 */
|
|
147
|
+
--color-accent: var(--color-grey-3);
|
|
148
|
+
--color-accent-hover: var(--color-grey-4);
|
|
149
|
+
--color-accent-active: var(--color-grey-5);
|
|
150
|
+
--color-accent-foreground: var(--color-grey-12);
|
|
151
|
+
|
|
152
|
+
/* Borders - step 6 for subtle, step 7 for interactive */
|
|
153
|
+
--color-border: var(--color-grey-6);
|
|
154
|
+
--color-input: var(--color-grey-7);
|
|
155
|
+
--color-ring: var(--color-grey-12);
|
|
156
|
+
|
|
157
|
+
/* Disabled states */
|
|
158
|
+
--color-disabled: var(--color-grey-3);
|
|
159
|
+
--color-disabled-foreground: var(--color-grey-9);
|
|
160
|
+
|
|
161
|
+
/* Destructive/Error */
|
|
162
|
+
--color-destructive: #e5484d;
|
|
163
|
+
--color-destructive-hover: #f16a6e;
|
|
164
|
+
--color-destructive-foreground: var(--color-white);
|
|
165
|
+
--color-destructive-muted: #291415;
|
|
166
|
+
--color-destructive-muted-foreground: #ff6369;
|
|
167
|
+
|
|
168
|
+
/* Success */
|
|
169
|
+
--color-success: #30a46c;
|
|
170
|
+
--color-success-hover: #3cb77a;
|
|
171
|
+
--color-success-foreground: var(--color-white);
|
|
172
|
+
--color-success-muted: #0d2117;
|
|
173
|
+
--color-success-muted-foreground: #4cc38a;
|
|
174
|
+
|
|
175
|
+
/* Warning/Alert */
|
|
176
|
+
--color-warning: #f5d90a;
|
|
177
|
+
--color-warning-hover: #ffe629;
|
|
178
|
+
--color-warning-foreground: var(--color-grey-12);
|
|
179
|
+
--color-warning-muted: #1c1500;
|
|
180
|
+
--color-warning-muted-foreground: #f0c000;
|
|
181
|
+
|
|
182
|
+
/* Separator */
|
|
183
|
+
--color-separator: var(--color-grey-3);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* Dark theme via class */
|
|
2
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
3
|
+
|
|
4
|
+
/* Helpers for data attributes (Base UI) */
|
|
5
|
+
@custom-variant data-active (&[data-state~="active"]);
|
|
6
|
+
@custom-variant data-open (&[data-state~="open"]);
|
|
7
|
+
@custom-variant data-close (&[data-state~="close"]);
|
|
8
|
+
@custom-variant data-disabled (&[data-state~="disabled"]);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// ─── Auction Data Types ─────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tick configuration for the auction's price grid.
|
|
5
|
+
* Prices below the threshold use smallTickSize; prices above use largeTickSize.
|
|
6
|
+
*/
|
|
7
|
+
export interface AuctionTickConfig {
|
|
8
|
+
threshold: bigint;
|
|
9
|
+
smallTickSize: bigint;
|
|
10
|
+
largeTickSize: bigint;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Core auction data. All prices are in bigint.
|
|
15
|
+
* This type is fully generic -- no blockchain or framework coupling.
|
|
16
|
+
*/
|
|
17
|
+
export interface AuctionData {
|
|
18
|
+
/** Unique identifier for the auction */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Minimum accepted bid price */
|
|
21
|
+
reservePrice: bigint;
|
|
22
|
+
/** Final clearing price after settlement, if finalized */
|
|
23
|
+
clearingPrice?: bigint;
|
|
24
|
+
/** When the auction opens for bidding (null = already open) */
|
|
25
|
+
opensAt: Date | null;
|
|
26
|
+
/** When the auction closes (null = no end time set) */
|
|
27
|
+
endsAt: Date | null;
|
|
28
|
+
/** Maximum number of winning bidders / editions */
|
|
29
|
+
maxTotalItems: number;
|
|
30
|
+
/** Tick configuration for bid price stepping */
|
|
31
|
+
tickConfig?: AuctionTickConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ─── Bid Types ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A bidder's public profile. Consumers map their own user type into this.
|
|
38
|
+
*/
|
|
39
|
+
export interface AuctionBidder {
|
|
40
|
+
/** Unique user identifier (e.g. wallet address) */
|
|
41
|
+
id: string;
|
|
42
|
+
/** Display name */
|
|
43
|
+
name?: string;
|
|
44
|
+
/** Avatar image URL */
|
|
45
|
+
avatarUrl?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A single bid in the auction leaderboard (public view).
|
|
50
|
+
* All prices are in the auction's native unit as bigint.
|
|
51
|
+
*/
|
|
52
|
+
export interface AuctionBid {
|
|
53
|
+
/** Unique bid identifier */
|
|
54
|
+
id: string;
|
|
55
|
+
/** Bid price */
|
|
56
|
+
price: bigint;
|
|
57
|
+
/** When the bid was placed */
|
|
58
|
+
createdAt: Date;
|
|
59
|
+
/** The bidder */
|
|
60
|
+
bidder: AuctionBidder;
|
|
61
|
+
/** Link to the bid transaction on a block explorer */
|
|
62
|
+
explorerUrl?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Bid status for user's own bids */
|
|
66
|
+
export type AuctionBidStatus = "active" | "refunded" | "claimed";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Extended bid data for the current user's bids.
|
|
70
|
+
* Includes status and claim information.
|
|
71
|
+
*/
|
|
72
|
+
export interface AuctionUserBid extends AuctionBid {
|
|
73
|
+
/** On-chain global bid ID (used for top-up) */
|
|
74
|
+
globalBidId: bigint;
|
|
75
|
+
/** Current bid status */
|
|
76
|
+
status: AuctionBidStatus;
|
|
77
|
+
/** Whether this bid is currently in the winning range */
|
|
78
|
+
isWinning: boolean;
|
|
79
|
+
/** When the item was claimed (for winning bids) */
|
|
80
|
+
claimedAt?: Date;
|
|
81
|
+
/** Refund transaction explorer URL */
|
|
82
|
+
refundExplorerUrl?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Operation Status ───────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/** Status of an async operation (bid placement, top-up, claim) */
|
|
88
|
+
export type OperationStatus =
|
|
89
|
+
| "idle"
|
|
90
|
+
| "pending"
|
|
91
|
+
| "confirming"
|
|
92
|
+
| "indexing"
|
|
93
|
+
| "success"
|
|
94
|
+
| "error";
|
|
95
|
+
|
|
96
|
+
/** Tracks the state of an in-flight operation */
|
|
97
|
+
export interface OperationState {
|
|
98
|
+
status: OperationStatus;
|
|
99
|
+
error?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Callbacks ──────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Callbacks that consumers must provide to wire up blockchain/API operations.
|
|
106
|
+
* All return a boolean indicating success/failure.
|
|
107
|
+
*/
|
|
108
|
+
export interface AuctionCallbacks {
|
|
109
|
+
/** Place a new bid. Called with price and quantity. */
|
|
110
|
+
onPlaceBid: (price: bigint, quantity: bigint) => Promise<boolean>;
|
|
111
|
+
/** Top up an existing bid to a new price. */
|
|
112
|
+
onTopUpBid: (
|
|
113
|
+
bidId: bigint,
|
|
114
|
+
newPrice: bigint,
|
|
115
|
+
additionalValue: bigint,
|
|
116
|
+
) => Promise<boolean>;
|
|
117
|
+
/** Claim an item for a winning bid. Optional. */
|
|
118
|
+
onClaimEdition?: (bidId: string) => Promise<boolean>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Formatters ─────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Formatting functions consumers can provide to customize display.
|
|
125
|
+
* All are optional -- sensible defaults are used when not provided.
|
|
126
|
+
*/
|
|
127
|
+
export interface AuctionFormatters {
|
|
128
|
+
/** Format a price value for display. Default: divide by 10^decimals */
|
|
129
|
+
formatPrice?: (priceValue: bigint) => string;
|
|
130
|
+
/** Format a date for display. Default: relative time (e.g. "5m ago") */
|
|
131
|
+
formatTime?: (date: Date) => string;
|
|
132
|
+
/** Currency symbol to display alongside prices. Default: "USD" */
|
|
133
|
+
currencySymbol?: string;
|
|
134
|
+
/** Number of decimal places used when editing bigint-backed prices. */
|
|
135
|
+
inputDecimals?: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Internal Rankable Bid ──────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Lightweight bid representation used internally for rank calculations.
|
|
142
|
+
* Consumers don't need to use this directly -- it's derived from AuctionBid.
|
|
143
|
+
*/
|
|
144
|
+
export interface RankableBid {
|
|
145
|
+
id: string;
|
|
146
|
+
price: string;
|
|
147
|
+
created_at: string;
|
|
148
|
+
bidder?: AuctionBidder;
|
|
149
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default price formatter: converts wei (bigint) to a human-readable ETH string.
|
|
3
|
+
* Uses native bigint arithmetic -- no external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
export function formatWeiToEth(wei: bigint, decimals: number = 18): string {
|
|
6
|
+
const isNegative = wei < 0n;
|
|
7
|
+
const abs = isNegative ? -wei : wei;
|
|
8
|
+
const divisor = 10n ** BigInt(decimals);
|
|
9
|
+
const whole = abs / divisor;
|
|
10
|
+
const remainder = abs % divisor;
|
|
11
|
+
|
|
12
|
+
if (remainder === 0n) {
|
|
13
|
+
return `${isNegative ? "-" : ""}${whole.toString()}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Pad remainder to full decimal length and strip trailing zeros
|
|
17
|
+
const remainderStr = remainder.toString().padStart(decimals, "0");
|
|
18
|
+
const trimmed = remainderStr.replace(/0+$/, "");
|
|
19
|
+
|
|
20
|
+
return `${isNegative ? "-" : ""}${whole.toString()}.${trimmed}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses an ETH string (e.g. "0.01") to wei (bigint).
|
|
25
|
+
* Inverse of formatWeiToEth.
|
|
26
|
+
*/
|
|
27
|
+
export function parseEthToWei(eth: string, decimals: number = 18): bigint {
|
|
28
|
+
const [wholePart = "0", fracPart = ""] = eth.split(".");
|
|
29
|
+
const paddedFrac = fracPart.padEnd(decimals, "0").slice(0, decimals);
|
|
30
|
+
return BigInt(wholePart) * 10n ** BigInt(decimals) + BigInt(paddedFrac);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Formats a date as a short relative time string.
|
|
35
|
+
* Examples: "now", "5m", "2h", "3d", "2w"
|
|
36
|
+
*/
|
|
37
|
+
export function formatShortRelative(date: Date): string {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const diff = now - date.getTime();
|
|
40
|
+
const secs = Math.floor(diff / 1000);
|
|
41
|
+
|
|
42
|
+
if (secs < 60) return "now";
|
|
43
|
+
const mins = Math.floor(secs / 60);
|
|
44
|
+
if (mins < 60) return `${mins}m`;
|
|
45
|
+
const hours = Math.floor(mins / 60);
|
|
46
|
+
if (hours < 24) return `${hours}h`;
|
|
47
|
+
const days = Math.floor(hours / 24);
|
|
48
|
+
if (days < 7) return `${days}d`;
|
|
49
|
+
return `${Math.min(Math.floor(days / 7), 99)}w`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats a date as a full timestamp string.
|
|
54
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
55
|
+
*/
|
|
56
|
+
export function formatFullTimestamp(date: Date): string {
|
|
57
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
58
|
+
const months = [
|
|
59
|
+
"Jan",
|
|
60
|
+
"Feb",
|
|
61
|
+
"Mar",
|
|
62
|
+
"Apr",
|
|
63
|
+
"May",
|
|
64
|
+
"Jun",
|
|
65
|
+
"Jul",
|
|
66
|
+
"Aug",
|
|
67
|
+
"Sep",
|
|
68
|
+
"Oct",
|
|
69
|
+
"Nov",
|
|
70
|
+
"Dec",
|
|
71
|
+
];
|
|
72
|
+
const month = months[date.getMonth()];
|
|
73
|
+
const year = date.getFullYear();
|
|
74
|
+
const hours = date.getHours().toString().padStart(2, "0");
|
|
75
|
+
const minutes = date.getMinutes().toString().padStart(2, "0");
|
|
76
|
+
return `${day} ${month} ${year} at ${hours}:${minutes}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Formats a date as a short date string.
|
|
81
|
+
* Example: "09 Mar 2026"
|
|
82
|
+
*/
|
|
83
|
+
export function formatShortDate(date: Date): string {
|
|
84
|
+
const day = date.getDate().toString().padStart(2, "0");
|
|
85
|
+
const months = [
|
|
86
|
+
"Jan",
|
|
87
|
+
"Feb",
|
|
88
|
+
"Mar",
|
|
89
|
+
"Apr",
|
|
90
|
+
"May",
|
|
91
|
+
"Jun",
|
|
92
|
+
"Jul",
|
|
93
|
+
"Aug",
|
|
94
|
+
"Sep",
|
|
95
|
+
"Oct",
|
|
96
|
+
"Nov",
|
|
97
|
+
"Dec",
|
|
98
|
+
];
|
|
99
|
+
const month = months[date.getMonth()];
|
|
100
|
+
const year = date.getFullYear();
|
|
101
|
+
return `${day} ${month} ${year}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Formats a date as a full date-time string for display.
|
|
106
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
107
|
+
*/
|
|
108
|
+
export function formatDateTime(date: Date): string {
|
|
109
|
+
return formatFullTimestamp(date);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Formats a countdown duration in ms to "DDd:HHh:MMm:SSs" format.
|
|
114
|
+
* If elapsed is negative (time has passed), counts up (shows elapsed time).
|
|
115
|
+
*/
|
|
116
|
+
export function formatCountdownString(remainingMs: number): string {
|
|
117
|
+
const absMs = Math.abs(remainingMs);
|
|
118
|
+
const totalSeconds = Math.floor(absMs / 1000);
|
|
119
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
120
|
+
const hours = Math.floor((totalSeconds % 86400) / 3600);
|
|
121
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
122
|
+
const seconds = totalSeconds % 60;
|
|
123
|
+
|
|
124
|
+
const dd = days.toString().padStart(2, "0");
|
|
125
|
+
const hh = hours.toString().padStart(2, "0");
|
|
126
|
+
const mm = minutes.toString().padStart(2, "0");
|
|
127
|
+
const ss = seconds.toString().padStart(2, "0");
|
|
128
|
+
|
|
129
|
+
return `${dd}d:${hh}h:${mm}m:${ss}s`;
|
|
130
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
formatCountdownString,
|
|
3
|
+
formatDateTime,
|
|
4
|
+
formatFullTimestamp,
|
|
5
|
+
formatShortDate,
|
|
6
|
+
formatShortRelative,
|
|
7
|
+
formatWeiToEth,
|
|
8
|
+
parseEthToWei,
|
|
9
|
+
} from "./format";
|
|
10
|
+
export type { ProjectedRankResult, SuggestedBidsInput } from "./rank-utils";
|
|
11
|
+
export { getProjectedRankForPrice, getSuggestedBidPrices } from "./rank-utils";
|
|
12
|
+
export {
|
|
13
|
+
getActiveTickSize,
|
|
14
|
+
isValidTickPrice,
|
|
15
|
+
roundDownToValidBid,
|
|
16
|
+
} from "./tick-validation";
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { AuctionTickConfig, RankableBid } from "@/types";
|
|
2
|
+
import { getActiveTickSize, roundDownToValidBid } from "./tick-validation";
|
|
3
|
+
|
|
4
|
+
export interface ProjectedRankResult {
|
|
5
|
+
rank: number | null;
|
|
6
|
+
isWinning: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Projects where a bid at a given price would land in the current rankings.
|
|
11
|
+
* Returns the 1-based rank and whether it would be in the winning range.
|
|
12
|
+
*/
|
|
13
|
+
export function getProjectedRankForPrice(
|
|
14
|
+
priceValue: bigint,
|
|
15
|
+
mergedForRank: RankableBid[],
|
|
16
|
+
maxTotalItems: number,
|
|
17
|
+
): ProjectedRankResult {
|
|
18
|
+
if (priceValue <= 0n) return { rank: null, isWinning: false };
|
|
19
|
+
const synthetic: RankableBid = {
|
|
20
|
+
id: "__synthetic__",
|
|
21
|
+
price: priceValue.toString(),
|
|
22
|
+
created_at: new Date().toISOString(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sorted = [...mergedForRank, synthetic].sort((a, b) => {
|
|
26
|
+
const priceA = BigInt(a.price);
|
|
27
|
+
const priceB = BigInt(b.price);
|
|
28
|
+
if (priceA !== priceB) return priceB > priceA ? 1 : -1;
|
|
29
|
+
|
|
30
|
+
// Tie-breaker: synthetic bid sorts below same-priced bids
|
|
31
|
+
if (a.id === synthetic.id) return 1;
|
|
32
|
+
if (b.id === synthetic.id) return -1;
|
|
33
|
+
|
|
34
|
+
// Regular tie-breaker: earlier bids rank higher
|
|
35
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const idx = sorted.findIndex((bid) => bid.id === synthetic.id);
|
|
39
|
+
if (idx === -1) return { rank: null, isWinning: false };
|
|
40
|
+
|
|
41
|
+
const rank = idx + 1;
|
|
42
|
+
const isWinning = rank <= maxTotalItems;
|
|
43
|
+
return { rank, isWinning };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SuggestedBidsInput {
|
|
47
|
+
mergedForRank: RankableBid[];
|
|
48
|
+
maxTotalItems: number;
|
|
49
|
+
minBidValue: bigint;
|
|
50
|
+
reservePriceValue: bigint;
|
|
51
|
+
tickConfig: AuctionTickConfig | undefined;
|
|
52
|
+
tickSize?: bigint;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generates an array of suggested bid prices based on the current leaderboard state.
|
|
57
|
+
* Returns 1-5 unique suggested prices in ascending order.
|
|
58
|
+
*/
|
|
59
|
+
export function getSuggestedBidPrices({
|
|
60
|
+
mergedForRank,
|
|
61
|
+
maxTotalItems,
|
|
62
|
+
minBidValue,
|
|
63
|
+
reservePriceValue,
|
|
64
|
+
tickConfig,
|
|
65
|
+
tickSize: tickSizeParam,
|
|
66
|
+
}: SuggestedBidsInput): bigint[] {
|
|
67
|
+
const suggestions: bigint[] = [];
|
|
68
|
+
|
|
69
|
+
if (minBidValue <= 0n) {
|
|
70
|
+
return suggestions;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fallbackTick =
|
|
74
|
+
tickSizeParam ?? (reservePriceValue > 0n ? reservePriceValue : minBidValue);
|
|
75
|
+
|
|
76
|
+
const tickAt = (priceValue: bigint): bigint =>
|
|
77
|
+
tickConfig ? getActiveTickSize(priceValue, tickConfig) : fallbackTick;
|
|
78
|
+
|
|
79
|
+
const ensureValid = (value: bigint): bigint =>
|
|
80
|
+
roundDownToValidBid(value, minBidValue, reservePriceValue, tickConfig);
|
|
81
|
+
|
|
82
|
+
// S1: minimum valid bid
|
|
83
|
+
suggestions.push(minBidValue);
|
|
84
|
+
|
|
85
|
+
const winning = mergedForRank.slice(0, maxTotalItems);
|
|
86
|
+
|
|
87
|
+
if (winning.length === 0) {
|
|
88
|
+
// No bids yet: use simple ladder above minBid
|
|
89
|
+
const base = minBidValue;
|
|
90
|
+
const t = tickAt(base);
|
|
91
|
+
suggestions.push(ensureValid(base + t));
|
|
92
|
+
suggestions.push(ensureValid(base + 3n * t));
|
|
93
|
+
suggestions.push(ensureValid(base + 5n * t));
|
|
94
|
+
return dedupeAndSortAscending(suggestions);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const lastWinning = winning[winning.length - 1];
|
|
98
|
+
const topWinning = winning[0];
|
|
99
|
+
|
|
100
|
+
const toValue = (bid: RankableBid): bigint => {
|
|
101
|
+
try {
|
|
102
|
+
return BigInt(bid.price);
|
|
103
|
+
} catch {
|
|
104
|
+
return minBidValue;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const lastWinningValue = toValue(lastWinning);
|
|
109
|
+
const topWinningValue = toValue(topWinning);
|
|
110
|
+
|
|
111
|
+
const upperIdxRaw = Math.floor(winning.length * 0.25);
|
|
112
|
+
const upperIdx = Math.min(
|
|
113
|
+
Math.max(upperIdxRaw, 0),
|
|
114
|
+
Math.max(winning.length - 1, 0),
|
|
115
|
+
);
|
|
116
|
+
const upperMidBid = winning[upperIdx];
|
|
117
|
+
const upperMidValue = toValue(upperMidBid);
|
|
118
|
+
|
|
119
|
+
suggestions.push(ensureValid(lastWinningValue + tickAt(lastWinningValue)));
|
|
120
|
+
suggestions.push(ensureValid(upperMidValue + tickAt(upperMidValue)));
|
|
121
|
+
suggestions.push(ensureValid(topWinningValue + tickAt(topWinningValue)));
|
|
122
|
+
|
|
123
|
+
return dedupeAndSortAscending(suggestions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dedupeAndSortAscending(values: bigint[]): bigint[] {
|
|
127
|
+
const uniq = Array.from(new Set(values.map((v) => v.toString()))).map((v) =>
|
|
128
|
+
BigInt(v),
|
|
129
|
+
);
|
|
130
|
+
return uniq.sort((a, b) => (a === b ? 0 : a > b ? 1 : -1));
|
|
131
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { AuctionTickConfig } from "@/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the tick size to use for +/- steps based on the current bid.
|
|
5
|
+
* Threshold is EXCLUSIVE: bid must be strictly greater than threshold to use large tick.
|
|
6
|
+
*/
|
|
7
|
+
export function getActiveTickSize(
|
|
8
|
+
bidPrice: bigint,
|
|
9
|
+
tickConfig: AuctionTickConfig,
|
|
10
|
+
): bigint {
|
|
11
|
+
return bidPrice > tickConfig.threshold
|
|
12
|
+
? tickConfig.largeTickSize
|
|
13
|
+
: tickConfig.smallTickSize;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a bid price is on the auction's valid tick grid.
|
|
18
|
+
* Which tick applies is determined by the reference price (lowest possible bid:
|
|
19
|
+
* cutoff when full, else reserve), not by the user's bid.
|
|
20
|
+
*/
|
|
21
|
+
export function isValidTickPrice(
|
|
22
|
+
price: bigint,
|
|
23
|
+
reservePrice: bigint,
|
|
24
|
+
tickConfig: AuctionTickConfig | undefined,
|
|
25
|
+
referencePrice: bigint,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (!tickConfig) return true;
|
|
28
|
+
if (price < reservePrice) return false;
|
|
29
|
+
|
|
30
|
+
const { threshold, smallTickSize, largeTickSize } = tickConfig;
|
|
31
|
+
if (smallTickSize === 0n || largeTickSize === 0n) return false;
|
|
32
|
+
|
|
33
|
+
const activeTickSize = getActiveTickSize(referencePrice, tickConfig);
|
|
34
|
+
const base = activeTickSize === smallTickSize ? reservePrice : threshold;
|
|
35
|
+
return (price - base) % activeTickSize === 0n;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns the largest valid tick price that is <= price and >= minBid.
|
|
40
|
+
* Used when setPrice(price) is called so the displayed bid stays on the grid.
|
|
41
|
+
* When tickConfig is missing, returns minBid (single allowed value).
|
|
42
|
+
*/
|
|
43
|
+
export function roundDownToValidBid(
|
|
44
|
+
price: bigint,
|
|
45
|
+
minBid: bigint,
|
|
46
|
+
reservePrice: bigint,
|
|
47
|
+
tickConfig: AuctionTickConfig | undefined,
|
|
48
|
+
): bigint {
|
|
49
|
+
if (price < minBid) return minBid;
|
|
50
|
+
if (!tickConfig) return minBid;
|
|
51
|
+
|
|
52
|
+
const { threshold, smallTickSize, largeTickSize } = tickConfig;
|
|
53
|
+
if (smallTickSize === 0n || largeTickSize === 0n) return minBid;
|
|
54
|
+
|
|
55
|
+
const activeTickSize = getActiveTickSize(price, tickConfig);
|
|
56
|
+
const base =
|
|
57
|
+
activeTickSize === smallTickSize ? reservePrice : threshold + smallTickSize;
|
|
58
|
+
|
|
59
|
+
if (price < base) return minBid;
|
|
60
|
+
|
|
61
|
+
const n = (price - base) / activeTickSize;
|
|
62
|
+
const candidate = base + n * activeTickSize;
|
|
63
|
+
|
|
64
|
+
return candidate >= minBid ? candidate : minBid;
|
|
65
|
+
}
|