feat: Begins hvac system performance
This commit is contained in:
@@ -59,6 +59,14 @@ let package = Package(
|
|||||||
],
|
],
|
||||||
swiftSettings: swiftSettings
|
swiftSettings: swiftSettings
|
||||||
),
|
),
|
||||||
|
.target(
|
||||||
|
name: "HTMLSnapshotTesting",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Elementary", package: "elementary"),
|
||||||
|
.product(name: "SnapshotTesting", package: "swift-snapshot-testing")
|
||||||
|
],
|
||||||
|
swiftSettings: swiftSettings
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "Routes",
|
name: "Routes",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -29,6 +29,11 @@ extension ApiController: DependencyKey {
|
|||||||
case let .calculateDehumidifierSize(request):
|
case let .calculateDehumidifierSize(request):
|
||||||
logger.debug("Calculating dehumidifier size: \(request)")
|
logger.debug("Calculating dehumidifier size: \(request)")
|
||||||
return try await request.respond(logger)
|
return try await request.respond(logger)
|
||||||
|
|
||||||
|
case let .calculateHVACSystemPerformance(request):
|
||||||
|
logger.debug("Calculating hvac system performance: \(request)")
|
||||||
|
fatalError()
|
||||||
|
|
||||||
case let .calculateMoldRisk(request):
|
case let .calculateMoldRisk(request):
|
||||||
logger.debug("Calculating mold risk: \(request)")
|
logger.debug("Calculating mold risk: \(request)")
|
||||||
return try await psychrometricClient.respond(request, logger)
|
return try await psychrometricClient.respond(request, logger)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public extension DehumidifierSize.Request {
|
|||||||
|
|
||||||
addDefaultWarnings(&warnings)
|
addDefaultWarnings(&warnings)
|
||||||
|
|
||||||
|
// TODO: Return an error if required capacity is larger than biggest residential dehumidifier.
|
||||||
// TODO: Return early here ??
|
// TODO: Return early here ??
|
||||||
if requiredCapacity > 205 {
|
if requiredCapacity > 205 {
|
||||||
logger.debug("Required capacity exceeds residential unit.")
|
logger.debug("Required capacity exceeds residential unit.")
|
||||||
@@ -80,10 +81,15 @@ public extension DehumidifierSize.Request {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func parseRecommendedSize(_ requiredCapacity: Double) -> (Int, String) {
|
private func parseRecommendedSize(_ requiredCapacity: Double) -> (Int, String) {
|
||||||
for (key, value) in Self.dehumidifierSizes where Double(key) >= requiredCapacity {
|
// TODO: Return an error if required capacity is larger than biggest residential dehumidifier.
|
||||||
return (key, value)
|
|
||||||
}
|
// Ensure the keys are sorted, otherwise it gives unpredictable results.
|
||||||
return (205, Self.dehumidifierSizes[205]!)
|
let key = Self.dehumidifierSizes.keys.sorted()
|
||||||
|
.first { Double($0) >= requiredCapacity }
|
||||||
|
// Use the largest dehumidifier because the requirement is larger than the largest key.
|
||||||
|
?? 255
|
||||||
|
|
||||||
|
return (key, Self.dehumidifierSizes[key]!)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addDefaultWarnings(_ warnings: inout [String]) {
|
private func addDefaultWarnings(_ warnings: inout [String]) {
|
||||||
|
|||||||
22
Sources/HTMLSnapshotTesting/HTMLSnapshotTesting.swift
Normal file
22
Sources/HTMLSnapshotTesting/HTMLSnapshotTesting.swift
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import Elementary
|
||||||
|
@preconcurrency import SnapshotTesting
|
||||||
|
|
||||||
|
public extension Snapshotting where Value == (any HTML), Format == String {
|
||||||
|
static var html: Snapshotting {
|
||||||
|
var snapshotting = SimplySnapshotting.lines
|
||||||
|
.pullback { (html: any HTML) in html.renderFormatted() }
|
||||||
|
|
||||||
|
snapshotting.pathExtension = "html"
|
||||||
|
return snapshotting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Snapshotting where Value == String, Format == String {
|
||||||
|
static var html: Snapshotting {
|
||||||
|
var snapshotting = SimplySnapshotting.lines
|
||||||
|
.pullback { $0 }
|
||||||
|
|
||||||
|
snapshotting.pathExtension = "html"
|
||||||
|
return snapshotting
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,3 +42,15 @@ public enum DehumidifierSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
|
||||||
|
public extension DehumidifierSize.Response {
|
||||||
|
|
||||||
|
static var mock: Self {
|
||||||
|
.init(requiredCapacity: 100, pintsPerDay: 100, recommendedSize: 100, recommendedUrl: "#", warnings: [
|
||||||
|
"A warning.", "B warning"
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
87
Sources/Routes/HVACSystemPerformance.swift
Normal file
87
Sources/Routes/HVACSystemPerformance.swift
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import PsychrometricClient
|
||||||
|
|
||||||
|
public enum HVACSystemPerformance {
|
||||||
|
|
||||||
|
public struct Request: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let altitude: Double?
|
||||||
|
public let airflow: Double
|
||||||
|
public let returnAirTemperature: Double
|
||||||
|
public let returnAirHumidity: Double
|
||||||
|
public let supplyAirTemperature: Double
|
||||||
|
public let supplyAirHumidity: Double
|
||||||
|
public let systemSize: Double
|
||||||
|
|
||||||
|
public init(
|
||||||
|
altitude: Double? = nil,
|
||||||
|
airflow: Double,
|
||||||
|
returnAirTemperature: Double,
|
||||||
|
returnAirHumidity: Double,
|
||||||
|
supplyAirTemperature: Double,
|
||||||
|
supplyAirHumidity: Double,
|
||||||
|
systemSize: Double
|
||||||
|
) {
|
||||||
|
self.altitude = altitude
|
||||||
|
self.airflow = airflow
|
||||||
|
self.returnAirTemperature = returnAirTemperature
|
||||||
|
self.returnAirHumidity = returnAirHumidity
|
||||||
|
self.supplyAirTemperature = supplyAirTemperature
|
||||||
|
self.supplyAirHumidity = supplyAirHumidity
|
||||||
|
self.systemSize = systemSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Response: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let returnAirProperties: PsychrometricProperties
|
||||||
|
public let supplyAirProperties: PsychrometricProperties
|
||||||
|
public let capacity: Capacity
|
||||||
|
public let systemMetrics: SystemMetrics
|
||||||
|
|
||||||
|
public init(
|
||||||
|
returnAirProperties: PsychrometricProperties,
|
||||||
|
supplyAirProperties: PsychrometricProperties,
|
||||||
|
capacity: HVACSystemPerformance.Capacity,
|
||||||
|
systemMetrics: HVACSystemPerformance.SystemMetrics
|
||||||
|
) {
|
||||||
|
self.returnAirProperties = returnAirProperties
|
||||||
|
self.supplyAirProperties = supplyAirProperties
|
||||||
|
self.capacity = capacity
|
||||||
|
self.systemMetrics = systemMetrics
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Capacity: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let total: Double
|
||||||
|
public let sensible: Double
|
||||||
|
public let latent: Double
|
||||||
|
public var shr: Double { sensible / total }
|
||||||
|
|
||||||
|
public init(total: Double, sensible: Double, latent: Double) {
|
||||||
|
self.total = total
|
||||||
|
self.sensible = sensible
|
||||||
|
self.latent = latent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct SystemMetrics: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
public let cfmPerTon: Double
|
||||||
|
public let targetTemperatureSplit: Double
|
||||||
|
public let actualTemperatureSplit: Double
|
||||||
|
public let condensationRate: Double
|
||||||
|
|
||||||
|
public init(
|
||||||
|
cfmPerTon: Double,
|
||||||
|
targetTemperatureSplit: Double,
|
||||||
|
actualTemperatureSplit: Double,
|
||||||
|
condensationRate: Double
|
||||||
|
) {
|
||||||
|
self.cfmPerTon = cfmPerTon
|
||||||
|
self.targetTemperatureSplit = targetTemperatureSplit
|
||||||
|
self.actualTemperatureSplit = actualTemperatureSplit
|
||||||
|
self.condensationRate = condensationRate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Sources/Routes/HVACSystemSize.swift
Normal file
11
Sources/Routes/HVACSystemSize.swift
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
public enum HVACSystemSize: Double, Codable, Equatable, Sendable {
|
||||||
|
case one = 1
|
||||||
|
case oneAndAHalf = 1.5
|
||||||
|
case two = 2
|
||||||
|
case twoAndAHalf = 2.5
|
||||||
|
case three = 3
|
||||||
|
case threeAndAHalf = 3.5
|
||||||
|
case four = 4
|
||||||
|
case fourAndAHalf = 4.5
|
||||||
|
case five = 5
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ public extension SiteRoute {
|
|||||||
enum Api: Equatable, Sendable {
|
enum Api: Equatable, Sendable {
|
||||||
|
|
||||||
case calculateDehumidifierSize(DehumidifierSize.Request)
|
case calculateDehumidifierSize(DehumidifierSize.Request)
|
||||||
|
case calculateHVACSystemPerformance(HVACSystemPerformance.Request)
|
||||||
case calculateMoldRisk(MoldRisk.Request)
|
case calculateMoldRisk(MoldRisk.Request)
|
||||||
|
|
||||||
static let rootPath = Path { "api"; "v1" }
|
static let rootPath = Path { "api"; "v1" }
|
||||||
@@ -34,6 +35,11 @@ public extension SiteRoute {
|
|||||||
Method.post
|
Method.post
|
||||||
Body(.json(DehumidifierSize.Request.self))
|
Body(.json(DehumidifierSize.Request.self))
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.calculateHVACSystemPerformance)) {
|
||||||
|
Path { "api"; "v1"; "calculateHVACSystemPerformance" }
|
||||||
|
Method.post
|
||||||
|
Body(.json(HVACSystemPerformance.Request.self))
|
||||||
|
}
|
||||||
Route(.case(Self.calculateMoldRisk)) {
|
Route(.case(Self.calculateMoldRisk)) {
|
||||||
Path { "api"; "v1"; "calculateMoldRisk" }
|
Path { "api"; "v1"; "calculateMoldRisk" }
|
||||||
Method.post
|
Method.post
|
||||||
@@ -48,6 +54,7 @@ public extension SiteRoute {
|
|||||||
|
|
||||||
case index
|
case index
|
||||||
case dehumidifierSize(DehumidifierSize)
|
case dehumidifierSize(DehumidifierSize)
|
||||||
|
case hvacSystemPerformance(HVACSystemPerformance)
|
||||||
case moldRisk(MoldRisk)
|
case moldRisk(MoldRisk)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
@@ -57,6 +64,9 @@ public extension SiteRoute {
|
|||||||
Route(.case(Self.dehumidifierSize)) {
|
Route(.case(Self.dehumidifierSize)) {
|
||||||
DehumidifierSize.router
|
DehumidifierSize.router
|
||||||
}
|
}
|
||||||
|
Route(.case(Self.hvacSystemPerformance)) {
|
||||||
|
HVACSystemPerformance.router
|
||||||
|
}
|
||||||
Route(.case(Self.moldRisk)) {
|
Route(.case(Self.moldRisk)) {
|
||||||
MoldRisk.router
|
MoldRisk.router
|
||||||
}
|
}
|
||||||
@@ -66,7 +76,7 @@ public extension SiteRoute {
|
|||||||
case index
|
case index
|
||||||
case submit(Routes.DehumidifierSize.Request)
|
case submit(Routes.DehumidifierSize.Request)
|
||||||
|
|
||||||
static let rootPath = "dehumidifier-size"
|
static let rootPath = "dehumidifier-sizing"
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.index)) {
|
Route(.case(Self.index)) {
|
||||||
@@ -88,6 +98,36 @@ public extension SiteRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HVACSystemPerformance: Equatable, Sendable {
|
||||||
|
case index
|
||||||
|
case submit(Routes.HVACSystemPerformance.Request)
|
||||||
|
|
||||||
|
static let rootPath = "hvac-system-performance"
|
||||||
|
|
||||||
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.index)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.get
|
||||||
|
}
|
||||||
|
Route(.case(Self.submit)) {
|
||||||
|
Path { rootPath }
|
||||||
|
Method.post
|
||||||
|
Body {
|
||||||
|
FormData {
|
||||||
|
Optionally { Field("altitude") { Double.parser() } }
|
||||||
|
Field("airflow") { Double.parser() }
|
||||||
|
Field("returnAirTemperature") { Double.parser() }
|
||||||
|
Field("returnAirHumidity") { Double.parser() }
|
||||||
|
Field("supplyAirTemperature") { Double.parser() }
|
||||||
|
Field("supplyAirHumidity") { Double.parser() }
|
||||||
|
Field("systemSize") { Double.parser() }
|
||||||
|
}
|
||||||
|
.map(.memberwise(Routes.HVACSystemPerformance.Request.init))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum MoldRisk: Equatable, Sendable {
|
public enum MoldRisk: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
case submit(Routes.MoldRisk.Request)
|
case submit(Routes.MoldRisk.Request)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Elementary
|
import Elementary
|
||||||
|
|
||||||
|
// TODO: Experiment with removing text colors and move them further up the call stack.
|
||||||
|
|
||||||
/// A styled header for a form element, which consists of an
|
/// A styled header for a form element, which consists of an
|
||||||
/// svg image and label / name for the form.
|
/// svg image and label / name for the form.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ extension ViewController: DependencyKey {
|
|||||||
return DehumidifierSizeResult(response: response)
|
return DehumidifierSizeResult(response: response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case let .hvacSystemPerformance(route):
|
||||||
|
switch route {
|
||||||
|
case .index:
|
||||||
|
return request.respond(HVACSystemPerformanceForm())
|
||||||
|
case .submit:
|
||||||
|
return HVACSystemPerformanceResult()
|
||||||
|
}
|
||||||
|
|
||||||
case let .moldRisk(route):
|
case let .moldRisk(route):
|
||||||
switch route {
|
switch route {
|
||||||
case .index:
|
case .index:
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import Routes
|
|||||||
import Styleguide
|
import Styleguide
|
||||||
|
|
||||||
struct DehumidifierSizeForm: HTML {
|
struct DehumidifierSizeForm: HTML {
|
||||||
|
|
||||||
|
let response: DehumidifierSize.Response?
|
||||||
|
|
||||||
|
init(response: DehumidifierSize.Response? = nil) {
|
||||||
|
self.response = response
|
||||||
|
}
|
||||||
|
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
FormHeader(label: "Dehumidifier Sizing Calculator", svg: .droplets)
|
FormHeader(label: "Dehumidifier Sizing Calculator", svg: .droplets)
|
||||||
form(
|
form(
|
||||||
@@ -32,7 +39,11 @@ struct DehumidifierSizeForm: HTML {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div(.id("result")) {}
|
div(.id("result")) {
|
||||||
|
if let response {
|
||||||
|
DehumidifierSizeResult(response: response)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,16 +52,59 @@ struct DehumidifierSizeResult: HTML {
|
|||||||
|
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
ResultContainer(reset: .dehumidifierSize(.index)) {
|
ResultContainer(reset: .dehumidifierSize(.index)) {
|
||||||
div(.class("""
|
div(.class("py-4 block")) {
|
||||||
p-2 rounded-lg shadow-lg bg-blue-500
|
// Display calculated metrics.
|
||||||
"""
|
div(.class("""
|
||||||
)) {
|
mb-6 sm:grid sm:grid-cols-1 sm:gap-4 lg:flex items-center lg:justify-between
|
||||||
p { "Recommended Size: \(response.recommendedSize)" }
|
text-blue-500 dark:text-slate-200
|
||||||
|
""")) {
|
||||||
|
div {
|
||||||
|
span(.class("font-semibold")) { "Base Moisture Load: " }
|
||||||
|
span(.class("font-light")) { "\(double: response.pintsPerDay) pints/day" }
|
||||||
|
}
|
||||||
|
if response.requiredCapacity != response.pintsPerDay {
|
||||||
|
div {
|
||||||
|
span(.class("font-semibold")) { "Required Capacity (derated): " }
|
||||||
|
span(.class("font-light")) { "\(double: response.requiredCapacity) pints/day" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p(.class("font-semibold mb-4 dark:text-yellow-300")) { "Recommended Size:" }
|
||||||
|
|
||||||
|
a(
|
||||||
|
.target("_blank"), .href(response.recommendedUrl ?? "#"), .rel("noopener noreferrer")
|
||||||
|
) {
|
||||||
|
div(.class("""
|
||||||
|
px-8 py-2 flex items-center justify-between border border-blue-700 rounded-lg shadow-lg group
|
||||||
|
dark:bg-blue-400 hover:bg-blue-300 hover:dark:bg-blue-600
|
||||||
|
transition-colors
|
||||||
|
""")) {
|
||||||
|
div {
|
||||||
|
span(.class("font-extrabold text-4xl text-blue-800")) { "\(response.recommendedSize) PPD" }
|
||||||
|
p(.class("text-sm mt-1")) { "Click to view recommended model →" }
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.class("""
|
||||||
|
w-12 h-12 rounded-full flex items-center justify-center
|
||||||
|
bg-blue-500 dark:bg-blue-600
|
||||||
|
group-hover:bg-blue-600 group-hover:dark:bg-blue-700
|
||||||
|
transition-colors
|
||||||
|
""")) {
|
||||||
|
SVG(.droplets, color: .white)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display warnings, if applicable
|
// Display warnings, if applicable
|
||||||
if response.warnings.count > 0 {
|
if response.warnings.count > 0 {
|
||||||
div(.class("w-full mt-8 p-4 rounded-lg shadow-lg text-amber-500 bg-amber-200 border border-amber-500")) {
|
div(.class("""
|
||||||
|
w-full mt-6 p-4 rounded-lg shadow-lg
|
||||||
|
text-amber-500
|
||||||
|
bg-amber-100 dark:bg-amber-200
|
||||||
|
border border-amber-500
|
||||||
|
""")) {
|
||||||
span(.class("font-semibold mb-4 border-b")) { "Warning\(response.warnings.count > 1 ? "s:" : ":")" }
|
span(.class("font-semibold mb-4 border-b")) { "Warning\(response.warnings.count > 1 ? "s:" : ":")" }
|
||||||
ul(.class("list-disc mx-10")) {
|
ul(.class("list-disc mx-10")) {
|
||||||
for warning in response.warnings {
|
for warning in response.warnings {
|
||||||
|
|||||||
62
Sources/ViewController/Views/HVACSystemPerformance.swift
Normal file
62
Sources/ViewController/Views/HVACSystemPerformance.swift
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Elementary
|
||||||
|
import ElementaryHTMX
|
||||||
|
import Routes
|
||||||
|
import Styleguide
|
||||||
|
|
||||||
|
struct HVACSystemPerformanceForm: HTML, Sendable {
|
||||||
|
|
||||||
|
var content: some HTML {
|
||||||
|
FormHeader(label: "HVAC System Performance", svg: .thermometerSun)
|
||||||
|
form(
|
||||||
|
// Using index to get the correct path, but really uses the `submit` end-point.
|
||||||
|
.hx.post(route: .hvacSystemPerformance(.index)),
|
||||||
|
.hx.target("#result")
|
||||||
|
) {
|
||||||
|
div(.class("space-y-6")) {
|
||||||
|
div(.class("grid grid-cols-1 md:grid-cols-3 gap-4")) {
|
||||||
|
LabeledContent(label: "System Size (Tons)") {
|
||||||
|
Input(id: "systemSize", placeholder: "System size")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"), .autofocus, .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Airflow (CFM)") {
|
||||||
|
Input(id: "airflow", placeholder: "Airflow")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"), .required)
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Altitude (ft.)") {
|
||||||
|
Input(id: "altitude", placeholder: "Altitude (Optional)")
|
||||||
|
.attributes(.type(.number), .min("1"), .step("0.5"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div(.class("grid grid-cols-1 md:grid-cols-2 gap-4")) {
|
||||||
|
div(.class("space-y-4")) {
|
||||||
|
h3(.class("text-lg font-medium")) { "Return Air" }
|
||||||
|
LabeledContent(label: "Dry Bulb (°F)") {
|
||||||
|
Input(id: "returnAirTemperature", placeholder: "Return temperature")
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Indoor Humdity (%)") {
|
||||||
|
Input(id: "returnAirHumidity", placeholder: "Return humidity")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.class("space-y-4")) {
|
||||||
|
h3(.class("text-lg font-medium")) { "Supply Air" }
|
||||||
|
LabeledContent(label: "Dry Bulb (°F)") {
|
||||||
|
Input(id: "supplyAirTemperature", placeholder: "Supply temperature")
|
||||||
|
}
|
||||||
|
LabeledContent(label: "Indoor Humdity (%)") {
|
||||||
|
Input(id: "supplyAirHumidity", placeholder: "Supply humidity")
|
||||||
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
SubmitButton(label: "Calculate Performance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div(.id("result")) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HVACSystemPerformanceResult: HTML, Sendable {}
|
||||||
@@ -77,6 +77,14 @@ private struct Header: HTML {
|
|||||||
"Dehumidifier-Sizing"
|
"Dehumidifier-Sizing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
|
a(
|
||||||
|
.class("hover:border-b \(border: .yellow)"),
|
||||||
|
.hx.get(route: .hvacSystemPerformance(.index)), .hx.target("#content"), .hx.pushURL(true)
|
||||||
|
) {
|
||||||
|
"HVAC-System-Performance"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +96,7 @@ private struct PageContent<Body: HTML>: HTML where Body: Sendable {
|
|||||||
|
|
||||||
var content: some HTML {
|
var content: some HTML {
|
||||||
div(.class("mx-5 lg:mx-20")) {
|
div(.class("mx-5 lg:mx-20")) {
|
||||||
div(.class("rounded-xl shadow-lg bg-white dark:bg-slate-700 p-8")) {
|
div(.class("rounded-xl shadow-lg bg-white dark:bg-slate-700 p-8 text-gray-600 dark:text-slate-200")) {
|
||||||
div(.id("content")) {
|
div(.id("content")) {
|
||||||
body()
|
body()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ struct MoldRiskForm: HTML {
|
|||||||
div(.class("space-y-6")) {
|
div(.class("space-y-6")) {
|
||||||
LabeledContent(label: "Indoor Temperature (°F)") {
|
LabeledContent(label: "Indoor Temperature (°F)") {
|
||||||
Input(id: "temperature", placeholder: "Dry bulb temperature")
|
Input(id: "temperature", placeholder: "Dry bulb temperature")
|
||||||
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus)
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .autofocus, .required)
|
||||||
}
|
}
|
||||||
|
|
||||||
LabeledContent(label: "Indoor Humdity (%)") {
|
LabeledContent(label: "Indoor Humdity (%)") {
|
||||||
Input(id: "humidity", placeholder: "Relative humidity")
|
Input(id: "humidity", placeholder: "Relative humidity")
|
||||||
.attributes(.type(.number), .step("0.1"), .min("0.1"))
|
.attributes(.type(.number), .step("0.1"), .min("0.1"), .required)
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
|
|||||||
Reference in New Issue
Block a user