Files
swift-hvac-toolbox/Sources/ViewController/Views/MoldRisk.swift

177 lines
5.6 KiB
Swift

import Elementary
import PsychrometricClient
import Routes
import Styleguide
struct MoldRiskForm: HTML {
let response: MoldRisk.Response?
var content: some HTML {
FormHeader(label: "Mold Risk Calculator", svg: .thermometer)
form(
// Using index to get the correct path, but really uses the `submit` end-point.
.hx.post(route: .moldRisk(.index)),
.hx.target("#result")
) {
div(.class("space-y-6")) {
LabeledContent(label: "Indoor Temperature (°F)") {
Input(id: "temperature", placeholder: "Dry bulb temperature")
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus)
}
LabeledContent(label: "Indor Humdity (%)") {
Input(id: "humidity", placeholder: "Relative humidity")
.attributes(.type(.number), .step("0.1"), .min("0.1"))
}
div {
SubmitButton(label: "Calculate Mold Risk")
}
}
}
div(.id("result")) {
if let response {
MoldRiskResponse(response: response)
}
}
}
}
struct MoldRiskResponse: HTML {
let response: MoldRisk.Response
var content: some HTML {
ResultContainer(reset: .moldRisk(.index)) {
div(
.class("""
p-2 rounded-lg shadow-lg \(response.riskLevel.backgroundColor) border-2 border \(response.riskLevel.borderColor)
""")
) {
// Risk level and days to mold header.
div(.class("\(response.riskLevel.textColor)")) {
div(.class("flex flex-wrap mt-2")) {
div(.class("w-full sm:w-1/2 flex gap-2")) {
SVG(.exclamation, color: "\(response.riskLevel.textColor)")
span(.class("text-lg font-extrabold")) {
"Risk Level: \(response.riskLevel.rawValue.capitalized)"
}
}
if let daysToMold = response.daysToMold {
div(.class("w-full sm:w-1/2 gap-2")) {
span(.class("font-semibold")) { "Estimated Days to Mold Growth: " }
span { "\(daysToMold) days" }
}
}
}
// Recommendations
if response.recommendations.count > 0 {
div(.class("mt-6 pb-4")) {
p(.class("font-semibold mb-4")) {
u {
"Recommendation\(response.recommendations.count == 1 ? "" : "s"):"
}
}
ul(.class("list-disc mx-10")) {
for recommendation in response.recommendations {
li { recommendation }
}
}
}
}
}
}
// Display psychrometric properties.
PsychrometricPropertiesGrid(properties: response.psychrometricProperties)
.attributes(.class("mx-6"))
// Disclaimer.
div(.class("mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md border border-blue-500")) {
p(.class("text-sm text-blue-500")) {
span(.class("font-extrabold pe-2")) { "Note:" }
"""
These calculations are based on typical indoor conditions and common mold species. Actual mold growth can
vary based on surface materials, air movement, and other environmental factors. Always address moisture
issues promptly and consult professionals for severe cases.
"""
}
}
}
}
}
private extension MoldRisk.RiskLevel {
var backgroundColor: String {
switch self {
case .low: return "bg-green-200"
case .moderate: return "bg-amber-200"
case .high: return "bg-orange-200"
case .severe: return "bg-red-200"
}
}
var borderColor: String {
switch self {
case .low: return "border-green-500"
case .moderate: return "border-amber-500"
case .high: return "border-orange-500"
case .severe: return "border-red-500"
}
}
var textColor: String {
switch self {
case .low: return "text-green-500"
case .moderate: return "text-amber-500"
case .high: return "text-orange-500"
case .severe: return "text-red-500"
}
}
}
struct PsychrometricPropertiesGrid: HTML {
let properties: PsychrometricProperties
var content: some HTML<HTMLTag.div> {
div(.class("pt-8")) {
p(.class("text-xl font-semibold border-b")) {
"Psychrometric Properties:"
}
div(.class("w-full mt-6 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4")) {
displayProperty("Dew Point", \.dewPoint.rawValue)
displayProperty("Wet Bulb", \.wetBulb.rawValue)
displayProperty("Enthalpy", \.enthalpy.rawValue)
displayProperty("Density", \.density.rawValue)
displayProperty("Vapor Pressure", \.vaporPressure.rawValue)
displayProperty("Specific Volume", properties.specificVolume.rawValue)
displayProperty("Absolute Humidity", \.absoluteHumidity)
displayProperty("Humidity Ratio", properties.humidityRatio.value)
displayProperty("Degree of Saturation", properties.degreeOfSaturation.value)
}
}
}
private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML {
let symbol = "\(symbol == nil ? "" : " \(symbol!)")"
return p(.class("text-blue-500 dark:text-slate-200")) {
span(.class("font-semibold")) { "\(label): " }
span(.class("font-light")) {
"\(double: value)\(symbol)"
}
}
}
private func displayProperty<N: NumberWithUnitOfMeasure>(
_ label: String,
_ keyPath: KeyPath<PsychrometricProperties, N>
) -> some HTML where N.Number == Double, N.Units: RawRepresentable, N.Units.RawValue == String {
let property = properties[keyPath: keyPath]
return displayProperty(label, property.rawValue, property.units.rawValue)
}
}