hoodquidity/apps/web/src/components/PositionPanel.tsx
maxgort 399610bcc8
All checks were successful
deploy / deploy (push) Successful in 1m18s
revert: restore the d6e38d2 showcase, keep only the $500 minimum deposit
Remove the client-side simulated vault and all its wiring (simVault.ts,
AppBoard/DepositPanel/PositionPanel/useCampaigns sim paths, the balance/
faucet changes) and restore the original local dev addresses. The only
change retained over d6e38d2 is the $500 minimum-deposit rule: the
MIN_DEPOSIT/BelowMinimum contract enforcement plus the "Min. deposit $500"
frontend validation. Working tree now equals d6e38d2 + that feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 21:41:12 +03:00

163 lines
5.3 KiB
TypeScript

import { parseUnits } from "viem";
import { useWaitForTransactionReceipt, useWriteContract } from "wagmi";
import { hoodquidityVaultsAbi } from "@/lib/abi/HoodquidityVaults";
import { ACTIVE_CHAIN_ID, getActiveDeployment, USD_DECIMALS } from "@/lib/contracts";
import { fmtUsd, fmtCountdown, fmtNum } from "@/lib/format";
import { campaignPoints, isGenesisLp } from "@/lib/points";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { CountUp } from "@/components/CountUp";
import type { CampaignState } from "@/hooks/useCampaigns";
import type { usePosition } from "@/hooks/useCampaigns";
type Position = NonNullable<ReturnType<typeof usePosition>["position"]>;
export function PositionPanel({
campaign,
position,
onSettled,
}: {
campaign: CampaignState;
position: Position;
onSettled: () => void;
}) {
const deployment = getActiveDeployment();
const { writeContract, data: txHash, isPending, reset } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash: txHash,
});
if (isSuccess) {
reset();
onSettled();
}
const busy = isPending || isConfirming;
const ended = campaign.status === "ended";
const id = BigInt(campaign.meta.campaignId);
const exit = () =>
writeContract({
address: deployment.vaults,
abi: hoodquidityVaultsAbi,
functionName: "withdraw",
args: [id, parseUnits(position.amountUsd.toString(), USD_DECIMALS)],
chainId: ACTIVE_CHAIN_ID,
});
const claim = () =>
writeContract({
address: deployment.vaults,
abi: hoodquidityVaultsAbi,
functionName: "claim",
args: [id],
chainId: ACTIVE_CHAIN_ID,
});
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="slabel">LP receipt</div>
<div className="flex gap-2">
{isGenesisLp(position.lpIndex) && <Badge tone="mint">Genesis LP</Badge>}
{position.boosted && <Badge tone="lime">1.5x boosted</Badge>}
</div>
</div>
<div className="space-y-2.5 text-sm">
<div className="leader">
<span className="text-fog">Deposited</span>
<span className="num font-semibold">{fmtUsd(position.amountUsd, false)}</span>
</div>
<div className="leader">
<span className="text-fog">Vault share</span>
<span className="num font-semibold">
{campaign.tvlUsd > 0
? `${((position.amountUsd / campaign.tvlUsd) * 100).toFixed(1)}%`
: "0%"}
</span>
</div>
<div className="leader">
<span className="text-fog">Rewards accrued</span>
<span className="num font-semibold text-mint">
{fmtUsd(position.pendingUsd, false)}
</span>
</div>
<div className="leader">
<span className="text-fog">Rewards claimed</span>
<span className="num font-semibold">{fmtUsd(position.claimedUsd, false)}</span>
</div>
{position.stockAmount > 0 && (
<div className="leader">
<span className="text-fog">Stock leg</span>
<span className="num font-semibold">
{position.stockAmount.toFixed(4)} {campaign.meta.symbol}
</span>
</div>
)}
<div className="leader">
<span className="text-fog">Points</span>
<span className="num font-semibold text-lime">
<CountUp
value={campaignPoints({
amountUsd: position.amountUsd,
depositedAt: position.depositedAt,
boosted: position.boosted,
})}
format={(n) => fmtNum(Math.floor(n))}
/>
</span>
</div>
<div className="leader">
<span className="text-fog">Full rewards unlock</span>
<span className="num font-semibold">
{ended ? "Unlocked" : fmtCountdown(campaign.end)}
</span>
</div>
</div>
<div className="flex gap-3 pt-1">
{ended ? (
<>
<Button
className="flex-1"
disabled={busy || position.pendingUsd === 0}
onClick={claim}
>
{busy ? "Confirming..." : "Claim rewards"}
</Button>
<Button
className="flex-1"
variant="outline"
disabled={busy || position.amountUsd === 0}
onClick={exit}
>
Withdraw all
</Button>
</>
) : (
<Button
className="flex-1"
variant="danger"
disabled={busy || position.amountUsd === 0}
onClick={exit}
>
{busy ? "Confirming..." : "Exit early"}
</Button>
)}
</div>
{!ended ? (
<p className="text-[11px] leading-relaxed text-fog-dim">
Early exit returns your principal minus a 0.1% fee. Accrued campaign
rewards are forfeited and redistributed to remaining LPs.
</p>
) : (
<p className="text-[11px] leading-relaxed text-fog-dim">
Reward payouts carry a 10% platform fee. Points have no monetary
value and feed future programs.
</p>
)}
</div>
);
}