diff --git a/Package.resolved b/Package.resolved index 98debd1..bdca991 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "9668a267c19bc66e844dc3e46718c4415359171b587f26c2a592bc347bd5446a", + "originHash" : "5bbd172c602e6484b32782f8cb68faca2d5120adc511acdff74e62fec2178c15", "pins" : [ { "identity" : "async-http-client", @@ -456,8 +456,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/m-housh/swift-validations.git", "state" : { - "revision" : "95ea5d267e37f6cdb9f91c5c8a01e718b9299db6", - "version" : "0.3.4" + "revision" : "ae939c146f380ca12d0a04ca1f6b0c4c270fdd5a", + "version" : "0.3.5" } }, { diff --git a/Package.swift b/Package.swift index 0c4b14a..b769a13 100644 --- a/Package.swift +++ b/Package.swift @@ -36,7 +36,7 @@ let package = Package( .package(url: "https://github.com/elementary-swift/elementary.git", from: "0.6.0"), .package(url: "https://github.com/elementary-swift/elementary-htmx.git", from: "0.5.0"), .package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.1.0"), - .package(url: "https://github.com/m-housh/swift-validations.git", from: "0.1.0"), + .package(url: "https://github.com/m-housh/swift-validations.git", from: "0.3.5"), ], targets: [ .executableTarget( @@ -168,6 +168,7 @@ let package = Package( .product(name: "CasePaths", package: "swift-case-paths"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "Fluent", package: "fluent"), + .product(name: "Tagged", package: "swift-tagged"), .product(name: "URLRouting", package: "swift-url-routing"), ] ), diff --git a/Public/css/output.css b/Public/css/output.css index 84b660b..b5abb82 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -6920,6 +6920,12 @@ } } } + .grid-flow-col { + grid-auto-flow: column; + } + .grid-flow-row { + grid-auto-flow: row; + } .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -8753,6 +8759,9 @@ color: var(--color-warning); } } + .text-accent { + color: var(--color-accent); + } .text-base-content { color: var(--color-base-content); } @@ -9732,6 +9741,31 @@ grid-template-columns: repeat(2, minmax(0, 1fr)); } } + .md\:justify-between { + @media (width >= 48rem) { + justify-content: space-between; + } + } + .md\:place-self-center { + @media (width >= 48rem) { + place-self: center; + } + } + .md\:place-self-end { + @media (width >= 48rem) { + place-self: end; + } + } + .md\:place-self-start { + @media (width >= 48rem) { + place-self: start; + } + } + .md\:justify-self-end { + @media (width >= 48rem) { + justify-self: flex-end; + } + } .lg\:drawer-open { @media (width >= 64rem) { @layer daisyui.l1.l2.l3 { diff --git a/Sources/CSVParser/Internal/Room+parsing.swift b/Sources/CSVParser/Internal/Room+parsing.swift index 5610f75..04519ab 100644 --- a/Sources/CSVParser/Internal/Room+parsing.swift +++ b/Sources/CSVParser/Internal/Room+parsing.swift @@ -38,6 +38,11 @@ struct RoomCreateParser: ParserPrinter { ParsePrint { Prefix { $0 != UInt8(ascii: ",") }.map(.string) ",".utf8 + Optionally { + Int.parser() + .map(.memberwise(Room.Level.init(rawValue:))) + } + ",".utf8 Double.parser() ",".utf8 Optionally { diff --git a/Sources/DatabaseClient/Internal/Rooms.swift b/Sources/DatabaseClient/Internal/Rooms.swift index e037a26..38115f7 100644 --- a/Sources/DatabaseClient/Internal/Rooms.swift +++ b/Sources/DatabaseClient/Internal/Rooms.swift @@ -28,6 +28,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { $0.delegatedToName != nil && $0.delegatedToName != "" }) + // Filter out the rest of the rooms that don't delegate their airflow / loads. let initialRooms = rows.filter({ $0.delegatedToName == nil || $0.delegatedToName == "" }) @@ -54,6 +55,7 @@ extension DatabaseClient.Rooms: TestDependencyKey { array.append( Room.Create.init( name: row.name, + level: row.level, heatingLoad: row.heatingLoad, coolingTotal: row.coolingTotal, coolingSensible: row.coolingSensible, @@ -134,6 +136,7 @@ extension Room.CSV.Row { assert(delegatedToName == nil || delegatedToName == "") return .init( name: name, + level: level, heatingLoad: heatingLoad, coolingTotal: coolingTotal, coolingSensible: coolingSensible, @@ -170,6 +173,7 @@ extension Room.Create { return .init( name: name, + level: level?.rawValue, heatingLoad: heatingLoad, coolingLoad: coolingLoad, registerCount: registerCount, @@ -187,6 +191,7 @@ extension Room { try await database.schema(RoomModel.schema) .id() .field("name", .string, .required) + .field("level", .int8) .field("heatingLoad", .double, .required) .field("coolingLoad", .dictionary, .required) .field("registerCount", .int8, .required) @@ -217,6 +222,9 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { @Field(key: "name") var name: String + @Field(key: "level") + var level: Int? + @Field(key: "heatingLoad") var heatingLoad: Double @@ -246,6 +254,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { init( id: UUID? = nil, name: String, + level: Int? = nil, heatingLoad: Double, coolingLoad: Room.CoolingLoad, registerCount: Int, @@ -257,6 +266,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { ) { self.id = id self.name = name + self.level = level self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.registerCount = registerCount @@ -272,6 +282,7 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { id: requireID(), projectID: $project.id, name: name, + level: level.map(Room.Level.init(rawValue:)), heatingLoad: heatingLoad, coolingLoad: coolingLoad, registerCount: registerCount, @@ -287,6 +298,9 @@ final class RoomModel: Model, @unchecked Sendable, Validatable { if let name = updates.name, name != self.name { self.name = name } + if let level = updates.level?.rawValue, level != self.level { + self.level = level + } if let heatingLoad = updates.heatingLoad, heatingLoad != self.heatingLoad { self.heatingLoad = heatingLoad } diff --git a/Sources/ManualDCore/Room.swift b/Sources/ManualDCore/Room.swift index fabc008..63bfecb 100644 --- a/Sources/ManualDCore/Room.swift +++ b/Sources/ManualDCore/Room.swift @@ -1,5 +1,6 @@ import Dependencies import Foundation +import Tagged /// Represents a room in a project. /// @@ -17,6 +18,9 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { /// A unique name for the room in the project. public let name: String + /// The level of the home the room is on. + public let level: Level? + /// The heating load required for the room (from Manual-J). public let heatingLoad: Double @@ -45,6 +49,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { id: UUID, projectID: Project.ID, name: String, + level: Level? = nil, heatingLoad: Double, coolingLoad: CoolingLoad, registerCount: Int = 1, @@ -56,6 +61,7 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { self.id = id self.projectID = projectID self.name = name + self.level = level self.heatingLoad = heatingLoad self.coolingLoad = coolingLoad self.registerCount = registerCount @@ -98,6 +104,11 @@ public struct Room: Codable, Equatable, Identifiable, Sendable { } } + + public enum LevelTag {} + + public typealias Level = Tagged + } extension Room { @@ -106,6 +117,9 @@ extension Room { /// A unique name for the room in the project. public let name: String + /// An optional level of the home the room is on. + public let level: Room.Level? + /// The heating load required for the room (from Manual-J). public let heatingLoad: Double @@ -127,6 +141,7 @@ extension Room { public init( name: String, + level: Room.Level? = nil, heatingLoad: Double, coolingTotal: Double? = nil, coolingSensible: Double? = nil, @@ -134,6 +149,7 @@ extension Room { delegatedTo: Room.ID? = nil ) { self.name = name + self.level = level self.heatingLoad = heatingLoad self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible @@ -160,6 +176,9 @@ extension Room { /// A unique name for the room in the project. public let name: String + /// An optional level of the home the room is on. + public let level: Room.Level? + /// The heating load required for the room (from Manual-J). public let heatingLoad: Double @@ -177,6 +196,7 @@ extension Room { public init( name: String, + level: Room.Level? = nil, heatingLoad: Double, coolingTotal: Double? = nil, coolingSensible: Double? = nil, @@ -184,6 +204,7 @@ extension Room { delegatedToName: String? = nil ) { self.name = name + self.level = level self.heatingLoad = heatingLoad self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible @@ -226,6 +247,10 @@ extension Room { public struct Update: Codable, Equatable, Sendable { /// A unique name for the room in the project. public let name: String? + + /// An optional level of the home the room is on. + public let level: Room.Level? + /// The heating load required for the room (from Manual-J). public let heatingLoad: Double? /// The total cooling load required for the room (from Manual-J). @@ -246,12 +271,14 @@ extension Room { public init( name: String? = nil, + level: Room.Level? = nil, heatingLoad: Double? = nil, coolingTotal: Double? = nil, coolingSensible: Double? = nil, registerCount: Int? = nil ) { self.name = name + self.level = level self.heatingLoad = heatingLoad self.coolingTotal = coolingTotal self.coolingSensible = coolingSensible @@ -263,6 +290,7 @@ extension Room { rectangularSizes: [RectangularSize] ) { self.name = nil + self.level = nil self.heatingLoad = nil self.coolingTotal = nil self.coolingSensible = nil @@ -304,6 +332,16 @@ public struct CoolingLoadError: Error, Equatable, Sendable { } } +extension Room.Level { + /// The label for the level, i.e. 'Basement' or 'Level-1', etc. + public var label: String { + if rawValue <= 0 { + return "Basement" + } + return "Level-\(rawValue)" + } +} + #if DEBUG extension Room { diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 3f3de2b..8d21790 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -216,12 +216,6 @@ extension SiteRoute.View.ProjectRoute { } Method.post Body().map(.memberwise(Room.CSV.init)) - // Body { - // FormData { - // - // } - // .map(.memberwise(Room.CSV.init)) - // } } Route(.case(Self.delete)) { Path { @@ -242,6 +236,12 @@ extension SiteRoute.View.ProjectRoute { Body { FormData { Field("name", .string) + Optionally { + Field("level") { + Int.parser() + } + .map(.memberwise(Room.Level.init(rawValue:))) + } Field("heatingLoad") { Double.parser() } Optionally { Field("coolingTotal") { Double.parser() } @@ -268,6 +268,12 @@ extension SiteRoute.View.ProjectRoute { Optionally { Field("name", .string) } + Optionally { + Field("level") { + Int.parser() + } + .map(.memberwise(Room.Level.init(rawValue:))) + } Optionally { Field("heatingLoad") { Double.parser() } } diff --git a/Sources/Styleguide/ResultView.swift b/Sources/Styleguide/ResultView.swift index ac4ab0a..2e4ad8a 100644 --- a/Sources/Styleguide/ResultView.swift +++ b/Sources/Styleguide/ResultView.swift @@ -1,5 +1,6 @@ import Elementary import Foundation +import Validations public struct ResultView: HTML where ValueView: HTML, ErrorView: HTML { @@ -69,7 +70,11 @@ public struct ErrorView: HTML, Sendable { div { h1(.class("text-xl font-bold text-error")) { "Oops: Error" } p { - "\(error.localizedDescription)" + if let validationError = (error as? ValidationError) { + "\(validationError.debugDescription)" + } else { + "\(error.localizedDescription)" + } } } } diff --git a/Sources/ViewController/Views/Home.swift b/Sources/ViewController/Views/Home.swift index 42302e1..bf4462a 100644 --- a/Sources/ViewController/Views/Home.swift +++ b/Sources/ViewController/Views/Home.swift @@ -49,7 +49,7 @@ struct HomeView: HTML, Sendable { header a( .class("btn btn-ghost text-md text-primary font-bold italic"), - .href("https://git.housh.dev/michael/swift-duct-calc"), + .href("https://github.com/m-housh/swift-duct-calc"), .target(.blank) ) { "Open source residential duct design program" diff --git a/Sources/ViewController/Views/MainPage.swift b/Sources/ViewController/Views/MainPage.swift index fac4639..fba6133 100644 --- a/Sources/ViewController/Views/MainPage.swift +++ b/Sources/ViewController/Views/MainPage.swift @@ -94,22 +94,36 @@ public struct MainPage: SendableHTMLDocument where Inner: Sendable footer( .class( """ - footer sm:footer-horizontal footer-center + footer footer-horizontal footer-center bg-base-300 text-base-content p-4 """ ) ) { - aside { - p { - "Copyright © \(Date().description.prefix(4)) - All rights reserved by Michael Housh" + aside( + .class("grid-flow-row items-center") + ) { + + div(.class("flex mx-auto")) { + a( + .class("btn btn-ghost"), + .href("mailto:support@ductcalc.pro") + ) { + SVG(.email) + span { "support@ductcalc.pro" } + } } + a( - .class("btn btn-ghost"), - .href("https://git.housh.dev/michael/swift-duct-calc/src/branch/main/LICENSE"), + .class("btn btn-ghost mx-auto"), + .href("https://github.com/m-housh/swift-duct-calc/src/branch/main/LICENSE"), .target(.blank) ) { "Openly licensed via CC-BY-NC-SA 4.0" } + + p(.class("")) { + "Copyright © \(Date().description.prefix(4)) - All rights reserved by Michael Housh" + } } } } diff --git a/Sources/ViewController/Views/Navbar.swift b/Sources/ViewController/Views/Navbar.swift index 8257087..636a5f5 100644 --- a/Sources/ViewController/Views/Navbar.swift +++ b/Sources/ViewController/Views/Navbar.swift @@ -34,12 +34,12 @@ struct Navbar: HTML, Sendable { label( .for("my-drawer-1"), .class("size-7"), - .init(name: "aria-label", value: "open sidebar") + .init(name: "aria-label", value: "open / close sidebar") ) { SVG(.sidebarToggle) } .navButton() - .tooltip("Open sidebar", position: .right) + .tooltip("Open / close sidebar", position: .right) } a( diff --git a/Sources/ViewController/Views/Rooms/RoomForm.swift b/Sources/ViewController/Views/Rooms/RoomForm.swift index c63c156..2ac8b01 100644 --- a/Sources/ViewController/Views/Rooms/RoomForm.swift +++ b/Sources/ViewController/Views/Rooms/RoomForm.swift @@ -68,6 +68,21 @@ struct RoomForm: HTML, Sendable { .value(room?.name) ) + LabeledInput( + "Level", + .name("level"), + .type(.number), + .placeholder("1 (Optional)"), + .value(room?.level?.rawValue), + .min("-1"), + .step("1") + ) + div(.class("text-sm italic -mt-2")) { + span(.class("text-primary")) { + "Use -1 or 0 for a basement" + } + } + LabeledInput( "Heating Load", .name("heatingLoad"), @@ -78,8 +93,6 @@ struct RoomForm: HTML, Sendable { .value(room?.heatingLoad) ) - // TODO: Add description that only one is required (cooling total or sensible) - LabeledInput( "Cooling Total", .name("coolingTotal"), @@ -97,6 +110,14 @@ struct RoomForm: HTML, Sendable { .min("0"), .value(room?.coolingLoad.sensible) ) + div(.class("text-primary text-sm italic -mt-2")) { + p { + "Should enter at least one of the cooling loads." + } + p { + "Both are also acceptable." + } + } LabeledInput( "Registers", diff --git a/Sources/ViewController/Views/Rooms/RoomsView.swift b/Sources/ViewController/Views/Rooms/RoomsView.swift index 4d94f5f..394ef14 100644 --- a/Sources/ViewController/Views/Rooms/RoomsView.swift +++ b/Sources/ViewController/Views/Rooms/RoomsView.swift @@ -15,6 +15,14 @@ struct RoomsView: HTML, Sendable { .appendingPath("csv") } + // Sort the rooms based on level, they should already be sorted by name, + // so this puts lower level rooms towards the top in alphabetical order. + // + // If rooms do not have a level we shove those all the way to the bottom. + private var sortedRooms: [Room] { + rooms.sorted { ($0.level?.rawValue ?? 20) < ($1.level?.rawValue ?? 20) } + } + var body: some HTML { div(.class("flex w-full flex-col")) { PageTitleRow { @@ -133,7 +141,7 @@ struct RoomsView: HTML, Sendable { } } tbody { - for room in rooms { + for room in sortedRooms { RoomRow(room: room, shr: sensibleHeatRatio, rooms: rooms) } } @@ -166,7 +174,13 @@ struct RoomsView: HTML, Sendable { public var body: some HTML { tr(.id("roomRow_\(room.id.idString)")) { - td { room.name } + td { + if let level = room.level { + "\(level.label) - \(room.name)" + } else { + room.name + } + } td { div(.class("flex justify-center")) { Number(room.heatingLoad, digits: 0) diff --git a/Sources/ViewController/Views/User/LoginForm.swift b/Sources/ViewController/Views/User/LoginForm.swift index 68c6c33..11ed2ce 100644 --- a/Sources/ViewController/Views/User/LoginForm.swift +++ b/Sources/ViewController/Views/User/LoginForm.swift @@ -1,5 +1,6 @@ import Elementary import ElementaryHTMX +import ManualDCore import Styleguide struct LoginForm: HTML, Sendable { @@ -12,6 +13,13 @@ struct LoginForm: HTML, Sendable { self.next = next } + private var route: SiteRoute.View { + if style == .login { + return .login(.index(next: next)) + } + return .signup(.index) + } + var body: some HTML { ModalForm(id: "loginForm", closeButton: false, dismiss: false) { h1(.class("text-2xl font-bold mb-6")) { style.title } diff --git a/Sources/ViewController/Views/User/UserProfileForm.swift b/Sources/ViewController/Views/User/UserProfileForm.swift index 7818076..f45ff45 100644 --- a/Sources/ViewController/Views/User/UserProfileForm.swift +++ b/Sources/ViewController/Views/User/UserProfileForm.swift @@ -150,6 +150,10 @@ struct UserProfileForm: HTML, Sendable { .attributes(.class("btn-block")) } + .attributes( + .hx.pushURL("/projects"), + when: signup == true + ) } } } diff --git a/TODO.md b/TODO.md index 0b499bf..0f777b7 100644 --- a/TODO.md +++ b/TODO.md @@ -3,8 +3,8 @@ - [x] Fix theme not working when selected upon signup. - [x] Pdf generation - [x] Add postgres / mysql support -- [x] Opensource / license ?? -- [x] Figure out domain to host (currently thinking ductcalc.pro) +- [x] Opensource / license +- [x] Figure out domain to host - [x] Add ability for either sensible or total load while specifying a room load. - CoolCalc current version specifies the sensible cooling for a room break down, and currently we require the total load and calculate sensible based on project @@ -14,5 +14,9 @@ - They will overlap each other making it difficult to read / decipher which checkbox belongs to which label. - [x] Add select all rooms for trunks, useful for sizing main supply or return trunks. -- [ ] Add optional level to room model. +- [x] Add optional level to room model. - [ ] Add way to sponsor the project. +- [x] Need to push url after signup. +- [x] Update urls to point to public mirror of repository +- [x] Add email to footer +- [x] Validation errors, if they occur are vague in ResultView diff --git a/Tests/CSVParsingTests/CSVParsingTests.swift b/Tests/CSVParsingTests/CSVParsingTests.swift index 4f48c49..dc0e07e 100644 --- a/Tests/CSVParsingTests/CSVParsingTests.swift +++ b/Tests/CSVParsingTests/CSVParsingTests.swift @@ -11,12 +11,15 @@ struct CSVParsingTests { let parser = CSVParser.liveValue let input = """ - Name,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To - Bed-1,12345,12345,,2, - Bed-2,1223,,1123,1, + Name,Level,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To + Bed-1,2,12345,2345,2345,2, + Entry,1,3456,1234,990,1, + Kitchen,1,6789,3456,,2, + Bath-1,1,890,,345,0,Kitchen """ let rooms = try await parser.parseRooms(.init(file: Data(input.utf8))) - #expect(rooms.count == 2) + #expect(rooms.count == 4) + #expect(rooms.first!.level == 2) } } diff --git a/Tests/DatabaseClientTests/Resources/rooms.csv b/Tests/DatabaseClientTests/Resources/rooms.csv index 9b12eaf..e2517a3 100644 --- a/Tests/DatabaseClientTests/Resources/rooms.csv +++ b/Tests/DatabaseClientTests/Resources/rooms.csv @@ -1,5 +1,5 @@ -Name,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To -Bed-1,2345,1234,1321,1, -Entry,3456,2345,1234,1, -Kitchen,7654,3456,2453,2, -Bath-1,890,345,,0,Kitchen +Name,Level,Heating Load,Cooling Total,Cooling Sensible,Register Count,Delegated To +Bed-1,2,2345,1234,1321,1, +Entry,1,3456,2345,1234,1, +Kitchen,1,7654,3456,2453,2, +Bath-1,1,890,345,,0,Kitchen diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/ductulator.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/ductulator.1.html index 1890195..ef7a48f 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/ductulator.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/ductulator.1.html @@ -69,11 +69,25 @@ p-6 w-full">
-
-
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/home.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/home.1.html index fdba3c8..39834e9 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/home.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/home.1.html @@ -53,7 +53,7 @@ text-8xl font-bold my-auto space-2"> - Open source residential duct design program + Open source residential duct design program

Manual-D™ speed sheet, but on the web!

@@ -100,11 +100,25 @@ text-8xl font-bold my-auto space-2">

-
-
diff --git a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html index e846e3d..e8338ad 100644 --- a/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html +++ b/Tests/ViewControllerTests/__Snapshots__/ViewControllerTests/projectDetail.1.html @@ -36,7 +36,7 @@