diff --git a/Public/css/main.css b/Public/css/main.css index df2969c..5f5c54b 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -1,3 +1,11 @@ +:root { + --primary: #ff66ff; + --secondary: #00ffcc; + --dark-bg: #14141f; + --bg: #1e1e2e; + --hover-bg: #444; +} + * { margin: 0; padding: 0; @@ -5,10 +13,13 @@ } body { - background-color: #1e1e2e; - color: #ff66ff; + background-color: var(--bg); + color: var(--primary); } +p { font-size: 1.25em; } +h1 { font-size: 2.5em; } + .container { margin: 0 auto; padding: 0 20px; @@ -16,13 +27,39 @@ body { } header { - background-color: #14141f; - color: #ff66ff; + background-color: var(--dark-bg); + color: var(--primary); padding: 10px 0; height: 60px; border-bottom: 1px solid grey; } +.primary { + color: var(--primary); +} + +.secondary { + color: var(--secondary); +} + +.btn-secondary { + font-size: 1.25em; + background-color: var(--primary); + color: var(--secondary); + border: 1px solid var(--secondary); + padding: 10px 20px; + border-radius: 10px; +} + +.btn-primary { + font-size: 1.25em; + background-color: var(--secondary); + color: var(--primary); + border: 1px solid var(--primary); + padding: 10px 20px; + border-radius: 10px; +} + .col-1 {width: 8.33%;} .col-2 {width: 16.66%;} .col-3 {width: 25%;} @@ -51,10 +88,7 @@ header { float: left; font-size: 1.5em; margin-top: 10px; -} - -.content { - padding: 50px 0; + margin-left: 10px; } form { @@ -63,43 +97,14 @@ form { text-align: center; } -form label { - padding: 15px; -} - -form input { - margin-bottom: 15px; - width: 100%; -} - -.login-form { - padding: 50px 0; - text-align: center; -} - -.login-form label { - padding: 15px; -} - -.login-form input { - margin-bottom: 15px; -} - -.user-form, .employee-form { - padding: 80px; - width: 100%; -} - -.user-form input { - width: 100%; -} - -.employee-form input { +#user-form input { width: 100%; + margin: 20px; } table { width: 100%; + font-size: 1.25em; } table, th, td { @@ -112,7 +117,7 @@ td, th { } table th { - color: #00ffcc; + color: var(--secondary); } input[type=submit] { @@ -147,31 +152,6 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus { outline: none; } -.btn { - cursor: pointer; -} - -.btn-edit img { - position: fixed; - right: 30px; - width: 30px; - height: 30px; -} - -.btn img:hover { - background-color: #555; -} - -.btn-delete img { - width: 20px; - height: 20px; - margin-top: 5px; -} - -.btn-delete img:hover { - background-color: #555; -} - .toggle, .toggle img { background-color: inherit; width: 60px; @@ -182,16 +162,10 @@ a.toggle, a img.toggle { cursor: pointer; } - .toggle img:hover { background-color: #555; } -tr.htmx-swapping td { - opacity: 0; - transition: opacity 0.5s ease-out; -} - .sidepanel { height: 275px; width: 0; @@ -242,18 +216,7 @@ tr.htmx-swapping td { } .openbtn:hover { - background-color: #444; -} - -.form-content { - transition: 0.5s; - overflow: auto; - z-index: 1; - position: fixed; - top: 60px; - left: 0; - background-color: #14141f; - width: 100%; + background-color: var(--hover-bg); } .closebtn { @@ -264,15 +227,6 @@ tr.htmx-swapping td { text-decoration: none; } -.form-content .closebtn { - position: absolute; - top: 0; - right: 25px; - font-size: 36px; - margin-left: 50px; - color: grey; -} - .btn-add { color: grey; font-size: 1.5em; @@ -280,37 +234,23 @@ tr.htmx-swapping td { } .btn-add:hover { - background-color: #444; + background-color: var(--hover-bg); } .btn { text-decoration: none; + background-color: inherit; + border: none; } .btn:hover { - background-color: #444; + background-color: var(--hover-bg); } .danger { color: red; } -.vendor-branches { - width: 350px; -} - -.vendor-branches ul li a { - position: relative; - top: 2px; - right: 0; - margin-left: 10px; - font-size: 1.5em; -} - -.vendor-branches ul li { - transition: 0.3s ease-out; -} - .branch-row { display: inline-block; width: 300px; @@ -333,16 +273,6 @@ tr.htmx-swapping td { font-size: 1.5em; } -.btn-row button { - border: none; - text-decoration: none; - color: grey; - background-color: inherit; - font-size: 1.3em; - padding-bottom: 10px; - cursor: pointer; -} - .btn-detail { border: none; text-decoration: none; @@ -351,18 +281,9 @@ tr.htmx-swapping td { font-size: 1.3em; } -.po-detail table { - border-collapse: collapse; - border: none; - max-width: 300px; -} - -.po-detail td { - border: none; -} - .label { - color: #00ffcc; + color: var(--secondary); + font-size: 1.25em; } .float { @@ -389,18 +310,13 @@ tr.htmx-swapping td { color: white; } -.float.htmx-swapping { - opacity: 0; - transition: opacity 0.5s ease-out; -} - .float table { position: relative; top: 15px; } .btn-row { - margin: 5px 40px; + margin: 20px 20px; padding: 10px 0px; } @@ -409,6 +325,8 @@ tr.htmx-swapping td { padding: 10px 20px; border-radius: 10px; margin-left: 20px; + text-decoration: none; + font-size: 1.25em; } button.danger { @@ -436,7 +354,7 @@ button.edit { } .row label { - display: inline; + display: inline-block; } .row .label { @@ -485,11 +403,14 @@ button.edit { display: inline; } -.btn-detail, .btn-add, .btn-close { - border: none; - color: grey; - text-decoration: none; - background-color: inherit; +tr.htmx-swapping td { + opacity: 0; + transition: opacity 0.5s ease-out; +} + +.float.htmx-swapping { + opacity: 0; + transition: opacity 0.5s ease-out; } .btn-detail { @@ -501,12 +422,10 @@ button:hover { opacity: 0.8; } -.btn-detail:hover { - background-color: #444; - opacity: 0.8; -} - .btn-close { float: right; font-size: 1.5em; + background-color: inherit; + border: none; + color: grey; } diff --git a/Sources/App/Controllers/View/UsersViewController.swift b/Sources/App/Controllers/View/UsersViewController.swift index a2c8b7e..199f188 100644 --- a/Sources/App/Controllers/View/UsersViewController.swift +++ b/Sources/App/Controllers/View/UsersViewController.swift @@ -77,125 +77,3 @@ struct UserViewController: RouteCollection { } } } - -// import Dependencies -// import Fluent -// import Vapor -// -// struct UserViewController: RouteCollection { -// -// @Dependency(\.users) var users -// -// private let api = UserApiController() -// -// func boot(routes: any RoutesBuilder) throws { -// let users = routes.protected.grouped("users") -// users.get(use: index(req:)) -// users.post(use: create(req:)) -// users.group(":id") { -// $0.get(use: details(req:)) -// $0.delete(use: delete(req:)) -// } -// } -// -// @Sendable -// func index(req: Request) async throws -> View { -// try await renderIndex(req) -// } -// -// @Sendable -// private func renderIndex(_ req: Request, _ user: User.DTO? = nil) async throws -> View { -// let users = try await api.getSortedUsers(req: req) -// return try await req.view.render("users/index", UsersCTX(user: user, users: users)) -// } -// -// @Sendable -// func create(req: Request) async throws -> View { -// let user = try await api.create(req: req) -// return try await req.view.render("users/table-row", user) -// } -// -// @Sendable -// func details(req: Request) async throws -> View { -// let user = try await users.get(req.ensureIDPathComponent()) -// // Check if the page has been rendered before. -// guard req.isHtmxRequest else { -// // Not an htmx-request, so render the whole page with the details. -// return try await renderIndex(req, user) -// } -// // An htmx-request header was present, so just return the details, -// return try await req.view.render("users/detail", ["user": user]) -// } -// -// @Sendable -// func delete(req: Request) async throws -> View { -// _ = try await api.delete(req: req) -// return try await req.view.render("users/table", ["users": api.getSortedUsers(req: req)]) -// } -// } -// -// struct UserFormCTX: Content { -// let htmxForm: HtmxFormCTX -// -// struct Context: Content { -// let showConfirmPassword: Bool -// let showEmailInput: Bool -// let buttonLabel: String -// } -// -// static func signIn(next: String?) -> Self { -// .init( -// htmxForm: .init( -// formClass: "user-form", -// formId: "user-form", -// htmxTargetUrl: .post("/login\((next != nil && next != "/") ? "?next=\(next!)" : "")"), -// htmxTarget: "user-table", -// htmxPushUrl: true, -// htmxResetAfterRequest: true, -// htmxSwapOob: nil, -// htmxSwap: .afterbegin, -// context: .init(showConfirmPassword: false, showEmailInput: false, buttonLabel: "Sign In") -// ) -// ) -// } -// -// static func create() -> Self { -// .init( -// htmxForm: .init( -// formClass: "user-form", -// formId: "user-form", -// htmxTargetUrl: .post("/users"), -// htmxTarget: "#user-table", -// htmxPushUrl: false, -// htmxResetAfterRequest: true, -// htmxSwapOob: nil, -// htmxSwap: nil, -// context: .init(showConfirmPassword: true, showEmailInput: true, buttonLabel: "Create") -// ) -// ) -// } -// } -// -// private struct UsersCTX: Content { -// let user: User.DTO? -// let users: [User.DTO] -// let form: UserFormCTX -// -// init( -// user: User.DTO? = nil, -// users: [User.DTO], -// form: UserFormCTX? = nil -// ) { -// self.user = user -// self.users = users -// self.form = form ?? .create() -// } -// } -// -// private extension UserApiController { -// -// func getSortedUsers(req: Request) async throws -> [User.DTO] { -// try await index(req: req) -// .sorted { ($0.username ?? "") < ($1.username ?? "") } -// } -// } diff --git a/Sources/App/DependenciesMiddleware.swift b/Sources/App/DependenciesMiddleware.swift index e6c388f..3d5d8e3 100644 --- a/Sources/App/DependenciesMiddleware.swift +++ b/Sources/App/DependenciesMiddleware.swift @@ -2,6 +2,9 @@ import DatabaseClientLive import Dependencies import Vapor +// Taken from discussions page on `swift-dependencies`. + +// TODO: Pass dependencies to set into this middleware. struct DependenciesMiddleware: AsyncMiddleware { private let values: DependencyValues.Continuation diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift index 7f30af3..61cfb20 100644 --- a/Sources/App/Views/Main.swift +++ b/Sources/App/Views/Main.swift @@ -4,6 +4,7 @@ import ElementaryHTMX struct MainPage: SendableHTMLDocument where Inner: Sendable { var title: String { "Purchase Orders" } + var lang: String { "en" } let inner: Inner let displayNav: Bool @@ -56,7 +57,7 @@ struct RouteHeaderView: HTML { div(.class("container"), .style("padding: 20px 20px;")) { h1 { title } br() - p { description } + p(.class("secondary")) { i { description } } br() } } diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift index de6fe26..60e1f29 100644 --- a/Sources/App/Views/Users/UserDetail.swift +++ b/Sources/App/Views/Users/UserDetail.swift @@ -19,20 +19,20 @@ struct UserDetail: HTML, Sendable { ) { div(.class("row")) { makeLabel(for: "username", value: "Username:") - input(.class("col-5"), .type(.text), .id("username"), .name("username"), .value(user.username), .required) + input(.class("col-4"), .type(.text), .id("username"), .name("username"), .value(user.username), .required) makeLabel(for: "email", value: "Email:") - input(.class("col-5"), .type(.email), .id("email"), .name("email"), .value(user.email), .required) + input(.class("col-4"), .type(.email), .id("email"), .name("email"), .value(user.email), .required) } div(.class("row")) { - span(.class("label col-1")) { "Created:" } + span(.class("label col-2")) { "Created:" } span(.class("date col-4")) { dateFormatter.formattedDate(user.createdAt) } - span(.class("label col-1")) { "Updated:" } + span(.class("label col-2")) { "Updated:" } span(.class("date col-4")) { dateFormatter.formattedDate(user.updatedAt) } } div(.class("btn-row user-buttons")) { button( .type(.submit), - .style("background-color: blue; color: white;") + .class("btn-secondary") ) { "Update" } Button.danger { "Delete" } .attributes( @@ -40,6 +40,7 @@ struct UserDetail: HTML, Sendable { .hx.trigger(.event(.click)), .hx.swap(.outerHTML), .hx.target("#user_\(user.id)"), + .hx.confirm("Are you sure you want to delete this user?"), .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';") ) } @@ -52,7 +53,7 @@ struct UserDetail: HTML, Sendable { for name: String, value: String ) -> some HTML { - label(.for(name), .class("col-1")) { span(.class("label")) { value } } + label(.for(name), .class("col-2")) { span(.class("label")) { value } } } func row(_ label: String, _ value: String) -> some HTML { diff --git a/Sources/App/Views/Users/UserForm.swift b/Sources/App/Views/Users/UserForm.swift index 0d73bda..a637f8e 100644 --- a/Sources/App/Views/Users/UserForm.swift +++ b/Sources/App/Views/Users/UserForm.swift @@ -1,6 +1,7 @@ import Elementary import ElementaryHTMX +// Form used to login or create a new user. struct UserForm: HTML, Sendable { let context: Context @@ -27,25 +28,31 @@ struct UserForm: HTML, Sendable { value: "if(event.detail.successful) this.reset(); toggleContent('float');" ) ) { - input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required) - br() + div(.class("row")) { + input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required) + } if context.showEmailInput { - input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required) - br() + div(.class("row")) { + input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required) + } + } + div(.class("row")) { + input(.type(.password), .id("password"), .name("password"), .placeholder("Password"), .required) } - input(.type(.password), .id("password"), .name("password"), .placeholder("Password"), .required) - br() if context.showConfirmPassword { - input( - .type(.password), - .id("confirmPassword"), - .name("confirmPassword"), - .placeholder("Confirm Password"), - .required - ) - br() + div(.class("row")) { + input( + .type(.password), + .id("confirmPassword"), + .name("confirmPassword"), + .placeholder("Confirm Password"), + .required + ) + } + } + div(.class("row")) { + button(.type(.submit), .class("btn-primary")) { context.buttonLabel } } - input(.type(.submit), .value(context.buttonLabel)) } } diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift index 5f8f85f..46bb6d7 100644 --- a/Sources/App/Views/Users/UserTable.swift +++ b/Sources/App/Views/Users/UserTable.swift @@ -44,15 +44,12 @@ struct UserTable: HTML { td { user.username } td { user.email } td { - button( + Button.detail().attributes( .hx.get("/users/\(user.id.uuidString)"), .hx.target("#float"), .hx.swap(.outerHTML), - .hx.pushURL(true), - .class("btn-detail") - ) { - "〉" - } + .hx.pushURL(true) + ) } } } diff --git a/Sources/App/Views/Buttons.swift b/Sources/App/Views/Utils/Buttons.swift similarity index 81% rename from Sources/App/Views/Buttons.swift rename to Sources/App/Views/Utils/Buttons.swift index 9c18649..8207b50 100644 --- a/Sources/App/Views/Buttons.swift +++ b/Sources/App/Views/Utils/Buttons.swift @@ -11,7 +11,7 @@ struct ToggleFormButton: HTML { enum Button { static func add() -> some HTML { - button(.class("btn-add")) { "+" } + button(.class("btn btn-add")) { "+" } } static func danger(@HTMLBuilder body: () -> C) -> some HTML { @@ -28,8 +28,14 @@ enum Button { button(.class("btn-update")) { "Update" } } + static func detail() -> some HTML { + button(.class("btn-detail")) { + "〉" + } + } + private static func makeOnClick(_ id: String, _ resetURL: String?) -> String { - var output = "toggleContent('\(id)');" + let output = "toggleContent('\(id)');" if let resetURL { return "\(output) window.location.href='\(resetURL)';" } diff --git a/Sources/App/Views/Float.swift b/Sources/App/Views/Utils/Float.swift similarity index 100% rename from Sources/App/Views/Float.swift rename to Sources/App/Views/Utils/Float.swift diff --git a/Sources/App/Views/Navbar.swift b/Sources/App/Views/Utils/Navbar.swift similarity index 100% rename from Sources/App/Views/Navbar.swift rename to Sources/App/Views/Utils/Navbar.swift diff --git a/Sources/App/Views/Vendors/VendorDetail.swift b/Sources/App/Views/Vendors/VendorDetail.swift new file mode 100644 index 0000000..519a36d --- /dev/null +++ b/Sources/App/Views/Vendors/VendorDetail.swift @@ -0,0 +1,15 @@ +import Elementary +import ElementaryHTMX +import SharedModels + +struct VendorDetail: HTML { + let vendor: Vendor? + + var content: some HTML { + div(.class("container")) { + VendorForm(vendor: vendor) + // TODO: Branch table + form. + } + } + +} diff --git a/Sources/App/Views/Vendors/VendorForm.swift b/Sources/App/Views/Vendors/VendorForm.swift new file mode 100644 index 0000000..a89fbd0 --- /dev/null +++ b/Sources/App/Views/Vendors/VendorForm.swift @@ -0,0 +1,37 @@ +import Elementary +import ElementaryHTMX +import SharedModels + +struct VendorForm: HTML { + let vendor: Vendor? + + var content: some HTML { + form( + .id("vendor-form"), + vendor != nil ? .hx.put(targetURL) : .hx.post(targetURL), + .hx.target("this"), + .hx.swap(.outerHTML) + ) { + div(.class("row")) { + input( + .id("vendor-name"), + .name("name"), + .value(vendor?.name ?? ""), + .placeholder("Vendor Name"), + .required + ) + button(.type(.submit), .class("btn-primary")) { buttonLabel } + } + } + } + + private var buttonLabel: String { + guard vendor != nil else { return "Update" } + return "Create" + } + + var targetURL: String { + guard let vendor else { return "/vendors" } + return "/vendors/\(vendor.id)" + } +} diff --git a/Sources/App/Views/Vendors/VendorTable.swift b/Sources/App/Views/Vendors/VendorTable.swift new file mode 100644 index 0000000..11dc800 --- /dev/null +++ b/Sources/App/Views/Vendors/VendorTable.swift @@ -0,0 +1,34 @@ +import Elementary +import ElementaryHTMX +import SharedModels + +struct VendorTable: HTML { + let vendors: [Vendor] + + var content: some HTML { + table { + thead { + th { "Name" } + th {} + th { Button.add() } + } + tbody(.id("vendor-table")) { + for vendor in vendors { + Row(vendor: vendor) + } + } + } + } + + struct Row: HTML { + let vendor: Vendor + + var content: some HTML { + tr(.id("vendor_\(vendor.id)")) { + td { vendor.name.capitalized } + td { "(\(vendor.branches?.count ?? 0)) Branches" } + td {} + } + } + } +}