Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/components/common/GraduatedCurveMilestoneChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@ export const GraduatedCurveMilestoneChart: React.FC<GraduatedCurveMilestoneChart
exponentChange,
};
}).sort((a, b) => a.supplyThreshold - b.supplyThreshold);
const finalMilestone = normalizedMilestones[normalizedMilestones.length - 1];
const nextMilestone =
currentSupply === undefined
? finalMilestone
: normalizedMilestones.find(m => m.supplyThreshold > currentSupply);
const progressPercent =
currentSupply === undefined || finalMilestone.supplyThreshold <= 0
? 0
: Math.min(100, Math.max(0, (currentSupply / finalMilestone.supplyThreshold) * 100));
const remainingSupply = nextMilestone
? Math.max(0, nextMilestone.supplyThreshold - (currentSupply ?? 0))
: 0;

// Ensure milestone step points for Recharts step chart rendering
const chartData = [...normalizedMilestones];
Expand Down Expand Up @@ -125,6 +137,40 @@ export const GraduatedCurveMilestoneChart: React.FC<GraduatedCurveMilestoneChart
</div>
)}
</div>
<div className="mb-6 rounded-2xl border border-white/10 bg-white/[0.03] p-4" data-testid="milestone-progress-panel">
<div className="flex items-start justify-between gap-4 text-sm">
<div>
<p className="font-semibold text-white">Graduation progress</p>
<p className="mt-1 text-xs text-white/55">
{currentSupply === undefined
? 'Connect live supply data to see your position.'
: nextMilestone
? `${remainingSupply.toLocaleString()} keys until the ${nextMilestone.supplyThreshold.toLocaleString()}-key milestone`
: 'Final milestone reached — the key is graduated.'}
</p>
</div>
<span className="font-semibold text-emerald-400" data-testid="milestone-progress-value">
{Math.round(progressPercent)}%
</span>
</div>
<div
className="mt-3 h-2 overflow-hidden rounded-full bg-white/10"
role="progressbar"
aria-label="Graduated curve progress"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(progressPercent)}
>
<div className="h-full rounded-full bg-emerald-400 transition-[width]" style={{ width: `${progressPercent}%` }} />
</div>
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-white/50">
{normalizedMilestones.map(milestone => (
<span key={milestone.supplyThreshold}>
{milestone.supplyThreshold.toLocaleString()} keys · {milestone.simulatedPrice} XLM
</span>
))}
</div>
</div>

<div
className="w-full relative min-h-[280px]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ describe('GraduatedCurveMilestoneChart', () => {
{ supplyThreshold: 50, exponent: 1.5, simulatedPrice: 5.0, exponentChange: '+0.3' },
{ supplyThreshold: 100, exponent: 2.0, simulatedPrice: 10.0, exponentChange: '+0.5' },
]);
expect(screen.getByTestId('milestone-progress-panel')).toBeInTheDocument();
expect(screen.getByTestId('milestone-progress-value')).toHaveTextContent('45%');
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '45');
expect(screen.getByText('5 keys until the 50-key milestone')).toBeInTheDocument();
});

it('marks each milestone threshold with vertical dashed lines and labels showing exponent change', () => {
Expand Down
15 changes: 10 additions & 5 deletions src/pages/CreatorDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useKeyConfig } from '@/hooks/useKeyConfig';
import KeyStatsPanel from '@/components/common/KeyStatsPanel';
import Skeleton from '@/components/ui/skeleton';
import { Tooltip } from '@/components/ui/tooltip';
import GraduatedCurveMilestoneChart from '@/components/common/GraduatedCurveMilestoneChart';
import KeyDeprecationBanner from '@/components/common/KeyDeprecationBanner';
import MergeProposalBanner from '@/components/common/MergeProposalBanner';
import KeyBuybackModal from '@/components/common/KeyBuybackModal';
Expand Down Expand Up @@ -483,12 +484,16 @@ function CreatorDetailPageContent() {
<h2 className="font-grotesque text-xl font-black tracking-tight text-white mb-6">
Price Curve
</h2>
<BondingCurveChart
data={chartData}
currentSupply={creator.creatorShareSupply ?? 100}
height={300}
/>
<BondingCurveChart
data={chartData}
currentSupply={creator.creatorShareSupply ?? 100}
height={300}
/>
</div>
<GraduatedCurveMilestoneChart
keyId={creator.id}
currentSupply={creator.creatorShareSupply ?? 0}
/>
{/* Buy Simulation Tool */}
<KeySimulationTool
currentSupply={creator.creatorShareSupply ?? 100}
Expand Down
Loading