init
This commit is contained in:
parent
3f9345b064
commit
495987c06c
10 changed files with 341 additions and 73 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -16,6 +16,8 @@ bin/
|
|||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
.kotlin
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE tasks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
completed BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
113
src/main/resources/static/app.js
Normal file
113
src/main/resources/static/app.js
Normal 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();
|
||||
151
src/main/resources/static/index.html
Normal file
151
src/main/resources/static/index.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue