feat: Begins vendor views

This commit is contained in:
2025-01-15 16:37:18 -05:00
parent 24570e7191
commit 6f2e87e886
13 changed files with 199 additions and 301 deletions

View File

@@ -1,3 +1,11 @@
:root {
--primary: #ff66ff;
--secondary: #00ffcc;
--dark-bg: #14141f;
--bg: #1e1e2e;
--hover-bg: #444;
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
@@ -5,10 +13,13 @@
} }
body { body {
background-color: #1e1e2e; background-color: var(--bg);
color: #ff66ff; color: var(--primary);
} }
p { font-size: 1.25em; }
h1 { font-size: 2.5em; }
.container { .container {
margin: 0 auto; margin: 0 auto;
padding: 0 20px; padding: 0 20px;
@@ -16,13 +27,39 @@ body {
} }
header { header {
background-color: #14141f; background-color: var(--dark-bg);
color: #ff66ff; color: var(--primary);
padding: 10px 0; padding: 10px 0;
height: 60px; height: 60px;
border-bottom: 1px solid grey; 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-1 {width: 8.33%;}
.col-2 {width: 16.66%;} .col-2 {width: 16.66%;}
.col-3 {width: 25%;} .col-3 {width: 25%;}
@@ -51,10 +88,7 @@ header {
float: left; float: left;
font-size: 1.5em; font-size: 1.5em;
margin-top: 10px; margin-top: 10px;
} margin-left: 10px;
.content {
padding: 50px 0;
} }
form { form {
@@ -63,43 +97,14 @@ form {
text-align: center; text-align: center;
} }
form label { #user-form input {
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 {
width: 100%; width: 100%;
margin: 20px;
} }
table { table {
width: 100%; width: 100%;
font-size: 1.25em;
} }
table, th, td { table, th, td {
@@ -112,7 +117,7 @@ td, th {
} }
table th { table th {
color: #00ffcc; color: var(--secondary);
} }
input[type=submit] { input[type=submit] {
@@ -147,31 +152,6 @@ input[type=text]:focus, input[type=password]:focus, input[type=email]:focus {
outline: none; 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 { .toggle, .toggle img {
background-color: inherit; background-color: inherit;
width: 60px; width: 60px;
@@ -182,16 +162,10 @@ a.toggle, a img.toggle {
cursor: pointer; cursor: pointer;
} }
.toggle img:hover { .toggle img:hover {
background-color: #555; background-color: #555;
} }
tr.htmx-swapping td {
opacity: 0;
transition: opacity 0.5s ease-out;
}
.sidepanel { .sidepanel {
height: 275px; height: 275px;
width: 0; width: 0;
@@ -242,18 +216,7 @@ tr.htmx-swapping td {
} }
.openbtn:hover { .openbtn:hover {
background-color: #444; background-color: var(--hover-bg);
}
.form-content {
transition: 0.5s;
overflow: auto;
z-index: 1;
position: fixed;
top: 60px;
left: 0;
background-color: #14141f;
width: 100%;
} }
.closebtn { .closebtn {
@@ -264,15 +227,6 @@ tr.htmx-swapping td {
text-decoration: none; text-decoration: none;
} }
.form-content .closebtn {
position: absolute;
top: 0;
right: 25px;
font-size: 36px;
margin-left: 50px;
color: grey;
}
.btn-add { .btn-add {
color: grey; color: grey;
font-size: 1.5em; font-size: 1.5em;
@@ -280,37 +234,23 @@ tr.htmx-swapping td {
} }
.btn-add:hover { .btn-add:hover {
background-color: #444; background-color: var(--hover-bg);
} }
.btn { .btn {
text-decoration: none; text-decoration: none;
background-color: inherit;
border: none;
} }
.btn:hover { .btn:hover {
background-color: #444; background-color: var(--hover-bg);
} }
.danger { .danger {
color: red; 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 { .branch-row {
display: inline-block; display: inline-block;
width: 300px; width: 300px;
@@ -333,16 +273,6 @@ tr.htmx-swapping td {
font-size: 1.5em; 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 { .btn-detail {
border: none; border: none;
text-decoration: none; text-decoration: none;
@@ -351,18 +281,9 @@ tr.htmx-swapping td {
font-size: 1.3em; font-size: 1.3em;
} }
.po-detail table {
border-collapse: collapse;
border: none;
max-width: 300px;
}
.po-detail td {
border: none;
}
.label { .label {
color: #00ffcc; color: var(--secondary);
font-size: 1.25em;
} }
.float { .float {
@@ -389,18 +310,13 @@ tr.htmx-swapping td {
color: white; color: white;
} }
.float.htmx-swapping {
opacity: 0;
transition: opacity 0.5s ease-out;
}
.float table { .float table {
position: relative; position: relative;
top: 15px; top: 15px;
} }
.btn-row { .btn-row {
margin: 5px 40px; margin: 20px 20px;
padding: 10px 0px; padding: 10px 0px;
} }
@@ -409,6 +325,8 @@ tr.htmx-swapping td {
padding: 10px 20px; padding: 10px 20px;
border-radius: 10px; border-radius: 10px;
margin-left: 20px; margin-left: 20px;
text-decoration: none;
font-size: 1.25em;
} }
button.danger { button.danger {
@@ -436,7 +354,7 @@ button.edit {
} }
.row label { .row label {
display: inline; display: inline-block;
} }
.row .label { .row .label {
@@ -485,11 +403,14 @@ button.edit {
display: inline; display: inline;
} }
.btn-detail, .btn-add, .btn-close { tr.htmx-swapping td {
border: none; opacity: 0;
color: grey; transition: opacity 0.5s ease-out;
text-decoration: none; }
background-color: inherit;
.float.htmx-swapping {
opacity: 0;
transition: opacity 0.5s ease-out;
} }
.btn-detail { .btn-detail {
@@ -501,12 +422,10 @@ button:hover {
opacity: 0.8; opacity: 0.8;
} }
.btn-detail:hover {
background-color: #444;
opacity: 0.8;
}
.btn-close { .btn-close {
float: right; float: right;
font-size: 1.5em; font-size: 1.5em;
background-color: inherit;
border: none;
color: grey;
} }

View File

@@ -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<Context>
//
// 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 ?? "") }
// }
// }

View File

@@ -2,6 +2,9 @@ import DatabaseClientLive
import Dependencies import Dependencies
import Vapor import Vapor
// Taken from discussions page on `swift-dependencies`.
// TODO: Pass dependencies to set into this middleware.
struct DependenciesMiddleware: AsyncMiddleware { struct DependenciesMiddleware: AsyncMiddleware {
private let values: DependencyValues.Continuation private let values: DependencyValues.Continuation

View File

@@ -4,6 +4,7 @@ import ElementaryHTMX
struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable { struct MainPage<Inner: HTML>: SendableHTMLDocument where Inner: Sendable {
var title: String { "Purchase Orders" } var title: String { "Purchase Orders" }
var lang: String { "en" }
let inner: Inner let inner: Inner
let displayNav: Bool let displayNav: Bool
@@ -56,7 +57,7 @@ struct RouteHeaderView: HTML {
div(.class("container"), .style("padding: 20px 20px;")) { div(.class("container"), .style("padding: 20px 20px;")) {
h1 { title } h1 { title }
br() br()
p { description } p(.class("secondary")) { i { description } }
br() br()
} }
} }

View File

@@ -19,20 +19,20 @@ struct UserDetail: HTML, Sendable {
) { ) {
div(.class("row")) { div(.class("row")) {
makeLabel(for: "username", value: "Username:") 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:") 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")) { 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("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) } span(.class("date col-4")) { dateFormatter.formattedDate(user.updatedAt) }
} }
div(.class("btn-row user-buttons")) { div(.class("btn-row user-buttons")) {
button( button(
.type(.submit), .type(.submit),
.style("background-color: blue; color: white;") .class("btn-secondary")
) { "Update" } ) { "Update" }
Button.danger { "Delete" } Button.danger { "Delete" }
.attributes( .attributes(
@@ -40,6 +40,7 @@ struct UserDetail: HTML, Sendable {
.hx.trigger(.event(.click)), .hx.trigger(.event(.click)),
.hx.swap(.outerHTML), .hx.swap(.outerHTML),
.hx.target("#user_\(user.id)"), .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';") .custom(name: "hx-on::after-request", value: "toggleContent('float'); window.location.href='/users';")
) )
} }
@@ -52,7 +53,7 @@ struct UserDetail: HTML, Sendable {
for name: String, for name: String,
value: String value: String
) -> some HTML { ) -> 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<HTMLTag.tr> { func row(_ label: String, _ value: String) -> some HTML<HTMLTag.tr> {

View File

@@ -1,6 +1,7 @@
import Elementary import Elementary
import ElementaryHTMX import ElementaryHTMX
// Form used to login or create a new user.
struct UserForm: HTML, Sendable { struct UserForm: HTML, Sendable {
let context: Context let context: Context
@@ -27,25 +28,31 @@ struct UserForm: HTML, Sendable {
value: "if(event.detail.successful) this.reset(); toggleContent('float');" value: "if(event.detail.successful) this.reset(); toggleContent('float');"
) )
) { ) {
input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required) div(.class("row")) {
br() input(.type(.text), .id("username"), .name("username"), .placeholder("Username"), .autofocus, .required)
}
if context.showEmailInput { if context.showEmailInput {
input(.type(.email), .id("email"), .name("email"), .placeholder("Email"), .required) div(.class("row")) {
br() 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 { if context.showConfirmPassword {
input( div(.class("row")) {
.type(.password), input(
.id("confirmPassword"), .type(.password),
.name("confirmPassword"), .id("confirmPassword"),
.placeholder("Confirm Password"), .name("confirmPassword"),
.required .placeholder("Confirm Password"),
) .required
br() )
}
}
div(.class("row")) {
button(.type(.submit), .class("btn-primary")) { context.buttonLabel }
} }
input(.type(.submit), .value(context.buttonLabel))
} }
} }

View File

@@ -44,15 +44,12 @@ struct UserTable: HTML {
td { user.username } td { user.username }
td { user.email } td { user.email }
td { td {
button( Button.detail().attributes(
.hx.get("/users/\(user.id.uuidString)"), .hx.get("/users/\(user.id.uuidString)"),
.hx.target("#float"), .hx.target("#float"),
.hx.swap(.outerHTML), .hx.swap(.outerHTML),
.hx.pushURL(true), .hx.pushURL(true)
.class("btn-detail") )
) {
""
}
} }
} }
} }

View File

@@ -11,7 +11,7 @@ struct ToggleFormButton: HTML {
enum Button { enum Button {
static func add() -> some HTML<HTMLTag.button> { static func add() -> some HTML<HTMLTag.button> {
button(.class("btn-add")) { "+" } button(.class("btn btn-add")) { "+" }
} }
static func danger<C: HTML>(@HTMLBuilder body: () -> C) -> some HTML<HTMLTag.button> { static func danger<C: HTML>(@HTMLBuilder body: () -> C) -> some HTML<HTMLTag.button> {
@@ -28,8 +28,14 @@ enum Button {
button(.class("btn-update")) { "Update" } button(.class("btn-update")) { "Update" }
} }
static func detail() -> some HTML<HTMLTag.button> {
button(.class("btn-detail")) {
""
}
}
private static func makeOnClick(_ id: String, _ resetURL: String?) -> String { private static func makeOnClick(_ id: String, _ resetURL: String?) -> String {
var output = "toggleContent('\(id)');" let output = "toggleContent('\(id)');"
if let resetURL { if let resetURL {
return "\(output) window.location.href='\(resetURL)';" return "\(output) window.location.href='\(resetURL)';"
} }

View File

@@ -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.
}
}
}

View File

@@ -0,0 +1,37 @@
import Elementary
import ElementaryHTMX
import SharedModels
struct VendorForm: HTML {
let vendor: Vendor?
var content: some HTML<HTMLTag.form> {
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)"
}
}

View File

@@ -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<HTMLTag.tr> {
tr(.id("vendor_\(vendor.id)")) {
td { vendor.name.capitalized }
td { "(\(vendor.branches?.count ?? 0)) Branches" }
td {}
}
}
}
}