183 lines
6.2 KiB
Swift
183 lines
6.2 KiB
Swift
import Elementary
|
|
import ElementaryHTMX
|
|
import Routes
|
|
import Styleguide
|
|
|
|
struct AtticVentilationForm: HTML, Sendable {
|
|
let response: AtticVentilation.Response?
|
|
|
|
var content: some HTML {
|
|
FormHeader(label: "Attic Ventilation Calculator", svg: .wind)
|
|
|
|
// Form.
|
|
form(
|
|
.hx.post(route: .atticVentilation(.index)),
|
|
.hx.target("#result")
|
|
) {
|
|
div(.class("space-y-6")) {
|
|
div {
|
|
LabeledContent(label: "Pressure Differential: (Pascals - WRT outside)") {
|
|
Input(id: "pressureDifferential", placeholder: "Pressure differential")
|
|
.attributes(.type(.number), .step("0.1"), .autofocus, .required)
|
|
}
|
|
p(.class("text-sm text-light mt-2")) { "Positive values indicate higher attic pressure" }
|
|
}
|
|
|
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
|
div {
|
|
p(.class("font-bold mb-6")) { "Outdoor Conditions" }
|
|
LabeledContent(label: "Temperature: (°F)") {
|
|
Input(id: "outdoorTemperature", placeholder: "Outdoor temperature")
|
|
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
|
|
}
|
|
LabeledContent(label: "Dew Point: (°F)") {
|
|
Input(id: "outdoorDewpoint", placeholder: "Outdoor dewpoint")
|
|
.attributes(.type(.number), .step("0.1"), .required)
|
|
}
|
|
}
|
|
div {
|
|
p(.class("font-bold mb-6")) { "Attic Conditions" }
|
|
LabeledContent(label: "Temperature: (°F)") {
|
|
Input(id: "atticTemperature", placeholder: "Attic temperature")
|
|
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .required)
|
|
}
|
|
LabeledContent(label: "Dew Point: (°F)") {
|
|
Input(id: "atticDewpoint", placeholder: "Attic dewpoint")
|
|
.attributes(.type(.number), .step("0.1"), .required)
|
|
}
|
|
}
|
|
}
|
|
|
|
LabeledContent(label: "Attic Floor Area: (ft²)") {
|
|
Input(id: "atticFloorArea", placeholder: "Attic floor area")
|
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
|
}
|
|
|
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
|
LabeledContent(label: "Existing Intake Area: (ft²)") {
|
|
Input(id: "existingIntakeArea", placeholder: "Intake area (optional)")
|
|
.attributes(.class("mb-4"), .type(.number), .step("0.1"), .min("0.1"), .required)
|
|
}
|
|
LabeledContent(label: "Existing Exhaust Area: (ft²)") {
|
|
Input(id: "existingExhaustArea", placeholder: "Exhaust area (optional)")
|
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
|
}
|
|
}
|
|
|
|
div {
|
|
SubmitButton(label: "Calculate Ventilation")
|
|
}
|
|
}
|
|
}
|
|
|
|
div(.id("result")) {
|
|
if let response {
|
|
AtticVentilationResponse(response: response)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AtticVentilationResponse: HTML, Sendable {
|
|
let response: AtticVentilation.Response
|
|
|
|
var content: some HTML {
|
|
ResultContainer(reset: .atticVentilation(.index)) {
|
|
div(.class("space-y-6")) {
|
|
statusView()
|
|
requiredVentilationView()
|
|
WarningBox(warnings: response.warnings)
|
|
Note {
|
|
"""
|
|
Calculations are based on standard ventilation guidelines and building codes.
|
|
Local codes may vary. Consider consulting with a qualified professional for specific
|
|
recommendations.
|
|
"""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func statusView() -> some HTML {
|
|
div(
|
|
.class("""
|
|
w-full rounded-lg shadow-lg border-2 p-6 \(response.ventilationStatus.borderColor)
|
|
\(response.ventilationStatus.textColor) \(response.ventilationStatus.backgroundColor)
|
|
""")
|
|
) {
|
|
div(.class("flex text-2xl mb-6")) {
|
|
SVG(.thermometerSun, color: response.ventilationStatus.textColor)
|
|
h4(.class("font-extrabold ms-2")) { "Ventilation Status: \(response.ventilationStatus.rawValue.capitalized)" }
|
|
}
|
|
div(.class("grid justify-items-stretch grid-cols-1 md:grid-cols-3")) {
|
|
group("Temperature Differential", "\(double: response.temperatureDifferential, fractionDigits: 1) °F")
|
|
group("Dewpoint Differential", "\(double: response.dewpointDifferential, fractionDigits: 1) °F")
|
|
group("Pressure Differential", "\(double: response.pressureDifferential, fractionDigits: 1) °F")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func requiredVentilationView() -> some HTML {
|
|
div(
|
|
.class("""
|
|
w-full rounded-lg border p-6 border-blue-600 text-blue-600 bg-blue-100 dark:bg-blue-300
|
|
""")
|
|
) {
|
|
div(.class("flex text-2xl mb-6")) {
|
|
h4(.class("font-extrabold ms-2")) { "Required Ventilation" }
|
|
}
|
|
div(.class("grid grid-cols-1 md:grid-cols-2 content-center")) {
|
|
group("Intake", "\(double: response.requiredVentilationArea.intake, fractionDigits: 1) ft²")
|
|
group("Exhaust", "\(double: response.requiredVentilationArea.exhaust, fractionDigits: 1) ft²")
|
|
}
|
|
if response.recommendations.count > 0 {
|
|
div(.class("mt-8")) {
|
|
h4(.class("font-bold")) { "Recommendations:" }
|
|
ul(.class("list-disc mx-10")) {
|
|
for recommendation in response.recommendations {
|
|
li { recommendation }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func group(_ label: String, _ value: String) -> some HTML<HTMLTag.div> {
|
|
div(.class("justify-self-center")) {
|
|
div(.class("grid grid-cols-1 justify-items-center")) {
|
|
p(.class("font-medium")) { label }
|
|
h3(.class("text-3xl font-extrabold")) { value }
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private extension AtticVentilation.VentilationStatus {
|
|
var backgroundColor: String {
|
|
switch self {
|
|
case .adequate: return "bg-green-200"
|
|
case .inadequate: return "bg-amber-200"
|
|
case .critical: return "bg-red-200"
|
|
}
|
|
}
|
|
|
|
var borderColor: String {
|
|
switch self {
|
|
case .adequate: return "border-green-500"
|
|
case .inadequate: return "border-amber-500"
|
|
case .critical: return "border-red-500"
|
|
}
|
|
}
|
|
|
|
var textColor: String {
|
|
switch self {
|
|
case .adequate: return "text-green-500"
|
|
case .inadequate: return "text-amber-500"
|
|
case .critical: return "text-red-500"
|
|
}
|
|
}
|
|
|
|
}
|