Everywhere: Implement HTMX todo page
This commit is contained in:
+10
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "928a4b649897cc0b7c73d0862d9400289c0503386d6f44998334e327c5931856",
|
||||
"originHash" : "d39a0a797e191acb95e23a306e38d51e93c56adbbcfd76f4476a7deecf94537b",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "async-http-client",
|
||||
@@ -37,6 +37,15 @@
|
||||
"version" : "0.4.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "elementary-htmx",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/sliemeobn/elementary-htmx.git",
|
||||
"state" : {
|
||||
"revision" : "3e09519a605be410e8332627b260cebaf74bb9fa",
|
||||
"version" : "0.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "fluent",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
+5
-3
@@ -17,14 +17,16 @@ let package = Package(
|
||||
.package(url: "https://github.com/apple/swift-nio.git", from: "2.76.1"),
|
||||
//
|
||||
.package(url: "https://github.com/vapor-community/vapor-elementary.git", from: "0.2.0"),
|
||||
//
|
||||
.package(url: "https://github.com/sliemeobn/elementary-htmx.git", from: "0.3.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
name: "App",
|
||||
dependencies: [
|
||||
// .product(name: "ElementaryHTMX", package: "elementary-htmx"),
|
||||
// .product(name: "ElementaryHTMXSSE", package: "elementary-htmx"),
|
||||
// .product(name: "ElementaryHTMXWS", package: "elementary-htmx"),
|
||||
.product(name: "ElementaryHTMX", package: "elementary-htmx"),
|
||||
.product(name: "ElementaryHTMXSSE", package: "elementary-htmx"),
|
||||
.product(name: "ElementaryHTMXWS", package: "elementary-htmx"),
|
||||
.product(name: "Fluent", package: "fluent"),
|
||||
.product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"),
|
||||
.product(name: "NIOCore", package: "swift-nio"),
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
tr.htmx-swapping td {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s ease-out;
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
* Website
|
||||
|
||||
Website, written in Swift 6.
|
||||
|
||||
* Libraries
|
||||
|
||||
- [[https://github.com/vapor/vapor][Vapor]] HTTP web framework
|
||||
- [[https://github.com/vapor/fluent.git][Fluent]] ORM
|
||||
- [[https://github.com/sliemeobn/elementary][Elementary]] HTML templating
|
||||
- [[https://github.com/bigskysoftware/htmx][htmx]] font-end JS
|
||||
@@ -0,0 +1,82 @@
|
||||
import Fluent
|
||||
import Vapor
|
||||
|
||||
struct TodoAPIController: RouteCollection {
|
||||
func boot(routes: RoutesBuilder) throws {
|
||||
let todos = routes.grouped("todos")
|
||||
|
||||
todos.get(use: index)
|
||||
todos.post(use: create)
|
||||
todos.group(":id") { todo in
|
||||
todo.get(use: show)
|
||||
todo.put(use: update)
|
||||
todo.delete(use: delete)
|
||||
}
|
||||
// todos.delete(":id", use: delete)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func index(req: Request) async throws -> [TodoDTO] {
|
||||
try await Todo.query(on: req.db).all().map { $0.toDTO() }
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func show(req: Request) async throws -> TodoDTO {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func create(req: Request) async throws -> TodoDTO {
|
||||
let todo = try req.content.decode(TodoDTO.self).toModel()
|
||||
|
||||
try await todo.save(on: req.db)
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func update(req: Request) async throws -> TodoDTO {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
|
||||
let updatedTodo = try req.content.decode(Todo.self)
|
||||
todo.title = updatedTodo.title
|
||||
try await todo.save(on: req.db)
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func delete(req: Request) async throws -> HTTPStatus {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
}
|
||||
|
||||
try await todo.delete(on: req.db)
|
||||
|
||||
return .noContent
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
private func hexToUUID(hex: String) -> UUID? {
|
||||
var uuid: String = hex.replacingOccurrences(of: "-", with: "")
|
||||
|
||||
guard uuid.count == 32 else { return nil }
|
||||
|
||||
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 8))
|
||||
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 12 + 1))
|
||||
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 16 + 2))
|
||||
uuid.insert("-", at: uuid.index(uuid.startIndex, offsetBy: 20 + 3))
|
||||
|
||||
return UUID(uuidString: uuid)
|
||||
}
|
||||
}
|
||||
@@ -1,60 +1,63 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import ElementaryHTMXSSE
|
||||
import ElementaryHTMXWS
|
||||
import Fluent
|
||||
import Vapor
|
||||
import VaporElementary
|
||||
|
||||
struct TodoController: RouteCollection {
|
||||
func boot(routes: RoutesBuilder) throws {
|
||||
let todos = routes.grouped("todos")
|
||||
routes.group("todos") { todos in
|
||||
todos.get(use: index)
|
||||
|
||||
todos.get(use: index)
|
||||
todos.post(use: create)
|
||||
todos.group(":id") { todo in
|
||||
todo.get(use: show)
|
||||
todo.put(use: update)
|
||||
todo.delete(use: delete)
|
||||
todos.post(use: create)
|
||||
|
||||
todos.group(":id") { todo in
|
||||
todo.delete(use: delete)
|
||||
}
|
||||
}
|
||||
// todos.delete(":todoID", use: delete)
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func index(req: Request) async throws -> [TodoDTO] {
|
||||
try await Todo.query(on: req.db).all().map { $0.toDTO() }
|
||||
func index(req: Request) async throws -> HTMLResponse {
|
||||
let todos = try await todos(db: req.db)
|
||||
|
||||
return HTMLResponse {
|
||||
MainLayout(title: "Todos") {
|
||||
TodosTableComponent(name: "todos", todos: todos)
|
||||
TodosFormComponent(name: "todos-form", target: "todos")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func show(req: Request) async throws -> TodoDTO {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
func create(req: Request) async throws -> HTMLResponse {
|
||||
do {
|
||||
try TodoDTO.validate(content: req)
|
||||
}
|
||||
catch let error as ValidationsError {
|
||||
return HTMLResponse {
|
||||
TodosFormComponent(name: "todos-form", target: "todos", errors: ["title": error.description])
|
||||
}
|
||||
}
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func create(req: Request) async throws -> TodoDTO {
|
||||
let todo = try req.content.decode(TodoDTO.self).toModel()
|
||||
|
||||
try await todo.save(on: req.db)
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
let todos = try await todos(db: req.db)
|
||||
|
||||
@Sendable
|
||||
func update(req: Request) async throws -> TodoDTO {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
return HTMLResponse {
|
||||
// Return the empty form
|
||||
TodosFormComponent(name: "todos-form", target: "todos")
|
||||
|
||||
// Also update the todos table
|
||||
TodosTableComponent(name: "todos", todos: todos, refresh: true) // TODO: Put component names inside variables
|
||||
}
|
||||
|
||||
let updatedTodo = try req.content.decode(Todo.self)
|
||||
todo.title = updatedTodo.title
|
||||
try await todo.save(on: req.db)
|
||||
|
||||
return todo.toDTO()
|
||||
}
|
||||
|
||||
@Sendable
|
||||
func delete(req: Request) async throws -> HTTPStatus {
|
||||
func delete(req: Request) async throws -> HTMLResponse {
|
||||
guard let uuid = hexToUUID(hex: req.parameters.get("id")!),
|
||||
let todo = try await Todo.find(uuid, on: req.db) else {
|
||||
throw Abort(.notFound)
|
||||
@@ -62,11 +65,16 @@ struct TodoController: RouteCollection {
|
||||
|
||||
try await todo.delete(on: req.db)
|
||||
|
||||
return .noContent
|
||||
return HTMLResponse {} // TODO: Return 204 No Content
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
@Sendable
|
||||
private func todos(db: any Database) async throws -> [TodoDTO] {
|
||||
try await Todo.query(on: db).all().map { $0.toDTO() }
|
||||
}
|
||||
|
||||
private func hexToUUID(hex: String) -> UUID? {
|
||||
var uuid: String = hex.replacingOccurrences(of: "-", with: "")
|
||||
|
||||
@@ -79,4 +87,5 @@ struct TodoController: RouteCollection {
|
||||
|
||||
return UUID(uuidString: uuid)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,3 +15,11 @@ struct TodoDTO: Content {
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
extension TodoDTO: Validatable {
|
||||
static func validations(_ validations: inout Validations) {
|
||||
validations.add(
|
||||
"title", as: String.self, is: !.empty, required: true,
|
||||
customFailureDescription: "Title is required")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
|
||||
// -----------------------------------------
|
||||
// Elementary
|
||||
|
||||
public extension HTMLAttribute {
|
||||
static func empty() -> HTMLAttribute {
|
||||
return HTMLAttribute(name: "", value: "")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
// ElementaryHTMX
|
||||
|
||||
public extension HTMLAttribute.hx {
|
||||
static func swap(_ value: HTMLAttributeValue.HTMX.ModifiedSwapTarget, _ spec: String? = nil) -> HTMLAttribute {
|
||||
if let spec {
|
||||
.init(name: "hx-swap", value: "\(value.rawValue) \(spec)")
|
||||
} else {
|
||||
.init(name: "hx-swap", value: value.rawValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Elementary
|
||||
import Fluent
|
||||
|
||||
struct TodosFormComponent: HTML {
|
||||
|
||||
var name: String
|
||||
var target: String
|
||||
var errors: [String: String] = [:]
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
var content: some HTML {
|
||||
div(.id("cdiv_" + name)) {
|
||||
form(
|
||||
.id("\(name)-form"),
|
||||
.hx.post("/\(target)"), .hx.target("#cdiv_" + name), .hx.swap(.outerHTML)
|
||||
) {
|
||||
div(.class("row")) {
|
||||
div(.class("col-11")) {
|
||||
input(
|
||||
.class("form-control"), .type(.text), .id("\(name)-title"),
|
||||
.name("title"), .placeholder("Title")) // .required
|
||||
}
|
||||
div(.class("col-1")) {
|
||||
button(.class("btn btn-success"), .type(.submit)) { "Add" }
|
||||
}
|
||||
}
|
||||
if let error = errors["title"] {
|
||||
div(.class("form-text text-danger px-2")) { "\(error)." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import Elementary
|
||||
import ElementaryHTMX
|
||||
import Fluent
|
||||
|
||||
struct TodosTableComponent: HTML {
|
||||
|
||||
var name: String
|
||||
var todos: [TodoDTO]
|
||||
var refresh: Bool = false
|
||||
|
||||
// -------------------------------------
|
||||
|
||||
var content: some HTML {
|
||||
div(.id("cdiv_" + name), refresh ? .hx.swapOOB(.outerHTML) : .empty()) {
|
||||
table(.class("table")) {
|
||||
thead {
|
||||
tr {
|
||||
th { "#" }
|
||||
th { "ID" }
|
||||
th { "Title" }
|
||||
th { "Modifier" }
|
||||
}
|
||||
}
|
||||
tbody(
|
||||
.hx.confirm("Are you sure?"), .hx.target("closest tr"),
|
||||
.hx.swap(.outerHTML, "swap:0.5s")
|
||||
) {
|
||||
for (index, todo) in todos.enumerated() {
|
||||
tr {
|
||||
td { "\(index)" }
|
||||
td { todo.id?.uuidString ?? "" }
|
||||
td { todo.title ?? "" }
|
||||
td {
|
||||
if let id = todo.id {
|
||||
button(
|
||||
.class("btn btn-danger"),
|
||||
.hx.delete("/\(name)/\(id.uuidString)")
|
||||
) { "Delete" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,51 +8,51 @@ struct IndexPage: HTML {
|
||||
}
|
||||
p { "The navigation bar will stay at the top of the page as you scroll down." }
|
||||
p {
|
||||
"""
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt arcu
|
||||
sit amet leo rutrum luctus. Sed metus mi, consectetur vitae dui at, sodales
|
||||
dignissim odio. Curabitur a nisi eros. Suspendisse semper ac justo non gravida.
|
||||
Vestibulum accumsan interdum varius. Morbi at diam luctus, mattis mi nec, mollis
|
||||
libero. Pellentesque habitant morbi tristique senectus et netus et malesuada
|
||||
fames ac turpis egestas. Integer congue, nisl in tempus ultricies, ligula est
|
||||
laoreet elit, sed elementum erat dui ac nisl. Aenean pulvinar arcu eget urna
|
||||
venenatis, at dictum arcu ultrices. Etiam hendrerit, purus vitae sagittis
|
||||
lobortis, nisi sapien vestibulum arcu, ut mattis arcu elit ut arcu. Nulla vitae
|
||||
sem ac eros ullamcorper efficitur ut id arcu. Vestibulum euismod arcu eget
|
||||
aliquet tempor. Nullam nec consequat magna. Etiam posuere, ipsum id condimentum
|
||||
mollis, massa libero efficitur lectus, nec tempor mauris tellus ut ligula.
|
||||
Quisque viverra diam velit, quis ultricies nisl lacinia vitae.
|
||||
"""
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam tincidunt arcu
|
||||
sit amet leo rutrum luctus. Sed metus mi, consectetur vitae dui at, sodales
|
||||
dignissim odio. Curabitur a nisi eros. Suspendisse semper ac justo non gravida.
|
||||
Vestibulum accumsan interdum varius. Morbi at diam luctus, mattis mi nec, mollis
|
||||
libero. Pellentesque habitant morbi tristique senectus et netus et malesuada
|
||||
fames ac turpis egestas. Integer congue, nisl in tempus ultricies, ligula est
|
||||
laoreet elit, sed elementum erat dui ac nisl. Aenean pulvinar arcu eget urna
|
||||
venenatis, at dictum arcu ultrices. Etiam hendrerit, purus vitae sagittis
|
||||
lobortis, nisi sapien vestibulum arcu, ut mattis arcu elit ut arcu. Nulla vitae
|
||||
sem ac eros ullamcorper efficitur ut id arcu. Vestibulum euismod arcu eget
|
||||
aliquet tempor. Nullam nec consequat magna. Etiam posuere, ipsum id condimentum
|
||||
mollis, massa libero efficitur lectus, nec tempor mauris tellus ut ligula.
|
||||
Quisque viverra diam velit, quis ultricies nisl lacinia vitae.
|
||||
|
||||
Aliquam libero nibh, luctus vel augue at, congue feugiat risus. Mauris volutpat
|
||||
eget eros ac congue. Duis venenatis, arcu vel sodales accumsan, diam mi posuere
|
||||
mi, vitae rutrum ex mauris nec libero. Sed eleifend nulla magna, eu lobortis
|
||||
mauris fringilla nec. In posuere dignissim eros, ut hendrerit quam lacinia eu.
|
||||
Praesent vestibulum arcu enim, hendrerit convallis risus facilisis eu. Nunc
|
||||
vitae mauris eu nulla laoreet rhoncus. Nullam ligula tellus, vulputate in
|
||||
viverra nec, eleifend eu nulla. Praesent suscipit rutrum imperdiet.
|
||||
Aliquam libero nibh, luctus vel augue at, congue feugiat risus. Mauris volutpat
|
||||
eget eros ac congue. Duis venenatis, arcu vel sodales accumsan, diam mi posuere
|
||||
mi, vitae rutrum ex mauris nec libero. Sed eleifend nulla magna, eu lobortis
|
||||
mauris fringilla nec. In posuere dignissim eros, ut hendrerit quam lacinia eu.
|
||||
Praesent vestibulum arcu enim, hendrerit convallis risus facilisis eu. Nunc
|
||||
vitae mauris eu nulla laoreet rhoncus. Nullam ligula tellus, vulputate in
|
||||
viverra nec, eleifend eu nulla. Praesent suscipit rutrum imperdiet.
|
||||
|
||||
Curabitur in lacus eu diam cursus viverra non eget turpis. Donec a ornare ipsum,
|
||||
sed egestas orci. Vivamus congue gravida elementum. Pellentesque vitae mauris
|
||||
magna. Phasellus blandit urna vitae auctor consectetur. Aenean iaculis eget arcu
|
||||
vitae ultricies. Nunc maximus, massa hendrerit faucibus fringilla, eros quam
|
||||
consequat enim, sit amet sodales erat quam eget massa.
|
||||
Curabitur in lacus eu diam cursus viverra non eget turpis. Donec a ornare ipsum,
|
||||
sed egestas orci. Vivamus congue gravida elementum. Pellentesque vitae mauris
|
||||
magna. Phasellus blandit urna vitae auctor consectetur. Aenean iaculis eget arcu
|
||||
vitae ultricies. Nunc maximus, massa hendrerit faucibus fringilla, eros quam
|
||||
consequat enim, sit amet sodales erat quam eget massa.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in venenatis
|
||||
urna. Vestibulum lectus arcu, scelerisque in ipsum vitae, feugiat cursus tortor.
|
||||
Maecenas aliquam nunc enim, id fringilla est mattis et. Vivamus vitae
|
||||
ullamcorper erat. Sed quis vehicula felis, quis bibendum nunc. Duis semper
|
||||
fermentum ante, id fermentum neque varius convallis. Donec dui leo, fringilla
|
||||
nec massa nec, fermentum molestie purus. Donec eget feugiat velit. Nulla
|
||||
facilisi. Cras maximus felis eu libero mollis consectetur. Nulla molestie vitae
|
||||
neque venenatis porta.
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum in venenatis
|
||||
urna. Vestibulum lectus arcu, scelerisque in ipsum vitae, feugiat cursus tortor.
|
||||
Maecenas aliquam nunc enim, id fringilla est mattis et. Vivamus vitae
|
||||
ullamcorper erat. Sed quis vehicula felis, quis bibendum nunc. Duis semper
|
||||
fermentum ante, id fermentum neque varius convallis. Donec dui leo, fringilla
|
||||
nec massa nec, fermentum molestie purus. Donec eget feugiat velit. Nulla
|
||||
facilisi. Cras maximus felis eu libero mollis consectetur. Nulla molestie vitae
|
||||
neque venenatis porta.
|
||||
|
||||
Vestibulum nunc diam, mattis eu bibendum at, sagittis vitae nunc. Duis lacinia
|
||||
sodales enim, et elementum libero posuere et. Donec ut fringilla orci. Donec at
|
||||
aliquet ipsum, quis mattis risus. Donec malesuada enim in egestas blandit. Proin
|
||||
sagittis mauris magna, elementum faucibus metus tempus eu. Sed in sem ut tellus
|
||||
porta luctus et sed diam. Donec felis ante, euismod a est vitae, mollis
|
||||
condimentum nulla.
|
||||
"""
|
||||
Vestibulum nunc diam, mattis eu bibendum at, sagittis vitae nunc. Duis lacinia
|
||||
sodales enim, et elementum libero posuere et. Donec ut fringilla orci. Donec at
|
||||
aliquet ipsum, quis mattis risus. Donec malesuada enim in egestas blandit. Proin
|
||||
sagittis mauris magna, elementum faucibus metus tempus eu. Sed in sem ut tellus
|
||||
porta luctus et sed diam. Donec felis ante, euismod a est vitae, mollis
|
||||
condimentum nulla.
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Elementary
|
||||
|
||||
// https://www.srihash.org/
|
||||
|
||||
extension MainLayout: Sendable where Body: Sendable {}
|
||||
struct MainLayout<Body: HTML>: HTMLDocument {
|
||||
var title: String
|
||||
@HTMLBuilder var pageContent: Body // This var name can't be changed!
|
||||
|
||||
// https://www.srihash.org/
|
||||
var head: some HTML {
|
||||
meta(.charset(.utf8))
|
||||
meta(.name(.viewport), .content("width=device-width, initial-scale=1.0"))
|
||||
@@ -18,14 +19,17 @@ struct MainLayout<Body: HTML>: HTMLDocument {
|
||||
.href("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css"),
|
||||
.integrity("sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"),
|
||||
.crossorigin(.anonymous))
|
||||
link(.rel(.stylesheet), .href("/style.css"))
|
||||
link(.rel(.stylesheet), .href("/css/style.css"))
|
||||
|
||||
// ---------------------------------
|
||||
// Style
|
||||
|
||||
style {
|
||||
"""
|
||||
body {
|
||||
padding-top: 56px;
|
||||
}
|
||||
"""
|
||||
"""
|
||||
body {
|
||||
padding-top: 56px;
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +42,10 @@ struct MainLayout<Body: HTML>: HTMLDocument {
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Body
|
||||
// Content
|
||||
|
||||
main {
|
||||
div(.class("cotainer mt-4")) {
|
||||
div(.class("container mt-4")) {
|
||||
div(.class("content px-4 pb-4")) {
|
||||
pageContent
|
||||
}
|
||||
@@ -57,6 +61,21 @@ struct MainLayout<Body: HTML>: HTMLDocument {
|
||||
.integrity("sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"),
|
||||
.crossorigin(.anonymous)
|
||||
) {}
|
||||
script(
|
||||
.src("https://cdnjs.cloudflare.com/ajax/libs/htmx/2.0.3/htmx.min.js"),
|
||||
.integrity("sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq"),
|
||||
.crossorigin(.anonymous)
|
||||
) {}
|
||||
script(
|
||||
.src("https://cdn.jsdelivr.net/npm/htmx-ext-sse@2.2.2/sse.min.js"),
|
||||
.integrity("sha384-yhS+rWHB2hwrHEg86hWiQV7XL6u+PH9X+3BlmS2+CNBaGYU8Nd7RZ2rZ9DWXgTdr"),
|
||||
.crossorigin(.anonymous)
|
||||
) {}
|
||||
script(
|
||||
.src("https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.1/ws.min.js"),
|
||||
.integrity("sha384-yhWpPsq2os1hEnx1I8cH7Ius6rclwTm3G2fhXDLF6Pzv7UnSsXY7BAj4fB6PIgSz"),
|
||||
.crossorigin(.anonymous)
|
||||
) {}
|
||||
script(.src("/js/site.js")) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ struct NavMenu: HTML {
|
||||
div(.class("collapse navbar-collapse"), .id("navbarNav")) {
|
||||
ul(.class("navbar-nav me-auto mb-2 mb-lg-0")) {
|
||||
li(.class("nav-item")) {
|
||||
a(.class("nav-link active"), .href("#")) { "Home" }
|
||||
a(.class("nav-link active"), .href("/")) { "Home" }
|
||||
}
|
||||
li(.class("nav-item")) {
|
||||
a(.class("nav-link active"), .href("/todos")) { "Todos" }
|
||||
}
|
||||
li(.class("nav-item")) {
|
||||
a(.class("nav-link"), .href("#")) { "About" }
|
||||
|
||||
+10
-14
@@ -6,27 +6,23 @@ import VaporElementary
|
||||
func routes(_ app: Application) throws {
|
||||
app.routes.caseInsensitive = true
|
||||
|
||||
app.get { req async throws in
|
||||
|
||||
let todo = Todo(title: "Test Todo")
|
||||
try await todo.save(on: req.db)
|
||||
|
||||
return "It works!"
|
||||
app.get() { req async throws in
|
||||
HTMLResponse {
|
||||
MainLayout(title: "Homepage") {
|
||||
IndexPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.get("hello") { req async -> String in
|
||||
"Hello, world!"
|
||||
}
|
||||
|
||||
app.get("test") { _ in
|
||||
HTMLResponse {
|
||||
MainLayout(title: "Test123") {
|
||||
IndexPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try app.register(collection: TodoController())
|
||||
|
||||
try app.group("api") { api in
|
||||
try api.register(collection: TodoAPIController())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -36,7 +36,7 @@ struct AppTests {
|
||||
let sampleTodos = [Todo(title: "sample1"), Todo(title: "sample2")]
|
||||
try await sampleTodos.create(on: app.db)
|
||||
|
||||
try await app.test(.GET, "todos", afterResponse: { res async throws in
|
||||
try await app.test(.GET, "api/todos", afterResponse: { res async throws in
|
||||
#expect(res.status == .ok)
|
||||
#expect(try res.content.decode([TodoDTO].self) == sampleTodos.map { $0.toDTO()} )
|
||||
})
|
||||
@@ -48,7 +48,7 @@ struct AppTests {
|
||||
let newDTO = TodoDTO(id: nil, title: "test")
|
||||
|
||||
try await withApp { app in
|
||||
try await app.test(.POST, "todos", beforeRequest: { req in
|
||||
try await app.test(.POST, "api/todos", beforeRequest: { req in
|
||||
try req.content.encode(newDTO)
|
||||
}, afterResponse: { res async throws in
|
||||
#expect(res.status == .ok)
|
||||
@@ -66,7 +66,7 @@ struct AppTests {
|
||||
try await withApp { app in
|
||||
try await testTodos.create(on: app.db)
|
||||
|
||||
try await app.test(.DELETE, "todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
|
||||
try await app.test(.DELETE, "api/todos/\(testTodos[0].requireID())", afterResponse: { res async throws in
|
||||
#expect(res.status == .noContent)
|
||||
let model = try await Todo.find(testTodos[0].id, on: app.db)
|
||||
#expect(model == nil)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
GET http://localhost:8080/todos
|
||||
GET http://localhost:8080/api/todos
|
||||
-> jq-set-var :id .[0].id
|
||||
Accept: application/json
|
||||
|
||||
@@ -22,12 +22,12 @@ Accept: application/json
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
GET http://localhost:8080/todos/:id
|
||||
GET http://localhost:8080/api/todos/:id
|
||||
Accept: application/json
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
POST http://localhost:8080/todos
|
||||
POST http://localhost:8080/api/todos
|
||||
-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read))))
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -38,7 +38,7 @@ Content-Type: application/json
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
PUT http://localhost:8080/todos/:id
|
||||
PUT http://localhost:8080/api/todos/:id
|
||||
-> run-hook (restclient-set-var ":id" (cdr (assq 'id (json-read))))
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
@@ -55,7 +55,7 @@ Content-Type: application/json
|
||||
# INSERT(INSERT(INSERT(INSERT(HEX(id), 9, 0, '-'), 14, 0, '-'), 19, 0, '-'), 24, 0, '-') AS id,
|
||||
# title FROM riyyi.todos;
|
||||
|
||||
DELETE http://localhost:8080/todos/:id
|
||||
DELETE http://localhost:8080/api/todos/:id
|
||||
Accept: application/json
|
||||
|
||||
# ------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user