Compare commits
	
		
			5 Commits 
		
	
	
		
			94cef15048
			...
			293503d7a2
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						293503d7a2 | 12 months ago | 
| 
							
							
								 | 
						f0cb958ea2 | 12 months ago | 
| 
							
							
								 | 
						7f1e78fa08 | 12 months ago | 
| 
							
							
								 | 
						6b93045514 | 12 months ago | 
| 
							
							
								 | 
						56dcd4fb35 | 12 months ago | 
				 25 changed files with 747 additions and 149 deletions
			
			
		@ -0,0 +1,18 @@ | 
				
			|||||||
 | 
					/* -------------------------------------- */ | 
				
			||||||
 | 
					/* Toast */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toast-container { | 
				
			||||||
 | 
						position: fixed; | 
				
			||||||
 | 
						bottom: 30px; | 
				
			||||||
 | 
						left: 50%; | 
				
			||||||
 | 
						transform: translateX(-50%); | 
				
			||||||
 | 
						z-index: 999; | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* -------------------------------------- */ | 
				
			||||||
 | 
					/* Table */ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					tr.htmx-swapping td { | 
				
			||||||
 | 
						opacity: 0; | 
				
			||||||
 | 
						transition: opacity 0.5s ease-out; | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,20 @@ | 
				
			|||||||
 | 
					window.web = {}; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// -----------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Activate Bootstrap tooltips
 | 
				
			||||||
 | 
					web.tooltips = function() { | 
				
			||||||
 | 
						const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]') | 
				
			||||||
 | 
						const tooltipList = [...tooltipTriggerList].map(element => new bootstrap.Tooltip(element)) | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// -----------------------------------------
 | 
				
			||||||
 | 
					// HTMX events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					web.afterLoad = function(func) { | 
				
			||||||
 | 
						// https://htmx.org/docs/#request-operations
 | 
				
			||||||
 | 
						document.addEventListener("htmx:afterSettle", function handler(event) { | 
				
			||||||
 | 
					        func(); | 
				
			||||||
 | 
					        document.removeEventListener("htmx:afterSwap", handler); | 
				
			||||||
 | 
					    }); | 
				
			||||||
 | 
					} | 
				
			||||||
@ -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]] front-end JS | 
				
			||||||
@ -0,0 +1,84 @@ | 
				
			|||||||
 | 
					import Fluent | 
				
			||||||
 | 
					import Vapor | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// The code in this file is unused | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,30 @@ | 
				
			|||||||
 | 
					import Elementary | 
				
			||||||
 | 
					import ElementaryHTMX | 
				
			||||||
 | 
					import ElementaryHTMXSSE | 
				
			||||||
 | 
					import ElementaryHTMXWS | 
				
			||||||
 | 
					import Fluent | 
				
			||||||
 | 
					import Vapor | 
				
			||||||
 | 
					import VaporElementary | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TestController: RouteCollection { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func boot(routes: RoutesBuilder) throws { | 
				
			||||||
 | 
					        routes.group("test") { test in | 
				
			||||||
 | 
								test.get("toast", use: toast) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Sendable | 
				
			||||||
 | 
					    func toast(req: Request) async throws -> HTMLResponse { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = try getState(request: req) | 
				
			||||||
 | 
					        state.toast = ToastState(message: "Wow!", | 
				
			||||||
 | 
					                                 title: "This is my title", | 
				
			||||||
 | 
					                                 level: ToastState.Level.success) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        throw Abort(.badRequest, headers: ["HX-Trigger": "toast"]) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // return HTMLResponse { } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -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,54 @@ | 
				
			|||||||
 | 
					import Vapor | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public func getState(request: Request) throws -> UserState { | 
				
			||||||
 | 
					    guard let state =  request.storage[UserStateKey.self] else { | 
				
			||||||
 | 
					        throw Abort(.internalServerError) | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return state | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public final class StateMiddleware: AsyncMiddleware { | 
				
			||||||
 | 
					    public func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This code is run *before* the route endpoint code | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        var setCookie: Bool = true | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let uuid: UUID | 
				
			||||||
 | 
					        if let sessionID = request.cookies["SWIFTSESSID"]?.string, | 
				
			||||||
 | 
					           let sessionUUID = UUID(uuidString: sessionID) { | 
				
			||||||
 | 
					            setCookie = false | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            uuid = sessionUUID | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if !request.application.manager.states.keys.contains(uuid.uuidString) { | 
				
			||||||
 | 
					                request.application.manager.states[uuid.uuidString] = UserState() | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					            uuid = UUID() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Register a new user state into the application storage | 
				
			||||||
 | 
					            // https://docs.vapor.codes/advanced/services/ | 
				
			||||||
 | 
					            request.application.manager.states[uuid.uuidString] = UserState() | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Provide the user state to the request | 
				
			||||||
 | 
					        request.storage[UserStateKey.self] = request.application.manager.states[uuid.uuidString] | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let response = try await next.respond(to: request) | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This code is run *after* the route endpoint code | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if setCookie { | 
				
			||||||
 | 
					            response.cookies["SWIFTSESSID"] = HTTPCookies.Value( | 
				
			||||||
 | 
					              string: uuid.uuidString, | 
				
			||||||
 | 
					              path: "/", | 
				
			||||||
 | 
					              isSecure: true, | 
				
			||||||
 | 
					              isHTTPOnly: true | 
				
			||||||
 | 
					            ) | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,15 @@ | 
				
			|||||||
 | 
					public struct ToastState: Sendable { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    enum Level: String { | 
				
			||||||
 | 
					        case success = "success" | 
				
			||||||
 | 
					        case error = "danger" | 
				
			||||||
 | 
					        case warning = "warning" | 
				
			||||||
 | 
					        case info = "info" | 
				
			||||||
 | 
					        case verbose = "secondary" | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var message: String = "" | 
				
			||||||
 | 
					    var title: String = "" | 
				
			||||||
 | 
					    var level: Level = Level.verbose | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,33 @@ | 
				
			|||||||
 | 
					import Vapor | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ----------------------------------------- | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public final class UserState: @unchecked Sendable { | 
				
			||||||
 | 
						var toast: ToastState = ToastState() | 
				
			||||||
 | 
					    var todos: TodosState = TodosState() | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ----------------------------------------- | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct UserStateKey: StorageKey { | 
				
			||||||
 | 
					    typealias Value = UserState | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct UserStateManager: Sendable { | 
				
			||||||
 | 
						var states: [String: UserState] = [:] | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct UserStateManagerKey : StorageKey { | 
				
			||||||
 | 
						typealias Value = UserStateManager | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extension Application { | 
				
			||||||
 | 
					    var manager: UserStateManager { | 
				
			||||||
 | 
					        get { | 
				
			||||||
 | 
					            self.storage[UserStateManagerKey.self]! | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        set { | 
				
			||||||
 | 
					            self.storage[UserStateManagerKey.self] = newValue | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,51 @@ | 
				
			|||||||
 | 
					import Elementary | 
				
			||||||
 | 
					import ElementaryHTMX | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Usage: | 
				
			||||||
 | 
					// | 
				
			||||||
 | 
					// let state = try getState(request: req) | 
				
			||||||
 | 
					// state.toast = ToastState(message: "", title: "", level: ToastState.Level.error) | 
				
			||||||
 | 
					// throw Abort(.badRequest, headers: ["HX-Trigger": "toast"]) | 
				
			||||||
 | 
					// | 
				
			||||||
 | 
					// The header "HX-Trigger" will make the part refresh and show the toast message | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ToastComponent: HTML { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var state: ToastState = ToastState() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ------------------------------------- | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var content: some HTML { | 
				
			||||||
 | 
					        div( | 
				
			||||||
 | 
					            .class("toast-container"), .id("cdiv_toast"), | 
				
			||||||
 | 
					            .hx.get("/toast"), | 
				
			||||||
 | 
					            .hx.trigger(HTMLAttributeValue.HTMX.EventTrigger(rawValue: "toast from:body")), | 
				
			||||||
 | 
					            .hx.swap(.outerHTML) | 
				
			||||||
 | 
					        ) { | 
				
			||||||
 | 
					            div(.class("toast"), .id("toast")) { | 
				
			||||||
 | 
					                div(.class("toast-header bg-\(state.level.rawValue) text-white")) { | 
				
			||||||
 | 
					                    strong(.class("me-auto")) { state.title } | 
				
			||||||
 | 
					                    button( | 
				
			||||||
 | 
					                        .class("btn-close btn-close-white"), .type(.button), | 
				
			||||||
 | 
					                        .data("bs-dismiss", value: "toast") | 
				
			||||||
 | 
					                    ) {} | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                div(.class("toast-body")) { state.message } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            if !state.message.isEmpty { | 
				
			||||||
 | 
					                ScriptAfterLoad(initial: false) { | 
				
			||||||
 | 
					                    """ | 
				
			||||||
 | 
					                    const element = document.getElementById("toast"); | 
				
			||||||
 | 
					                    const toast = new bootstrap.Toast(element, { autohide: true, delay: 5000 }); | 
				
			||||||
 | 
					                    toast.show(); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    element.addEventListener("hidden.bs.toast", function () { | 
				
			||||||
 | 
					                        element.remove(); | 
				
			||||||
 | 
					                    }); | 
				
			||||||
 | 
					                    """ | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
@ -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,84 @@ | 
				
			|||||||
 | 
					import Elementary | 
				
			||||||
 | 
					import ElementaryHTMX | 
				
			||||||
 | 
					import Fluent | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct TodosTableComponent: HTML { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var state: TodosTableState = TodosTableState() | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ------------------------------------- | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var content: some HTML { | 
				
			||||||
 | 
					        div(.id("cdiv_" + state.name), state.refresh ? .hx.swapOOB(.outerHTML) : .empty()) { | 
				
			||||||
 | 
					            table(.class("table")) { | 
				
			||||||
 | 
					                thead { | 
				
			||||||
 | 
					                    tr { | 
				
			||||||
 | 
					                        th { "#" } | 
				
			||||||
 | 
					                        th { "ID" } | 
				
			||||||
 | 
					                        th { | 
				
			||||||
 | 
					                            let order = state.sort["title"]?.description ?? "ascending" | 
				
			||||||
 | 
					                            span(.style("cursor: pointer;"), | 
				
			||||||
 | 
					                                 .hx.get("/\(state.name)/sort?title=\(order == "descending" ? "ascending" : "descending")"), | 
				
			||||||
 | 
					                                 .hx.target("closest div")) { | 
				
			||||||
 | 
					                                "Title " | 
				
			||||||
 | 
					                                i(.class("bi bi-arrow-\(order == "descending" ? "down" : "up")-circle")) {} | 
				
			||||||
 | 
					                            } | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                        th { "Modifier" } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					                tbody( | 
				
			||||||
 | 
					                    .hx.confirm("Are you sure?"), .hx.target("closest tr"), | 
				
			||||||
 | 
					                    .hx.swap(.outerHTML, "swap:0.5s") | 
				
			||||||
 | 
					                ) { | 
				
			||||||
 | 
					                    for (index, todo) in state.todos.enumerated() { | 
				
			||||||
 | 
					                        tr { | 
				
			||||||
 | 
					                            td { "\(index)" } | 
				
			||||||
 | 
					                            td { todo.id?.uuidString ?? "" } | 
				
			||||||
 | 
					                            td { todo.title ?? "" } | 
				
			||||||
 | 
					                            td { | 
				
			||||||
 | 
					                                if let id = todo.id { | 
				
			||||||
 | 
					                                    i( | 
				
			||||||
 | 
					                                        .class("bi bi-trash3 text-danger"), | 
				
			||||||
 | 
					                                        .data("bs-toggle", value: "tooltip"), | 
				
			||||||
 | 
					                                        .data("bs-title", value: "Delete"), | 
				
			||||||
 | 
					                                        .hx.delete("/\(state.name)/\(id.uuidString)") | 
				
			||||||
 | 
					                                    ) {} | 
				
			||||||
 | 
					                                } | 
				
			||||||
 | 
					                            } | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            ScriptAfterLoad(initial: !state.refresh) { "web.tooltips();" }; | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct Modal: HTML { | 
				
			||||||
 | 
					    var content: some HTML { | 
				
			||||||
 | 
					        div(.class("modal fade"), .id("alertModal"), .tabindex(-1)) { | 
				
			||||||
 | 
					            div(.class("modal-dialog modal-dialog-centered")) { | 
				
			||||||
 | 
					                div(.class("modal-content")) { | 
				
			||||||
 | 
					                    div(.class("modal-header border-bottom-0")) { | 
				
			||||||
 | 
					                        button( | 
				
			||||||
 | 
					                            .class("btn-close"), .type(.button), .data("bs-dismiss", value: "modal") | 
				
			||||||
 | 
					                        ) {} | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                    div(.class("modal-body")) { | 
				
			||||||
 | 
					                        div(.class("alert alert-danger")) { | 
				
			||||||
 | 
					                            "Toby123" | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					        button( | 
				
			||||||
 | 
					            .class("btn btn-primary"), .type(.button), .data("bs-toggle", value: "modal"), | 
				
			||||||
 | 
					            .data("bs-target", value: "#alertModal") | 
				
			||||||
 | 
					        ) { "Show" } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -0,0 +1,35 @@ | 
				
			|||||||
 | 
					import Elementary | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ScriptAfterLoad: HTML { | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var initial: Bool = false | 
				
			||||||
 | 
					    var js: String = "" | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(initial: Bool = false, js: () -> String) { | 
				
			||||||
 | 
					        self.initial = initial | 
				
			||||||
 | 
					        self.js = js() | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ------------------------------------- | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var content: some HTML { | 
				
			||||||
 | 
					        if initial { | 
				
			||||||
 | 
					            script { | 
				
			||||||
 | 
					                """ | 
				
			||||||
 | 
					                document.addEventListener("DOMContentLoaded", function() { | 
				
			||||||
 | 
					                    \(js) | 
				
			||||||
 | 
					                }); | 
				
			||||||
 | 
					                """ | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } else { | 
				
			||||||
 | 
					            script { | 
				
			||||||
 | 
					                """ | 
				
			||||||
 | 
					                web.afterLoad(function () { | 
				
			||||||
 | 
					                    \(js) | 
				
			||||||
 | 
					                }); | 
				
			||||||
 | 
					                """ | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					        } | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					} | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue