v1-beta-tema #9

Merged
bilal merged 19 commits from v1-beta-tema into main 2026-10-09 02:30:44 +03:00
13 changed files with 395 additions and 42 deletions
Showing only changes of commit 5f7ea8bbcd - Show all commits

View File

@@ -133,6 +133,13 @@ Seçici `src/components/tema-secici.tsx` (footer'da), sağlayıcı `src/componen
(`next-themes`, `<html class="dark">`). Renklerin tek kaynağı `src/app/globals.css` içindeki
`.dark` bloğu.
**Geçiş ve kısayol (7 Eki 2026):** tema değişimi anlık değildir; yeni tema header'daki
KolayTercih işaretinin biçiminde logodan büyüyerek açılır (View Transitions + CSS maskesi:
`src/lib/tema-gecisi.ts`, `globals.css` `.kt-tema-gecis`; hareket azaltılmışsa düz değişir).
`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 |

View File

@@ -1,6 +1,6 @@
{
"name": "kolaytercih",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"scripts": {
"dev": "next dev",

View File

@@ -476,3 +476,72 @@
.toaster [data-sonner-toast][data-styled="true"] [data-button]:hover {
background: var(--color-slate-700);
}
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): tema değişirken yeni
görüntü, header'daki KolayTercih işaretinin biçiminde logodan büyüyerek
açılır. Eski görüntü yerinde durur; yeni görüntünün maskesi iki katman:
(1) işaretin SVG'si, merkezi --kt-tema-x/y'de, kenarı 0 → --kt-tema-son;
(2) aynı merkezden biraz geç kalkan bir daire — işaretin kareleri arasındaki
boşlukları kapatır ki geçiş sonunda eski temadan iz kalmasın. Hareket
azaltılmışsa JS geçişi hiç başlatmaz, tema düz değişir. */
@property --kt-tema-kenar {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
@property --kt-tema-yaricap {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
.kt-tema-gecis {
view-transition-name: root;
}
.kt-tema-gecis::view-transition-old(root),
.kt-tema-gecis::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
.kt-tema-gecis::view-transition-old(root) {
z-index: 1;
}
.kt-tema-gecis::view-transition-new(root) {
z-index: 2;
mask-image:
url("/kolay-tercih-mark.svg"),
radial-gradient(
circle at var(--kt-tema-x) var(--kt-tema-y),
#000 calc(var(--kt-tema-yaricap) - 1px),
transparent var(--kt-tema-yaricap)
);
mask-repeat: no-repeat;
mask-size:
var(--kt-tema-kenar) var(--kt-tema-kenar),
100% 100%;
mask-position:
calc(var(--kt-tema-x) - var(--kt-tema-kenar) / 2)
calc(var(--kt-tema-y) - var(--kt-tema-kenar) / 2),
0 0;
animation:
kt-tema-isaret 0.9s cubic-bezier(0.65, 0, 0.35, 1) both,
kt-tema-daire 0.9s cubic-bezier(0.65, 0, 0.35, 1) both;
}
@keyframes kt-tema-isaret {
from {
--kt-tema-kenar: 0px;
}
to {
--kt-tema-kenar: var(--kt-tema-son);
}
}
/* Daire işaretin ardından gelir: ilk %35'te sıfırda bekler. */
@keyframes kt-tema-daire {
0%,
35% {
--kt-tema-yaricap: 0px;
}
100% {
--kt-tema-yaricap: var(--kt-tema-son);
}
}

View File

@@ -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`,

View File

@@ -0,0 +1,149 @@
import type { Metadata } from "next";
import { ArrowRight, Mail } from "lucide-react";
import { Parallax } from "@/components/parallax";
import { PagePixelDivider, SectionEyebrow } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer";
import { Kbd } from "@/components/ui/kbd";
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, neler geliyor 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; "Geliyor" listesi bitenlerden arındırılır.
*/
const SURUMLER: {
surum: string;
tarih: string;
baslik: string;
maddeler: string[];
}[] = [
{
surum: "1.1.0",
tarih: "7 Ekim 2026",
baslik: "Tema kısayolu ve sürüm notları",
maddeler: [
"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 adresi. Header'daki Beta rozeti buraya getirir.",
],
},
{
surum: "1.0.0",
tarih: "7 Ekim 2026",
baslik: "Beta açılışı",
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ı.",
"Açık, koyu ve sistem teması.",
],
},
];
const GELIYOR = [
"Listeni PDF olarak indirme ve paylaşma.",
"Yerleşme sonuçları açıklandığında listenin gerçekleşme karnesi.",
"Veli görünümü: listeyi birlikte inceleyip not bırakma.",
];
export default function SurumNotlariPage() {
return (
<>
<main className="mx-auto w-full max-w-2xl px-4 py-16">
<SectionEyebrow>Beta · v{SURUM}</SectionEyebrow>
<Parallax fromStart strength={10}>
<h1 className="mt-4 font-heading text-3xl font-bold">Sürüm notları</h1>
</Parallax>
<PagePixelDivider seed={61} className="mt-6" />
<section
aria-labelledby="beta-baslik"
className="mt-6 rounded-2xl border border-slate-200 bg-card p-5 shadow-xs sm:p-6"
>
<h2 id="beta-baslik" className="font-heading text-xl font-bold">
KolayTercih şu an beta sürümünde
</h2>
<p className="mt-3 text-sm leading-7 text-slate-700">
Ü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. Bir hata, yanlış bir sayı, anlaşılmayan bir ekran
ya da “keşke şöyle olsa” dediğin bir şey görürsen bize yaz: her
geri bildirim okunur ve çoğu bir sonraki sürüme girer.
</p>
<a
href="mailto:destek@kolaytercih.com?subject=KolayTercih%20beta%20geri%20bildirimi"
className="mt-5 inline-flex items-center gap-2 rounded-full bg-slate-900 px-5 py-3 text-sm font-semibold text-slate-50 transition-[background-color,transform] duration-150 hover:bg-slate-700 active:scale-[0.97]"
>
<Mail className="size-4" aria-hidden />
Geri bildirim gönder
<ArrowRight className="size-4" aria-hidden />
</a>
<p className="mt-3 text-xs text-slate-500">
destek@kolaytercih.com · Hangi sayfada, hangi sıralamayla ne
gördüğünü yazarsan daha hızlı çözeriz. Kart bilgini asla e-postayla
paylaşma.
</p>
</section>
<section aria-labelledby="gunluk-baslik" className="mt-12">
<h2 id="gunluk-baslik" className="font-heading text-xl font-bold">
Neler değişti
</h2>
<ol className="mt-4 space-y-8">
{SURUMLER.map((s) => (
<li key={s.surum} className="relative pl-6">
<span
aria-hidden
className="absolute top-2 left-0 size-2.5 rounded-[2px] bg-primary"
/>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="font-heading text-lg font-bold tabular-nums">
v{s.surum}
</h3>
<span className="text-sm font-semibold text-slate-700">
{s.baslik}
</span>
<time className="text-xs text-slate-500">{s.tarih}</time>
</div>
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
{s.maddeler.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
</li>
))}
</ol>
</section>
<section aria-labelledby="geliyor-baslik" className="mt-12">
<h2 id="geliyor-baslik" className="font-heading text-xl font-bold">
Sırada ne var
</h2>
<ul className="mt-4 list-disc space-y-1.5 pl-5 text-sm leading-7 text-slate-700">
{GELIYOR.map((m) => (
<li key={m}>{m}</li>
))}
</ul>
<p className="mt-4 text-xs text-slate-500">
Sıra geri bildirimle değişir; en çok istenen öne alınır.
</p>
</section>
<p className="mt-12 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
İpucu: <Kbd>D</Kbd> açık / koyu temayı değiştirir,
<Kbd>⌘</Kbd>
<Kbd>K</Kbd> aramayı açar.
</p>
</main>
<SiteFooter />
</>
);
}

View File

@@ -166,12 +166,15 @@ function donguyuBaslat(
* yerinden başlar.
*/
export function AnimasyonluMark({
id,
className,
sadeceHover = false,
oynat = true,
varyant = "rota",
renk = "marka",
}: {
/** Kök elemanın id'si (ör. tema geçişinin merkez noktası, lib/tema-gecisi.ts). */
id?: string;
className?: string;
sadeceHover?: boolean;
/** `sadeceHover` yokken döngü açık mı. `false` iken işaret durağandır. */
@@ -284,6 +287,7 @@ export function AnimasyonluMark({
return (
<span
id={id}
ref={kokRef}
aria-hidden="true"
className={cn(

View File

@@ -199,6 +199,12 @@ export function SiteFooter() {
>
İletişim
</Link>
<Link
href="/surum-notlari"
className="hover:text-slate-700 hover:underline"
>
Sürüm notları
</Link>
</nav>
<TemaSecici className="mt-6" />
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları

View File

@@ -2,6 +2,7 @@ import { Suspense } from "react";
import Link from "next/link";
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
import { SurumRozeti } from "@/components/surum-rozeti";
import { TEMA_ODAK_ID } from "@/lib/tema-gecisi";
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
import { getRehberAramaKayitlari } from "@/lib/rehber";
@@ -23,8 +24,13 @@ export function SiteHeader() {
className="group flex min-w-0 shrink items-center font-bricolage"
>
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
kareler danışman panelindeki nabız animasyonuyla canlanır. */}
<AnimasyonluMark sadeceHover className="size-8 sm:size-11" />
kareler danışman panelindeki nabız animasyonuyla canlanır.
id: tema geçişi maskesi bu noktadan yayılır (lib/tema-gecisi.ts). */}
<AnimasyonluMark
id={TEMA_ODAK_ID}
sadeceHover
className="size-8 sm:size-11"
/>
<span className="truncate transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity">
<span className="text-lg font-light tracking-tighter sm:text-3xl">
Kolay
@@ -33,10 +39,12 @@ export function SiteHeader() {
Tercih
</span>
</span>
{/* 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. */}
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
</Link>
{/* 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. */}
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
<div
id="nav-baslik-yuvasi"
className="pointer-events-none absolute inset-y-0 right-0 left-9 flex min-w-0 items-center sm:left-12 lg:right-auto lg:w-52 xl:w-72"

View File

@@ -1,3 +1,4 @@
import Link from "next/link";
import { SURUM } from "@/lib/surum";
/**
@@ -7,20 +8,25 @@ import { SURUM } from "@/lib/surum";
* (`bg-indigo-500`, §2.2 istisnası). Sürüm rozetin içinde değil, yanında
* küçük puntoyla; dar ekranda yalnız rozet kalır. Logo fade'iyle birlikte
* söner (NavBaslik yuvası açılınca), o yüzden site-header'daki fade
* sınıflarını aynen taşır.
* sınıflarını aynen taşır. Rozet /surum-notlari'na bağlantıdır (beta
* açıklaması, sürüm günlüğü, geri bildirim adresi).
*/
export function SurumRozeti({ className = "" }: { className?: string }) {
return (
<span className={`inline-flex shrink-0 items-center gap-1.5 ${className}`}>
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white">
<Link
href="/surum-notlari"
aria-label={`Beta sürüm notları, sürüm ${SURUM}`}
className={`group/rozet inline-flex shrink-0 items-center gap-1.5 rounded-full outline-none focus-visible:ring-3 focus-visible:ring-ring/50 ${className}`}
>
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white transition-opacity duration-150 group-hover/rozet:opacity-85">
Beta
</span>
<span
aria-hidden
className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline"
aria-label={`Sürüm ${SURUM}`}
>
v{SURUM}
</span>
</span>
</Link>
);
}

View File

@@ -0,0 +1,38 @@
"use client";
import { useEffect } from "react";
import { useTheme } from "next-themes";
import { temayiGecisle } from "@/lib/tema-gecisi";
/** Tuş yazı alanında basıldıysa kısayol değildir. */
function yaziAlanindaMi(hedef: EventTarget | null) {
if (!(hedef instanceof HTMLElement)) return false;
if (hedef.isContentEditable) return true;
const etiket = hedef.tagName;
return etiket === "INPUT" || etiket === "TEXTAREA" || etiket === "SELECT";
}
/**
* Klavye kısayolu: `D` açık ↔ koyu temayı değiştirir (ipucu footer'daki
* TemaSecici'nin altında, Kbd ile). Değiştirici tuşla (⌘/Ctrl/Alt) ya da yazı
* alanındayken çalışmaz; "sistem" seçiliyken o anki çözümlenmiş temanın
* tersine, açık bir seçime geçer. Geçiş logodan yayılan maskeyle oynar.
*/
export function TemaKisayolu() {
const { resolvedTheme, setTheme } = useTheme();
useEffect(() => {
const dinle = (e: KeyboardEvent) => {
if (e.key !== "d" && e.key !== "D") return;
if (e.metaKey || e.ctrlKey || e.altKey || e.repeat) return;
if (yaziAlanindaMi(e.target)) return;
e.preventDefault();
const hedef = resolvedTheme === "dark" ? "light" : "dark";
temayiGecisle(() => setTheme(hedef));
};
window.addEventListener("keydown", dinle);
return () => window.removeEventListener("keydown", dinle);
}, [resolvedTheme, setTheme]);
return null;
}

View File

@@ -2,6 +2,7 @@
import { useEffect } from "react";
import { ThemeProvider, useTheme } from "next-themes";
import { TemaKisayolu } from "@/components/tema-kisayolu";
// Gövde zemini (bg-slate-50) — açık ve koyu temadaki karşılığı
const ZEMIN = { light: "#f8fafc", dark: "#080e19" };
@@ -25,7 +26,8 @@ function AdresCubuguRengi() {
* Tema (DESIGN.md §2.3): <html>'e `dark` sınıfı yazılır, renkler
* globals.css'teki `.dark` token eşlemesinden gelir. Varsayılan açık tema;
* kullanıcı footer'daki TemaSecici ile koyu ya da "sistem"e geçer, seçim
* tarayıcıda saklanır.
* tarayıcıda saklanır; `D` tuşu açık ↔ koyu geçişi yapar (TemaKisayolu).
* Tema değişimi logodan yayılan maskeyle oynar (lib/tema-gecisi.ts).
*/
export function TemaSaglayici({ children }: { children: React.ReactNode }) {
return (
@@ -37,6 +39,7 @@ export function TemaSaglayici({ children }: { children: React.ReactNode }) {
disableTransitionOnChange
>
<AdresCubuguRengi />
<TemaKisayolu />
{children}
</ThemeProvider>
);

View File

@@ -3,6 +3,8 @@
import { useSyncExternalStore } from "react";
import { useTheme } from "next-themes";
import { Monitor, Moon, Sun } from "lucide-react";
import { Kbd } from "@/components/ui/kbd";
import { temayiGecisle } from "@/lib/tema-gecisi";
import { cn } from "@/lib/utils";
const SECENEKLER = [
@@ -16,7 +18,8 @@ const aboneOlma = () => () => {};
/**
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
* hydration sonrası işaretlenir (yerleşim oynamaz).
* hydration sonrası işaretlenir (yerleşim oynamaz). Seçim logodan yayılan
* maskeyle uygulanır (lib/tema-gecisi.ts); altında `D` kısayolu ipucu durur.
*/
export function TemaSecici({ className }: { className?: string }) {
const { theme, setTheme } = useTheme();
@@ -27,13 +30,11 @@ export function TemaSecici({ className }: { className?: string }) {
);
return (
<div className={cn("flex flex-col items-center gap-2", className)}>
<div
role="radiogroup"
aria-label="Tema"
className={cn(
"inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1",
className,
)}
className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1"
>
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
const secili = tarayicida && theme === deger;
@@ -43,7 +44,7 @@ export function TemaSecici({ className }: { className?: string }) {
type="button"
role="radio"
aria-checked={secili}
onClick={() => setTheme(deger)}
onClick={() => temayiGecisle(() => setTheme(deger))}
className={cn(
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-semibold transition-[background-color,color,transform] duration-150 outline-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
secili
@@ -57,5 +58,10 @@ export function TemaSecici({ className }: { className?: string }) {
);
})}
</div>
<p className="flex items-center gap-1.5 text-xs text-slate-500">
<Kbd>D</Kbd>
<span>ile açık / koyu arasında geç</span>
</p>
</div>
);
}

56
src/lib/tema-gecisi.ts Normal file
View File

@@ -0,0 +1,56 @@
// Tema geçişi (DESIGN.md §2.3): açık ↔ koyu değişimi ekranı bir anda
// çevirmez; yeni tema, header'daki KolayTercih işaretinin biçiminde logodan
// başlayıp büyüyerek ekrana yayılır (View Transitions API + CSS maskesi,
// globals.css `.kt-tema-gecis`). Tarayıcı View Transitions bilmiyorsa ya da
// kullanıcı hareketi azaltmışsa tema düz değişir.
/** Header'daki işaretin kök elemanı (site-header.tsx, AnimasyonluMark). */
export const TEMA_ODAK_ID = "kt-tema-odak";
/** Geçiş sırasında <html>'de duran sınıf ve maskenin merkez değişkenleri. */
const GECIS_SINIFI = "kt-tema-gecis";
let suruyor = false;
/**
* `uygula` temayı değiştiren çağrıdır (ör. `setTheme("dark")`). Geçiş
* destekleniyorsa yeni tema işaretin bulunduğu noktadan maskeyle açılır;
* işaret ekranda değilse (krom gizli sayfalar) sol üst köşeden açılır.
*/
export function temayiGecisle(uygula: () => void) {
if (typeof document === "undefined") {
uygula();
return;
}
const azHareket = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (!document.startViewTransition || azHareket || suruyor) {
uygula();
return;
}
const odak = document.getElementById(TEMA_ODAK_ID)?.getBoundingClientRect();
const kok = document.documentElement;
const x = odak && odak.width > 0 ? odak.left + odak.width / 2 : 40;
const y = odak && odak.height > 0 ? odak.top + odak.height / 2 : 48;
// Maske merkezden ekranın en uzak köşesine kadar büyümeli.
const uzak = Math.hypot(
Math.max(x, window.innerWidth - x),
Math.max(y, window.innerHeight - y),
);
kok.style.setProperty("--kt-tema-x", `${x}px`);
kok.style.setProperty("--kt-tema-y", `${y}px`);
// İşaretin görünür karesi viewBox'ın ~%67'si; kenar bu oranla çarpılır ki
// en büyük hâlde köşe karesi bile ekranı aşsın.
kok.style.setProperty("--kt-tema-son", `${Math.ceil(uzak * 3.4)}px`);
kok.classList.add(GECIS_SINIFI);
suruyor = true;
const gecis = document.startViewTransition(uygula);
gecis.finished
.catch(() => {})
.finally(() => {
kok.classList.remove(GECIS_SINIFI);
suruyor = false;
});
}