This commit is contained in:
pavel 2026-02-05 01:25:47 +01:00
commit 495987c06c
10 changed files with 341 additions and 73 deletions

2
.gitignore vendored
View file

@ -16,6 +16,8 @@ bin/
!**/src/main/**/bin/ !**/src/main/**/bin/
!**/src/test/**/bin/ !**/src/test/**/bin/
.kotlin
### IntelliJ IDEA ### ### IntelliJ IDEA ###
.idea .idea
*.iws *.iws

View file

@ -1,13 +1,18 @@
package cz.flegr package cz.flegr
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
fun main(args: Array<String>) { fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args) io.ktor.server.netty.EngineMain.main(args)
} }
fun Application.module() { fun Application.module() {
configureSerialization() install(ContentNegotiation) {
json()
}
configureDatabases() configureDatabases()
configureMonitoring() configureMonitoring()
configureRouting() configureRouting()

View file

@ -9,40 +9,6 @@ import org.flywaydb.core.Flyway
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
fun Application.configureDatabases() { fun Application.configureDatabases() {
val dbConnection = connectToPostgres()
val taskService = TaskService()
routing {
// Create city
post("/tasks") {
val task = call.receive<Task>()
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() val url = environment.config.property("postgres.url").getString()
log.info("Connecting to postgres database at $url") log.info("Connecting to postgres database at $url")
val user = environment.config.property("postgres.user").getString() val user = environment.config.property("postgres.user").getString()
@ -50,6 +16,5 @@ fun Application.connectToPostgres(): Database {
Flyway.configure().dataSource(url, user, password).load().migrate() 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)
}
}

View file

@ -1,22 +1,68 @@
package cz.flegr package cz.flegr
import io.ktor.http.* import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.plugins.calllogging.* import io.ktor.server.http.content.staticResources
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.* import io.ktor.server.request.*
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.* import io.ktor.server.routing.*
import java.sql.Connection
import java.sql.DriverManager
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.slf4j.event.* import org.jetbrains.exposed.sql.transactions.transaction
fun Application.configureRouting() { fun Application.configureRouting() {
val taskService = TaskService()
routing { routing {
get("/") { staticResources("/", "static")
call.respondText("Hello World!")
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<Task>()
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<Task>()
transaction {
taskService.update(updates, id)
}
val updated = transaction {
taskService.find(id)
}
call.respond(updated)
}
} }
} }
} }

View file

@ -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"))
}
}
}

View file

@ -1,15 +1,11 @@
package cz.flegr package cz.flegr
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import java.sql.Connection import org.jetbrains.exposed.sql.update
import java.sql.Statement
@Serializable @Serializable
data class Task(val id: Int = 0, val name: String, val completed: Boolean) { data class Task(val id: Int = 0, val name: String, val completed: Boolean) {
@ -41,4 +37,11 @@ class TaskService {
fun delete(id: Int) { fun delete(id: Int) {
Tasks.deleteWhere { Tasks.id eq id } 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
}
}
} }

View file

@ -0,0 +1,5 @@
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
completed BOOLEAN NOT NULL DEFAULT FALSE
);

View file

@ -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 = '<div class="empty-state">No tasks yet. Add one above!</div>';
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();

View file

@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
width: 100%;
max-width: 600px;
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 30px;
text-align: center;
font-size: 2.5rem;
}
.input-container {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
#taskInput {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
#taskInput:focus {
outline: none;
border-color: #667eea;
}
#addBtn {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
#addBtn:hover {
background: #5568d3;
}
#taskList {
list-style: none;
}
.task-item {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 12px;
transition: all 0.3s;
}
.task-item:hover {
background: #e9ecef;
transform: translateY(-2px);
}
.task-item.completed {
opacity: 0.6;
}
.task-item input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.task-item span {
flex: 1;
font-size: 16px;
color: #333;
}
.task-item.completed span {
text-decoration: line-through;
color: #999;
}
.delete-btn {
padding: 8px 16px;
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.delete-btn:hover {
background: #c82333;
}
.empty-state {
text-align: center;
color: #999;
padding: 40px;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<h1>Todo List</h1>
<div class="input-container">
<input type="text" id="taskInput" placeholder="Add a new task..." />
<button id="addBtn">Add Task</button>
</div>
<ul id="taskList"></ul>
</div>
<script src="app.js"></script>
</body>
</html>