diff --git a/Public/css/output.css b/Public/css/output.css index 1ca9a8c..ec3a096 100644 --- a/Public/css/output.css +++ b/Public/css/output.css @@ -5366,9 +5366,15 @@ } } } + .my-1 { + margin-block: calc(var(--spacing) * 1); + } .my-1\.5 { margin-block: calc(var(--spacing) * 1.5); } + .my-6 { + margin-block: calc(var(--spacing) * 6); + } .my-auto { margin-block: auto; } @@ -7823,6 +7829,9 @@ .px-4 { padding-inline: calc(var(--spacing) * 4); } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } .py-1\.5 { padding-block: calc(var(--spacing) * 1.5); } @@ -7832,6 +7841,9 @@ .py-4 { padding-block: calc(var(--spacing) * 4); } + .py-6 { + padding-block: calc(var(--spacing) * 6); + } .ps-2 { padding-inline-start: calc(var(--spacing) * 2); } @@ -8476,6 +8488,9 @@ color: var(--color-warning); } } + .text-base-100 { + color: var(--color-base-100); + } .text-base-content { color: var(--color-base-content); } @@ -8491,12 +8506,21 @@ .text-info { color: var(--color-info); } + .text-neutral { + color: var(--color-neutral); + } + .text-neutral-content { + color: var(--color-neutral-content); + } .text-primary { color: var(--color-primary); } .text-secondary { color: var(--color-secondary); } + .text-secondary-content { + color: var(--color-secondary-content); + } .text-slate-900 { color: var(--color-slate-900); } diff --git a/Sources/DatabaseClient/UserProfile.swift b/Sources/DatabaseClient/UserProfile.swift index 3ef5579..b755db8 100644 --- a/Sources/DatabaseClient/UserProfile.swift +++ b/Sources/DatabaseClient/UserProfile.swift @@ -9,6 +9,7 @@ extension DatabaseClient { public struct UserProfile: Sendable { public var create: @Sendable (User.Profile.Create) async throws -> User.Profile public var delete: @Sendable (User.Profile.ID) async throws -> Void + public var fetch: @Sendable (User.ID) async throws -> User.Profile? public var get: @Sendable (User.Profile.ID) async throws -> User.Profile? public var update: @Sendable (User.Profile.ID, User.Profile.Update) async throws -> User.Profile } @@ -32,6 +33,13 @@ extension DatabaseClient.UserProfile: TestDependencyKey { } try await model.delete(on: database) }, + fetch: { userID in + try await UserProfileModel.query(on: database) + .with(\.$user) + .filter(\.$user.$id == userID) + .first() + .map { try $0.toDTO() } + }, get: { id in try await UserProfileModel.find(id, on: database) .map { try $0.toDTO() } diff --git a/Sources/Styleguide/Input.swift b/Sources/Styleguide/Input.swift index 9979313..9ba876e 100644 --- a/Sources/Styleguide/Input.swift +++ b/Sources/Styleguide/Input.swift @@ -1,5 +1,26 @@ import Elementary +public struct LabeledInput: HTML, Sendable { + + let labelText: String + let inputAttributes: [HTMLAttribute] + + public init( + _ label: String, + _ attributes: HTMLAttribute... + ) { + self.labelText = label + self.inputAttributes = attributes + } + + public var body: some HTML { + label(.class("input w-full")) { + span(.class("label")) { labelText } + input(attributes: inputAttributes) + } + } +} + public struct Input: HTML, Sendable { let id: String? diff --git a/Sources/Styleguide/Label.swift b/Sources/Styleguide/Label.swift index 5bc252f..96e8c44 100644 --- a/Sources/Styleguide/Label.swift +++ b/Sources/Styleguide/Label.swift @@ -13,7 +13,7 @@ public struct Label: HTML, Sendable { } public var body: some HTML { - span(.class("text-xl text-gray-400 font-bold")) { + span(.class("text-xl text-secondary font-bold")) { title } } diff --git a/Sources/ViewController/Interface.swift b/Sources/ViewController/Interface.swift index 3e3084d..abcdab3 100644 --- a/Sources/ViewController/Interface.swift +++ b/Sources/ViewController/Interface.swift @@ -73,6 +73,7 @@ extension ViewController.Request { return user } + @discardableResult func createAndAuthenticate( _ signup: User.Create ) async throws -> User { diff --git a/Sources/ViewController/Live.swift b/Sources/ViewController/Live.swift index dc82943..8430807 100644 --- a/Sources/ViewController/Live.swift +++ b/Sources/ViewController/Live.swift @@ -13,13 +13,13 @@ extension ViewController.Request { switch route { case .test: - return view { + return await view { TestPage() } case .login(let route): switch route { case .index(let next): - return view { + return await view { LoginForm(next: next) } case .submit(let login): @@ -35,7 +35,7 @@ extension ViewController.Request { case .signup(let route): switch route { case .index: - return view { + return await view { LoginForm(style: .signup) } case .submit(let request): @@ -53,10 +53,11 @@ extension ViewController.Request { return await view { await ResultView { _ = try await database.userProfile.create(profile) - let user = try currentUser() + let userID = profile.userID + // let user = try currentUser() return ( - user.id, - try await database.projects.fetch(user.id, .init(page: 1, per: 25)) + userID, + try await database.projects.fetch(userID, .init(page: 1, per: 25)) ) } onSuccess: { (userID, projects) in ProjectsTable(userID: userID, projects: projects) @@ -71,16 +72,11 @@ extension ViewController.Request { } } - func view( - @HTMLBuilder inner: () -> C - ) -> AnySendableHTML where C: Sendable { - MainPage(theme: theme) { inner() } - } - func view( @HTMLBuilder inner: () async -> C ) async -> AnySendableHTML where C: Sendable { let inner = await inner() + let theme = await self.theme return MainPage(theme: theme) { inner @@ -88,8 +84,11 @@ extension ViewController.Request { } var theme: Theme? { - nil - // .dracula + get async { + @Dependency(\.database) var database + guard let user = try? currentUser() else { return nil } + return try? await database.userProfile.fetch(user.id)?.theme + } } } @@ -634,7 +633,7 @@ extension SiteRoute.View.UserRoute.Profile { let user = try request.currentUser() return ( user, - try await database.userProfile.get(user.id) + try await database.userProfile.fetch(user.id) ) } onSuccess: { (user, profile) in UserView(user: user, profile: profile) diff --git a/Sources/ViewController/Views/Navbar.swift b/Sources/ViewController/Views/Navbar.swift index cf4b4a7..1845010 100644 --- a/Sources/ViewController/Views/Navbar.swift +++ b/Sources/ViewController/Views/Navbar.swift @@ -4,6 +4,15 @@ import Styleguide struct Navbar: HTML, Sendable { let sidebarToggle: Bool + let userProfile: Bool + + init( + sidebarToggle: Bool, + userProfile: Bool = true + ) { + self.sidebarToggle = sidebarToggle + self.userProfile = userProfile + } var body: some HTML { nav( @@ -41,41 +50,44 @@ struct Navbar: HTML, Sendable { .navButton() } } - div(.class("flex-none")) { - a( - .href(route: .user(.profile(.index))), - ) { - SVG(.circleUser) + if userProfile { + // TODO: Make dropdown + div(.class("flex-none")) { + a( + .href(route: .user(.profile(.index))), + ) { + SVG(.circleUser) + } + .navButton() + // details(.class("dropdown dropdown-left dropdown-bottom")) { + // summary(.class("btn w-fit px-4 py-2")) { + // SVG(.circleUser) + // } + // .navButton() + // + // ul( + // .class( + // """ + // menu dropdown-content bg-base-100 + // rounded-box z-1 w-fit p-2 shadow-sm + // """ + // ) + // ) { + // li(.class("w-full")) { + // // TODO: Save theme to user profile ?? + // div(.class("flex justify-between p-4 space-x-6")) { + // Label("Theme") + // input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) + // } + // } + // } + // + // // button(.class("w-fit px-4 py-2")) { + // // SVG(.circleUser) + // // } + // // .navButton() + // } } - .navButton() - // details(.class("dropdown dropdown-left dropdown-bottom")) { - // summary(.class("btn w-fit px-4 py-2")) { - // SVG(.circleUser) - // } - // .navButton() - // - // ul( - // .class( - // """ - // menu dropdown-content bg-base-100 - // rounded-box z-1 w-fit p-2 shadow-sm - // """ - // ) - // ) { - // li(.class("w-full")) { - // // TODO: Save theme to user profile ?? - // div(.class("flex justify-between p-4 space-x-6")) { - // Label("Theme") - // input(.type(.checkbox), .class("toggle theme-controller"), .value("light")) - // } - // } - // } - // - // // button(.class("w-fit px-4 py-2")) { - // // SVG(.circleUser) - // // } - // // .navButton() - // } } } } diff --git a/Sources/ViewController/Views/Project/ProjectForm.swift b/Sources/ViewController/Views/Project/ProjectForm.swift index 339deda..5789fb5 100644 --- a/Sources/ViewController/Views/Project/ProjectForm.swift +++ b/Sources/ViewController/Views/Project/ProjectForm.swift @@ -27,7 +27,7 @@ struct ProjectForm: HTML, Sendable { ModalForm(id: Self.id, dismiss: dismiss) { h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } form( - .class("space-y-4 p-4"), + .class("grid grid-cols-1 gap-4"), project == nil ? .hx.post(route) : .hx.patch(route), @@ -38,36 +38,54 @@ struct ProjectForm: HTML, Sendable { input(.class("hidden"), .name("id"), .value("\(project.id)")) } - div { - label(.for("name")) { "Name" } - Input(id: "name", placeholder: "Name") - .attributes(.type(.text), .required, .autofocus, .value(project?.name)) - } - div { - label(.for("streetAddress")) { "Address" } - Input(id: "streetAddress", placeholder: "Street Address") - .attributes(.type(.text), .required, .value(project?.streetAddress)) - } - div { - label(.for("city")) { "City" } - Input(id: "city", placeholder: "City") - .attributes(.type(.text), .required, .value(project?.city)) - } - div { - label(.for("state")) { "State" } - Input(id: "state", placeholder: "State") - .attributes(.type(.text), .required, .value(project?.state)) - } - div { - label(.for("zipCode")) { "Zip" } - Input(id: "zipCode", placeholder: "Zip code") - .attributes(.type(.text), .required, .value(project?.zipCode)) - } + LabeledInput( + "Name", + .name("name"), + .type(.text), + .value(project?.name), + .placeholder("Project Name"), + .required, + .autofocus + ) - div(.class("flex mt-6")) { - SubmitButton() - .attributes(.class("btn-block")) - } + LabeledInput( + "Address", + .name("streetAddress"), + .type(.text), + .value(project?.streetAddress), + .placeholder("Street Address"), + .required + ) + + LabeledInput( + "City", + .name("city"), + .type(.text), + .value(project?.city), + .placeholder("City"), + .required + ) + + LabeledInput( + "State", + .name("state"), + .type(.text), + .value(project?.state), + .placeholder("State"), + .required + ) + + LabeledInput( + "Zip", + .name("zipCode"), + .type(.text), + .value(project?.zipCode), + .placeholder("Zip Code"), + .required + ) + + SubmitButton() + .attributes(.class("btn-block my-6")) } } } diff --git a/Sources/ViewController/Views/User/UserProfileForm.swift b/Sources/ViewController/Views/User/UserProfileForm.swift index 383ad2b..7818076 100644 --- a/Sources/ViewController/Views/User/UserProfileForm.swift +++ b/Sources/ViewController/Views/User/UserProfileForm.swift @@ -57,37 +57,37 @@ struct UserProfileForm: HTML, Sendable { label(.class("input w-full")) { span(.class("label")) { "First Name" } - input(.name("firstName"), .required, .autofocus) + input(.name("firstName"), .value(profile?.firstName), .required, .autofocus) } label(.class("input w-full")) { span(.class("label")) { "Last Name" } - input(.name("lastName"), .required) + input(.name("lastName"), .value(profile?.lastName), .required) } label(.class("input w-full")) { span(.class("label")) { "Company" } - input(.name("companyName"), .required) + input(.name("companyName"), .value(profile?.companyName), .required) } label(.class("input w-full")) { span(.class("label")) { "Address" } - input(.name("streetAddress"), .required) + input(.name("streetAddress"), .value(profile?.streetAddress), .required) } label(.class("input w-full")) { span(.class("label")) { "City" } - input(.name("city"), .required) + input(.name("city"), .value(profile?.city), .required) } label(.class("input w-full")) { span(.class("label")) { "State" } - input(.name("state"), .required) + input(.name("state"), .value(profile?.state), .required) } label(.class("input w-full")) { span(.class("label")) { "Zip" } - input(.name("zipCode"), .required) + input(.name("zipCode"), .value(profile?.zipCode), .required) } div(.class("dropdown dropdown-top")) { diff --git a/Sources/ViewController/Views/User/UserView.swift b/Sources/ViewController/Views/User/UserView.swift index bc84b82..c5addde 100644 --- a/Sources/ViewController/Views/User/UserView.swift +++ b/Sources/ViewController/Views/User/UserView.swift @@ -8,46 +8,50 @@ struct UserView: HTML, Sendable { var body: some HTML { div { - Row { - h1(.class("text-2xl font-bold")) { "Account" } - EditButton() - .attributes(.showModal(id: UserProfileForm.id(profile))) - } - - if let profile { - table(.class("table table-zebra")) { - tr { - td { Label("Name") } - td { "\(profile.firstName) \(profile.lastName)" } - } - tr { - td { Label("Company") } - td { profile.companyName } - } - tr { - td { Label("Street Address") } - td { profile.streetAddress } - } - tr { - td { Label("City") } - td { profile.city } - } - tr { - td { Label("State") } - td { profile.state } - } - tr { - td { Label("Zip Code") } - td { profile.zipCode } - } - tr { - td { Label("Theme") } - td { profile.theme?.rawValue ?? "" } - } + Navbar(sidebarToggle: false, userProfile: false) + div(.class("p-4")) { + Row { + h1(.class("text-2xl font-bold")) { "Account" } + EditButton() + .attributes(.showModal(id: UserProfileForm.id(profile))) } + + if let profile { + table(.class("table table-zebra border rounded-lg")) { + tr { + td { Label("Name") } + td { "\(profile.firstName) \(profile.lastName)" } + } + tr { + td { Label("Company") } + td { profile.companyName } + } + tr { + td { Label("Street Address") } + td { profile.streetAddress } + } + tr { + td { Label("City") } + td { profile.city } + } + tr { + td { Label("State") } + td { profile.state } + } + tr { + td { Label("Zip Code") } + td { profile.zipCode } + } + tr { + td { Label("Theme") } + td { profile.theme?.rawValue ?? "" } + } + + } + } + UserProfileForm(userID: user.id, profile: profile, dismiss: true) } - UserProfileForm(userID: user.id, profile: profile, dismiss: true) } } }