175 lines
5.5 KiB
Swift
175 lines
5.5 KiB
Swift
import Elementary
|
|
import ElementaryHTMX
|
|
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: "Indoor 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.
|
|
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)
|
|
}
|
|
|
|
}
|