diff --git a/Public/css/main.css b/Public/css/main.css index b9cd4cc..a93cf40 100644 --- a/Public/css/main.css +++ b/Public/css/main.css @@ -464,3 +464,15 @@ button.edit { .htmx-request.htmx-indicator { display: inline; } + +.btn-detail { + border: none; + color: grey; + text-decoration: .none; + margin-left: 10px; +} + +.btn-detail:hover { + background-color: #444; + opacity: 0.8; +} diff --git a/Sources/App/Controllers/View/UsersViewController.swift b/Sources/App/Controllers/View/UsersViewController.swift index 9176574..4cb2017 100644 --- a/Sources/App/Controllers/View/UsersViewController.swift +++ b/Sources/App/Controllers/View/UsersViewController.swift @@ -1,3 +1,42 @@ +import DatabaseClient +import Dependencies +import Elementary +import SharedModels +import Vapor +import VaporElementary + +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") + users.get(use: index) + users.group(":id") { + $0.get(use: get) + } + } + + @Sendable + func index(req: Request) async throws -> HTMLResponse { + HTMLResponse { + MainPage(route: .users) { + div(.class("container")) { + UserDetail(user: nil) + UserTable() + } + } + } + } + + @Sendable + func get(req: Request) async throws -> HTMLResponse { + let user = try await users.get(req.ensureIDPathComponent()) + return HTMLResponse { UserDetail(user: user) } + } +} + // import Dependencies // import Fluent // import Vapor diff --git a/Sources/App/Dependencies/DateFormatter.swift b/Sources/App/Dependencies/DateFormatter.swift new file mode 100644 index 0000000..bb96c8e --- /dev/null +++ b/Sources/App/Dependencies/DateFormatter.swift @@ -0,0 +1,28 @@ +import Dependencies +import Foundation + +public extension DependencyValues { + var dateFormatter: DateFormatter { + get { self[DateFormatter.self] } + set { self[DateFormatter.self] = newValue } + } +} + +#if hasFeature(RetroactiveAttribute) + extension DateFormatter: @retroactive DependencyKey { + + public static var liveValue: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + } + } +#else + extension DateFormatter: DependencyKey { + public static var liveValue: DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .short + return formatter + } + } +#endif diff --git a/Sources/App/DependenciesMiddleware.swift b/Sources/App/DependenciesMiddleware.swift index 59fcb5e..e6c388f 100644 --- a/Sources/App/DependenciesMiddleware.swift +++ b/Sources/App/DependenciesMiddleware.swift @@ -14,6 +14,7 @@ struct DependenciesMiddleware: AsyncMiddleware { try await values.yield { try await withDependencies { $0.database = .live(database: request.db) + $0.dateFormatter = .liveValue } operation: { try await next.respond(to: request) } diff --git a/Sources/App/Views/Buttons.swift b/Sources/App/Views/Buttons.swift index 74e3aa5..6a2603e 100644 --- a/Sources/App/Views/Buttons.swift +++ b/Sources/App/Views/Buttons.swift @@ -7,3 +7,14 @@ struct ToggleFormButton: HTML { } } } + +enum Button { + + static func danger(@HTMLBuilder body: () -> C) -> some HTML { + button(.class("danger")) { body() } + } + + static func close(id: String) -> some HTML { + button(.class("btn-add"), .on(.click, "toggleContent('\(id)')")) { "x" } + } +} diff --git a/Sources/App/Views/Float.swift b/Sources/App/Views/Float.swift new file mode 100644 index 0000000..883a3db --- /dev/null +++ b/Sources/App/Views/Float.swift @@ -0,0 +1,27 @@ +import Elementary + +struct Float: HTML { + + let id: String + let body: C? + + init(id: String = "float") { + self.id = id + self.body = nil + } + + init(id: String = "float", @HTMLBuilder body: () -> C) { + self.id = id + self.body = body() + } + + var content: some HTML { + div(.id(id), .class("float")) { + if let body { + body + } + } + } +} + +extension Float: Sendable where C: Sendable {} diff --git a/Sources/App/Views/Main.swift b/Sources/App/Views/Main.swift index 7fd94a6..e324bd6 100644 --- a/Sources/App/Views/Main.swift +++ b/Sources/App/Views/Main.swift @@ -7,9 +7,15 @@ struct MainPage: HTMLDocument { let inner: Inner let displayNav: Bool + let routeHeader: RouteHeaderView - init(displayNav: Bool = false, _ inner: () -> Inner) { + init( + displayNav: Bool = false, + route: ViewRoute, + _ inner: () -> Inner + ) { self.displayNav = displayNav + self.routeHeader = .init(route: route) self.inner = inner() } @@ -27,6 +33,7 @@ struct MainPage: HTMLDocument { Navbar() } } + routeHeader inner } } diff --git a/Sources/App/Views/RouteHeaderView.swift b/Sources/App/Views/RouteHeaderView.swift new file mode 100644 index 0000000..2fa27a7 --- /dev/null +++ b/Sources/App/Views/RouteHeaderView.swift @@ -0,0 +1,26 @@ +import Elementary +import ElementaryHTMX + +struct RouteHeaderView: HTML { + + let title: String + let description: String + + init(title: String, description: String) { + self.title = title + self.description = description + } + + init(route: ViewRoute) { + self.init(title: route.title, description: route.description) + } + + var content: some HTML { + div(.class("container"), .style("padding: 20px 20px;")) { + h1 { title } + br() + p { description } + br() + } + } +} diff --git a/Sources/App/Views/Users/UserDetail.swift b/Sources/App/Views/Users/UserDetail.swift new file mode 100644 index 0000000..511d8cc --- /dev/null +++ b/Sources/App/Views/Users/UserDetail.swift @@ -0,0 +1,73 @@ +import Dependencies +import Elementary +import Foundation +import SharedModels + +struct UserDetail: HTML, Sendable { + @Dependency(\.dateFormatter) var dateFormatter + + let user: User? + + var classString: String { + user != nil ? "float" : "" + } + + var display: String { + user != nil ? "block" : "hidden" + } + + var content: some HTML { + div( + .id("float"), + .class(classString), + .style("display: \(display);") + ) { + if let user { + Button.close(id: "float") + form { + div(.class("row")) { + makeLabel(for: "username", value: "Username:") + input(.type(.text), .name("username"), .value(user.username)) + makeLabel(for: "email", value: "Email:") + input(.type(.email), .name("email"), .value(user.username)) + } + div(.class("row")) { + div(.style("display: inline-block;")) { + h3(.class("label")) { "Created:" } + h3 { dateFormatter.formattedDate(user.createdAt) } + } + div(.style("display: inline-block;")) { + h3(.class("label")) { "Updated:" } + h3 { dateFormatter.formattedDate(user.updatedAt) } + } + } + } + div(.class("btn-row user-buttons")) { + Button.danger { "Delete" } + } + } + } + } + + func makeLabel( + for name: String, + value: String + ) -> some HTML { + label(.for(name)) { h3 { value } } + } + + func row(_ label: String, _ value: String) -> some HTML { + tr { + td(.class("label")) { h3 { label } } + td { h3 { value } } + } + } +} + +extension DateFormatter { + + func formattedDate(_ date: Date?) -> String { + guard let date else { return "" } + return string(from: date) + } +} diff --git a/Sources/App/Views/Users/UserIndex.swift b/Sources/App/Views/Users/UserIndex.swift new file mode 100644 index 0000000..86e4dc1 --- /dev/null +++ b/Sources/App/Views/Users/UserIndex.swift @@ -0,0 +1,19 @@ +import DatabaseClient +import Elementary +import SharedModels + +struct UserIndex: HTML { + let user: User? + + init(user: User? = nil) { + self.user = user + } + + var content: some HTML { + div { + // UserDetail(user: user) + div(.id("float"), .class("float")) {} + UserTable() + } + } +} diff --git a/Sources/App/Views/Users/UserTable.swift b/Sources/App/Views/Users/UserTable.swift index 3e2bc86..7d2e076 100644 --- a/Sources/App/Views/Users/UserTable.swift +++ b/Sources/App/Views/Users/UserTable.swift @@ -14,7 +14,7 @@ struct UserTable: HTML { tr { th { "Username" } th { "Email" } - th { ToggleFormButton() } + th(.style("width: 50px;")) { ToggleFormButton() } } } tbody { @@ -33,7 +33,16 @@ struct UserTable: HTML { tr { td { user.username } td { user.email } - td { "Fix me." } + td { + button( + .hx.get("/users/\(user.id.uuidString)"), + .hx.target("#float"), + .hx.swap(.outerHTML), + .class("btn-detail") + ) { + "〉" + } + } } } } diff --git a/Sources/App/Views/ViewRoute.swift b/Sources/App/Views/ViewRoute.swift new file mode 100644 index 0000000..ec16346 --- /dev/null +++ b/Sources/App/Views/ViewRoute.swift @@ -0,0 +1,31 @@ +enum ViewRoute: String { + + case employees + case login + case purchaseOrders + case users + case vendors + + var title: String { + switch self { + case .purchaseOrders: + return "Purchase Orders" + default: + return rawValue.capitalized + } + } + + var description: String { + switch self { + case .employees: + return "Employees are who purchase orders can be issued to." + case .purchaseOrders, .login: + return "" + case .users: + return "Users are who can login and issue purchase orders for employees." + case .vendors: + return "Vendors are where purchase orders can be issued to." + } + } + +} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index c8112ab..c421f17 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -7,11 +7,12 @@ import VaporElementary func routes(_ app: Application) throws { try app.register(collection: ApiController()) + try app.register(collection: UserViewController()) // try app.register(collection: ViewController()) app.get("test") { _ in HTMLResponse { - MainPage(displayNav: false) { + MainPage(displayNav: false, route: .purchaseOrders) { div(.class("container")) { h1 { "iT WORKS" } } @@ -21,17 +22,18 @@ func routes(_ app: Application) throws { app.get("login") { _ in HTMLResponse { - MainPage(displayNav: false) { + MainPage(displayNav: false, route: .login) { UserForm(context: .login(next: nil)) } } } - app.get("users") { _ in - HTMLResponse { - MainPage(displayNav: false) { - UserTable() - } - } - } + // app.get("users") { _ in + // HTMLResponse { + // // UserIndex() + // MainPage(displayNav: false, route: .users) { + // UserTable() + // } + // } + // } }