From 5787b7d891cb2421e49e37c3fd531130cb9f0255 Mon Sep 17 00:00:00 2001 From: Thad Kerosky Date: Tue, 26 Mar 2024 22:13:56 -0400 Subject: [PATCH] CurrentHeatingSystem form checking (#158) * WIP: explore abstract in zod types Co-authored-by: plocket Co-authored-by: Clayton Schneider Co-authored-by: Leopardfoot Co-authored-by: Camden Blatchly * Added zod types for CurrentHeatingSystem and change types/index.ts to zod. Co-authored-by: Camden Blatchly Co-authored-by: plocket * fix display analysis+charts to use zod --------- Co-authored-by: plocket Co-authored-by: Clayton Schneider Co-authored-by: Leopardfoot Co-authored-by: Camden Blatchly --- .../CaseSummaryComponents/AnalysisHeader.tsx | 4 +- .../CurrentHeatingSystem.tsx | 4 +- .../EnergyUseHistory.tsx | 13 +-- .../EnergyUseHistoryChart.tsx | 4 +- .../CaseSummaryComponents/HomeInformation.tsx | 14 +--- heat-stack/app/routes/_heat+/inputs2.tsx | 2 +- heat-stack/app/routes/_heat+/single.tsx | 84 ++++++++++++------- heat-stack/types/index.d.ts | 62 -------------- heat-stack/types/index.ts | 68 +++++++++++++++ 9 files changed, 140 insertions(+), 115 deletions(-) delete mode 100644 heat-stack/types/index.d.ts create mode 100644 heat-stack/types/index.ts diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx index fda3c915..095a1df4 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/AnalysisHeader.tsx @@ -1,5 +1,7 @@ -import { type HeatLoadAnalysis } from '#types/index.js' +import { type z } from 'zod' +import { type HeatLoadAnalysis as HeatLoadAnalysisZod} from '#types/index' +type HeatLoadAnalysis = z.infer export function AnalysisHeader() { const heatLoadAnalysis: HeatLoadAnalysis = { rulesEngineVersion: 'Beta 1', diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx index cc3427d1..9f1d9f97 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx @@ -4,7 +4,9 @@ import { Button } from '#/app/components/ui/button.tsx' import { Input } from '#/app/components/ui/input.tsx' import { Label } from '#/app/components/ui/label.tsx' -export function CurrentHeatingSystem() { +type CurrentHeatingSystemProps = {fields: any}; + +export function CurrentHeatingSystem(props: CurrentHeatingSystemProps) { const titleClass = 'text-5xl font-extrabold tracking-wide' const descriptiveClass = 'mt-2 text-sm text-slate-500' const componentMargin = 'mt-10' diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx index 45d8096a..b378c532 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx @@ -1,14 +1,17 @@ -import { AnalysisHeader } from './AnalysisHeader.tsx' -import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' -import { Button } from '#/app/components/ui/button.tsx' - +import { FieldMetadata, useForm } from '@conform-to/react' import { Form } from '@remix-run/react' import { ErrorList } from "./ErrorList.tsx" import { Input } from '#/app/components/ui/input.tsx' import { Label } from '#/app/components/ui/label.tsx' -import { FieldMetadata, useForm } from '@conform-to/react' +import { Button } from '#/app/components/ui/button.tsx' + +import { AnalysisHeader } from './AnalysisHeader.tsx' +import { EnergyUseHistoryChart } from './EnergyUseHistoryChart.tsx' + +// type EnergyUseProps = {fields: any}; +// export function EnergyUseHistory(props: EnergyUseProps) { export function EnergyUseHistory() { const titleClass = 'text-5xl font-extrabold tracking-wide mt-10' const subtitleClass = 'text-2xl font-semibold text-zinc-950 mt-9' diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx index 2e8d20bc..bcf8f910 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/EnergyUseHistoryChart.tsx @@ -1,4 +1,5 @@ -import { type NaturalGasBillRecord } from '#types/index.js' +import { type z } from 'zod' +import { type NaturalGasBillRecord as NaturalGasBillRecordZod } from '#types/index' import { Checkbox } from '../../../../components/ui/checkbox.tsx' import { @@ -10,6 +11,7 @@ import { TableRow, } from '../../../../components/ui/table.tsx' +type NaturalGasBillRecord = z.infer const naturalGasBillRecord01: NaturalGasBillRecord = { periodStartDate: new Date('12/08/2017'), periodEndDate: new Date('01/07/2018'), diff --git a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx index 82c9f9d9..2f121dc1 100644 --- a/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx +++ b/heat-stack/app/components/ui/heat/CaseSummaryComponents/HomeInformation.tsx @@ -42,19 +42,7 @@ import { FieldMetadata, useForm } from '@conform-to/react' // // return redirect(`/inputs1`) // } -type HomeInformationProps = {fields: { - name: FieldMetadata; - address: FieldMetadata; - livingSpace: FieldMetadata; -}}; +type HomeInformationProps = {fields: any}; export function HomeInformation(props: HomeInformationProps) { diff --git a/heat-stack/app/routes/_heat+/inputs2.tsx b/heat-stack/app/routes/_heat+/inputs2.tsx index 735b41a9..ec79460e 100644 --- a/heat-stack/app/routes/_heat+/inputs2.tsx +++ b/heat-stack/app/routes/_heat+/inputs2.tsx @@ -2,7 +2,7 @@ import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryCompon export default function Inputs2() { return (
- +
) } diff --git a/heat-stack/app/routes/_heat+/single.tsx b/heat-stack/app/routes/_heat+/single.tsx index 4d6e82dc..c2676cbc 100644 --- a/heat-stack/app/routes/_heat+/single.tsx +++ b/heat-stack/app/routes/_heat+/single.tsx @@ -9,6 +9,7 @@ import { z } from 'zod' // Ours import { ErrorList } from '#app/components/ui/heat/CaseSummaryComponents/ErrorList.tsx' +import { Home, Location, Case } from '../../../types/index.ts' import { CurrentHeatingSystem } from '../../components/ui/heat/CaseSummaryComponents/CurrentHeatingSystem.tsx' import { EnergyUseHistory } from '../../components/ui/heat/CaseSummaryComponents/EnergyUseHistory.tsx' import { HomeInformation } from '../../components/ui/heat/CaseSummaryComponents/HomeInformation.tsx' @@ -20,53 +21,71 @@ const addressMaxLength = 100 /** Modeled off the conform example at * https://github.com/epicweb-dev/web-forms/blob/b69e441f5577b91e7df116eba415d4714daacb9d/exercises/03.schema-validation/03.solution.conform-form/app/routes/users%2B/%24username_%2B/notes.%24noteId_.edit.tsx#L48 */ -const HomeInformationSchema = z.object({ - name: z.string().min(1).max(nameMaxLength), - address: z.string().min(1).max(addressMaxLength), - livingSpace: z.number().min(1), -}) -const EnergyUseSchema = z.object({ - fuelType: z.string().min(1).max(nameMaxLength), - efficiency: z.string().min(1).max(addressMaxLength), - override: z.number().min(1), - setpoint: z.string().min(1).max(nameMaxLength), - setbackTemp: z.string().min(1).max(addressMaxLength), - setbackHours: z.number().min(1), +// const HomeInformationSchema = { +// name: z.string().min(1).max(nameMaxLength), +// address: z.string().min(1).max(addressMaxLength), +// livingSpace: z.number().min(1), +// } +// // type Home = z.infer + +// // TODO Next: Ask an LLM how we get fuelType out of HomeSchema from zod + +const HomeFormSchema = Home.pick({ livingArea: true }) + .and(Location.pick({ address: true })) + .and(Case.pick({ name: true })) + +const CurrentHeatingSystemSchema = Home.pick({ + fuelType: true, + heatingSystemEfficiency: true, + thermostatSetPoint: true, + setbackTemperature: true, + setbackHoursPerDay: true, + designTemperatureOverride: true, }) +const Schema = HomeFormSchema.and(CurrentHeatingSystemSchema) + +// const EnergyUseSchema = ''; + export async function action({ request, params }: ActionFunctionArgs) { // Checks if url has a homeId parameter, throws 400 if not there // invariantResponse(params.homeId, 'homeId param is required') const formData = await request.formData() const submission = parseWithZod(formData, { - schema: HomeInformationSchema, + schema: Schema, }) - if(submission.status !== "success") { + if (submission.status !== 'success') { return submission.reply() - // submission.reply({ - // // You can also pass additional error to the `reply` method - // formErrors: ['Submission failed'], - // fieldErrors: { - // address: ['Address is invalid'], - // }, - - // // or avoid sending the the field value back to client by specifying the field names - // hideFields: ['password'], - // }), - // {status: submission.status === "error" ? 400 : 200} + // submission.reply({ + // // You can also pass additional error to the `reply` method + // formErrors: ['Submission failed'], + // fieldErrors: { + // address: ['Address is invalid'], + // }, + + // // or avoid sending the the field value back to client by specifying the field names + // hideFields: ['password'], + // }), + // {status: submission.status === "error" ? 400 : 200} } // TODO NEXT WEEK // - [x] Server side error checking/handling // - [x] ~Save to cookie and redirect to next form~ Put everything on the same page + // - [ ] - Get zod and Typescript to play nice // - [ ] (We're here) Build form #2 // - [ ] Build form #3 // - [ ] Form errors (if we think of a use case - 2 fields conflicting...) - const { name, address, livingSpace } = submission.value + const { name, address, livingArea, fuelType, + heatingSystemEfficiency, + thermostatSetPoint, + setbackTemperature, + setbackHoursPerDay, + designTemperatureOverride } = submission.value // await updateNote({ id: params.noteId, title, content }) @@ -78,17 +97,20 @@ export default function Inputs() { const [form, fields] = useForm({ lastResult, onValidate({ formData }) { - return parseWithZod(formData, { schema: HomeInformationSchema }) - }, - defaultValue: { - + return parseWithZod(formData, { schema: Schema }) }, + defaultValue: {}, shouldValidate: 'onBlur', }) return ( <> -
+ diff --git a/heat-stack/types/index.d.ts b/heat-stack/types/index.d.ts deleted file mode 100644 index 62bc01ab..00000000 --- a/heat-stack/types/index.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -export interface Case { - firstName: string - lastName: string -} - -export interface HeatLoadAnalysis { - rulesEngineVersion: string - estimatedBalancePoint: number - otherFuelUsage: number - averageIndoorTemperature: number - differenceBetweenTiAndTbp: number - designTemperature: number - wholeHomeHeatLossRate: number - standardDeviationHeatLossRate: number - averageHeatLoad: number - maximumHeatLoad: number -} - -export interface Home { - livingArea: number - fuelType: 'Natural Gas' | 'Oil' | 'Propane' - designTemperatureOverride: number - heatingSystemEfficiency: number - thermostatSetPoint: number - setbackTemperature: number - setbackHoursPerDay: number - numberOfOccupants: number - estimatedWaterHeatingEfficiency: number - standByLosses: number -} - -export interface Location { - address: string - addressLine2: string - city: string - state: string - zip: string - country: string -} - -export interface NaturalGasBill { - provider: string -} - -export interface NaturalGasBillRecord { - periodStartDate: Date - periodEndDate: Date - usageTherms: number - inclusionOverride: 'Include' | 'Do not include' | 'Include in other analysis' -} - -export interface OilPropaneBill { - provider: string - precedingDeliveryDate: Date -} - -export interface OilPropaneBillRecord { - periodStartDate: Date - periodEndDate: Date - gallons: number - inclusionOverride: 'Include' | 'Do not include' | 'Include in other analysis' -} diff --git a/heat-stack/types/index.ts b/heat-stack/types/index.ts new file mode 100644 index 00000000..ddc5182a --- /dev/null +++ b/heat-stack/types/index.ts @@ -0,0 +1,68 @@ +import { z } from 'zod'; + +// JS team wants to discuss this name +export const Case = z.object({ + name: z.string() +}) + +export const HeatLoadAnalysis = z.object({ + rulesEngineVersion: z.string(), + estimatedBalancePoint: z.number(), + otherFuelUsage: z.number(), + averageIndoorTemperature: z.number(), + differenceBetweenTiAndTbp: z.number(), + /** + * designTemperature in Fahrenheit + */ + designTemperature: z.number().max(-10).min(50), + wholeHomeHeatLossRate: z.number(), + standardDeviationHeatLossRate: z.number(), + averageHeatLoad: z.number(), + maximumHeatLoad: z.number(), +}); + +export const Home = z.object({ + /** + * unit: square feet + */ + livingArea: z.number().min(500).max(10000), + fuelType: z.enum(['Natural Gas','Oil','Propane']), + designTemperatureOverride: z.number(), + /** + * unit: percentage in decimal numbers, but not 0 to 1 + */ + heatingSystemEfficiency: z.number().min(60).max(100), + thermostatSetPoint: z.number(), + setbackTemperature: z.number(), + setbackHoursPerDay: z.number(), + numberOfOccupants: z.number(), + estimatedWaterHeatingEfficiency: z.number(), + standByLosses: z.number(), +}); + +export const Location = z.object({ + address: z.string(), +}); + +export const NaturalGasBill = z.object({ + provider: z.string(), +}); + +export const NaturalGasBillRecord = z.object({ + periodStartDate: z.date(), + periodEndDate: z.date(), + usageTherms: z.number(), + inclusionOverride: z.enum(['Include', 'Do not include', 'Include in other analysis']), +}); + +export const OilPropaneBill = z.object({ + provider: z.string(), + precedingDeliveryDate: z.date(), +}); + +export const OilPropaneBillRecord = z.object({ + periodStartDate: z.date(), + periodEndDate: z.date(), + gallons: z.number(), + inclusionOverride: z.enum(['Include', 'Do not include', 'Include in other analysis']), +});