KabaLang örnekleri: konsol, masaüstü ve web için dokuz proje
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. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Anket — canlı oylama
|
||||
|
||||

|
||||
|
||||
Tek soruluk canlı anket. Oylar SQLite'ta tutulur, aynı adresten ikinci oy **veritabanı
|
||||
kısıtıyla** engellenir, sonuç grafiği her istekte sunucuda KabaLang ile **SVG** olarak çizilir.
|
||||
|
||||
```bash
|
||||
kabalang calistir web/anket/anket.kbl
|
||||
# → http://127.0.0.1:8473
|
||||
```
|
||||
|
||||
## Neyi gösteriyor
|
||||
|
||||
| Konu | Nerede |
|
||||
|---|---|
|
||||
| `yapi Secenek` + tipli alanlar, yapı listesi döndüren fonksiyon (`-> liste<Secenek>`) | `secenekler` |
|
||||
| Gövdesiz form: oy kimliği yolda (`POST /oy/{id}`) | `oy_ver` |
|
||||
| POST → **303** → GET deseni (sayfa yenilenince oy tekrar gitmez) | `oy_ver` |
|
||||
| Aynı grafiği iki genişlikte üretmek (masaüstü / telefon) | `grafik_svg(liste, isaretli, gen)` |
|
||||
| Doğru içerik tipiyle tek başına SVG: `.tip("image/svg+xml")` | `/sonuc.svg` |
|
||||
| JSON'u SQLite'ın `json_object` işlevine kurdurmak | `/api/sonuclar` |
|
||||
|
||||

|
||||
|
||||
## Güvenlik
|
||||
|
||||
- **SQL enjeksiyonu:** seçenek kimliği ve istemci adresi sorguya `?` ile bağlanır.
|
||||
`/oy/1 OR 1=1` var olmayan bir seçenektir → 404.
|
||||
- **Çift oy:** `oy.adres` sütunu `UNIQUE`; denetim ile kayıt tek iş parçacıklı sunucuda sırayla
|
||||
koşar, yarış koşulu yoktur.
|
||||
- Sunucu yalnız `127.0.0.1`'i dinler. (Yerelde her istemci aynı adres olduğu için tarayıcı
|
||||
bir kez oy verebilir — anketi sıfırlamak için `anket.db`'yi silin.)
|
||||
|
||||
Veritabanı yolu `ANKET_DB` ile değiştirilebilir (varsayılan `anket.db`).
|
||||
@@ -0,0 +1,197 @@
|
||||
# ╔══════════════════════════════════════════════════════════════════╗
|
||||
# ║ ANKET — KabaLang ile canlı anket ║
|
||||
# ║ ║
|
||||
# ║ · Oy: POST /oy/{id} — gövdesiz form, kimlik yolda ║
|
||||
# ║ · Aynı adresten ikinci oy veritabanı kısıtıyla engellenir ║
|
||||
# ║ · Sonuç grafiği sunucuda KabaLang ile SVG olarak çizilir ║
|
||||
# ║ · /sonuc.svg tek başına gömülebilir · /api/sonuclar JSON ║
|
||||
# ╚══════════════════════════════════════════════════════════════════╝
|
||||
#
|
||||
# Çalıştır: kabalang calistir web/anket/anket.kbl
|
||||
# Aç: http://127.0.0.1:8473
|
||||
|
||||
kullan kaba.web
|
||||
kullan kaba.json
|
||||
kullan kaba.veritabani
|
||||
kullan kaba.sistem
|
||||
|
||||
SORU = "Bir sonraki KabaLang örneği ne olsun?"
|
||||
PORT = 8473
|
||||
|
||||
db = cevre.al("ANKET_DB")
|
||||
eger db == "":
|
||||
db = "anket.db"
|
||||
|
||||
yapi Secenek:
|
||||
kimlik: sayi = 0
|
||||
ad: metin = ""
|
||||
renk: metin = ""
|
||||
oy: sayi = 0
|
||||
|
||||
# ── Veritabanı ──────────────────────────────────────────────────────
|
||||
|
||||
fonksiyon hazirla(db):
|
||||
# `adres UNIQUE`: aynı istemciden ikinci oy veritabanı düzeyinde reddedilir.
|
||||
veritabani.sqlite(db, "CREATE TABLE IF NOT EXISTS secenek(id INTEGER PRIMARY KEY, ad TEXT, renk TEXT); CREATE TABLE IF NOT EXISTS oy(secenek INTEGER, adres TEXT UNIQUE, zaman TEXT);")
|
||||
eger veritabani.sqlite(db, "SELECT count(*) FROM secenek") != "0":
|
||||
don 0
|
||||
adlar = ["2B oyun", "Yapay zekâ asistanı", "Veri panosu", "Sohbet uygulaması", "Sistem servisi"]
|
||||
renkler = ["#377ef4", "#22d3ee", "#a78bfa", "#34d399", "#fbbf24"]
|
||||
ornek_oy = [18, 27, 12, 21, 7]
|
||||
dongu i icinde 0..adlar.uzunluk() - 1:
|
||||
veritabani.sqlite(db, "INSERT INTO secenek(id, ad, renk) VALUES (?, ?, ?)", [metin(i + 1), adlar[i], renkler[i]])
|
||||
# Tanıtım için önceden verilmiş oylar (adres alanı uydurma ve benzersiz).
|
||||
dongu n icinde 1..ornek_oy[i]:
|
||||
veritabani.sqlite(db, "INSERT INTO oy(secenek, adres, zaman) VALUES (?, ?, datetime('now', 'localtime'))", [metin(i + 1), "ornek-" + metin(i) + "-" + metin(n)])
|
||||
don 1
|
||||
|
||||
fonksiyon secenekler(db) -> liste<Secenek>:
|
||||
sonuc = [Secenek()]
|
||||
sonuc.sil(0)
|
||||
ham = veritabani.sqlite(db, "SELECT s.id, s.renk, (SELECT count(*) FROM oy WHERE oy.secenek = s.id), s.ad FROM secenek s ORDER BY s.id")
|
||||
dongu satir icinde ham.bol("\n"):
|
||||
p = satir.bol("|")
|
||||
sonuc.ekle(Secenek(sayi(p[0]), p.dilim(3, p.uzunluk() - 3).birlestir("|"), p[1], sayi(p[2])))
|
||||
don sonuc
|
||||
|
||||
fonksiyon toplam_oy(liste):
|
||||
t = 0
|
||||
dongu s icinde liste:
|
||||
t = t + s.oy
|
||||
don t
|
||||
|
||||
fonksiyon verdigi_oy(db, adres):
|
||||
don veritabani.sqlite(db, "SELECT secenek FROM oy WHERE adres = ?", [adres])
|
||||
|
||||
# ── SVG grafik: tamamen KabaLang ile ────────────────────────────────
|
||||
|
||||
# `gen`: çizim genişliği. Sayfa aynı grafiği iki genişlikte üretir
|
||||
# (masaüstü / telefon) ki yazılar küçülmesin.
|
||||
fonksiyon grafik_svg(liste, isaretli, gen):
|
||||
toplam = toplam_oy(liste)
|
||||
eger toplam == 0:
|
||||
toplam = 1
|
||||
# Çubuk boyu en çok oy alana göre ölçeklenir: lider tam dolar.
|
||||
en_cok = 1
|
||||
dongu sec icinde liste:
|
||||
eger sec.oy > en_cok:
|
||||
en_cok = sec.oy
|
||||
satir_yuk = 64
|
||||
yuk = liste.uzunluk() * satir_yuk + 10
|
||||
s = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 " + metin(gen) + " " + metin(yuk) + "' font-family='Inter, system-ui, sans-serif'>"
|
||||
i = 0
|
||||
dongu sec icinde liste:
|
||||
y = i * satir_yuk + 8
|
||||
yuzde = sec.oy * 100 // toplam
|
||||
cubuk = (gen - 120) * sec.oy // en_cok
|
||||
eger cubuk < 6:
|
||||
cubuk = 6
|
||||
kalinlik = "500"
|
||||
eger metin(sec.kimlik) == isaretli:
|
||||
kalinlik = "800"
|
||||
# Ad metni kullanıcıdan gelmiyor ama yine de kaçırılır: alışkanlık.
|
||||
s = s + "<text x='0' y='" + metin(y + 14) + "' fill='#e6ebf5' font-size='15' font-weight='" + kalinlik + "'>" + web.kacir(sec.ad) + "</text>"
|
||||
s = s + "<rect x='0' y='" + metin(y + 24) + "' width='" + metin(gen - 120) + "' height='22' rx='11' fill='#182033'/>"
|
||||
s = s + "<rect x='0' y='" + metin(y + 24) + "' width='" + metin(cubuk) + "' height='22' rx='11' fill='" + sec.renk + "'/>"
|
||||
s = s + "<text x='" + metin(gen) + "' y='" + metin(y + 41) + "' fill='#e6ebf5' font-size='17' font-weight='700' text-anchor='end'>%" + metin(yuzde) + "</text>"
|
||||
s = s + "<text x='" + metin(gen - 52) + "' y='" + metin(y + 41) + "' fill='#8a94ab' font-size='13' text-anchor='end'>" + metin(sec.oy) + "</text>"
|
||||
i = i + 1
|
||||
don s + "</svg>"
|
||||
|
||||
# ── Sayfa iskeleti ──────────────────────────────────────────────────
|
||||
|
||||
STIL = """
|
||||
:root { --zemin:#0a0d15; --yuzey:#121826; --yuzey2:#182033; --cizgi:#232c42; --metin:#e6ebf5;
|
||||
--soluk:#8a94ab; --mavi:#377ef4; --cam:#22d3ee; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin:0; min-height:100vh; color:var(--metin); font:15px/1.55 Inter, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
background: radial-gradient(800px 460px at 20% -10%, #377ef430, transparent 60%),
|
||||
radial-gradient(700px 420px at 90% 110%, #a78bfa22, transparent 60%), var(--zemin);
|
||||
display:grid; place-items:center; padding: 32px 16px; }
|
||||
.kart { width: 100%; max-width: 820px; background: #121826e6; border:1px solid var(--cizgi); border-radius: 24px;
|
||||
padding: 34px 38px; box-shadow: 0 30px 80px #0009; }
|
||||
.ust { display:flex; align-items:center; gap:10px; color: var(--soluk); font-size: 13px; margin-bottom: 18px; }
|
||||
.canli { display:inline-flex; align-items:center; gap:7px; color:#34d399; margin-left:auto; }
|
||||
.canli::before { content:''; width:8px; height:8px; border-radius:50%; background:#34d399; box-shadow:0 0 0 4px #34d39928; }
|
||||
h1 { font-size: clamp(24px, 4vw, 34px); line-height: 1.15; letter-spacing: -.02em; margin: 0 0 24px; }
|
||||
.secenekler { display:grid; gap: 10px; }
|
||||
.secenekler form { margin:0; }
|
||||
.secenekler button { width:100%; display:flex; align-items:center; gap:14px; text-align:left; font:inherit; font-size:16px;
|
||||
color: var(--metin); background: var(--yuzey2); border:1px solid var(--cizgi); border-radius: 14px;
|
||||
padding: 15px 18px; cursor:pointer; transition: border-color .15s, transform .15s; }
|
||||
.secenekler button:hover { border-color: var(--mavi); transform: translateX(4px); }
|
||||
.top { width: 14px; height: 14px; border-radius: 50%; flex: none; }
|
||||
.alt { display:flex; justify-content: space-between; color: var(--soluk); font-size: 13px; margin-top: 22px; flex-wrap: wrap; gap: 8px; }
|
||||
.alt a { color: var(--cam); text-decoration:none; }
|
||||
.bilgi { background: #377ef414; border:1px solid #377ef444; color:#bcd3ff; border-radius: 12px; padding: 10px 14px; margin-bottom: 20px; font-size: 14px; }
|
||||
svg { width:100%; height:auto; display:block; }
|
||||
.dar { display: none; }
|
||||
@media (max-width: 600px) { .kart { padding: 24px 18px; border-radius: 20px; } .genis { display: none; } .dar { display: block; } }
|
||||
"""
|
||||
|
||||
fonksiyon sayfa(govde, yenile):
|
||||
ek = ""
|
||||
eger yenile:
|
||||
# "Canlı": sonuç sayfası 5 saniyede bir kendini tazeler.
|
||||
ek = "<meta http-equiv='refresh' content='5'>"
|
||||
don "<!doctype html><html lang='tr'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'>" + ek + "<title>Anket · KabaLang</title><style>" + STIL + "</style></head><body><main class='kart'>" + govde + "</main></body></html>"
|
||||
|
||||
fonksiyon ust_bant(toplam):
|
||||
don "<div class='ust'><b style='color:#e6ebf5'>KabaLang Anket</b> · " + metin(toplam) + " oy<span class='canli'>canlı</span></div>"
|
||||
|
||||
# ── Uçlar ───────────────────────────────────────────────────────────
|
||||
|
||||
hazirla(db)
|
||||
|
||||
fonksiyon sonuc_sayfasi(istek):
|
||||
liste = secenekler(db)
|
||||
benim = verdigi_oy(db, istek.adres())
|
||||
g = ust_bant(toplam_oy(liste)) + "<h1>" + web.kacir(SORU) + "</h1>"
|
||||
eger istek.sorgu("zaten") == "1":
|
||||
g = g + "<div class='bilgi'>Bu adresten zaten oy verilmiş — ikinci oy sayılmadı.</div>"
|
||||
degilse eger benim != "":
|
||||
g = g + "<div class='bilgi'>✓ Oyunuz kaydedildi. Sonuçlar 5 saniyede bir tazelenir.</div>"
|
||||
g = g + "<div class='genis'>" + grafik_svg(liste, benim, 760) + "</div>"
|
||||
g = g + "<div class='dar'>" + grafik_svg(liste, benim, 380) + "</div>"
|
||||
g = g + "<div class='alt'><span>Grafik sunucuda KabaLang ile çizildi · <a href='/sonuc.svg'>sonuc.svg</a> · <a href='/api/sonuclar'>JSON</a></span><a href='/'>← ankete dön</a></div>"
|
||||
don web.html(sayfa(g, dogru))
|
||||
|
||||
fonksiyon ana_sayfa(istek):
|
||||
# Daha önce oy verdiyse doğrudan sonuçları göster.
|
||||
eger verdigi_oy(db, istek.adres()) != "":
|
||||
don sonuc_sayfasi(istek)
|
||||
liste = secenekler(db)
|
||||
g = ust_bant(toplam_oy(liste)) + "<h1>" + web.kacir(SORU) + "</h1><div class='secenekler'>"
|
||||
dongu s icinde liste:
|
||||
g = g + "<form method='post' action='/oy/" + metin(s.kimlik) + "'><button><span class='top' style='background:" + s.renk + "'></span>" + web.kacir(s.ad) + "</button></form>"
|
||||
g = g + "</div><div class='alt'><span>Her adres bir kez oy verebilir.</span><a href='/sonuc'>sonuçları gör →</a></div>"
|
||||
don web.html(sayfa(g, yanlis))
|
||||
|
||||
fonksiyon oy_ver(istek):
|
||||
kimlik = istek.parametre("id")
|
||||
# Kimlik gerçekten var olan bir seçenek mi? (parametre bağlamalı)
|
||||
eger veritabani.sqlite(db, "SELECT count(*) FROM secenek WHERE id = ?", [kimlik]) != "1":
|
||||
don web.html(sayfa("<h1>Böyle bir seçenek yok</h1><div class='alt'><a href='/'>← ankete dön</a></div>", yanlis), 404)
|
||||
adres = istek.adres()
|
||||
eger verdigi_oy(db, adres) != "":
|
||||
don web.yanit("", 303).baslik("Location", "/sonuc?zaten=1")
|
||||
veritabani.sqlite(db, "INSERT OR IGNORE INTO oy(secenek, adres, zaman) VALUES (?, ?, datetime('now', 'localtime'))", [kimlik, adres])
|
||||
# POST → 303 → GET: sayfa yenilenince oy ikinci kez gönderilmez.
|
||||
don web.yanit("", 303).baslik("Location", "/sonuc")
|
||||
|
||||
fonksiyon sonuc_svg(istek):
|
||||
don web.yanit(grafik_svg(secenekler(db), verdigi_oy(db, istek.adres()), 760)).tip("image/svg+xml; charset=utf-8")
|
||||
|
||||
fonksiyon sonuc_json(_istek):
|
||||
don web.json(veritabani.sqlite(db, "SELECT json_object('soru', ?, 'toplam', (SELECT count(*) FROM oy), 'secenekler', (SELECT json_group_array(json_object('id', s.id, 'ad', s.ad, 'oy', (SELECT count(*) FROM oy WHERE oy.secenek = s.id))) FROM secenek s))", [SORU]))
|
||||
|
||||
# Tek iş parçacığı: "oy verdi mi?" denetimi ile kayıt arasında yarış olmasın.
|
||||
sunucu = web.sunucu(PORT, "127.0.0.1", 1)
|
||||
sunucu.al("/", ana_sayfa)
|
||||
sunucu.al("/sonuc", sonuc_sayfasi)
|
||||
sunucu.al("/sonuc.svg", sonuc_svg)
|
||||
sunucu.al("/api/sonuclar", sonuc_json)
|
||||
sunucu.gonder("/oy/{id}", oy_ver)
|
||||
|
||||
yazdir("🗳 Anket hazır — http://127.0.0.1:" + metin(PORT))
|
||||
sunucu.basla()
|
||||
Reference in New Issue
Block a user