From 51124205b866f0e299ed3e02c9ae343d7b9f882f Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Thu, 16 Jan 2025 11:09:35 -0500 Subject: [PATCH] feat: Working vendor views, does need some tweeks to user experience. --- Public/css/main.css | 16 +-- .../View/UsersViewController.swift | 4 +- .../View/VendorViewController.swift | 20 +++- .../Extensions/RouteBuilder+protected.swift | 40 ++++--- Sources/App/Views/Utils/Float.swift | 46 ++++++-- Sources/App/Views/Vendors/VendorDetail.swift | 100 ++++++++++-------- Sources/App/Views/Vendors/VendorForm.swift | 20 +++- 7 files changed, 155 insertions(+), 91 deletions(-) diff --git a/Public/css/main.css b/Public/css/main.css index 5f5c54b..be03492 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -253,22 +253,24 @@ a.toggle, a img.toggle { .branch-row { display: inline-block; - width: 300px; - height: 40px; - background-color: #14141f; + width: 100%; + height: 60px; + background-color: grey; border-radius: 25px; padding-left: 15px; margin: 5px; + border: 1px solid var(--primary); } -.branch-row .branch-name { +.branch-row .label { display: inline-block; - margin-top: 10px; + margin-top: 18px; + font-size: 1.25em; } -.branch-row a { +.branch-row button { float: right; - margin-top: 5px; + margin-top: 8px; margin-right: 15px; font-size: 1.5em; } diff --git a/Sources/App/Controllers/View/UsersViewController.swift b/Sources/App/Controllers/View/UsersViewController.swift index 199f188..5ec3766 100644 --- a/Sources/App/Controllers/View/UsersViewController.swift +++ b/Sources/App/Controllers/View/UsersViewController.swift @@ -10,8 +10,8 @@ struct UserViewController: RouteCollection { @Dependency(\.database.users) var users func boot(routes: any RoutesBuilder) throws { - // let users = routes.protected.grouped("users") - let users = routes.grouped("users") + let users = routes.protected.grouped("users") + // let users = routes.grouped("users") users.get(use: index) users.post(use: create) users.get("create", use: form) diff --git a/Sources/App/Controllers/View/VendorViewController.swift b/Sources/App/Controllers/View/VendorViewController.swift index aba103f..6d797a3 100644 --- a/Sources/App/Controllers/View/VendorViewController.swift +++ b/Sources/App/Controllers/View/VendorViewController.swift @@ -11,7 +11,7 @@ struct VendorViewController: RouteCollection { @Dependency(\.database.vendorBranches) var vendorBranches func boot(routes: any RoutesBuilder) throws { - let route = routes.grouped("vendors") + let route = routes.protected.grouped("vendors") route.get(use: index) route.post(use: create) route.get("create", use: form) @@ -32,7 +32,13 @@ struct VendorViewController: RouteCollection { @Sendable func create(req: Request) async throws -> HTMLResponse { let vendor = try await vendors.create(req.content.decode(Vendor.Create.self)) - return await req.render { VendorDetail(vendor: vendor) } + let vendors = try await vendors.fetchAll(.withBranches) + return await req.render { + div(.class("container"), .id("content")) { + VendorDetail(vendor: vendor) + VendorTable(vendors: vendors) + } + } } @Sendable @@ -67,13 +73,19 @@ struct VendorViewController: RouteCollection { let vendorID = try req.ensureIDPathComponent() let create = try req.content.decode(CreateBranch.self) let branch = try await vendorBranches.create(.init(name: create.name, vendorID: vendorID)) - return await req.render { VendorDetail.BranchTable.Row(branch: branch) } + return await req.render { VendorDetail.BranchRow(branch: branch) } + } + + @Sendable + func deleteBranch(req: Request) async throws -> HTTPStatus { + try await vendorBranches.delete(req.ensureIDPathComponent(key: "branchID")) + return .ok } private func mainPage(_ html: C) async throws -> some SendableHTMLDocument where C: Sendable { let vendors = try await vendors.fetchAll(.withBranches) return MainPage(displayNav: true, route: .vendors) { - div(.class("container")) { + div(.class("container"), .id("content")) { html VendorTable(vendors: vendors) } diff --git a/Sources/App/Extensions/RouteBuilder+protected.swift b/Sources/App/Extensions/RouteBuilder+protected.swift index 8ed0a0b..da4f3f8 100644 --- a/Sources/App/Extensions/RouteBuilder+protected.swift +++ b/Sources/App/Extensions/RouteBuilder+protected.swift @@ -2,23 +2,22 @@ import DatabaseClientLive import SharedModels import Vapor +// TODO: Fix before production. + extension RoutesBuilder { // Used to ensure views are protected, redirects users to the login page if they're // not authenticated. var protected: any RoutesBuilder { - // #if DEBUG - // return self - // #else - return grouped( - UserPasswordAuthenticator(), - UserTokenAuthenticator(), - UserSessionAuthenticator(), - User.redirectMiddleware { req in - "login?next=\(req.url)" - } - ) - // #endif + return self + // return grouped( + // UserPasswordAuthenticator(), + // UserTokenAuthenticator(), + // UserSessionAuthenticator(), + // User.redirectMiddleware { req in + // "login?next=\(req.url)" + // } + // ) } func apiUnprotected(route: PathComponent) -> any RoutesBuilder { @@ -28,15 +27,12 @@ extension RoutesBuilder { // Allows basic or token authentication for api routes and prefixes the // given route with "/api/v1". func apiProtected(route: PathComponent) -> any RoutesBuilder { - // #if DEBUG - // return apiUnprotected(route: route) - // #else - let prefixed = grouped("api", "v1", route) - return prefixed.grouped( - UserPasswordAuthenticator(), - UserTokenAuthenticator(), - User.guardMiddleware() - ) - // #endif + return apiUnprotected(route: route) + // let prefixed = grouped("api", "v1", route) + // return prefixed.grouped( + // UserPasswordAuthenticator(), + // UserTokenAuthenticator(), + // User.guardMiddleware() + // ) } } diff --git a/Sources/App/Views/Utils/Float.swift b/Sources/App/Views/Utils/Float.swift index 2d4963b..10ec012 100644 --- a/Sources/App/Views/Utils/Float.swift +++ b/Sources/App/Views/Utils/Float.swift @@ -1,29 +1,29 @@ import Elementary -struct Float: HTML { +struct Float: HTML { let id: String let shouldDisplay: Bool let body: C? - let resetURL: String? + let closeButton: B? init(id: String = "float") { self.id = id self.shouldDisplay = false - self.resetURL = nil self.body = nil + self.closeButton = nil } init( id: String = "float", shouldDisplay: Bool, - resetURL: String? = nil, - @HTMLBuilder body: () -> C + @HTMLBuilder body: () -> C, + @HTMLBuilder closeButton: () -> B ) { self.id = id self.shouldDisplay = shouldDisplay - self.resetURL = resetURL self.body = body() + self.closeButton = closeButton() } private var classString: String { @@ -37,8 +37,10 @@ struct Float: HTML { var content: some HTML { div(.id(id), .class(classString), .style("display: \(display);")) { if let body, shouldDisplay { - div(.class("btn-row")) { - Button.close(id: id, resetURL: resetURL) + if let closeButton { + div(.class("btn-row")) { + closeButton + } } body } @@ -46,4 +48,30 @@ struct Float: HTML { } } -extension Float: Sendable where C: Sendable {} +struct DefaultCloseButton: HTML { + let id: String + let resetURL: String? + + var content: some HTML { + Button.close(id: id, resetURL: resetURL) + } +} + +extension Float where B == DefaultCloseButton { + init( + id: String = "float", + shouldDisplay: Bool, + resetURL: String? = nil, + @HTMLBuilder body: () -> C + ) { + self.init( + id: id, + shouldDisplay: shouldDisplay, + body: body, + closeButton: { DefaultCloseButton(id: id, resetURL: resetURL) } + ) + } + +} + +extension Float: Sendable where C: Sendable, B: Sendable {} diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift index 1aae8ed..8a1d23c 100644 --- a/Sources/App/Views/Vendors/VendorDetail.swift +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -9,57 +9,67 @@ struct VendorDetail: HTML { var content: some HTML { Float(shouldDisplay: true) { VendorForm(.formOnly(vendor)) - BranchTable(branches: vendor.branches ?? []) - div(.class("row btn-row")) { - button(.class("btn-done")) { "Done" } - button(.class("danger")) { "Delete" } + h2(.style("margin-left: 20px; font-size: 1.5em;"), .class("label")) { "Branches" } + branchForm + branches + } closeButton: { + Button.close(id: "float") + .attributes( + .hx.get("/vendors"), + .hx.pushURL(true), + .hx.target("body"), + .hx.swap(.outerHTML) + ) + } + } + + var branchForm: some HTML { + form( + .id("branch-form"), + .hx.post("/vendors/\(vendor.id)/branches"), + .hx.target("#branches"), + .hx.swap(.beforeEnd), + .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") + ) { + input( + .type(.text), .class("col-9"), .name("name"), .placeholder("Add branch..."), .required, + .hx.post("/vendors/\(vendor.id)/branches"), + .hx.trigger(.event(.keyup).changed().delay("800ms")), + .hx.target("#branches"), + .hx.swap(.beforeEnd) // , + // .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") + ) + button( + .type(.submit), + .class("btn-secondary"), + .style("float: right; padding: 10px 50px;"), + .hx.target("#branch-table"), + .hx.swap(.beforeEnd) + ) { "+" } + } + } + + var branches: some HTML { + ul(.id("branches")) { + for branch in vendor.branches ?? [] { + BranchRow(branch: branch) } } } - struct BranchTable: HTML { - let branches: [VendorBranch] + struct BranchRow: HTML { + let branch: VendorBranch - var content: some HTML { - div { - form( - .id("branch-form"), - .custom(name: "hx-on::after-request", value: "if(event.detail.successful) this.reset();") + var content: some HTML { + li(.id("branch_\(branch.id)"), .class("branch-row")) { + span(.class("label")) { branch.name.capitalized } + button( + .class("btn"), + .hx.delete("/api/v1/vendors/branches/\(branch.id)"), + .hx.target("#branch_\(branch.id)"), + .hx.swap(.outerHTML.transition(true).swap("0.5s")) ) { - div(.class("row"), .style("margin: 0;")) { - input(.type(.text), .class("col-10"), .placeholder("Branch Name"), .required) - button( - .type(.submit), - .class("btn-secondary"), - .style("float: right; padding: 10px 50px;"), - .hx.target("#branch-table"), - .hx.swap(.beforeEnd) - ) { "+" } - } - } - table { - thead { - tr { - th(.class("label col-11")) { "Branch Location" } - th(.class("col-1")) {} - } - } - tbody(.id("branch-table")) { - for branch in branches { - Row(branch: branch) - } - } - } - } - } - - struct Row: HTML { - let branch: VendorBranch - - var content: some HTML { - tr { - td(.class("col-11")) { branch.name } - td(.class("col-1")) { Button.danger { "Delete" } } + img(.src("/images/trash-can.svg"), .width(30), .height(30), .style("margin-top: 5px;")) } } } diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift index db97b65..cef1a98 100644 --- a/Sources/App/Views/Vendors/VendorForm.swift +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -42,7 +42,7 @@ struct VendorForm: HTML { form( .id("vendor-form"), vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), - .hx.target("#float"), + .hx.target("#content"), .hx.swap(.outerHTML) ) { div(.class("row")) { @@ -53,11 +53,27 @@ struct VendorForm: HTML { .name("name"), .value(vendor?.name ?? ""), .placeholder("Vendor Name"), + vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), + .hx.trigger(.event(.keyup).changed().delay("500ms")), .required ) + if let vendor { + button( + .class("danger"), + .style("font-size: 1.25em; padding: 10px 20px; border-radius: 10px;"), + .hx.delete("/api/v1/vendors/\(vendor.id)"), + .hx.confirm("Are you sure you want to delete this vendor?"), + .hx.target("#vendor_\(vendor.id)"), + .hx.swap(.outerHTML.transition(true).swap("1s")), + .custom( + name: "hx-on::after-request", + value: "if(event.detail.successful) toggleContent('float'); window.location.href='/vendors';" + ) + ) { "Delete" } + } button( .type(.submit), - .class("col-1 btn-primary"), + .class("btn-primary"), .style("float: right") ) { buttonLabel } }