diff --git a/src/features/locked/components/LockPlanModal.tsx b/src/features/locked/components/LockPlanModal.tsx new file mode 100644 index 0000000..a58723b --- /dev/null +++ b/src/features/locked/components/LockPlanModal.tsx @@ -0,0 +1,85 @@ +import type { Lock } from "../hooks/useLocks"; +import { lockMonths } from "../lib/lockMath"; +import { formatUsdc, formatDate } from "../../../shared/lib/money"; +import DashModal from "../../../shared/dash/DashModal"; +import DashButton from "../../../shared/dash/DashButton"; + +interface Props { + lock: Lock; + onClose: () => void; +} + +/** + * Read-only details for a lock ("View Plan"). Mirrors the create-flow review: + * principal, term, APY, unlock date, projected yield, and the maturity total — + * which is `amount + projected_yield`, since the contract charges no fee. + */ +export default function LockPlanModal({ lock, onClose }: Props) { + const months = lockMonths(lock); + const receive = lock.amount + lock.projected_yield; + + return ( + +

+ View Plan +

+

+ Here are more details about your locked plan. +

+ +
+ + +
+
APY Tier
+
+ + {(lock.apy_basis_points / 100).toFixed( + lock.apy_basis_points % 100 === 0 ? 0 : 1, + )} + % APY + +
+
+ + +
+
+ You receive at maturity +
+
+ {formatUsdc(receive)} USDC +
+
+
+ + + Dismiss + +
+ ); +} + +function Row({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} diff --git a/src/features/locked/components/LockRow.tsx b/src/features/locked/components/LockRow.tsx index ecab00d..6b3c13c 100644 --- a/src/features/locked/components/LockRow.tsx +++ b/src/features/locked/components/LockRow.tsx @@ -23,9 +23,15 @@ interface Props { lock: Lock; nowSecs: number; onUnlock: (lock: Lock) => void; + onViewPlan: (lock: Lock) => void; } -export default function LockRow({ lock, nowSecs, onUnlock }: Props) { +export default function LockRow({ + lock, + nowSecs, + onUnlock, + onViewPlan, +}: Props) { const status = lockStatus(lock, nowSecs); const months = lockMonths(lock); @@ -102,24 +108,31 @@ export default function LockRow({ lock, nowSecs, onUnlock }: Props) { -
+
{status === "matured" ? ( onUnlock(lock)} - className="w-full sm:w-auto" + className="max-sm:flex-1" > Unlock Now ) : status === "locked" ? ( - + Locked · {timeLeftLabel(lock, nowSecs)} ) : ( - + Withdrawn )} +
); diff --git a/src/features/targets/components/GoalPlanModal.tsx b/src/features/targets/components/GoalPlanModal.tsx new file mode 100644 index 0000000..2ebd3cf --- /dev/null +++ b/src/features/targets/components/GoalPlanModal.tsx @@ -0,0 +1,103 @@ +import type { TargetGoal } from "../hooks/useTargets"; +import { + frequencyOf, + isEarlyWithdrawal, + targetStatus, +} from "../lib/targetMath"; +import { + formatUsdc, + formatUsdcAdaptive, + formatDate, +} from "../../../shared/lib/money"; +import DashModal from "../../../shared/dash/DashModal"; +import DashButton from "../../../shared/dash/DashButton"; + +interface Props { + goal: TargetGoal; + nowSecs: number; + /** Open the withdraw payout modal for this goal. */ + onWithdraw: (goal: TargetGoal) => void; + onClose: () => void; +} + +/** + * Read-only details for a goal ("View Plan"), and the entry point for + * withdrawing — the row itself only offers Top Up. Withdrawing before the + * deadline forfeits 1%, so that button is red and leads to the payout modal. + */ +export default function GoalPlanModal({ + goal, + nowSecs, + onWithdraw, + onClose, +}: Props) { + const status = targetStatus(goal, nowSecs); + const early = isEarlyWithdrawal(goal, nowSecs); + const withdrawn = status === "withdrawn"; + + return ( + +

+ View Plan +

+

+ Here are more details about your target savings. +

+ +
+ + + + + +
+
+ Per deposit amount +
+
+ {formatUsdc(goal.period_amount)} USDC +
+
+
+ +
+ {!withdrawn && ( + onWithdraw(goal)} + className="w-full py-3.5" + > + {early ? "Withdraw early" : "Withdraw Now"} + + )} + + Dismiss + +
+
+ ); +} + +function Row({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
+ {value} +
+
+ ); +} diff --git a/src/features/targets/components/TargetRow.tsx b/src/features/targets/components/TargetRow.tsx index cdc8459..bc0b0a7 100644 --- a/src/features/targets/components/TargetRow.tsx +++ b/src/features/targets/components/TargetRow.tsx @@ -32,6 +32,7 @@ interface Props { nowSecs: number; onTopUp: (goal: TargetGoal) => void; onWithdraw: (goal: TargetGoal) => void; + onViewPlan: (goal: TargetGoal) => void; } /* public/dashboard/icons/target-02.svg, inlined so stroke follows currentColor @@ -99,16 +100,16 @@ export default function TargetRow({ nowSecs, onTopUp, onWithdraw, + onViewPlan, }: Props) { const status = targetStatus(goal, nowSecs); if (status === "withdrawn") return ; const pct = progressPct(goal); - const matured = status === "matured"; - - // A goal reaches its target before the deadline still forfeits 1% on - // withdrawal — the forfeit keys off `end_date`, not how full the goal is — so - // that case reads "Withdraw early" rather than "Withdraw Now". + // "Finished" = deadline passed or target fully funded. Topping up a finished + // goal makes no sense, so it swaps Top Up for Withdraw (early ⇒ 1% forfeit, + // hence the red button). + const finished = status === "matured"; const early = isEarlyWithdrawal(goal, nowSecs); return ( @@ -161,38 +162,31 @@ export default function TargetRow({
-
- {matured ? ( +
+ {finished ? ( onWithdraw(goal)} - title={ - early - ? `The contract charges the 1% forfeit until ${formatDateShort(goal.end_date)}.` - : undefined - } - className="w-full sm:w-auto" + className="max-sm:flex-1" > {early ? "Withdraw early" : "Withdraw Now"} ) : ( - <> - onTopUp(goal)} - className="w-full sm:w-auto" - > - Top Up - - onWithdraw(goal)} - className="w-full sm:w-auto" - > - Withdraw early - - + onTopUp(goal)} + className="max-sm:flex-1" + > + Top Up + )} +
); diff --git a/src/pages/dashboard/Locked.tsx b/src/pages/dashboard/Locked.tsx index 017a4e0..28f1612 100644 --- a/src/pages/dashboard/Locked.tsx +++ b/src/pages/dashboard/Locked.tsx @@ -7,6 +7,7 @@ import { lockStatus } from "../../features/locked/lib/lockMath"; import LockRow from "../../features/locked/components/LockRow"; import ProjectedGrowth from "../../features/locked/components/ProjectedGrowth"; import UnlockModal from "../../features/locked/components/UnlockModal"; +import LockPlanModal from "../../features/locked/components/LockPlanModal"; import DashButton from "../../shared/dash/DashButton"; import Spinner from "../../shared/dash/Spinner"; import FilterPills from "../../shared/dash/FilterPills"; @@ -25,6 +26,7 @@ export default function Locked() { const [filter, setFilter] = useState("All"); const [page, setPage] = useState(1); const [unlockTarget, setUnlockTarget] = useState(null); + const [planTarget, setPlanTarget] = useState(null); // Recompute maturity on a timer: a lock can cross its end_date while the page // is open, and nothing else would re-render the row. @@ -152,6 +154,7 @@ export default function Locked() { lock={l} nowSecs={nowSecs} onUnlock={setUnlockTarget} + onViewPlan={setPlanTarget} /> ))} @@ -175,6 +178,10 @@ export default function Locked() { onCancel={() => setUnlockTarget(null)} /> )} + + {planTarget && ( + setPlanTarget(null)} /> + )}
); } diff --git a/src/pages/dashboard/Targets.tsx b/src/pages/dashboard/Targets.tsx index b5f090e..d3d5d81 100644 --- a/src/pages/dashboard/Targets.tsx +++ b/src/pages/dashboard/Targets.tsx @@ -14,6 +14,7 @@ import TargetRow from "../../features/targets/components/TargetRow"; import GoalProjection from "../../features/targets/components/GoalProjection"; import TopUpModal from "../../features/targets/components/TopUpModal"; import WithdrawModal from "../../features/targets/components/WithdrawModal"; +import GoalPlanModal from "../../features/targets/components/GoalPlanModal"; import DashButton from "../../shared/dash/DashButton"; import Spinner from "../../shared/dash/Spinner"; import FilterPills from "../../shared/dash/FilterPills"; @@ -53,6 +54,7 @@ export default function Targets() { const [page, setPage] = useState(1); const [topUpTarget, setTopUpTarget] = useState(null); const [withdrawTarget, setWithdrawTarget] = useState(null); + const [planTarget, setPlanTarget] = useState(null); const [goalYield, setGoalYield] = useState(null); // A goal can cross its deadline while the page is open; nothing else would @@ -205,6 +207,7 @@ export default function Targets() { nowSecs={nowSecs} onTopUp={setTopUpTarget} onWithdraw={openWithdraw} + onViewPlan={setPlanTarget} /> ))} @@ -231,6 +234,18 @@ export default function Targets() { /> )} + {planTarget && ( + { + setPlanTarget(null); + openWithdraw(g); + }} + onClose={() => setPlanTarget(null)} + /> + )} + {withdrawTarget && (