Konsol: fraktal (paralel şeritli 24-bit Mandelbrot/Julia + yakınlaşma), sudoku (geri izlemeli, canlı animasyon), fatura (JSON → terminal tablosu, PDF ve Excel). Masaüstü: hesap makinesi (işlem önceliği, canlı bağlama), odak (Pomodoro), parola atölyesi (entropi ölçer, SHA-256). Web: pano (Kanban REST API + SQLite), kisalt (302 yönlendirme, SVG istatistik), anket (adres başına tek oy, sunucuda çizilen grafik). Hepsi `kabalang calistir` ile derlenip çalışır; web uygulamaları yalnız 127.0.0.1'i dinler, SQL parametre bağlamalı, HTML çıktısı kaçırılır.
218 lines
8.5 KiB
HTML
218 lines
8.5 KiB
HTML
<!doctype html>
|
|
<html lang="tr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Pano · KabaLang</title>
|
|
<style>
|
|
:root {
|
|
--zemin: #0a0d15; --yuzey: #121826; --yuzey2: #182033; --cizgi: #232c42;
|
|
--metin: #e6ebf5; --soluk: #8a94ab; --mavi: #377ef4; --cam: #22d3ee;
|
|
--yesil: #34d399; --sari: #fbbf24; --kirmizi: #f87171;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0; min-height: 100vh; color: var(--metin);
|
|
font: 15px/1.5 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
background:
|
|
radial-gradient(900px 500px at 10% -10%, #377ef42a, transparent 60%),
|
|
radial-gradient(700px 400px at 100% 0%, #22d3ee1c, transparent 60%),
|
|
var(--zemin);
|
|
}
|
|
header {
|
|
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
padding: 22px 32px; border-bottom: 1px solid var(--cizgi);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
.logo {
|
|
width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center;
|
|
background: linear-gradient(135deg, var(--mavi), var(--cam)); font-weight: 800; color: #fff;
|
|
box-shadow: 0 6px 22px #377ef455;
|
|
}
|
|
h1 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
|
|
.alt { color: var(--soluk); font-size: 13px; }
|
|
.rozet {
|
|
margin-left: auto; font-size: 12px; color: var(--cam); border: 1px solid #22d3ee44;
|
|
padding: 5px 11px; border-radius: 999px; background: #22d3ee10;
|
|
}
|
|
main { padding: 26px 32px 40px; max-width: 1360px; margin: 0 auto; }
|
|
form.ekle {
|
|
display: grid; grid-template-columns: 2fr 2fr 1fr auto; gap: 10px;
|
|
background: var(--yuzey); border: 1px solid var(--cizgi); border-radius: 16px; padding: 14px;
|
|
margin-bottom: 24px;
|
|
}
|
|
input, select, button {
|
|
font: inherit; color: var(--metin); background: var(--yuzey2);
|
|
border: 1px solid var(--cizgi); border-radius: 10px; padding: 10px 12px; min-width: 0;
|
|
}
|
|
input:focus, select:focus { outline: 2px solid #377ef488; border-color: transparent; }
|
|
button {
|
|
background: linear-gradient(135deg, var(--mavi), #2563eb); border: 0; font-weight: 600;
|
|
cursor: pointer; padding: 10px 18px; box-shadow: 0 6px 18px #377ef440;
|
|
}
|
|
.sutunlar { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
|
|
.sutun {
|
|
background: #121826cc; border: 1px solid var(--cizgi); border-radius: 18px;
|
|
padding: 14px; min-height: 380px; transition: border-color .15s, background .15s;
|
|
}
|
|
.sutun.ustunde { border-color: var(--mavi); background: #377ef412; }
|
|
.sutun h2 {
|
|
font-size: 13px; text-transform: uppercase; letter-spacing: .08em; color: var(--soluk);
|
|
margin: 4px 6px 14px; display: flex; align-items: center; gap: 8px;
|
|
}
|
|
.nokta { width: 9px; height: 9px; border-radius: 50%; }
|
|
.sayi { margin-left: auto; background: var(--yuzey2); border-radius: 999px; padding: 1px 9px; font-size: 12px; }
|
|
.kart {
|
|
background: var(--yuzey2); border: 1px solid var(--cizgi); border-radius: 14px;
|
|
padding: 12px 14px; margin-bottom: 10px; cursor: grab; position: relative;
|
|
transition: transform .12s, box-shadow .12s;
|
|
}
|
|
.kart:hover { transform: translateY(-2px); box-shadow: 0 10px 26px #0008; }
|
|
.kart.sur { opacity: .45; }
|
|
.kart b { display: block; font-weight: 600; padding-right: 26px; }
|
|
.kart p { margin: 4px 0 10px; color: var(--soluk); font-size: 13px; }
|
|
.alt-satir { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--soluk); }
|
|
.etiket { padding: 2px 9px; border-radius: 999px; font-weight: 600; font-size: 11px; }
|
|
.yuksek { color: var(--kirmizi); background: #f8717118; }
|
|
.orta { color: var(--sari); background: #fbbf2418; }
|
|
.dusuk { color: var(--yesil); background: #34d39918; }
|
|
.sil {
|
|
position: absolute; top: 8px; right: 8px; background: none; box-shadow: none; color: var(--soluk);
|
|
padding: 2px 8px; font-size: 16px; opacity: 0; transition: opacity .15s;
|
|
}
|
|
.kart:hover .sil { opacity: 1; }
|
|
.tasi { display: none; gap: 6px; margin-top: 8px; }
|
|
.tasi button { padding: 5px 10px; font-size: 12px; box-shadow: none; background: var(--yuzey); border: 1px solid var(--cizgi); }
|
|
footer { text-align: center; color: var(--soluk); font-size: 12px; padding: 0 0 28px; }
|
|
footer code { color: var(--cam); }
|
|
@media (max-width: 820px) {
|
|
header, main { padding-left: 16px; padding-right: 16px; }
|
|
form.ekle { grid-template-columns: 1fr 1fr; }
|
|
form.ekle input[name=baslik], form.ekle button { grid-column: 1 / -1; }
|
|
.sutunlar { grid-template-columns: 1fr; }
|
|
.sutun { min-height: 0; }
|
|
.tasi { display: flex; }
|
|
.sil { opacity: 1; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="logo">K</div>
|
|
<div>
|
|
<h1>Proje Panosu</h1>
|
|
<div class="alt">Sunucusu KabaLang ile yazıldı · SQLite · REST API</div>
|
|
</div>
|
|
<span class="rozet" id="ozet">yükleniyor…</span>
|
|
</header>
|
|
<main>
|
|
<form class="ekle" id="form">
|
|
<input name="baslik" placeholder="Yeni görev başlığı" maxlength="120" required>
|
|
<input name="aciklama" placeholder="Kısa açıklama (isteğe bağlı)" maxlength="200">
|
|
<select name="oncelik">
|
|
<option value="orta">Orta öncelik</option>
|
|
<option value="yuksek">Yüksek öncelik</option>
|
|
<option value="dusuk">Düşük öncelik</option>
|
|
</select>
|
|
<button type="submit">+ Ekle</button>
|
|
</form>
|
|
<div class="sutunlar" id="sutunlar"></div>
|
|
</main>
|
|
<footer>Kartları sütunlar arasında sürükleyin · <code>GET /api/gorevler</code></footer>
|
|
<script>
|
|
const SUTUNLAR = [
|
|
["yapilacak", "Yapılacak", "#8a94ab"],
|
|
["suruyor", "Sürüyor", "#377ef4"],
|
|
["bitti", "Bitti", "#34d399"],
|
|
];
|
|
const ONCELIK = { yuksek: "Yüksek", orta: "Orta", dusuk: "Düşük" };
|
|
const kok = document.getElementById("sutunlar");
|
|
let gorevler = [];
|
|
|
|
async function api(yontem, yol, govde) {
|
|
const y = await fetch(yol, {
|
|
method: yontem,
|
|
headers: govde ? { "Content-Type": "application/json" } : {},
|
|
body: govde ? JSON.stringify(govde) : undefined,
|
|
});
|
|
if (!y.ok && y.status !== 204) throw new Error((await y.json()).hata || y.status);
|
|
return y.status === 204 ? null : y.json();
|
|
}
|
|
|
|
function el(etiket, sinif, metin) {
|
|
const e = document.createElement(etiket);
|
|
if (sinif) e.className = sinif;
|
|
if (metin !== undefined) e.textContent = metin; // textContent → XSS yok
|
|
return e;
|
|
}
|
|
|
|
function ciz() {
|
|
kok.replaceChildren();
|
|
for (const [ad, baslik, renk] of SUTUNLAR) {
|
|
const liste = gorevler.filter(g => g.sutun === ad);
|
|
const s = el("section", "sutun");
|
|
s.dataset.sutun = ad;
|
|
const h = el("h2");
|
|
const n = el("span", "nokta"); n.style.background = renk;
|
|
h.append(n, baslik, el("span", "sayi", liste.length));
|
|
s.append(h);
|
|
for (const g of liste) s.append(kart(g));
|
|
s.addEventListener("dragover", e => { e.preventDefault(); s.classList.add("ustunde"); });
|
|
s.addEventListener("dragleave", () => s.classList.remove("ustunde"));
|
|
s.addEventListener("drop", e => {
|
|
e.preventDefault(); s.classList.remove("ustunde");
|
|
tasi(+e.dataTransfer.getData("text/plain"), ad);
|
|
});
|
|
kok.append(s);
|
|
}
|
|
const bitti = gorevler.filter(g => g.sutun === "bitti").length;
|
|
document.getElementById("ozet").textContent =
|
|
`${gorevler.length} görev · %${gorevler.length ? Math.round(100 * bitti / gorevler.length) : 0} tamam`;
|
|
}
|
|
|
|
function kart(g) {
|
|
const k = el("article", "kart");
|
|
k.draggable = true;
|
|
k.addEventListener("dragstart", e => { e.dataTransfer.setData("text/plain", g.id); k.classList.add("sur"); });
|
|
k.addEventListener("dragend", () => k.classList.remove("sur"));
|
|
k.append(el("b", "", g.baslik));
|
|
if (g.aciklama) k.append(el("p", "", g.aciklama));
|
|
const alt = el("div", "alt-satir");
|
|
alt.append(el("span", "etiket " + g.oncelik, ONCELIK[g.oncelik] || g.oncelik), "#" + g.id);
|
|
k.append(alt);
|
|
const sil = el("button", "sil", "×");
|
|
sil.title = "Sil";
|
|
sil.onclick = async () => { await api("DELETE", "/api/gorevler/" + g.id); yukle(); };
|
|
k.append(sil);
|
|
const t = el("div", "tasi");
|
|
for (const [ad, baslik] of SUTUNLAR) if (ad !== g.sutun) {
|
|
const b = el("button", "", "→ " + baslik);
|
|
b.onclick = () => tasi(g.id, ad);
|
|
t.append(b);
|
|
}
|
|
k.append(t);
|
|
return k;
|
|
}
|
|
|
|
async function tasi(id, sutun) {
|
|
await api("PUT", "/api/gorevler/" + id, { sutun });
|
|
yukle();
|
|
}
|
|
|
|
async function yukle() { gorevler = await api("GET", "/api/gorevler"); ciz(); }
|
|
|
|
document.getElementById("form").addEventListener("submit", async e => {
|
|
e.preventDefault();
|
|
const f = new FormData(e.target);
|
|
await api("POST", "/api/gorevler", {
|
|
baslik: f.get("baslik"), aciklama: f.get("aciklama"), oncelik: f.get("oncelik"), sutun: "yapilacak",
|
|
});
|
|
e.target.reset();
|
|
yukle();
|
|
});
|
|
|
|
yukle();
|
|
</script>
|
|
</body>
|
|
</html>
|