WIP: Updates to login / signup forms, rearranges some view routes.
This commit is contained in:
@@ -25,7 +25,9 @@ extension SiteRoute.Api.ProjectRoute {
|
|||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case .create(let request):
|
case .create(let request):
|
||||||
return try await database.projects.create(request)
|
// return try await database.projects.create(request)
|
||||||
|
// FIX:
|
||||||
|
fatalError()
|
||||||
case .delete(let id):
|
case .delete(let id):
|
||||||
try await database.projects.delete(id)
|
try await database.projects.delete(id)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -7,21 +7,20 @@ import ManualDCore
|
|||||||
extension DatabaseClient {
|
extension DatabaseClient {
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct Projects: Sendable {
|
public struct Projects: Sendable {
|
||||||
public var create: @Sendable (Project.Create) async throws -> Project
|
public var create: @Sendable (User.ID, Project.Create) async throws -> Project
|
||||||
public var delete: @Sendable (Project.ID) async throws -> Void
|
public var delete: @Sendable (Project.ID) async throws -> Void
|
||||||
public var get: @Sendable (Project.ID) async throws -> Project?
|
public var get: @Sendable (Project.ID) async throws -> Project?
|
||||||
|
public var fetch: @Sendable (User.ID) async throws -> [Project]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DatabaseClient.Projects: TestDependencyKey {
|
extension DatabaseClient.Projects: TestDependencyKey {
|
||||||
public static let testValue = Self()
|
public static let testValue = Self()
|
||||||
}
|
|
||||||
|
|
||||||
extension DatabaseClient.Projects {
|
|
||||||
public static func live(database: any Database) -> Self {
|
public static func live(database: any Database) -> Self {
|
||||||
.init(
|
.init(
|
||||||
create: { request in
|
create: { userID, request in
|
||||||
let model = try request.toModel()
|
let model = try request.toModel(userID: userID)
|
||||||
try await model.save(on: database)
|
try await model.save(on: database)
|
||||||
return try model.toDTO()
|
return try model.toDTO()
|
||||||
},
|
},
|
||||||
@@ -33,6 +32,13 @@ extension DatabaseClient.Projects {
|
|||||||
},
|
},
|
||||||
get: { id in
|
get: { id in
|
||||||
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
||||||
|
},
|
||||||
|
fetch: { userID in
|
||||||
|
try await ProjectModel.query(on: database)
|
||||||
|
.with(\.$user)
|
||||||
|
.filter(\.$user.$id == userID)
|
||||||
|
.all()
|
||||||
|
.map { try $0.toDTO() }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -40,14 +46,15 @@ extension DatabaseClient.Projects {
|
|||||||
|
|
||||||
extension Project.Create {
|
extension Project.Create {
|
||||||
|
|
||||||
func toModel() throws -> ProjectModel {
|
func toModel(userID: User.ID) throws -> ProjectModel {
|
||||||
try validate()
|
try validate()
|
||||||
return .init(
|
return .init(
|
||||||
name: name,
|
name: name,
|
||||||
streetAddress: streetAddress,
|
streetAddress: streetAddress,
|
||||||
city: city,
|
city: city,
|
||||||
state: state,
|
state: state,
|
||||||
zipCode: zipCode
|
zipCode: zipCode,
|
||||||
|
userID: userID
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +91,8 @@ extension Project {
|
|||||||
.field("zipCode", .string, .required)
|
.field("zipCode", .string, .required)
|
||||||
.field("createdAt", .datetime)
|
.field("createdAt", .datetime)
|
||||||
.field("updatedAt", .datetime)
|
.field("updatedAt", .datetime)
|
||||||
.unique(on: "name")
|
.field("userID", .uuid, .required, .references(UserModel.schema, "id"))
|
||||||
|
.unique(on: "userID", "name")
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,6 +134,9 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
@Children(for: \.$project)
|
@Children(for: \.$project)
|
||||||
var componentLosses: [ComponentLossModel]
|
var componentLosses: [ComponentLossModel]
|
||||||
|
|
||||||
|
@Parent(key: "userID")
|
||||||
|
var user: UserModel
|
||||||
|
|
||||||
init() {}
|
init() {}
|
||||||
|
|
||||||
init(
|
init(
|
||||||
@@ -135,6 +146,7 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
city: String,
|
city: String,
|
||||||
state: String,
|
state: String,
|
||||||
zipCode: String,
|
zipCode: String,
|
||||||
|
userID: User.ID,
|
||||||
createdAt: Date? = nil,
|
createdAt: Date? = nil,
|
||||||
updatedAt: Date? = nil
|
updatedAt: Date? = nil
|
||||||
) {
|
) {
|
||||||
@@ -145,6 +157,7 @@ final class ProjectModel: Model, @unchecked Sendable {
|
|||||||
self.city = city
|
self.city = city
|
||||||
self.state = state
|
self.state = state
|
||||||
self.zipCode = zipCode
|
self.zipCode = zipCode
|
||||||
|
$user.id = userID
|
||||||
self.createdAt = createdAt
|
self.createdAt = createdAt
|
||||||
self.updatedAt = updatedAt
|
self.updatedAt = updatedAt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ extension Project {
|
|||||||
streetAddress: String,
|
streetAddress: String,
|
||||||
city: String,
|
city: String,
|
||||||
state: String,
|
state: String,
|
||||||
zipCode: String
|
zipCode: String,
|
||||||
) {
|
) {
|
||||||
self.name = name
|
self.name = name
|
||||||
self.streetAddress = streetAddress
|
self.streetAddress = streetAddress
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ extension SiteRoute {
|
|||||||
///
|
///
|
||||||
/// The routes return html.
|
/// The routes return html.
|
||||||
public enum View: Equatable, Sendable {
|
public enum View: Equatable, Sendable {
|
||||||
|
case login(LoginRoute)
|
||||||
case project(ProjectRoute)
|
case project(ProjectRoute)
|
||||||
case room(RoomRoute)
|
case room(RoomRoute)
|
||||||
case frictionRate(FrictionRateRoute)
|
case frictionRate(FrictionRateRoute)
|
||||||
@@ -14,6 +15,9 @@ extension SiteRoute {
|
|||||||
case user(UserRoute)
|
case user(UserRoute)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
|
Route(.case(Self.login)) {
|
||||||
|
SiteRoute.View.LoginRoute.router
|
||||||
|
}
|
||||||
Route(.case(Self.project)) {
|
Route(.case(Self.project)) {
|
||||||
SiteRoute.View.ProjectRoute.router
|
SiteRoute.View.ProjectRoute.router
|
||||||
}
|
}
|
||||||
@@ -180,13 +184,9 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
|||||||
|
|
||||||
extension SiteRoute.View {
|
extension SiteRoute.View {
|
||||||
public enum UserRoute: Equatable, Sendable {
|
public enum UserRoute: Equatable, Sendable {
|
||||||
case login(Login)
|
|
||||||
case signup(Signup)
|
case signup(Signup)
|
||||||
|
|
||||||
public static let router = OneOf {
|
public static let router = OneOf {
|
||||||
Route(.case(Self.login)) {
|
|
||||||
SiteRoute.View.UserRoute.Login.router
|
|
||||||
}
|
|
||||||
Route(.case(Self.signup)) {
|
Route(.case(Self.signup)) {
|
||||||
SiteRoute.View.UserRoute.Signup.router
|
SiteRoute.View.UserRoute.Signup.router
|
||||||
}
|
}
|
||||||
@@ -194,9 +194,9 @@ extension SiteRoute.View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SiteRoute.View.UserRoute {
|
extension SiteRoute.View {
|
||||||
|
|
||||||
public enum Login: Equatable, Sendable {
|
public enum LoginRoute: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
case submit(User.Login)
|
case submit(User.Login)
|
||||||
|
|
||||||
@@ -220,6 +220,9 @@ extension SiteRoute.View.UserRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SiteRoute.View.UserRoute {
|
||||||
|
|
||||||
public enum Signup: Equatable, Sendable {
|
public enum Signup: Equatable, Sendable {
|
||||||
case index
|
case index
|
||||||
|
|||||||
9
Sources/Styleguide/ElementaryExtensions.swift
Normal file
9
Sources/Styleguide/ElementaryExtensions.swift
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Elementary
|
||||||
|
import ManualDCore
|
||||||
|
|
||||||
|
extension HTMLAttribute where Tag: HTMLTrait.Attributes.href {
|
||||||
|
|
||||||
|
public static func href(route: SiteRoute.View) -> Self {
|
||||||
|
href(SiteRoute.View.router.path(for: route))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ extension ViewController.Request {
|
|||||||
|
|
||||||
func render() async throws -> AnySendableHTML {
|
func render() async throws -> AnySendableHTML {
|
||||||
switch route {
|
switch route {
|
||||||
|
case .login(let route):
|
||||||
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
case .project(let route):
|
case .project(let route):
|
||||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
case .room(let route):
|
case .room(let route):
|
||||||
@@ -17,23 +19,24 @@ extension ViewController.Request {
|
|||||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||||
default:
|
default:
|
||||||
// FIX: FIX
|
// FIX: FIX
|
||||||
return mainPage
|
return _render(isHtmxRequest: false) {
|
||||||
|
div { "Fix me!" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension SiteRoute.View.ProjectRoute {
|
extension SiteRoute.View.ProjectRoute {
|
||||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
|
_render(isHtmxRequest: isHtmxRequest) {
|
||||||
switch self {
|
switch self {
|
||||||
case .index:
|
case .index:
|
||||||
return MainPage(active: .projects) {
|
|
||||||
ProjectView(project: .mock)
|
ProjectView(project: .mock)
|
||||||
}
|
|
||||||
case .form(let dismiss):
|
case .form(let dismiss):
|
||||||
return ProjectForm(dismiss: dismiss)
|
ProjectForm(dismiss: dismiss)
|
||||||
|
|
||||||
case .create:
|
case .create:
|
||||||
return mainPage
|
div { "Fix me!" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +47,7 @@ extension SiteRoute.View.RoomRoute {
|
|||||||
case .form(let dismiss):
|
case .form(let dismiss):
|
||||||
return RoomForm(dismiss: dismiss)
|
return RoomForm(dismiss: dismiss)
|
||||||
case .index:
|
case .index:
|
||||||
return MainPage(active: .rooms) {
|
return _render(isHtmxRequest: isHtmxRequest, active: .rooms) {
|
||||||
RoomsView(rooms: Room.mocks)
|
RoomsView(rooms: Room.mocks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +58,7 @@ extension SiteRoute.View.FrictionRateRoute {
|
|||||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
switch self {
|
switch self {
|
||||||
case .index:
|
case .index:
|
||||||
return MainPage(active: .frictionRate) {
|
return _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) {
|
||||||
FrictionRateView()
|
FrictionRateView()
|
||||||
}
|
}
|
||||||
case .form(let type, let dismiss):
|
case .form(let type, let dismiss):
|
||||||
@@ -86,7 +89,7 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
|||||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
switch self {
|
switch self {
|
||||||
case .index:
|
case .index:
|
||||||
return MainPage(active: .effectiveLength) {
|
return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) {
|
||||||
EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks)
|
EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks)
|
||||||
}
|
}
|
||||||
case .form(let dismiss):
|
case .form(let dismiss):
|
||||||
@@ -107,12 +110,12 @@ extension SiteRoute.View.UserRoute {
|
|||||||
|
|
||||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
switch self {
|
switch self {
|
||||||
case .login(.index):
|
// case .login(.index):
|
||||||
return MainPage(active: .projects, showSidebar: false) {
|
// return _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||||
LoginForm()
|
// LoginForm()
|
||||||
}
|
// }
|
||||||
case .signup(.index):
|
case .signup(.index):
|
||||||
return MainPage(active: .projects, showSidebar: false) {
|
return _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||||
LoginForm(style: .signup)
|
LoginForm(style: .signup)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -121,31 +124,33 @@ extension SiteRoute.View.UserRoute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let mainPage: AnySendableHTML = {
|
extension SiteRoute.View.LoginRoute {
|
||||||
MainPage(active: .projects) {
|
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||||
div {
|
_render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||||
h1 { "It works!" }
|
switch self {
|
||||||
|
case .index:
|
||||||
|
LoginForm()
|
||||||
|
case .submit:
|
||||||
|
// FIX:
|
||||||
|
div { "Fix me!" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
@Sendable
|
|
||||||
private func render<C: HTML>(
|
|
||||||
_ mainPage: (C) async throws -> AnySendableHTML,
|
|
||||||
_ isHtmxRequest: Bool,
|
|
||||||
@HTMLBuilder html: () -> C
|
|
||||||
) async rethrows -> AnySendableHTML where C: Sendable {
|
|
||||||
guard isHtmxRequest else {
|
|
||||||
return try await mainPage(html())
|
|
||||||
}
|
}
|
||||||
return html()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sendable
|
private func _render<C: HTML>(
|
||||||
private func render<C: HTML>(
|
isHtmxRequest: Bool,
|
||||||
_ mainPage: (C) async throws -> AnySendableHTML,
|
active activeTab: Sidebar.ActiveTab = .projects,
|
||||||
_ isHtmxRequest: Bool,
|
showSidebar: Bool = true,
|
||||||
_ html: @autoclosure @escaping () -> C
|
@HTMLBuilder inner: () -> C
|
||||||
) async rethrows -> AnySendableHTML where C: Sendable {
|
) -> AnySendableHTML where C: Sendable {
|
||||||
try await render(mainPage, isHtmxRequest) { html() }
|
if isHtmxRequest {
|
||||||
|
return inner()
|
||||||
|
}
|
||||||
|
return MainPage(
|
||||||
|
active: activeTab,
|
||||||
|
showSidebar: showSidebar
|
||||||
|
) {
|
||||||
|
inner()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ struct LoginForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some HTML {
|
var body: some HTML {
|
||||||
|
div(
|
||||||
|
.id("loginForm"),
|
||||||
|
.class("flex items-center justify-center")
|
||||||
|
) {
|
||||||
form {
|
form {
|
||||||
fieldset(.class("fieldset bg-base-200 border-base-300 rounded-box w-xl border p-4")) {
|
fieldset(.class("fieldset bg-base-200 border-base-300 rounded-box w-xl border p-4")) {
|
||||||
legend(.class("fieldset-legend")) { style.title }
|
legend(.class("fieldset-legend")) { style.title }
|
||||||
@@ -68,78 +72,16 @@ struct LoginForm: HTML, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button(.class("btn btn-neutral mt-4")) { style.title }
|
button(.class("btn btn-secondary mt-4")) { style.title }
|
||||||
|
a(
|
||||||
|
.class("btn btn-link mt-4"),
|
||||||
|
.href(route: style == .signup ? .login(.index) : .user(.signup(.index)))
|
||||||
|
) {
|
||||||
|
style == .login ? "Sign Up" : "Login"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// div(.class("flex items-center justify-center")) {
|
|
||||||
// div(.class("w-full mx-auto")) {
|
|
||||||
// h1(.class("text-2xl font-bold")) { style.title }
|
|
||||||
// form(
|
|
||||||
// .class("w-full h-screen")
|
|
||||||
// ) {
|
|
||||||
// fieldset(.class("fieldset w-full")) {
|
|
||||||
// legend(.class("fieldset-legend")) { "Email" }
|
|
||||||
// label(.class("input validator")) {
|
|
||||||
// SVG(.email)
|
|
||||||
// input(
|
|
||||||
// .type(.email), .placeholder("mail@site.com"), .required,
|
|
||||||
// .autofocus
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// div(.class("validator-hint hidden")) { "Enter valid email address." }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if style == .signup {
|
|
||||||
// fieldset(.class("fieldset")) {
|
|
||||||
// legend(.class("fieldset-legend")) { "Name" }
|
|
||||||
// label(.class("input validator")) {
|
|
||||||
// input(
|
|
||||||
// .type(.text), .placeholder("Username"), .required,
|
|
||||||
// .init(name: "pattern", value: "[A-Za-z][A-Za-z0-9\\-]*"),
|
|
||||||
// .init(name: "minlength", value: "3")
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// div(.class("validator-hint hidden")) { "Enter valid email address." }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// fieldset(.class("fieldset")) {
|
|
||||||
// legend(.class("fieldset-legend")) { "Password" }
|
|
||||||
// label(.class("input validator")) {
|
|
||||||
// SVG(.key)
|
|
||||||
// input(
|
|
||||||
// .type(.password), .placeholder("Password"), .required,
|
|
||||||
// .init(name: "pattern", value: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"),
|
|
||||||
// .init(name: "minlength", value: "8")
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// if style == .signup {
|
|
||||||
// label(.class("input validator")) {
|
|
||||||
// SVG(.key)
|
|
||||||
// input(
|
|
||||||
// .type(.password), .placeholder("Confirm Password"), .required,
|
|
||||||
// .init(name: "pattern", value: "(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"),
|
|
||||||
// .init(name: "minlength", value: "8")
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// div(.class("validator-hint hidden")) {
|
|
||||||
// p {
|
|
||||||
// "Must be more than 8 characters, including"
|
|
||||||
// br()
|
|
||||||
// "At least one number"
|
|
||||||
// br()
|
|
||||||
// "At least one lowercase letter"
|
|
||||||
// br()
|
|
||||||
// "At least one uppercase letter"
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// SubmitButton(title: style.title)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1523
output.css
1523
output.css
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
touch .build/browser-dev-sync
|
touch .build/browser-dev-sync
|
||||||
# browser-sync start --proxy localhost:8080 --ws -w --no-notify &
|
browser-sync start --proxy localhost:8080 --ws &
|
||||||
watchexec -w Sources -e .swift -r 'swift build --product App && touch .build/browser-dev-sync' &
|
watchexec -w Sources -e .swift -r 'swift build --product App && touch .build/browser-dev-sync' &
|
||||||
watchexec -w .build/browser-dev-sync -r 'swift run App'
|
watchexec -w .build/browser-dev-sync -r 'swift run App'
|
||||||
|
|||||||
Reference in New Issue
Block a user