this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(fire): init

+228 -640
+5 -5
pnpm-lock.yaml
··· 1787 1787 resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 1788 1788 engines: {node: ^10 || ^12 || >=14} 1789 1789 1790 - postcss@8.5.9: 1791 - resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} 1790 + postcss@8.5.10: 1791 + resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==} 1792 1792 engines: {node: ^10 || ^12 || >=14} 1793 1793 1794 1794 powershell-utils@0.1.0: ··· 2821 2821 '@alloc/quick-lru': 5.2.0 2822 2822 '@tailwindcss/node': 4.2.2 2823 2823 '@tailwindcss/oxide': 4.2.2 2824 - postcss: 8.5.9 2824 + postcss: 8.5.10 2825 2825 tailwindcss: 4.2.2 2826 2826 2827 2827 '@ts-morph/common@0.27.0': ··· 3733 3733 picocolors: 1.1.1 3734 3734 source-map-js: 1.2.1 3735 3735 3736 - postcss@8.5.9: 3736 + postcss@8.5.10: 3737 3737 dependencies: 3738 3738 nanoid: 3.3.11 3739 3739 picocolors: 1.1.1 ··· 3916 3916 node-fetch: 3.3.2 3917 3917 open: 11.0.0 3918 3918 ora: 8.2.0 3919 - postcss: 8.5.9 3919 + postcss: 8.5.10 3920 3920 postcss-selector-parser: 7.1.1 3921 3921 prompts: 2.4.2 3922 3922 recast: 0.23.11
-186
src/app/calculator/page.tsx
··· 1 - "use client"; 2 - 3 - import { useEffect, useState } from "react"; 4 - import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts"; 5 - import { 6 - Card, 7 - CardContent, 8 - CardDescription, 9 - CardHeader, 10 - CardTitle, 11 - } from "@/components/ui/card"; 12 - import type { ChartConfig } from "@/components/ui/chart"; 13 - import { 14 - ChartContainer, 15 - ChartTooltip, 16 - ChartTooltipContent, 17 - } from "@/components/ui/chart"; 18 - import { 19 - InputGroup, 20 - InputGroupAddon, 21 - InputGroupInput, 22 - } from "@/components/ui/input-group"; 23 - import { Label } from "@/components/ui/label"; 24 - 25 - const chartConfig = { 26 - openingBalance: { label: "Principal + Contribution", color: "#3b82f6" }, 27 - interest: { label: "Interest", color: "#14b8a6" }, 28 - } satisfies ChartConfig; 29 - 30 - interface GrowthHistory { 31 - year: number; 32 - interest: number; 33 - openingBalance: number; 34 - balance: number; 35 - } 36 - 37 - const calculateYearlyGrowth = ( 38 - principal: number, 39 - pmt: number, 40 - rate: number, 41 - years: number, 42 - ): GrowthHistory[] => { 43 - const r = rate / 100; 44 - let balance = principal; 45 - const history: GrowthHistory[] = []; 46 - 47 - for (let year = 1; year <= years; year++) { 48 - const openingBalance = balance + pmt; 49 - const interest = openingBalance * r; 50 - balance = openingBalance + interest; 51 - history.push({ 52 - year, 53 - interest: parseFloat(interest.toFixed(2)), 54 - openingBalance: parseFloat(openingBalance.toFixed(2)), 55 - balance: parseFloat(balance.toFixed(2)), 56 - }); 57 - } 58 - return history; 59 - }; 60 - 61 - export default function CalculatorPage() { 62 - const [principal, setPrincipal] = useState("1000"); 63 - const [pmt, setPmt] = useState("500"); 64 - const [rate, setRate] = useState("7"); 65 - const [years, setYears] = useState("5"); 66 - const [history, setHistory] = useState<GrowthHistory[]>([]); 67 - 68 - useEffect(() => { 69 - const p = parseFloat(principal); 70 - const m = parseFloat(pmt); 71 - const r = parseFloat(rate); 72 - const y = parseInt(years, 10); 73 - 74 - if ( 75 - !Number.isNaN(p) && 76 - !Number.isNaN(m) && 77 - !Number.isNaN(r) && 78 - !Number.isNaN(y) && 79 - y > 0 80 - ) { 81 - setHistory(calculateYearlyGrowth(p, m, r, y)); 82 - } else { 83 - setHistory([]); 84 - } 85 - }, [principal, pmt, rate, years]); 86 - 87 - return ( 88 - <main className="flex flex-1 items-center justify-center p-4"> 89 - <Card className="w-full max-w-4xl"> 90 - <CardHeader> 91 - <CardTitle>Growth Calculator</CardTitle> 92 - <CardDescription> 93 - Project the growth of your investment over time, year by year. 94 - </CardDescription> 95 - </CardHeader> 96 - <CardContent> 97 - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"> 98 - <div className="space-y-2"> 99 - <Label htmlFor="principal">Principal</Label> 100 - <InputGroup> 101 - <InputGroupAddon>$</InputGroupAddon> 102 - <InputGroupInput 103 - id="principal" 104 - type="number" 105 - value={principal} 106 - onChange={(e) => setPrincipal(e.target.value)} 107 - /> 108 - </InputGroup> 109 - </div> 110 - <div className="space-y-2"> 111 - <Label htmlFor="pmt">Annual Contribution</Label> 112 - <InputGroup> 113 - <InputGroupAddon>$</InputGroupAddon> 114 - <InputGroupInput 115 - id="pmt" 116 - type="number" 117 - value={pmt} 118 - onChange={(e) => setPmt(e.target.value)} 119 - /> 120 - </InputGroup> 121 - </div> 122 - <div className="space-y-2"> 123 - <Label htmlFor="rate">Interest Rate</Label> 124 - <InputGroup> 125 - <InputGroupInput 126 - id="rate" 127 - type="number" 128 - value={rate} 129 - onChange={(e) => setRate(e.target.value)} 130 - /> 131 - <InputGroupAddon>%</InputGroupAddon> 132 - </InputGroup> 133 - </div> 134 - <div className="space-y-2"> 135 - <Label htmlFor="years">Years</Label> 136 - <InputGroup> 137 - <InputGroupInput 138 - id="years" 139 - type="number" 140 - value={years} 141 - onChange={(e) => setYears(e.target.value)} 142 - /> 143 - </InputGroup> 144 - </div> 145 - </div> 146 - 147 - {history.length > 0 && ( 148 - <div className="mt-8"> 149 - <ChartContainer 150 - config={chartConfig} 151 - className="min-h-[200px] w-full" 152 - > 153 - <BarChart accessibilityLayer data={history}> 154 - <CartesianGrid vertical={false} /> 155 - <XAxis 156 - dataKey="year" 157 - tickLine={false} 158 - tickMargin={10} 159 - axisLine={false} 160 - tickFormatter={(value) => `Year ${value}`} 161 - /> 162 - <YAxis 163 - tickFormatter={(value) => `$${value.toLocaleString()}`} 164 - /> 165 - <ChartTooltip content={<ChartTooltipContent />} /> 166 - <Bar 167 - dataKey="openingBalance" 168 - stackId="a" 169 - fill="var(--color-openingBalance)" 170 - radius={[0, 0, 4, 4]} 171 - /> 172 - <Bar 173 - dataKey="interest" 174 - stackId="a" 175 - fill="var(--color-interest)" 176 - radius={[4, 4, 0, 0]} 177 - /> 178 - </BarChart> 179 - </ChartContainer> 180 - </div> 181 - )} 182 - </CardContent> 183 - </Card> 184 - </main> 185 - ); 186 - }
+178
src/app/fire/page.tsx
··· 1 + "use client"; 2 + 3 + import { useEffect, useState } from "react"; 4 + import { 5 + Bar, 6 + BarChart, 7 + CartesianGrid, 8 + ReferenceLine, 9 + XAxis, 10 + YAxis, 11 + } from "recharts"; 12 + import { 13 + Card, 14 + CardAction, 15 + CardContent, 16 + CardDescription, 17 + CardFooter, 18 + CardHeader, 19 + CardTitle, 20 + } from "@/components/ui/card"; 21 + import type { ChartConfig } from "@/components/ui/chart"; 22 + import { 23 + ChartContainer, 24 + ChartTooltip, 25 + ChartTooltipContent, 26 + } from "@/components/ui/chart"; 27 + import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 28 + import { Input } from "@/components/ui/input"; 29 + import { getPeriodsToTarget, getTarget, getTotalFv } from "@/lib/math"; 30 + 31 + const chartConfig = { 32 + contribution: { label: "Contribution" }, 33 + interest: { label: "Interest" }, 34 + } satisfies ChartConfig; 35 + 36 + function calculate( 37 + principal: number, 38 + payment: number, 39 + rate: number, 40 + periods: number, 41 + ) { 42 + return Array.from({ length: periods }).map((_, i) => { 43 + const period = i + 1; 44 + const contribution = principal + payment * period; 45 + const projected = getTotalFv(principal, payment, rate, period); 46 + return { period, contribution, interest: projected - contribution }; 47 + }); 48 + } 49 + 50 + export default function Fire() { 51 + const [principal, setPrincipal] = useState(100000); 52 + const [payment, setPayment] = useState(20000); 53 + const [rate, setRate] = useState(7 / 100); 54 + const [spending, setSpending] = useState(40000); 55 + const [withdrawal, setWithDrawal] = useState(4 / 100); 56 + const [target, setTarget] = useState(0); 57 + const [periods, setPeriods] = useState(0); 58 + const [history, setHistory] = useState<ReturnType<typeof calculate>>([]); 59 + 60 + useEffect(() => { 61 + if ( 62 + Number.isNaN(principal) || 63 + Number.isNaN(payment) || 64 + Number.isNaN(rate) || 65 + Number.isNaN(spending) || 66 + Number.isNaN(withdrawal) 67 + ) 68 + return; 69 + const target = getTarget(spending, withdrawal); 70 + const periods = getPeriodsToTarget(target, principal, payment, rate); 71 + const history = calculate(principal, payment, rate, Math.ceil(periods) + 5); 72 + setTarget(target); 73 + setPeriods(periods); 74 + setHistory(history); 75 + }, [principal, payment, rate, spending, withdrawal]); 76 + 77 + return ( 78 + <main className="flex flex-1 items-center justify-center p-4"> 79 + <Card className="w-full max-w-4xl"> 80 + <CardHeader> 81 + <CardTitle>Fire</CardTitle> 82 + <CardDescription> A simple Fire visualizer </CardDescription> 83 + <CardAction> 84 + In{" "} 85 + <span className="text-lg leading-none font-bold sm:text-3xl"> 86 + {Math.ceil(periods * 100) / 100} 87 + </span>{" "} 88 + years. 89 + </CardAction> 90 + </CardHeader> 91 + <CardContent> 92 + <ChartContainer config={chartConfig}> 93 + <BarChart accessibilityLayer data={history}> 94 + <CartesianGrid vertical={false} /> 95 + <XAxis dataKey="period" /> 96 + <YAxis /> 97 + <ChartTooltip content={<ChartTooltipContent hideLabel />} /> 98 + <ReferenceLine y={target} /> 99 + <Bar 100 + dataKey="contribution" 101 + stackId="a" 102 + fill="var(--chart-1)" 103 + radius={[0, 0, 4, 4]} 104 + /> 105 + <Bar 106 + dataKey="interest" 107 + stackId="a" 108 + fill="var(--chart-2)" 109 + radius={[4, 4, 0, 0]} 110 + /> 111 + </BarChart> 112 + </ChartContainer> 113 + </CardContent> 114 + <CardFooter> 115 + <FieldGroup> 116 + <div className="grid grid-cols-3 gap-4"> 117 + <Field> 118 + <FieldLabel htmlFor="principal">Principal</FieldLabel> 119 + <Input 120 + id="principal" 121 + type="number" 122 + value={principal} 123 + onChange={(e) => 124 + setPrincipal(Number.parseInt(e.target.value, 10)) 125 + } 126 + /> 127 + </Field> 128 + <Field> 129 + <FieldLabel htmlFor="payment">Payment</FieldLabel> 130 + <Input 131 + id="payment" 132 + type="number" 133 + value={payment} 134 + onChange={(e) => 135 + setPayment(Number.parseInt(e.target.value, 10)) 136 + } 137 + /> 138 + </Field> 139 + <Field> 140 + <FieldLabel htmlFor="rate">Rate</FieldLabel> 141 + <Input 142 + id="rate" 143 + type="number" 144 + value={rate} 145 + onChange={(e) => setRate(Number.parseFloat(e.target.value))} 146 + /> 147 + </Field> 148 + </div> 149 + <div className="grid grid-cols-2 gap-4"> 150 + <Field> 151 + <FieldLabel htmlFor="spending">Spending</FieldLabel> 152 + <Input 153 + id="spending" 154 + type="number" 155 + value={spending} 156 + onChange={(e) => 157 + setSpending(Number.parseInt(e.target.value, 10)) 158 + } 159 + /> 160 + </Field> 161 + <Field> 162 + <FieldLabel htmlFor="withdrawal">WithDrawal</FieldLabel> 163 + <Input 164 + id="withdrawal" 165 + type="number" 166 + value={withdrawal} 167 + onChange={(e) => 168 + setWithDrawal(Number.parseFloat(e.target.value)) 169 + } 170 + /> 171 + </Field> 172 + </div> 173 + </FieldGroup> 174 + </CardFooter> 175 + </Card> 176 + </main> 177 + ); 178 + }
+1 -1
src/app/layout.tsx
··· 21 21 22 22 const navItems = [ 23 23 { href: "/", label: "Home" }, 24 - { href: "/calculator", label: "Calculator" }, 24 + { href: "/fire", label: "Fire" }, 25 25 ]; 26 26 27 27 export default function RootLayout({
+14 -14
src/app/page.tsx
··· 1 - const HomePage = () => ( 2 - <main className="flex-1 flex flex-col items-center justify-center"> 3 - <h1 className="text-6xl font-extrabold tracking-tighter"> 4 - <span className="bg-gradient-to-r from-blue-500 to-teal-400 bg-clip-text text-transparent"> 5 - Very Good Tools 6 - </span> 7 - </h1> 8 - <p className="mt-4 text-lg text-muted-foreground"> 9 - A collection of simple, yet powerful tools. 10 - </p> 11 - </main> 12 - ); 13 - 14 - export default HomePage; 1 + export default function Home() { 2 + return ( 3 + <main className="flex-1 flex flex-col items-center justify-center"> 4 + <h1 className="text-6xl font-extrabold tracking-tighter"> 5 + <span className="bg-gradient-to-r from-blue-500 to-teal-400 bg-clip-text text-transparent"> 6 + Very Good Tools 7 + </span> 8 + </h1> 9 + <p className="mt-4 text-lg text-muted-foreground"> 10 + A collection of simple, yet powerful tools. 11 + </p> 12 + </main> 13 + ); 14 + }
-58
src/components/ui/button.tsx
··· 1 - import { Button as ButtonPrimitive } from "@base-ui/react/button"; 2 - import { cva, type VariantProps } from "class-variance-authority"; 3 - 4 - import { cn } from "@/lib/utils"; 5 - 6 - const buttonVariants = cva( 7 - "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 8 - { 9 - variants: { 10 - variant: { 11 - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", 12 - outline: 13 - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", 14 - secondary: 15 - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", 16 - ghost: 17 - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", 18 - destructive: 19 - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", 20 - link: "text-primary underline-offset-4 hover:underline", 21 - }, 22 - size: { 23 - default: 24 - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", 25 - xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", 26 - sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", 27 - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", 28 - icon: "size-8", 29 - "icon-xs": 30 - "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3", 31 - "icon-sm": 32 - "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg", 33 - "icon-lg": "size-9", 34 - }, 35 - }, 36 - defaultVariants: { 37 - variant: "default", 38 - size: "default", 39 - }, 40 - }, 41 - ); 42 - 43 - function Button({ 44 - className, 45 - variant = "default", 46 - size = "default", 47 - ...props 48 - }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) { 49 - return ( 50 - <ButtonPrimitive 51 - data-slot="button" 52 - className={cn(buttonVariants({ variant, size, className }))} 53 - {...props} 54 - /> 55 - ); 56 - } 57 - 58 - export { Button, buttonVariants };
-157
src/components/ui/input-group.tsx
··· 1 - "use client"; 2 - 3 - import { cva, type VariantProps } from "class-variance-authority"; 4 - import * as React from "react"; 5 - import { Button } from "@/components/ui/button"; 6 - import { Input } from "@/components/ui/input"; 7 - import { Textarea } from "@/components/ui/textarea"; 8 - import { cn } from "@/lib/utils"; 9 - 10 - function InputGroup({ className, ...props }: React.ComponentProps<"div">) { 11 - return ( 12 - <div 13 - data-slot="input-group" 14 - role="group" 15 - className={cn( 16 - "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5", 17 - className, 18 - )} 19 - {...props} 20 - /> 21 - ); 22 - } 23 - 24 - const inputGroupAddonVariants = cva( 25 - "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4", 26 - { 27 - variants: { 28 - align: { 29 - "inline-start": 30 - "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]", 31 - "inline-end": 32 - "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]", 33 - "block-start": 34 - "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2", 35 - "block-end": 36 - "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2", 37 - }, 38 - }, 39 - defaultVariants: { 40 - align: "inline-start", 41 - }, 42 - }, 43 - ); 44 - 45 - function InputGroupAddon({ 46 - className, 47 - align = "inline-start", 48 - ...props 49 - }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) { 50 - return ( 51 - <div 52 - role="group" 53 - data-slot="input-group-addon" 54 - data-align={align} 55 - className={cn(inputGroupAddonVariants({ align }), className)} 56 - onClick={(e) => { 57 - if ((e.target as HTMLElement).closest("button")) { 58 - return; 59 - } 60 - e.currentTarget.parentElement?.querySelector("input")?.focus(); 61 - }} 62 - {...props} 63 - /> 64 - ); 65 - } 66 - 67 - const inputGroupButtonVariants = cva( 68 - "flex items-center gap-2 text-sm shadow-none", 69 - { 70 - variants: { 71 - size: { 72 - xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5", 73 - sm: "", 74 - "icon-xs": 75 - "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0", 76 - "icon-sm": "size-8 p-0 has-[>svg]:p-0", 77 - }, 78 - }, 79 - defaultVariants: { 80 - size: "xs", 81 - }, 82 - }, 83 - ); 84 - 85 - function InputGroupButton({ 86 - className, 87 - type = "button", 88 - variant = "ghost", 89 - size = "xs", 90 - ...props 91 - }: Omit<React.ComponentProps<typeof Button>, "size" | "type"> & 92 - VariantProps<typeof inputGroupButtonVariants> & { 93 - type?: "button" | "submit" | "reset"; 94 - }) { 95 - return ( 96 - <Button 97 - type={type} 98 - data-size={size} 99 - variant={variant} 100 - className={cn(inputGroupButtonVariants({ size }), className)} 101 - {...props} 102 - /> 103 - ); 104 - } 105 - 106 - function InputGroupText({ className, ...props }: React.ComponentProps<"span">) { 107 - return ( 108 - <span 109 - className={cn( 110 - "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", 111 - className, 112 - )} 113 - {...props} 114 - /> 115 - ); 116 - } 117 - 118 - function InputGroupInput({ 119 - className, 120 - ...props 121 - }: React.ComponentProps<"input">) { 122 - return ( 123 - <Input 124 - data-slot="input-group-control" 125 - className={cn( 126 - "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", 127 - className, 128 - )} 129 - {...props} 130 - /> 131 - ); 132 - } 133 - 134 - function InputGroupTextarea({ 135 - className, 136 - ...props 137 - }: React.ComponentProps<"textarea">) { 138 - return ( 139 - <Textarea 140 - data-slot="input-group-control" 141 - className={cn( 142 - "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent", 143 - className, 144 - )} 145 - {...props} 146 - /> 147 - ); 148 - } 149 - 150 - export { 151 - InputGroup, 152 - InputGroupAddon, 153 - InputGroupButton, 154 - InputGroupInput, 155 - InputGroupText, 156 - InputGroupTextarea, 157 - };
-201
src/components/ui/select.tsx
··· 1 - "use client"; 2 - 3 - import { Select as SelectPrimitive } from "@base-ui/react/select"; 4 - import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; 5 - import * as React from "react"; 6 - import { cn } from "@/lib/utils"; 7 - 8 - const Select = SelectPrimitive.Root; 9 - 10 - function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { 11 - return ( 12 - <SelectPrimitive.Group 13 - data-slot="select-group" 14 - className={cn("scroll-my-1 p-1", className)} 15 - {...props} 16 - /> 17 - ); 18 - } 19 - 20 - function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { 21 - return ( 22 - <SelectPrimitive.Value 23 - data-slot="select-value" 24 - className={cn("flex flex-1 text-left", className)} 25 - {...props} 26 - /> 27 - ); 28 - } 29 - 30 - function SelectTrigger({ 31 - className, 32 - size = "default", 33 - children, 34 - ...props 35 - }: SelectPrimitive.Trigger.Props & { 36 - size?: "sm" | "default"; 37 - }) { 38 - return ( 39 - <SelectPrimitive.Trigger 40 - data-slot="select-trigger" 41 - data-size={size} 42 - className={cn( 43 - "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", 44 - className, 45 - )} 46 - {...props} 47 - > 48 - {children} 49 - <SelectPrimitive.Icon 50 - render={ 51 - <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" /> 52 - } 53 - /> 54 - </SelectPrimitive.Trigger> 55 - ); 56 - } 57 - 58 - function SelectContent({ 59 - className, 60 - children, 61 - side = "bottom", 62 - sideOffset = 4, 63 - align = "center", 64 - alignOffset = 0, 65 - alignItemWithTrigger = true, 66 - ...props 67 - }: SelectPrimitive.Popup.Props & 68 - Pick< 69 - SelectPrimitive.Positioner.Props, 70 - "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" 71 - >) { 72 - return ( 73 - <SelectPrimitive.Portal> 74 - <SelectPrimitive.Positioner 75 - side={side} 76 - sideOffset={sideOffset} 77 - align={align} 78 - alignOffset={alignOffset} 79 - alignItemWithTrigger={alignItemWithTrigger} 80 - className="isolate z-50" 81 - > 82 - <SelectPrimitive.Popup 83 - data-slot="select-content" 84 - data-align-trigger={alignItemWithTrigger} 85 - className={cn( 86 - "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", 87 - className, 88 - )} 89 - {...props} 90 - > 91 - <SelectScrollUpButton /> 92 - <SelectPrimitive.List>{children}</SelectPrimitive.List> 93 - <SelectScrollDownButton /> 94 - </SelectPrimitive.Popup> 95 - </SelectPrimitive.Positioner> 96 - </SelectPrimitive.Portal> 97 - ); 98 - } 99 - 100 - function SelectLabel({ 101 - className, 102 - ...props 103 - }: SelectPrimitive.GroupLabel.Props) { 104 - return ( 105 - <SelectPrimitive.GroupLabel 106 - data-slot="select-label" 107 - className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)} 108 - {...props} 109 - /> 110 - ); 111 - } 112 - 113 - function SelectItem({ 114 - className, 115 - children, 116 - ...props 117 - }: SelectPrimitive.Item.Props) { 118 - return ( 119 - <SelectPrimitive.Item 120 - data-slot="select-item" 121 - className={cn( 122 - "relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", 123 - className, 124 - )} 125 - {...props} 126 - > 127 - <SelectPrimitive.ItemText className="flex flex-1 shrink-0 gap-2 whitespace-nowrap"> 128 - {children} 129 - </SelectPrimitive.ItemText> 130 - <SelectPrimitive.ItemIndicator 131 - render={ 132 - <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> 133 - } 134 - > 135 - <CheckIcon className="pointer-events-none" /> 136 - </SelectPrimitive.ItemIndicator> 137 - </SelectPrimitive.Item> 138 - ); 139 - } 140 - 141 - function SelectSeparator({ 142 - className, 143 - ...props 144 - }: SelectPrimitive.Separator.Props) { 145 - return ( 146 - <SelectPrimitive.Separator 147 - data-slot="select-separator" 148 - className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)} 149 - {...props} 150 - /> 151 - ); 152 - } 153 - 154 - function SelectScrollUpButton({ 155 - className, 156 - ...props 157 - }: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { 158 - return ( 159 - <SelectPrimitive.ScrollUpArrow 160 - data-slot="select-scroll-up-button" 161 - className={cn( 162 - "top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", 163 - className, 164 - )} 165 - {...props} 166 - > 167 - <ChevronUpIcon /> 168 - </SelectPrimitive.ScrollUpArrow> 169 - ); 170 - } 171 - 172 - function SelectScrollDownButton({ 173 - className, 174 - ...props 175 - }: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { 176 - return ( 177 - <SelectPrimitive.ScrollDownArrow 178 - data-slot="select-scroll-down-button" 179 - className={cn( 180 - "bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4", 181 - className, 182 - )} 183 - {...props} 184 - > 185 - <ChevronDownIcon /> 186 - </SelectPrimitive.ScrollDownArrow> 187 - ); 188 - } 189 - 190 - export { 191 - Select, 192 - SelectContent, 193 - SelectGroup, 194 - SelectItem, 195 - SelectLabel, 196 - SelectScrollDownButton, 197 - SelectScrollUpButton, 198 - SelectSeparator, 199 - SelectTrigger, 200 - SelectValue, 201 - };
-18
src/components/ui/textarea.tsx
··· 1 - import * as React from "react"; 2 - 3 - import { cn } from "@/lib/utils"; 4 - 5 - function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 - return ( 7 - <textarea 8 - data-slot="textarea" 9 - className={cn( 10 - "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", 11 - className, 12 - )} 13 - {...props} 14 - /> 15 - ); 16 - } 17 - 18 - export { Textarea };
+30
src/lib/math.ts
··· 1 + // Future Value of a single sum 2 + export const getFv = (p: number, r: number, n: number): number => 3 + p * (1 + r) ** n; 4 + 5 + // Future Value of an Annuity (regular contributions) 6 + export const getFva = (pmt: number, r: number, n: number): number => 7 + r === 0 ? pmt * n : pmt * (((1 + r) ** n - 1) / r); 8 + 9 + // Total Future Value (Principal + Annuity) 10 + export const getTotalFv = ( 11 + p: number, 12 + pmt: number, 13 + r: number, 14 + n: number, 15 + ): number => getFv(p, r, n) + getFva(pmt, r, n); 16 + 17 + // Real Rate of Return (Fisher Equation) 18 + export const getRealRate = (r: number, i: number): number => 19 + (1 + r) / (1 + i) - 1; 20 + 21 + // Target Capital (Perpetuity formula) 22 + export const getTarget = (pmt: number, r: number): number => pmt / r; 23 + 24 + // Calculate exact number of periods (n) to reach a target capital 25 + export const getPeriodsToTarget = ( 26 + target: number, 27 + p: number, 28 + pmt: number, 29 + r: number, 30 + ): number => Math.log((target * r + pmt) / (p * r + pmt)) / Math.log(1 + r);