WIP: Working signup and login forms, along with initial view auth middleware.
This commit is contained in:
@@ -13,10 +13,10 @@ extension ViewController {
|
||||
route: route,
|
||||
isHtmxRequest: request.isHtmxRequest,
|
||||
logger: request.logger,
|
||||
// authenticate: { request.session.authenticate($0) },
|
||||
// currentUser: {
|
||||
// try request.auth.require(User.self)
|
||||
// }
|
||||
authenticateUser: { request.session.authenticate($0) },
|
||||
currentUser: {
|
||||
try request.auth.require(User.self)
|
||||
}
|
||||
)
|
||||
)
|
||||
return AnyHTMLResponse(value: html)
|
||||
|
||||
24
Sources/App/Middleware/ViewRoute+middleware.swift
Normal file
24
Sources/App/Middleware/ViewRoute+middleware.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import DatabaseClient
|
||||
import Fluent
|
||||
import ManualDCore
|
||||
import Vapor
|
||||
|
||||
private let viewRouteMiddleware: [any Middleware] = [
|
||||
UserPasswordAuthenticator(),
|
||||
UserSessionAuthenticator(),
|
||||
User.redirectMiddleware(path: "/login"),
|
||||
]
|
||||
|
||||
extension SiteRoute.View {
|
||||
var middleware: [any Middleware]? {
|
||||
switch self {
|
||||
case .project,
|
||||
.frictionRate,
|
||||
.effectiveLength,
|
||||
.room:
|
||||
return viewRouteMiddleware
|
||||
case .login, .signup:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,15 +100,14 @@ private func addCommands(to app: Application) {
|
||||
extension SiteRoute {
|
||||
|
||||
fileprivate func middleware() -> [any Middleware]? {
|
||||
switch self {
|
||||
case .api:
|
||||
return nil
|
||||
// switch self {
|
||||
// case .api(let route):
|
||||
// return route.middleware
|
||||
// // case .health:
|
||||
// // return nil
|
||||
// case .view(let route):
|
||||
// return route.middleware
|
||||
// }
|
||||
case .health:
|
||||
return nil
|
||||
case .view(let route):
|
||||
return route.middleware
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ public struct DatabaseClient: Sendable {
|
||||
public var equipment: Equipment
|
||||
public var componentLoss: ComponentLoss
|
||||
public var effectiveLength: EffectiveLengthClient
|
||||
public var users: Users
|
||||
}
|
||||
|
||||
extension DatabaseClient: TestDependencyKey {
|
||||
@@ -27,7 +28,8 @@ extension DatabaseClient: TestDependencyKey {
|
||||
rooms: .testValue,
|
||||
equipment: .testValue,
|
||||
componentLoss: .testValue,
|
||||
effectiveLength: .testValue
|
||||
effectiveLength: .testValue,
|
||||
users: .testValue
|
||||
)
|
||||
|
||||
public static func live(database: any Database) -> Self {
|
||||
@@ -37,7 +39,8 @@ extension DatabaseClient: TestDependencyKey {
|
||||
rooms: .live(database: database),
|
||||
equipment: .live(database: database),
|
||||
componentLoss: .live(database: database),
|
||||
effectiveLength: .live(database: database)
|
||||
effectiveLength: .live(database: database),
|
||||
users: .live(database: database)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ extension DatabaseClient {
|
||||
public var create: @Sendable (User.ID, Project.Create) async throws -> Project
|
||||
public var delete: @Sendable (Project.ID) async throws -> Void
|
||||
public var get: @Sendable (Project.ID) async throws -> Project?
|
||||
public var fetch: @Sendable (User.ID) async throws -> [Project]
|
||||
public var fetch: @Sendable (User.ID, PageRequest) async throws -> Page<Project>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,11 +33,12 @@ extension DatabaseClient.Projects: TestDependencyKey {
|
||||
get: { id in
|
||||
try await ProjectModel.find(id, on: database).map { try $0.toDTO() }
|
||||
},
|
||||
fetch: { userID in
|
||||
fetch: { userID, request in
|
||||
try await ProjectModel.query(on: database)
|
||||
.sort(\.$createdAt, .descending)
|
||||
.with(\.$user)
|
||||
.filter(\.$user.$id == userID)
|
||||
.all()
|
||||
.paginate(request)
|
||||
.map { try $0.toDTO() }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -83,8 +83,8 @@ extension User {
|
||||
.field("username", .string, .required)
|
||||
.field("email", .string, .required)
|
||||
.field("password_hash", .string, .required)
|
||||
.field("created_at", .datetime)
|
||||
.field("updated_at", .datetime)
|
||||
.field("createdAt", .datetime)
|
||||
.field("updatedAt", .datetime)
|
||||
.unique(on: "email", "username")
|
||||
.create()
|
||||
}
|
||||
|
||||
@@ -8,16 +8,20 @@ extension SiteRoute {
|
||||
/// The routes return html.
|
||||
public enum View: Equatable, Sendable {
|
||||
case login(LoginRoute)
|
||||
case signup(SignupRoute)
|
||||
case project(ProjectRoute)
|
||||
case room(RoomRoute)
|
||||
case frictionRate(FrictionRateRoute)
|
||||
case effectiveLength(EffectiveLengthRoute)
|
||||
case user(UserRoute)
|
||||
// case user(UserRoute)
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.login)) {
|
||||
SiteRoute.View.LoginRoute.router
|
||||
}
|
||||
Route(.case(Self.signup)) {
|
||||
SiteRoute.View.SignupRoute.router
|
||||
}
|
||||
Route(.case(Self.project)) {
|
||||
SiteRoute.View.ProjectRoute.router
|
||||
}
|
||||
@@ -30,9 +34,9 @@ extension SiteRoute {
|
||||
Route(.case(Self.effectiveLength)) {
|
||||
SiteRoute.View.EffectiveLengthRoute.router
|
||||
}
|
||||
Route(.case(Self.user)) {
|
||||
SiteRoute.View.UserRoute.router
|
||||
}
|
||||
// Route(.case(Self.user)) {
|
||||
// SiteRoute.View.UserRoute.router
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +46,7 @@ extension SiteRoute.View {
|
||||
case create(Project.Create)
|
||||
case form(dismiss: Bool = false)
|
||||
case index
|
||||
case page(page: Int = 1, limit: Int = 25)
|
||||
|
||||
static let rootPath = "projects"
|
||||
|
||||
@@ -74,6 +79,17 @@ extension SiteRoute.View {
|
||||
Path { rootPath }
|
||||
Method.get
|
||||
}
|
||||
Route(.case(Self.page(page:limit:))) {
|
||||
Path {
|
||||
rootPath
|
||||
"page"
|
||||
}
|
||||
Method.get
|
||||
Query {
|
||||
Field("page", default: 1) { Int.parser() }
|
||||
Field("limit", default: 25) { Int.parser() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,17 +198,17 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.View {
|
||||
public enum UserRoute: Equatable, Sendable {
|
||||
case signup(Signup)
|
||||
|
||||
public static let router = OneOf {
|
||||
Route(.case(Self.signup)) {
|
||||
SiteRoute.View.UserRoute.Signup.router
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// extension SiteRoute.View {
|
||||
// public enum UserRoute: Equatable, Sendable {
|
||||
// case signup(Signup)
|
||||
//
|
||||
// public static let router = OneOf {
|
||||
// Route(.case(Self.signup)) {
|
||||
// SiteRoute.View.UserRoute.Signup.router
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
extension SiteRoute.View {
|
||||
|
||||
@@ -222,9 +238,9 @@ extension SiteRoute.View {
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.View.UserRoute {
|
||||
extension SiteRoute.View {
|
||||
|
||||
public enum Signup: Equatable, Sendable {
|
||||
public enum SignupRoute: Equatable, Sendable {
|
||||
case index
|
||||
case submit(User.Create)
|
||||
|
||||
|
||||
26
Sources/Styleguide/Indicator.swift
Normal file
26
Sources/Styleguide/Indicator.swift
Normal file
@@ -0,0 +1,26 @@
|
||||
import Elementary
|
||||
|
||||
public struct Indicator: HTML, Sendable {
|
||||
|
||||
let size: Size
|
||||
|
||||
public init(size: Size = .lg) {
|
||||
self.size = size
|
||||
}
|
||||
|
||||
public var body: some HTML<HTMLTag.span> {
|
||||
span(.class("loading loading-spinner \(size.class) htmx-indicator")) {}
|
||||
}
|
||||
|
||||
public enum Size: String, Equatable, Sendable {
|
||||
case xs
|
||||
case sm
|
||||
case md
|
||||
case lg
|
||||
case xl
|
||||
|
||||
var `class`: String {
|
||||
"loading-\(rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,10 @@ public typealias AnySendableHTML = (any HTML & Sendable)
|
||||
|
||||
@DependencyClient
|
||||
public struct ViewController: Sendable {
|
||||
|
||||
public typealias AuthenticateHandler = @Sendable (User) -> Void
|
||||
public typealias CurrentUserHandler = @Sendable () throws -> User
|
||||
|
||||
public var view: @Sendable (Request) async throws -> AnySendableHTML
|
||||
}
|
||||
|
||||
@@ -25,15 +29,25 @@ extension ViewController {
|
||||
public let route: SiteRoute.View
|
||||
public let isHtmxRequest: Bool
|
||||
public let logger: Logger
|
||||
public let authenticateUser: AuthenticateHandler
|
||||
public let currentUser: CurrentUserHandler
|
||||
|
||||
public init(
|
||||
route: SiteRoute.View,
|
||||
isHtmxRequest: Bool,
|
||||
logger: Logger
|
||||
logger: Logger,
|
||||
authenticateUser: @escaping AuthenticateHandler,
|
||||
currentUser: @escaping CurrentUserHandler
|
||||
) {
|
||||
self.route = route
|
||||
self.isHtmxRequest = isHtmxRequest
|
||||
self.logger = logger
|
||||
self.authenticateUser = authenticateUser
|
||||
self.currentUser = currentUser
|
||||
}
|
||||
|
||||
func authenticate(_ user: User) {
|
||||
self.authenticateUser(user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
import DatabaseClient
|
||||
import Dependencies
|
||||
import Elementary
|
||||
import Foundation
|
||||
import ManualDCore
|
||||
|
||||
extension ViewController.Request {
|
||||
|
||||
func render() async throws -> AnySendableHTML {
|
||||
|
||||
@Dependency(\.database) var database
|
||||
|
||||
switch route {
|
||||
case .login(let route):
|
||||
switch route {
|
||||
case .index:
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
LoginForm()
|
||||
}
|
||||
case .submit(let login):
|
||||
let token = try await database.users.login(login)
|
||||
let user = try await database.users.get(token.userID)!
|
||||
authenticate(user)
|
||||
let projects = try await database.projects.fetch(user.id, .init(page: 1, per: 25))
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
ProjectsTable(userID: user.id, projects: projects)
|
||||
}
|
||||
}
|
||||
case .signup(let route):
|
||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
case .project(let route):
|
||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
@@ -15,11 +36,11 @@ extension ViewController.Request {
|
||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
case .effectiveLength(let route):
|
||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
case .user(let route):
|
||||
return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
// case .user(let route):
|
||||
// return try await route.renderView(isHtmxRequest: isHtmxRequest)
|
||||
default:
|
||||
// FIX: FIX
|
||||
return _render(isHtmxRequest: false) {
|
||||
return try await _render(isHtmxRequest: false) {
|
||||
div { "Fix me!" }
|
||||
}
|
||||
}
|
||||
@@ -27,11 +48,29 @@ extension ViewController.Request {
|
||||
}
|
||||
|
||||
extension SiteRoute.View.ProjectRoute {
|
||||
|
||||
private var shouldShowSidebar: Bool {
|
||||
switch self {
|
||||
case .index, .page: return false
|
||||
default: return true
|
||||
}
|
||||
}
|
||||
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
_render(isHtmxRequest: isHtmxRequest) {
|
||||
@Dependency(\.database.projects) var projects
|
||||
|
||||
return try await _render(
|
||||
isHtmxRequest: isHtmxRequest,
|
||||
showSidebar: shouldShowSidebar
|
||||
) {
|
||||
switch self {
|
||||
case .index:
|
||||
ProjectView(project: .mock)
|
||||
// ProjectView(project: .mock)
|
||||
let page = try await projects.fetch(UUID(0), .init(page: 1, per: 25))
|
||||
ProjectsTable(userID: UUID(0), projects: page)
|
||||
case .page(let page, let limit):
|
||||
let page = try await projects.fetch(UUID(0), .init(page: page, per: limit))
|
||||
ProjectsTable.Rows(projects: page)
|
||||
case .form(let dismiss):
|
||||
ProjectForm(dismiss: dismiss)
|
||||
case .create:
|
||||
@@ -47,7 +86,7 @@ extension SiteRoute.View.RoomRoute {
|
||||
case .form(let dismiss):
|
||||
return RoomForm(dismiss: dismiss)
|
||||
case .index:
|
||||
return _render(isHtmxRequest: isHtmxRequest, active: .rooms) {
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, active: .rooms) {
|
||||
RoomsView(rooms: Room.mocks)
|
||||
}
|
||||
}
|
||||
@@ -58,7 +97,7 @@ extension SiteRoute.View.FrictionRateRoute {
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
switch self {
|
||||
case .index:
|
||||
return _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) {
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, active: .frictionRate) {
|
||||
FrictionRateView()
|
||||
}
|
||||
case .form(let type, let dismiss):
|
||||
@@ -89,7 +128,7 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
switch self {
|
||||
case .index:
|
||||
return _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) {
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, active: .effectiveLength) {
|
||||
EffectiveLengthsView(effectiveLengths: EffectiveLength.mocks)
|
||||
}
|
||||
case .form(let dismiss):
|
||||
@@ -106,51 +145,66 @@ extension SiteRoute.View.EffectiveLengthRoute {
|
||||
}
|
||||
}
|
||||
|
||||
extension SiteRoute.View.UserRoute {
|
||||
extension SiteRoute.View.SignupRoute {
|
||||
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
switch self {
|
||||
// case .login(.index):
|
||||
// return _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
// LoginForm()
|
||||
// }
|
||||
case .signup(.index):
|
||||
return _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
LoginForm(style: .signup)
|
||||
}
|
||||
default:
|
||||
return div { "Fix Me!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@Dependency(\.database.users) var users
|
||||
|
||||
extension SiteRoute.View.LoginRoute {
|
||||
func renderView(isHtmxRequest: Bool) async throws -> AnySendableHTML {
|
||||
_render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
switch self {
|
||||
case .index:
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
LoginForm(style: .signup)
|
||||
}
|
||||
case .submit(let request):
|
||||
_ = try await users.create(request)
|
||||
// FIX: We should just login the new user at this point.
|
||||
return try await _render(isHtmxRequest: isHtmxRequest, showSidebar: false) {
|
||||
LoginForm()
|
||||
case .submit:
|
||||
// FIX:
|
||||
div { "Fix me!" }
|
||||
}
|
||||
}
|
||||
|
||||
// default:
|
||||
// return div { "Fix Me!" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extension SiteRoute.View.LoginRoute {
|
||||
// func renderView(on req: ViewController.Request) async throws -> AnySendableHTML {
|
||||
//
|
||||
// @Dependency(\.database) var database
|
||||
//
|
||||
// return try await _render(isHtmxRequest: req.isHtmxRequest, showSidebar: false) {
|
||||
// switch self {
|
||||
// case .index:
|
||||
// LoginForm()
|
||||
// case .submit(let login):
|
||||
// // FIX:
|
||||
// // div { "Logged in Success! Fix me!" }
|
||||
// let token = try await database.users.login(login)
|
||||
// let user = try await database.users.get(token.userID)!
|
||||
// _ = req.authenticate(user)
|
||||
// // req.authenticate(user)
|
||||
// let page = try await database.projects.fetch(user.id, .init(page: 1, per: 25))
|
||||
// ProjectsTable(userID: user.id, projects: page)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private func _render<C: HTML>(
|
||||
isHtmxRequest: Bool,
|
||||
active activeTab: Sidebar.ActiveTab = .projects,
|
||||
showSidebar: Bool = true,
|
||||
@HTMLBuilder inner: () -> C
|
||||
) -> AnySendableHTML where C: Sendable {
|
||||
@HTMLBuilder inner: () async throws -> C
|
||||
) async throws -> AnySendableHTML where C: Sendable {
|
||||
let inner = try await inner()
|
||||
if isHtmxRequest {
|
||||
return inner()
|
||||
return inner
|
||||
}
|
||||
return MainPage(
|
||||
active: activeTab,
|
||||
showSidebar: showSidebar
|
||||
) {
|
||||
inner()
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
80
Sources/ViewController/Views/Project/ProjectsTable.swift
Normal file
80
Sources/ViewController/Views/Project/ProjectsTable.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Fluent
|
||||
import ManualDCore
|
||||
import Styleguide
|
||||
import Vapor
|
||||
|
||||
struct ProjectsTable: HTML, Sendable {
|
||||
|
||||
let userID: User.ID
|
||||
let projects: Page<Project>
|
||||
|
||||
init(userID: User.ID, projects: Page<Project>) {
|
||||
self.userID = userID
|
||||
self.projects = projects
|
||||
}
|
||||
|
||||
var body: some HTML {
|
||||
div {
|
||||
Row {
|
||||
h1(.class("text-2xl font-bold")) { "Projects" }
|
||||
div(
|
||||
.class("tooltip tooltip-left"),
|
||||
.data("tip", value: "Add project")
|
||||
) {
|
||||
button(
|
||||
.class("btn btn-primary w-[40px] text-2xl")
|
||||
) {
|
||||
"+"
|
||||
}
|
||||
}
|
||||
}
|
||||
.attributes(.class("pb-6"))
|
||||
|
||||
div(.class("overflow-x-auto rounded-box border")) {
|
||||
table(.class("table table-zebra")) {
|
||||
thead {
|
||||
tr {
|
||||
th { Label("Date") }
|
||||
th { Label("Name") }
|
||||
th { Label("Address") }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
Rows(projects: projects)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ProjectsTable {
|
||||
struct Rows: HTML, Sendable {
|
||||
let projects: Page<Project>
|
||||
|
||||
var body: some HTML {
|
||||
for project in projects.items {
|
||||
tr(.id("\(project.id)")) {
|
||||
td { "\(project.createdAt)" }
|
||||
td { "\(project.name)" }
|
||||
td { "\(project.streetAddress)" }
|
||||
}
|
||||
}
|
||||
// Have a row that when revealed fetches the next page,
|
||||
// if there are more pages left.
|
||||
if projects.metadata.pageCount > projects.metadata.page {
|
||||
tr(
|
||||
.hx.get(route: .project(.page(page: projects.metadata.page + 1, limit: 25))),
|
||||
.hx.trigger(.event(.revealed)),
|
||||
.hx.swap(.outerHTML),
|
||||
.hx.target("this"),
|
||||
.hx.indicator("next .htmx-indicator")
|
||||
) {
|
||||
Indicator(size: .lg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,9 @@ struct LoginForm: HTML, Sendable {
|
||||
.id("loginForm"),
|
||||
.class("flex items-center justify-center")
|
||||
) {
|
||||
form {
|
||||
form(
|
||||
.method(.post)
|
||||
) {
|
||||
fieldset(.class("fieldset bg-base-200 border-base-300 rounded-box w-xl border p-4")) {
|
||||
legend(.class("fieldset-legend")) { style.title }
|
||||
|
||||
@@ -24,6 +26,7 @@ struct LoginForm: HTML, Sendable {
|
||||
SVG(.user)
|
||||
input(
|
||||
.type(.text), .required, .placeholder("Username"),
|
||||
.name("username"), .id("username"),
|
||||
.minlength("3"), .pattern(.username)
|
||||
)
|
||||
}
|
||||
@@ -37,7 +40,8 @@ struct LoginForm: HTML, Sendable {
|
||||
label(.class("input validator w-full")) {
|
||||
SVG(.email)
|
||||
input(
|
||||
.type(.email), .placeholder("Email"), .required
|
||||
.type(.email), .placeholder("Email"), .required,
|
||||
.name("email"), .id("email"),
|
||||
)
|
||||
}
|
||||
div(.class("validator-hint hidden")) { "Enter valid email address." }
|
||||
@@ -46,7 +50,8 @@ struct LoginForm: HTML, Sendable {
|
||||
SVG(.key)
|
||||
input(
|
||||
.type(.password), .placeholder("Password"), .required,
|
||||
.pattern(.password), .minlength("8")
|
||||
.pattern(.password), .minlength("8"),
|
||||
.name("password"), .id("password"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,7 +60,8 @@ struct LoginForm: HTML, Sendable {
|
||||
SVG(.key)
|
||||
input(
|
||||
.type(.password), .placeholder("Confirm Password"), .required,
|
||||
.pattern(.password), .minlength("8")
|
||||
.pattern(.password), .minlength("8"),
|
||||
.name("confirmPassword"), .id("confirmPassword"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -75,7 +81,7 @@ struct LoginForm: HTML, Sendable {
|
||||
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)))
|
||||
.href(route: style == .signup ? .login(.index) : .signup(.index))
|
||||
) {
|
||||
style == .login ? "Sign Up" : "Login"
|
||||
}
|
||||
|
||||
2
justfile
2
justfile
@@ -7,7 +7,7 @@ run-css:
|
||||
@./tailwindcss -i input.css -o output.css --watch
|
||||
|
||||
run:
|
||||
@swift run App
|
||||
@SWIFT_BACTRACE=enable=no swift run App
|
||||
|
||||
build-docker:
|
||||
@podman build -f docker/Dockerfile.dev -t {{docker_image}}:dev .
|
||||
|
||||
1089
output.css
1089
output.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user