diff --git a/Sources/ManualDCore/ComponentPressureLosses.swift b/Sources/ManualDCore/ComponentPressureLosses.swift index d7b1a21..6aac771 100644 --- a/Sources/ManualDCore/ComponentPressureLosses.swift +++ b/Sources/ManualDCore/ComponentPressureLosses.swift @@ -59,4 +59,51 @@ public typealias ComponentPressureLosses = [String: Double] ] } } + + extension ComponentPressureLoss { + public static var mock: [Self] { + [ + .init( + id: UUID(0), + projectID: UUID(0), + name: "evaporator-coil", + value: 0.2, + createdAt: Date(), + updatedAt: Date() + ), + .init( + id: UUID(1), + projectID: UUID(0), + name: "filter", + value: 0.1, + createdAt: Date(), + updatedAt: Date() + ), + .init( + id: UUID(2), + projectID: UUID(0), + name: "supply-outlet", + value: 0.03, + createdAt: Date(), + updatedAt: Date() + ), + .init( + id: UUID(3), + projectID: UUID(0), + name: "return-grille", + value: 0.03, + createdAt: Date(), + updatedAt: Date() + ), + .init( + id: UUID(4), + projectID: UUID(0), + name: "balance-damper", + value: 0.03, + createdAt: Date(), + updatedAt: Date() + ), + ] + } + } #endif diff --git a/Sources/ManualDCore/EquipmentInfo.swift b/Sources/ManualDCore/EquipmentInfo.swift index 686721d..d15a50d 100644 --- a/Sources/ManualDCore/EquipmentInfo.swift +++ b/Sources/ManualDCore/EquipmentInfo.swift @@ -1,3 +1,4 @@ +import Dependencies import Foundation public struct EquipmentInfo: Codable, Equatable, Identifiable, Sendable { @@ -50,3 +51,16 @@ extension EquipmentInfo { } } } + +#if DEBUG + extension EquipmentInfo { + public static let mock = Self( + id: UUID(0), + projectID: UUID(0), + heatingCFM: 1000, + coolingCFM: 1000, + createdAt: Date(), + updatedAt: Date() + ) + } +#endif diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 315c47a..f087ed4 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -89,7 +89,7 @@ extension SiteRoute.View { extension SiteRoute.View { public enum FrictionRateRoute: Equatable, Sendable { case index - case form + case form(FormType, dismiss: Bool = false) static let rootPath = "friction-rate" @@ -104,7 +104,18 @@ extension SiteRoute.View { "create" } Method.get + Query { + Field("type") { FormType.parser() } + Field("dismiss", default: false) { Bool.parser() } + } } } } } + +extension SiteRoute.View.FrictionRateRoute { + public enum FormType: String, CaseIterable, Codable, Equatable, Sendable { + case equipmentInfo + case componentPressureLoss + } +} diff --git a/Sources/Styleguide/Buttons.swift b/Sources/Styleguide/Buttons.swift index f742c85..9390ffa 100644 --- a/Sources/Styleguide/Buttons.swift +++ b/Sources/Styleguide/Buttons.swift @@ -51,3 +51,32 @@ public struct CancelButton: HTML, Sendable { } } } + +public struct EditButton: HTML, Sendable { + let title: String + let type: HTMLAttribute.ButtonType + + public init( + title: String = "Edit", + type: HTMLAttribute.ButtonType = .button + ) { + self.title = title + self.type = type + } + + public var body: some HTML { + button( + .class( + """ + text-white font-bold text-xl bg-blue-500 hover:bg-blue-600 px-4 py-2 rounded-lg shadow-lg + """ + ), + .type(type) + ) { + div(.class("flex")) { + span(.class("pe-2")) { title } + SVG(.squarePen) + } + } + } +} diff --git a/Sources/Styleguide/Label.swift b/Sources/Styleguide/Label.swift new file mode 100644 index 0000000..5bc252f --- /dev/null +++ b/Sources/Styleguide/Label.swift @@ -0,0 +1,20 @@ +import Elementary + +public struct Label: HTML, Sendable { + + let title: String + + public init(_ title: String) { + self.title = title + } + + public init(_ title: @escaping () -> String) { + self.title = title() + } + + public var body: some HTML { + span(.class("text-xl text-gray-400 font-bold")) { + title + } + } +} diff --git a/Sources/Styleguide/SVG.swift b/Sources/Styleguide/SVG.swift index 510ca90..373d586 100644 --- a/Sources/Styleguide/SVG.swift +++ b/Sources/Styleguide/SVG.swift @@ -16,6 +16,7 @@ public struct SVG: HTML, Sendable { extension SVG { public enum Key: Sendable { case close + case squarePen var svg: String { switch self { @@ -23,6 +24,10 @@ extension SVG { return """ """ + case .squarePen: + return """ + + """ } } } diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 1ba9ad8..8284ca9 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -58,11 +58,28 @@ extension SiteRoute.View.FrictionRateRoute { return MainPage { FrictionRateView() } - // FIX: - default: - return MainPage { - FrictionRateView() + case .form(let type, let dismiss): + guard !dismiss else { + return div(.id(type.id)) {} } + // FIX: Forms need to reference existing items. + switch type { + case .equipmentInfo: + return EquipmentForm() + case .componentPressureLoss: + return ComponentLossForm() + } + } + } +} + +extension SiteRoute.View.FrictionRateRoute.FormType { + var id: String { + switch self { + case .equipmentInfo: + return "equipmentForm" + case .componentPressureLoss: + return "componentLossForm" } } } diff --git a/Sources/ViewController/Views/FrictionRate/ComponentLossForm.swift b/Sources/ViewController/Views/FrictionRate/ComponentLossForm.swift new file mode 100644 index 0000000..61f167f --- /dev/null +++ b/Sources/ViewController/Views/FrictionRate/ComponentLossForm.swift @@ -0,0 +1,45 @@ +import Elementary +import ElementaryHTMX +import ManualDCore +import Styleguide + +struct ComponentLossForm: HTML, Sendable { + var body: some HTML { + div( + .id("componentLossForm"), + .class( + """ + fixed top-40 left-[25vw] w-1/2 z-50 text-gray-800 + bg-gray-200 border border-gray-400 + rounded-lg shadow-lg mx-10 + """ + ) + ) { + h1(.class("text-2xl font-bold")) { "Component Loss" } + form(.class("space-y-4 p-4")) { + div { + label(.for("name")) { "Name" } + Input(id: "name", placeholder: "Name") + .attributes(.type(.text), .required, .autofocus) + } + div { + label(.for("value")) { "Value" } + Input(id: "name", placeholder: "Pressure loss") + .attributes(.type(.number), .min("0"), .max("1"), .step("0.1"), .required) + } + Row { + div {} + div { + CancelButton() + .attributes( + .hx.get(route: .frictionRate(.form(.componentPressureLoss, dismiss: true))), + .hx.target("#componentLossForm"), + .hx.swap(.outerHTML) + ) + SubmitButton() + } + } + } + } + } +} diff --git a/Sources/ViewController/Views/FrictionRate/ComponentLossTable.swift b/Sources/ViewController/Views/FrictionRate/ComponentLossTable.swift new file mode 100644 index 0000000..972ab70 --- /dev/null +++ b/Sources/ViewController/Views/FrictionRate/ComponentLossTable.swift @@ -0,0 +1,62 @@ +import Elementary +import ElementaryHTMX +import ManualDCore +import Styleguide + +struct ComponentPressureLossTable: HTML, Sendable { + + let componentPressureLosses: [ComponentPressureLoss] + + var body: some HTML { + div(.id("cplTable")) { + h1(.class("text-2xl font-bold pb-4")) { "Component Pressure Losses" } + table( + .class( + "w-full border-collapse border border-gray-200 table-fixed" + ) + ) { + thead { tableHeader } + tbody(.id("cplTableBody")) { + Rows(componentPressureLosses: componentPressureLosses) + } + } + } + div(.id("componentLossForm")) {} + } + + private var tableHeader: some HTML { + tr { + th(.class("border border-gray-200 text-xl font-bold")) { "Name" } + th(.class("border border-gray-200 text-xl font-bold")) { "Pressure Loss" } + th(.class("border border-gray-200 text-xl font-bold")) { + Row { + div {} + button( + .class("px-2"), + .hx.get(route: .frictionRate(.form(.componentPressureLoss))), + .hx.target("#componentLossForm"), + .hx.swap(.outerHTML) + ) { + Icon(.circlePlus) + } + } + } + } + } + + private struct Rows: HTML, Sendable { + let componentPressureLosses: [ComponentPressureLoss] + + var body: some HTML { + for cpl in componentPressureLosses { + tr { + td(.class("border border-gray-200 p-2")) { cpl.name } + td(.class("border border-gray-200 p-2")) { "\(cpl.value)" } + td(.class("border border-gray-200 p-2")) { + // FIX: Add edit button + } + } + } + } + } +} diff --git a/Sources/ViewController/Views/FrictionRate/EquipmentForm.swift b/Sources/ViewController/Views/FrictionRate/EquipmentForm.swift index 94129fb..d7168ce 100644 --- a/Sources/ViewController/Views/FrictionRate/EquipmentForm.swift +++ b/Sources/ViewController/Views/FrictionRate/EquipmentForm.swift @@ -2,16 +2,28 @@ import Elementary import ManualDCore import Styleguide +// TODO: Have form hold onto equipment info model to edit. struct EquipmentForm: HTML, Sendable { var body: some HTML { - div(.id("equipmentForm")) { - h1(.class("text-3xl font-bold pb-6")) { "Equipment Info" } - form { + div( + .id("equipmentForm"), + .class( + """ + fixed top-40 left-[25vw] w-1/2 z-50 text-gray-800 + bg-gray-200 border border-gray-400 + rounded-lg shadow-lg mx-10 + """ + ) + ) { + h1(.class("text-3xl font-bold pb-6 ps-2")) { "Equipment Info" } + form(.class("space-y-4 p-4")) { div { label(.for("staticPressure")) { "Static Pressure" } Input(id: "staticPressure", placeholder: "Static pressure") - .attributes(.type(.number), .value("0.5"), .min("0"), .max("1.0"), .step("0.1")) + .attributes( + .type(.number), .value("0.5"), .min("0"), .max("1.0"), .step("0.1") + ) } div { label(.for("heatingCFM")) { "Heating CFM" } @@ -26,6 +38,12 @@ struct EquipmentForm: HTML, Sendable { Row { div {} div(.class("space-x-4")) { + CancelButton() + .attributes( + .hx.get(route: .frictionRate(.form(.equipmentInfo, dismiss: true))), + .hx.target("#equipmentForm"), + .hx.swap(.outerHTML) + ) SubmitButton(title: "Save") } } diff --git a/Sources/ViewController/Views/FrictionRate/EquipmentInfoView.swift b/Sources/ViewController/Views/FrictionRate/EquipmentInfoView.swift new file mode 100644 index 0000000..5b5631a --- /dev/null +++ b/Sources/ViewController/Views/FrictionRate/EquipmentInfoView.swift @@ -0,0 +1,45 @@ +import Elementary +import ManualDCore +import Styleguide + +struct EquipmentInfoView: HTML, Sendable { + let equipmentInfo: EquipmentInfo + + var body: some HTML { + div(.class("space-y-4 border border-gray-200 rounded-lg shadow-lg p-4")) { + Row { + h1(.class("text-2xl font-bold")) { "Equipment Info" } + } + + Row { + Label { "Static Pressure" } + span { "\(equipmentInfo.staticPressure)" } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label { "Heating CFM" } + span { "\(equipmentInfo.heatingCFM)" } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label { "Cooling CFM" } + span { "\(equipmentInfo.coolingCFM)" } + } + .attributes(.class("border-b border-gray-200")) + + Row { + div {} + EditButton() + .attributes( + .hx.get(route: .frictionRate(.form(.equipmentInfo))), + .hx.target("#equipmentForm"), + .hx.swap(.outerHTML) + ) + } + } + + div(.id("equipmentForm")) {} + } +} diff --git a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift index 3eb2d85..0e3fb49 100644 --- a/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift +++ b/Sources/ViewController/Views/FrictionRate/FrictionRateView.swift @@ -5,8 +5,10 @@ import Styleguide struct FrictionRateView: HTML, Sendable { var body: some HTML { - div { - EquipmentForm() + div(.class("w-full flex-1 p-4 space-y-6")) { + h1(.class("text-4xl font-bold pb-6")) { "Friction Rate" } + EquipmentInfoView(equipmentInfo: EquipmentInfo.mock) + ComponentPressureLossTable(componentPressureLosses: ComponentPressureLoss.mock) } } }