diff --git a/src/main/scala/Main.scala b/src/main/scala/Main.scala index c21bdfd..9c767e0 100644 --- a/src/main/scala/Main.scala +++ b/src/main/scala/Main.scala @@ -1,162 +1,10 @@ -import com.sun.net.httpserver.{HttpExchange, HttpServer} -import java.net.InetSocketAddress -import java.net.URLDecoder -import java.nio.charset.StandardCharsets +import com.greenfossil.thorium.{*, given} object Main: - def main(args: Array[String]): Unit = val port = sys.env.getOrElse("PORT", "8080").toInt - val server = HttpServer.create(InetSocketAddress(port), 0) - - server.createContext("/health", exchange => { - val response = """{"status":"ok"}""" - exchange.getResponseHeaders.set("Content-Type", "application/json") - exchange.sendResponseHeaders(200, response.getBytes.length) - val os = exchange.getResponseBody - os.write(response.getBytes) - os.close() - }) - - server.createContext("/tasks", exchange => { - val method = exchange.getRequestMethod - val path = exchange.getRequestURI.getPath - - (method, path) match - case ("POST", "/tasks") => - val body = String(exchange.getRequestBody.readAllBytes(), StandardCharsets.UTF_8) - val title = parseFormField(body, "title") - TaskController.createTask(title) - redirect(exchange, "/") - - case ("POST", p) if p.endsWith("/toggle") => - val id = p.stripPrefix("/tasks/").stripSuffix("/toggle") - TaskController.toggleTask(id) - redirect(exchange, "/") - - case ("POST", p) if p.endsWith("/delete") => - val id = p.stripPrefix("/tasks/").stripSuffix("/delete") - TaskController.deleteTask(id) - redirect(exchange, "/") - - case _ => - exchange.sendResponseHeaders(404, -1) - exchange.close() - }) - - server.createContext("/", exchange => { - val path = exchange.getRequestURI.getPath - if path == "/" || path.isEmpty then - val html = renderPage() - exchange.getResponseHeaders.set("Content-Type", "text/html; charset=utf-8") - exchange.sendResponseHeaders(200, html.getBytes(StandardCharsets.UTF_8).length) - val os = exchange.getResponseBody - os.write(html.getBytes(StandardCharsets.UTF_8)) - os.close() - else - exchange.sendResponseHeaders(404, -1) - exchange.close() - }) - - server.setExecutor(null) - server.start() + Server(port) + .addServices(TaskController) + .start() println(s"demoapp started on port $port") println(s" app: ${SiteConfig.appName} v${SiteConfig.appVersion}") - - private def redirect(exchange: HttpExchange, location: String): Unit = - exchange.getResponseHeaders.set("Location", location) - exchange.sendResponseHeaders(303, -1) - exchange.close() - - private def parseFormField(body: String, field: String): String = - body.split("&").map(_.split("=", 2)).collectFirst { - case Array(k, v) if k == field => - URLDecoder.decode(v, StandardCharsets.UTF_8) - }.getOrElse("") - - private def esc(s: String): String = - s.replace("&", "&").replace("<", "<").replace(">", ">") - .replace("\"", """).replace("'", "'") - - private def renderPage(): String = - val cfg = SiteConfig - val tasks = TaskStore.all() - val totalCount = tasks.size - val doneCount = tasks.count(_.completed) - - val taskRows = tasks.map { t => - val checkedClass = if t.completed then "line-through text-gray-400" else "" - val checkIcon = if t.completed then "✓" else "" - s""" -
-
- -
- ${esc(t.title)} -
- -
-
""" - }.mkString("\n") - - s""" - - - - - ${esc(cfg.appName)} - - - - - -
-
-

- ${esc(cfg.appName)} -

- v${esc(cfg.appVersion)} -
-
- -
-
-
-

Tasks

- $doneCount / $totalCount completed -
- -
- $taskRows - ${if tasks.isEmpty then """

No tasks yet. Add one below!

""" else ""} -
-
- -
-

Add Task

-
- - -
-
- - -
- -""" diff --git a/src/main/scala/TaskController.scala b/src/main/scala/TaskController.scala index 8f20ebd..26492c2 100644 --- a/src/main/scala/TaskController.scala +++ b/src/main/scala/TaskController.scala @@ -1,14 +1,126 @@ +import com.greenfossil.thorium.{*, given} +import com.greenfossil.data.mapping.{*, given} +import com.linecorp.armeria.server.annotation.{Get, Post, Param} import java.util.UUID object TaskController: - def createTask(title: String): Unit = - if title.trim.nonEmpty then TaskStore.add(title) + private val titleForm = mapping[String]("title" -> text) - def toggleTask(id: String): Unit = + @Get("/health") + def health = Action: request => + Ok("""{"status":"ok"}""").as("application/json") + + @Get("/") + def index = Action: request => + Ok(renderPage()).as("text/html; charset=utf-8") + + @Post("/tasks") + def create = Action: implicit request => + titleForm.bindFromRequest().fold( + _ => Redirect("/"), + title => + if title.trim.nonEmpty then TaskStore.add(title) + Redirect("/") + ) + + @Post("/tasks/:id/toggle") + def toggle(@Param id: String) = Action: request => try TaskStore.toggle(UUID.fromString(id)) catch case _: IllegalArgumentException => () + Redirect("/") - def deleteTask(id: String): Unit = + @Post("/tasks/:id/delete") + def delete(@Param id: String) = Action: request => try TaskStore.delete(UUID.fromString(id)) catch case _: IllegalArgumentException => () + Redirect("/") + + // ── rendering helpers (moved from Main) ────────────────────────────────── + + private def esc(s: String): String = + s.replace("&", "&").replace("<", "<").replace(">", ">") + .replace("\"", """).replace("'", "'") + + private def renderPage(): String = + val cfg = SiteConfig + val tasks = TaskStore.all() + val totalCount = tasks.size + val doneCount = tasks.count(_.completed) + + val taskRows = tasks.map { t => + val checkedClass = if t.completed then "line-through text-gray-400" else "" + val checkIcon = if t.completed then "✓" else "" + s""" +
+
+ +
+ ${esc(t.title)} +
+ +
+
""" + }.mkString("\n") + + s""" + + + + + ${esc(cfg.appName)} + + + + + +
+
+

+ ${esc(cfg.appName)} +

+ v${esc(cfg.appVersion)} +
+
+ +
+
+
+

Tasks

+ $doneCount / $totalCount completed +
+ +
+ $taskRows + ${if tasks.isEmpty then """

No tasks yet. Add one below!

""" else ""} +
+
+ +
+

Add Task

+
+ + +
+
+ + +
+ +"""