diff --git a/.gitignore b/.gitignore index c426c32..86b99be 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ +.kotlin + ### IntelliJ IDEA ### .idea *.iws diff --git a/.kotlin/sessions/kotlin-compiler-18391528531158821791.salive b/.kotlin/sessions/kotlin-compiler-18391528531158821791.salive deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/kotlin/Application.kt b/src/main/kotlin/Application.kt index ad5ba4b..024074d 100644 --- a/src/main/kotlin/Application.kt +++ b/src/main/kotlin/Application.kt @@ -1,13 +1,18 @@ package cz.flegr +import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation fun main(args: Array) { io.ktor.server.netty.EngineMain.main(args) } fun Application.module() { - configureSerialization() + install(ContentNegotiation) { + json() + } + configureDatabases() configureMonitoring() configureRouting() diff --git a/src/main/kotlin/Databases.kt b/src/main/kotlin/Databases.kt index 529963d..d2f8825 100644 --- a/src/main/kotlin/Databases.kt +++ b/src/main/kotlin/Databases.kt @@ -9,40 +9,6 @@ import org.flywaydb.core.Flyway import org.jetbrains.exposed.sql.Database fun Application.configureDatabases() { - val dbConnection = connectToPostgres() - - val taskService = TaskService() - - routing { - - // Create city - post("/tasks") { - val task = call.receive() - val id = taskService.create(task) - call.respond(HttpStatusCode.Created, id) - } - - // Read city - get("/cities/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - try { - val task = taskService.find(id) - call.respond(HttpStatusCode.OK, task) - } catch (e: Exception) { - call.respond(HttpStatusCode.NotFound) - } - } - - // Delete city - delete("/cities/{id}") { - val id = call.parameters["id"]?.toInt() ?: throw IllegalArgumentException("Invalid ID") - taskService.delete(id) - call.respond(HttpStatusCode.OK) - } - } -} - -fun Application.connectToPostgres(): Database { val url = environment.config.property("postgres.url").getString() log.info("Connecting to postgres database at $url") val user = environment.config.property("postgres.user").getString() @@ -50,6 +16,5 @@ fun Application.connectToPostgres(): Database { Flyway.configure().dataSource(url, user, password).load().migrate() - return Database.connect(url, "org.postgresql.Driver", user, password) - -} + Database.connect(url, "org.postgresql.Driver", user, password) +} \ No newline at end of file diff --git a/src/main/kotlin/Routing.kt b/src/main/kotlin/Routing.kt index 858ac9c..66a3f98 100644 --- a/src/main/kotlin/Routing.kt +++ b/src/main/kotlin/Routing.kt @@ -1,22 +1,68 @@ package cz.flegr import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* -import io.ktor.server.plugins.calllogging.* -import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.http.content.staticResources import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -import java.sql.Connection -import java.sql.DriverManager import org.jetbrains.exposed.sql.* -import org.slf4j.event.* +import org.jetbrains.exposed.sql.transactions.transaction fun Application.configureRouting() { + val taskService = TaskService() + routing { - get("/") { - call.respondText("Hello World!") + staticResources("/", "static") + + route("/api/tasks") { + get { + val tasks = transaction { + Tasks.selectAll().map { + Task(it[Tasks.id], it[Tasks.name], it[Tasks.completed]) + } + } + call.respond(tasks) + } + + post { + val task = call.receive() + val id = transaction { + taskService.create(task) + } + val created = transaction { + taskService.find(id) + } + call.respond(HttpStatusCode.Created, created) + } + + delete("/{id}") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid ID") + return@delete + } + transaction { + taskService.delete(id) + } + call.respond(HttpStatusCode.NoContent) + } + + patch("/{id}") { + val id = call.parameters["id"]?.toIntOrNull() + if (id == null) { + call.respond(HttpStatusCode.BadRequest, "Invalid ID") + return@patch + } + val updates = call.receive() + transaction { + taskService.update(updates, id) + } + val updated = transaction { + taskService.find(id) + } + call.respond(updated) + } } } } diff --git a/src/main/kotlin/Serialization.kt b/src/main/kotlin/Serialization.kt deleted file mode 100644 index 3388033..0000000 --- a/src/main/kotlin/Serialization.kt +++ /dev/null @@ -1,22 +0,0 @@ -package cz.flegr - -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import io.ktor.server.application.* -import io.ktor.server.plugins.calllogging.* -import io.ktor.server.plugins.contentnegotiation.* -import io.ktor.server.request.* -import io.ktor.server.response.* -import io.ktor.server.routing.* -import java.sql.Connection -import java.sql.DriverManager -import org.jetbrains.exposed.sql.* -import org.slf4j.event.* - -fun Application.configureSerialization() { - routing { - get("/json/kotlinx-serialization") { - call.respond(mapOf("hello" to "world")) - } - } -} diff --git a/src/main/kotlin/TaskSchema.kt b/src/main/kotlin/TaskSchema.kt index 98b698a..001dc1a 100644 --- a/src/main/kotlin/TaskSchema.kt +++ b/src/main/kotlin/TaskSchema.kt @@ -1,15 +1,11 @@ package cz.flegr -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable -import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.insert -import java.sql.Connection -import java.sql.Statement +import org.jetbrains.exposed.sql.update @Serializable data class Task(val id: Int = 0, val name: String, val completed: Boolean) { @@ -41,4 +37,11 @@ class TaskService { fun delete(id: Int) { Tasks.deleteWhere { Tasks.id eq id } } + + fun update(updates: Task, id: Int) { + Tasks.update({ Tasks.id eq id }) { + it[name] = updates.name + it[completed] = updates.completed + } + } } diff --git a/src/main/resources/db/migration/V1__Create_tasks_table.sql b/src/main/resources/db/migration/V1__Create_tasks_table.sql new file mode 100644 index 0000000..0cc2b84 --- /dev/null +++ b/src/main/resources/db/migration/V1__Create_tasks_table.sql @@ -0,0 +1,5 @@ +CREATE TABLE tasks ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/src/main/resources/static/app.js b/src/main/resources/static/app.js new file mode 100644 index 0000000..f416823 --- /dev/null +++ b/src/main/resources/static/app.js @@ -0,0 +1,113 @@ +const API_BASE = '/api/tasks'; + +const taskInput = document.getElementById('taskInput'); +const addBtn = document.getElementById('addBtn'); +const taskList = document.getElementById('taskList'); + +async function fetchTasks() { + try { + const response = await fetch(API_BASE); + const tasks = await response.json(); + renderTasks(tasks); + } catch (error) { + console.error('Error fetching tasks:', error); + } +} + +function renderTasks(tasks) { + taskList.innerHTML = ''; + + if (tasks.length === 0) { + taskList.innerHTML = '
No tasks yet. Add one above!
'; + return; + } + + tasks.forEach(task => { + const li = document.createElement('li'); + li.className = `task-item ${task.completed ? 'completed' : ''}`; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = task.completed; + checkbox.addEventListener('change', () => toggleTask(task)); + + const span = document.createElement('span'); + span.textContent = task.name; + + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'delete-btn'; + deleteBtn.textContent = 'Delete'; + deleteBtn.addEventListener('click', () => deleteTask(task.id)); + + li.appendChild(checkbox); + li.appendChild(span); + li.appendChild(deleteBtn); + taskList.appendChild(li); + }); +} + +async function addTask() { + const name = taskInput.value.trim(); + if (!name) return; + + try { + const response = await fetch(API_BASE, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name, completed: false }), + }); + + if (response.ok) { + taskInput.value = ''; + await fetchTasks(); + } + } catch (error) { + console.error('Error adding task:', error); + } +} + +async function toggleTask(task) { + try { + const response = await fetch(`${API_BASE}/${task.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: task.name, + completed: !task.completed + }), + }); + + if (response.ok) { + await fetchTasks(); + } + } catch (error) { + console.error('Error toggling task:', error); + } +} + +async function deleteTask(id) { + try { + const response = await fetch(`${API_BASE}/${id}`, { + method: 'DELETE', + }); + + if (response.ok || response.status === 204) { + await fetchTasks(); + } + } catch (error) { + console.error('Error deleting task:', error); + } +} + +addBtn.addEventListener('click', addTask); +taskInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + addTask(); + } +}); + +fetchTasks(); diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..4bee4ff --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,151 @@ + + + + + + Todo App + + + +
+

Todo List

+
+ + +
+
    +
    + + +