<?php
/**
* Mini Todo aplikace
* Kombinace HTML, CSS, JavaScript a PHP v jednom souboru!
*/
$dataFile = __DIR__ . "/todos_data.json";
// Načtení úkolů
function loadTodos() {
global $dataFile;
if (!file_exists($dataFile)) return [];
$content = file_get_contents($dataFile);
return json_decode($content, true) ?: [];
}
function saveTodos($todos) {
global $dataFile;
file_put_contents($dataFile, json_encode($todos, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
// Zpracování API požadavků
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_SERVER["CONTENT_TYPE"]) && strpos($_SERVER["CONTENT_TYPE"], "application/json") !== false) {
header("Content-Type: application/json");
$input = json_decode(file_get_contents("php://input"), true);
$action = $input["action"] ?? "";
$todos = loadTodos();
switch ($action) {
case "add":
$text = trim($input["text"] ?? "");
$category = $input["category"] ?? "general";
if (strlen($text) >= 1) {
$todos[] = [
"id" => uniqid(),
"text" => htmlspecialchars($text),
"done" => false,
"category" => $category,
"created" => date("j.n.Y H:i")
];
saveTodos($todos);
echo json_encode(["ok" => true]);
} else {
echo json_encode(["ok" => false, "error" => "Prázdný úkol"]);
}
break;
case "toggle":
$id = $input["id"] ?? "";
foreach ($todos as &$todo) {
if ($todo["id"] === $id) {
$todo["done"] = !$todo["done"];
break;
}
}
saveTodos($todos);
echo json_encode(["ok" => true]);
break;
case "delete":
$id = $input["id"] ?? "";
$todos = array_values(array_filter($todos, fn($t) => $t["id"] !== $id));
saveTodos($todos);
echo json_encode(["ok" => true]);
break;
case "list":
echo json_encode(["ok" => true, "todos" => $todos]);
break;
}
exit;
}
$todos = loadTodos();
$total = count($todos);
$done = count(array_filter($todos, fn($t) => $t["done"]));
?>
<!DOCTYPE html>
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo aplikace</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
font-family: "Segoe UI", system-ui, sans-serif;
background: linear-gradient(135deg, #0f0c29, #1a1a3e);
color: #e0e0e0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 2rem;
}
.app {
width: 100%;
max-width: 600px;
margin-top: 1rem;
}
h1 {
text-align: center;
font-size: 2.2rem;
background: linear-gradient(to right, #00d4aa, #7b68ee);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 0.3rem;
}
.subtitle { text-align: center; color: #6b7280; margin-bottom: 1.5rem; }
/* Stats */
.stats {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat {
flex: 1;
text-align: center;
padding: 0.8rem;
background: rgba(255,255,255,0.04);
border-radius: 12px;
border: 1px solid rgba(255,255,255,0.06);
}
.stat-num { font-size: 1.8rem; font-weight: 800; }
.stat-label { font-size: 0.75rem; color: #6b7280; text-transform: uppercase; letter-spacing: 1px; }
.stat-total .stat-num { color: #7b68ee; }
.stat-done .stat-num { color: #00d4aa; }
.stat-left .stat-num { color: #f59e0b; }
/* Add form */
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.add-form input {
flex: 1;
padding: 0.8rem 1rem;
background: rgba(255,255,255,0.06);
border: 2px solid rgba(255,255,255,0.1);
border-radius: 12px;
color: #e0e0e0;
font-size: 1rem;
outline: none;
transition: border-color 0.3s;
}
.add-form input:focus { border-color: #7b68ee; }
.add-form select {
padding: 0.8rem;
background: rgba(255,255,255,0.06);
border: 2px solid rgba(255,255,255,0.1);
border-radius: 12px;
color: #e0e0e0;
font-size: 0.9rem;
outline: none;
}
.add-form select option { background: #1a1a3e; }
.add-btn {
padding: 0.8rem 1.5rem;
background: linear-gradient(135deg, #00d4aa, #7b68ee);
color: white;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
white-space: nowrap;
}
.add-btn:hover { transform: scale(1.05); }
/* Filters */
.filters {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.4rem 1rem;
border: 1px solid rgba(255,255,255,0.15);
border-radius: 20px;
background: none;
color: #8892a4;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s;
}
.filter-btn.active { border-color: #00d4aa; color: #00d4aa; background: rgba(0,212,170,0.1); }
.filter-btn:hover { border-color: #00d4aa; }
/* Todo list */
.todo-list { list-style: none; }
.todo-item {
display: flex;
align-items: center;
gap: 0.8rem;
padding: 1rem;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 12px;
margin-bottom: 0.5rem;
transition: all 0.3s;
animation: slideIn 0.3s ease;
}
.todo-item:hover { border-color: rgba(0,212,170,0.2); }
.todo-item.done { opacity: 0.5; }
.todo-item.done .todo-text { text-decoration: line-through; }
.todo-check {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
flex-shrink: 0;
}
.todo-check:hover { border-color: #00d4aa; }
.todo-check.checked { background: #00d4aa; border-color: #00d4aa; }
.todo-text { flex: 1; font-size: 0.95rem; }
.todo-cat {
font-size: 0.7rem;
padding: 0.2rem 0.6rem;
border-radius: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cat-work { background: rgba(123,104,238,0.15); color: #a78bfa; }
.cat-personal { background: rgba(0,212,170,0.15); color: #00d4aa; }
.cat-shopping { background: rgba(245,158,11,0.15); color: #f59e0b; }
.cat-general { background: rgba(255,255,255,0.1); color: #8892a4; }
.todo-delete {
width: 28px;
height: 28px;
border: none;
background: rgba(255,107,157,0.1);
color: #ff6b9d;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
flex-shrink: 0;
}
.todo-delete:hover { background: rgba(255,107,157,0.3); transform: scale(1.1); }
.todo-date { font-size: 0.75rem; color: #555; }
.empty-state { text-align: center; padding: 3rem; color: #555; font-size: 1.1rem; }
.progress-bar {
width: 100%;
height: 6px;
background: rgba(255,255,255,0.06);
border-radius: 3px;
margin-bottom: 1.5rem;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(to right, #00d4aa, #7b68ee);
border-radius: 3px;
transition: width 0.5s ease;
}
@keyframes slideIn { from { opacity:0; transform:translateX(-20px); } to { opacity:1; transform:translateX(0); } }
@media (max-width: 600px) {
body { padding: 1rem; }
.add-form { flex-wrap: wrap; }
.add-form input, .add-form select { width: 100%; }
.stats { gap: 0.5rem; }
}
</style>
</head>
<body>
<div class="app">
<h1>✅ Moje úkoly</h1>
<p class="subtitle">Organizuj svůj den</p>
<div class="stats">
<div class="stat stat-total">
<div class="stat-num" id="statTotal"><?php echo $total; ?></div>
<div class="stat-label">Celkem</div>
</div>
<div class="stat stat-done">
<div class="stat-num" id="statDone"><?php echo $done; ?></div>
<div class="stat-label">Hotovo</div>
</div>
<div class="stat stat-left">
<div class="stat-num" id="statLeft"><?php echo $total - $done; ?></div>
<div class="stat-label">Zbývá</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width:<?php echo $total > 0 ? round($done/$total*100) : 0; ?>%"></div>
</div>
<div class="add-form">
<input type="text" id="todoInput" placeholder="Nový úkol..." onkeydown="if(event.key==='Enter')addTodo()">
<select id="todoCategory">
<option value="general">📋 Obecné</option>
<option value="work">💼 Práce</option>
<option value="personal">👤 Osobní</option>
<option value="shopping">🛒 Nákupy</option>
</select>
<button class="add-btn" onclick="addTodo()">+</button>
</div>
<div class="filters">
<button class="filter-btn active" onclick="setFilter('all', this)">Vše</button>
<button class="filter-btn" onclick="setFilter('active', this)">Aktivní</button>
<button class="filter-btn" onclick="setFilter('done', this)">Hotové</button>
</div>
<ul class="todo-list" id="todoList"></ul>
</div>
<script>
let todos = <?php echo json_encode(loadTodos()); ?>;
let currentFilter = "all";
function renderTodos() {
const list = document.getElementById("todoList");
const filtered = todos.filter(t => {
if (currentFilter === "active") return !t.done;
if (currentFilter === "done") return t.done;
return true;
});
if (filtered.length === 0) {
list.innerHTML = '<div class="empty-state">💭 Žádné úkoly. Přidej první!</div>';
} else {
list.innerHTML = filtered.map(t => '<li class="todo-item ' + (t.done ? "done" : "") + '">' +
'<div class="todo-check ' + (t.done ? "checked" : "") + '" onclick="toggleTodo(\x27' + t.id + '\x27)">' + (t.done ? "✔" : "") + '</div>' +
'<div style="flex:1"><div class="todo-text">' + t.text + '</div><div class="todo-date">' + t.created + '</div></div>' +
'<span class="todo-cat cat-' + t.category + '">' + t.category + '</span>' +
'<button class="todo-delete" onclick="deleteTodo(\x27' + t.id + '\x27)">✖</button>' +
'</li>').join("");
}
const total = todos.length;
const done = todos.filter(t => t.done).length;
document.getElementById("statTotal").textContent = total;
document.getElementById("statDone").textContent = done;
document.getElementById("statLeft").textContent = total - done;
document.getElementById("progressFill").style.width = (total > 0 ? Math.round(done / total * 100) : 0) + "%";
}
async function apiCall(data) {
const resp = await fetch(location.href, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data)
});
return await resp.json();
}
async function addTodo() {
const input = document.getElementById("todoInput");
const cat = document.getElementById("todoCategory").value;
const text = input.value.trim();
if (!text) return;
await apiCall({ action: "add", text: text, category: cat });
const result = await apiCall({ action: "list" });
todos = result.todos;
input.value = "";
renderTodos();
}
async function toggleTodo(id) {
await apiCall({ action: "toggle", id: id });
const result = await apiCall({ action: "list" });
todos = result.todos;
renderTodos();
}
async function deleteTodo(id) {
await apiCall({ action: "delete", id: id });
const result = await apiCall({ action: "list" });
todos = result.todos;
renderTodos();
}
function setFilter(filter, btn) {
currentFilter = filter;
document.querySelectorAll(".filter-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
renderTodos();
}
renderTodos();
</script>
</body>
</html>