diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md
index 2eed47a..3817f7b 100644
--- a/docs/tasarim/DESIGN.md
+++ b/docs/tasarim/DESIGN.md
@@ -133,6 +133,22 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
(`next-themes`, ``). Renklerin tek kaynağı `src/app/globals.css` içindeki
`.dark` bloğu.
+**Geçiş ve kısayol (7 Eki 2026; son hâli 9 Eki):** tema değişimi anlık değildir; yeni
+tema header'daki logodan **yumuşak bir halka** gibi açılır: kenarı keskin değil, geniş ve
+tüylü (ışık yayılır gibi), kısa ve hafif: 600 ms. **Geçiş bitince** logonun kareleri
+yeni temanın biçimine dönüşür: koyuya geçişte şeftali kare hilale, mavi kare hilalin iki ucunun
+ortasındaki yıldıza (turuncu söner); açığa geçişte turuncu kare güneşe, şeftali kare halesine. Kısa bir an durur, yeniden
+logoya toplanır (`animasyonlu-mark.tsx` `sekilOynat`, 1,7 sn; yalnız header'daki logo; hover döngüsünden
+önceliklidir, biçim bitince imleç üzerindeyse döngü kaldığı yerden değil yerinden sürer). Tema
+seçicinin ikonları aynı biçimlerin durağan hâlidir (ayrıntı ve kurallar §4.34) (`tema-ikonu.tsx`: güneş, ay, Sistem için
+logonun üç karesi); burada lucide ikonu kullanılmaz. View Transitions + radyal degrade
+maskesi: `src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket azaltılmışsa düz
+değişir. Denenip bırakılanlar: logo biçimli maske ("logo içinde logo"), düz crossfade, piksel
+ızgarası (sert bulundu). Kural: bu geçişte sert kenar ve ikinci bir şekil yok.
+`D` tuşu açık ↔ koyu geçişi yapar (`src/components/tema-kisayolu.tsx`); ipucu seçicinin
+altında `Kbd` ile durur, başka yere kopyalanmaz. Header'daki Beta rozeti `/surum-notlari`na
+bağlantıdır.
+
**Nasıl çalışır:** bileşenlere `dark:` yazılmaz; palet token'ları çevrilir.
| Açık temadaki sınıf | Koyu temada ne olur |
@@ -1276,6 +1292,8 @@ Hareket kuralları (Apple, `motion/react`):
- Kaydırmaya bağlı tetik `IntersectionObserver` ile (kaydırma dinleyicisi değil).
- "Hareketi azalt": kayma/yay yok, yalnız opaklık (`useReducedMotion`,
`motion-reduce:transition-opacity`).
+- Logo kareleri bir durumu anlatmak için kısa bir an başka biçime dönüşebilir (§4.34); sayfa
+ geçişiyle üst üste binmez, sırayla oynar.
## 7. Yap / Yapma
@@ -1293,6 +1311,7 @@ Hareket kuralları (Apple, `motion/react`):
| Gösterge kareleri masonry (kaymış sütun, farklı boy) | Hizalı eşit ızgara; 8 px'lik "neredeyse hizalı" kaymalar |
| Liste satırında ad sarar, sayı alt satıra iner | `truncate` ile yarım ad; 32 px satır butonu |
| Hareket: `Reveal`, piksel ısısı, `kt-unblur`, sekmesiz yay; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` |
+| Tema ikonu ve logo dönüşümü: logonun üç karesinden güneş / ay (§4.34) | Tema seçicide lucide `Sun`/`Moon`; logoyu döngüde sürekli dönüştürmek |
| Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket, 10–11 px yazı |
| Risk rengi + metin etiketi; tür ayrımı ikonla | Renk tek başına, "yerleşirsin" gibi vaat cümlesi |
| Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi |
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index edb57ac..c0133dd 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -43,6 +43,7 @@ const staticEntries: MetadataRoute.Sitemap = [
{ url: `${SITE_URL}/paket`, priority: 0.7, changeFrequency: "monthly" },
{ url: `${SITE_URL}/meraklisina`, priority: 0.6, changeFrequency: "monthly" },
{ url: `${SITE_URL}/iletisim`, priority: 0.3, changeFrequency: "yearly" },
+ { url: `${SITE_URL}/surum-notlari`, priority: 0.3, changeFrequency: "monthly" },
{ url: `${SITE_URL}/gizlilik`, priority: 0.2, changeFrequency: "yearly" },
{
url: `${SITE_URL}/cerez-politikasi`,
diff --git a/src/app/surum-notlari/page.tsx b/src/app/surum-notlari/page.tsx
new file mode 100644
index 0000000..8b0282f
--- /dev/null
+++ b/src/app/surum-notlari/page.tsx
@@ -0,0 +1,114 @@
+import type { Metadata } from "next";
+import { ArayuzBilgisi } from "@/components/arayuz-bilgisi";
+import { Parallax } from "@/components/parallax";
+import { GeriBildirimFormu } from "@/features/geri-bildirim/components/geri-bildirim-formu";
+import { SiteFooter } from "@/components/site-footer";
+import { SURUM } from "@/lib/surum";
+import { sayfaMetadata } from "@/lib/seo";
+
+export const metadata: Metadata = sayfaMetadata({
+ title: "Sürüm notları",
+ description:
+ "KolayTercih beta sürüm notları: neler değişti ve geri bildirimini nasıl iletirsin.",
+ path: "/surum-notlari",
+});
+
+/**
+ * Sürüm günlüğü. En yeni en üstte; her sürüm package.json'daki numarayla
+ * aynı adı taşır (AGENTS.md "Sürümleme"). Yeni bir sürüm çıkarken buraya bir
+ * kayıt eklenir. Yalnız görünüşü etkileyen değişiklikler `arayuz` alanına
+ * yazılır: listede tek satır ("Arayüz iyileştirmeleri") olur, ayrıntısı
+ * yanındaki bilgi düğmesinin balonunda durur.
+ */
+const SURUMLER: {
+ surum: string;
+ tarih: string;
+ maddeler: string[];
+ arayuz?: string[];
+}[] = [
+ {
+ surum: "1.1.0",
+ tarih: "7 Ekim 2026",
+ maddeler: [],
+ arayuz: [
+ "D tuşu açık / koyu temayı değiştirir; geçiş logodan yayılarak yumuşak oynar.",
+ "Bu sayfa: beta durumu, sürüm günlüğü ve geri bildirim kutusu. Header'daki Beta rozeti buraya getirir.",
+ ],
+ },
+ {
+ surum: "1.0.0",
+ tarih: "7 Ekim 2026",
+ maddeler: [
+ "Sıralama, alan, il ve devlet/vakıf tercihine göre 24 tercihlik dengeli liste: hayal, dengeli ve güvenli dilimler.",
+ "Her program için YÖK Atlas verisinden taban sıralama, kontenjan ve yıllara göre eğilim.",
+ "Yapay zekâ destekli tercih raporu ve liste üzerinde soru sorabildiğin danışman sohbeti.",
+ "Üniversite ve bölüm kataloğu, üniversite sıralamaları, rehber yazıları.",
+ ],
+ arayuz: ["Açık, koyu ve sistem teması."],
+ },
+];
+
+export default function SurumNotlariPage() {
+ return (
+ <>
+
+
+
Sürüm notları
+
+ KolayTercih şu an beta sürümünde. Ürün çalışıyor ve gerçek YÖK
+ Atlas verisiyle liste üretiyor; ama hâlâ her gün bir şeyler
+ düzeltiyor, eksik gördüğümüz yerleri tamamlıyoruz. Her sürümde
+ neyin değiştiği aşağıda.
+
+
+
+ {/* Zaman çizgisi: her sürümün piksel karesi solda durur, kareleri
+ ince bir çizgi birbirine bağlar (karenin ortasından bir sonraki
+ karenin ortasına). En eski sürümde çizgi biter. */}
+
+ {SURUMLER.map((s, i) => (
+
+ {i < SURUMLER.length - 1 && (
+
+ )}
+
+
+
+ v{s.surum}
+
+
+ {s.surum === SURUM && (
+
+ Şu anki sürüm
+
+ )}
+
+
+ {s.maddeler.map((m) => (
+
{m}
+ ))}
+ {s.arayuz && (
+
+
+ Arayüz iyileştirmeleri
+
+
+
+ )}
+
+
+ ))}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/arayuz-bilgisi.tsx b/src/components/arayuz-bilgisi.tsx
new file mode 100644
index 0000000..fc1c261
--- /dev/null
+++ b/src/components/arayuz-bilgisi.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { useRef, useState } from "react";
+import { Info } from "lucide-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+/**
+ * Sürüm notlarında "Arayüz iyileştirmeleri" satırının yanındaki bilgi düğmesi.
+ * Düğme ve balon, danışman sohbetindeki "Danışmanın bildikleri" düğmesiyle
+ * birebir aynıdır (`sohbet-client.tsx`). Fark: dokunmatikte üzerine gelme
+ * olmadığı için dokununca da açılıp kapanır.
+ */
+export function ArayuzBilgisi({ maddeler }: { maddeler: string[] }) {
+ const [acik, setAcik] = useState(false);
+ // Basış anındaki durum: Radix basışta balonu kapatır, tık ona göre çevirir.
+ const basistaAcikti = useRef(false);
+
+ return (
+
+
+
+
+
+
+
Bu sürümdeki arayüz iyileştirmeleri:
+
+ {maddeler.map((m) => (
+
{m}
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx
index 453a5a9..e9b945e 100644
--- a/src/components/site-footer.tsx
+++ b/src/components/site-footer.tsx
@@ -201,6 +201,12 @@ export function SiteFooter() {
>
İletişim
+
+ Sürüm notları
+
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları
diff --git a/src/components/site-header.tsx b/src/components/site-header.tsx
index 8c4ea01..357f6c1 100644
--- a/src/components/site-header.tsx
+++ b/src/components/site-header.tsx
@@ -33,10 +33,12 @@ export function SiteHeader() {
Tercih
- {/* Beta rozeti + sürüm (surum-rozeti.tsx); yazı logosuyla aynı
- fade'i taşır ki NavBaslik yuvası açılınca birlikte sönsün. */}
-
+ {/* Beta rozeti + sürüm (surum-rozeti.tsx): /surum-notlari'na giden
+ ayrı bir Link — logo Link'inin içinde bağlantı içinde bağlantı
+ olmasın. Yazı logosuyla aynı fade'i taşır ki NavBaslik yuvası
+ açılınca birlikte sönsün. */}
+
-
+
+
Beta
v{SURUM}
-
+
);
}
diff --git a/src/features/geri-bildirim/components/geri-bildirim-formu.tsx b/src/features/geri-bildirim/components/geri-bildirim-formu.tsx
new file mode 100644
index 0000000..af72c78
--- /dev/null
+++ b/src/features/geri-bildirim/components/geri-bildirim-formu.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+// Sürüm notları sayfasının altındaki geri bildirim kutusu: başlık, metin
+// alanı, buton. Mesaj geriBildirimGonder sunucu aksiyonuyla destek kutusuna
+// e-posta olarak gider.
+
+import { useState } from "react";
+import { ArrowRight } from "lucide-react";
+import { toast } from "@/components/ui/bildirim";
+import { Button } from "@/components/ui/button";
+import { Textarea } from "@/components/ui/textarea";
+import { TypingAnimation } from "@/components/ui/typing-animation";
+import { geriBildirimGonder } from "@/features/geri-bildirim/geri-bildirim-actions";
+
+const BASLIK = "Bir hata mı gördün?";
+const DEVAM = "Yalın üretim metodunu benimsiyor ve fikirlerine önem veriyoruz.";
+
+export function GeriBildirimFormu({ className = "" }: { className?: string }) {
+ const [mesaj, setMesaj] = useState("");
+ const [pending, setPending] = useState(false);
+
+ async function gonder(e: React.FormEvent) {
+ e.preventDefault();
+ setPending(true);
+ try {
+ const sonuc = await geriBildirimGonder(mesaj);
+ if (sonuc.ok) {
+ toast.success("Geri bildirimin ulaştı, teşekkürler.");
+ setMesaj("");
+ } else {
+ toast.error(sonuc.error);
+ }
+ } catch {
+ toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?");
+ } finally {
+ setPending(false);
+ }
+ }
+
+ // Dış kart yok: başlık sayfadaki sürüm başlıklarıyla aynı ölçekte durur,
+ // altında tek yüzey var — metin alanı kenarlıksız, buton aynı yüzeyin
+ // içinde (sohbet kutusuyla aynı kalıp, DESIGN.md §4.6).
+ return (
+
+
+
+ );
+}
diff --git a/src/features/geri-bildirim/geri-bildirim-actions.ts b/src/features/geri-bildirim/geri-bildirim-actions.ts
new file mode 100644
index 0000000..a89306b
--- /dev/null
+++ b/src/features/geri-bildirim/geri-bildirim-actions.ts
@@ -0,0 +1,82 @@
+"use server";
+
+import { headers } from "next/headers";
+import { getSession } from "@/lib/session";
+import { geriBildirimEpostasi } from "@/lib/eposta";
+import { epostaGonder } from "@/lib/eposta-gonder";
+import { SURUM } from "@/lib/surum";
+
+const DESTEK_ADRESI = "destek@kolaytercih.com";
+const MIN_UZUNLUK = 10;
+const MAX_UZUNLUK = 2000;
+
+// Uç oturumsuz da çalışır; kötüye kullanım yalnız kendi destek kutumuzu ve
+// Resend kotasını doldurur. Tek süreçte bellek içi sayaç bunu sınırlamaya
+// yeter: IP başına 10 dakikada 3 mesaj.
+const PENCERE_MS = 10 * 60 * 1000;
+const PENCERE_LIMIT = 3;
+const sonGonderimler = new Map();
+
+function sinirAsildi(ip: string): boolean {
+ const simdi = Date.now();
+ const taze = (sonGonderimler.get(ip) ?? []).filter(
+ (t) => simdi - t < PENCERE_MS,
+ );
+ if (taze.length >= PENCERE_LIMIT) {
+ sonGonderimler.set(ip, taze);
+ return true;
+ }
+ taze.push(simdi);
+ sonGonderimler.set(ip, taze);
+ // Harita sınırsız büyümesin: eskimiş kayıtları ara sıra süpür.
+ if (sonGonderimler.size > 1000) {
+ for (const [anahtar, zamanlar] of sonGonderimler) {
+ if (zamanlar.every((t) => simdi - t >= PENCERE_MS)) {
+ sonGonderimler.delete(anahtar);
+ }
+ }
+ }
+ return false;
+}
+
+export async function geriBildirimGonder(
+ hamMesaj: string,
+): Promise<{ ok: true } | { ok: false; error: string }> {
+ const mesaj = typeof hamMesaj === "string" ? hamMesaj.trim() : "";
+ if (mesaj.length < MIN_UZUNLUK) {
+ return { ok: false, error: "Biraz daha ayrıntı yazar mısın?" };
+ }
+ if (mesaj.length > MAX_UZUNLUK) {
+ return {
+ ok: false,
+ error: `Mesaj en fazla ${MAX_UZUNLUK} karakter olabilir.`,
+ };
+ }
+
+ const [session, h] = await Promise.all([getSession(), headers()]);
+ const ip = h.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "bilinmiyor";
+ if (sinirAsildi(ip)) {
+ return {
+ ok: false,
+ error: "Kısa sürede çok mesaj gönderdin. Birkaç dakika sonra tekrar dene.",
+ };
+ }
+
+ try {
+ await epostaGonder(
+ DESTEK_ADRESI,
+ geriBildirimEpostasi({
+ mesaj,
+ gonderen: session?.user.email ?? null,
+ surum: SURUM,
+ }),
+ );
+ return { ok: true };
+ } catch (hata) {
+ console.error("[geri-bildirim] Gönderilemedi:", hata);
+ return {
+ ok: false,
+ error: `Mesajın gönderilemedi. ${DESTEK_ADRESI} adresine yazabilirsin.`,
+ };
+ }
+}
diff --git a/src/lib/eposta.ts b/src/lib/eposta.ts
index f5dc673..fc5b874 100644
--- a/src/lib/eposta.ts
+++ b/src/lib/eposta.ts
@@ -210,4 +210,40 @@ export function krediHatirlatmaEpostasi(): EpostaIcerik {
...METIN_ALT_BILGI,
].join("\n"),
};
-}
\ No newline at end of file
+}
+
+/**
+ * Sürüm notları sayfasındaki geri bildirim kutusundan gelen mesaj — destek
+ * kutusuna düşer, kullanıcıya gitmez. `gonderen` oturum varsa hesabın
+ * e-postasıdır; yoksa mesaj anonimdir.
+ */
+export function geriBildirimEpostasi({
+ mesaj,
+ gonderen,
+ surum,
+}: {
+ mesaj: string;
+ gonderen: string | null;
+ surum: string;
+}): EpostaIcerik {
+ const konu = `Geri bildirim (v${surum})`;
+ const kimden = gonderen ?? "Anonim (oturum yok)";
+ const govde = `
+