Skip to content

Commit

Permalink
Merge branch 'main' into gg/ui-input-layout
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielegranello authored Jan 9, 2025
2 parents 9845db2 + 92b1bdb commit 90858ff
Show file tree
Hide file tree
Showing 29 changed files with 1,156 additions and 799 deletions.
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
prisma:
patterns:
- "@prisma/client"
- "prisma"
2 changes: 0 additions & 2 deletions .github/workflows/test-on-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ jobs:

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "18.17.0"

- name: Install dependencies
run: npm install
Expand Down
47 changes: 35 additions & 12 deletions app/api/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { POST } from "../api/route";
import * as calculationService from "../services/calculationService";
import calculateFairhold from "../models/testClasses";
import { NextResponse } from "next/server";
import { calculationSchema, Calculation } from "../schemas/calculationSchema";

// Mock dependencies
jest.mock("../services/calculationService");
Expand All @@ -18,7 +17,7 @@ const callResponse = (res: unknown) => {
};

describe("POST API Route", () => {
const mockRequest = (data: Calculation | string) => ({
const mockRequest = (data: unknown) => ({
json: jest.fn().mockResolvedValueOnce(data),
});

Expand All @@ -27,14 +26,14 @@ describe("POST API Route", () => {
});

it("should return processed data for valid apiSchema input", async () => {
const validApiInput = calculationSchema.parse({
const validApiInput = {
housePostcode: "SE17 1PE",
houseSize: 100,
houseAge: 3,
houseBedrooms: 2,
houseType: "D",
maintenancePercentage: 0.02,
});
};

const householdData = {
/* mock household data */
Expand All @@ -54,9 +53,21 @@ describe("POST API Route", () => {
const res = await POST(req as unknown as Request);

// Assertions
expect(calculationService.getHouseholdData).toHaveBeenCalledWith(
validApiInput
);
expect(calculationService.getHouseholdData).toHaveBeenCalledWith({
...validApiInput,
// Parsed postcode object
housePostcode: {
area: "SE",
district: "SE17",
incode: "1PE",
outcode: "SE17",
postcode: "SE17 1PE",
sector: "SE17 1",
subDistrict: null,
unit: "PE",
valid: true,
},
});
expect(calculateFairhold).toHaveBeenCalledWith(householdData);
expect(res).toEqual(NextResponse.json(processedData));
});
Expand All @@ -77,14 +88,14 @@ describe("POST API Route", () => {
});

it("should handle service errors", async () => {
const validApiInput = calculationSchema.parse({
const validApiInput = {
housePostcode: "SE17 1PE",
houseSize: 100,
houseAge: 3,
houseBedrooms: 2,
houseType: "D",
maintenancePercentage: 0.02,
});
};

const errorMessage = "Service error";

Expand All @@ -98,9 +109,21 @@ describe("POST API Route", () => {
callResponse(res);

// Assertions
expect(calculationService.getHouseholdData).toHaveBeenCalledWith(
validApiInput
);
expect(calculationService.getHouseholdData).toHaveBeenCalledWith({
...validApiInput,
// Parsed postcode object
housePostcode: {
area: "SE",
district: "SE17",
incode: "1PE",
outcode: "SE17",
postcode: "SE17 1PE",
sector: "SE17 1",
subDistrict: null,
unit: "PE",
valid: true,
},
});
expect(NextResponse.json).toHaveBeenCalledWith(
{ error: errorMessage },
{ status: 500 }
Expand Down
8 changes: 1 addition & 7 deletions app/api/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { NextResponse } from "next/server";
import { api, apiSchema } from "../schemas/apiSchema";
import { calculationSchema } from "../schemas/calculationSchema";
import * as calculationService from "../services/calculationService";
import calculateFairhold from "../models/testClasses";
Expand All @@ -8,12 +7,7 @@ export async function POST(req: Request) {
try {
// Parse and validate user input
const data = await req.json();
let input: api;
if (!apiSchema.safeParse(data).success) {
input = calculationSchema.parse(data);
} else {
input = data;
}
const input = calculationSchema.parse(data);
const householdData = await calculationService.getHouseholdData(input);
const processedData = calculateFairhold(householdData);
return NextResponse.json(processedData);
Expand Down
8 changes: 5 additions & 3 deletions app/components/ui/CalculatorInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ const CalculatorInput = () => {
? (Number(urlMaintenancePercentage) as 0.015 | 0.02 | 0.0375) // Type assertion
: 0.015,
housePostcode: urlPostcode || "",
houseSize: Number(urlHouseSize) || undefined,
houseAge: Number(urlHouseAge) || undefined,
houseBedrooms: Number(urlHouseBedrooms) || undefined,
// Apply defaults if provided
// Type-safe to account for exactOptionalPropertyTypes propert in tsconfig.json
...(urlHouseSize && { houseSize: Number(urlHouseSize )}),
...(urlHouseAge && { houseAge: Number(urlHouseAge )}),
...(urlHouseBedrooms && { houseBedrooms: Number(urlHouseBedrooms )}),
},
});

Expand Down
77 changes: 57 additions & 20 deletions app/components/ui/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,68 @@
import React from "react";
import TenureComparisonWrapper from "../graphs/TenureComparisonWrapper";
import TotalPaymentWrapper from "../graphs/TotalPaymentWrapper";
import LifetimeMarketPurchaseWrapper from "../graphs/LifetimeMarketPurchaseWrapper";
import LifetimeMarketRentWrapper from "../graphs/LifetimeMarketRentWrapper";
import LifetimeFairholdLandPurchaseWrapper from "../graphs/LifetimeFairholdLandPurchaseWrapper";
import LifetimeFairholdLandRentWrapper from "../graphs/LifetimeFairholdLandRentWrapper";
import { formType } from "@/app/schemas/formSchema";
import FormEdit from "./FormEdit";
import React, { useRef, useState } from "react";
import GraphCard from "./GraphCard";
import { Household } from "@/app/models/Household";

interface DashboardProps {
processedData: Household;
inputData: formType; // Add this property
}

const Dashboard: React.FC<DashboardProps> = ({ processedData, inputData }) => {
const Dashboard: React.FC<DashboardProps> = ({ data }) => {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [currentPage, setCurrentPage] = useState(0);
const totalPages = 6;

const handleNext = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollBy({
top: window.innerHeight,
behavior: "smooth",
});
}
setCurrentPage((prev) => Math.min(prev + 1, totalPages - 1));
};

const handleScroll = () => {
if (scrollContainerRef.current) {
const scrollTop = scrollContainerRef.current.scrollTop;
const pageHeight = window.innerHeight;
const newPage = Math.round(scrollTop / pageHeight);
setCurrentPage(newPage);
}
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const someUnusedVariable = data;

return (
<div>
<h1>Dashboard</h1>
{/* Render multiple graph components here */}
<FormEdit formData={inputData} />
<TenureComparisonWrapper household={processedData} />
<TotalPaymentWrapper household={processedData} />
<LifetimeMarketPurchaseWrapper household={processedData} />
<LifetimeMarketRentWrapper household={processedData} />
<LifetimeFairholdLandPurchaseWrapper household={processedData} />
<LifetimeFairholdLandRentWrapper household={processedData} />{" "}
<div className="snap-container">
<div
className="snap-scroll"
ref={scrollContainerRef}
onScroll={handleScroll}
>
<GraphCard title="How much would a Fairhold home cost?">
<span className="text-red-500">Not much</span>
</GraphCard>

<GraphCard title="How much would it cost every month?">
<span className="text-red-500">in theory less than Freehold</span>
</GraphCard>
<GraphCard title="How would the cost change over my life?"></GraphCard>
<GraphCard title="How much could I sell it for?"></GraphCard>
<GraphCard title="What difference would Fairhold make to me, my community, and the world?"></GraphCard>
<GraphCard title="What would you choose?"></GraphCard>
</div>
{currentPage < totalPages - 1 && (
<div className="fixed bottom-4 right-4">
<button
onClick={handleNext}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
Next
</button>
</div>
)}
</div>
);
};
Expand Down
16 changes: 16 additions & 0 deletions app/components/ui/GraphCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

type Props = React.PropsWithChildren<{
title: string;
}>;

const GraphCard: React.FC<Props> = ({ title, children }) => {
return (
<div className="h-screen snap-start">
<span className="text-2xl text-black">{title}</span>
{children && <div className="mt-4">{children}</div>}
</div>
);
};

export default GraphCard;
3 changes: 1 addition & 2 deletions app/data/itlRepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ const getItl3ByPostcodeDistrict = async (
},
});

// Cast to string as 'not: null' clause in Prisma query does not type narrow
return itl3 as string;
return itl3;
} catch (error) {
throw new Error(
`Data error: Unable get get itl3 for postcode district ${postcodeDistrict}`
Expand Down
Binary file modified app/favicon.ico
Binary file not shown.
17 changes: 17 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,20 @@ body {
transform: scale(1.05);
background: rgb(var(--text-default-rgb)) !important;
}

.snap-container {
height: 100vh;
overflow: hidden;
}

.snap-scroll {
height: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
}

.snap-scroll > * {
scroll-snap-align: start;
height: 100vh;
}
5 changes: 2 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Fairhold",
description: "How much would Fairhold cost me?",
};

export default function RootLayout({
children,
}: Readonly<{
Expand Down
67 changes: 67 additions & 0 deletions app/models/Lifetime.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Lifetime } from "./Lifetime";
import { createTestProperty, createTestLifetime } from "./testHelpers";

let lifetime = createTestLifetime();

beforeEach(() => {
lifetime = createTestLifetime();
})

it("can be instantiated", () => {
expect(lifetime).toBeInstanceOf(Lifetime);
})

it("creates an array with the correct number of years", () => {
expect(lifetime.lifetimeData).toHaveLength(40)
})

it("reduces mortgage payments to 0 after the mortgage term is reached", () => {
expect(lifetime.lifetimeData[35].newbuildHouseMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[34].marketLandMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[33].fairholdLandMortgageYearly).toBe(0);
expect(lifetime.lifetimeData[32].marketLandMortgageYearly).toBe(0);
})

describe("resale values", () => {
it("correctly calculates for a newbuild house", () => {
// Test newbuild (age 0)
lifetime = createTestLifetime({
property: createTestProperty({ age: 0 })
});
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBe(186560);
});
it("correctly calculates for a 10-year old house", () => { // Test 10-year-old house
lifetime = createTestLifetime({
property: createTestProperty({
age: 10,
newBuildPricePerMetre: 2120,
size: 88
})
});
// Calculate expected depreciation running `calculateDepreciatedBuildPrice()` method on its own
const houseY10 = createTestProperty({
age: 10,
newBuildPricePerMetre: 2120,
size: 88
})
const depreciatedHouseY10 = houseY10.calculateDepreciatedBuildPrice()
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBe(depreciatedHouseY10);
});
it("depreciates the house over time", () => {
// Test value changes over time
expect(lifetime.lifetimeData[0].depreciatedHouseResaleValue).toBeGreaterThan(
lifetime.lifetimeData[10].depreciatedHouseResaleValue);
})
});


it("correctly ages the house", () => {
lifetime = createTestLifetime({
property: createTestProperty({ age: 10 })
})

expect(lifetime.lifetimeData[0].houseAge).toBe(10);
expect(lifetime.lifetimeData[5].houseAge).toBe(15);
expect(lifetime.lifetimeData[20].houseAge).toBe(30);
expect(lifetime.lifetimeData[39].houseAge).toBe(49);
})
Loading

0 comments on commit 90858ff

Please sign in to comment.