feat: Fixes signup flow, begins updating form input fields.

This commit is contained in:
2026-01-12 18:53:45 -05:00
parent c2aedfac1a
commit fa9e8cffb0
10 changed files with 211 additions and 124 deletions

View File

@@ -5366,9 +5366,15 @@
} }
} }
} }
.my-1 {
margin-block: calc(var(--spacing) * 1);
}
.my-1\.5 { .my-1\.5 {
margin-block: calc(var(--spacing) * 1.5); margin-block: calc(var(--spacing) * 1.5);
} }
.my-6 {
margin-block: calc(var(--spacing) * 6);
}
.my-auto { .my-auto {
margin-block: auto; margin-block: auto;
} }
@@ -7823,6 +7829,9 @@
.px-4 { .px-4 {
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.py-1 {
padding-block: calc(var(--spacing) * 1);
}
.py-1\.5 { .py-1\.5 {
padding-block: calc(var(--spacing) * 1.5); padding-block: calc(var(--spacing) * 1.5);
} }
@@ -7832,6 +7841,9 @@
.py-4 { .py-4 {
padding-block: calc(var(--spacing) * 4); padding-block: calc(var(--spacing) * 4);
} }
.py-6 {
padding-block: calc(var(--spacing) * 6);
}
.ps-2 { .ps-2 {
padding-inline-start: calc(var(--spacing) * 2); padding-inline-start: calc(var(--spacing) * 2);
} }
@@ -8476,6 +8488,9 @@
color: var(--color-warning); color: var(--color-warning);
} }
} }
.text-base-100 {
color: var(--color-base-100);
}
.text-base-content { .text-base-content {
color: var(--color-base-content); color: var(--color-base-content);
} }
@@ -8491,12 +8506,21 @@
.text-info { .text-info {
color: var(--color-info); color: var(--color-info);
} }
.text-neutral {
color: var(--color-neutral);
}
.text-neutral-content {
color: var(--color-neutral-content);
}
.text-primary { .text-primary {
color: var(--color-primary); color: var(--color-primary);
} }
.text-secondary { .text-secondary {
color: var(--color-secondary); color: var(--color-secondary);
} }
.text-secondary-content {
color: var(--color-secondary-content);
}
.text-slate-900 { .text-slate-900 {
color: var(--color-slate-900); color: var(--color-slate-900);
} }

View File

@@ -9,6 +9,7 @@ extension DatabaseClient {
public struct UserProfile: Sendable { public struct UserProfile: Sendable {
public var create: @Sendable (User.Profile.Create) async throws -> User.Profile public var create: @Sendable (User.Profile.Create) async throws -> User.Profile
public var delete: @Sendable (User.Profile.ID) async throws -> Void 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 get: @Sendable (User.Profile.ID) async throws -> User.Profile?
public var update: @Sendable (User.Profile.ID, User.Profile.Update) 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) 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 get: { id in
try await UserProfileModel.find(id, on: database) try await UserProfileModel.find(id, on: database)
.map { try $0.toDTO() } .map { try $0.toDTO() }

View File

@@ -1,5 +1,26 @@
import Elementary import Elementary
public struct LabeledInput: HTML, Sendable {
let labelText: String
let inputAttributes: [HTMLAttribute<HTMLTag.input>]
public init(
_ label: String,
_ attributes: HTMLAttribute<HTMLTag.input>...
) {
self.labelText = label
self.inputAttributes = attributes
}
public var body: some HTML<HTMLTag.label> {
label(.class("input w-full")) {
span(.class("label")) { labelText }
input(attributes: inputAttributes)
}
}
}
public struct Input: HTML, Sendable { public struct Input: HTML, Sendable {
let id: String? let id: String?

View File

@@ -13,7 +13,7 @@ public struct Label: HTML, Sendable {
} }
public var body: some HTML<HTMLTag.span> { public var body: some HTML<HTMLTag.span> {
span(.class("text-xl text-gray-400 font-bold")) { span(.class("text-xl text-secondary font-bold")) {
title title
} }
} }

View File

@@ -73,6 +73,7 @@ extension ViewController.Request {
return user return user
} }
@discardableResult
func createAndAuthenticate( func createAndAuthenticate(
_ signup: User.Create _ signup: User.Create
) async throws -> User { ) async throws -> User {

View File

@@ -13,13 +13,13 @@ extension ViewController.Request {
switch route { switch route {
case .test: case .test:
return view { return await view {
TestPage() TestPage()
} }
case .login(let route): case .login(let route):
switch route { switch route {
case .index(let next): case .index(let next):
return view { return await view {
LoginForm(next: next) LoginForm(next: next)
} }
case .submit(let login): case .submit(let login):
@@ -35,7 +35,7 @@ extension ViewController.Request {
case .signup(let route): case .signup(let route):
switch route { switch route {
case .index: case .index:
return view { return await view {
LoginForm(style: .signup) LoginForm(style: .signup)
} }
case .submit(let request): case .submit(let request):
@@ -53,10 +53,11 @@ extension ViewController.Request {
return await view { return await view {
await ResultView { await ResultView {
_ = try await database.userProfile.create(profile) _ = try await database.userProfile.create(profile)
let user = try currentUser() let userID = profile.userID
// let user = try currentUser()
return ( return (
user.id, userID,
try await database.projects.fetch(user.id, .init(page: 1, per: 25)) try await database.projects.fetch(userID, .init(page: 1, per: 25))
) )
} onSuccess: { (userID, projects) in } onSuccess: { (userID, projects) in
ProjectsTable(userID: userID, projects: projects) ProjectsTable(userID: userID, projects: projects)
@@ -71,16 +72,11 @@ extension ViewController.Request {
} }
} }
func view<C: HTML>(
@HTMLBuilder inner: () -> C
) -> AnySendableHTML where C: Sendable {
MainPage(theme: theme) { inner() }
}
func view<C: HTML>( func view<C: HTML>(
@HTMLBuilder inner: () async -> C @HTMLBuilder inner: () async -> C
) async -> AnySendableHTML where C: Sendable { ) async -> AnySendableHTML where C: Sendable {
let inner = await inner() let inner = await inner()
let theme = await self.theme
return MainPage(theme: theme) { return MainPage(theme: theme) {
inner inner
@@ -88,8 +84,11 @@ extension ViewController.Request {
} }
var theme: Theme? { var theme: Theme? {
nil get async {
// .dracula @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() let user = try request.currentUser()
return ( return (
user, user,
try await database.userProfile.get(user.id) try await database.userProfile.fetch(user.id)
) )
} onSuccess: { (user, profile) in } onSuccess: { (user, profile) in
UserView(user: user, profile: profile) UserView(user: user, profile: profile)

View File

@@ -4,6 +4,15 @@ import Styleguide
struct Navbar: HTML, Sendable { struct Navbar: HTML, Sendable {
let sidebarToggle: Bool let sidebarToggle: Bool
let userProfile: Bool
init(
sidebarToggle: Bool,
userProfile: Bool = true
) {
self.sidebarToggle = sidebarToggle
self.userProfile = userProfile
}
var body: some HTML<HTMLTag.nav> { var body: some HTML<HTMLTag.nav> {
nav( nav(
@@ -41,41 +50,44 @@ struct Navbar: HTML, Sendable {
.navButton() .navButton()
} }
} }
div(.class("flex-none")) { if userProfile {
a( // TODO: Make dropdown
.href(route: .user(.profile(.index))), div(.class("flex-none")) {
) { a(
SVG(.circleUser) .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()
// }
} }
} }
} }

View File

@@ -27,7 +27,7 @@ struct ProjectForm: HTML, Sendable {
ModalForm(id: Self.id, dismiss: dismiss) { ModalForm(id: Self.id, dismiss: dismiss) {
h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" } h1(.class("text-3xl font-bold pb-6 ps-2")) { "Project" }
form( form(
.class("space-y-4 p-4"), .class("grid grid-cols-1 gap-4"),
project == nil project == nil
? .hx.post(route) ? .hx.post(route)
: .hx.patch(route), : .hx.patch(route),
@@ -38,36 +38,54 @@ struct ProjectForm: HTML, Sendable {
input(.class("hidden"), .name("id"), .value("\(project.id)")) input(.class("hidden"), .name("id"), .value("\(project.id)"))
} }
div { LabeledInput(
label(.for("name")) { "Name" } "Name",
Input(id: "name", placeholder: "Name") .name("name"),
.attributes(.type(.text), .required, .autofocus, .value(project?.name)) .type(.text),
} .value(project?.name),
div { .placeholder("Project Name"),
label(.for("streetAddress")) { "Address" } .required,
Input(id: "streetAddress", placeholder: "Street Address") .autofocus
.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))
}
div(.class("flex mt-6")) { LabeledInput(
SubmitButton() "Address",
.attributes(.class("btn-block")) .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"))
} }
} }
} }

View File

@@ -57,37 +57,37 @@ struct UserProfileForm: HTML, Sendable {
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "First Name" } span(.class("label")) { "First Name" }
input(.name("firstName"), .required, .autofocus) input(.name("firstName"), .value(profile?.firstName), .required, .autofocus)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "Last Name" } span(.class("label")) { "Last Name" }
input(.name("lastName"), .required) input(.name("lastName"), .value(profile?.lastName), .required)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "Company" } span(.class("label")) { "Company" }
input(.name("companyName"), .required) input(.name("companyName"), .value(profile?.companyName), .required)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "Address" } span(.class("label")) { "Address" }
input(.name("streetAddress"), .required) input(.name("streetAddress"), .value(profile?.streetAddress), .required)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "City" } span(.class("label")) { "City" }
input(.name("city"), .required) input(.name("city"), .value(profile?.city), .required)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "State" } span(.class("label")) { "State" }
input(.name("state"), .required) input(.name("state"), .value(profile?.state), .required)
} }
label(.class("input w-full")) { label(.class("input w-full")) {
span(.class("label")) { "Zip" } span(.class("label")) { "Zip" }
input(.name("zipCode"), .required) input(.name("zipCode"), .value(profile?.zipCode), .required)
} }
div(.class("dropdown dropdown-top")) { div(.class("dropdown dropdown-top")) {

View File

@@ -8,46 +8,50 @@ struct UserView: HTML, Sendable {
var body: some HTML { var body: some HTML {
div { div {
Row { Navbar(sidebarToggle: false, userProfile: false)
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 ?? "" }
}
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)
} }
} }
} }