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

View file

@ -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<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
configureSerialization()
install(ContentNegotiation) {
json()
}
configureDatabases()
configureMonitoring()
configureRouting()

View file

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

View file

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

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>