@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
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/cn.ts
|
|
5
|
+
const PRIMITIVE_COLORS = [
|
|
6
|
+
"grey-1",
|
|
7
|
+
"grey-2",
|
|
8
|
+
"grey-3",
|
|
9
|
+
"grey-4",
|
|
10
|
+
"grey-5",
|
|
11
|
+
"grey-6",
|
|
12
|
+
"grey-7",
|
|
13
|
+
"grey-8",
|
|
14
|
+
"grey-9",
|
|
15
|
+
"grey-10",
|
|
16
|
+
"grey-11",
|
|
17
|
+
"grey-12",
|
|
18
|
+
"white",
|
|
19
|
+
"black",
|
|
20
|
+
"current",
|
|
21
|
+
"transparent"
|
|
22
|
+
];
|
|
23
|
+
const SEMANTIC_COLORS = [
|
|
24
|
+
"background",
|
|
25
|
+
"foreground",
|
|
26
|
+
"muted",
|
|
27
|
+
"muted-foreground",
|
|
28
|
+
"card",
|
|
29
|
+
"card-foreground",
|
|
30
|
+
"popover",
|
|
31
|
+
"popover-foreground",
|
|
32
|
+
"primary",
|
|
33
|
+
"primary-hover",
|
|
34
|
+
"primary-foreground",
|
|
35
|
+
"secondary",
|
|
36
|
+
"secondary-hover",
|
|
37
|
+
"secondary-foreground",
|
|
38
|
+
"accent",
|
|
39
|
+
"accent-hover",
|
|
40
|
+
"accent-active",
|
|
41
|
+
"accent-foreground",
|
|
42
|
+
"border",
|
|
43
|
+
"input",
|
|
44
|
+
"ring",
|
|
45
|
+
"disabled",
|
|
46
|
+
"disabled-foreground",
|
|
47
|
+
"destructive",
|
|
48
|
+
"destructive-hover",
|
|
49
|
+
"destructive-foreground",
|
|
50
|
+
"destructive-muted",
|
|
51
|
+
"destructive-muted-foreground",
|
|
52
|
+
"success",
|
|
53
|
+
"success-hover",
|
|
54
|
+
"success-foreground",
|
|
55
|
+
"success-muted",
|
|
56
|
+
"success-muted-foreground",
|
|
57
|
+
"warning",
|
|
58
|
+
"warning-hover",
|
|
59
|
+
"warning-foreground",
|
|
60
|
+
"warning-muted",
|
|
61
|
+
"warning-muted-foreground",
|
|
62
|
+
"separator"
|
|
63
|
+
];
|
|
64
|
+
const THEME_COLORS = [...PRIMITIVE_COLORS, ...SEMANTIC_COLORS];
|
|
65
|
+
const customTwMerge = extendTailwindMerge({ extend: { classGroups: {
|
|
66
|
+
"text-color": [{ text: THEME_COLORS }],
|
|
67
|
+
"bg-color": [{ bg: THEME_COLORS }],
|
|
68
|
+
"border-color": [{ border: THEME_COLORS }]
|
|
69
|
+
} } });
|
|
70
|
+
function cn(...inputs) {
|
|
71
|
+
return customTwMerge(clsx(inputs));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { cn };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Transition } from "motion/react";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/motion.d.ts
|
|
4
|
+
type SpringTransition = {
|
|
5
|
+
type: "spring";
|
|
6
|
+
stiffness: number;
|
|
7
|
+
damping: number;
|
|
8
|
+
mass: number;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Shared spring configurations for consistent animations across the app.
|
|
12
|
+
*/
|
|
13
|
+
declare const springs: Record<string, SpringTransition>;
|
|
14
|
+
/**
|
|
15
|
+
* Common transition presets combining spring with other settings.
|
|
16
|
+
*/
|
|
17
|
+
declare const transitions: Record<string, Transition>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { springs, transitions };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/lib/motion.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared spring configurations for consistent animations across the app.
|
|
4
|
+
*/
|
|
5
|
+
const springs = {
|
|
6
|
+
snappy: {
|
|
7
|
+
type: "spring",
|
|
8
|
+
stiffness: 500,
|
|
9
|
+
damping: 30,
|
|
10
|
+
mass: 1
|
|
11
|
+
},
|
|
12
|
+
bouncy: {
|
|
13
|
+
type: "spring",
|
|
14
|
+
stiffness: 600,
|
|
15
|
+
damping: 20,
|
|
16
|
+
mass: 1
|
|
17
|
+
},
|
|
18
|
+
smooth: {
|
|
19
|
+
type: "spring",
|
|
20
|
+
stiffness: 350,
|
|
21
|
+
damping: 35,
|
|
22
|
+
mass: 1
|
|
23
|
+
},
|
|
24
|
+
quick: {
|
|
25
|
+
type: "spring",
|
|
26
|
+
stiffness: 700,
|
|
27
|
+
damping: 35,
|
|
28
|
+
mass: .8
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Common transition presets combining spring with other settings.
|
|
33
|
+
*/
|
|
34
|
+
const transitions = {
|
|
35
|
+
layout: springs.snappy,
|
|
36
|
+
fade: {
|
|
37
|
+
duration: .15,
|
|
38
|
+
ease: "easeOut"
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { springs, transitions };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//#region src/types/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Tick configuration for the auction's price grid.
|
|
4
|
+
* Prices below the threshold use smallTickSize; prices above use largeTickSize.
|
|
5
|
+
*/
|
|
6
|
+
interface AuctionTickConfig {
|
|
7
|
+
threshold: bigint;
|
|
8
|
+
smallTickSize: bigint;
|
|
9
|
+
largeTickSize: bigint;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Core auction data. All prices are in bigint.
|
|
13
|
+
* This type is fully generic -- no blockchain or framework coupling.
|
|
14
|
+
*/
|
|
15
|
+
interface AuctionData {
|
|
16
|
+
/** Unique identifier for the auction */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Minimum accepted bid price */
|
|
19
|
+
reservePrice: bigint;
|
|
20
|
+
/** Final clearing price after settlement, if finalized */
|
|
21
|
+
clearingPrice?: bigint;
|
|
22
|
+
/** When the auction opens for bidding (null = already open) */
|
|
23
|
+
opensAt: Date | null;
|
|
24
|
+
/** When the auction closes (null = no end time set) */
|
|
25
|
+
endsAt: Date | null;
|
|
26
|
+
/** Maximum number of winning bidders / editions */
|
|
27
|
+
maxTotalItems: number;
|
|
28
|
+
/** Tick configuration for bid price stepping */
|
|
29
|
+
tickConfig?: AuctionTickConfig;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A bidder's public profile. Consumers map their own user type into this.
|
|
33
|
+
*/
|
|
34
|
+
interface AuctionBidder {
|
|
35
|
+
/** Unique user identifier (e.g. wallet address) */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Display name */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Avatar image URL */
|
|
40
|
+
avatarUrl?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A single bid in the auction leaderboard (public view).
|
|
44
|
+
* All prices are in the auction's native unit as bigint.
|
|
45
|
+
*/
|
|
46
|
+
interface AuctionBid {
|
|
47
|
+
/** Unique bid identifier */
|
|
48
|
+
id: string;
|
|
49
|
+
/** Bid price */
|
|
50
|
+
price: bigint;
|
|
51
|
+
/** When the bid was placed */
|
|
52
|
+
createdAt: Date;
|
|
53
|
+
/** The bidder */
|
|
54
|
+
bidder: AuctionBidder;
|
|
55
|
+
/** Link to the bid transaction on a block explorer */
|
|
56
|
+
explorerUrl?: string;
|
|
57
|
+
}
|
|
58
|
+
/** Bid status for user's own bids */
|
|
59
|
+
type AuctionBidStatus = "active" | "refunded" | "claimed";
|
|
60
|
+
/**
|
|
61
|
+
* Extended bid data for the current user's bids.
|
|
62
|
+
* Includes status and claim information.
|
|
63
|
+
*/
|
|
64
|
+
interface AuctionUserBid extends AuctionBid {
|
|
65
|
+
/** On-chain global bid ID (used for top-up) */
|
|
66
|
+
globalBidId: bigint;
|
|
67
|
+
/** Current bid status */
|
|
68
|
+
status: AuctionBidStatus;
|
|
69
|
+
/** Whether this bid is currently in the winning range */
|
|
70
|
+
isWinning: boolean;
|
|
71
|
+
/** When the item was claimed (for winning bids) */
|
|
72
|
+
claimedAt?: Date;
|
|
73
|
+
/** Refund transaction explorer URL */
|
|
74
|
+
refundExplorerUrl?: string;
|
|
75
|
+
}
|
|
76
|
+
/** Status of an async operation (bid placement, top-up, claim) */
|
|
77
|
+
type OperationStatus = "idle" | "pending" | "confirming" | "indexing" | "success" | "error";
|
|
78
|
+
/** Tracks the state of an in-flight operation */
|
|
79
|
+
interface OperationState {
|
|
80
|
+
status: OperationStatus;
|
|
81
|
+
error?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Callbacks that consumers must provide to wire up blockchain/API operations.
|
|
85
|
+
* All return a boolean indicating success/failure.
|
|
86
|
+
*/
|
|
87
|
+
interface AuctionCallbacks {
|
|
88
|
+
/** Place a new bid. Called with price and quantity. */
|
|
89
|
+
onPlaceBid: (price: bigint, quantity: bigint) => Promise<boolean>;
|
|
90
|
+
/** Top up an existing bid to a new price. */
|
|
91
|
+
onTopUpBid: (bidId: bigint, newPrice: bigint, additionalValue: bigint) => Promise<boolean>;
|
|
92
|
+
/** Claim an item for a winning bid. Optional. */
|
|
93
|
+
onClaimEdition?: (bidId: string) => Promise<boolean>;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Formatting functions consumers can provide to customize display.
|
|
97
|
+
* All are optional -- sensible defaults are used when not provided.
|
|
98
|
+
*/
|
|
99
|
+
interface AuctionFormatters {
|
|
100
|
+
/** Format a price value for display. Default: divide by 10^decimals */
|
|
101
|
+
formatPrice?: (priceValue: bigint) => string;
|
|
102
|
+
/** Format a date for display. Default: relative time (e.g. "5m ago") */
|
|
103
|
+
formatTime?: (date: Date) => string;
|
|
104
|
+
/** Currency symbol to display alongside prices. Default: "USD" */
|
|
105
|
+
currencySymbol?: string;
|
|
106
|
+
/** Number of decimal places used when editing bigint-backed prices. */
|
|
107
|
+
inputDecimals?: number;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Lightweight bid representation used internally for rank calculations.
|
|
111
|
+
* Consumers don't need to use this directly -- it's derived from AuctionBid.
|
|
112
|
+
*/
|
|
113
|
+
interface RankableBid {
|
|
114
|
+
id: string;
|
|
115
|
+
price: string;
|
|
116
|
+
created_at: string;
|
|
117
|
+
bidder?: AuctionBidder;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { AuctionBid, AuctionBidStatus, AuctionBidder, AuctionCallbacks, AuctionData, AuctionFormatters, AuctionTickConfig, AuctionUserBid, OperationState, OperationStatus, RankableBid };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
//#region src/utils/format.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default price formatter: converts wei (bigint) to a human-readable ETH string.
|
|
4
|
+
* Uses native bigint arithmetic -- no external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
declare function formatWeiToEth(wei: bigint, decimals?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Parses an ETH string (e.g. "0.01") to wei (bigint).
|
|
9
|
+
* Inverse of formatWeiToEth.
|
|
10
|
+
*/
|
|
11
|
+
declare function parseEthToWei(eth: string, decimals?: number): bigint;
|
|
12
|
+
/**
|
|
13
|
+
* Formats a date as a short relative time string.
|
|
14
|
+
* Examples: "now", "5m", "2h", "3d", "2w"
|
|
15
|
+
*/
|
|
16
|
+
declare function formatShortRelative(date: Date): string;
|
|
17
|
+
/**
|
|
18
|
+
* Formats a date as a full timestamp string.
|
|
19
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
20
|
+
*/
|
|
21
|
+
declare function formatFullTimestamp(date: Date): string;
|
|
22
|
+
/**
|
|
23
|
+
* Formats a date as a short date string.
|
|
24
|
+
* Example: "09 Mar 2026"
|
|
25
|
+
*/
|
|
26
|
+
declare function formatShortDate(date: Date): string;
|
|
27
|
+
/**
|
|
28
|
+
* Formats a date as a full date-time string for display.
|
|
29
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
30
|
+
*/
|
|
31
|
+
declare function formatDateTime(date: Date): string;
|
|
32
|
+
/**
|
|
33
|
+
* Formats a countdown duration in ms to "DDd:HHh:MMm:SSs" format.
|
|
34
|
+
* If elapsed is negative (time has passed), counts up (shows elapsed time).
|
|
35
|
+
*/
|
|
36
|
+
declare function formatCountdownString(remainingMs: number): string;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { formatCountdownString, formatDateTime, formatFullTimestamp, formatShortDate, formatShortRelative, formatWeiToEth, parseEthToWei };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//#region src/utils/format.ts
|
|
2
|
+
/**
|
|
3
|
+
* Default price formatter: converts wei (bigint) to a human-readable ETH string.
|
|
4
|
+
* Uses native bigint arithmetic -- no external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
function formatWeiToEth(wei, decimals = 18) {
|
|
7
|
+
const isNegative = wei < 0n;
|
|
8
|
+
const abs = isNegative ? -wei : wei;
|
|
9
|
+
const divisor = 10n ** BigInt(decimals);
|
|
10
|
+
const whole = abs / divisor;
|
|
11
|
+
const remainder = abs % divisor;
|
|
12
|
+
if (remainder === 0n) return `${isNegative ? "-" : ""}${whole.toString()}`;
|
|
13
|
+
const trimmed = remainder.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
14
|
+
return `${isNegative ? "-" : ""}${whole.toString()}.${trimmed}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parses an ETH string (e.g. "0.01") to wei (bigint).
|
|
18
|
+
* Inverse of formatWeiToEth.
|
|
19
|
+
*/
|
|
20
|
+
function parseEthToWei(eth, decimals = 18) {
|
|
21
|
+
const [wholePart = "0", fracPart = ""] = eth.split(".");
|
|
22
|
+
const paddedFrac = fracPart.padEnd(decimals, "0").slice(0, decimals);
|
|
23
|
+
return BigInt(wholePart) * 10n ** BigInt(decimals) + BigInt(paddedFrac);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Formats a date as a short relative time string.
|
|
27
|
+
* Examples: "now", "5m", "2h", "3d", "2w"
|
|
28
|
+
*/
|
|
29
|
+
function formatShortRelative(date) {
|
|
30
|
+
const diff = Date.now() - date.getTime();
|
|
31
|
+
const secs = Math.floor(diff / 1e3);
|
|
32
|
+
if (secs < 60) return "now";
|
|
33
|
+
const mins = Math.floor(secs / 60);
|
|
34
|
+
if (mins < 60) return `${mins}m`;
|
|
35
|
+
const hours = Math.floor(mins / 60);
|
|
36
|
+
if (hours < 24) return `${hours}h`;
|
|
37
|
+
const days = Math.floor(hours / 24);
|
|
38
|
+
if (days < 7) return `${days}d`;
|
|
39
|
+
return `${Math.min(Math.floor(days / 7), 99)}w`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Formats a date as a full timestamp string.
|
|
43
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
44
|
+
*/
|
|
45
|
+
function formatFullTimestamp(date) {
|
|
46
|
+
return `${date.getDate().toString().padStart(2, "0")} ${[
|
|
47
|
+
"Jan",
|
|
48
|
+
"Feb",
|
|
49
|
+
"Mar",
|
|
50
|
+
"Apr",
|
|
51
|
+
"May",
|
|
52
|
+
"Jun",
|
|
53
|
+
"Jul",
|
|
54
|
+
"Aug",
|
|
55
|
+
"Sep",
|
|
56
|
+
"Oct",
|
|
57
|
+
"Nov",
|
|
58
|
+
"Dec"
|
|
59
|
+
][date.getMonth()]} ${date.getFullYear()} at ${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Formats a date as a short date string.
|
|
63
|
+
* Example: "09 Mar 2026"
|
|
64
|
+
*/
|
|
65
|
+
function formatShortDate(date) {
|
|
66
|
+
return `${date.getDate().toString().padStart(2, "0")} ${[
|
|
67
|
+
"Jan",
|
|
68
|
+
"Feb",
|
|
69
|
+
"Mar",
|
|
70
|
+
"Apr",
|
|
71
|
+
"May",
|
|
72
|
+
"Jun",
|
|
73
|
+
"Jul",
|
|
74
|
+
"Aug",
|
|
75
|
+
"Sep",
|
|
76
|
+
"Oct",
|
|
77
|
+
"Nov",
|
|
78
|
+
"Dec"
|
|
79
|
+
][date.getMonth()]} ${date.getFullYear()}`;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Formats a date as a full date-time string for display.
|
|
83
|
+
* Example: "09 Mar 2026 at 14:30"
|
|
84
|
+
*/
|
|
85
|
+
function formatDateTime(date) {
|
|
86
|
+
return formatFullTimestamp(date);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Formats a countdown duration in ms to "DDd:HHh:MMm:SSs" format.
|
|
90
|
+
* If elapsed is negative (time has passed), counts up (shows elapsed time).
|
|
91
|
+
*/
|
|
92
|
+
function formatCountdownString(remainingMs) {
|
|
93
|
+
const absMs = Math.abs(remainingMs);
|
|
94
|
+
const totalSeconds = Math.floor(absMs / 1e3);
|
|
95
|
+
const days = Math.floor(totalSeconds / 86400);
|
|
96
|
+
const hours = Math.floor(totalSeconds % 86400 / 3600);
|
|
97
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
98
|
+
const seconds = totalSeconds % 60;
|
|
99
|
+
return `${days.toString().padStart(2, "0")}d:${hours.toString().padStart(2, "0")}h:${minutes.toString().padStart(2, "0")}m:${seconds.toString().padStart(2, "0")}s`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
export { formatCountdownString, formatDateTime, formatFullTimestamp, formatShortDate, formatShortRelative, formatWeiToEth, parseEthToWei };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AuctionTickConfig, RankableBid } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/rank-utils.d.ts
|
|
4
|
+
interface ProjectedRankResult {
|
|
5
|
+
rank: number | null;
|
|
6
|
+
isWinning: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Projects where a bid at a given price would land in the current rankings.
|
|
10
|
+
* Returns the 1-based rank and whether it would be in the winning range.
|
|
11
|
+
*/
|
|
12
|
+
declare function getProjectedRankForPrice(priceValue: bigint, mergedForRank: RankableBid[], maxTotalItems: number): ProjectedRankResult;
|
|
13
|
+
interface SuggestedBidsInput {
|
|
14
|
+
mergedForRank: RankableBid[];
|
|
15
|
+
maxTotalItems: number;
|
|
16
|
+
minBidValue: bigint;
|
|
17
|
+
reservePriceValue: bigint;
|
|
18
|
+
tickConfig: AuctionTickConfig | undefined;
|
|
19
|
+
tickSize?: bigint;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generates an array of suggested bid prices based on the current leaderboard state.
|
|
23
|
+
* Returns 1-5 unique suggested prices in ascending order.
|
|
24
|
+
*/
|
|
25
|
+
declare function getSuggestedBidPrices({
|
|
26
|
+
mergedForRank,
|
|
27
|
+
maxTotalItems,
|
|
28
|
+
minBidValue,
|
|
29
|
+
reservePriceValue,
|
|
30
|
+
tickConfig,
|
|
31
|
+
tickSize: tickSizeParam
|
|
32
|
+
}: SuggestedBidsInput): bigint[];
|
|
33
|
+
//#endregion
|
|
34
|
+
export { getProjectedRankForPrice, getSuggestedBidPrices };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getActiveTickSize, roundDownToValidBid } from "./tick-validation.js";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/rank-utils.ts
|
|
4
|
+
/**
|
|
5
|
+
* Projects where a bid at a given price would land in the current rankings.
|
|
6
|
+
* Returns the 1-based rank and whether it would be in the winning range.
|
|
7
|
+
*/
|
|
8
|
+
function getProjectedRankForPrice(priceValue, mergedForRank, maxTotalItems) {
|
|
9
|
+
if (priceValue <= 0n) return {
|
|
10
|
+
rank: null,
|
|
11
|
+
isWinning: false
|
|
12
|
+
};
|
|
13
|
+
const synthetic = {
|
|
14
|
+
id: "__synthetic__",
|
|
15
|
+
price: priceValue.toString(),
|
|
16
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
17
|
+
};
|
|
18
|
+
const idx = [...mergedForRank, synthetic].sort((a, b) => {
|
|
19
|
+
const priceA = BigInt(a.price);
|
|
20
|
+
const priceB = BigInt(b.price);
|
|
21
|
+
if (priceA !== priceB) return priceB > priceA ? 1 : -1;
|
|
22
|
+
if (a.id === synthetic.id) return 1;
|
|
23
|
+
if (b.id === synthetic.id) return -1;
|
|
24
|
+
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
|
25
|
+
}).findIndex((bid) => bid.id === synthetic.id);
|
|
26
|
+
if (idx === -1) return {
|
|
27
|
+
rank: null,
|
|
28
|
+
isWinning: false
|
|
29
|
+
};
|
|
30
|
+
const rank = idx + 1;
|
|
31
|
+
return {
|
|
32
|
+
rank,
|
|
33
|
+
isWinning: rank <= maxTotalItems
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Generates an array of suggested bid prices based on the current leaderboard state.
|
|
38
|
+
* Returns 1-5 unique suggested prices in ascending order.
|
|
39
|
+
*/
|
|
40
|
+
function getSuggestedBidPrices({ mergedForRank, maxTotalItems, minBidValue, reservePriceValue, tickConfig, tickSize: tickSizeParam }) {
|
|
41
|
+
const suggestions = [];
|
|
42
|
+
if (minBidValue <= 0n) return suggestions;
|
|
43
|
+
const fallbackTick = tickSizeParam ?? (reservePriceValue > 0n ? reservePriceValue : minBidValue);
|
|
44
|
+
const tickAt = (priceValue) => tickConfig ? getActiveTickSize(priceValue, tickConfig) : fallbackTick;
|
|
45
|
+
const ensureValid = (value) => roundDownToValidBid(value, minBidValue, reservePriceValue, tickConfig);
|
|
46
|
+
suggestions.push(minBidValue);
|
|
47
|
+
const winning = mergedForRank.slice(0, maxTotalItems);
|
|
48
|
+
if (winning.length === 0) {
|
|
49
|
+
const base = minBidValue;
|
|
50
|
+
const t = tickAt(base);
|
|
51
|
+
suggestions.push(ensureValid(base + t));
|
|
52
|
+
suggestions.push(ensureValid(base + 3n * t));
|
|
53
|
+
suggestions.push(ensureValid(base + 5n * t));
|
|
54
|
+
return dedupeAndSortAscending(suggestions);
|
|
55
|
+
}
|
|
56
|
+
const lastWinning = winning[winning.length - 1];
|
|
57
|
+
const topWinning = winning[0];
|
|
58
|
+
const toValue = (bid) => {
|
|
59
|
+
try {
|
|
60
|
+
return BigInt(bid.price);
|
|
61
|
+
} catch {
|
|
62
|
+
return minBidValue;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const lastWinningValue = toValue(lastWinning);
|
|
66
|
+
const topWinningValue = toValue(topWinning);
|
|
67
|
+
const upperIdxRaw = Math.floor(winning.length * .25);
|
|
68
|
+
const upperMidBid = winning[Math.min(Math.max(upperIdxRaw, 0), Math.max(winning.length - 1, 0))];
|
|
69
|
+
const upperMidValue = toValue(upperMidBid);
|
|
70
|
+
suggestions.push(ensureValid(lastWinningValue + tickAt(lastWinningValue)));
|
|
71
|
+
suggestions.push(ensureValid(upperMidValue + tickAt(upperMidValue)));
|
|
72
|
+
suggestions.push(ensureValid(topWinningValue + tickAt(topWinningValue)));
|
|
73
|
+
return dedupeAndSortAscending(suggestions);
|
|
74
|
+
}
|
|
75
|
+
function dedupeAndSortAscending(values) {
|
|
76
|
+
return Array.from(new Set(values.map((v) => v.toString()))).map((v) => BigInt(v)).sort((a, b) => a === b ? 0 : a > b ? 1 : -1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { getProjectedRankForPrice, getSuggestedBidPrices };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { AuctionTickConfig } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
//#region src/utils/tick-validation.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Returns the tick size to use for +/- steps based on the current bid.
|
|
6
|
+
* Threshold is EXCLUSIVE: bid must be strictly greater than threshold to use large tick.
|
|
7
|
+
*/
|
|
8
|
+
declare function getActiveTickSize(bidPrice: bigint, tickConfig: AuctionTickConfig): bigint;
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a bid price is on the auction's valid tick grid.
|
|
11
|
+
* Which tick applies is determined by the reference price (lowest possible bid:
|
|
12
|
+
* cutoff when full, else reserve), not by the user's bid.
|
|
13
|
+
*/
|
|
14
|
+
declare function isValidTickPrice(price: bigint, reservePrice: bigint, tickConfig: AuctionTickConfig | undefined, referencePrice: bigint): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Returns the largest valid tick price that is <= price and >= minBid.
|
|
17
|
+
* Used when setPrice(price) is called so the displayed bid stays on the grid.
|
|
18
|
+
* When tickConfig is missing, returns minBid (single allowed value).
|
|
19
|
+
*/
|
|
20
|
+
declare function roundDownToValidBid(price: bigint, minBid: bigint, reservePrice: bigint, tickConfig: AuctionTickConfig | undefined): bigint;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { getActiveTickSize, isValidTickPrice, roundDownToValidBid };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/utils/tick-validation.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns the tick size to use for +/- steps based on the current bid.
|
|
4
|
+
* Threshold is EXCLUSIVE: bid must be strictly greater than threshold to use large tick.
|
|
5
|
+
*/
|
|
6
|
+
function getActiveTickSize(bidPrice, tickConfig) {
|
|
7
|
+
return bidPrice > tickConfig.threshold ? tickConfig.largeTickSize : tickConfig.smallTickSize;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a bid price is on the auction's valid tick grid.
|
|
11
|
+
* Which tick applies is determined by the reference price (lowest possible bid:
|
|
12
|
+
* cutoff when full, else reserve), not by the user's bid.
|
|
13
|
+
*/
|
|
14
|
+
function isValidTickPrice(price, reservePrice, tickConfig, referencePrice) {
|
|
15
|
+
if (!tickConfig) return true;
|
|
16
|
+
if (price < reservePrice) return false;
|
|
17
|
+
const { threshold, smallTickSize, largeTickSize } = tickConfig;
|
|
18
|
+
if (smallTickSize === 0n || largeTickSize === 0n) return false;
|
|
19
|
+
const activeTickSize = getActiveTickSize(referencePrice, tickConfig);
|
|
20
|
+
return (price - (activeTickSize === smallTickSize ? reservePrice : threshold)) % activeTickSize === 0n;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Returns the largest valid tick price that is <= price and >= minBid.
|
|
24
|
+
* Used when setPrice(price) is called so the displayed bid stays on the grid.
|
|
25
|
+
* When tickConfig is missing, returns minBid (single allowed value).
|
|
26
|
+
*/
|
|
27
|
+
function roundDownToValidBid(price, minBid, reservePrice, tickConfig) {
|
|
28
|
+
if (price < minBid) return minBid;
|
|
29
|
+
if (!tickConfig) return minBid;
|
|
30
|
+
const { threshold, smallTickSize, largeTickSize } = tickConfig;
|
|
31
|
+
if (smallTickSize === 0n || largeTickSize === 0n) return minBid;
|
|
32
|
+
const activeTickSize = getActiveTickSize(price, tickConfig);
|
|
33
|
+
const base = activeTickSize === smallTickSize ? reservePrice : threshold + smallTickSize;
|
|
34
|
+
if (price < base) return minBid;
|
|
35
|
+
const candidate = base + (price - base) / activeTickSize * activeTickSize;
|
|
36
|
+
return candidate >= minBid ? candidate : minBid;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { getActiveTickSize, isValidTickPrice, roundDownToValidBid };
|
package/package.json
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@m3000/market",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles.css": "./src/styles/index.css",
|
|
14
|
+
"./stories/*": "./src/stories/*"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": [
|
|
17
|
+
"*.css"
|
|
18
|
+
],
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@base-ui/react": "1.3.0",
|
|
28
|
+
"@base-ui/utils": "0.2.5",
|
|
29
|
+
"@floating-ui/core": "1.7.5",
|
|
30
|
+
"@floating-ui/dom": "1.7.6",
|
|
31
|
+
"@floating-ui/react-dom": "2.1.8",
|
|
32
|
+
"@floating-ui/utils": "0.2.11",
|
|
33
|
+
"@tabler/icons-react": "3.40.0",
|
|
34
|
+
"class-variance-authority": "0.7.1",
|
|
35
|
+
"clsx": "2.1.1",
|
|
36
|
+
"motion": "12.35.2",
|
|
37
|
+
"reselect": "5.1.1",
|
|
38
|
+
"tabbable": "6.4.0",
|
|
39
|
+
"tailwind-merge": "3.3.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"react": "19.2.4",
|
|
43
|
+
"react-dom": "19.2.4",
|
|
44
|
+
"tailwindcss": "4.2.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "2.2.0",
|
|
48
|
+
"@storybook/addon-a11y": "10.1.2",
|
|
49
|
+
"@storybook/addon-docs": "10.1.2",
|
|
50
|
+
"@storybook/addon-themes": "10.1.2",
|
|
51
|
+
"@storybook/react": "10.1.2",
|
|
52
|
+
"@storybook/react-vite": "10.1.2",
|
|
53
|
+
"@tailwindcss/postcss": "4.1.11",
|
|
54
|
+
"@tsconfig/vite-react": "7.0.2",
|
|
55
|
+
"@types/node": "22.14.1",
|
|
56
|
+
"@types/react": "19.2.2",
|
|
57
|
+
"@types/react-dom": "19.2.3",
|
|
58
|
+
"react": "19.2.4",
|
|
59
|
+
"react-docgen-typescript": "2.4.0",
|
|
60
|
+
"react-dom": "19.2.4",
|
|
61
|
+
"storybook": "10.1.2",
|
|
62
|
+
"tailwindcss": "4.2.1",
|
|
63
|
+
"tsdown": "0.20.3",
|
|
64
|
+
"typescript": "5.9.2",
|
|
65
|
+
"typescript-eslint": "8.46.4",
|
|
66
|
+
"vite-tsconfig-paths": "5.1.4"
|
|
67
|
+
},
|
|
68
|
+
"peerDependenciesMeta": {
|
|
69
|
+
"react": {
|
|
70
|
+
"optional": false
|
|
71
|
+
},
|
|
72
|
+
"@types/react": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@types/react-dom": {
|
|
76
|
+
"optional": true
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsdown",
|
|
81
|
+
"build:storybook": "storybook build",
|
|
82
|
+
"build:watch": "tsdown --watch",
|
|
83
|
+
"dev": "tsdown --watch",
|
|
84
|
+
"format": "biome format .",
|
|
85
|
+
"format:fix": "biome format --write .",
|
|
86
|
+
"lint": "biome lint .",
|
|
87
|
+
"lint:fix": "biome lint --write .",
|
|
88
|
+
"quality": "pnpm run lint && pnpm run format && pnpm run typecheck",
|
|
89
|
+
"storybook": "storybook dev -p 6007",
|
|
90
|
+
"typecheck": "tsc --noEmit"
|
|
91
|
+
}
|
|
92
|
+
}
|