Compare commits
8 Commits
e0e5b10a34
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
a44cc6975d
|
|||
|
159031a023
|
|||
|
bdbe89e101
|
|||
|
ce31efa005
|
|||
|
f34e0a8b25
|
|||
|
35a54db9a3
|
|||
|
b42b9cf03b
|
|||
|
ee577003d5
|
32
.github/workflows/ci.yaml
vendored
Normal file
32
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
macOS:
|
||||
name: macOS
|
||||
runs-on: macOS-latest
|
||||
strategy:
|
||||
matrix:
|
||||
xcode: ['16.2']
|
||||
config: ['debug', 'release']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Select Xcode ${{ matrix.xcode }}
|
||||
run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app
|
||||
- name: Run ${{ matrix.xcode }} Tests for ${{ matrix.config }}
|
||||
run: swift test
|
||||
|
||||
ubuntu:
|
||||
name: ubuntu
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build docker test image.
|
||||
run: docker build -t 'hvac-toolbox:test' -f Dockerfile.dev .
|
||||
- name: Run tests
|
||||
run: docker run -i 'hvac-toolbox:test' swift test
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,3 +11,4 @@ db.sqlite
|
||||
!.env.example
|
||||
.vscode
|
||||
node_modules
|
||||
.nvim
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
# NOTE: Builds currently fail when building in release mode.
|
||||
|
||||
ARG SWIFT_MODE="debug"
|
||||
ARG SWIFT_MODE="release"
|
||||
|
||||
# ================================
|
||||
# Build image
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
# NOTE: Builds currently fail when building in release mode.
|
||||
|
||||
ARG SWIFT_MODE="debug"
|
||||
# ================================
|
||||
# Build image
|
||||
@@ -29,70 +27,6 @@ COPY . .
|
||||
|
||||
# Build the application, with optimizations, with static linking, and using jemalloc
|
||||
# N.B.: The static version of jemalloc is incompatible with the static Swift runtime.
|
||||
RUN --mount=type=cache,target=/build/.build swift build \
|
||||
-c ${SWIFT_MODE} \
|
||||
--product App \
|
||||
--static-swift-stdlib \
|
||||
-Xswiftc -g
|
||||
RUN swift build
|
||||
|
||||
# Switch to the staging area
|
||||
WORKDIR /staging
|
||||
|
||||
# Copy main executable to staging area
|
||||
RUN --mount=type=cache,target=/build/.build cp \
|
||||
"$(swift build --package-path /build -c ${SWIFT_MODE} --show-bin-path)/App" ./
|
||||
|
||||
# Copy static swift backtracer binary to staging area
|
||||
RUN --mount=type=cache,target=/build/.build \
|
||||
cp "/usr/libexec/swift/linux/swift-backtrace-static" ./
|
||||
|
||||
# Copy resources bundled by SPM to staging area
|
||||
RUN --mount=type=cache,target=/build/.build \
|
||||
find -L "$(swift build --package-path /build -c ${SWIFT_MODE} --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \;
|
||||
|
||||
# Copy any resources from the public directory and views directory if the directories exist
|
||||
# Ensure that by default, neither the directory nor any of its contents are writable.
|
||||
RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true
|
||||
RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true
|
||||
|
||||
# ================================
|
||||
# Run image
|
||||
# ================================
|
||||
FROM ubuntu:noble
|
||||
|
||||
# Make sure all system packages are up to date, and install only essential packages.
|
||||
RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \
|
||||
&& apt-get -q update \
|
||||
&& apt-get -q dist-upgrade -y \
|
||||
&& apt-get -q install -y \
|
||||
libjemalloc2 \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
# If your app or its dependencies import FoundationNetworking, also install `libcurl4`.
|
||||
libcurl4 \
|
||||
curl \
|
||||
# If your app or its dependencies import FoundationXML, also install `libxml2`.
|
||||
# libxml2 \
|
||||
&& rm -r /var/lib/apt/lists/*
|
||||
|
||||
# Create a vapor user and group with /app as its home directory
|
||||
RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor
|
||||
|
||||
# Switch to the new home directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy built executable and any staged resources from builder
|
||||
COPY --from=build --chown=vapor:vapor /staging /app
|
||||
|
||||
# Provide configuration needed by the built-in crash reporter and some sensible default behaviors.
|
||||
ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static
|
||||
|
||||
# Ensure all further commands run as the vapor user
|
||||
USER vapor:vapor
|
||||
|
||||
# Let Docker bind to port 8080
|
||||
EXPOSE 8080
|
||||
|
||||
# Start the Vapor service when the image is run, default to listening on 8080 in production environment
|
||||
ENTRYPOINT ["./App"]
|
||||
CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"]
|
||||
CMD ["swift", "test"]
|
||||
|
||||
@@ -114,6 +114,17 @@ let package = Package(
|
||||
.product(name: "ElementaryHTMX", package: "elementary-htmx")
|
||||
],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "ViewControllerTests",
|
||||
dependencies: [
|
||||
"HTMLSnapshotTesting",
|
||||
"ViewController"
|
||||
],
|
||||
resources: [
|
||||
.copy("__Snapshots__")
|
||||
],
|
||||
swiftSettings: swiftSettings
|
||||
)
|
||||
],
|
||||
swiftLanguageModes: [.v5]
|
||||
|
||||
File diff suppressed because one or more lines are too long
38
Sources/ApiController/Extensions/FeetOfHead.swift
Normal file
38
Sources/ApiController/Extensions/FeetOfHead.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import Dependencies
|
||||
import Logging
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
|
||||
public extension FeetOfHead.Request {
|
||||
|
||||
private static let specificGravity = 1.02
|
||||
|
||||
func respond(logger: Logger) async throws -> FeetOfHead.Response {
|
||||
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||
|
||||
try validate()
|
||||
let waterTemperature = DryBulb.fahrenheit(self.waterTemperature ?? 60)
|
||||
let density = try await psychrometricClient.density.water(waterTemperature)
|
||||
let feetOfHead = pressure / ((density.value / 144) * Self.specificGravity)
|
||||
var warnings = [String]()
|
||||
|
||||
if self.waterTemperature == nil {
|
||||
warnings.append(
|
||||
"Calculations are based on 60°F water temperature."
|
||||
)
|
||||
}
|
||||
|
||||
return .init(feetOfHead: feetOfHead, density: density, warnings: warnings)
|
||||
}
|
||||
|
||||
private func validate() throws {
|
||||
guard pressure > 0 else {
|
||||
throw ValidationError(message: "Pressure should be greater than 0.")
|
||||
}
|
||||
if let waterTemperature {
|
||||
guard waterTemperature > 32 else {
|
||||
throw ValidationError(message: "Water temperature should be above freezing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import Dependencies
|
||||
import Logging
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
|
||||
public extension HydronicSystemPressure.Request {
|
||||
|
||||
func respond(logger: Logger) async throws -> HydronicSystemPressure.Response {
|
||||
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||
|
||||
try validate()
|
||||
|
||||
let waterTemperature = DryBulb.fahrenheit(self.waterTemperature ?? 60)
|
||||
|
||||
let density = try await psychrometricClient.density.water(waterTemperature)
|
||||
let pressure = height * (density.value / 144) + 5
|
||||
var warnings = [String]()
|
||||
|
||||
if self.waterTemperature == nil {
|
||||
warnings.append(
|
||||
"""
|
||||
Calculations based on default water temperature of 60°F.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
return .init(pressure: pressure, waterDensity: density, warnings: warnings)
|
||||
}
|
||||
|
||||
private func validate() throws {
|
||||
guard height > 0 else {
|
||||
throw ValidationError(message: "Height should be greater than 0.")
|
||||
}
|
||||
if let waterTemperature {
|
||||
guard waterTemperature > 32 else {
|
||||
throw ValidationError(message: "Water temperature should be above freezing.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Sources/ApiController/Extensions/Psychrometrics.swift
Normal file
36
Sources/ApiController/Extensions/Psychrometrics.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Dependencies
|
||||
import Logging
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
|
||||
public extension Routes.Psychrometrics.Request {
|
||||
|
||||
func respond(logger: Logger) async throws -> Psychrometrics.Response {
|
||||
@Dependency(\.psychrometricClient) var psychrometricClient
|
||||
try validate()
|
||||
let properties = try await psychrometricClient.psychrometricProperties(
|
||||
.dryBulb(.fahrenheit(temperature), relativeHumidity: humidity%, altitude: .feet(altitude ?? 0))
|
||||
)
|
||||
var warnings = [String]()
|
||||
if altitude == nil || altitude == 0 {
|
||||
warnings.append(
|
||||
"Calculations based on altitude of sea level - set project altitude for higher accuracy."
|
||||
)
|
||||
}
|
||||
|
||||
return .init(properties: properties, warnings: warnings)
|
||||
}
|
||||
|
||||
private func validate() throws {
|
||||
guard temperature > 0 else {
|
||||
throw ValidationError(message: "Temperature should be greater than 0.")
|
||||
}
|
||||
guard humidity > 0, humidity <= 100 else {
|
||||
throw ValidationError(message: "Relative humidity should be greater than 0 and less than or equal to 100.")
|
||||
}
|
||||
if let altitude, altitude < 0 {
|
||||
throw ValidationError(message: "Altitude should be greater than 0.")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,7 +4,7 @@ public enum AtticVentilation {
|
||||
Calculate attic ventilation requirements and assess current conditions.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
public struct Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let pressureDifferential: Double
|
||||
public let outdoorTemperature: Double
|
||||
|
||||
@@ -4,17 +4,17 @@ public enum Capacitor {
|
||||
Calculate run capacitor values based on electrical measurements.
|
||||
"""
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
case size
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case test
|
||||
case size
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
public enum Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
case size(SizeRequest)
|
||||
case test(TestRequest)
|
||||
|
||||
public struct SizeRequest: Codable, Equatable, Sendable {
|
||||
public struct SizeRequest: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let runningAmps: Double
|
||||
public let lineVoltage: Double
|
||||
@@ -27,7 +27,7 @@ public enum Capacitor {
|
||||
}
|
||||
}
|
||||
|
||||
public struct TestRequest: Codable, Equatable, Sendable {
|
||||
public struct TestRequest: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let startWindingAmps: Double
|
||||
public let runToCommonVoltage: Double
|
||||
|
||||
@@ -8,7 +8,7 @@ public enum DehumidifierSize {
|
||||
|
||||
/// Represents the request for determining dehumidifier size based on
|
||||
/// latent load and indoor conditions.
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
public struct Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let latentLoad: Double
|
||||
public let temperature: Double
|
||||
|
||||
74
Sources/Routes/Models/FeetOfHead.swift
Normal file
74
Sources/Routes/Models/FeetOfHead.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import CasePaths
|
||||
import PsychrometricClient
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
public enum FeetOfHead {
|
||||
public static let description = """
|
||||
Convert PSI to Feet of Head, to aid in pump flow calculations.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Hashable, Sendable {
|
||||
|
||||
public let pressure: Double
|
||||
public let waterTemperature: Double?
|
||||
|
||||
public init(pressure: Double, waterTemperature: Double? = nil) {
|
||||
self.pressure = pressure
|
||||
self.waterTemperature = waterTemperature
|
||||
}
|
||||
|
||||
public enum FieldKey: String, CaseIterable {
|
||||
case pressure
|
||||
case waterTemperature
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable, Equatable, Sendable {
|
||||
|
||||
public let feetOfHead: Double
|
||||
public let density: DensityOf<Water>
|
||||
public let warnings: [String]
|
||||
|
||||
public init(feetOfHead: Double, density: DensityOf<Water>, warnings: [String]) {
|
||||
self.feetOfHead = feetOfHead
|
||||
self.density = density
|
||||
self.warnings = warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Router
|
||||
|
||||
public extension SiteRoute.View {
|
||||
enum FeetOfHead: Equatable, Hashable, Sendable {
|
||||
case index
|
||||
case submit(Routes.FeetOfHead.Request)
|
||||
|
||||
static let rootPath = "feet-of-head"
|
||||
typealias Key = Routes.FeetOfHead.Request.FieldKey
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.submit)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field(Key.pressure) { Double.parser() }
|
||||
Optionally { Field(Key.waterTemperature) { Double.parser() } }
|
||||
}
|
||||
.map(.memberwise(Routes.FeetOfHead.Request.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public extension FeetOfHead.Response {
|
||||
static let mock = Self(feetOfHead: 7.95, density: 62.37, warnings: [])
|
||||
}
|
||||
#endif
|
||||
@@ -4,16 +4,16 @@ public enum FilterPressureDrop {
|
||||
Calculate filter pressure drop and sizing based on system requirements.
|
||||
"""
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case basic
|
||||
case fanLaw
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
public enum Request: Codable, Equatable, Sendable, Hashable {
|
||||
case basic(Basic)
|
||||
case fanLaw(FanLaw)
|
||||
|
||||
public struct Basic: Codable, Equatable, Sendable {
|
||||
public struct Basic: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let systemSize: Double
|
||||
public let climateZone: ClimateZone.ZoneType
|
||||
@@ -36,7 +36,7 @@ public enum FilterPressureDrop {
|
||||
}
|
||||
}
|
||||
|
||||
public struct FanLaw: Codable, Equatable, Sendable {
|
||||
public struct FanLaw: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let filterWidth: Double
|
||||
public let filterHeight: Double
|
||||
|
||||
@@ -7,7 +7,7 @@ public enum HVACSystemPerformance {
|
||||
Analyze HVAC system performance and capacity.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
public struct Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let altitude: Double?
|
||||
public let airflow: Double
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum HVACSystemSize: Double, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case one = 1
|
||||
case oneAndAHalf = 1.5
|
||||
case two = 2
|
||||
|
||||
@@ -6,16 +6,16 @@ public enum HeatingBalancePoint {
|
||||
Calculate the heating balance point.
|
||||
"""
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case economic
|
||||
case thermal
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
public enum Request: Codable, Equatable, Sendable, Hashable {
|
||||
case economic(Economic)
|
||||
case thermal(Thermal)
|
||||
|
||||
public struct Economic: Codable, Equatable, Sendable {
|
||||
public struct Economic: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let fuelType: FuelType
|
||||
public let fuelCostPerUnit: Double
|
||||
@@ -35,7 +35,7 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
}
|
||||
|
||||
public struct Thermal: Codable, Equatable, Sendable {
|
||||
public struct Thermal: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let systemSize: Double
|
||||
public let capacityAt47: Double?
|
||||
@@ -119,9 +119,9 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
}
|
||||
|
||||
public enum HeatLoss: Codable, Equatable, Sendable {
|
||||
public enum HeatLoss: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case estimated
|
||||
case known
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public enum HeatingBalancePoint {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FuelType: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum FuelType: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case naturalGas
|
||||
case propane
|
||||
case oil
|
||||
|
||||
83
Sources/Routes/Models/HydronicSystemPressure.swift
Normal file
83
Sources/Routes/Models/HydronicSystemPressure.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
import CasePaths
|
||||
import PsychrometricClient
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
public enum HydronicSystemPressure {
|
||||
|
||||
public static let description = """
|
||||
Calculate the required hydronic system pressure based on height of a building.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Hashable, Sendable {
|
||||
public let height: Double
|
||||
public let waterTemperature: Double?
|
||||
|
||||
public init(height: Double, waterTemperature: Double? = nil) {
|
||||
self.height = height
|
||||
self.waterTemperature = waterTemperature
|
||||
}
|
||||
|
||||
public enum FieldKey: String, CaseIterable {
|
||||
case height
|
||||
case waterTemperature
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable, Equatable, Sendable {
|
||||
|
||||
public let pressure: Double
|
||||
public let waterDensity: DensityOf<Water>
|
||||
public let warnings: [String]
|
||||
|
||||
public init(pressure: Double, waterDensity: DensityOf<Water>, warnings: [String]) {
|
||||
self.pressure = pressure
|
||||
self.waterDensity = waterDensity
|
||||
self.warnings = warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Router
|
||||
|
||||
public extension SiteRoute.View {
|
||||
|
||||
enum HydronicSystemPressure: Equatable, Hashable, Sendable {
|
||||
case index
|
||||
case submit(Routes.HydronicSystemPressure.Request)
|
||||
|
||||
typealias Key = Routes.HydronicSystemPressure.Request.FieldKey
|
||||
static let rootPath = "hydronic-system-pressure"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.submit)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field(Key.height) { Double.parser() }
|
||||
Optionally { Field(Key.waterTemperature) { Double.parser() } }
|
||||
}
|
||||
.map(.memberwise(Routes.HydronicSystemPressure.Request.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public extension HydronicSystemPressure.Response {
|
||||
static let mock = Self(
|
||||
pressure: 15,
|
||||
waterDensity: 62.37,
|
||||
warnings: [
|
||||
"Water density based on 60 water - include water temperature for more accurate result."
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -7,7 +7,7 @@ public enum MoldRisk {
|
||||
Assess mold risk based on indoor conditions.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable {
|
||||
public struct Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let temperature: Double
|
||||
public let humidity: Double
|
||||
|
||||
107
Sources/Routes/Models/PsychrometricProperties.swift
Normal file
107
Sources/Routes/Models/PsychrometricProperties.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
import CasePaths
|
||||
import PsychrometricClient
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
public enum Psychrometrics {
|
||||
|
||||
public static let description = """
|
||||
Calculate the psychrometric properties of air based on temperature and humidity readings.
|
||||
"""
|
||||
|
||||
public struct Request: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
// public let mode: Mode
|
||||
public let temperature: Double
|
||||
public let humidity: Double
|
||||
public let altitude: Double?
|
||||
|
||||
public init(temperature: Double, humidity: Double, altitude: Double? = nil) {
|
||||
self.temperature = temperature
|
||||
self.humidity = humidity
|
||||
self.altitude = altitude
|
||||
}
|
||||
|
||||
public enum FieldKey: String {
|
||||
case temperature
|
||||
case humidity
|
||||
case altitude
|
||||
}
|
||||
}
|
||||
|
||||
public struct Response: Codable, Equatable, Sendable {
|
||||
public let properties: PsychrometricProperties
|
||||
public let warnings: [String]
|
||||
|
||||
public init(properties: PsychrometricProperties, warnings: [String]) {
|
||||
self.properties = properties
|
||||
self.warnings = warnings
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Router
|
||||
|
||||
public extension SiteRoute.View {
|
||||
|
||||
enum Psychrometrics: Equatable, Sendable, Hashable {
|
||||
|
||||
case index
|
||||
case submit(Routes.Psychrometrics.Request)
|
||||
|
||||
typealias Key = Routes.Psychrometrics.Request.FieldKey
|
||||
static let rootPath = "psychrometric-properties"
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.index)) {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.submit)) {
|
||||
Path { rootPath }
|
||||
Method.post
|
||||
Body {
|
||||
FormData {
|
||||
Field(Key.temperature) {
|
||||
Double.parser()
|
||||
}
|
||||
Field(Key.humidity) {
|
||||
Double.parser()
|
||||
}
|
||||
Optionally {
|
||||
Field(Key.altitude) {
|
||||
Double.parser()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.map(.memberwise(Routes.Psychrometrics.Request.init))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
public extension Psychrometrics.Response {
|
||||
static let mock = Self(
|
||||
properties: .init(
|
||||
absoluteHumidity: 91.4,
|
||||
atmosphericPressure: 0.3,
|
||||
degreeOfSaturation: 0.66,
|
||||
density: 0.07,
|
||||
dewPoint: 64.3,
|
||||
dryBulb: 76,
|
||||
enthalpy: 32.33,
|
||||
grainsOfMoisture: 91.4,
|
||||
humidityRatio: 0.01,
|
||||
relativeHumidity: 67,
|
||||
specificVolume: 13.78,
|
||||
vaporPressure: 0.3,
|
||||
wetBulb: 67.7,
|
||||
units: .imperial
|
||||
),
|
||||
warnings: [
|
||||
"Test warning."
|
||||
]
|
||||
)
|
||||
}
|
||||
#endif
|
||||
@@ -4,7 +4,7 @@ public enum RoomPressure {
|
||||
Calculate return grille and duct sizing for room pressure balancing.
|
||||
"""
|
||||
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable {
|
||||
public enum Mode: String, CaseIterable, Codable, Equatable, Sendable, Hashable {
|
||||
case knownAirflow
|
||||
case measuredPressure
|
||||
|
||||
@@ -16,11 +16,11 @@ public enum RoomPressure {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Request: Codable, Equatable, Sendable {
|
||||
public enum Request: Codable, Equatable, Sendable, Hashable {
|
||||
case knownAirflow(KnownAirflow)
|
||||
case measuredPressure(MeasuredPressure)
|
||||
|
||||
public struct KnownAirflow: Codable, Equatable, Sendable {
|
||||
public struct KnownAirflow: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let targetRoomPressure: Double
|
||||
public let doorWidth: Double
|
||||
@@ -46,7 +46,7 @@ public enum RoomPressure {
|
||||
}
|
||||
}
|
||||
|
||||
public struct MeasuredPressure: Codable, Equatable, Sendable {
|
||||
public struct MeasuredPressure: Codable, Equatable, Sendable, Hashable {
|
||||
|
||||
public let measuredRoomPressure: Double // pascals.
|
||||
public let doorWidth: Double
|
||||
|
||||
@@ -4,6 +4,9 @@ import Foundation
|
||||
import PsychrometricClient
|
||||
@preconcurrency import URLRouting
|
||||
|
||||
// FIX: Move Routers into their respective model files, to reduce the size / complexity of this file
|
||||
// and keep them closer to where changes may need made.
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
public enum SiteRoute: Equatable, Sendable {
|
||||
|
||||
@@ -95,16 +98,19 @@ public enum SiteRoute: Equatable, Sendable {
|
||||
// }
|
||||
|
||||
public extension SiteRoute {
|
||||
enum View: Equatable, Sendable {
|
||||
enum View: Equatable, Sendable, Hashable {
|
||||
|
||||
case index
|
||||
case atticVentilation(AtticVentilation)
|
||||
case capacitor(Capacitor)
|
||||
case dehumidifierSize(DehumidifierSize)
|
||||
case feetOfHead(Self.FeetOfHead)
|
||||
case filterPressureDrop(FilterPressureDrop)
|
||||
case heatingBalancePoint(HeatingBalancePoint)
|
||||
case hvacSystemPerformance(HVACSystemPerformance)
|
||||
case hydronicSystemPressure(Self.HydronicSystemPressure)
|
||||
case moldRisk(MoldRisk)
|
||||
case psychrometrics(Self.Psychrometrics)
|
||||
case roomPressure(RoomPressure)
|
||||
|
||||
public static let router = OneOf {
|
||||
@@ -120,6 +126,9 @@ public extension SiteRoute {
|
||||
Route(.case(Self.dehumidifierSize)) {
|
||||
DehumidifierSize.router
|
||||
}
|
||||
Route(.case(Self.feetOfHead)) {
|
||||
FeetOfHead.router
|
||||
}
|
||||
Route(.case(Self.filterPressureDrop)) {
|
||||
FilterPressureDrop.router
|
||||
}
|
||||
@@ -129,15 +138,21 @@ public extension SiteRoute {
|
||||
Route(.case(Self.hvacSystemPerformance)) {
|
||||
HVACSystemPerformance.router
|
||||
}
|
||||
Route(.case(Self.hydronicSystemPressure)) {
|
||||
HydronicSystemPressure.router
|
||||
}
|
||||
Route(.case(Self.moldRisk)) {
|
||||
MoldRisk.router
|
||||
}
|
||||
Route(.case(Self.psychrometrics)) {
|
||||
Self.Psychrometrics.router
|
||||
}
|
||||
Route(.case(Self.roomPressure)) {
|
||||
RoomPressure.router
|
||||
}
|
||||
}
|
||||
|
||||
public enum AtticVentilation: Equatable, Sendable {
|
||||
public enum AtticVentilation: Equatable, Sendable, Hashable {
|
||||
case index
|
||||
case submit(Routes.AtticVentilation.Request)
|
||||
|
||||
@@ -168,7 +183,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Capacitor: Equatable, Sendable {
|
||||
public enum Capacitor: Equatable, Sendable, Hashable {
|
||||
case index(mode: Routes.Capacitor.Mode? = nil)
|
||||
case submit(Routes.Capacitor.Request)
|
||||
|
||||
@@ -212,7 +227,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum DehumidifierSize: Equatable, Sendable {
|
||||
public enum DehumidifierSize: Equatable, Sendable, Hashable {
|
||||
case index
|
||||
case submit(Routes.DehumidifierSize.Request)
|
||||
|
||||
@@ -238,7 +253,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum FilterPressureDrop: Equatable, Sendable {
|
||||
public enum FilterPressureDrop: Equatable, Sendable, Hashable {
|
||||
case index(mode: Routes.FilterPressureDrop.Mode? = nil)
|
||||
case submit(Routes.FilterPressureDrop.Request)
|
||||
|
||||
@@ -285,7 +300,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum HeatingBalancePoint: Equatable, Sendable {
|
||||
public enum HeatingBalancePoint: Equatable, Sendable, Hashable {
|
||||
case index(
|
||||
mode: Routes.HeatingBalancePoint.Mode? = nil,
|
||||
heatLossMode: Routes.HeatingBalancePoint.HeatLoss.Mode? = nil
|
||||
@@ -350,7 +365,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum HVACSystemPerformance: Equatable, Sendable {
|
||||
public enum HVACSystemPerformance: Equatable, Sendable, Hashable {
|
||||
case index
|
||||
case submit(Routes.HVACSystemPerformance.Request)
|
||||
|
||||
@@ -380,7 +395,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum MoldRisk: Equatable, Sendable {
|
||||
public enum MoldRisk: Equatable, Sendable, Hashable {
|
||||
case index
|
||||
case submit(Routes.MoldRisk.Request)
|
||||
|
||||
@@ -410,7 +425,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RoomPressure: Equatable, Sendable {
|
||||
public enum RoomPressure: Equatable, Sendable, Hashable {
|
||||
case index(mode: Routes.RoomPressure.Mode? = nil)
|
||||
case submit(Routes.RoomPressure.Request)
|
||||
|
||||
@@ -460,6 +475,7 @@ public extension SiteRoute {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
40
Sources/Routes/URLRoutingExtensions.swift
Normal file
40
Sources/Routes/URLRoutingExtensions.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
import URLRouting
|
||||
|
||||
// Allow the use of a field key enum as the `name` parameter, to avoid
|
||||
// stringly type name fields.
|
||||
extension Field {
|
||||
|
||||
@inlinable
|
||||
init<Key>(
|
||||
_ name: Key,
|
||||
default defaultValue: Value.Output? = nil,
|
||||
@ParserBuilder<Substring> _ value: () -> Value
|
||||
) where Key: RawRepresentable, Key.RawValue == String {
|
||||
self.init(name.rawValue, default: defaultValue, value)
|
||||
}
|
||||
|
||||
// @inlinable
|
||||
// init<Key, C>(
|
||||
// _ name: Key,
|
||||
// _ value: C,
|
||||
// default defaultValue: Value.Output? = nil,
|
||||
// ) where Key: RawRepresentable, Key.RawValue == String,
|
||||
// Value == Parsers.MapConversion<Parsers.ReplaceError<Rest<Substring>>, C>
|
||||
// {
|
||||
// self.init(name.rawValue, value, default: defaultValue)
|
||||
// }
|
||||
//
|
||||
// @inlinable
|
||||
// init<Key>(
|
||||
// _ name: Key,
|
||||
// default defaultValue: Value.Output? = nil
|
||||
// )
|
||||
// where
|
||||
// Key: RawRepresentable, Key.RawValue == String,
|
||||
// Value == Parsers.MapConversion<
|
||||
// Parsers.ReplaceError<Rest<Substring>>, Conversions.SubstringToString
|
||||
// >
|
||||
// {
|
||||
// self.init(name.rawValue, default: defaultValue)
|
||||
// }
|
||||
}
|
||||
@@ -59,6 +59,17 @@ public struct Input: HTML, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public extension Input {
|
||||
|
||||
init<Key>(
|
||||
id: Key,
|
||||
name: String? = nil,
|
||||
placeholder: String
|
||||
) where Key: RawRepresentable, Key.RawValue == String {
|
||||
self.init(id: id.rawValue, name: name, placeholder: placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
/// A style form input label.
|
||||
public struct InputLabel<InputLabel: HTML>: HTML {
|
||||
|
||||
|
||||
17
Sources/Styleguide/ResultBox.swift
Normal file
17
Sources/Styleguide/ResultBox.swift
Normal file
@@ -0,0 +1,17 @@
|
||||
import Elementary
|
||||
|
||||
public struct ResultBox<Body: HTML>: HTML {
|
||||
let body: Body
|
||||
|
||||
public init(@HTMLBuilder body: () -> Body) {
|
||||
self.body = body()
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.div> {
|
||||
div(.class("w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6")) {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ResultBox: Sendable where Body: Sendable {}
|
||||
18
Sources/Styleguide/RoundedBox.swift
Normal file
18
Sources/Styleguide/RoundedBox.swift
Normal file
@@ -0,0 +1,18 @@
|
||||
import Elementary
|
||||
|
||||
// A rounded box, with no color styles. Colors should be added at call site.
|
||||
public struct Box<Body: HTML>: HTML {
|
||||
let body: Body
|
||||
|
||||
public init(@HTMLBuilder body: () -> Body) {
|
||||
self.body = body()
|
||||
}
|
||||
|
||||
public var content: some HTML<HTMLTag.div> {
|
||||
div(.class("w-full rounded-lg shadow-lgp-6")) {
|
||||
body
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Box: Sendable where Body: Sendable {}
|
||||
@@ -45,8 +45,10 @@ public struct SVGSize: Sendable {
|
||||
public enum SVGType: Sendable, CaseIterable {
|
||||
case calculator
|
||||
case checkCircle
|
||||
case circleGauge
|
||||
case droplets
|
||||
case exclamation
|
||||
case footprints
|
||||
case funnel
|
||||
case house
|
||||
case leftRightArrow
|
||||
@@ -63,8 +65,10 @@ public enum SVGType: Sendable, CaseIterable {
|
||||
switch self {
|
||||
case .calculator: return calculatorSvg(size: size)
|
||||
case .checkCircle: return checkCircleSvg(size: size)
|
||||
case .circleGauge: return circleGaugeSvg(size: size)
|
||||
case .droplets: return dropletsSvg(size: size)
|
||||
case .exclamation: return exclamationSvg(size: size)
|
||||
case .footprints: return footprintsSvg(size: size)
|
||||
case .funnel: return funnelSvg(size: size)
|
||||
case .house: return houseSvg(size: size)
|
||||
case .leftRightArrow: return leftRightArrowSvg(size: size)
|
||||
@@ -84,6 +88,27 @@ public enum SVGType: Sendable, CaseIterable {
|
||||
|
||||
// swiftlint:disable line_length
|
||||
|
||||
private func footprintsSvg(size: SVGSize) -> HTMLRaw {
|
||||
HTMLRaw("""
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-footprints">
|
||||
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"/>
|
||||
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"/>
|
||||
<path d="M16 17h4"/>
|
||||
<path d="M4 13h4"/>
|
||||
</svg>
|
||||
""")
|
||||
}
|
||||
|
||||
private func circleGaugeSvg(size: SVGSize) -> HTMLRaw {
|
||||
HTMLRaw("""
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-gauge">
|
||||
<path d="M15.6 2.7a10 10 0 1 0 5.7 5.7"/>
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<path d="M13.4 10.6 19 5"/>
|
||||
</svg>
|
||||
""")
|
||||
}
|
||||
|
||||
private func scaleSvg(size: SVGSize) -> HTMLRaw {
|
||||
HTMLRaw("""
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="\(size.width)" height="\(size.height)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
|
||||
|
||||
3
Sources/ViewController/Extensions/String+units.swift
Normal file
3
Sources/ViewController/Extensions/String+units.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
public extension String {
|
||||
static let fahrenheit = "°F"
|
||||
}
|
||||
@@ -55,6 +55,24 @@ struct HomePage: HTML, Sendable {
|
||||
svg: .scale,
|
||||
route: .heatingBalancePoint(.index)
|
||||
)
|
||||
group(
|
||||
label: "Psychrometrics",
|
||||
description: Psychrometrics.description,
|
||||
svg: .droplets,
|
||||
route: .psychrometrics(.index)
|
||||
)
|
||||
group(
|
||||
label: "Hydronic System Pressure",
|
||||
description: HydronicSystemPressure.description,
|
||||
svg: .circleGauge,
|
||||
route: .hydronicSystemPressure(.index)
|
||||
)
|
||||
group(
|
||||
label: "Feet of Head",
|
||||
description: FeetOfHead.description,
|
||||
svg: .footprints,
|
||||
route: .feetOfHead(.index)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,15 @@ extension ViewController: DependencyKey {
|
||||
return DehumidifierSizeResult(response: response)
|
||||
}
|
||||
|
||||
case let .feetOfHead(route):
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(FeetOfHeadForm(response: nil))
|
||||
case let .submit(request):
|
||||
let response = try await request.respond(logger: logger)
|
||||
return FeetOfHeadResponse(response: response)
|
||||
}
|
||||
|
||||
case let .filterPressureDrop(route):
|
||||
switch route {
|
||||
case let .index(mode: mode):
|
||||
@@ -127,13 +136,21 @@ extension ViewController: DependencyKey {
|
||||
case let .hvacSystemPerformance(route):
|
||||
switch route {
|
||||
case .index:
|
||||
// let response = try await HVACSystemPerformance.Response.mock()
|
||||
return request.respond(HVACSystemPerformanceForm(response: nil))
|
||||
case let .submit(hvacRequest):
|
||||
let response = try await hvacRequest.respond(logger: request.logger)
|
||||
return HVACSystemPerformanceResult(response: response)
|
||||
}
|
||||
|
||||
case let .hydronicSystemPressure(route):
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(HydronicSystemPressureForm(response: nil))
|
||||
case let .submit(request):
|
||||
let response = try await request.respond(logger: logger)
|
||||
return HydronicSystemPressureResponse(response: response)
|
||||
}
|
||||
|
||||
case let .moldRisk(route):
|
||||
switch route {
|
||||
case .index:
|
||||
@@ -143,6 +160,15 @@ extension ViewController: DependencyKey {
|
||||
return MoldRiskResponse(response: response)
|
||||
}
|
||||
|
||||
case let .psychrometrics(route):
|
||||
switch route {
|
||||
case .index:
|
||||
return request.respond(PsychrometricsForm(response: nil))
|
||||
case let .submit(request):
|
||||
let response = try await request.respond(logger: logger)
|
||||
return PsychrometricsResponse(response: response)
|
||||
}
|
||||
|
||||
case let .roomPressure(route):
|
||||
switch route {
|
||||
case let .index(mode):
|
||||
|
||||
70
Sources/ViewController/Views/FeetOfHead.swift
Normal file
70
Sources/ViewController/Views/FeetOfHead.swift
Normal file
@@ -0,0 +1,70 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct FeetOfHeadForm: HTML, Sendable {
|
||||
let response: FeetOfHead.Response?
|
||||
|
||||
typealias Key = FeetOfHead.Request.FieldKey
|
||||
|
||||
var content: some HTML {
|
||||
FormHeader(label: "Feet of Head", svg: .footprints)
|
||||
|
||||
form(
|
||||
.hx.post(route: .feetOfHead(.index)),
|
||||
.hx.target("#result")
|
||||
) {
|
||||
div(.class("space-y-6")) {
|
||||
div(.class("grid grid-cols-2 gap-4")) {
|
||||
LabeledContent(label: "Pressure (psi)") {
|
||||
Input(id: Key.pressure, placeholder: "Pressure")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
|
||||
}
|
||||
|
||||
LabeledContent(label: "Water Temperature (\(String.fahrenheit))") {
|
||||
Input(id: Key.waterTemperature, placeholder: "Temperature (optional)")
|
||||
.attributes(.type(.number), .min("32.0"), .step("0.1"))
|
||||
}
|
||||
}
|
||||
|
||||
Note {
|
||||
"""
|
||||
If water temperature is not supplied, then calculations will be based on 60\(String.fahrenheit) water.
|
||||
"""
|
||||
}
|
||||
|
||||
div {
|
||||
SubmitButton(label: "Feet of Head")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
FeetOfHeadResponse(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeetOfHeadResponse: HTML, Sendable {
|
||||
let response: FeetOfHead.Response
|
||||
|
||||
var content: some HTML {
|
||||
ResultContainer(reset: .feetOfHead(.index)) {
|
||||
ResultBox {
|
||||
div(.class("grid grid-cols-2")) {
|
||||
VerticalGroup(label: "Feet of Head", value: "\(double: response.feetOfHead)", valueLabel: "ft.")
|
||||
VerticalGroup(
|
||||
label: "Water Density",
|
||||
value: "\(double: response.density.value)",
|
||||
valueLabel: response.density.units.rawValue
|
||||
)
|
||||
}
|
||||
}
|
||||
WarningBox(warnings: response.warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Sources/ViewController/Views/HydronicSystemPressure.swift
Normal file
74
Sources/ViewController/Views/HydronicSystemPressure.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct HydronicSystemPressureForm: HTML, Sendable {
|
||||
let response: HydronicSystemPressure.Response?
|
||||
|
||||
typealias Key = HydronicSystemPressure.Request.FieldKey
|
||||
|
||||
var content: some HTML {
|
||||
FormHeader(label: "Hydronic System Pressure", svg: .circleGauge)
|
||||
|
||||
form(
|
||||
.hx.post(route: .hydronicSystemPressure(.index)),
|
||||
.hx.target("#result")
|
||||
) {
|
||||
div(.class("space-y-6")) {
|
||||
div(.class("grid grid-cols-2 gap-4")) {
|
||||
LabeledContent(label: "Height (ft.)") {
|
||||
Input(id: Key.height, placeholder: "Building height")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
|
||||
}
|
||||
|
||||
LabeledContent(label: "Water Temperature (\(String.fahrenheit))") {
|
||||
Input(id: Key.waterTemperature, placeholder: "Temperature (optional)")
|
||||
.attributes(.type(.number), .min("32.0"), .step("0.1"))
|
||||
}
|
||||
}
|
||||
|
||||
Note {
|
||||
"""
|
||||
Water temperature should be the coldest water temperature the system sees, which for boilers will be
|
||||
when the system is filled with water.
|
||||
"""
|
||||
}
|
||||
|
||||
div {
|
||||
SubmitButton(label: "Calculate System Pressure")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
HydronicSystemPressureResponse(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HydronicSystemPressureResponse: HTML, Sendable {
|
||||
let response: HydronicSystemPressure.Response
|
||||
|
||||
var content: some HTML {
|
||||
ResultContainer(reset: .hydronicSystemPressure(.index)) {
|
||||
ResultBox {
|
||||
div(.class("grid grid-cols-2")) {
|
||||
VerticalGroup(label: "Pressure", value: "\(double: response.pressure)", valueLabel: "psi")
|
||||
VerticalGroup(
|
||||
label: "Water Density",
|
||||
value: "\(double: response.waterDensity.value)",
|
||||
valueLabel: response.waterDensity.units.rawValue
|
||||
)
|
||||
}
|
||||
}
|
||||
WarningBox(warnings: response.warnings)
|
||||
Note {
|
||||
"Expansion tank pressure should match system fill pressure."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import Elementary
|
||||
import PsychrometricClient
|
||||
|
||||
// FIX: Remove text colors.
|
||||
struct PsychrometricPropertiesView: HTML {
|
||||
let properties: PsychrometricProperties
|
||||
|
||||
|
||||
126
Sources/ViewController/Views/Psychrometrics.swift
Normal file
126
Sources/ViewController/Views/Psychrometrics.swift
Normal file
@@ -0,0 +1,126 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import PsychrometricClient
|
||||
import Routes
|
||||
import Styleguide
|
||||
|
||||
struct PsychrometricsForm: HTML, Sendable {
|
||||
|
||||
// let mode: Psychrometrics.Mode
|
||||
let response: Psychrometrics.Response?
|
||||
|
||||
init(response: Psychrometrics.Response? = nil) {
|
||||
self.response = response
|
||||
}
|
||||
|
||||
var content: some HTML {
|
||||
FormHeader(label: "Psychrometric Properties", svg: .droplets)
|
||||
|
||||
form(
|
||||
.hx.post(route: .psychrometrics(.index)),
|
||||
.hx.target("#result")
|
||||
) {
|
||||
div(.class("space-y-6")) {
|
||||
div(.class("grid grid-cols-1 lg:grid-cols-2 gap-4")) {
|
||||
LabeledContent(label: "Temperature (°F)") {
|
||||
Input(id: Psychrometrics.Request.FieldKey.temperature.rawValue, placeholder: "Dry bulb temperature")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.1"), .autofocus, .required)
|
||||
}
|
||||
LabeledContent(label: "Relative Humidity (%)") {
|
||||
Input(id: Psychrometrics.Request.FieldKey.humidity.rawValue, placeholder: "Relative humidity")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.1"), .max("100"), .required)
|
||||
}
|
||||
}
|
||||
|
||||
LabeledContent(label: "Altitude (ft.)") {
|
||||
Input(id: Psychrometrics.Request.FieldKey.altitude.rawValue, placeholder: "Altitude (optional)")
|
||||
.attributes(.type(.number), .min("0.1"), .step("0.1"))
|
||||
}
|
||||
|
||||
div {
|
||||
SubmitButton(label: "Calculate Psychrometrics")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(.id("result")) {
|
||||
if let response {
|
||||
PsychrometricsResponse(response: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PsychrometricsResponse: HTML, Sendable {
|
||||
let response: Psychrometrics.Response
|
||||
|
||||
var content: some HTML {
|
||||
ResultContainer(reset: .psychrometrics(.index)) {
|
||||
div(.class("w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6")) {
|
||||
div(.class("flex mb-8")) {
|
||||
SVG(.droplets, color: "text-blue-600")
|
||||
h3(.class("text-xl px-2 font-semibold")) { "Psychrometrics" }
|
||||
}
|
||||
div(.class("grid grid-cols-3 gap-4")) {
|
||||
div(.class("rounded-lg bg-purple-200 border border-purple-600 text-purple-600")) {
|
||||
VerticalGroup(
|
||||
label: "Dew Point",
|
||||
value: "\(double: response.properties.dewPoint.value, fractionDigits: 1)",
|
||||
valueLabel: response.properties.dewPoint.units.symbol
|
||||
)
|
||||
.attributes(.class("my-8"))
|
||||
}
|
||||
div(.class("rounded-lg bg-orange-200 border border-orange-600 text-orange-600")) {
|
||||
VerticalGroup(
|
||||
label: "Enthalpy",
|
||||
value: "\(double: response.properties.enthalpy.value, fractionDigits: 1)",
|
||||
valueLabel: response.properties.enthalpy.units.rawValue
|
||||
)
|
||||
.attributes(.class("my-8"))
|
||||
}
|
||||
div(.class("rounded-lg bg-green-200 border border-green-600 text-green-600")) {
|
||||
VerticalGroup(
|
||||
label: "Wet Bulb",
|
||||
value: "\(double: response.properties.wetBulb.value, fractionDigits: 1)",
|
||||
valueLabel: response.properties.wetBulb.units.symbol
|
||||
)
|
||||
.attributes(.class("my-8"))
|
||||
}
|
||||
}
|
||||
|
||||
div(.class("mt-8")) {
|
||||
h4(.class("text-lg font-semibold")) { "Other Properties" }
|
||||
div(.class("rounded-lg border border-blue-300")) {
|
||||
displayProperty("Density", \.density.rawValue)
|
||||
displayProperty("Vapor Pressure", \.vaporPressure.rawValue)
|
||||
displayProperty("Specific Volume", response.properties.specificVolume.rawValue)
|
||||
displayProperty("Absolute Humidity", \.absoluteHumidity)
|
||||
displayProperty("Humidity Ratio", response.properties.humidityRatio.value)
|
||||
displayProperty("Degree of Saturation", response.properties.degreeOfSaturation.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WarningBox(warnings: response.warnings)
|
||||
}
|
||||
}
|
||||
|
||||
private func displayProperty(_ label: String, _ value: Double, _ symbol: String? = nil) -> some HTML {
|
||||
let symbol = "\(symbol == nil ? "" : " \(symbol!)")"
|
||||
|
||||
return div(.class("flex items-center justify-between border-b border-blue-300 p-2")) {
|
||||
span(.class("font-semibold")) { "\(label): " }
|
||||
span(.class("font-light")) {
|
||||
"\(double: value, fractionDigits: 2)\(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 = response.properties[keyPath: keyPath]
|
||||
return displayProperty(label, property.rawValue, property.units.rawValue)
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,29 @@
|
||||
@testable import App
|
||||
import VaporTesting
|
||||
import Testing
|
||||
|
||||
@Suite("App Tests")
|
||||
struct AppTests {
|
||||
private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||
let app = try await Application.make(.testing)
|
||||
do {
|
||||
try await configure(app)
|
||||
try await test(app)
|
||||
}
|
||||
catch {
|
||||
try await app.asyncShutdown()
|
||||
throw error
|
||||
}
|
||||
try await app.asyncShutdown()
|
||||
}
|
||||
|
||||
@Test("Test Hello World Route")
|
||||
func helloWorld() async throws {
|
||||
try await withApp { app in
|
||||
try await app.testing().test(.GET, "hello", afterResponse: { res async in
|
||||
#expect(res.status == .ok)
|
||||
#expect(res.body.string == "Hello, world!")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// @testable import App
|
||||
// import VaporTesting
|
||||
// import Testing
|
||||
//
|
||||
// @Suite("App Tests")
|
||||
// struct AppTests {
|
||||
// private func withApp(_ test: (Application) async throws -> ()) async throws {
|
||||
// let app = try await Application.make(.testing)
|
||||
// do {
|
||||
// try await configure(app)
|
||||
// try await test(app)
|
||||
// }
|
||||
// catch {
|
||||
// try await app.asyncShutdown()
|
||||
// throw error
|
||||
// }
|
||||
// try await app.asyncShutdown()
|
||||
// }
|
||||
//
|
||||
// @Test("Test Hello World Route")
|
||||
// func helloWorld() async throws {
|
||||
// try await withApp { app in
|
||||
// try await app.testing().test(.GET, "hello", afterResponse: { res async in
|
||||
// #expect(res.status == .ok)
|
||||
// #expect(res.body.string == "Hello, world!")
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
157
Tests/ViewControllerTests/ViewControllerTests.swift
Normal file
157
Tests/ViewControllerTests/ViewControllerTests.swift
Normal file
@@ -0,0 +1,157 @@
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import HTMLSnapshotTesting
|
||||
import Logging
|
||||
import OrderedCollections
|
||||
import PsychrometricClientLive
|
||||
import Routes
|
||||
import SnapshotTesting
|
||||
import Testing
|
||||
@testable import ViewController
|
||||
|
||||
@Suite("ViewControllerTests")
|
||||
struct ViewControllerTests {
|
||||
|
||||
let record = SnapshotTestingConfiguration.Record.missing
|
||||
let logger = Logger(label: "ViewControllerTests")
|
||||
|
||||
// swiftlint:disable function_body_length
|
||||
@Test
|
||||
func snapShotTests() async throws {
|
||||
try await withSnapshotTesting(record: record) {
|
||||
try await withDependencies {
|
||||
$0.viewController = .liveValue
|
||||
$0.psychrometricClient = .liveValue
|
||||
} operation: {
|
||||
@Dependency(\.viewController) var viewController
|
||||
|
||||
let arguments = OrderedSet([
|
||||
// Attic ventilation
|
||||
SiteRoute.View.atticVentilation(.index),
|
||||
.atticVentilation(.submit(.init(
|
||||
pressureDifferential: 1,
|
||||
outdoorTemperature: 76,
|
||||
outdoorDewpoint: 67,
|
||||
atticTemperature: 76,
|
||||
atticDewpoint: 76,
|
||||
atticFloorArea: 1234
|
||||
))),
|
||||
|
||||
// Capacitor
|
||||
.capacitor(.index),
|
||||
.capacitor(.index(mode: .size)),
|
||||
.capacitor(.index(mode: .test)),
|
||||
.capacitor(.submit(.size(.init(runningAmps: 10.7, lineVoltage: 243, powerFactor: 0.86)))),
|
||||
.capacitor(.submit(.test(.init(startWindingAmps: 4.3, runToCommonVoltage: 343)))),
|
||||
|
||||
// Dehumidifier Sizing
|
||||
.dehumidifierSize(.index),
|
||||
.dehumidifierSize(.submit(.init(latentLoad: 3443, temperature: 76, humidity: 67))),
|
||||
|
||||
// Filter Pressure Drop
|
||||
.filterPressureDrop(.index),
|
||||
.filterPressureDrop(.index(mode: .basic)),
|
||||
.filterPressureDrop(.index(mode: .fanLaw)),
|
||||
.filterPressureDrop(.submit(.basic(.init(
|
||||
systemSize: 2,
|
||||
climateZone: .hotHumid,
|
||||
filterType: .pleatedBest,
|
||||
filterWidth: 20,
|
||||
filterHeight: 25
|
||||
)))),
|
||||
.filterPressureDrop(.submit(.fanLaw(.init(
|
||||
filterWidth: 20,
|
||||
filterHeight: 25,
|
||||
filterDepth: 4,
|
||||
ratedAirflow: 800,
|
||||
ratedPressureDrop: 0.1,
|
||||
designAirflow: 900
|
||||
)))),
|
||||
|
||||
// Heating Balance Point
|
||||
.heatingBalancePoint(.index),
|
||||
.heatingBalancePoint(.heatLossFields(mode: .estimated)),
|
||||
.heatingBalancePoint(.heatLossFields(mode: .known)),
|
||||
.heatingBalancePoint(.submit(.economic(.init(
|
||||
fuelType: .propane,
|
||||
fuelCostPerUnit: 2.43,
|
||||
fuelAFUE: 90,
|
||||
costPerKW: 0.13
|
||||
)))),
|
||||
.heatingBalancePoint(.submit(.thermal(.init(
|
||||
systemSize: 2,
|
||||
capacityAt47: 24600,
|
||||
capacityAt17: 15100,
|
||||
heatingDesignTemperature: 5,
|
||||
buildingHeatLoss: .known(btu: 45667),
|
||||
climateZone: .five
|
||||
)))),
|
||||
|
||||
// HVAC System Performance
|
||||
.hvacSystemPerformance(.index),
|
||||
.hvacSystemPerformance(.submit(.init(
|
||||
altitude: 800,
|
||||
airflow: 800,
|
||||
returnAirTemperature: 76,
|
||||
returnAirHumidity: 67,
|
||||
supplyAirTemperature: 56,
|
||||
supplyAirHumidity: 89,
|
||||
systemSize: 2
|
||||
))),
|
||||
|
||||
// Mold risk
|
||||
.moldRisk(.index),
|
||||
.moldRisk(.submit(.init(temperature: 76, humidity: 67))),
|
||||
|
||||
// Psychrometrics
|
||||
.psychrometrics(.index),
|
||||
.psychrometrics(.submit(.init(temperature: 76, humidity: 67, altitude: 800))),
|
||||
|
||||
// Room pressures
|
||||
.roomPressure(.index),
|
||||
.roomPressure(.index(mode: .measuredPressure)),
|
||||
.roomPressure(.submit(.knownAirflow(.init(
|
||||
targetRoomPressure: 3,
|
||||
doorWidth: 30,
|
||||
doorHeight: 86,
|
||||
doorUndercut: 1,
|
||||
supplyAirflow: 200,
|
||||
preferredGrilleHeight: .fourteen
|
||||
)))),
|
||||
.roomPressure(.submit(.measuredPressure(.init(
|
||||
measuredRoomPressure: 4,
|
||||
doorWidth: 30,
|
||||
doorHeight: 86,
|
||||
doorUndercut: 1,
|
||||
preferredGrilleHeight: .fourteen
|
||||
)))),
|
||||
|
||||
// Hydronic system pressure
|
||||
.hydronicSystemPressure(.index),
|
||||
.hydronicSystemPressure(.submit(.init(height: 12, waterTemperature: 60))),
|
||||
|
||||
// Feet of head
|
||||
.feetOfHead(.index),
|
||||
.feetOfHead(.submit(.init(pressure: 3.5, waterTemperature: 60)))
|
||||
|
||||
])
|
||||
|
||||
for route in arguments {
|
||||
let html = try await viewController.render(route, logger: logger)
|
||||
assertSnapshot(of: html, as: .html)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
}
|
||||
|
||||
private extension ViewController {
|
||||
|
||||
func render(_ route: SiteRoute.View, logger: Logger) async throws -> String {
|
||||
let html = try await view(.init(route, isHtmxRequest: true, logger: logger))
|
||||
return html.renderFormatted()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="w-8 h-8">
|
||||
<path d="M17.7 7.7a2.5 2.5 0 1 1 1.8 4.3H2"></path>
|
||||
<path d="M9.6 4.6A2 2 0 1 1 11 8H2"></path>
|
||||
<path d="M12.6 19.4A2 2 0 1 0 14 16H2"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Attic Ventilation Calculator</h2>
|
||||
</div>
|
||||
<form hx-post="/attic-ventilation" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<div>
|
||||
<label for="pressureDifferential" class="block text-sm font-medium mb-2">Pressure Differential: (Pascals - WRT outside)</label>
|
||||
<input id="pressureDifferential" placeholder="Pressure differential" name="pressureDifferential" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" autofocus required>
|
||||
</div>
|
||||
<p class="text-sm text-light mt-2">Positive values indicate higher attic pressure</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="font-bold mb-6">Outdoor Conditions</p>
|
||||
<div>
|
||||
<label for="outdoorTemperature" class="block text-sm font-medium mb-2">Temperature: (°F)</label>
|
||||
<input id="outdoorTemperature" placeholder="Outdoor temperature" name="outdoorTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="outdoorDewpoint" class="block text-sm font-medium mb-2">Dew Point: (°F)</label>
|
||||
<input id="outdoorDewpoint" placeholder="Outdoor dewpoint" name="outdoorDewpoint" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold mb-6">Attic Conditions</p>
|
||||
<div>
|
||||
<label for="atticTemperature" class="block text-sm font-medium mb-2">Temperature: (°F)</label>
|
||||
<input id="atticTemperature" placeholder="Attic temperature" name="atticTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="atticDewpoint" class="block text-sm font-medium mb-2">Dew Point: (°F)</label>
|
||||
<input id="atticDewpoint" placeholder="Attic dewpoint" name="atticDewpoint" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="atticFloorArea" class="block text-sm font-medium mb-2">Attic Floor Area: (ft²)</label>
|
||||
<input id="atticFloorArea" placeholder="Attic floor area" name="atticFloorArea" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="existingIntakeArea" class="block text-sm font-medium mb-2">Existing Intake Area: (ft²)</label>
|
||||
<input id="existingIntakeArea" placeholder="Intake area (optional)" name="existingIntakeArea" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500 mb-4" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="existingExhaustArea" class="block text-sm font-medium mb-2">Existing Exhaust Area: (ft²)</label>
|
||||
<input id="existingExhaustArea" placeholder="Exhaust area (optional)" name="existingExhaustArea" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Ventilation</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,90 @@
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g clip-path="url(#clip0_429_11128)">
|
||||
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Basic</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Basic</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=fanLaw" type="button">Fan Law</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/filter-pressure-drop" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<div>
|
||||
<h4 class="text-lg font-bold mb-4">System Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="systemSize" class="block text-sm font-medium mb-2">System Size: (Tons)</label>
|
||||
<input id="systemSize" placeholder="Tons" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" min="1.0" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
|
||||
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="hotHumid">Hot Humid (1A, 2A)</option>
|
||||
<option value="moist">Moist (3A, 4A, 5A, 6A, 7A)</option>
|
||||
<option value="dry">Dry (2B, 3B, 4B, 5B, 6B, 7B)</option>
|
||||
<option value="marine">Marine (3C, 4C)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterType" class="block text-sm font-medium mb-2">Filter Type</label>
|
||||
<select id="filterType" name="filterType" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="fiberglass">Fiberglass (MERV 1-4)</option>
|
||||
<option value="pleatedBasic">Basic Pleated (MERV 5-8)</option>
|
||||
<option value="pleatedBetter">Better Pleated (MERV 9-12)</option>
|
||||
<option value="pleatedBest">Best Pleated (MERV 13-16)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 lg:mt-0">
|
||||
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
|
||||
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
|
||||
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,90 @@
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g clip-path="url(#clip0_429_11128)">
|
||||
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Basic</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Basic</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=fanLaw" type="button">Fan Law</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/filter-pressure-drop" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<div>
|
||||
<h4 class="text-lg font-bold mb-4">System Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="systemSize" class="block text-sm font-medium mb-2">System Size: (Tons)</label>
|
||||
<input id="systemSize" placeholder="Tons" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" min="1.0" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
|
||||
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="hotHumid">Hot Humid (1A, 2A)</option>
|
||||
<option value="moist">Moist (3A, 4A, 5A, 6A, 7A)</option>
|
||||
<option value="dry">Dry (2B, 3B, 4B, 5B, 6B, 7B)</option>
|
||||
<option value="marine">Marine (3C, 4C)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterType" class="block text-sm font-medium mb-2">Filter Type</label>
|
||||
<select id="filterType" name="filterType" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="fiberglass">Fiberglass (MERV 1-4)</option>
|
||||
<option value="pleatedBasic">Basic Pleated (MERV 5-8)</option>
|
||||
<option value="pleatedBetter">Better Pleated (MERV 9-12)</option>
|
||||
<option value="pleatedBest">Best Pleated (MERV 13-16)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 lg:mt-0">
|
||||
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
|
||||
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
|
||||
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,93 @@
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<g clip-path="url(#clip0_429_11128)">
|
||||
<path d="M20 3.99994H4L9.6 11.4666C9.85964 11.8128 10 12.2339 10 12.6666V19.9999L14 17.9999V12.6666C14 12.2339 14.1404 11.8128 14.4 11.4666L20 3.99994Z" stroke="currentColor" stroke-width="2.5" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_429_11128"> <rect width="24" height="24" fill="white"></rect>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Filter Pressure Drop - Fan Law</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/filter-pressure-drop?mode=basic" type="button">Basic</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-e-lg" disabled type="button">Fan Law</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/filter-pressure-drop" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<div class="mt-6 lg:mt-0">
|
||||
<h4 class="text-lg font-bold mb-4">Filter Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="filterWidth" class="block text-sm font-medium mb-2">Width: (in)</label>
|
||||
<input id="filterWidth" placeholder="Width" name="filterWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required autofocus>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterHeight" class="block text-sm font-medium mb-2">Height: (in)</label>
|
||||
<input id="filterHeight" placeholder="Height" name="filterHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="filterDepth" class="block text-sm font-medium mb-2">Depth: (in)</label>
|
||||
<input id="filterDepth" placeholder="Depth" name="filterDepth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="1.0" min="1.0" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 lg:mt-0">
|
||||
<h4 class="text-lg font-bold mb-4">Airflow Parameters</h4>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-x-4 space-y-6">
|
||||
<div>
|
||||
<label for="ratedAirflow" class="block text-sm font-medium mb-2">Rated Airflow: (CFM)</label>
|
||||
<input id="ratedAirflow" placeholder="Rated or measured Airflow" name="ratedAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ratedPressureDrop" class="block text-sm font-medium mb-2">Filter Pressure Drop: (in. w.c.)</label>
|
||||
<input id="ratedPressureDrop" placeholder="Rated or measured pressure drop" name="ratedPressureDrop" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="designAirflow" class="block text-sm font-medium mb-2">Design Airflow: (CFM)</label>
|
||||
<input id="designAirflow" placeholder="Design or target airflow" name="designAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="1.0" min="1.0" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Pressure Drop</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,42 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/filter-pressure-drop?mode=basic" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class=" grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||
border border-blue-600 text-blue-600 bg-blue-100 p-6">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Filter Face Area</p>
|
||||
<h3 class="text-3xl font-extrabold">3.47<span class="text-lg ms-2">ft²</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Filter Face Velocity</p>
|
||||
<h3 class="text-3xl font-extrabold">201.6<span class="text-lg ms-2">FPM</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Initial Pressure Drop</p>
|
||||
<h3 class="text-3xl font-extrabold">0.2"<span class="text-lg ms-2">w.c.</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Maximum Allowable Pressure Drop</p>
|
||||
<h3 class="text-3xl font-extrabold">0.15"<span class="text-lg ms-2">w.c.</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings" class=" 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 border-amber-500">Warning:</span>
|
||||
<ul class="list-disc mx-10 mt-4">
|
||||
<li>
|
||||
Initial pressure drop is more than 50% of maximum allowable - consider using a larger filter or different
|
||||
filter type with lower initial pressure drop.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/filter-pressure-drop?mode=fanLaw" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class=" grid grid-cols-1 lg:grid-cols-2 gap-6 rounded-lg shadow-lg
|
||||
border border-blue-600 text-blue-600 bg-blue-100 p-6">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Filter Face Velocity</p>
|
||||
<h3 class="text-3xl font-extrabold">259.2<span class="text-lg ms-2">FPM</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Predicted Pressure Drop</p>
|
||||
<h3 class="text-3xl font-extrabold">0.13"<span class="text-lg ms-2">w.c.</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Velocity Ratio</p>
|
||||
<h3 class="text-3xl font-extrabold">1.12</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Pressure Ratio</p>
|
||||
<h3 class="text-3xl font-extrabold">1.27</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Predictions are based on fan laws where pressure drop varies with the square of the airflow ratio.
|
||||
Results assume similar air properties and filter loading conditions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,102 @@
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
|
||||
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="M7 21h10"/><path d="M12 3v18"/>
|
||||
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Balance Point - Thermal</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Thermal</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/balance-point?mode=economic&heatLossMode=estimated" type="button">Economic</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/balance-point" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="systemSize" class="block text-sm font-medium mb-2">System Size (Tons)</label>
|
||||
<input id="systemSize" placeholder="System size" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<h4 class="text-lg font-bold">Capacities</h4>
|
||||
<p class="text-xs text-blue-500">Entering known capacities gives better results, otherwise capacities will be estimated.</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="capacityAt47" class="block text-sm font-medium mb-2">Capacity @ 47° (BTU/h)</label>
|
||||
<input id="capacityAt47" placeholder="Capacity @ 47° (optional)" name="capacityAt47" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
|
||||
</div>
|
||||
<div>
|
||||
<label for="capacityAt17" class="block text-sm font-medium mb-2">Capacity @ 17° (BTU/h)</label>
|
||||
<input id="capacityAt17" placeholder="Capacity @ 17° (optional)" name="capacityAt17" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="heatLossFields">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<h4 class="text-lg font-bold">Heat Loss - Estimated</h4>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Estimated</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-get="/balance-point/heat-loss?mode=known" hx-target="#heatLossFields" type="button">Known</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="simplifiedHeatLoss" class="block text-sm font-medium mb-2">Building Size (ft²)</label>
|
||||
<input id="simplifiedHeatLoss" placeholder="Square feet" name="simplifiedHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
|
||||
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="CZ1">CZ1</option>
|
||||
<option value="CZ2">CZ2</option>
|
||||
<option value="CZ3">CZ3</option>
|
||||
<option value="CZ4">CZ4</option>
|
||||
<option value="CZ5">CZ5</option>
|
||||
<option value="CZ6">CZ6</option>
|
||||
<option value="CZ7">CZ7</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
|
||||
<input id="heatingDesignTemperature" placeholder="Design temperature (optional)" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Balance Point</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,41 @@
|
||||
<div id="heatLossFields">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<h4 class="text-lg font-bold">Heat Loss - Estimated</h4>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Estimated</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-get="/balance-point/heat-loss?mode=known" hx-target="#heatLossFields" type="button">Known</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="simplifiedHeatLoss" class="block text-sm font-medium mb-2">Building Size (ft²)</label>
|
||||
<input id="simplifiedHeatLoss" placeholder="Square feet" name="simplifiedHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="climateZone" class="block text-sm font-medium mb-2">Climate Zone</label>
|
||||
<select id="climateZone" name="climateZone" class="w-full rounded-md border px-4 py-2 min-h-11">
|
||||
<option value="CZ1">CZ1</option>
|
||||
<option value="CZ2">CZ2</option>
|
||||
<option value="CZ3">CZ3</option>
|
||||
<option value="CZ4">CZ4</option>
|
||||
<option value="CZ5">CZ5</option>
|
||||
<option value="CZ6">CZ6</option>
|
||||
<option value="CZ7">CZ7</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
|
||||
<input id="heatingDesignTemperature" placeholder="Design temperature (optional)" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div id="heatLossFields">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<h4 class="text-lg font-bold">Heat Loss - Known</h4>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-s-lg" hx-get="/balance-point/heat-loss?mode=estimated" hx-target="#heatLossFields" type="button">Estimated</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-e-lg" disabled type="button">Known</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="knownHeatLoss" class="block text-sm font-medium mb-2">Heat Loss (BTU/h)</label>
|
||||
<input id="knownHeatLoss" placeholder="Heat loss" name="knownHeatLoss" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="heatingDesignTemperature" class="block text-sm font-medium mb-2">Outdoor Design Temperature (°F)</label>
|
||||
<input id="heatingDesignTemperature" placeholder="Design temperature" name="heatingDesignTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.5" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/balance-point?mode=economic" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4">
|
||||
<div class="flex">
|
||||
<div class="block text-blue-600 px-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
|
||||
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="M7 21h10"/><path d="M12 3v18"/>
|
||||
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium">Economic Balance Point</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center mb-8">
|
||||
<p class="font-medium">Balance Point</p>
|
||||
<h3 class="text-3xl font-extrabold">-13<span class="text-lg ms-2">°F</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 space-y-6">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Electric Cost</p>
|
||||
<h3 class="text-3xl font-extrabold">$38.1<span class="text-lg ms-2">/ MMBTU</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Fuel Cost</p>
|
||||
<h3 class="text-3xl font-extrabold">$29.56<span class="text-lg ms-2">/ MMBTU</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">COP at Balance Point</p>
|
||||
<h3 class="text-3xl font-extrabold">1.29</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,57 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/balance-point?mode=thermal" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-xl shadow-xl bg-blue-100 text-blue-600 border border-blue-600 py-4">
|
||||
<div class="flex">
|
||||
<div class="block text-blue-600 px-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scale">
|
||||
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="M7 21h10"/><path d="M12 3v18"/>
|
||||
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium">Thermal Balance Point</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center mb-8">
|
||||
<p class="font-medium">Balance Point</p>
|
||||
<h3 class="text-3xl font-extrabold">36.9<span class="text-lg ms-2">°F</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 space-y-6">
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Heat Loss - Known</p>
|
||||
<h3 class="text-3xl font-extrabold">45,667<span class="text-lg ms-2">BTU/h</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Heating Design Temperature</p>
|
||||
<h3 class="text-3xl font-extrabold">5<span class="text-lg ms-2">°F</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Capacity @ 47°</p>
|
||||
<h3 class="text-3xl font-extrabold">24,600<span class="text-lg ms-2">BTU/h</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Capacity @ 17°</p>
|
||||
<h3 class="text-3xl font-extrabold">15,100<span class="text-lg ms-2">BTU/h</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,93 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/attic-ventilation" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div class=" w-full rounded-lg shadow-lg border-2 p-6 border-amber-500
|
||||
text-amber-500 bg-amber-200">
|
||||
<div class="flex text-2xl mb-6">
|
||||
<div class="block text-amber-500">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path stroke="currentColor" d="M12 9C11.1077 8.98562 10.2363 9.27003 9.52424 9.808C8.81222 10.346 8.30055 11.1066 8.07061 11.9688C7.84068 12.8311 7.90568 13.7455 8.25529 14.5665C8.6049 15.3876 9.21904 16.0682 10 16.5M12 3V5M6.6 18.4L5.2 19.8M4 13H2M6.6 7.6L5.2 6.2M20 14.5351V4C20 2.89543 19.1046 2 18 2C16.8954 2 16 2.89543 16 4V14.5351C14.8044 15.2267 14 16.5194 14 18C14 20.2091 15.7909 22 18 22C20.2091 22 22 20.2091 22 18C22 16.5194 21.1956 15.2267 20 14.5351Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h4 class="font-extrabold ms-2">Ventilation Status: Inadequate</h4>
|
||||
</div>
|
||||
<div class="grid justify-items-stretch grid-cols-1 md:grid-cols-3">
|
||||
<div class="justify-self-center">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Temperature Differential</p>
|
||||
<h3 class="text-3xl font-extrabold">0 °F</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-self-center">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Dewpoint Differential</p>
|
||||
<h3 class="text-3xl font-extrabold">9 °F</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-self-center">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Pressure Differential</p>
|
||||
<h3 class="text-3xl font-extrabold">1 °F</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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</h4>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 content-center">
|
||||
<div class="justify-self-center">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Intake</p>
|
||||
<h3 class="text-3xl font-extrabold">4.9 ft²</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="justify-self-center">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Exhaust</p>
|
||||
<h3 class="text-3xl font-extrabold">3.3 ft²</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h4 class="font-bold">Recommendations:</h4>
|
||||
<ul class="list-disc mx-10">
|
||||
<li>Add 4.9 ft² of intake ventilation.</li>
|
||||
<li>Add 3.3 ft² of exhaust ventilation.</li>
|
||||
<li>Consider adding more exhaust ventilation to balance pressure.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings" class=" 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 border-amber-500">Warning:</span>
|
||||
<ul class="list-disc mx-10 mt-4">
|
||||
<li>High moisture levels in the attic - increased ventilation is recommended</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Calculations are based on standard ventilation guidelines and building codes.
|
||||
Local codes may vary. Consider consulting with a qualified professional for specific
|
||||
recommendations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,82 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path stroke="currentColor" d="M12 9C11.1077 8.98562 10.2363 9.27003 9.52424 9.808C8.81222 10.346 8.30055 11.1066 8.07061 11.9688C7.84068 12.8311 7.90568 13.7455 8.25529 14.5665C8.6049 15.3876 9.21904 16.0682 10 16.5M12 3V5M6.6 18.4L5.2 19.8M4 13H2M6.6 7.6L5.2 6.2M20 14.5351V4C20 2.89543 19.1046 2 18 2C16.8954 2 16 2.89543 16 4V14.5351C14.8044 15.2267 14 16.5194 14 18C14 20.2091 15.7909 22 18 22C20.2091 22 22 20.2091 22 18C22 16.5194 21.1956 15.2267 20 14.5351Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">HVAC System Performance</h2>
|
||||
</div>
|
||||
<form hx-post="/hvac-system-performance" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="systemSize" class="block text-sm font-medium mb-2">System Size (Tons)</label>
|
||||
<input id="systemSize" placeholder="System size" name="systemSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="airflow" class="block text-sm font-medium mb-2">Airflow (CFM)</label>
|
||||
<input id="airflow" placeholder="Airflow" name="airflow" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="altitude" class="block text-sm font-medium mb-2">Altitude (ft.)</label>
|
||||
<input id="altitude" placeholder="Project altitude (Optional)" name="altitude" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="1" step="0.5">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium">Return Air</h3>
|
||||
<div>
|
||||
<label for="returnAirTemperature" class="block text-sm font-medium mb-2">Dry Bulb (°F)</label>
|
||||
<input id="returnAirTemperature" placeholder="Return temperature" name="returnAirTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="returnAirHumidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
|
||||
<input id="returnAirHumidity" placeholder="Return humidity" name="returnAirHumidity" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-medium">Supply Air</h3>
|
||||
<div>
|
||||
<label for="supplyAirTemperature" class="block text-sm font-medium mb-2">Dry Bulb (°F)</label>
|
||||
<input id="supplyAirTemperature" placeholder="Supply temperature" name="supplyAirTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500">
|
||||
</div>
|
||||
<div>
|
||||
<label for="supplyAirHumidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
|
||||
<input id="supplyAirHumidity" placeholder="Supply humidity" name="supplyAirHumidity" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Performance</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,95 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/hvac-system-performance" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<div class="rounded-xl text-blue-600 bg-blue-100 border border-blue-600 p-6">
|
||||
<h4 class="text-lg font-semibold mb-2">Total Capacity</h4>
|
||||
<div class="text-3xl font-bold">2.9<span class="text-xl ps-2">Tons</span></div>
|
||||
<div class="text-sm mt-1">35,396.9 BTU/h</div>
|
||||
</div>
|
||||
<div class="rounded-xl text-green-600 bg-green-100 border border-green-600 p-6">
|
||||
<h4 class="text-lg font-semibold mb-2">Sensible Capacity</h4>
|
||||
<div class="text-3xl font-bold">1.4<span class="text-xl ps-2">Tons</span></div>
|
||||
<div class="text-sm mt-1">17,280 BTU/h</div>
|
||||
</div>
|
||||
<div class="rounded-xl text-purple-600 bg-purple-100 border border-purple-600 p-6">
|
||||
<h4 class="text-lg font-semibold mb-2">Latent Capacity</h4>
|
||||
<div class="text-3xl font-bold">1.5<span class="text-xl ps-2">Tons</span></div>
|
||||
<div class="text-sm mt-1">18,116.9 BTU/h</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 border rounded-xl shadow-lg">
|
||||
<h4 class="text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">System Performance Metrics</h4>
|
||||
<div class="flex justify-between items-center p-4">
|
||||
<div class="space-y-3">
|
||||
<div class="mb-8">
|
||||
<span class="text-sm">Airflow per Ton</span>
|
||||
<p class="text-2xl font-semibold">400<span class="text-base font-normal ps-2">CFM/ton</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<span class="text-sm">Temperature Split</span>
|
||||
<p class="text-2xl font-semibold">20<span class="text-base font-normal ps-2">°F</span></p>
|
||||
</div>
|
||||
<p class="text-xs">Target: 20.83 °F</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="mb-8">
|
||||
<span class="text-sm">Moisture Removal</span>
|
||||
<p class="text-2xl font-semibold">2<span class="text-base font-normal ps-2">gal/h</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm">Sensible Heat Ratio</span>
|
||||
<p class="text-2xl font-semibold">0.5<span class="text-base font-normal ps-2">%</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings" class=" mt-6 p-4 rounded-lg shadow-lg
|
||||
text-amber-500
|
||||
bg-amber-100 dark:bg-amber-200
|
||||
border border-amber-500 mb-4 mx-8">
|
||||
<span class="font-semibold mb-4 border-b border-amber-500">Warning:</span>
|
||||
<ul class="list-disc mx-10 mt-4">
|
||||
<li>Low sensible heat ratio may indicate excessive dehumidification or low airflow.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-8">
|
||||
<div class="rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4">
|
||||
<h4 class=" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">Return Air Properties</h4>
|
||||
<div class="w-full grid grid-cols-1 gap-4">
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">64.3 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">67.64 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">32.75 Btu/lb</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">14.2</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">94.18 gr/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-xl border border-blue-600 dark:border-gray-400 bg-blue-100 dark:bg-slate-700 p-4">
|
||||
<h4 class=" text-lg font-semibold flex justify-center py-2 mb-4 text-blue-600 dark:text-slate-300">Supply Air Properties</h4>
|
||||
<div class="w-full grid grid-cols-1 gap-4">
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">52.87 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">54.01 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">22.92 Btu/lb</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.2 psi</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">13.57</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">61.47 gr/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.89</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="w-8 h-8">
|
||||
<path d="M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Mold Risk Calculator</h2>
|
||||
</div>
|
||||
<form hx-post="/mold-risk" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="temperature" class="block text-sm font-medium mb-2">Indoor Temperature (°F)</label>
|
||||
<input id="temperature" placeholder="Dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="humidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
|
||||
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Mold Risk</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,60 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/mold-risk" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="p-2 rounded-lg shadow-lg bg-amber-200 border-2 border border-amber-500">
|
||||
<div class="text-amber-500">
|
||||
<div class="flex flex-wrap mt-2">
|
||||
<div class="w-full sm:w-1/2 flex gap-2">
|
||||
<div class="block text-amber-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
|
||||
<path d="M12 9v4"></path>
|
||||
<path d="M12 17h.01"></path>
|
||||
</svg>
|
||||
</div>
|
||||
Risk Level: Moderate<span class="text-lg font-extrabold"></span>
|
||||
</div>
|
||||
<div class="w-full sm:w-1/2 gap-2"><span class="font-semibold">Estimated Days to Mold Growth: </span><span>30 days</span></div>
|
||||
</div>
|
||||
<div class="mt-6 pb-4">
|
||||
<p class="font-semibold mb-4">
|
||||
<u>Recommendations:</u>
|
||||
</p>
|
||||
<ul class="list-disc mx-10">
|
||||
<li>Improve ventilation to reduce moisture accumulation</li>
|
||||
<li>Inspect for and repair any water leaks or intrusion</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full rounded-lg border mt-8">
|
||||
<h3 class="flex justify-center text-xl font-semibold mb-6 mt-2">Psychrometric Properties</h3>
|
||||
<div class="w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-6 gap-y-2 px-4 pb-4">
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Dew Point: </span><span class="font-light">64.3 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Wet Bulb: </span><span class="font-light">67.7 °F</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Enthalpy: </span><span class="font-light">32.33 Btu/lb</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Specific Volume: </span><span class="font-light">13.78</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">91.4 gr/ft³</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
|
||||
<div class="flex items-center justify-between text-blue-500 dark:text-slate-200"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
|
||||
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Psychrometric Properties</h2>
|
||||
</div>
|
||||
<form hx-post="/psychrometric-properties" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="temperature" class="block text-sm font-medium mb-2">Temperature (°F)</label>
|
||||
<input id="temperature" placeholder="Dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="humidity" class="block text-sm font-medium mb-2">Relative Humidity (%)</label>
|
||||
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" max="100" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="altitude" class="block text-sm font-medium mb-2">Altitude (ft.)</label>
|
||||
<input id="altitude" placeholder="Altitude (optional)" name="altitude" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Psychrometrics</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/psychrometric-properties" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
|
||||
<div class="flex mb-8">
|
||||
<div class="block text-blue-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
|
||||
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl px-2 font-semibold">Psychrometrics</h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="rounded-lg bg-purple-200 border border-purple-600 text-purple-600">
|
||||
<div class="grid grid-cols-1 justify-items-center my-8">
|
||||
<p class="font-medium">Dew Point</p>
|
||||
<h3 class="text-3xl font-extrabold">64.3<span class="text-lg ms-2">°F</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-orange-200 border border-orange-600 text-orange-600">
|
||||
<div class="grid grid-cols-1 justify-items-center my-8">
|
||||
<p class="font-medium">Enthalpy</p>
|
||||
<h3 class="text-3xl font-extrabold">32.8<span class="text-lg ms-2">Btu/lb</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg bg-green-200 border border-green-600 text-green-600">
|
||||
<div class="grid grid-cols-1 justify-items-center my-8">
|
||||
<p class="font-medium">Wet Bulb</p>
|
||||
<h3 class="text-3xl font-extrabold">67.6<span class="text-lg ms-2">°F</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<h4 class="text-lg font-semibold">Other Properties</h4>
|
||||
<div class="rounded-lg border border-blue-300">
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Density: </span><span class="font-light">0.07 lb/ft³</span></div>
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Vapor Pressure: </span><span class="font-light">0.3 psi</span></div>
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Specific Volume: </span><span class="font-light">14.2</span></div>
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Absolute Humidity: </span><span class="font-light">94.18 gr/ft³</span></div>
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Humidity Ratio: </span><span class="font-light">0.01</span></div>
|
||||
<div class="flex items-center justify-between border-b border-blue-300 p-2"><span class="font-semibold">Degree of Saturation: </span><span class="font-light">0.66</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,80 @@
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-right">
|
||||
<path d="M8 3 4 7l4 4"/>
|
||||
<path d="M4 7h16"/>
|
||||
<path d="m16 21 4-4-4-4"/>
|
||||
<path d="M20 17H4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Room Pressure Calculator - Known Airflow</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Known Airflow</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/room-pressure?mode=measuredPressure" type="button">Measured Pressure</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/room-pressure" hx-target="#result" class="mt-6">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="targetRoomPressure" class="block text-sm font-medium mb-2">Target Room Pressure (Pascals)</label>
|
||||
<input id="targetRoomPressure" placeholder="Room pressure (max 3 pa.)" name="targetRoomPressure" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" max="3.0" autofocus required>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="doorWidth" class="block text-sm font-medium mb-2">Door Width (in.)</label>
|
||||
<input id="doorWidth" placeholder="Width" name="doorWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="doorHeight" class="block text-sm font-medium mb-2">Door Height (in.)</label>
|
||||
<input id="doorHeight" placeholder="Height" name="doorHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="doorUndercut" class="block text-sm font-medium mb-2">Door Undercut (in.)</label>
|
||||
<input id="doorUndercut" placeholder="Undercut height" name="doorUndercut" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="supplyAirflow" class="block text-sm font-medium mb-2">Supply Airflow (CFM)</label>
|
||||
<input id="supplyAirflow" placeholder="Airflow" name="supplyAirflow" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
Preferred Grille Height<label for="preferredGrilleHeight" class="block text-sm font-medium mb-2"></label>
|
||||
<select id="preferredGrilleHeight" name="preferredGrilleHeight" class="w-full px-4 py-2 rounded-md border">
|
||||
<option value="4">4"</option>
|
||||
<option value="6">6"</option>
|
||||
<option value="8">8"</option>
|
||||
<option value="10">10"</option>
|
||||
<option value="12">12"</option>
|
||||
<option value="14">14"</option>
|
||||
</select>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Return Path Size</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,73 @@
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-left-right">
|
||||
<path d="M8 3 4 7l4 4"/>
|
||||
<path d="M4 7h16"/>
|
||||
<path d="m16 21 4-4-4-4"/>
|
||||
<path d="M20 17H4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Room Pressure Calculator - Measured Pressure</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/room-pressure?mode=knownAirflow" type="button">Known Airflow</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-e-lg" disabled type="button">Measured Pressure</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/room-pressure" hx-target="#result" class="mt-6">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="measuredRoomPressure" class="block text-sm font-medium mb-2">Measured Room Pressure (Pascals)</label>
|
||||
<input id="measuredRoomPressure" placeholder="Measured pressure" name="measuredRoomPressure" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="doorWidth" class="block text-sm font-medium mb-2">Door Width (in.)</label>
|
||||
<input id="doorWidth" placeholder="Width" name="doorWidth" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="doorHeight" class="block text-sm font-medium mb-2">Door Height (in.)</label>
|
||||
<input id="doorHeight" placeholder="Height" name="doorHeight" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="doorUndercut" class="block text-sm font-medium mb-2">Door Undercut (in.)</label>
|
||||
<input id="doorUndercut" placeholder="Undercut height" name="doorUndercut" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
Preferred Grille Height<label for="preferredGrilleHeight" class="block text-sm font-medium mb-2"></label>
|
||||
<select id="preferredGrilleHeight" name="preferredGrilleHeight" class="w-full px-4 py-2 rounded-md border">
|
||||
<option value="4">4"</option>
|
||||
<option value="6">6"</option>
|
||||
<option value="8">8"</option>
|
||||
<option value="10">10"</option>
|
||||
<option value="12">12"</option>
|
||||
<option value="14">14"</option>
|
||||
</select>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Return Path Size</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/room-pressure?mode=knownAirflow" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="rounded-xl p-6 bg-blue-100 border border-blue-600 text-blue-600">
|
||||
<h4 class="text-xl font-bold">Return / Transfer Grille</h4>
|
||||
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span> 14" x 14"</span></div>
|
||||
<div class="flex justify-between mt-3"><span class="font-semibold">Required Net Free Area:</span><span> 0.5 in<sup>2</sup></span></div>
|
||||
<div class="mt-8 text-sm"><span class="font-semibold">Note: </span><span>Select a grille with at least 0.5 in<sup>2</sup><span> net free area.</span></span></div>
|
||||
</div>
|
||||
<div class="rounded-xl p-6 bg-purple-100 border border-purple-600 text-purple-600">
|
||||
<h4 class="text-xl font-bold">Return / Transfer Duct</h4>
|
||||
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span>9"</span></div>
|
||||
<div class="flex justify-between mt-3"><span class="font-semibold">Air Velocity:</span><span>452.7 FPM</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Calculations are based on a target velocity of 400 FPM for return/transfer air paths.
|
||||
The required net free area is the minimum needed - select a grille that meets or exceeds this value.
|
||||
Verify manufacturer specifications for actual net free area of selected grilles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/room-pressure?mode=measuredPressure" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="rounded-xl p-6 bg-blue-100 border border-blue-600 text-blue-600">
|
||||
<h4 class="text-xl font-bold">Return / Transfer Grille</h4>
|
||||
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span> 14" x 14"</span></div>
|
||||
<div class="flex justify-between mt-3"><span class="font-semibold">Required Net Free Area:</span><span> 0.5 in<sup>2</sup></span></div>
|
||||
<div class="mt-8 text-sm"><span class="font-semibold">Note: </span><span>Select a grille with at least 0.5 in<sup>2</sup><span> net free area.</span></span></div>
|
||||
</div>
|
||||
<div class="rounded-xl p-6 bg-purple-100 border border-purple-600 text-purple-600">
|
||||
<h4 class="text-xl font-bold">Return / Transfer Duct</h4>
|
||||
<div class="flex justify-between mt-6"><span class="font-semibold">Standard Size:</span><span>9"</span></div>
|
||||
<div class="flex justify-between mt-3"><span class="font-semibold">Air Velocity:</span><span>440.7 FPM</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Calculations are based on a target velocity of 400 FPM for return/transfer air paths.
|
||||
The required net free area is the minimum needed - select a grille that meets or exceeds this value.
|
||||
Verify manufacturer specifications for actual net free area of selected grilles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
|
||||
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Test Capacitor</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Test Capacitor</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=size" type="button">Size Capacitor</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="startWindingAmps" class="block text-sm font-medium mb-2">Start Winding: (amps)</label>
|
||||
<input id="startWindingAmps" placeholder="Current amps" name="startWindingAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="runToCommonVoltage" class="block text-sm font-medium mb-2">Run to Common: (volts)</label>
|
||||
<input id="runToCommonVoltage" placeholder="Voltage" name="runToCommonVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ratedCapacitorSize" class="block text-sm font-medium mb-2">Capacitor Rated Size: (µF)</label>
|
||||
<input id="ratedCapacitorSize" placeholder="Size (optional)" name="ratedCapacitorSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Test Capacitor</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-gauge">
|
||||
<path d="M15.6 2.7a10 10 0 1 0 5.7 5.7"/>
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
<path d="M13.4 10.6 19 5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Hydronic System Pressure</h2>
|
||||
</div>
|
||||
<form hx-post="/hydronic-system-pressure" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="height" class="block text-sm font-medium mb-2">Height (ft.)</label>
|
||||
<input id="height" placeholder="Building height" name="height" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="waterTemperature" class="block text-sm font-medium mb-2">Water Temperature (°F)</label>
|
||||
<input id="waterTemperature" placeholder="Temperature (optional)" name="waterTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="32.0" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Water temperature should be the coldest water temperature the system sees, which for boilers will be
|
||||
when the system is filled with water.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate System Pressure</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/hydronic-system-pressure" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Pressure</p>
|
||||
<h3 class="text-3xl font-extrabold">10.22<span class="text-lg ms-2">psi</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Water Density</p>
|
||||
<h3 class="text-3xl font-extrabold">62.58<span class="text-lg ms-2">lb/ft³</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">Expansion tank pressure should match system fill pressure.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,68 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-footprints">
|
||||
<path d="M4 16v-2.38C4 11.5 2.97 10.5 3 8c.03-2.72 1.49-6 4.5-6C9.37 2 10 3.8 10 5.5c0 3.11-2 5.66-2 8.68V16a2 2 0 1 1-4 0Z"/>
|
||||
<path d="M20 20v-2.38c0-2.12 1.03-3.12 1-5.62-.03-2.72-1.49-6-4.5-6C14.63 6 14 7.8 14 9.5c0 3.11 2 5.66 2 8.68V20a2 2 0 1 0 4 0Z"/>
|
||||
<path d="M16 17h4"/>
|
||||
<path d="M4 13h4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Feet of Head</h2>
|
||||
</div>
|
||||
<form hx-post="/feet-of-head" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="pressure" class="block text-sm font-medium mb-2">Pressure (psi)</label>
|
||||
<input id="pressure" placeholder="Pressure" name="pressure" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="0.1" step="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="waterTemperature" class="block text-sm font-medium mb-2">Water Temperature (°F)</label>
|
||||
<input id="waterTemperature" placeholder="Temperature (optional)" name="waterTemperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" min="32.0" step="0.1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">If water temperature is not supplied, then calculations will be based on 60°F water.</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Feet of Head</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result">
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/feet-of-head" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Feet of Head</p>
|
||||
<h3 class="text-3xl font-extrabold">7.95<span class="text-lg ms-2">ft.</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Water Density</p>
|
||||
<h3 class="text-3xl font-extrabold">62.37<span class="text-lg ms-2">lb/ft³</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/feet-of-head" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="w-full rounded-lg shadow-lg bg-blue-100 border border-blue-600 text-blue-600 p-6">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Feet of Head</p>
|
||||
<h3 class="text-3xl font-extrabold">7.9<span class="text-lg ms-2">ft.</span></h3>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 justify-items-center">
|
||||
<p class="font-medium">Water Density</p>
|
||||
<h3 class="text-3xl font-extrabold">62.58<span class="text-lg ms-2">lb/ft³</span></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="warnings"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
|
||||
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Size Capacitor</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-s-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=test" type="button">Test Capacitor</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-e-lg" disabled type="button">Size Capacitor</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="runningAmps" class="block text-sm font-medium mb-2">Running Current: (amps)</label>
|
||||
<input id="runningAmps" placeholder="Current amps" name="runningAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lineVoltage" class="block text-sm font-medium mb-2">Line Voltage</label>
|
||||
<input id="lineVoltage" placeholder="Voltage" name="lineVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="powerFactor" class="block text-sm font-medium mb-2">Power Factor</label>
|
||||
<input id="powerFactor" placeholder="Power factor (0-1)" name="powerFactor" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.01" min="0.1" max="1.00" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Size</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<div class="relative">
|
||||
<div class="flex flex-wrap justify-between">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-zap">
|
||||
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Capacitor Calculator - Test Capacitor</h2>
|
||||
</div>
|
||||
<div class="flex items-center gap-x-0 mb-6">
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-yellow-300 enabled:hover:bg-yellow-400
|
||||
text-blue-600 rounded-s-lg" disabled type="button">Test Capacitor</button>
|
||||
<button class=" font-bold py-2 px-4 transition-colors
|
||||
bg-blue-500 enabled:hover:bg-blue-600
|
||||
text-yellow-300 rounded-e-lg" hx-target="#content" hx-push-url="true" hx-get="/capacitor-calculator?mode=size" type="button">Size Capacitor</button>
|
||||
</div>
|
||||
</div>
|
||||
<form hx-post="/capacitor-calculator" hx-target="#result" class="mt-6">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="startWindingAmps" class="block text-sm font-medium mb-2">Start Winding: (amps)</label>
|
||||
<input id="startWindingAmps" placeholder="Current amps" name="startWindingAmps" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="runToCommonVoltage" class="block text-sm font-medium mb-2">Run to Common: (volts)</label>
|
||||
<input id="runToCommonVoltage" placeholder="Voltage" name="runToCommonVoltage" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ratedCapacitorSize" class="block text-sm font-medium mb-2">Capacitor Rated Size: (µF)</label>
|
||||
<input id="ratedCapacitorSize" placeholder="Size (optional)" name="ratedCapacitorSize" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Test Capacitor</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/capacitor-calculator?mode=size" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class=" w-full rounded-lg shadow-lg p-4
|
||||
border-2 border-blue-600 bg-blue-100 text-blue-600">
|
||||
<div class="flex justify-center"><span class="font-extrabold">Required Capacitor Size</span></div>
|
||||
<div class="flex justify-between mb-2"><span class="font-semibold">Recommended Standard Size</span><span>60.0 µF</span></div>
|
||||
<div class="flex justify-between mb-2"><span class="font-semibold">Calculated Size:</span><span>59.6 µF</span></div>
|
||||
<div class="flex justify-between mb-2"><span class="font-semibold">Acceptable Range (±6%):</span><span>57.2 - 63.2 µF</span></div>
|
||||
</div>
|
||||
<div id="warnings" class=" 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-extrabold">Important Notes:</span>
|
||||
<ul class="list-disc mx-10 mt-4">
|
||||
<li>Always verify voltage rating matches the application.</li>
|
||||
<li>Use the next larger size if exact match is unavailable.</li>
|
||||
<li>Ensure capacitor is rated for continuous duty.</li>
|
||||
<li>Consider ambient temperature in final selection.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/capacitor-calculator?mode=test" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 gap-4 mt-8">
|
||||
<div class="bg-blue-100 rounded-lg border-2 border-blue-600 text-blue-500 px-4 pb-4">
|
||||
<div class="flex justify-center mb-6 mt-2"><span class="text-2xl font-extrabold">Measured</span></div>
|
||||
<div class="flex justify-between mb-2"><span class="font-semibold">Capacitance</span><span>33.26 µF</span></div>
|
||||
<div class="flex justify-between mb-2"><span class="font-semibold">Acceptable Range (±6%):</span><span>31.9 - 35.3 µF</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="block text-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
|
||||
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-2xl font-extrabold">Dehumidifier Sizing Calculator</h2>
|
||||
</div>
|
||||
<form hx-post="/dehumidifier-sizing" hx-target="#result">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="latentLoad" class="block text-sm font-medium mb-2">Latent Load (BTU/h)</label>
|
||||
<input id="latentLoad" placeholder="Latent load from Manual-J" name="latentLoad" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" autofocus required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="temperature" class="block text-sm font-medium mb-2">Indoor Temperature (°F)</label>
|
||||
<input id="temperature" placeholder="Indoor dry bulb temperature" name="temperature" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="humidity" class="block text-sm font-medium mb-2">Indoor Humdity (%)</label>
|
||||
<input id="humidity" placeholder="Relative humidity" name="humidity" class=" w-full px-4 py-2 border rounded-md min-h-11
|
||||
focus:ring-2 focus:ring-yellow-800 focus:border-yellow-800
|
||||
placeholder-shown:!border-gray-400
|
||||
invalid:border-red-500 out-of-range:border-red-500" type="number" step="0.1" min="0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class=" w-full font-bold py-3 rounded-md transition-colors
|
||||
bg-yellow-300 dark:bg-blue-500
|
||||
hover:bg-yellow-400 hover:dark:bg-blue-600
|
||||
text-blue-500 dark:text-yellow-300">Calculate Dehumidifier Size</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="result"></div>
|
||||
@@ -0,0 +1,59 @@
|
||||
<div class=" mt-6 p-6 rounded-lg border border-blue-500
|
||||
bg-blue-50 dark:bg-slate-600
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div class="relative">
|
||||
<h3 class="text-xl font-semibold mb-4">Results</h3>
|
||||
<button class=" font-bold px-4 py-2 rounded-md transition-colors
|
||||
bg-blue-500 dark:bg-yellow-300
|
||||
hover:bg-blue-600 hover:dark:bg-yellow-400
|
||||
text-yellow-300 dark:text-blue-500 absolute bottom-0 right-0" hx-get="/dehumidifier-sizing" hx-target="#content">Reset</button>
|
||||
</div>
|
||||
<div class="py-4 block">
|
||||
<div class="mb-6 sm:grid sm:grid-cols-1 sm:gap-4 lg:flex items-center lg:justify-between
|
||||
text-blue-500 dark:text-slate-200">
|
||||
<div><span class="font-semibold">Base Moisture Load: </span><span class="font-light">78.4 pints/day</span></div>
|
||||
</div>
|
||||
<p class="font-semibold mb-4 dark:text-yellow-300">Recommended Size:</p><a target="_blank" href="https://www.santa-fe-products.com/product/ultra98-dehumidifier/" 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">100 PPD</span>
|
||||
<p class="text-sm mt-1">Click to view recommended model →</p>
|
||||
</div>
|
||||
<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">
|
||||
<div class="block text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M7 16.3c2.2 0 4-1.83 4-4.05 0-1.16-.57-2.26-1.71-3.19S7.29 6.75 7 5.3c-.29 1.45-1.14 2.84-2.29 3.76S3 11.1 3 12.25c0 2.22 1.8 4.05 4 4.05z"></path>
|
||||
<path d="M12.56 6.6A10.97 10.97 0 0014 3.02c.5 2.5 2 4.9 4 6.5s3 3.5 3 5.5a6.98 6.98 0 01-11.91 4.97"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div></a>
|
||||
</div>
|
||||
<div id="warnings" class=" 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 border-amber-500">Warning:</span>
|
||||
<ul class="list-disc mx-10 mt-4">
|
||||
<li>High relative humidity - unit may need to run continuously.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mt-8 p-4 bg-gray-100 dark:bg-gray-700 rounded-md shadow-md
|
||||
border border-blue-500 text-blue-500 text-sm">
|
||||
<p class="font-extrabold mb-3">Note:</p>
|
||||
<p class="px-6">
|
||||
Sizing is based on continuous operation at rated conditions. Actual performance will vary based on
|
||||
operating conditions. Consider Energy Star rated units for better efficiency. For whole-house
|
||||
applications, ensure proper air distribution and drainage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
6
justfile
6
justfile
@@ -12,7 +12,7 @@ build-docker platform="linux/arm64":
|
||||
run:
|
||||
#!/usr/bin/env zsh
|
||||
touch .build/browser-dev-sync
|
||||
browser-sync start -p localhost:8080 --ws &
|
||||
browser-sync start -p localhost:8080 --watch --files '.build/browser-dev-sync' &
|
||||
watchexec -w Sources -e .swift -r 'swift build --product App && touch .build/browser-dev-sync' &
|
||||
watchexec -w .build/browser-dev-sync --ignore-nothing -r '.build/debug/App serve --log debug'
|
||||
|
||||
@@ -27,3 +27,7 @@ push-image:
|
||||
|
||||
build-docker-production:
|
||||
@docker build --platform "linux/amd64" -t {{docker_registiry}}/{{docker_image}}:{{docker_tag}} .
|
||||
|
||||
test-docker:
|
||||
@docker build --tag {{docker_image}}:test --file Dockerfile.dev . \
|
||||
&& docker run --interactive --rm {{docker_image}}:test swift test
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"tailwindcss": "^4.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/cli": "^4.0.8"
|
||||
"@tailwindcss/cli": "^4.0.8",
|
||||
"browser-sync": "^3.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
1049
pnpm-lock.yaml
generated
1049
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user