-
Notifications
You must be signed in to change notification settings - Fork 11
feat: price calculations for waitlist #1180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
6b52f76
feat: add chart
GarageInc 7fc3bb5
feat: anon view for waitlist
GarageInc a593e3e
chore: fix chain id
GarageInc d016d4f
feat: add points and tooltip
GarageInc 6e33cd8
feat: use recharts
GarageInc 047cca0
fix: price chart token symbol
GarageInc 33799a3
chore: applications price
GarageInc 9e1436d
fix: price value
GarageInc 8bd68a9
Merge branch 'dev' into feat/price-calculations
GarageInc adb2f0a
chore: format
GarageInc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,251 @@ | ||
| 'use client'; | ||
|
|
||
| import { useId, useMemo } from 'react'; | ||
| import { | ||
| Area, | ||
| AreaChart, | ||
| CartesianGrid, | ||
| ResponsiveContainer, | ||
| Tooltip, | ||
| XAxis, | ||
| YAxis, | ||
| } from 'recharts'; | ||
| import type { PriceChartPoint } from '../hooks/use-waitlist-price-chart'; | ||
| import { formatEthDecimal } from '@haqq/shell-shared'; | ||
|
|
||
| export interface PriceChartProps { | ||
| data: PriceChartPoint[]; | ||
| /** Chart height in pixels */ | ||
| height?: number; | ||
| /** Whether price values are in atto (wei) - will format for display */ | ||
| priceInAtto?: boolean; | ||
| isLoading?: boolean; | ||
| error?: Error | null; | ||
| } | ||
|
|
||
| const DEFAULT_HEIGHT = 240; | ||
| const ONE_HOUR = 3600; | ||
|
|
||
| /** | ||
| * Expands chart data by adding a point every hour from the last point up to now. | ||
| * So a single API point becomes a line from that time to current time (same price). | ||
| */ | ||
| function expandDataWithHourlyPoints( | ||
| data: Array<{ timestamp: number; price: number }>, | ||
| ): Array<{ timestamp: number; price: number }> { | ||
| if (!data.length) return []; | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const last = data[data.length - 1]!; | ||
| if (last.timestamp >= now) return data; | ||
| const result: PriceChartPoint[] = [...data]; | ||
| for (let t = last.timestamp + ONE_HOUR; t <= now; t += ONE_HOUR) { | ||
| result.push({ timestamp: t, price: last.price }); | ||
| } | ||
| if (result[result.length - 1]!.timestamp < now) { | ||
| result.push({ timestamp: now, price: last.price }); | ||
| } | ||
| return result; | ||
| } | ||
|
|
||
| function formatChartTime(ts: number): string { | ||
| const d = new Date(ts * 1000); | ||
| const now = new Date(); | ||
| const isToday = | ||
| d.getDate() === now.getDate() && | ||
| d.getMonth() === now.getMonth() && | ||
| d.getFullYear() === now.getFullYear(); | ||
| if (isToday) { | ||
| return d.toLocaleTimeString(undefined, { | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| }); | ||
| } | ||
| return d.toLocaleDateString(undefined, { | ||
| month: 'short', | ||
| day: 'numeric', | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Price chart built with Recharts (SVG-based). Web3-style dark card. | ||
| * @see https://recharts.org/ | ||
| */ | ||
| export function PriceChart({ | ||
| data, | ||
| height = DEFAULT_HEIGHT, | ||
| priceInAtto = true, | ||
| isLoading = false, | ||
| error = null, | ||
| }: PriceChartProps) { | ||
| const uid = useId(); | ||
| const expandedData = useMemo(() => expandDataWithHourlyPoints(data), [data]); | ||
|
|
||
| const chartData = useMemo( | ||
| () => | ||
| expandedData.map((d) => ({ | ||
| ...d, | ||
| timeLabel: formatChartTime(d.timestamp), | ||
| })), | ||
| [expandedData], | ||
| ); | ||
|
|
||
| const formatPrice = useMemo( | ||
| () => (p: number) => | ||
| priceInAtto && p > 0 ? formatEthDecimal(BigInt(p), 2, 0) : p.toFixed(2), | ||
| [priceInAtto], | ||
| ); | ||
|
|
||
| const currentPriceFormatted = useMemo(() => { | ||
| if (!chartData.length) return '0'; | ||
| return formatPrice(chartData[chartData.length - 1]!.price); | ||
| }, [chartData, formatPrice]); | ||
|
|
||
| const isSinglePoint = chartData.length === 1; | ||
|
|
||
| const gradientId = `price-chart-gradient-${uid.replace(/:/g, '')}`; | ||
|
|
||
| if (error) { | ||
| return ( | ||
| <div | ||
| className="border-haqq-border bg-haqq-black/80 rounded-xl border p-6 text-center" | ||
| style={{ minHeight: height }} | ||
| > | ||
| <div className="text-haqq-modal-border text-sm"> | ||
| Failed to load price chart | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <div | ||
| className="border-haqq-border bg-haqq-black/50 animate-pulse rounded-xl border" | ||
| style={{ height, width: '100%' }} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| if (!data.length) { | ||
| return ( | ||
| <div | ||
| className="border-haqq-border bg-haqq-black/80 rounded-xl border p-6 text-center" | ||
| style={{ minHeight: height }} | ||
| > | ||
| <div className="text-haqq-modal-border text-sm">No chart data yet</div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="border-haqq-border bg-haqq-black/95 w-full overflow-hidden rounded-xl border shadow-lg"> | ||
| <div className="border-haqq-border flex flex-wrap items-center justify-between gap-2 border-b px-4 py-3"> | ||
| <span className="text-haqq-modal-border text-xs font-medium tracking-wider uppercase"> | ||
| Price history | ||
| </span> | ||
| <div className="flex items-baseline gap-2"> | ||
| {isSinglePoint && ( | ||
| <span className="bg-haqq-seaweed/20 text-haqq-seaweed rounded px-2 py-0.5 text-[10px] font-medium"> | ||
| Holding | ||
| </span> | ||
| )} | ||
| <span className="text-haqq-azure font-mono text-sm font-semibold tabular-nums"> | ||
| {currentPriceFormatted} | ||
| </span> | ||
| <span className="text-haqq-modal-border text-[10px]">ISLM/HAQQ</span> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div | ||
| className="px-2 py-3" | ||
| style={{ width: '100%', height, minHeight: 200 }} | ||
| > | ||
| <ResponsiveContainer width="100%" height="100%"> | ||
| <AreaChart | ||
| data={chartData} | ||
| margin={{ top: 16, right: 24, bottom: 28, left: 16 }} | ||
| > | ||
| <defs> | ||
| <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1"> | ||
| <stop offset="0%" stopColor="#157C83" stopOpacity={0.3} /> | ||
| <stop offset="60%" stopColor="#157C83" stopOpacity={0.08} /> | ||
| <stop offset="100%" stopColor="#157C83" stopOpacity={0} /> | ||
| </linearGradient> | ||
| </defs> | ||
| <CartesianGrid | ||
| strokeDasharray="0" | ||
| stroke="#FFFFFF1A" | ||
| vertical={false} | ||
| /> | ||
| <XAxis | ||
| dataKey="timestamp" | ||
| type="number" | ||
| domain={['dataMin', 'dataMax']} | ||
| tickFormatter={(ts: number) => formatChartTime(ts)} | ||
| stroke="#C5C5C5" | ||
| tick={{ fill: '#C5C5C5', fontSize: 9 }} | ||
| axisLine={false} | ||
| tickLine={false} | ||
| /> | ||
| <YAxis | ||
| dataKey="price" | ||
| type="number" | ||
| domain={['auto', 'auto']} | ||
| tickFormatter={(p: number) => formatPrice(p)} | ||
| stroke="#C5C5C5" | ||
| tick={{ fill: '#C5C5C5', fontSize: 10 }} | ||
| axisLine={false} | ||
| tickLine={false} | ||
| width={56} | ||
| tickMargin={12} | ||
| /> | ||
| <Tooltip | ||
| content={({ | ||
| active, | ||
| payload, | ||
| }: { | ||
| active?: boolean; | ||
| payload?: ReadonlyArray<{ | ||
| payload: { | ||
| timestamp: number; | ||
| price: number; | ||
| timeLabel: string; | ||
| }; | ||
| }>; | ||
| }) => { | ||
| if (!active || !payload?.length) return null; | ||
| const point = payload[0]?.payload as { | ||
| timestamp: number; | ||
| price: number; | ||
| timeLabel: string; | ||
| }; | ||
| if (!point) return null; | ||
| return ( | ||
| <div className="border-haqq-border bg-haqq-black rounded-lg border px-3 py-2 shadow-xl"> | ||
| <div className="text-haqq-modal-border text-[10px]"> | ||
| {point.timeLabel} | ||
| </div> | ||
| <div className="text-haqq-azure font-mono text-sm font-semibold tabular-nums"> | ||
| {formatPrice(point.price)} ISLM | ||
| </div> | ||
| </div> | ||
| ); | ||
| }} | ||
| cursor={{ stroke: '#C5C5C5', strokeWidth: 1 }} | ||
| /> | ||
| <Area | ||
| type="monotone" | ||
| dataKey="price" | ||
| stroke="#157C83" | ||
| strokeWidth={2.5} | ||
| fill={`url(#${gradientId})`} | ||
| isAnimationActive={false} | ||
| /> | ||
| </AreaChart> | ||
| </ResponsiveContainer> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { Button } from '@haqq/shell-ui-kit'; | |
| import { FundsSource } from '../constants/waitlist-config'; | ||
| import type { Application } from '../hooks/use-waitlist-applications'; | ||
| import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances'; | ||
| import { formatEthDecimal } from '@haqq/shell-shared'; | ||
|
|
||
| export interface RequestsListProps { | ||
| applications: Array< | ||
|
|
@@ -100,6 +101,24 @@ export function RequestsList({ | |
| {amount} ISLM | ||
| </span> | ||
| </div> | ||
| {app.price !== undefined && app.price !== '' && ( | ||
| <div> | ||
| Minting price:{' '} | ||
| <span className="font-[500] text-[#0D0D0E]"> | ||
| {formatEthDecimal(BigInt(app.price), 0, 0)} ISLM/HAQQ | ||
| </span> | ||
| </div> | ||
| )} | ||
| {app.receiveAmount !== undefined && | ||
| app.receiveAmount !== '' && ( | ||
| <div> | ||
| Expected receive:{' '} | ||
| <span className="font-[500] text-[#0D0D0E]"> | ||
| {formatEthDecimal(BigInt(app.receiveAmount), 4, 18)}{' '} | ||
| HAQQ | ||
| </span> | ||
| </div> | ||
| )} | ||
| <div> | ||
| Source:{' '} | ||
| <span className="font-medium text-[#0D0D0E]"> | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Check notice
Code scanning / CodeQL
Unused variable, import, function or class Note
Copilot Autofix
AI 8 days ago
In general, the correct fix for an unused import is to remove that import line (or just the unused specifier from a grouped import) so that the code only imports what it actually uses. This avoids confusion and keeps the dependency surface minimal without affecting runtime behavior.
For this specific case in
libs/burn-waitlist/src/lib/components/participation-form.tsx, the best fix is to delete the lineimport { formatUnits } from 'viem';at line 6. No other parts of the file need to change, sinceformatUnitsis not referenced in the props, component body, or other visible code, and we are already usingformatEthDecimalandformatNumberWithSuffixfor formatting. No new methods, imports, or definitions are required.