-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into gg/how-much-per-month
- Loading branch information
Showing
14 changed files
with
496 additions
and
127 deletions.
There are no files selected for viewing
This file contains 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,49 @@ | ||
import { useState } from "react"; | ||
import GraphCard from "../../ui/GraphCard"; | ||
import ResaleValueWrapper from "../../graphs/ResaleValueWrapper"; | ||
import { Drawer } from "../../ui/Drawer"; | ||
import { Household } from "@/app/models/Household"; | ||
import TenureSelector from "../../ui/TenureSelector"; | ||
|
||
const TENURES = ['landPurchase', 'landRent'] as const; | ||
type Tenure = (typeof TENURES)[number]; | ||
|
||
interface DashboardProps { | ||
data: Household; | ||
} | ||
|
||
export const ResaleValue: React.FC<DashboardProps> = ({ data }) => { | ||
const [selectedTenure, setSelectedTenure] = useState<Tenure>('landPurchase'); | ||
|
||
return ( | ||
<GraphCard | ||
title="How much could I sell it for?" | ||
subtitle="Estimated sale price at any time" | ||
> | ||
<div className="flex flex-col h-full w-3/4 justify-between"> | ||
<div className="flex gap-2 mb-4"> | ||
{TENURES.map(tenure => ( | ||
<TenureSelector | ||
key={tenure} | ||
isSelected={selectedTenure === tenure} | ||
onClick={() => setSelectedTenure(tenure)} | ||
> | ||
{`Fairhold ${tenure === 'landPurchase' ? 'Land Purchase' : 'Land Rent'}`} | ||
</TenureSelector> | ||
))} | ||
</div> | ||
|
||
<ResaleValueWrapper | ||
household={data} | ||
tenure={selectedTenure} | ||
/> | ||
|
||
<Drawer | ||
buttonTitle="Find out more about how we estimated these" | ||
title="How we estimated these figures" | ||
description="Lorem ipsum dolor sit amet consectetur adipisicing elit. Illum minus eligendi fugit nulla officia dolor inventore nemo ex quo quia, laborum qui ratione aperiam, pariatur explicabo ipsum culpa impedit ad!" | ||
/> | ||
</div> | ||
</GraphCard> | ||
); | ||
}; |
This file contains 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,140 @@ | ||
import React from "react"; | ||
import { LineChart, Line, CartesianGrid, XAxis, YAxis, Label, TooltipProps } from "recharts"; | ||
import { Card, CardContent, CardHeader } from "@/components/ui/card"; | ||
import { | ||
ChartConfig, | ||
ChartContainer, | ||
ChartTooltip, | ||
} from "@/components/ui/chart"; | ||
import { formatValue } from "@/app/lib/format"; | ||
|
||
type CustomTooltipProps = TooltipProps<number, string> & { | ||
payload?: Array<{ | ||
name: keyof Omit<DataPoint, "year">; | ||
value: number; | ||
stroke: string; | ||
}>; | ||
}; | ||
|
||
const chartConfig = { | ||
noMaintenance: { | ||
label: "No maintenance", | ||
color: "rgb(var(--fairhold-land-color-rgb))", | ||
}, | ||
lowMaintenance: { | ||
label: "Low maintenance", | ||
color: "rgb(var(--fairhold-land-color-rgb))", | ||
}, | ||
mediumMaintenance: { | ||
label: "Medium maintenance", | ||
color: "rgb(var(--fairhold-land-color-rgb))", | ||
}, | ||
highMaintenance: { | ||
label: "High maintenance", | ||
color: "rgb(var(--fairhold-land-color-rgb))", | ||
}, | ||
} satisfies ChartConfig; | ||
|
||
export interface DataPoint { | ||
year: number; | ||
noMaintenance: number; | ||
lowMaintenance: number; | ||
mediumMaintenance: number; | ||
highMaintenance: number; | ||
} | ||
|
||
interface ResaleValueLineChartProps { | ||
data: DataPoint[]; | ||
selectedMaintenance: "noMaintenance" | "lowMaintenance" | "mediumMaintenance" | "highMaintenance"; | ||
maxY: number; | ||
} | ||
|
||
const ResaleValueLineChart: React.FC<ResaleValueLineChartProps> = ({ | ||
data, | ||
selectedMaintenance, | ||
maxY | ||
}) => { | ||
const renderLine = (dataKey: keyof Omit<DataPoint, "year">) => ( | ||
<Line | ||
type="monotone" | ||
dataKey={dataKey} | ||
stroke={`var(--color-${dataKey})`} | ||
strokeWidth={2} | ||
strokeDasharray={dataKey === selectedMaintenance ? "0" : "5 5"} | ||
dot={false} | ||
/> | ||
); | ||
const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { | ||
if (!active || !payload || !payload.length) return null; | ||
|
||
return ( | ||
<div className="bg-white p-3 border rounded shadow"> | ||
<p className="font-medium mb-2">Year {label}</p> | ||
{payload.map((entry) => ( | ||
<div key={entry.name} className="flex items-center gap-2 mb-1"> | ||
<svg width="20" height="2" className="flex-shrink-0"> | ||
<line | ||
x1="0" | ||
y1="1" | ||
x2="20" | ||
y2="1" | ||
stroke={entry.stroke} | ||
strokeWidth="2" | ||
strokeDasharray={entry.name === selectedMaintenance ? "0" : "5 5"} | ||
/> | ||
</svg> | ||
<span>{chartConfig[entry.name as keyof typeof chartConfig].label}:</span> | ||
<span className="font-medium">{formatValue(entry.value ?? 0)}</span> | ||
</div> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
return ( | ||
<Card> | ||
<CardHeader></CardHeader> | ||
<CardContent> | ||
<ChartContainer config={chartConfig}> | ||
<LineChart | ||
data={data} | ||
margin={{ top: 20, right: 30, left: 20, bottom: 50 }} | ||
> | ||
<CartesianGrid strokeDasharray="3 3" /> | ||
<XAxis | ||
dataKey="year" | ||
tickLine={false} | ||
> | ||
<Label | ||
value="Years" | ||
position="bottom" | ||
offset={20} | ||
className="label-class" | ||
/> | ||
</XAxis> | ||
<YAxis | ||
tickFormatter={formatValue} | ||
tickLine={false} | ||
domain={[0, maxY]} | ||
> | ||
<Label | ||
value="Resale Value" | ||
angle={-90} | ||
position="left" | ||
offset={0} | ||
className="label-class" | ||
/> | ||
</YAxis> | ||
<ChartTooltip content={<CustomTooltip />} /> | ||
{renderLine("highMaintenance")} | ||
{renderLine("mediumMaintenance")} | ||
{renderLine("lowMaintenance")} | ||
{renderLine("noMaintenance")} | ||
</LineChart> | ||
</ChartContainer> | ||
</CardContent> | ||
</Card> | ||
); | ||
}; | ||
|
||
export default ResaleValueLineChart; |
This file contains 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,78 @@ | ||
"use client"; | ||
import React from "react"; | ||
import ErrorBoundary from "../ErrorBoundary"; | ||
import { Household } from "@/app/models/Household"; | ||
import ResaleValueLineChart from "./ResaleValueLineChart"; | ||
import type { DataPoint } from "./ResaleValueLineChart" | ||
|
||
interface ResaleValueWrapperProps { | ||
tenure: 'landPurchase' | 'landRent'; | ||
household: Household; | ||
} | ||
|
||
const ResaleValueWrapper: React.FC<ResaleValueWrapperProps> = ({ | ||
tenure, | ||
household | ||
}) => { | ||
// Since we want one line (user selected) to be solid, need to map the maintenance percentage to the line type | ||
const getSelectedMaintenance = (maintenancePercentage: number): 'noMaintenance' | 'lowMaintenance' | 'mediumMaintenance' | 'highMaintenance' => { | ||
switch (maintenancePercentage) { | ||
case 0: | ||
return 'noMaintenance'; | ||
case 0.015: | ||
return 'lowMaintenance'; | ||
case 0.019: | ||
return 'mediumMaintenance'; | ||
case 0.025: | ||
return 'highMaintenance'; | ||
default: | ||
return 'lowMaintenance'; | ||
} | ||
}; | ||
|
||
/** Needs either `landRent` or `landPurchase` to denote Fairhold tenure type; based on this arg, it will determine if land resale value is 0 or FHLP over time */ | ||
const formatData = (household: Household) => { | ||
const lifetime = household.lifetime.lifetimeData | ||
const chartData: DataPoint[] = []; | ||
|
||
for (let i = 0; i < lifetime.length; i++ ) { | ||
// Fairhold land rent cannot be sold for anything, assign as 0 | ||
const landValue = tenure === 'landRent' ? 0 : lifetime[i].fairholdLandPurchaseResaleValue; | ||
|
||
chartData.push({ | ||
year: i + 1, | ||
noMaintenance: landValue + lifetime[i].depreciatedHouseResaleValueNoMaintenance, | ||
lowMaintenance: landValue + lifetime[i].depreciatedHouseResaleValueLowMaintenance, | ||
mediumMaintenance: landValue + lifetime[i].depreciatedHouseResaleValueMediumMaintenance, | ||
highMaintenance: landValue + lifetime[i].depreciatedHouseResaleValueHighMaintenance | ||
}) | ||
|
||
} | ||
return chartData | ||
} | ||
|
||
const formattedData = formatData(household); | ||
const selectedMaintenance = getSelectedMaintenance(household.property.maintenancePercentage) | ||
|
||
// We want a constant y value across the graphs so we can compare resale values between them | ||
const finalYear = household.lifetime.lifetimeData[household.lifetime.lifetimeData.length - 1] | ||
const maxY = Math.ceil((1.1 * (finalYear.fairholdLandPurchaseResaleValue + finalYear.depreciatedHouseResaleValueHighMaintenance)) / 100000) * 100000 // Scale y axis by 1.1 (for a bit of visual headroom) and round to nearest hundred thousand to make things tidy | ||
|
||
if (!household) { | ||
return <div>No household data available</div>; | ||
} | ||
|
||
return ( | ||
<ErrorBoundary> | ||
<div> | ||
<ResaleValueLineChart | ||
data={formattedData} | ||
selectedMaintenance={selectedMaintenance} | ||
maxY={maxY} | ||
/> | ||
</div> | ||
</ErrorBoundary> | ||
); | ||
}; | ||
|
||
export default ResaleValueWrapper |
This file contains 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 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 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,34 @@ | ||
import React from 'react'; | ||
import { cn } from "@/lib/utils"; | ||
|
||
interface TenureSelectorProps { | ||
isSelected?: boolean; | ||
onClick?: () => void; | ||
className?: string; | ||
children: React.ReactNode; | ||
} | ||
|
||
const TenureSelector: React.FC<TenureSelectorProps> = ({ | ||
isSelected = false, | ||
onClick, | ||
className, | ||
children, | ||
}) => { | ||
return ( | ||
<button | ||
onClick={onClick} | ||
className={cn( | ||
"px-4 py-2 rounded-lg transition-colors duration-200", | ||
"text-sm font-medium", | ||
isSelected | ||
? "bg-green-100 text-green-700" // Selected state | ||
: "bg-gray-100 text-gray-700 hover:bg-gray-200", // Default state | ||
className | ||
)} | ||
> | ||
{children} | ||
</button> | ||
); | ||
}; | ||
|
||
export default TenureSelector; |
This file contains 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
Oops, something went wrong.