From 1aeb6144d5bddf538dc0c76c98c7bd6d0aea2f3d Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Sat, 3 Jan 2026 16:24:53 -0500 Subject: [PATCH] WIP: Changes main page to not include sidebar, that moves to project view. --- Sources/DatabaseClient/Rooms.swift | 10 + Sources/ManualDCore/Routes/ViewRoute.swift | 13 +- Sources/Styleguide/Date.swift | 20 ++ Sources/Styleguide/ElementaryExtensions.swift | 7 + .../Extensions/DatabaseExtensions.swift | 28 +++ Sources/ViewController/Interface.swift | 30 ++- Sources/ViewController/Live.swift | 186 +++++++++--------- Sources/ViewController/Views/MainPage.swift | 24 +-- .../Views/Project/ProjectDetail.swift | 59 ++++++ .../Views/Project/ProjectForm.swift | 6 +- .../Views/Project/ProjectView.swift | 126 ++++++++---- .../Views/Project/ProjectsTable.swift | 16 +- Sources/ViewController/Views/Sidebar.swift | 86 -------- justfile | 2 +- output.css | 6 + 15 files changed, 383 insertions(+), 236 deletions(-) create mode 100644 Sources/Styleguide/Date.swift create mode 100644 Sources/ViewController/Extensions/DatabaseExtensions.swift create mode 100644 Sources/ViewController/Views/Project/ProjectDetail.swift delete mode 100644 Sources/ViewController/Views/Sidebar.swift diff --git a/Sources/DatabaseClient/Rooms.swift b/Sources/DatabaseClient/Rooms.swift index 3b3b5b1..65d550f 100644 --- a/Sources/DatabaseClient/Rooms.swift +++ b/Sources/DatabaseClient/Rooms.swift @@ -10,6 +10,7 @@ extension DatabaseClient { public var create: @Sendable (Room.Create) async throws -> Room public var delete: @Sendable (Room.ID) async throws -> Void public var get: @Sendable (Room.ID) async throws -> Room? + public var fetch: @Sendable (Project.ID) async throws -> [Room] } } @@ -33,6 +34,13 @@ extension DatabaseClient.Rooms { }, get: { id in try await RoomModel.find(id, on: database).map { try $0.toDTO() } + }, + fetch: { projectID in + try await RoomModel.query(on: database) + .with(\.$project) + .filter(\.$project.$id, .equal, projectID) + .all() + .map { try $0.toDTO() } } ) } @@ -83,6 +91,8 @@ extension Room { .field("coolingTotal", .double, .required) .field("coolingSensible", .double, .required) .field("registerCount", .int8, .required) + .field("createdAt", .datetime) + .field("updatedAt", .datetime) .field("projectID", .uuid, .required, .references(ProjectModel.schema, "id")) .unique(on: "projectID", "name") .create() diff --git a/Sources/ManualDCore/Routes/ViewRoute.swift b/Sources/ManualDCore/Routes/ViewRoute.swift index 01c25ac..b42752a 100644 --- a/Sources/ManualDCore/Routes/ViewRoute.swift +++ b/Sources/ManualDCore/Routes/ViewRoute.swift @@ -44,6 +44,7 @@ extension SiteRoute { extension SiteRoute.View { public enum ProjectRoute: Equatable, Sendable { case create(Project.Create) + case detail(Project.ID) case form(dismiss: Bool = false) case index case page(page: Int = 1, limit: Int = 25) @@ -65,6 +66,13 @@ extension SiteRoute.View { .map(.memberwise(Project.Create.init)) } } + Route(.case(Self.detail)) { + Path { + rootPath + Project.ID.parser() + } + Method.get + } Route(.case(Self.form)) { Path { rootPath @@ -97,7 +105,7 @@ extension SiteRoute.View { extension SiteRoute.View { public enum RoomRoute: Equatable, Sendable { case form(dismiss: Bool = false) - case index + case index(Project.ID) static let rootPath = "rooms" @@ -115,6 +123,9 @@ extension SiteRoute.View { Route(.case(Self.index)) { Path { rootPath } Method.get + Query { + Field("projectID") { Project.ID.parser() } + } } } } diff --git a/Sources/Styleguide/Date.swift b/Sources/Styleguide/Date.swift new file mode 100644 index 0000000..e679e4e --- /dev/null +++ b/Sources/Styleguide/Date.swift @@ -0,0 +1,20 @@ +import Elementary +import Foundation + +public struct DateView: HTML, Sendable { + let date: Date + + var formatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + } + + public init(_ date: Date) { + self.date = date + } + + public var body: some HTML { + span { formatter.string(from: date) } + } +} diff --git a/Sources/Styleguide/ElementaryExtensions.swift b/Sources/Styleguide/ElementaryExtensions.swift index b5672c4..65ddc38 100644 --- a/Sources/Styleguide/ElementaryExtensions.swift +++ b/Sources/Styleguide/ElementaryExtensions.swift @@ -7,3 +7,10 @@ extension HTMLAttribute where Tag: HTMLTrait.Attributes.href { href(SiteRoute.View.router.path(for: route)) } } + +extension HTMLAttribute where Tag == HTMLTag.form { + + public static func action(route: SiteRoute.View) -> Self { + action(SiteRoute.View.router.path(for: route)) + } +} diff --git a/Sources/ViewController/Extensions/DatabaseExtensions.swift b/Sources/ViewController/Extensions/DatabaseExtensions.swift new file mode 100644 index 0000000..0f58e3a --- /dev/null +++ b/Sources/ViewController/Extensions/DatabaseExtensions.swift @@ -0,0 +1,28 @@ +import DatabaseClient +import Fluent +import ManualDCore +import Vapor + +extension DatabaseClient.Projects { + + func fetchPage( + userID: User.ID, + page: Int = 1, + limit: Int = 25 + ) async throws -> Page { + try await fetch(userID, .init(page: page, per: limit)) + } + + func fetchPage( + userID: User.ID, + page: PageRequest + ) async throws -> Page { + try await fetch(userID, page) + } +} + +extension PageRequest { + static func next(_ currentPage: Page) -> Self { + .init(page: currentPage.metadata.page + 1, per: currentPage.metadata.per) + } +} diff --git a/Sources/ViewController/Interface.swift b/Sources/ViewController/Interface.swift index ae974d7..18d41cb 100644 --- a/Sources/ViewController/Interface.swift +++ b/Sources/ViewController/Interface.swift @@ -46,9 +46,6 @@ extension ViewController { self.currentUser = currentUser } - func authenticate(_ user: User) { - self.authenticateUser(user) - } } } @@ -62,3 +59,30 @@ extension ViewController: DependencyKey { } ) } + +extension ViewController.Request { + + func authenticate( + _ login: User.Login + ) async throws -> User { + @Dependency(\.database.users) var users + let token = try await users.login(login) + let user = try await users.get(token.userID)! + authenticateUser(user) + logger.debug("Logged in user: \(user.id)") + return user + } + + func createAndAuthenticate( + _ signup: User.Create + ) async throws -> User { + @Dependency(\.database.users) var users + let user = try await users.create(signup) + let _ = try await users.login( + .init(email: signup.email, password: signup.password) + ) + authenticateUser(user) + logger.debug("Created and logged in user: \(user.id)") + return user + } +} diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index 2fb408b..ea21176 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -14,24 +14,34 @@ extension ViewController.Request { case .login(let route): switch route { case .index: - return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { + return view { LoginForm() } case .submit(let login): - let token = try await database.users.login(login) - let user = try await database.users.get(token.userID)! - authenticate(user) + let user = try await authenticate(login) let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25)) - return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { + return view { ProjectsTable(userID: user.id, projects: projects) } } case .signup(let route): - return try await route.renderView(isHtmxRequest: isHtmxRequest) + switch route { + case .index: + return view { + LoginForm(style: .signup) + } + case .submit(let request): + // Create a new user and log them in. + let user = try await createAndAuthenticate(request) + let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25)) + return view { + ProjectsTable(userID: user.id, projects: projects) + } + } case .project(let route): - return try await route.renderView(isHtmxRequest: isHtmxRequest) + return try await route.renderView(on: self) case .room(let route): - return try await route.renderView(isHtmxRequest: isHtmxRequest) + return try await route.renderView(on: self) case .frictionRate(let route): return try await route.renderView(isHtmxRequest: isHtmxRequest) case .effectiveLength(let route): @@ -40,54 +50,84 @@ extension ViewController.Request { // return try await route.renderView(isHtmxRequest: isHtmxRequest) default: // FIX: FIX - return try await _render(isHtmxRequest: false) { + return _render(isHtmxRequest: false) { div { "Fix me!" } } } } + + func view( + @HTMLBuilder inner: () -> C + ) -> AnySendableHTML where C: Sendable { + _render(isHtmxRequest: isHtmxRequest, showSidebar: showSidebar) { + inner() + } + } + + var showSidebar: Bool { + switch route { + case .login, .signup, .project(.page): + return false + default: + return true + } + } } extension SiteRoute.View.ProjectRoute { - private var shouldShowSidebar: Bool { + func renderView(on request: ViewController.Request) async throws -> AnySendableHTML { + @Dependency(\.database) var database + let user = try request.currentUser() + switch self { - case .index, .page: return false - default: return true - } - } - - func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { - @Dependency(\.database.projects) var projects - - return try await _render( - isHtmxRequest: isHtmxRequest, - showSidebar: shouldShowSidebar - ) { - switch self { - case .index: - // ProjectView(project: .mock) - let page = try await projects.fetch(UUID(0), .init(page: 1, per: 25)) - ProjectsTable(userID: UUID(0), projects: page) - case .page(let page, let limit): - let page = try await projects.fetch(UUID(0), .init(page: page, per: limit)) - ProjectsTable.Rows(projects: page) - case .form(let dismiss): - ProjectForm(dismiss: dismiss) - case .create: - div { "Fix me!" } + case .index: + let projects = try await database.projects.fetchPage(userID: user.id) + return request.view { + ProjectsTable(userID: user.id, projects: projects) } + case .page(let page, let limit): + let projects = try await database.projects.fetchPage( + userID: user.id, page: page, limit: limit) + return ProjectsTable(userID: user.id, projects: projects) + + case .form(let dismiss): + return ProjectForm(dismiss: dismiss) + + case .create(let form): + let project = try await database.projects.create(user.id, form) + return request.view { + ProjectView(projectID: project.id, activeTab: .projects) { + ProjectDetail(project: project) + } + } + + case .detail(let projectID): + let project = try await database.projects.get(projectID)! + return request.view { + ProjectView(projectID: projectID, activeTab: .projects) { + ProjectDetail(project: project) + } + } + } + } } extension SiteRoute.View.RoomRoute { - func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { + func renderView(on request: ViewController.Request) async throws -> AnySendableHTML { + @Dependency(\.database) var database + switch self { case .form(let dismiss): return RoomForm(dismiss: dismiss) - case .index: - return try await _render(isHtmxRequest: isHtmxRequest, active: .rooms) { - RoomsView(rooms: Room.mocks) + case .index(let projectID): + let rooms = try await database.rooms.fetch(projectID) + return request.view { + ProjectView(projectID: projectID, activeTab: .rooms) { + RoomsView(rooms: rooms) + } } } } @@ -97,7 +137,7 @@ extension SiteRoute.View.FrictionRateRoute { func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { switch self { case .index: - return try await _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) { + return _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) { FrictionRateView() } case .form(let type, let dismiss): @@ -128,7 +168,7 @@ extension SiteRoute.View.EffectiveLengthRoute { func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { switch self { case .index: - return try await _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) { + return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) { EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks) } case .form(let dismiss): @@ -145,52 +185,6 @@ extension SiteRoute.View.EffectiveLengthRoute { } } -extension SiteRoute.View.SignupRoute { - - func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML { - @Dependency(\.database.users) var users - - switch self { - case .index: - return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { - LoginForm(style: .signup) - } - case .submit(let request): - _ = try await users.create(request) - // FIX: We should just login the new user at this point. - return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) { - LoginForm() - } - - // default: - // return div { "Fix Me!" } - } - } -} - -// extension SiteRoute.View.LoginRoute { -// func renderView(on req: ViewController.Request) async throws -> AnySendableHTML { -// -// @Dependency(\.database) var database -// -// return try await _render(isHtmxRequest: req.isHtmxRequest, showSidebar: false) { -// switch self { -// case .index: -// LoginForm() -// case .submit(let login): -// // FIX: -// // div { "Logged in Success! Fix me!" } -// let token = try await database.users.login(login) -// let user = try await database.users.get(token.userID)! -// _ = req.authenticate(user) -// // req.authenticate(user) -// let page = try await database.projects.fetch(user.id, .init(page: 1, per: 25)) -// ProjectsTable(userID: user.id, projects: page) -// } -// } -// } -// } - private func _render( isHtmxRequest: Bool, active activeTab: Sidebar.ActiveTab = .projects, @@ -201,10 +195,18 @@ private func _render( if isHtmxRequest { return inner } - return MainPage( - active: activeTab, - showSidebar: showSidebar - ) { - inner - } + return MainPage { inner } +} + +private func _render( + isHtmxRequest: Bool, + active activeTab: Sidebar.ActiveTab = .projects, + showSidebar: Bool = true, + @HTMLBuilder inner: () -> C +) -> AnySendableHTML where C: Sendable { + let inner = inner() + if isHtmxRequest { + return inner + } + return MainPage { inner } } diff --git a/Sources/ViewController/Views/MainPage.swift b/Sources/ViewController/Views/MainPage.swift index a557305..5b14b48 100644 --- a/Sources/ViewController/Views/MainPage.swift +++ b/Sources/ViewController/Views/MainPage.swift @@ -1,19 +1,21 @@ import Elementary public struct MainPage: SendableHTMLDocument where Inner: Sendable { + public var title: String { "Manual-D" } public var lang: String { "en" } + let inner: Inner - let activeTab: Sidebar.ActiveTab - let showSidebar: Bool + // let activeTab: Sidebar.ActiveTab + // let showSidebar: Bool init( - active activeTab: Sidebar.ActiveTab, - showSidebar: Bool = true, + // active activeTab: Sidebar.ActiveTab, + // showSidebar: Bool = true, _ inner: () -> Inner ) { - self.activeTab = activeTab - self.showSidebar = showSidebar + // self.activeTab = activeTab + // self.showSidebar = showSidebar self.inner = inner() } @@ -27,16 +29,8 @@ public struct MainPage: SendableHTMLDocument where Inner: Sendable } public var body: some HTML { - // div(.class("bg-white dark:bg-gray-800 dark:text-white")) { div { - div(.class("flex flex-row")) { - if showSidebar { - Sidebar(active: activeTab) - } - main(.class("flex flex-col h-screen w-full px-6 py-10")) { - inner - } - } + inner } script(.src("https://unpkg.com/lucide@latest")) {} script { diff --git a/Sources/ViewController/Views/Project/ProjectDetail.swift b/Sources/ViewController/Views/Project/ProjectDetail.swift new file mode 100644 index 0000000..b116a2e --- /dev/null +++ b/Sources/ViewController/Views/Project/ProjectDetail.swift @@ -0,0 +1,59 @@ +import Elementary +import ElementaryHTMX +import ManualDCore +import Styleguide + +struct ProjectDetail: HTML, Sendable { + let project: Project + + var body: some HTML { + div( + .class( + """ + border border-gray-200 rounded-lg shadow-lg space-y-4 p-4 m-4 + """ + ) + ) { + Row { + h1(.class("text-2xl font-bold")) { "Project" } + EditButton() + .attributes( + .hx.get(route: .project(.form(dismiss: false))), + .hx.target("#projectForm"), + .hx.swap(.outerHTML) + ) + } + + Row { + Label("Name") + span { project.name } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label("Address") + span { project.streetAddress } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label("City") + span { project.city } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label("State") + span { project.state } + } + .attributes(.class("border-b border-gray-200")) + + Row { + Label("Zip") + span { project.zipCode } + } + } + + div(.id("projectForm")) {} + } +} diff --git a/Sources/ViewController/Views/Project/ProjectForm.swift b/Sources/ViewController/Views/Project/ProjectForm.swift index ab92e36..a5c656e 100644 --- a/Sources/ViewController/Views/Project/ProjectForm.swift +++ b/Sources/ViewController/Views/Project/ProjectForm.swift @@ -19,7 +19,11 @@ struct ProjectForm: HTML, Sendable { var body: some HTML { ModalForm(id: "projectForm", dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } - form(.class("space-y-4 p-4")) { + form( + .class("space-y-4 p-4"), + .method(.post), + .action(route: .project(.index)) + ) { div { label(.for("name")) { "Name" } Input(id: "name", placeholder: "Name") diff --git a/Sources/ViewController/Views/Project/ProjectView.swift b/Sources/ViewController/Views/Project/ProjectView.swift index c5ae8df..c99fca3 100644 --- a/Sources/ViewController/Views/Project/ProjectView.swift +++ b/Sources/ViewController/Views/Project/ProjectView.swift @@ -3,57 +3,113 @@ import ElementaryHTMX import ManualDCore import Styleguide -struct ProjectView: HTML, Sendable { - let project: Project +struct ProjectView: HTML, Sendable where Inner: Sendable { + let projectID: Project.ID + let activeTab: Sidebar.ActiveTab + let inner: Inner + + init( + projectID: Project.ID, + activeTab: Sidebar.ActiveTab, + @HTMLBuilder inner: () -> Inner + ) { + self.projectID = projectID + self.activeTab = activeTab + self.inner = inner() + } var body: some HTML { - div( + div { + div(.class("flex flex-row")) { + Sidebar(active: activeTab, projectID: projectID) + main(.class("flex flex-col h-screen w-full px-6 py-10")) { + inner + } + } + } + } +} + +// TODO: Update to use DaisyUI drawer. +struct Sidebar: HTML { + + let active: ActiveTab + let projectID: Project.ID + + var body: some HTML { + aside( .class( """ - border border-gray-200 rounded-lg shadow-lg space-y-4 p-4 m-4 + h-screen sticky top-0 max-w-[280px] flex-none + border-r-2 border-gray-200 + shadow-lg """ ) ) { - Row { - h1(.class("text-2xl font-bold")) { "Project" } - EditButton() - .attributes( - .hx.get(route: .project(.form(dismiss: false))), - .hx.target("#projectForm"), - .hx.swap(.outerHTML) - ) - } + // TODO: Move somewhere outside of the sidebar. Row { - Label("Name") - span { project.name } + Label("Theme") + input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) } - .attributes(.class("border-b border-gray-200")) + .attributes(.class("p-4")) - Row { - Label("Address") - span { project.streetAddress } - } - .attributes(.class("border-b border-gray-200")) + row(title: "Project", icon: .mapPin, route: .project(.index)) + .attributes(.data("active", value: active == .projects ? "true" : "false")) - Row { - Label("City") - span { project.city } - } - .attributes(.class("border-b border-gray-200")) + row(title: "Rooms", icon: .doorClosed, route: .room(.index(projectID))) + .attributes(.data("active", value: active == .rooms ? "true" : "false")) - Row { - Label("State") - span { project.state } - } - .attributes(.class("border-b border-gray-200")) + row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index)) + .attributes(.data("active", value: active == .effectiveLength ? "true" : "false")) - Row { - Label("Zip") - span { project.zipCode } + row(title: "Friction Rate", icon: .squareFunction, route: .frictionRate(.index)) + .attributes(.data("active", value: active == .frictionRate ? "true" : "false")) + + row(title: "Duct Sizes", icon: .wind, href: "#") + .attributes(.data("active", value: active == .ductSizing ? "true" : "false")) + } + } + + // TODO: Use SiteRoute.View routes as href. + private func row( + title: String, + icon: Icon.Key, + href: String + ) -> some HTML { + a( + .class( + """ + flex w-full items-center gap-4 + hover:bg-gray-300 hover:text-gray-800 + data-[active=true]:bg-gray-300 data-[active=true]:text-gray-800 + px-4 py-2 + """ + ), + .href(href) + ) { + Icon(icon) + span(.class("text-xl")) { + title } } + } - div(.id("projectForm")) {} + private func row( + title: String, + icon: Icon.Key, + route: SiteRoute.View + ) -> some HTML { + row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route)) + } +} + +extension Sidebar { + enum ActiveTab: Equatable, Sendable { + case projects + case rooms + case effectiveLength + case frictionRate + case ductSizing } } diff --git a/Sources/ViewController/Views/Project/ProjectsTable.swift b/Sources/ViewController/Views/Project/ProjectsTable.swift index 2f7c7c5..ceb4467 100644 --- a/Sources/ViewController/Views/Project/ProjectsTable.swift +++ b/Sources/ViewController/Views/Project/ProjectsTable.swift @@ -24,7 +24,10 @@ struct ProjectsTable: HTML, Sendable { .data("tip", value: "Add project") ) { button( - .class("btn btn-primary w-[40px] text-2xl") + .class("btn btn-primary w-[40px] text-2xl"), + .hx.get(route: .project(.form(dismiss: false))), + .hx.target("#projectForm"), + .hx.swap(.outerHTML) ) { "+" } @@ -39,6 +42,7 @@ struct ProjectsTable: HTML, Sendable { th { Label("Date") } th { Label("Name") } th { Label("Address") } + th {} } } tbody { @@ -46,6 +50,8 @@ struct ProjectsTable: HTML, Sendable { } } } + + ProjectForm(dismiss: true) } } } @@ -57,9 +63,15 @@ extension ProjectsTable { var body: some HTML { for project in projects.items { tr(.id("\(project.id)")) { - td { "\(project.createdAt)" } + td { DateView(project.createdAt) } td { "\(project.name)" } td { "\(project.streetAddress)" } + td { + a( + .class("btn btn-success"), + .href(route: .project(.detail(project.id))) + ) { ">" } + } } } // Have a row that when revealed fetches the next page, diff --git a/Sources/ViewController/Views/Sidebar.swift b/Sources/ViewController/Views/Sidebar.swift deleted file mode 100644 index 009df9e..0000000 --- a/Sources/ViewController/Views/Sidebar.swift +++ /dev/null @@ -1,86 +0,0 @@ -import Elementary -import ManualDCore -import Styleguide - -// TODO: Update to use DaisyUI drawer. -struct Sidebar: HTML { - - let active: ActiveTab - - var body: some HTML { - aside( - .class( - """ - h-screen sticky top-0 max-w-[280px] flex-none - border-r-2 border-gray-200 - shadow-lg - """ - ) - ) { - - // TODO: Move somewhere outside of the sidebar. - Row { - Label("Theme") - input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) - } - .attributes(.class("p-4")) - - row(title: "Project", icon: .mapPin, route: .project(.index)) - .attributes(.data("active", value: active == .projects ? "true" : "false")) - - row(title: "Rooms", icon: .doorClosed, route: .room(.index)) - .attributes(.data("active", value: active == .rooms ? "true" : "false")) - - row(title: "Equivalent Lengths", icon: .rulerDimensionLine, route: .effectiveLength(.index)) - .attributes(.data("active", value: active == .effectiveLength ? "true" : "false")) - - row(title: "Friction Rate", icon: .squareFunction, route: .frictionRate(.index)) - .attributes(.data("active", value: active == .frictionRate ? "true" : "false")) - - row(title: "Duct Sizes", icon: .wind, href: "#") - .attributes(.data("active", value: active == .ductSizing ? "true" : "false")) - } - } - - // TODO: Use SiteRoute.View routes as href. - private func row( - title: String, - icon: Icon.Key, - href: String - ) -> some HTML { - a( - .class( - """ - flex w-full items-center gap-4 - hover:bg-gray-300 hover:text-gray-800 - data-[active=true]:bg-gray-300 data-[active=true]:text-gray-800 - px-4 py-2 - """ - ), - .href(href) - ) { - Icon(icon) - span(.class("text-xl")) { - title - } - } - } - - private func row( - title: String, - icon: Icon.Key, - route: SiteRoute.View - ) -> some HTML { - row(title: title, icon: icon, href: SiteRoute.View.router.path(for: route)) - } -} - -extension Sidebar { - enum ActiveTab: Equatable, Sendable { - case projects - case rooms - case effectiveLength - case frictionRate - case ductSizing - } -} diff --git a/justfile b/justfile index 9aa3ba3..83f16a1 100644 --- a/justfile +++ b/justfile @@ -7,7 +7,7 @@ run-css: @./tailwindcss -i input.css -o output.css --watch run: - @SWIFT_BACTRACE=enable=no swift run App + @swift run App serve --log debug build-docker: @podman build -f docker/Dockerfile.dev -t {{docker_image}}:dev . diff --git a/output.css b/output.css index ca7347c..3e360f0 100644 --- a/output.css +++ b/output.css @@ -1325,6 +1325,12 @@ --btn-fg: var(--color-secondary-content); } } + .btn-success { + @layer daisyui.l1.l2.l3 { + --btn-color: var(--color-success); + --btn-fg: var(--color-success-content); + } + } .invalid\:border-red-500 { &:invalid { border-color: var(--color-red-500);