From 97cf8672c0fda5e2da930b3c6b8dd5e8c2fcc971 Mon Sep 17 00:00:00 2001 From: Eli <88557639+lishaduck@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:19:55 -0500 Subject: [PATCH] fix: literally everything about the calculator --- src/components/Checkbox.tsx | 31 +++ src/content/geothermal/what.mdx | 7 + src/content/geothermal/worth-it.mdx | 3 + src/fresh.gen.ts | 4 +- .../{StateSelector.tsx => Selector.tsx} | 60 +++--- src/routes/calculator.tsx | 201 +++++++++++++++--- src/utils/calc/geo.ts | 80 +++++++ src/utils/{calc.ts => calc/solar.ts} | 4 +- src/utils/intl.ts | 12 ++ src/utils/ip.ts | 2 +- 10 files changed, 343 insertions(+), 61 deletions(-) create mode 100644 src/components/Checkbox.tsx rename src/islands/{StateSelector.tsx => Selector.tsx} (71%) create mode 100644 src/utils/calc/geo.ts rename src/utils/{calc.ts => calc/solar.ts} (97%) create mode 100644 src/utils/intl.ts diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx new file mode 100644 index 00000000..63b414b5 --- /dev/null +++ b/src/components/Checkbox.tsx @@ -0,0 +1,31 @@ +import type { JSX } from "preact"; + +export interface CheckboxProps { + name: string; + id: string; + labelText: string; + required?: boolean; +} + +export function Checkbox({ + name, + id, + labelText, + required, +}: CheckboxProps): JSX.Element { + return ( +
+ + +
+ ); +} diff --git a/src/content/geothermal/what.mdx b/src/content/geothermal/what.mdx index f8d89d7c..c0f531d6 100644 --- a/src/content/geothermal/what.mdx +++ b/src/content/geothermal/what.mdx @@ -11,3 +11,10 @@ That’s an incredible amount of energy. However, it’s important to remember that geothermal energy isn’t a new concept. Humans have been using it for over 10,000 years for functions such as heating hot springs, cooking, and even space heating in cities such as Pompeii. Nowadays, it’s still mainly used for heating; however, if you install a converter, it can also be used for electricity. + +There are four types of geothermal energy solutions. + +1. Horizontal loop: Horizontal loop systems are best for properties with more land, as they are bigger and require deeper trenches. +2. Vertical Loop: Vertical loop systems are very narrow and great for residential areas! +3. Closed Loop: Closed loop systems mix water and antifreeze to accommodate cold climates, making them optimal for northern locations. +4. Open Loop: Open loop systems circulate water from an outside source, meaning that they are optimal for properties with a steady water source. diff --git a/src/content/geothermal/worth-it.mdx b/src/content/geothermal/worth-it.mdx index 9114aa22..0b1232ff 100644 --- a/src/content/geothermal/worth-it.mdx +++ b/src/content/geothermal/worth-it.mdx @@ -8,6 +8,9 @@ Deciding to invest in a geothermal energy solution can be tricky. When comparing it to solar it’s a lot more expensive to start and can take up to 20 years to pay itself off. The pumps also require electricity to power so it’s not technically _free_. Similar to solar energy, there are also federal tax credits that range from 22% to 30%, applicable on the install. +When considering what solution to buy, consider that estimates are rarely over $4000 off, though the actual prices may vary depending on local labor costs and other factors. +Of course, if your state has high geothermic activity, then you will recoup your cost faster. However, geothermal can be much more efficient than wind and solar as it isn’t impacted by weather. It will always run as long as it’s powered. Overall, investing in geothermal can be a smart choice depending on your location and budget as it will save you money over time and lessen carbon emissions. +Feel free to ask our Why Switch AI chatbot for more information! diff --git a/src/fresh.gen.ts b/src/fresh.gen.ts index 7fa38fc8..d1891561 100644 --- a/src/fresh.gen.ts +++ b/src/fresh.gen.ts @@ -12,7 +12,7 @@ import * as $index from "./routes/index.tsx"; import * as $solutions_category_slug_ from "./routes/solutions/[category]/[[slug]].tsx"; import * as $solutions_category_index from "./routes/solutions/[category]/index.tsx"; import * as $HeaderMenu from "./islands/HeaderMenu.tsx"; -import * as $StateSelector from "./islands/StateSelector.tsx"; +import * as $Selector from "./islands/Selector.tsx"; import { type Manifest } from "$fresh/server.ts"; const manifest = { @@ -29,7 +29,7 @@ const manifest = { }, islands: { "./islands/HeaderMenu.tsx": $HeaderMenu, - "./islands/StateSelector.tsx": $StateSelector, + "./islands/Selector.tsx": $Selector, }, baseUrl: import.meta.url, } satisfies Manifest; diff --git a/src/islands/StateSelector.tsx b/src/islands/Selector.tsx similarity index 71% rename from src/islands/StateSelector.tsx rename to src/islands/Selector.tsx index 5f5f0cc4..6efb80f6 100644 --- a/src/islands/StateSelector.tsx +++ b/src/islands/Selector.tsx @@ -2,22 +2,34 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; import { Combobox, Transition } from "@headlessui/react"; import { useSignal } from "@preact/signals"; import { Fragment, type JSX } from "preact"; -import { type State, states } from "../utils/calc.ts"; import { IconCheck, IconChevronDown } from "../utils/icons.ts"; import { tw } from "../utils/tailwind.ts"; -export interface StateSelectorProps { - currentState?: State | undefined; +export interface SelectorProps { + name: string; + question: string; + list: SelectorListObject[]; + current?: U | undefined; + required?: boolean; } -export function StateSelector({ - currentState, -}: StateSelectorProps): JSX.Element { - const state = useSignal(currentState); +export interface SelectorListObject { + name: string; + value: T; +} + +export function Selector({ + name, + question, + list, + current: currentValue, + required, +}: SelectorProps): JSX.Element { + const current = useSignal(list.find((val) => val.name === currentValue)); const query = useSignal(""); - const filteredStates = states.filter((state) => - state + const filtered = list.filter((item) => + item.name .toLowerCase() .replace(/\s+/g, "") .includes(query.value.toLowerCase().replace(/\s+/g, "")), @@ -27,20 +39,20 @@ export function StateSelector({
{ - state.value = newState; + value={current.value} + onChange={(newValue) => { + current.value = newValue; }} > - - What state are you from? - + {question}
) => `${state.name}`} onChange={(event) => { if (event.target instanceof HTMLInputElement) { query.value = event.target.value; @@ -66,17 +78,17 @@ export function StateSelector({ query.value = ""; }} > - - {filteredStates.length === 0 && query.value !== "" ? ( + + {filtered.length === 0 && query.value !== "" ? (
No results found
) : ( - filteredStates.map((state) => ( + filtered.map((item) => ( {({ selected, active }) => ( <> @@ -85,9 +97,9 @@ export function StateSelector({ selected ? tw`font-medium` : tw`font-normal` }`} > - {state} + {item.name} - {selected ? ( + {selected && ( - ) : undefined} + )} )} diff --git a/src/routes/calculator.tsx b/src/routes/calculator.tsx index ebb79077..2f1905e9 100644 --- a/src/routes/calculator.tsx +++ b/src/routes/calculator.tsx @@ -1,11 +1,29 @@ import { Head } from "$fresh/runtime.ts"; import type { Handlers, PageProps } from "$fresh/server.ts"; import type { JSX } from "preact"; +import { useId } from "preact/compat"; +import { Checkbox } from "../components/Checkbox.tsx"; import { Cover } from "../components/Cover.tsx"; import { Meta } from "../components/Meta.tsx"; -import { StateSelector } from "../islands/StateSelector.tsx"; -import { type State, type StateData, stateData } from "../utils/calc.ts"; +import { Selector, type SelectorListObject } from "../islands/Selector.tsx"; +import { + type GeoCostBreakdown, + calculatePricingIfHardInstallation, + geothermalLoopType, +} from "../utils/calc/geo.ts"; +import { calculatePricingFromType } from "../utils/calc/geo.ts"; +import { calculatePricing } from "../utils/calc/geo.ts"; +import { calculatePricingIfRequiresPermit } from "../utils/calc/geo.ts"; +import { calculatePricingMultiplierFromArea } from "../utils/calc/geo.ts"; +import { + type State, + type StateData, + stateData, + states, +} from "../utils/calc/solar.ts"; import type { FreshContextHelper } from "../utils/handlers.ts"; +import { usdFormat } from "../utils/intl.ts"; +import { yearFormat } from "../utils/intl.ts"; import { getIpLocation } from "../utils/ip.ts"; import { isKey } from "../utils/type-helpers.ts"; @@ -18,7 +36,8 @@ export interface CalculatorSearchProps { export interface CalculatorShowProps { state: "display"; - regionData: StateData; + solarRegionData: StateData; + geoCostData: GeoCostBreakdown; } /** @@ -34,21 +53,40 @@ export const handler: Handlers = { const visitor = await getIpLocation(ctx.remoteAddr.hostname); const url = new URL(req.url); - const state = url.searchParams.get("region"); - if (state === null) { + const state = url.searchParams.get("region[value]"); + const isHilly = url.searchParams.get("hills"); + const renovations = url.searchParams.get("renovations"); + const geoType = geothermalLoopType.safeParse( + url.searchParams.get("geo-type[value]"), + ); + const squareFootage = url.searchParams.get("area"); + const requiresPermit = url.searchParams.get("permit"); + + if (!state) { return ctx.render({ state: "search", region: visitor?.region, }); } + if (!geoType.success || !squareFootage) { + return ctx.renderNotFound(); + } + if (!isKey(stateData, state)) { return ctx.renderNotFound(); } return ctx.render({ state: "display", - regionData: stateData[state], + solarRegionData: stateData[state], + geoCostData: { + isHilly: Boolean(isHilly), + needsRenovations: Boolean(renovations), + type: geoType.data, + squareFootage: Number(squareFootage), + requiresPermit: Boolean(requiresPermit), + }, }); }, }; @@ -67,7 +105,10 @@ export default function Calculator({ -
+
+ + Calculate and compare the pricing for solar and geothermal. +
@@ -80,42 +121,136 @@ function CalculatorPages(data: CalculatorProps): JSX.Element { return ; case "display": - return ; + return ; } } -function CalculatorDisplay(data: CalculatorShowProps): JSX.Element { +function CalculatorSolarDisplay(data: CalculatorShowProps): JSX.Element { return ( - - - - - - - - - - - - - - - - - - - - - - -
Monies
Energy FormTime to PayoffSavings per MonthInstall CostRebateEmissions per Month
Solar{data.regionData.payoff.toFixed(2)} Years${data.regionData.savings.toFixed(2)}${data.regionData.install.toFixed(2)}${data.regionData.rebate.toFixed(2)}{data.regionData.emissions.toFixed(2)} lbs of Carbon per kWh
+
+
+

Solar Power

+
+

Time to Payoff

+ {yearFormat.format(data.solarRegionData.payoff)} +
+
+

Savings per Month

+ {usdFormat.format(data.solarRegionData.savings)} +
+
+

Install Cost

+ {usdFormat.format(data.solarRegionData.install)} +
+
+

Rebate

+ {usdFormat.format(data.solarRegionData.rebate)} +
+
+

Emissions per Month

+ + {data.solarRegionData.emissions} pounds of Carbon per kilowatt-hour + +
+
+ +
+

Geothermal Energy

+
+

Base Price

+ + {usdFormat.format(calculatePricingFromType(data.geoCostData.type))} + +
+
+

Area-based Price Multiplier

+ + {calculatePricingMultiplierFromArea(data.geoCostData.squareFootage)} + × + +
+
+

Fees for Hard Installation

+ + {usdFormat.format( + calculatePricingIfHardInstallation( + data.geoCostData.isHilly, + data.geoCostData.needsRenovations, + ), + )} + +
+
+

Permit fees

+ + {usdFormat.format( + calculatePricingIfRequiresPermit(data.geoCostData.requiresPermit), + )} + +
+
+

Total Price

+ {usdFormat.format(calculatePricing(data.geoCostData))} +
+
+
); } function CalculatorSearch(data: CalculatorSearchProps): JSX.Element { + const labelForHill = useId(); + const labelForRenovation = useId(); + const labelForArea = useId(); + const labelForPermit = useId(); return (
- + => { + return { name: state, value: state }; + })} + current={data.region} + required + /> + + + +
+ + +
+