feat: Adds quick calculation views, need to add buttons / links in navbar / home page.

This commit is contained in:
2026-02-09 15:34:28 -05:00
parent 88af6f722e
commit 007d13be2f
23 changed files with 584 additions and 490 deletions

View File

@@ -14,7 +14,7 @@ private let viewRouteMiddleware: [any Middleware] = [
extension SiteRoute.View {
var middleware: [any Middleware]? {
switch self {
case .home, .login, .signup, .test:
case .home, .login, .signup, .test, .quickCalc:
return nil
case .project, .user:
return viewRouteMiddleware

View File

@@ -12,6 +12,7 @@ extension SiteRoute {
case login(LoginRoute)
case signup(SignupRoute)
case project(ProjectRoute)
case quickCalc(QuickCalcRoute)
case user(UserRoute)
//FIX: Remove.
case test
@@ -33,6 +34,9 @@ extension SiteRoute {
Route(.case(Self.project)) {
SiteRoute.View.ProjectRoute.router
}
Route(.case(Self.quickCalc)) {
SiteRoute.View.QuickCalcRoute.router
}
Route(.case(Self.user)) {
SiteRoute.View.UserRoute.router
}
@@ -986,6 +990,49 @@ extension SiteRoute.View.UserRoute {
}
}
extension SiteRoute.View {
public enum QuickCalcRoute: Equatable, Sendable {
case index
case submit(Form)
public static let rootPath = "duct-size"
static let router = OneOf {
Route(.case(Self.index)) {
Path { rootPath }
Method.get
}
Route(.case(Self.submit)) {
Path { rootPath }
Method.post
Body {
FormData {
Field("cfm") { Int.parser() }
Field("frictionRate") { Double.parser() }
Optionally {
Field("height") { Int.parser() }
}
}
.map(.memberwise(Form.init))
}
}
}
public struct Form: Equatable, Sendable {
public let cfm: Int
public let frictionRate: Double
public let height: Int?
public init(cfm: Int, frictionRate: Double, height: Int? = nil) {
self.cfm = cfm
self.frictionRate = frictionRate
self.height = height
}
}
}
}
extension PageRequest: @retroactive Equatable {
public static func == (lhs: FluentKit.PageRequest, rhs: FluentKit.PageRequest) -> Bool {
lhs.page == rhs.page && lhs.per == rhs.per

View File

@@ -17,6 +17,7 @@ extension SVG {
public enum Key: Sendable {
case badgeCheck
case ban
case calculator
case chevronDown
case chevronRight
case chevronsLeft
@@ -48,6 +49,10 @@ extension SVG {
return """
<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-ban-icon lucide-ban"><path d="M4.929 4.929 19.07 19.071"/><circle cx="12" cy="12" r="10"/></svg>
"""
case .calculator:
return """
<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-calculator-icon lucide-calculator"><rect width="16" height="20" x="4" y="2" rx="2"/><line x1="8" x2="16" y1="6" y2="6"/><line x1="16" x2="16" y1="14" y2="18"/><path d="M16 10h.01"/><path d="M12 10h.01"/><path d="M8 10h.01"/><path d="M12 14h.01"/><path d="M8 14h.01"/><path d="M12 18h.01"/><path d="M8 18h.01"/></svg>
"""
case .chevronDown:
return """
<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-chevron-down-icon lucide-chevron-down"><path d="m6 9 6 6 6-6"/></svg>

View File

@@ -53,6 +53,13 @@ extension ViewController: DependencyKey {
extension ViewController.Request {
var isLoggedIn: Bool {
if (try? currentUser()) != nil {
return true
}
return false
}
func currentUser() throws -> User {
@Dependency(\.auth.currentUser) var currentUser
return try currentUser()

View File

@@ -3,6 +3,7 @@ import DatabaseClient
import Dependencies
import Elementary
import Foundation
import ManualDClient
import ManualDCore
import PdfClient
import ProjectClient
@@ -38,7 +39,9 @@ extension ViewController.Request {
// }
// }
// return try! await pdfClient.html(.mock())
return EmptyHTML()
return await view {
TestPage()
}
case .login(let route):
switch route {
case .index(let next):
@@ -93,6 +96,9 @@ extension ViewController.Request {
case .project(let route):
return await route.renderView(on: self)
case .quickCalc(let route):
return await route.renderView(on: self)
case .user(let route):
return await route.renderView(on: self)
}
@@ -705,3 +711,33 @@ extension SiteRoute.View.UserRoute.Profile {
}
}
}
extension SiteRoute.View.QuickCalcRoute {
func renderView(
on request: ViewController.Request
) async -> AnySendableHTML {
@Dependency(\.manualD) var manualD
switch self {
case .index:
return await request.view {
QuickCalcView(
isLoggedIn: request.isLoggedIn
)
}
case .submit(let form):
return await ResultView {
let ductSize = try await manualD.ductSize(cfm: form.cfm, frictionRate: form.frictionRate)
var rectangularSize: ManualDClient.RectangularSize? = nil
if let height = form.height {
rectangularSize = try await manualD.rectangularSize(
round: ductSize.finalSize, height: height)
}
return (ductSize, rectangularSize)
} onSuccess: { (ductSize, rectangularSize) in
QuickCalcView.Result(ductSize: ductSize, rectangularSize: rectangularSize)
}
}
}
}

View File

@@ -2,7 +2,6 @@ import Elementary
import ManualDCore
import Styleguide
// TODO: Have form hold onto equipment info model to edit.
struct EquipmentInfoForm: HTML, Sendable {
static let id = "equipmentForm"

View File

@@ -14,6 +14,13 @@ struct Navbar: HTML, Sendable {
self.userProfile = userProfile
}
var homeRoute: SiteRoute.View {
if userProfile {
return .project(.index)
}
return .home
}
var body: some HTML<HTMLTag.nav> {
nav(
.class(
@@ -37,7 +44,7 @@ struct Navbar: HTML, Sendable {
a(
.class("flex w-fit h-fit text-xl items-end px-4 py-2"),
.href(route: .project(.index))
.href(route: homeRoute)
) {
img(
.src("/images/mand_logo_sm.webp"),
@@ -48,11 +55,11 @@ struct Navbar: HTML, Sendable {
.tooltip("Home", position: .right)
}
if userProfile {
// TODO: Make dropdown
div(.class("flex-none dropdown dropdown-end dropdown-hover")) {
div(.class("btn m-1"), .tabindex(0), .role("button")) {
SVG(.circleUser)
}
.navButton()
ul(
.tabindex(-1),
.class("dropdown-content menu bg-base-200 rounded-box z-1 w-52 py-2 shadow-sm")

View File

@@ -0,0 +1,141 @@
import Dependencies
import Elementary
import ElementaryHTMX
import Foundation
import ManualDClient
import ManualDCore
import Styleguide
struct QuickCalcView: HTML, Sendable {
let isLoggedIn: Bool
init(isLoggedIn: Bool = false) {
self.isLoggedIn = isLoggedIn
}
var body: some HTML {
div {
Navbar(
sidebarToggle: false,
userProfile: isLoggedIn
)
div(.class("flex justify-center items-center px-10")) {
div(
.class(
"""
bg-base-300 rounded-3xl shadow-3xl
p-6 w-full
"""
)
) {
div(.class("flex space-x-6 items-center text-4xl")) {
SVG(.calculator)
h1(.class("text-4xl font-bold me-10")) {
"Duct Size"
}
}
p(.class("text-primary font-bold italic")) {
"Calculate duct size for the given parameters"
}
form(
.class("space-y-4 mt-6"),
.hx.post(route: .quickCalc(.index)),
.hx.target("#\(Result.id)"),
.hx.swap(.outerHTML)
) {
LabeledInput(
"CFM",
.name("cfm"),
.type(.number),
.placeholder("1000"),
.required,
.autofocus
)
LabeledInput(
"Friction Rate",
.name("frictionRate"),
.value("0.06"),
.required,
.type(.number),
.min("0.01"),
.step("0.01")
)
LabeledInput(
"Height",
.name("height"),
.type(.number),
.placeholder("Height (Optional)"),
)
SubmitButton()
.attributes(.class("btn-block mt-6"))
}
// Populate when submitted
div(.id(Result.id)) {}
}
}
}
}
struct Result: HTML, Sendable {
static let id = "resultView"
let ductSize: ManualDClient.DuctSize
let rectangularSize: ManualDClient.RectangularSize?
var body: some HTML<HTMLTag.div> {
div(
.id(Self.id),
.class(
"""
border-2 border-accent rounded-lg shadow-lg
w-full p-6 my-6
"""
)
) {
div(.class("flex justify-between p-4")) {
h2(.class("text-3xl font-bold")) { "Result" }
button(
.class("btn btn-primary"),
.hx.get(route: .quickCalc(.index)),
.hx.target("body"),
.hx.swap(.outerHTML)
) {
"Reset"
}
.tooltip("Reset form", position: .left)
}
table(.class("table table-zebra text-lg font-bold")) {
tbody {
tr {
td { Label("Calculated Size") }
td { Number(ductSize.calculatedSize, digits: 2) }
}
tr {
td { Label("Final Size") }
td { Number(ductSize.finalSize) }
}
tr {
td { Label("Flex Size") }
td { Number(ductSize.flexSize) }
}
if let rectangularSize {
tr {
td { Label("Rectangular Size") }
td { "\(rectangularSize.width) x \(rectangularSize.height)" }
}
}
}
}
}
}
}
}

View File

@@ -1,6 +1,7 @@
import Dependencies
import Elementary
import Foundation
import ManualDClient
import ManualDCore
import Styleguide
@@ -8,24 +9,77 @@ struct TestPage: HTML, Sendable {
// let ductSizes: DuctSizes
var body: some HTML {
div {}
// div(.class("overflow-auto")) {
// DuctSizingView.TrunkTable(ductSizes: ductSizes)
//
// Row {
// h2(.class("text-2xl font-bold")) { "Trunk Sizes" }
//
// PlusButton()
// .attributes(
// .class("me-6"),
// .showModal(id: TrunkSizeForm.id())
// )
// }
// .attributes(.class("mt-6"))
//
// div(.class("divider -mt-2")) {}
//
// DuctSizingView.TrunkTable(ductSizes: ductSizes)
// }
div {
Navbar(sidebarToggle: false, userProfile: false)
div(.class("flex justify-center items-center px-10")) {
div(
.class(
"""
bg-base-300 rounded-3xl shadow-3xl
p-6 w-full
"""
)
) {
div(.class("flex space-x-6 items-center text-4xl")) {
SVG(.calculator)
h1(.class("text-4xl font-bold me-10")) {
"Duct Size"
}
}
p(.class("text-primary font-bold italic")) {
"Calculate duct size for the given parameters"
}
form(
.class("space-y-4 mt-6"),
.action("#")
) {
LabeledInput(
"CFM",
.required,
.type(.number),
.placeholder("1000"),
.name("cfm")
)
LabeledInput(
"Friction Rate",
.value("0.06"),
.required,
.type(.number),
.name("frictionRate")
)
LabeledInput(
"Height",
.required,
.type(.number),
.placeholder("Height (Optional)"),
.name("frictionRate")
)
SubmitButton()
.attributes(.class("btn-block mt-6"))
}
}
// Populate when submitted
div(.id(Result.id)) {}
}
}
}
struct Result: HTML, Sendable {
static let id = "resultView"
let ductSize: ManualDClient.DuctSize
let rectangularSize: ManualDClient.RectangularSize?
var body: some HTML<HTMLTag.div> {
div(.id(Self.id)) {
}
}
}
}