SIEPEX - Python e FastAPI
Pasted Oct 29, 2026, 9:50 AM·4 files·1,129 lines·expires in 12 h
Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/siepex-python-fastapidatabase.pyPython
from collections.abc import Generator
from pathlib import Path
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
DATABASE_URL = (
f"sqlite:///{Path(__file__).parent / 'todo.db'}"
)
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
)
SessionLocal = sessionmaker(
bind=engine,
autoflush=False,
expire_on_commit=False,
)
def db_session() -> Generator[Session, None, None]:
with SessionLocal() as session:
yield session
class Base(DeclarativeBase):
pass
index.htmlHTML
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0b1018">
<title>Todo App — Gerenciador de tarefas</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/app.js" defer></script>
</head>
<body>
<header class="topbar">
<a class="brand" href="#" aria-label="Todo App - início">
<span class="brand-mark" aria-hidden="true">✓</span>
<span><strong>Todo App</strong><small>Gerencie suas tarefas de forma simples</small></span>
</a>
<div id="api-status" class="api-status" role="status"><span class="status-dot"></span><span>Verificando
API...</span></div>
</header>
<main class="layout">
<aside class="sidebar">
<nav aria-label="Filtros de tarefas">
<button class="nav-item active" data-filter="all"><span class="nav-icon">⌂</span><span>Todos</span><span
id="count-all" class="count">0</span></button>
<button class="nav-item" data-filter="completed"><span
class="nav-icon">✓</span><span>Concluídos</span><span id="count-completed"
class="count">0</span></button>
<button class="nav-item" data-filter="pending"><span
class="nav-icon">◷</span><span>Pendentes</span><span id="count-pending"
class="count">0</span></button>
</nav>
<div class="sidebar-footer"><span aria-hidden="true"></></span> Todo App · HTML, CSS e JS</div>
</aside>
<section class="content">
<div class="section-heading">
<div>
<p class="eyebrow">SEU ESPAÇO DE PRODUTIVIDADE</p>
<h1>Minhas tarefas</h1>
<p class="subtitle">Organize o que precisa ser feito, uma tarefa por vez.</p>
</div>
<button id="new-task-button" class="button button-primary"><span aria-hidden="true">+</span> Nova
tarefa</button>
</div>
<div class="toolbar">
<div class="filter-pills" aria-label="Filtrar tarefas">
<button class="filter-pill active" data-filter="all">Todas <span id="pill-all">0</span></button>
<button class="filter-pill" data-filter="pending">Pendentes <span
id="pill-pending">0</span></button>
<button class="filter-pill" data-filter="completed">Concluídas <span
id="pill-completed">0</span></button>
</div>
<button id="refresh-button" class="button button-quiet" title="Atualizar lista">↻ Atualizar</button>
</div>
<div id="notice" class="notice hidden" role="alert"></div>
<div id="loading" class="loading hidden"><span class="spinner"></span> Carregando tarefas...</div>
<div id="task-list" class="task-list" aria-live="polite"></div>
<div id="empty-state" class="empty-state hidden">
<div class="empty-icon">✓</div>
<h2>Nenhuma tarefa por aqui</h2>
<p>Adicione uma tarefa ou escolha outro filtro.</p>
<button class="button button-primary" id="empty-new-button">+ Criar tarefa</button>
</div>
<footer class="content-footer"><span id="summary">0 tarefas</span><span>Dados fornecidos pela sua API
FastAPI</span></footer>
</section>
</main>
<dialog id="task-dialog" class="task-dialog">
<form id="task-form">
<div class="dialog-heading">
<div>
<p class="eyebrow">TAREFAS</p>
<h2 id="dialog-title">Nova tarefa</h2>
</div>
<button type="button" id="close-dialog" class="icon-button" aria-label="Fechar">×</button>
</div>
<label for="task-title">Título</label>
<input id="task-title" name="title" type="text" maxlength="200" placeholder="Ex.: Estudar FastAPI" required
autocomplete="off">
<label for="task-status">Status</label>
<select id="task-status" name="status">
<option value="pending">Pendente</option>
<option value="completed">Concluída</option>
</select>
<p class="form-hint">O título deve ter entre 1 e 200 caracteres.</p>
<div class="dialog-actions">
<button type="button" id="cancel-dialog" class="button button-secondary">Cancelar</button>
<button type="submit" id="save-button" class="button button-primary">Salvar tarefa</button>
</div>
</form>
</dialog>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
</body>
</html>style.cssCSS
:root {
color-scheme: dark;
--bg: #090e15;
--panel: #0e1520;
--panel-soft: #121b28;
--border: #202c3b;
--text: #f1f5fb;
--muted: #91a0b5;
--blue: #1677f9;
--blue-hover: #3289ff;
--green: #28d6a0;
--yellow: #f5c451;
--red: #ff6676;
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--text);
background:
radial-gradient(ellipse at 50% -20%, #15263b 0, transparent 52%), var(--bg);
}
button,
input,
select {
font: inherit;
}
button {
cursor: pointer;
}
button:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 2px solid #69aaff;
outline-offset: 3px;
}
.topbar {
min-height: 100px;
padding: 22px clamp(20px, 4vw, 56px);
display: flex;
justify-content: space-between;
align-items: center;
gap: 24px;
border-bottom: 1px solid rgba(144, 164, 190, 0.12);
}
.brand {
display: flex;
gap: 16px;
align-items: center;
text-decoration: none;
color: inherit;
}
.brand-mark {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 15px;
background: linear-gradient(145deg, #2689ff, #0756d9);
font-size: 32px;
font-weight: 800;
box-shadow: 0 8px 28px #0965ee38;
}
.brand strong {
display: block;
font-size: 25px;
letter-spacing: -0.7px;
}
.brand small {
display: block;
color: var(--muted);
margin-top: 4px;
font-size: 13px;
}
.api-status {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 9px 13px;
border-radius: 999px;
color: var(--muted);
background: #101925;
border: 1px solid var(--border);
font-size: 12px;
white-space: nowrap;
}
.status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--yellow);
box-shadow: 0 0 12px currentColor;
}
.api-status.online {
color: var(--green);
border-color: #1d5547;
}
.api-status.online .status-dot {
background: var(--green);
}
.api-status.offline {
color: var(--red);
border-color: #63303b;
}
.api-status.offline .status-dot {
background: var(--red);
}
.layout {
display: grid;
grid-template-columns: 245px minmax(0, 1fr);
gap: 22px;
padding: 0 clamp(18px, 2.5vw, 38px) 32px;
min-height: calc(100vh - 100px);
}
.sidebar,
.content {
background: rgba(11, 17, 26, 0.72);
border: 1px solid var(--border);
border-radius: 15px;
}
.sidebar {
margin-top: 0;
display: flex;
flex-direction: column;
padding: 18px 12px;
min-height: calc(100vh - 122px);
}
.sidebar nav {
display: grid;
gap: 7px;
}
.nav-item {
display: flex;
align-items: center;
gap: 13px;
width: 100%;
padding: 13px 12px;
text-align: left;
color: #bdc9da;
border: 1px solid transparent;
background: transparent;
border-radius: 10px;
font-size: 14px;
}
.nav-item:hover,
.nav-item.active {
background: #102e57;
color: #fff;
}
.nav-item.active {
border-color: #173e70;
}
.nav-icon {
width: 22px;
text-align: center;
font-size: 21px;
color: #a9bdd7;
}
.count {
margin-left: auto;
display: grid;
place-items: center;
min-width: 27px;
height: 27px;
padding: 0 7px;
border: 1px solid #2a3a4e;
border-radius: 9px;
background: #101925;
font-size: 12px;
}
.sidebar-footer {
margin-top: auto;
padding: 20px 10px 4px;
color: var(--muted);
font-size: 12px;
display: flex;
gap: 11px;
align-items: center;
border-top: 1px solid #1a2533;
}
.sidebar-footer span {
color: #b4c9e5;
font-weight: 700;
}
.content {
padding: clamp(22px, 3vw, 38px);
min-width: 0;
}
.section-heading {
display: flex;
justify-content: space-between;
align-items: center;
gap: 18px;
}
.eyebrow {
color: #6faaff;
letter-spacing: 1.5px;
font-size: 10px;
font-weight: 800;
margin: 0 0 8px;
}
h1,
h2,
p {
margin-top: 0;
}
h1 {
font-size: clamp(25px, 3vw, 32px);
letter-spacing: -1px;
margin-bottom: 8px;
}
.subtitle {
color: var(--muted);
margin-bottom: 0;
font-size: 14px;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 42px;
padding: 0 16px;
border-radius: 9px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 650;
transition: 0.15s ease;
}
.button-primary {
color: white;
background: var(--blue);
box-shadow: 0 5px 18px #0c66ed24;
}
.button-primary:hover {
background: var(--blue-hover);
transform: translateY(-1px);
}
.button-primary:disabled {
opacity: 0.6;
cursor: wait;
transform: none;
}
.button-secondary {
color: #d3dceb;
background: #141e2b;
border-color: #29374a;
}
.button-secondary:hover,
.button-quiet:hover {
background: #1a2839;
}
.button-quiet {
color: var(--muted);
background: transparent;
border-color: #253347;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 15px;
margin: 28px 0 19px;
}
.filter-pills {
display: flex;
flex-wrap: wrap;
gap: 9px;
}
.filter-pill {
display: inline-flex;
align-items: center;
gap: 11px;
border: 1px solid transparent;
border-radius: 999px;
background: #111a26;
color: #afbdd0;
padding: 9px 15px;
font-size: 13px;
}
.filter-pill span {
font-size: 11px;
color: #d4deeb;
}
.filter-pill.active {
background: #1269e8;
color: white;
}
.filter-pill.active span {
color: white;
}
.task-list {
display: grid;
gap: 10px;
}
.task-card {
display: grid;
grid-template-columns: 25px minmax(0, 1fr) auto auto;
align-items: center;
gap: 16px;
padding: 17px 18px;
background: linear-gradient(110deg, #0f1722, #0d141e);
border: 1px solid #202c3b;
border-radius: 12px;
transition:
border-color 0.15s,
transform 0.15s;
}
.task-card:hover {
border-color: #35465d;
transform: translateY(-1px);
}
.task-check {
appearance: none;
width: 22px;
height: 22px;
margin: 0;
border: 2px solid #6f829a;
border-radius: 6px;
background: #0c131d;
cursor: pointer;
display: grid;
place-items: center;
}
.task-check:checked {
background: var(--blue);
border-color: var(--blue);
}
.task-check:checked::after {
content: "✓";
color: white;
font-size: 15px;
font-weight: 800;
}
.task-info {
min-width: 0;
}
.task-title {
margin: 0 0 5px;
overflow-wrap: anywhere;
font-size: 15px;
font-weight: 650;
}
.task-card.is-completed .task-title {
color: #94a4b9;
text-decoration: line-through;
}
.task-meta {
color: var(--muted);
font-size: 12px;
}
.status-badge {
white-space: nowrap;
border-radius: 999px;
padding: 6px 11px;
font-size: 11px;
font-weight: 650;
}
.status-badge.pending {
color: var(--yellow);
background: #392c12;
border: 1px solid #55421a;
}
.status-badge.completed {
color: var(--green);
background: #10372f;
border: 1px solid #1a5949;
}
.task-actions {
display: flex;
gap: 8px;
}
.icon-button {
display: grid;
place-items: center;
width: 38px;
height: 38px;
color: #a9c9f5;
background: #121c29;
border: 1px solid #26364a;
border-radius: 9px;
font-size: 19px;
}
.icon-button:hover {
background: #1b2c40;
}
.icon-button.delete {
color: var(--red);
border-color: #432a35;
background: #1c151d;
}
.icon-button.delete:hover {
background: #321923;
}
.icon-button svg {
width: 19px;
height: 19px;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.empty-state {
text-align: center;
padding: 58px 20px;
border: 1px dashed #29384b;
border-radius: 13px;
background: #0c131c;
}
.empty-icon {
margin: 0 auto 15px;
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 15px;
color: #7db6ff;
background: #122b4a;
font-size: 25px;
}
.empty-state h2 {
font-size: 18px;
margin-bottom: 8px;
}
.empty-state p {
color: var(--muted);
font-size: 13px;
margin-bottom: 20px;
}
.content-footer {
display: flex;
justify-content: space-between;
gap: 12px;
margin-top: 25px;
padding-top: 17px;
border-top: 1px solid #1b2634;
color: #718198;
font-size: 11px;
}
.notice {
margin: 0 0 15px;
padding: 12px 14px;
border-radius: 9px;
color: #ffb9c1;
background: #2a151d;
border: 1px solid #60303b;
font-size: 13px;
overflow-wrap: anywhere;
}
.loading {
color: var(--muted);
display: flex;
align-items: center;
gap: 10px;
padding: 25px 0;
font-size: 13px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #35465c;
border-top-color: #70adff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.hidden {
display: none !important;
}
.task-dialog {
width: min(100% - 30px, 500px);
color: var(--text);
background: #0f1722;
border: 1px solid #2b394c;
border-radius: 15px;
padding: 25px;
box-shadow: 0 30px 100px #000a;
}
.task-dialog::backdrop {
background: #02060bc9;
backdrop-filter: blur(5px);
}
.dialog-heading {
display: flex;
align-items: start;
justify-content: space-between;
margin-bottom: 24px;
}
.dialog-heading h2 {
margin: 0;
font-size: 23px;
}
.dialog-heading .icon-button {
font-size: 25px;
color: var(--muted);
background: transparent;
border: 0;
width: 30px;
height: 30px;
}
.task-dialog label {
display: block;
margin: 16px 0 8px;
font-size: 13px;
font-weight: 650;
}
.task-dialog input,
.task-dialog select {
width: 100%;
min-height: 44px;
padding: 0 13px;
border: 1px solid #2a394d;
border-radius: 8px;
background: #111c29;
color: var(--text);
}
.task-dialog input::placeholder {
color: #66788f;
}
.form-hint {
margin: 10px 0 0;
color: #7e8da2;
font-size: 11px;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 25px;
}
.toast {
position: fixed;
bottom: 22px;
right: 22px;
max-width: min(400px, calc(100vw - 44px));
padding: 13px 17px;
border-radius: 10px;
background: #12372e;
color: #b5f6df;
border: 1px solid #24604e;
box-shadow: 0 10px 40px #0005;
font-size: 13px;
opacity: 0;
pointer-events: none;
transform: translateY(10px);
transition: 0.2s;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 820px) {
.layout {
grid-template-columns: 1fr;
padding-top: 15px;
}
.sidebar {
min-height: 0;
padding: 10px;
}
.sidebar nav {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.nav-item {
justify-content: center;
gap: 7px;
padding: 10px 7px;
font-size: 12px;
}
.nav-item .count {
display: none;
}
.sidebar-footer {
display: none;
}
}
@media (max-width: 560px) {
.topbar {
min-height: 82px;
padding: 16px;
}
.brand-mark {
width: 43px;
height: 43px;
font-size: 26px;
}
.brand strong {
font-size: 20px;
}
.brand small {
max-width: 190px;
font-size: 11px;
}
.api-status {
padding: 8px;
font-size: 0;
}
.api-status .status-dot {
width: 9px;
height: 9px;
}
.layout {
padding: 12px;
gap: 12px;
}
.content {
padding: 19px 14px;
}
.section-heading {
align-items: flex-start;
flex-direction: column;
}
.section-heading .button {
width: 100%;
}
.toolbar {
align-items: flex-start;
flex-direction: column;
margin: 22px 0 16px;
}
.task-card {
grid-template-columns: 23px minmax(0, 1fr) auto;
gap: 10px;
padding: 13px 11px;
}
.status-badge {
grid-column: 2;
justify-self: start;
padding: 4px 8px;
}
.task-actions {
grid-column: 3;
grid-row: 1 / span 2;
flex-direction: column;
}
.icon-button {
width: 34px;
height: 34px;
}
.task-meta {
line-height: 1.5;
}
.content-footer {
flex-direction: column;
}
}
app.jsJavaScript
/*
* Frontend Todo App — HTML + CSS + Fetch.
* Ajuste API_URL para a URL da sua API FastAPI.
*/
const API_URL = "http://127.0.0.1:8000/api/v1/todos/";
const state = {
tasks: [],
filter: "all",
editingId: null,
};
const $ = (selector) => document.querySelector(selector);
const taskList = $("#task-list");
const emptyState = $("#empty-state");
const loading = $("#loading");
const notice = $("#notice");
const dialog = $("#task-dialog");
const form = $("#task-form");
const titleInput = $("#task-title");
const statusInput = $("#task-status");
const saveButton = $("#save-button");
const apiStatus = $("#api-status");
let toastTimeout;
function normalizeStatus(value) {
const normalized = String(value ?? "pending")
.trim()
.toLowerCase();
return [
"completed",
"complete",
"done",
"concluida",
"concluído",
"concluido",
"true",
"1",
].includes(normalized)
? "completed"
: "pending";
}
function isCompleted(task) {
// API esperada: { id, title, status }. Também aceita um campo booleano "completed".
return typeof task.completed === "boolean"
? task.completed
: normalizeStatus(task.status) === "completed";
}
function statusForApi(completed) {
return Boolean(completed);
}
function setApiStatus(online, message) {
apiStatus.classList.toggle("online", online);
apiStatus.classList.toggle("offline", !online);
apiStatus.lastElementChild.textContent = message;
}
function showNotice(message) {
notice.textContent = message;
notice.classList.remove("hidden");
}
function hideNotice() {
notice.textContent = "";
notice.classList.add("hidden");
}
function showToast(message) {
const toast = $("#toast");
toast.textContent = message;
toast.classList.add("show");
clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => toast.classList.remove("show"), 2600);
}
async function request(path = "", options = {}) {
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
...(options.body ? { "Content-Type": "application/json" } : {}),
...options.headers,
},
});
if (!response.ok) {
let detail = `HTTP ${response.status}`;
try {
const body = await response.json();
if (body.detail)
detail = Array.isArray(body.detail)
? body.detail.map((item) => item.msg).join("; ")
: body.detail;
} catch {
// A resposta pode não conter JSON.
}
throw new Error(detail);
}
if (response.status === 204) return null;
const text = await response.text();
return text ? JSON.parse(text) : null;
}
function taskId(task) {
return encodeURIComponent(task.id);
}
function getFilteredTasks() {
return state.tasks.filter((task) => {
if (state.filter === "completed") return isCompleted(task);
if (state.filter === "pending") return !isCompleted(task);
return true;
});
}
function updateCounts() {
const completed = state.tasks.filter(isCompleted).length;
const pending = state.tasks.length - completed;
$("#count-all").textContent = state.tasks.length;
$("#count-completed").textContent = completed;
$("#count-pending").textContent = pending;
$("#pill-all").textContent = state.tasks.length;
$("#pill-completed").textContent = completed;
$("#pill-pending").textContent = pending;
$("#summary").textContent =
`${state.tasks.length} ${state.tasks.length === 1 ? "tarefa" : "tarefas"} · ${pending} pendente${pending === 1 ? "" : "s"}`;
}
function makeTaskCard(task) {
const completed = isCompleted(task);
const card = document.createElement("article");
card.className = `task-card${completed ? " is-completed" : ""}`;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "task-check";
checkbox.checked = completed;
checkbox.setAttribute(
"aria-label",
`Marcar "${task.title}" como ${completed ? "pendente" : "concluída"}`,
);
checkbox.addEventListener("change", () => toggleTask(task, checkbox.checked));
const info = document.createElement("div");
info.className = "task-info";
const title = document.createElement("p");
title.className = "task-title";
title.textContent = task.title ?? "(Sem título)";
const meta = document.createElement("p");
meta.className = "task-meta";
meta.textContent = `ID: ${task.id}`;
info.append(title, meta);
const badge = document.createElement("span");
badge.className = `status-badge ${completed ? "completed" : "pending"}`;
badge.textContent = completed ? "Concluída" : "Pendente";
const actions = document.createElement("div");
actions.className = "task-actions";
const editButton = document.createElement("button");
editButton.className = "icon-button";
editButton.type = "button";
editButton.title = "Editar tarefa";
editButton.setAttribute("aria-label", `Editar ${task.title}`);
editButton.textContent = "✎";
editButton.addEventListener("click", () => openEditDialog(task));
const deleteButton = document.createElement("button");
deleteButton.className = "icon-button delete";
deleteButton.type = "button";
deleteButton.title = "Excluir tarefa";
deleteButton.setAttribute("aria-label", `Excluir ${task.title}`);
deleteButton.innerHTML = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h16M9 7V4h6v3m-9 0 1 13h10l1-13
M10 11v5m4-5v5"/>
</svg>
`;
deleteButton.addEventListener("click", () => deleteTask(task));
actions.append(editButton, deleteButton);
card.append(checkbox, info, badge, actions);
return card;
}
function render() {
taskList.replaceChildren();
const tasks = getFilteredTasks();
tasks.forEach((task) => taskList.append(makeTaskCard(task)));
emptyState.classList.toggle("hidden", tasks.length !== 0);
taskList.classList.toggle("hidden", tasks.length === 0);
updateCounts();
document.querySelectorAll("[data-filter]").forEach((button) => {
button.classList.toggle("active", button.dataset.filter === state.filter);
});
}
async function loadTasks() {
hideNotice();
loading.classList.remove("hidden");
taskList.classList.add("hidden");
emptyState.classList.add("hidden");
try {
const data = await request();
if (!Array.isArray(data))
throw new Error(
"A API não retornou uma lista. Confira o endpoint GET /api/v1/todos/.",
);
state.tasks = data;
setApiStatus(true, "API conectada");
render();
} catch (error) {
setApiStatus(false, "API desconectada");
showNotice(
`Não foi possível carregar as tarefas: ${error.message}. Verifique se a API está rodando e se o CORS está configurado.`,
);
state.tasks = [];
render();
emptyState.classList.add("hidden");
} finally {
loading.classList.add("hidden");
}
}
function openNewDialog() {
state.editingId = null;
form.reset();
$("#dialog-title").textContent = "Nova tarefa";
saveButton.textContent = "Salvar tarefa";
statusInput.value = "pending";
dialog.showModal();
titleInput.focus();
}
function openEditDialog(task) {
state.editingId = task.id;
form.reset();
$("#dialog-title").textContent = "Editar tarefa";
saveButton.textContent = "Salvar alterações";
titleInput.value = task.title ?? "";
statusInput.value = isCompleted(task) ? "completed" : "pending";
dialog.showModal();
titleInput.focus();
}
function closeDialog() {
dialog.close();
}
async function saveTask(event) {
event.preventDefault();
const title = titleInput.value.trim();
if (!title) {
titleInput.setCustomValidity("Informe um título para a tarefa.");
titleInput.reportValidity();
titleInput.setCustomValidity("");
return;
}
const completed = statusInput.value === "completed";
const payload = { title, status: statusForApi(completed) };
saveButton.disabled = true;
saveButton.textContent = "Salvando...";
try {
if (state.editingId === null) {
await request("", { method: "POST", body: JSON.stringify(payload) });
showToast("Tarefa criada com sucesso.");
} else {
await request(encodeURIComponent(state.editingId), {
method: "PUT",
body: JSON.stringify(payload),
});
showToast("Tarefa atualizada com sucesso.");
}
closeDialog();
await loadTasks();
} catch (error) {
showNotice(
`Não foi possível salvar a tarefa: ${error.message}. Confira os campos exigidos pelo schema da API.`,
);
} finally {
saveButton.disabled = false;
saveButton.textContent =
state.editingId === null ? "Salvar tarefa" : "Salvar alterações";
}
}
async function toggleTask(task, completed) {
const payload = { title: task.title, status: statusForApi(completed) };
try {
await request(taskId(task), {
method: "PUT",
body: JSON.stringify(payload),
});
showToast(
completed ? "Tarefa concluída." : "Tarefa marcada como pendente.",
);
await loadTasks();
} catch (error) {
showNotice(`Não foi possível alterar o status: ${error.message}`);
await loadTasks();
}
}
async function deleteTask(task) {
if (!window.confirm(`Deseja excluir a tarefa "${task.title}"?`)) return;
try {
await request(taskId(task), { method: "DELETE" });
showToast("Tarefa excluída.");
await loadTasks();
} catch (error) {
showNotice(`Não foi possível excluir a tarefa: ${error.message}`);
}
}
document.querySelectorAll("[data-filter]").forEach((button) => {
button.addEventListener("click", () => {
state.filter = button.dataset.filter;
render();
});
});
$("#new-task-button").addEventListener("click", openNewDialog);
$("#empty-new-button").addEventListener("click", openNewDialog);
$("#refresh-button").addEventListener("click", loadTasks);
$("#close-dialog").addEventListener("click", closeDialog);
$("#cancel-dialog").addEventListener("click", closeDialog);
form.addEventListener("submit", saveTask);
dialog.addEventListener("click", (event) => {
if (event.target === dialog) closeDialog();
});
loadTasks();27,388 chars · codepastes.com