diff --git a/GAMIFICATION.md b/GAMIFICATION.md new file mode 100644 index 0000000..d9310c6 --- /dev/null +++ b/GAMIFICATION.md @@ -0,0 +1,239 @@ +# Kolay Tercih — Gamification ve Listeye Ekleme Kurgusu + +> Durum: Ürün kararı +> Son güncelleme: 1 Ağustos 2026 + +## Ana fikir + +Kolay Tercih'te bütün deneyim kullanıcının başarı sıralaması üzerine inşa +edilir. Amaç kullanıcıya yalnızca erişebileceği programları göstermek değil; +seçtiği programların sıralamasına, ilgi alanlarına ve önceliklerine ne kadar +uyduğunu açıklayarak tercihine ışık tutmaktır. + +Gamification; puan, rozet veya yapay rekabet üzerinden değil, kullanıcının +belirsiz bir fikirden gerekçeli ve dengeli bir tercih listesine ilerlemesi +üzerinden çalışır. + +## Değişmez ürün kuralları + +1. Başarı sıralaması bütün hesaplamaların başlangıç noktasıdır. +2. Listeye yalnızca tekil bir program eklenir. Üniversitenin veya bölümün tamamı + listeye eklenmez. +3. Program satırlarında erişilebilir bir `+` düğmesi bulunur. +4. Kullanıcı sihirbazı doldurmadan ilk programı listesine ekleyemez. +5. Sihirbaz tamamlandıktan sonra `+` ile seçilen program doğrudan listeye + eklenir; uyumsuz programlar engellenmez. +6. Sistem kullanıcının yerine karar vermez. Çelişkileri görünür kılar, nedenini + açıklar ve son kararı kullanıcıya bırakır. +7. “Garanti kazanırsın” gibi kesinlik bildiren ifadeler kullanılmaz. Risk ve + uyum değerlendirmeleri geçmiş YÖK Atlas verilerine dayalı rehberliktir. + +## Kullanıcı yolculuğu + +### 1. Sıralamayı öğren + +Kullanıcı başarı sıralamasını ve puan türünü girer. Sistem buna göre program +havuzunu, hayal/dengeli/güvenli aralıklarını ve kullanılabilir sihirbaz +seçeneklerini oluşturur. + +### 2. Niyetini öğren + +Kullanıcı ilk kez bir programdaki `+` düğmesine bastığında sihirbaz seçimleri +yoksa program hemen eklenmez. Tıklanan program “bekleyen program” olarak +korunur ve kısa sihirbaz açılır. + +Sihirbaz en az şu bilgileri toplar: + +- İlgi alanları +- Tercih edilen şehirler +- Devlet/vakıf tercihi +- Kariyer ve yaşam öncelikleri + +Sihirbaz giriş mesajı: + +> Önce seni biraz tanıyalım +> +> Sadece sıralamana uyan bir liste değil, ne istediğine de uyan bir tercih +> listesi kurmak istiyoruz. Birkaç kısa seçim yap; eklediğin programların sana +> ne kadar uyduğunu birlikte değerlendirelim. + +Kısa alternatif: + +> Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim. + +Birincil eylem: + +> Tercihlerimi belirle + +İkincil eylem: + +> Vazgeç + +Sihirbaz zorunludur; “şimdilik atla ve ekle” seçeneği bulunmaz. + +### 3. İlk programı ekle + +Sihirbaz başarıyla tamamlandığında kullanıcının daha önce `+` ile seçtiği +bekleyen program otomatik olarak listeye eklenir. Kullanıcı aynı programı +yeniden bulmak veya tekrar `+` düğmesine basmak zorunda kalmaz. + +Başarı bildirimi: + +> Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik. + +### 4. Listeyi bilinçli biçimde geliştir + +Sihirbaz tamamlandıktan sonraki `+` tıklamaları programı doğrudan ekler. +Sistem ekleme öncesinde onay modalı göstermez ve uyumsuz seçimleri engellemez. +Her programın değerlendirmesi listede görünür. + +## Program değerlendirme modeli + +Her eklenen program iki ayrı eksende değerlendirilir: + +### Yerleşme riski + +Kullanıcının başarı sıralaması ile programın güncel ve geçmiş taban +sıralamaları karşılaştırılır. + +Örnek durumlar: + +- Hayal +- Dengeli +- Daha güvenli +- Veri yetersiz + +“Garanti” yerine mümkün olduğunda “daha güvenli” ifadesi tercih edilir. + +### Tercih uyumu + +Program, sihirbaz seçimleriyle karşılaştırılır. Uyum tek bir kapalı puan olarak +verilmek yerine anlaşılır gerekçelerle açıklanır. + +Örnek olumlu sinyaller: + +- İlgi alanlarınla uyumlu +- Tercih ettiğin şehirlerden birinde +- Devlet/vakıf tercihine uyuyor +- Kariyer önceliklerinle örtüşüyor + +Örnek çelişki sinyalleri: + +- Seçtiğin ilgi alanlarının dışında görünüyor +- Tercih ettiğin şehirlerin dışında +- Üniversite türü tercihinle uyuşmuyor +- Belirttiğin önceliklerle zayıf eşleşiyor + +Dil yargılayıcı olmamalıdır. “Sana uygun değil” yerine “seçimlerinle +karşılaştırdığımızda şu noktada ayrışıyor” denir. + +## Liste ekranı + +Liste yalnızca seçilen programları depolayan bir favoriler alanı değildir. +Kullanıcının tercih stratejisini görünür kılan karar merkezidir. + +Her satırda en az şu bilgiler bulunur: + +- Program ve üniversite +- Güncel taban sıralaması +- Kullanıcının sıralamasına göre risk durumu +- Tercihlerle uyumlu noktalar +- Varsa çelişki veya dikkat notu +- Listeden çıkarma eylemi + +Örnek uyumlu satır mesajı: + +> İlgi alanın ve devlet üniversitesi tercihinle uyumlu. Sıralama açısından +> dengeli aralıkta. + +Örnek çelişkili satır mesajı: + +> Bu program listende kalabilir; ancak seçtiğin ilgi alanlarının ve tercih +> ettiğin şehirlerin dışında görünüyor. + +Örnek risk mesajı: + +> Geçen yılın taban sıralaması senin sıralamandan daha yukarıda. Hayal +> tercihlerinden biri olarak değerlendirebilirsin. + +Birden fazla çelişki varsa sistem hepsini aynı anda bağırmaz. En önemli bir veya +iki gerekçe satırda gösterilir; ayrıntılar açılır alanda sunulur. + +## İlerleme hissi + +Deneyimin oyunlaştırma döngüsü şöyledir: + +1. **Keşfet:** Sıralamana göre programları incele. +2. **Seç:** `+` ile ilgini çeken programı listene al. +3. **Anla:** Sistem risk ve tercih uyumunu açıklar. +4. **Dengele:** Hayal, dengeli ve daha güvenli dağılımını geliştir. +5. **Tamamla:** Gerekçeli 24 tercihlik listeye ulaş. + +İlerleme göstergeleri: + +- `3 / 24 tercih` +- Hayal / dengeli / daha güvenli dağılımı +- Tercihlerle güçlü uyum gösteren program sayısı +- Dikkat edilmesi gereken seçim sayısı +- Listenin eksik kalan tarafına yönelik tek ve uygulanabilir öneri + +Örnek yönlendirmeler: + +> Listen dengeli tercihlerde güçleniyor. Birkaç daha güvenli seçenek eklemek +> riski dağıtabilir. + +> Seçtiklerinin çoğu aynı şehirde. Şehir konusunda esneksen alternatiflerini +> genişletebilirsin. + +> 24 tercihe ulaştın. Şimdi dağılımı ve dikkat notlarını birlikte gözden geçir. + +## `+` düğmesinin durumları + +- **Varsayılan:** “Listeme ekle” +- **Sihirbaz gerekli:** Tıklama sihirbaz ön bilgilendirmesini açar +- **Ekleniyor:** Düğme geçici olarak devre dışı kalır +- **Eklendi:** `+` yerine onay işareti gösterilir, erişilebilir etiketi + “Listende” olur +- **Liste dolu:** Düğme devre dışıdır ve “Listen 24 tercihe ulaştı” açıklaması + sunulur +- **Hata:** Seçim kaybolmaz; kullanıcıya yeniden deneme eylemi verilir + +İkon düğmesinin dokunma alanı en az 44×44 piksel olmalı ve yalnızca ikona bağlı +anlam bırakılmamalıdır. Görsel etiket veya erişilebilir ad bulunmalıdır. + +## Sihirbaz seçimlerini değiştirme + +Kullanıcı sihirbaz yanıtlarını daha sonra değiştirebilir. Değişiklik mevcut +programları listeden otomatik olarak çıkarmaz. Bütün satırların uyum +değerlendirmesi yeni yanıtlara göre yeniden hesaplanır ve değişen yorumlar +görünür biçimde güncellenir. + +Örnek bildirim: + +> Tercihlerin güncellendi. Listendeki programları yeni seçimlerine göre yeniden +> değerlendirdik; hiçbir programı silmedik. + +## Kaçınılacak kalıplar + +- Kullanıcıyı sihirbazı doldurmadığı için suçlayan dil +- Uyumsuz bir programın eklenmesini engellemek +- Sadece renk ile uyum veya risk anlatmak +- Kesin yerleşme vaadi vermek +- Her `+` tıklamasında modal veya onay adımı göstermek +- Rozet, puan ve seri gibi kararın ciddiyetini oyunlaştıran ödüller +- Kullanıcının sihirbazdan önce tıkladığı programı unutmak +- İlgi alanı uyumunu yerleşme ihtimaliyle aynı şeymiş gibi sunmak + +## Başarı ölçütleri + +- Sıralama sonucundan ilk `+` tıklamasına geçiş +- İlk `+` sonrasında sihirbaz tamamlama oranı +- Sihirbazdan sonra bekleyen programın başarıyla eklenme oranı +- Kullanıcı başına eklenen program sayısı +- 24 tercihe ulaşan kullanıcı oranı +- Uyarı görüldükten sonra programı koruma/çıkarma davranışı +- Sihirbaz tercihlerini güncelleme oranı +- Liste dağılımının zaman içinde daha dengeli hâle gelmesi + +Bu metrikler kullanıcıyı daha çok tıklamaya zorlamak için değil, daha bilinçli +bir tercih listesi kurup kuramadığını anlamak için kullanılmalıdır. diff --git a/src/app/bolumler/page.tsx b/src/app/bolumler/page.tsx index 813dba7..715f8e1 100644 --- a/src/app/bolumler/page.tsx +++ b/src/app/bolumler/page.tsx @@ -78,46 +78,46 @@ export default function BolumlerPage() { return ( <> -
- +
+ -

- Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026 -

-

- {sayi(hepsi.length)} bölümün 2025 YKS yerleştirme verileri: taban - puanları, başarı sıralamaları, kontenjanlar ve devlet/vakıf dağılımı. - Bölüme tıklayınca o bölümü sunan tüm üniversitelerin tablosunu - görürsün. Veriler YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 - tercihleri için rehber niteliğindedir. -

+

+ Tüm Bölümlerin Taban Puanları ve Sıralamaları 2026 +

+

+ {sayi(hepsi.length)} bölümün 2025 YKS yerleştirme verileri: taban + puanları, başarı sıralamaları, kontenjanlar ve devlet/vakıf dağılımı. + Bölüme tıklayınca o bölümü sunan tüm üniversitelerin tablosunu + görürsün. Veriler YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 + tercihleri için rehber niteliğindedir. +

- + -
-

- Lisans Bölümleri (4 Yıllık) -

- -
+
+

+ Lisans Bölümleri (4 Yıllık) +

+ +
-
- -
+
+ +
-
-

- Önlisans Bölümleri (2 Yıllık — TYT) -

- -
-
- +
+

+ Önlisans Bölümleri (2 Yıllık — TYT) +

+ +
+
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8190db3..69a9ec4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/sonner"; import { SiteHeader } from "@/components/site-header"; import { SiteTopBanner } from "@/components/site-top-banner"; import { ListeCekmecesiLazy } from "@/components/manuel-liste/liste-cekmecesi-lazy"; +import { TercihProfiliKapisiLazy } from "@/components/manuel-liste/tercih-profili-kapisi-lazy"; import { HeaderGate } from "@/components/header-gate"; import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41"; import { DevPanel } from "@/components/dev/dev-panel"; @@ -109,6 +110,8 @@ export default function RootLayout({ {/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */} + {/* İlk "+" için sıra + sihirbaz kapısı */} + {/* Dev süperadmin paneli — prod build'de hiç render edilmez */} {process.env.NODE_ENV !== "production" ? : null} diff --git a/src/app/listem/listem-sekmeleri.tsx b/src/app/listem/listem-sekmeleri.tsx new file mode 100644 index 0000000..f05ef03 --- /dev/null +++ b/src/app/listem/listem-sekmeleri.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { ListChecks, Sparkles } from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { SecimlerimPaneli } from "@/components/manuel-liste/secimlerim-paneli"; + +export type ListemSekme = "secimlerim" | "ai"; + +export function ListemSekmeleri({ + varsayilan, + aiIcerik, +}: { + varsayilan: ListemSekme; + aiIcerik: React.ReactNode; +}) { + return ( + + + + + Seçtiklerim + + + + AI Listem + + + + + + + {aiIcerik} + + + ); +} diff --git a/src/app/listem/page.tsx b/src/app/listem/page.tsx index b201491..14da066 100644 --- a/src/app/listem/page.tsx +++ b/src/app/listem/page.tsx @@ -29,6 +29,7 @@ import { PagePixelDivider } from "@/components/pixel-decor"; import { raporMaskele, ACIK_SATIR } from "@/lib/rapor-maske"; import { SohbetClient, type Mesaj } from "./sohbet-client"; import { RevizyonKutusu } from "./revizyon-kutusu"; +import { ListemSekmeleri, type ListemSekme } from "./listem-sekmeleri"; export const metadata: Metadata = { title: "Listem", @@ -42,18 +43,15 @@ const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", { export default async function ListemPage({ searchParams, }: { - searchParams: Promise<{ acildi?: string }>; + searchParams: Promise<{ acildi?: string; sekme?: string }>; }) { const session = await verifySession("/listem"); - // Kullanıcı satırı, ?acildi parametresi ve rapor birbirinden bağımsız — - // seri await zinciri yerine tek turda çöz. - const [user, { acildi }, satir, sohbetGecmisi] = await Promise.all([ + const [user, params, satir, sohbetGecmisi] = await Promise.all([ getCurrentUser(), searchParams, appDb.query.reports.findFirst({ where: eq(schema.reports.userId, session.user.id), }), - // Sohbet geçmişi burada gelirse client mount sonrası /api/soru turu atmaz appDb .select({ id: schema.chatMessages.id, @@ -67,41 +65,64 @@ export default async function ListemPage({ ]); if (!user) redirect("/giris?callback=/listem"); - // ?acildi=1: ödeme dönüşü — kilidin kalktığı tek seferlik açılış deneyimi + const { acildi, sekme } = params; const kilitAcilisi = Boolean(user.hasPaket && acildi === "1"); + const raporVar = Boolean(satir?.result); - // Liste yoksa önce sihirbazdan oluşturulmalı - if (!satir?.result) { + const varsayilanSekme: ListemSekme = + sekme === "secimlerim" + ? "secimlerim" + : sekme === "ai" || acildi === "1" || raporVar + ? "ai" + : "secimlerim"; + + const aiBos = ( +
+ +

+ Henüz AI listen yok +

+

+ Sıralamanı girip birkaç adımda 24 tercihlik risk analizli listeni kur; + sonra burada görüntüleyip AI danışmanla üzerinde konuşabilirsin. + Seçtiklerim sekmesinde ise tablolardan eklediğin programlar durur. +

+ + +
+ ); + + if (!raporVar) { return ( -
- -

- Henüz bir listen yok +
+

+ Tercih karar merkezin

-

- Sıralamanı girip birkaç adımda 24 tercihlik risk analizli listeni kur; - sonra burada görüntüleyip AI danışmanla üzerinde konuşabilirsin. +

+ Seçtiklerin ve AI listen burada yan yana — ikisi birbirinden bağımsız.

- - + +
); } - // Paketsize giden rapor burada da maskelenir (bkz. rapor-maske) const rapor = user.hasPaket - ? (satir.result as RaporSonuc) - : raporMaskele(satir.result as RaporSonuc); - const params = satir.params as RaporParams; + ? (satir!.result as RaporSonuc) + : raporMaskele(satir!.result as RaporSonuc); + const raporParams = satir!.params as RaporParams; - return ( -
+ const aiIcerik = ( + <> {kilitAcilisi ? (
Paketin aktif — 24 tercihinin tamamı, gerekçeler, riskler ve PDF - artık açık. 🎉 + artık açık.
) : null}
@@ -125,14 +146,16 @@ export default async function ListemPage({ YÖK Atlas verileri ücretsiz )} -

+

{user.hasPaket - ? "Tercih karar merkezin" + ? "AI tercih listen" : "Veriyi ücretsiz incele. Kararı AI ile netleştir."} -

+

- {params.sira.toLocaleString("tr-TR")}. sıra ·{" "} - {PUAN_TURLERI[params.tur]} ·{" "} + {raporParams.sira.toLocaleString("tr-TR")}. sıra ·{" "} + {PUAN_TURLERI[raporParams.tur as keyof typeof PUAN_TURLERI] ?? + raporParams.tur}{" "} + ·{" "} {user.hasPaket ? "24 tercihinin tamamını gerekçe, risk analizi ve trendleriyle aşağıda inceleyebilirsin." : "24 programın 2024–2025 taban sıralamalarını ve ham trendini aşağıda ücretsiz inceleyebilirsin."} @@ -165,16 +188,13 @@ export default async function ListemPage({ ) : null} - - {/* AI'ın genel değerlendirmesi ve uyarıları: paketliye tam, - paketsize sunucuda kırpılmış önizleme + kilit notu */} {rapor.genelDegerlendirme ? (

-

+

Danışman özeti -

+

{rapor.genelDegerlendirme}

@@ -205,51 +225,51 @@ export default async function ListemPage({
-
- -
- - - -
-

- İlk {ACIK_SATIR} tercihte AI analizini gördün -

-

- Kalan {24 - ACIK_SATIR} tercihin kişisel risk, gerekçe ve - AI yorumu paketle açılır. Program adları ve taban verileri - her zaman ücretsiz. -

+
+ +
+ + + +
+

+ İlk {ACIK_SATIR} tercihte AI analizini gördün +

+

+ Kalan {24 - ACIK_SATIR} tercihin kişisel risk, gerekçe ve + AI yorumu paketle açılır. Program adları ve taban + verileri her zaman ücretsiz. +

+
+
- -
- } - /> -
+ } + /> +
- {user.hasPaket ? ( - - ) : null} + {user.hasPaket ? ( + + ) : null}
@@ -260,91 +280,87 @@ export default async function ListemPage({ /> ) : (
-
-
-
- - - Tercih Dönemi Paketi - -
-

- Tablo ücretsiz. -
- Karar desteği pakette. -

-

- Sadece veriyi göstermez; hangi tercihin neden mantıklı - olduğunu sıralamana göre açıklar. -

-
- -
-
    - {[ - "24 tercihin tamamında kişisel risk analizi", - "Her program için kısa, gerekçeli AI yorumu", - "2 liste revizyonu ve paylaşılabilir PDF", - `${URUNLER.paket.credits} AI danışman sorusu`, - ].map((ozellik) => ( -
  • - - {ozellik} -
  • - ))} -
- -
-
-

Tek seferlik

-

- {PAKET_FIYATI} TL -

+
+
+
+ + + Tercih Dönemi Paketi +
- Abonelik yok +

+ Tablo ücretsiz. +
+ Karar desteği pakette. +

+

+ Sadece veriyi göstermez; hangi tercihin neden mantıklı + olduğunu sıralamana göre açıklar. +

- +
+
    + {[ + "24 tercihin tamamında kişisel risk analizi", + "Her program için kısa, gerekçeli AI yorumu", + "2 liste revizyonu ve paylaşılabilir PDF", + `${URUNLER.paket.credits} AI danışman sorusu`, + ].map((ozellik) => ( +
  • + + {ozellik} +
  • + ))} +
-
- - - iyzico güvenli ödeme - - - - Gerçek YÖK verisi - +
+
+

Tek seferlik

+

+ {PAKET_FIYATI} TL +

+
+ Abonelik yok +
+ + + +
+ + + iyzico güvenli ödeme + + + + Gerçek YÖK verisi + +
-
-
+
- {/* Deneme kredileri sohbette geçerli: AI'ı satın almadan önce - tattırır (kalan kredi biterse composer paket CTA'sına döner). */} - +
)}
- {/* Mobilde sohbet sağ rayla birlikte en alta düşer; yüzen kısayol - kullanıcıyı 24 satırı kaydırmadan danışmana ulaştırır. */} + + ); + + return ( +
+

+ Tercih karar merkezin +

+

+ Seçtiklerin ve AI listen burada yan yana — ikisi birbirinden bağımsız. +

+ +
); } diff --git a/src/app/universite/[slug]/universite-icerik.tsx b/src/app/universite/[slug]/universite-icerik.tsx index 6172ba6..ea02eed 100644 --- a/src/app/universite/[slug]/universite-icerik.tsx +++ b/src/app/universite/[slug]/universite-icerik.tsx @@ -10,26 +10,11 @@ import { SiteFooter } from "@/components/site-footer"; import { Sayfalama, SAYFA_BOYU } from "@/components/sayfalama"; import { PagePixelDivider } from "@/components/pixel-decor"; import { UniversiteKonumHaritasi } from "@/components/universite-konum-haritasi"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; +import { UniversiteProgramTablosu } from "@/components/universite-program-tablosu"; import type { Program } from "@/lib/db"; const sayi = (n: number) => n.toLocaleString("tr-TR"); -const TUR_ETIKET: Record = { - SAYISAL: "SAY", - "EŞİT AĞIRLIK": "EA", - SÖZEL: "SÖZ", - DİL: "DİL", - TYT: "TYT", -}; - export function uniToplamSayfa(programSayisi: number): number { return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU)); } @@ -69,12 +54,16 @@ export function UniversiteIcerik({ (sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU, ); - const gruplar: { fakulte: string; programlar: Program[] }[] = []; + const gruplar: { + fakulte: string; + programlar: (Program & { bolumSlug: string | null })[]; + }[] = []; for (const p of dilim) { const anahtar = p.fakulte ?? "Diğer"; + const satir = { ...p, bolumSlug: bolumSlugFromIsim(p.isim) }; const son = gruplar[gruplar.length - 1]; - if (son && son.fakulte === anahtar) son.programlar.push(p); - else gruplar.push({ fakulte: anahtar, programlar: [p] }); + if (son && son.fakulte === anahtar) son.programlar.push(satir); + else gruplar.push({ fakulte: anahtar, programlar: [satir] }); } const ilkSayfa = sayfa === 1; @@ -178,6 +167,7 @@ export function UniversiteIcerik({

+
@@ -216,7 +206,6 @@ export function UniversiteIcerik({
-
) : ( @@ -238,10 +227,7 @@ export function UniversiteIcerik({ -
+

Üniversite programları

@@ -250,67 +236,7 @@ export function UniversiteIcerik({

- {gruplar.map(({ fakulte, programlar }, i) => ( -
-

- {fakulte} -

-
- - - - Program - Puan Türü - 2025 Puan - 2025 Sıralama - 2024 Sıralama - Kontenjan - - - - {programlar.map((p) => { - const bolumSlug = bolumSlugFromIsim(p.isim); - return ( - - - {bolumSlug ? ( - - {p.isim.trim()} - - ) : ( - - {p.isim.trim()} - - )} - - {TUR_ETIKET[p.tur] ?? p.tur} - - {p.puan2025 != null - ? p.puan2025.toFixed(2).replace(".", ",") - : "—"} - - - {p.sira2025 != null ? sayi(p.sira2025) : "—"} - - - {p.sira2024 != null ? sayi(p.sira2024) : "—"} - - - {p.kontenjan2025 != null - ? sayi(p.kontenjan2025) - : "—"} - - - ); - })} - -
-
-
- ))} +

"—": o yıl için yerleştirme verisi bulunmuyor (yeni açılan diff --git a/src/app/universiteler/page.tsx b/src/app/universiteler/page.tsx index 3e65573..d9bab3d 100644 --- a/src/app/universiteler/page.tsx +++ b/src/app/universiteler/page.tsx @@ -49,64 +49,64 @@ export default function UniversitelerPage() { return ( <> -

- +
+ -

- Üniversitelerin Taban Puanları ve Sıralamaları 2026 -

-

- {sayi(hepsi.length)} üniversitenin 2025 YKS yerleştirme verileri. - Üniversiteye tıklayınca tüm programlarının taban puanlarını, başarı - sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler - YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri için - rehber niteliğindedir. -

+

+ Üniversitelerin Taban Puanları ve Sıralamaları 2026 +

+

+ {sayi(hepsi.length)} üniversitenin 2025 YKS yerleştirme verileri. + Üniversiteye tıklayınca tüm programlarının taban puanlarını, başarı + sıralamalarını ve kontenjanlarını fakülte fakülte görürsün. Veriler + YÖK Atlas 2025 yerleştirme sonuçlarına dayanır; 2026 tercihleri için + rehber niteliğindedir. +

- + -
-

- Devlet Üniversiteleri - - {sayi(devlet.length)} - -

- -
- -
-

- Vakıf Üniversiteleri - - {sayi(vakif.length)} - -

- -
- - {digerUniler.length > 0 ? ( -
+

- KKTC ve Yurt Dışı + Devlet Üniversiteleri - {sayi(digerUniler.length)} + {sayi(devlet.length)}

- +
- ) : null} -
- -
-
- +
+

+ Vakıf Üniversiteleri + + {sayi(vakif.length)} + +

+ +
+ + {digerUniler.length > 0 ? ( +
+

+ KKTC ve Yurt Dışı + + {sayi(digerUniler.length)} + +

+ +
+ ) : null} + +
+ +
+
+ ); } diff --git a/src/components/bolum-program-listesi.tsx b/src/components/bolum-program-listesi.tsx index 444c315..1cd304f 100644 --- a/src/components/bolum-program-listesi.tsx +++ b/src/components/bolum-program-listesi.tsx @@ -3,9 +3,21 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { ChevronDown, ChevronUp } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; import type { Program } from "@/lib/db"; import { trBaslikDuzeni } from "@/lib/slug"; +import { + ProgramSiraGecmisi, + ProgramTabanTrendi, +} from "@/components/program-liste-verileri"; +import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; const sayi = (n: number) => n.toLocaleString("tr-TR"); @@ -47,13 +59,17 @@ function SiralamaDugmesi({ @@ -85,11 +101,7 @@ export function BolumProgramListesi({ if (bDeger == null) return -1; const fark = aDeger - bDeger; - return fark === 0 - ? a.index - b.index - : yon === "artan" - ? fark - : -fark; + return fark === 0 ? a.index - b.index : yon === "artan" ? fark : -fark; }) .map(({ program }) => program); }, [aktifAlan, programlar, yon]); @@ -104,88 +116,116 @@ export function BolumProgramListesi({ } return ( -
    - {siraliProgramlar.map((p) => { - const uniAd = trBaslikDuzeni(p.universite); - const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null; - return ( -
  • -
    - {p.universiteSlug ? ( - - {uniAd} - - ) : ( -

    {uniAd}

    - )} -
    - {p.il ? {trBaslikDuzeni(p.il)} : null} - {p.il && p.unitur ? · : null} - {p.unitur ? ( - - {p.unitur === "DEVLET" - ? "Devlet" - : trBaslikDuzeni(p.unitur)} - - ) : null} -
    - {varyant ? ( -

    {varyant}

    - ) : null} -
    +
    + + + + Üniversite + + Son 5 yıl + + + + + + + + + + + + Listeye ekle + + + + + {siraliProgramlar.map((p) => { + const uniAd = trBaslikDuzeni(p.universite); + const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null; + const devlet = p.unitur === "DEVLET"; -
    -
    -
    - -
    -
    - {p.sira2025 != null ? sayi(p.sira2025) : "—"} -
    -
    -
    -
    - -
    -
    + return ( + + + {p.universiteSlug ? ( + + {uniAd} + + ) : ( + + {uniAd} + + )} + + {p.unitur ? ( + + {devlet ? "Devlet" : trBaslikDuzeni(p.unitur)} + + ) : null} + + {p.il ? trBaslikDuzeni(p.il) : null} + {varyant ? `${p.il ? " · " : ""}${varyant}` : ""} + + {p.sure ? ( + + · {p.sure} yıl + + ) : null} + + + + + + {p.puan2025 != null ? p.puan2025.toFixed(2).replace(".", ",") : "—"} -
    -
    -
    -
    - + + {p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"} + + + + + {p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"} + + + + -
    -
    - {p.kontenjan2025 != null ? sayi(p.kontenjan2025) : "—"} -
    -
    -
    - - ); - })} - + + + ); + })} +
    +
    +
    ); } diff --git a/src/components/manuel-liste/liste-cekmecesi.tsx b/src/components/manuel-liste/liste-cekmecesi.tsx index 10415f1..afc3106 100644 --- a/src/components/manuel-liste/liste-cekmecesi.tsx +++ b/src/components/manuel-liste/liste-cekmecesi.tsx @@ -178,8 +178,8 @@ function CekmeceSheet() { />

- Sıralamanla ulaşabildiğin programlardan kendi listen — tutamaktan - sürükleyip sırala, sağa kaydırıp sil. + Bölüm, üniversite veya sonuç tablolarından eklediğin programlar — + tutamaktan sürükleyip sırala, sağa kaydırıp sil.

@@ -194,8 +194,9 @@ function CekmeceSheet() { Henüz program seçmedin

- Sonuç sayfasındaki tablodan ile - program ekle; beğendiklerin burada birikir. + Bölüm, üniversite veya sonuç sayfalarındaki tablolardan{" "} + ile program ekle; beğendiklerin burada + birikir.

) : ( @@ -222,15 +223,23 @@ function CekmeceSheet() { )} - {/* Alt bant: AI danışman köprüsü */} + {/* Alt bant: karar merkezi köprüsü */} + + Seçtiklerimi ve uyum notlarını aç + + AI listeni ve danışmanı aç diff --git a/src/components/manuel-liste/profil-kapisi-store.ts b/src/components/manuel-liste/profil-kapisi-store.ts new file mode 100644 index 0000000..6eaa8f4 --- /dev/null +++ b/src/components/manuel-liste/profil-kapisi-store.ts @@ -0,0 +1,190 @@ +"use client"; + +// Global "+" kapısı: profil yokken bekleyen programı tutar, modalı açar. +// Layout'ta bir kez monte edilen TercihProfiliKapisi bu store'a abone olur. + +import { useSyncExternalStore } from "react"; +import type { Program } from "@/lib/db"; +import { + tercihProfiliOku, + tercihProfiliTamamlandiMi, + type TercihProfili, +} from "@/lib/sihirbaz"; +import { + ekle, + listedeMi, + MANUEL_LISTE_MAX, + programdanManuelTercih, + riskleriYenile, + useManuelListe, + type ManuelKaynak, +} from "./store"; + +export type BekleyenProgram = { + program: Pick< + Program, + | "id" + | "isim" + | "universite" + | "il" + | "unitur" + | "tur" + | "sira2025" + | "sira2024" + >; + kaynak: ManuelKaynak; + adaySira?: number; + adayTur?: string; +}; + +type KapıDurumu = { + acik: boolean; + duzenle: boolean; + bekleyen: BekleyenProgram | null; + profil: TercihProfili | null; + profilYuklendi: boolean; +}; + +let durum: KapıDurumu = { + acik: false, + duzenle: false, + bekleyen: null, + profil: null, + profilYuklendi: false, +}; + +const dinleyiciler = new Set<() => void>(); + +function yayinla() { + for (const cb of dinleyiciler) cb(); +} + +function profiliTazele() { + durum = { + ...durum, + profil: tercihProfiliOku(), + profilYuklendi: true, + }; +} + +function yukle() { + if (durum.profilYuklendi || typeof window === "undefined") return; + profiliTazele(); + window.addEventListener("storage", (e) => { + if (e.key !== "kolaytercih.sihirbaz") return; + profiliTazele(); + yayinla(); + }); +} + +function abone(cb: () => void) { + dinleyiciler.add(cb); + if (!durum.profilYuklendi && typeof window !== "undefined") { + yukle(); + queueMicrotask(yayinla); + } + return () => { + dinleyiciler.delete(cb); + }; +} + +const SSR_DURUM: KapıDurumu = { + acik: false, + duzenle: false, + bekleyen: null, + profil: null, + profilYuklendi: false, +}; + +export function useProfilKapisi(): KapıDurumu { + return useSyncExternalStore(abone, () => durum, () => SSR_DURUM); +} + +export function useTercihProfili(): TercihProfili | null { + return useSyncExternalStore( + abone, + () => { + if (!durum.profilYuklendi) yukle(); + return durum.profil; + }, + () => null, + ); +} + +export function profilKapisiniKapat() { + durum = { ...durum, acik: false, duzenle: false, bekleyen: null }; + yayinla(); +} + +export function profilDuzenlemeyiAc() { + if (!durum.profilYuklendi) yukle(); + durum = { ...durum, acik: true, duzenle: true, bekleyen: null }; + yayinla(); +} + +export function programEkleIste(opts: { + program: BekleyenProgram["program"]; + kaynak?: ManuelKaynak; + adaySira?: number; + adayTur?: string; +}): { + eklendi: boolean; + zatenVar: boolean; + doldu: boolean; + kapiAcildi: boolean; +} { + const { program, kaynak = "sonuc", adaySira, adayTur } = opts; + + if (listedeMi(program.id)) { + return { eklendi: false, zatenVar: true, doldu: false, kapiAcildi: false }; + } + + if (!tercihProfiliTamamlandiMi()) { + durum = { + ...durum, + acik: true, + duzenle: false, + bekleyen: { program, kaynak, adaySira, adayTur }, + }; + yayinla(); + return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true }; + } + + if (!durum.profilYuklendi) { + profiliTazele(); + } + + const profil = durum.profil ?? tercihProfiliOku(); + const tercih = programdanManuelTercih( + program, + adaySira ?? profil?.sira ?? null, + kaynak, + ); + const ok = ekle(tercih); + if (!ok) { + return { eklendi: false, zatenVar: false, doldu: true, kapiAcildi: false }; + } + return { eklendi: true, zatenVar: false, doldu: false, kapiAcildi: false }; +} + +export function profilTamamlandi(profil: TercihProfili) { + riskleriYenile(profil); + const bekleyen = durum.bekleyen; + if (bekleyen) { + ekle(programdanManuelTercih(bekleyen.program, profil.sira, bekleyen.kaynak)); + } + durum = { + ...durum, + acik: false, + duzenle: false, + bekleyen: null, + profil, + profilYuklendi: true, + }; + yayinla(); +} + +export function useListeDolu(): boolean { + const liste = useManuelListe(); + return liste.length >= MANUEL_LISTE_MAX; +} diff --git a/src/components/manuel-liste/program-ekle-butonu.tsx b/src/components/manuel-liste/program-ekle-butonu.tsx new file mode 100644 index 0000000..87f9eb1 --- /dev/null +++ b/src/components/manuel-liste/program-ekle-butonu.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { Check, Plus } from "lucide-react"; +import { toast } from "sonner"; +import type { Program } from "@/lib/db"; +import { + listedeMi, + MANUEL_LISTE_MAX, + useManuelListe, + type ManuelKaynak, +} from "./store"; +import { programEkleIste } from "./profil-kapisi-store"; + +type ProgramOzet = Pick< + Program, + | "id" + | "isim" + | "universite" + | "il" + | "unitur" + | "tur" + | "sira2025" + | "sira2024" +>; + +export function ProgramEkleButonu({ + program, + kaynak = "sonuc", + adaySira, + adayTur, + className = "", +}: { + program: ProgramOzet; + kaynak?: ManuelKaynak; + adaySira?: number; + adayTur?: string; + className?: string; +}) { + const liste = useManuelListe(); + const listede = liste.some((t) => t.id === program.id) || listedeMi(program.id); + const doldu = liste.length >= MANUEL_LISTE_MAX; + + function handleClick(e: React.MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + const sonuc = programEkleIste({ program, kaynak, adaySira, adayTur }); + if (sonuc.eklendi) { + toast.success("Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik."); + } else if (sonuc.doldu) { + toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`); + } else if (sonuc.zatenVar) { + toast.info("Bu program zaten seçtiklerinde."); + } + // kapiAcildi: modal açılır, toast yok + } + + return ( + + ); +} diff --git a/src/components/manuel-liste/secimlerim-paneli.tsx b/src/components/manuel-liste/secimlerim-paneli.tsx new file mode 100644 index 0000000..908cf52 --- /dev/null +++ b/src/components/manuel-liste/secimlerim-paneli.tsx @@ -0,0 +1,270 @@ +"use client"; + +import Link from "next/link"; +import { + AlertTriangle, + CheckCircle2, + ListChecks, + PencilLine, + Sparkles, + X, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk"; +import { + tercihDegerlendir, + tercihListesiOzeti, +} from "@/lib/tercih-degerlendirme"; +import { + cikar, + MANUEL_LISTE_MAX, + useManuelListe, + type ManuelTercih, +} from "./store"; +import { + profilDuzenlemeyiAc, + useTercihProfili, +} from "./profil-kapisi-store"; + +const RISK_STIL: Record = { + guvenli: { nokta: "bg-emerald-500", kenar: "border-l-emerald-500" }, + "az-riskli": { nokta: "bg-amber-500", kenar: "border-l-amber-500" }, + riskli: { nokta: "bg-red-500", kenar: "border-l-red-500" }, +}; + +export function SecimlerimPaneli() { + const liste = useManuelListe(); + const profil = useTercihProfili(); + + const ozet = profil + ? tercihListesiOzeti( + liste.map((t) => ({ + isim: t.isim, + il: t.il, + unitur: t.unitur, + efektifSira: t.efektifSira, + })), + profil, + ) + : null; + + if (liste.length === 0) { + return ( +
+ +

+ Henüz program seçmedin +

+

+ Bölüm, üniversite veya sonuç sayfalarındaki tablolardan{" "} + ile program ekle. Tercihlerine göre + risk ve uyum notlarını burada göreceksin. +

+
+ + +
+
+ ); + } + + return ( +
+
+
+
+

+ + Seçtiklerim + + {liste.length}/{MANUEL_LISTE_MAX} + +

+

+ {profil + ? `${profil.sira.toLocaleString("tr-TR")}. sıra · tercihlerine göre değerlendirme` + : "Tercih profilin oluşunca uyum notları görünür."} +

+
+ {profil ? ( + + ) : null} +
+ +
+
+
+ + {ozet ? ( +
+ + + + +
+ ) : null} + + {ozet ? ( +

+ + {ozet.oneri} + {ozet.gucluUyum > 0 ? ( + + {ozet.gucluUyum} program güçlü uyum gösteriyor. + + ) : null} +

+ ) : null} +
+ +
    + {liste.map((t, i) => ( + + ))} +
+
+ ); +} + +function Istatistik({ etiket, deger }: { etiket: string; deger: number }) { + return ( +
+
+ {etiket} +
+
+ {deger} +
+
+ ); +} + +function SecimSatiri({ + tercih, + sira, +}: { + tercih: ManuelTercih; + sira: number; +}) { + const profil = useTercihProfili(); + const degerlendirme = profil + ? tercihDegerlendir( + { + isim: tercih.isim, + il: tercih.il, + unitur: tercih.unitur, + efektifSira: tercih.efektifSira, + }, + profil, + ) + : null; + const risk = degerlendirme?.risk ?? tercih.risk; + const stil = risk ? RISK_STIL[risk] : null; + + return ( +
  • +
    + + {sira} + +
    +
    +
    +

    + {tercih.isim} +

    +

    + {tercih.universite} + {tercih.il + ? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}` + : ""} + {tercih.efektifSira != null + ? ` · ~${tercih.efektifSira.toLocaleString("tr-TR")}.` + : ""} +

    +
    + +
    + + {degerlendirme ? ( +
    +

    + {risk ? ( + + ) : null} + {degerlendirme.riskEtiketi} + {risk ? ( + + ({RISK_ETIKET[risk]}) + + ) : null} +

    +

    {degerlendirme.riskMetni}

    + {degerlendirme.olumlu.slice(0, 2).map((m) => ( +

    + + {m} +

    + ))} + {degerlendirme.celiskiler.slice(0, 2).map((m) => ( +

    + + {m} +

    + ))} +
    + ) : ( +

    + Tercih profilin tamamlanınca bu programı değerlendireceğiz. +

    + )} +
    +
    +
  • + ); +} diff --git a/src/components/manuel-liste/store.ts b/src/components/manuel-liste/store.ts index 12b75f6..eec6c9e 100644 --- a/src/components/manuel-liste/store.ts +++ b/src/components/manuel-liste/store.ts @@ -6,20 +6,30 @@ // (girişsiz kullanıcı da liste kurabilsin diye sunucuya yazmıyoruz). import { useSyncExternalStore } from "react"; -import type { RiskSeviyesi } from "@/lib/risk"; +import type { Program } from "@/lib/db"; +import { riskHesapla, type RiskSeviyesi } from "@/lib/risk"; +import { + tercihProfiliOku, + type TercihProfili, +} from "@/lib/sihirbaz"; export const MANUEL_LISTE_MAX = 24; const STORAGE_KEY = "kolaytercih.manuel-liste.v1"; +export type ManuelKaynak = "sonuc" | "bolum" | "universite"; + export type ManuelTercih = { /** yokatlas program id'si */ id: string; isim: string; universite: string; il: string | null; + unitur: string | null; + tur: string | null; efektifSira: number | null; - /** Ekleme anındaki aday sıralamasına göre risk (renklendirme için) */ + /** Ekleme / profil anındaki aday sıralamasına göre risk */ risk: RiskSeviyesi | null; + kaynak?: ManuelKaynak; }; // SSR snapshot'ı: her çağrıda aynı referans dönmeli, yoksa React döngüye girer @@ -43,15 +53,41 @@ function kaydet() { function dogrula(ham: unknown): ManuelTercih[] { if (!Array.isArray(ham)) return []; - return ham - .filter( - (t): t is ManuelTercih => - typeof t === "object" && - t !== null && - typeof (t as ManuelTercih).id === "string" && - typeof (t as ManuelTercih).isim === "string", - ) - .slice(0, MANUEL_LISTE_MAX); + const sonuc: ManuelTercih[] = []; + for (const t of ham) { + if (typeof t !== "object" || t === null) continue; + const kayit = t as Record; + if (typeof kayit.id !== "string" || typeof kayit.isim !== "string") continue; + const risk: RiskSeviyesi | null = + kayit.risk === "guvenli" || + kayit.risk === "az-riskli" || + kayit.risk === "riskli" + ? kayit.risk + : null; + const kaynak: ManuelKaynak | undefined = + kayit.kaynak === "sonuc" || + kayit.kaynak === "bolum" || + kayit.kaynak === "universite" + ? kayit.kaynak + : undefined; + sonuc.push({ + id: kayit.id, + isim: kayit.isim, + universite: typeof kayit.universite === "string" ? kayit.universite : "", + il: typeof kayit.il === "string" ? kayit.il : null, + unitur: typeof kayit.unitur === "string" ? kayit.unitur : null, + tur: typeof kayit.tur === "string" ? kayit.tur : null, + efektifSira: + typeof kayit.efektifSira === "number" && + Number.isFinite(kayit.efektifSira) + ? kayit.efektifSira + : null, + risk, + kaynak, + }); + if (sonuc.length >= MANUEL_LISTE_MAX) break; + } + return sonuc; } function yukle() { @@ -103,11 +139,71 @@ export function listedeMi(id: string): boolean { return liste.some((t) => t.id === id); } +export function efektifSiraAl( + p: Pick, +): number | null { + return p.sira2025 ?? p.sira2024 ?? null; +} + +/** Program satırını manuel liste kaydına çevirir. */ +export function programdanManuelTercih( + p: Pick< + Program, + | "id" + | "isim" + | "universite" + | "il" + | "unitur" + | "tur" + | "sira2025" + | "sira2024" + >, + adaySira?: number | null, + kaynak: ManuelKaynak = "sonuc", +): ManuelTercih { + const taban = efektifSiraAl(p); + return { + id: p.id, + isim: p.isim, + universite: p.universite, + il: p.il, + unitur: p.unitur ?? null, + tur: p.tur, + efektifSira: taban, + risk: + adaySira != null && Number.isFinite(adaySira) + ? riskHesapla(taban, adaySira) + : null, + kaynak, + }; +} + +/** Profil sırasına göre listedeki riskleri yeniden hesaplar (program silmez). */ +export function riskleriYenile(profil?: TercihProfili | null) { + yukle(); + const p = profil ?? tercihProfiliOku(); + if (!p) return; + liste = liste.map((t) => ({ + ...t, + risk: riskHesapla(t.efektifSira, p.sira), + })); + kaydet(); + yayinla(); +} + /** Listeye ekler; doluysa veya zaten varsa false döner. */ export function ekle(tercih: ManuelTercih): boolean { yukle(); if (liste.length >= MANUEL_LISTE_MAX || listedeMi(tercih.id)) return false; - liste = [...liste, tercih]; + const profil = tercihProfiliOku(); + const kayit: ManuelTercih = { + ...tercih, + risk: + profil != null + ? riskHesapla(tercih.efektifSira, profil.sira) + : tercih.risk, + }; + liste = [...liste, kayit]; kaydet(); yayinla(); return true; diff --git a/src/components/manuel-liste/tercih-profili-kapisi-lazy.tsx b/src/components/manuel-liste/tercih-profili-kapisi-lazy.tsx new file mode 100644 index 0000000..e7c2d4c --- /dev/null +++ b/src/components/manuel-liste/tercih-profili-kapisi-lazy.tsx @@ -0,0 +1,8 @@ +"use client"; + +import dynamic from "next/dynamic"; + +export const TercihProfiliKapisiLazy = dynamic( + () => import("./tercih-profili-kapisi").then((m) => m.TercihProfiliKapisi), + { ssr: false }, +); diff --git a/src/components/manuel-liste/tercih-profili-kapisi.tsx b/src/components/manuel-liste/tercih-profili-kapisi.tsx new file mode 100644 index 0000000..4ec99eb --- /dev/null +++ b/src/components/manuel-liste/tercih-profili-kapisi.tsx @@ -0,0 +1,319 @@ +"use client"; + +import { useState } from "react"; +import { ArrowRight, Sparkles } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog"; +import { + SihirbazAdimlarLazy, + preloadSihirbazAdimlar, +} from "@/components/sihirbaz-adimlar-lazy"; +import type { SihirbazFacetleri } from "@/lib/db"; +import { + tercihProfiliYaz, + type SihirbazSecimleri, + type TercihProfili, +} from "@/lib/sihirbaz"; +import { + profilKapisiniKapat, + profilTamamlandi, + useProfilKapisi, + type BekleyenProgram, +} from "./profil-kapisi-store"; + +const PUAN_TURLERI_SECENEK = [ + { deger: "say", etiket: "Sayısal" }, + { deger: "ea", etiket: "Eşit Ağırlık" }, + { deger: "soz", etiket: "Sözel" }, + { deger: "dil", etiket: "Dil" }, + { deger: "tyt", etiket: "TYT" }, +] as const; + +type Adim = "giris" | "sira" | "sihirbaz"; + +export function TercihProfiliKapisi() { + const { acik, duzenle, bekleyen, profil } = useProfilKapisi(); + const oturumAnahtari = acik + ? `${duzenle ? "d" : "n"}-${bekleyen?.program.id ?? "yok"}-${profil?.sira ?? 0}-${profil?.tur ?? ""}` + : "kapali"; + + return ( + { + if (!v) profilKapisiniKapat(); + }} + > + + + {duzenle ? "Tercihlerini düzenle" : "Tercih profili sihirbazı"} + + + Sıralamanı ve tercihlerini belirle; eklediğin programları buna göre + değerlendirelim. + + {acik ? ( + + ) : null} + + + ); +} + +function KapıIcerik({ + duzenle, + bekleyen, + profil, +}: { + duzenle: boolean; + bekleyen: BekleyenProgram | null; + profil: TercihProfili | null; +}) { + const bilinenSira = bekleyen?.adaySira ?? profil?.sira ?? null; + const bilinenTur = bekleyen?.adayTur ?? profil?.tur ?? "say"; + + const [adim, setAdim] = useState("giris"); + const [siralama, setSiralama] = useState( + bilinenSira != null ? bilinenSira.toLocaleString("tr-TR") : "", + ); + const [tur, setTur] = useState(bilinenTur); + const [sira, setSira] = useState(bilinenSira); + const [facetler, setFacetler] = useState(null); + const [yukleniyor, setYukleniyor] = useState(false); + const [hata, setHata] = useState(null); + + function siralamaDegisti(ham: string) { + setHata(null); + const rakamlar = ham.replace(/\D/g, "").slice(0, 7); + setSiralama(rakamlar ? Number(rakamlar).toLocaleString("tr-TR") : ""); + } + + async function facetYukle(siraDeger: number, turDeger: string) { + setYukleniyor(true); + setHata(null); + try { + const res = await fetch( + `/api/facetler?sira=${siraDeger}&tur=${turDeger}`, + ); + if (!res.ok) throw new Error(); + const veri = (await res.json()) as { facetler: SihirbazFacetleri }; + setSira(siraDeger); + setTur(turDeger); + setFacetler(veri.facetler); + setAdim("sihirbaz"); + } catch { + setHata("Bir şeyler ters gitti, tekrar dener misin?"); + } finally { + setYukleniyor(false); + } + } + + async function giristenIlerle() { + if (sira != null) { + await facetYukle(sira, tur); + return; + } + setAdim("sira"); + preloadSihirbazAdimlar(); + } + + async function siraGonder(e: React.FormEvent) { + e.preventDefault(); + const value = Number(siralama.replace(/\./g, "")); + if (!value || value < 1 || value > 4_000_000) { + setHata("Geçerli bir başarı sıralaması gir (ör. 85.000)."); + return; + } + await facetYukle(value, tur); + } + + function secimleriTamamla(secimler: SihirbazSecimleri) { + if (sira == null) return; + const yeniProfil: TercihProfili = { sira, tur, secimler }; + tercihProfiliYaz(yeniProfil); + const bekleyenVar = Boolean(bekleyen); + profilTamamlandi(yeniProfil); + if (duzenle) { + toast.success( + "Tercihlerin güncellendi. Listendeki programları yeni seçimlerine göre yeniden değerlendirdik; hiçbir programı silmedik.", + ); + } else if (bekleyenVar) { + toast.success( + "Program listene eklendi. Sıralamana ve tercihlerine göre değerlendirdik.", + ); + } else { + toast.success("Tercihlerin kaydedildi."); + } + } + + if (adim === "giris") { + return ( +
    +
    +

    + {duzenle ? "Tercihlerini güncelle" : "Önce seni biraz tanıyalım"} +

    +

    + {duzenle + ? "Seçimlerini değiştir" + : "Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim"} +

    +

    + {duzenle + ? "İlgi alanların, şehirlerin veya önceliklerin değiştiyse güncelle. Mevcut programların listede kalır; yalnızca değerlendirmeler yenilenir." + : "Sadece sıralamana uyan bir liste değil, ne istediğine de uyan bir tercih listesi kurmak istiyoruz. Birkaç kısa seçim yap; eklediğin programların sana ne kadar uyduğunu birlikte değerlendirelim."} +

    + {bekleyen ? ( +

    + Bekleyen program:{" "} + {bekleyen.program.isim} + + {" "} + · {bekleyen.program.universite} + +

    + ) : null} +
    +
    + + +
    +
    + ); + } + + if (adim === "sira") { + return ( +
    +
    +

    + Başarı sıralaman nedir? +

    +

    + Risk ve erişilebilir programlar sıralamana göre hesaplanır. +

    +
    +
    + {PUAN_TURLERI_SECENEK.map((t) => ( + + ))} +
    +
    + + siralamaDegisti(e.target.value)} + onFocus={preloadSihirbazAdimlar} + aria-invalid={hata ? true : undefined} + className="h-12 bg-background sm:flex-1" + /> + +
    + {hata ? ( +

    + {hata} +

    + ) : null} +
    + +
    +
    + ); + } + + if (facetler && sira != null) { + return ( +
    + {hata ? ( +

    + {hata} +

    + ) : null} + +
    + ); + } + + return null; +} diff --git a/src/components/program-liste-verileri.tsx b/src/components/program-liste-verileri.tsx new file mode 100644 index 0000000..f8492df --- /dev/null +++ b/src/components/program-liste-verileri.tsx @@ -0,0 +1,92 @@ +import { TrendingDown, TrendingUp } from "lucide-react"; +import type { Program } from "@/lib/db"; + +export function programEtkinSira(program: Program): number | null { + return program.sira2025 ?? program.sira2024; +} + +/** 2021–2025 taban sıralamasını sonuç listesindeki kompakt çizgiyle gösterir. */ +export function ProgramSiraGecmisi({ program }: { program: Program }) { + const yillar = [ + [2021, program.sira2021], + [2022, program.sira2022], + [2023, program.sira2023], + [2024, program.sira2024], + [2025, program.sira2025], + ].filter((yil): yil is [number, number] => yil[1] != null); + + if (yillar.length < 2) return ; + + const siralar = yillar.map(([, sira]) => sira); + const min = Math.min(...siralar); + const max = Math.max(...siralar); + const genislik = 56; + const yukseklik = 18; + const bosluk = 2; + const noktalar = yillar.map(([yil, sira], index) => { + const x = + bosluk + + (index / (yillar.length - 1)) * (genislik - bosluk * 2); + const y = + max === min + ? yukseklik / 2 + : bosluk + + ((sira - min) / (max - min)) * (yukseklik - bosluk * 2); + return { x, y, yil, sira }; + }); + const son = noktalar[noktalar.length - 1]; + const ozet = yillar + .map(([yil, sira]) => `${yil}: ~${sira.toLocaleString("tr-TR")}.`) + .join("\n"); + + return ( + + {ozet} + `${nokta.x},${nokta.y}`).join(" ")} + fill="none" + stroke="currentColor" + strokeWidth="1.5" + strokeLinecap="round" + strokeLinejoin="round" + /> + + + ); +} + +/** Son iki yılın tabanı %10'dan fazla değiştiğinde yönü işaretler. */ +export function ProgramTabanTrendi({ program }: { program: Program }) { + if (program.sira2025 == null || program.sira2024 == null) return null; + + const oran = (program.sira2024 - program.sira2025) / program.sira2024; + if (Math.abs(oran) < 0.1) return null; + + const gecis = `${program.sira2024.toLocaleString("tr-TR")} → ${program.sira2025.toLocaleString("tr-TR")}`; + if (oran > 0) { + return ( + + {`Taban yükseliyor: ${gecis}`} + + ); + } + + return ( + + {`Taban geriliyor: ${gecis}`} + + ); +} diff --git a/src/components/program-tablosu.tsx b/src/components/program-tablosu.tsx index 02796a4..bf671de 100644 --- a/src/components/program-tablosu.tsx +++ b/src/components/program-tablosu.tsx @@ -9,18 +9,19 @@ import { useState } from "react"; import Link from "next/link"; import { useLinkStatus } from "next/link"; import { - Check, ChevronDown, - Plus, Rocket, Scale, ShieldCheck, - TrendingDown, - TrendingUp, } from "lucide-react"; import type { DilimKey, Program, PuanTuruKey, RankResults } from "@/lib/db"; import { riskHesapla, type RiskSeviyesi } from "@/lib/risk"; import { turkishSlugify } from "@/lib/slug"; +import { + ProgramSiraGecmisi, + ProgramTabanTrendi, + programEtkinSira, +} from "@/components/program-liste-verileri"; import { Table, TableBody, @@ -30,12 +31,7 @@ import { TableRow, } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { - MANUEL_LISTE_MAX, - ekle, - useManuelListe, - type ManuelTercih, -} from "@/components/manuel-liste/store"; +import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu"; const DILIMLER: { key: DilimKey; @@ -128,22 +124,6 @@ function PuanTuruSecici({ ); } -function efektifSira(p: Program): number | null { - return p.sira2025 ?? p.sira2024; -} - -function tercihYap(p: Program, adaySira: number): ManuelTercih { - const taban = efektifSira(p); - return { - id: p.id, - isim: p.isim, - universite: p.universite, - il: p.il, - efektifSira: taban, - risk: riskHesapla(taban, adaySira), - }; -} - const BOS_EKSTRA: Record = { hayal: [], dengeli: [], @@ -167,12 +147,9 @@ export function ProgramTablosu({ adaySira: number; tur: PuanTuruKey; }) { - const liste = useManuelListe(); const [aktifDilim, setAktifDilim] = useState(() => varsayilanDilim(sonuclar), ); - const listedekiler = new Set(liste.map((t) => t.id)); - const doldu = liste.length >= MANUEL_LISTE_MAX; // "Daha fazla göster" ile sayfalanan devam satırları. Sıra/tür değişince // (puan türü seçici aynı bileşeni yeniden render eder, remount etmez) @@ -190,10 +167,6 @@ export function ProgramTablosu({ } } - function eklemeyiDene(p: Program) { - ekle(tercihYap(p, adaySira)); - } - async function dahaFazlaYukle(dilim: DilimKey, offset: number) { setYuklenen(dilim); try { @@ -300,9 +273,7 @@ export function ProgramTablosu({ key={p.id} program={p} adaySira={adaySira} - listede={listedekiler.has(p.id)} - doldu={doldu} - onEkle={eklemeyiDene} + tur={tur} /> ))} @@ -334,86 +305,6 @@ export function ProgramTablosu({ ); } -// Tabanın 2021–2025 seyri: küçük sıra = daha rekabetçi olduğundan y ekseni -// ters çevrilir; çizgi yukarı gidiyorsa bölüm yıllar içinde zorlaşıyor demektir. -function SiraGecmisi({ p }: { p: Program }) { - const yillar = [ - [2021, p.sira2021], - [2022, p.sira2022], - [2023, p.sira2023], - [2024, p.sira2024], - [2025, p.sira2025], - ].filter((y): y is [number, number] => y[1] != null); - if (yillar.length < 2) return ; - - const siralar = yillar.map(([, s]) => s); - const min = Math.min(...siralar); - const max = Math.max(...siralar); - const W = 56; - const H = 18; - const PAD = 2; - const noktalar = yillar.map(([yil, s], i) => { - const x = PAD + (i / (yillar.length - 1)) * (W - PAD * 2); - // min sıra (en rekabetçi) üstte dursun - const y = - max === min - ? H / 2 - : PAD + ((s - min) / (max - min)) * (H - PAD * 2); - return { x, y, yil, s }; - }); - const son = noktalar[noktalar.length - 1]; - const ozet = yillar - .map(([yil, s]) => `${yil}: ~${s.toLocaleString("tr-TR")}.`) - .join("\n"); - - return ( - - {ozet} - `${n.x},${n.y}`).join(" ")} - fill="none" - stroke="currentColor" - strokeWidth="1.5" - strokeLinecap="round" - strokeLinejoin="round" - /> - - - ); -} - -// Taban sıra iki yıl üst üste %10'dan fazla oynadıysa yönünü gösterir; -// küçük sıra = daha rekabetçi taban demektir. -function TabanTrendi({ p }: { p: Program }) { - if (p.sira2025 == null || p.sira2024 == null) return null; - const oran = (p.sira2024 - p.sira2025) / p.sira2024; - if (Math.abs(oran) < 0.1) return null; - const yukseliyor = oran > 0; - const gecis = `${p.sira2024.toLocaleString("tr-TR")} → ${p.sira2025.toLocaleString("tr-TR")}`; - return yukseliyor ? ( - - {`Taban yükseliyor: ${gecis}`} - - ) : ( - - {`Taban geriliyor: ${gecis}`} - - ); -} - // /universite/[slug] sayfa slug'ı: harita.ts'teki uniAdiNormalize ile aynı // kural (sondaki "(İL)" eki atılır) — client bundle'a harita verisi çekmemek // için burada tekrarlanır. Katalog haritası DB'deki tüm universite @@ -425,17 +316,13 @@ function uniSayfaSlug(ad: string): string { function ProgramSatiri({ program: p, adaySira, - listede, - doldu, - onEkle, + tur, }: { program: Program; adaySira: number; - listede: boolean; - doldu: boolean; - onEkle: (p: Program) => void; + tur: PuanTuruKey; }) { - const taban = efektifSira(p); + const taban = programEtkinSira(p); const risk = riskHesapla(taban, adaySira); const devlet = p.unitur === "DEVLET"; @@ -466,7 +353,6 @@ function ProgramSatiri({ href={`/universite/${uniSayfaSlug(p.universite)}`} className="hover:text-primary hover:underline" title={`${p.universite} taban puanları sayfası`} - // Satırdaki "+" ekleme akışıyla karışmasın: tıklama satıra yayılmaz onClick={(e) => e.stopPropagation()} > {p.universite} @@ -479,7 +365,7 @@ function ProgramSatiri({ - + - + {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"} - + ); diff --git a/src/components/universite-konum-haritasi.tsx b/src/components/universite-konum-haritasi.tsx index 33b4f12..39f0c76 100644 --- a/src/components/universite-konum-haritasi.tsx +++ b/src/components/universite-konum-haritasi.tsx @@ -1,4 +1,3 @@ -import { siApple, siGooglemaps } from "simple-icons"; import { cities } from "turkey-map-react/lib/data"; import { HARITA_GENISLIK, @@ -7,34 +6,6 @@ import { normalizeIlAdi, uniKonum, } from "@/lib/harita"; -import { trBaslikDuzeni } from "@/lib/slug"; - -function HaritaMarkaIkonu({ marka }: { marka: string }) { - const ikon = - marka === "Google" ? siGooglemaps : marka === "Apple" ? siApple : null; - - if (ikon) { - return ( - - - - ); - } - - return ( - - Я - - ); -} export function UniversiteKonumHaritasi({ universite, @@ -44,17 +15,7 @@ export function UniversiteKonumHaritasi({ il: string | null; }) { const konum = uniKonum(universite, il); - const ilAdi = il ? trBaslikDuzeni(il) : null; - const arama = `${universite}${ilAdi ? ` ${ilAdi}` : ""}`; - const sorgu = encodeURIComponent(arama); - const haritaBaglantilari = [ - { - ad: "Google", - href: `https://www.google.com/maps/search/?api=1&query=${sorgu}`, - }, - { ad: "Yandex", href: `https://yandex.com.tr/maps/?text=${sorgu}` }, - { ad: "Apple", href: `https://maps.apple.com/?q=${sorgu}` }, - ]; + if (!konum) return null; return (
    - {konum ? ( - - ) : ( -
    + - {haritaBaglantilari.map((baglanti) => ( - - - - ))} -
    - )} + {cities.map((city) => { + const secili = il ? normalizeIlAdi(city.name) === il : false; + return ( + + {city.name} + + ); + })} + + + + + + +
    ); } diff --git a/src/components/universite-program-tablosu.tsx b/src/components/universite-program-tablosu.tsx new file mode 100644 index 0000000..25295b7 --- /dev/null +++ b/src/components/universite-program-tablosu.tsx @@ -0,0 +1,127 @@ +"use client"; + +import Link from "next/link"; +import type { Program } from "@/lib/db"; +import { + ProgramSiraGecmisi, + ProgramTabanTrendi, + programEtkinSira, +} from "@/components/program-liste-verileri"; +import { ProgramEkleButonu } from "@/components/manuel-liste/program-ekle-butonu"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +const sayi = (n: number) => n.toLocaleString("tr-TR"); + +const TUR_ETIKET: Record = { + SAYISAL: "SAY", + "EŞİT AĞIRLIK": "EA", + SÖZEL: "SÖZ", + DİL: "DİL", + TYT: "TYT", +}; + +export type UniProgramSatiri = Program & { bolumSlug: string | null }; + +export function UniversiteProgramTablosu({ + gruplar, +}: { + gruplar: { fakulte: string; programlar: UniProgramSatiri[] }[]; +}) { + return ( + <> + {gruplar.map(({ fakulte, programlar }, i) => ( +
    +

    + {fakulte} +

    +
    + + + + Program + + Son 5 yıl + + + 2025 puan + + + Kontenjan + + Taban sıra + + Listeye ekle + + + + + {programlar.map((p) => { + const taban = programEtkinSira(p); + return ( + + + {p.bolumSlug ? ( + + {p.isim.trim()} + + ) : ( + + {p.isim.trim()} + + )} + + + {TUR_ETIKET[p.tur] ?? p.tur} + + {p.sure ? ( + {p.sure} yıl + ) : null} + + + + + + + {p.puan2025 != null + ? p.puan2025.toFixed(2).replace(".", ",") + : "—"} + + + {p.kontenjan2025 != null + ? `${sayi(p.kontenjan2025)} kişi` + : "—"} + + + + + {taban != null ? `~${sayi(taban)}.` : "—"} + + + + + + + ); + })} + +
    +
    +
    + ))} + + ); +} diff --git a/src/lib/sihirbaz.ts b/src/lib/sihirbaz.ts index 9f0bb61..0a598b3 100644 --- a/src/lib/sihirbaz.ts +++ b/src/lib/sihirbaz.ts @@ -34,6 +34,12 @@ export interface SihirbazSecimleri { oncelikler: string[]; } +export interface TercihProfili { + sira: number; + tur: string; + secimler: SihirbazSecimleri; +} + const UNI_TIPLERI: UniversiteTipi[] = ["devlet", "vakif", "farketmez"]; /** @@ -90,6 +96,44 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null { return { kategoriler, iller, universiteTipi, oncelikler }; } +/** localStorage'taki ortak sıra + sihirbaz profilini güvenle doğrular. */ +export function tercihProfiliDogrula(girdi: unknown): TercihProfili | null { + if (typeof girdi !== "object" || girdi === null) return null; + const p = girdi as Record; + const sira = typeof p.sira === "number" ? p.sira : Number(p.sira); + if (!Number.isFinite(sira) || sira < 1 || sira > 4_000_000) return null; + if (typeof p.tur !== "string" || p.tur.trim().length === 0) return null; + const secimler = sihirbazDogrula(p.secimler); + if (!secimler) return null; + return { sira, tur: p.tur, secimler }; +} + +/** Client'ta SIHIRBAZ_STORAGE_KEY'den doğrulanmış profil okur. */ +export function tercihProfiliOku(): TercihProfili | null { + if (typeof window === "undefined") return null; + try { + const ham = localStorage.getItem(SIHIRBAZ_STORAGE_KEY); + if (!ham) return null; + return tercihProfiliDogrula(JSON.parse(ham)); + } catch { + return null; + } +} + +/** Ortak profili yazar; hero ve AI sihirbazıyla aynı anahtarı kullanır. */ +export function tercihProfiliYaz(profil: TercihProfili): void { + if (typeof window === "undefined") return; + const dogru = tercihProfiliDogrula(profil); + if (!dogru) return; + try { + localStorage.setItem(SIHIRBAZ_STORAGE_KEY, JSON.stringify(dogru)); + } catch {} +} + +export function tercihProfiliTamamlandiMi(): boolean { + return tercihProfiliOku() !== null; +} + /** Prompt'a enjekte edilecek Türkçe özet. */ export function secimOzeti(secimler: SihirbazSecimleri): string { const satirlar = [ diff --git a/src/lib/tercih-degerlendirme.ts b/src/lib/tercih-degerlendirme.ts new file mode 100644 index 0000000..fd5e86a --- /dev/null +++ b/src/lib/tercih-degerlendirme.ts @@ -0,0 +1,127 @@ +import { kategoriEslesir } from "./kategoriler"; +import { riskHesapla, type RiskSeviyesi } from "./risk"; +import type { TercihProfili } from "./sihirbaz"; + +export type DegerlendirilecekProgram = { + isim: string; + il: string | null; + unitur?: string | null; + efektifSira: number | null; +}; + +export type TercihDegerlendirmesi = { + risk: RiskSeviyesi | null; + riskEtiketi: "Hayal" | "Dengeli" | "Daha güvenli" | "Veri yetersiz"; + riskMetni: string; + olumlu: string[]; + celiskiler: string[]; +}; + +function normalize(deger: string): string { + return deger.trim().toLocaleUpperCase("tr-TR"); +} + +export function tercihDegerlendir( + program: DegerlendirilecekProgram, + profil: TercihProfili, +): TercihDegerlendirmesi { + const risk = riskHesapla(program.efektifSira, profil.sira); + const olumlu: string[] = []; + const celiskiler: string[] = []; + + if (kategoriEslesir(program.isim, profil.secimler.kategoriler)) { + olumlu.push("İlgi alanlarınla uyumlu"); + } else { + celiskiler.push("Seçtiğin ilgi alanlarının dışında görünüyor"); + } + + if (profil.secimler.iller.length > 0) { + const seciliIller = new Set(profil.secimler.iller.map(normalize)); + if (program.il && seciliIller.has(normalize(program.il))) { + olumlu.push("Tercih ettiğin şehirlerden birinde"); + } else { + celiskiler.push("Tercih ettiğin şehirlerin dışında"); + } + } + + if (profil.secimler.universiteTipi !== "farketmez" && program.unitur) { + const devlet = normalize(program.unitur) === "DEVLET"; + const uyuyor = + (profil.secimler.universiteTipi === "devlet" && devlet) || + (profil.secimler.universiteTipi === "vakif" && !devlet); + if (uyuyor) olumlu.push("Üniversite türü tercihine uyuyor"); + else celiskiler.push("Üniversite türü tercihinle uyuşmuyor"); + } + + if (profil.secimler.oncelikler.includes("Kolay yerleşme")) { + if (risk === "guvenli") { + olumlu.push("Daha güvenli seçenek önceliğini destekliyor"); + } else if (risk === "riskli") { + celiskiler.push("Kolay yerleşme önceliğine göre iddialı bir seçim"); + } + } + + const riskEtiketi = + risk === "riskli" + ? "Hayal" + : risk === "az-riskli" + ? "Dengeli" + : risk === "guvenli" + ? "Daha güvenli" + : "Veri yetersiz"; + const riskMetni = + risk === "riskli" + ? "Geçen yılın taban sıralaması senin sıralamandan daha yukarıda." + : risk === "az-riskli" + ? "Geçmiş taban sıralamasına göre dengeli aralıkta." + : risk === "guvenli" + ? "Geçmiş taban sıralamasına göre daha güvenli aralıkta." + : "Yerleşme riskini hesaplamak için yeterli taban sıra verisi yok."; + + return { + risk, + riskEtiketi, + riskMetni, + olumlu: olumlu.slice(0, 3), + celiskiler: celiskiler.slice(0, 2), + }; +} + +export function tercihListesiOzeti( + programlar: DegerlendirilecekProgram[], + profil: TercihProfili, +) { + const degerlendirmeler = programlar.map((p) => tercihDegerlendir(p, profil)); + const hayal = degerlendirmeler.filter((d) => d.risk === "riskli").length; + const dengeli = degerlendirmeler.filter( + (d) => d.risk === "az-riskli", + ).length; + const dahaGuvenli = degerlendirmeler.filter( + (d) => d.risk === "guvenli", + ).length; + const dikkat = degerlendirmeler.filter( + (d) => d.celiskiler.length > 0, + ).length; + const gucluUyum = degerlendirmeler.filter( + (d) => d.olumlu.length >= 2 && d.celiskiler.length === 0, + ).length; + + let oneri = "Yeni seçimler ekledikçe listenin dengesi burada görünecek."; + if (programlar.length > 0 && dahaGuvenli === 0) { + oneri = + "Listen iddialı seçeneklerde yoğunlaşıyor. Birkaç daha güvenli program eklemek riski dağıtabilir."; + } else if (programlar.length > 2 && dengeli === 0) { + oneri = + "Listen için dengeli aralıkta birkaç program eklemek seçeneklerini güçlendirebilir."; + } else if (dikkat > Math.max(1, programlar.length / 2)) { + oneri = + "Seçtiklerinin çoğu profilinle ayrışıyor. Dikkat notlarını birlikte gözden geçir."; + } else if (programlar.length >= 24) { + oneri = + "24 tercihe ulaştın. Şimdi risk dağılımını ve dikkat notlarını gözden geçir."; + } else if (programlar.length > 0) { + oneri = "Listen dengeleniyor; uyum ve risk dağılımını koruyarak ilerle."; + } + + return { hayal, dengeli, dahaGuvenli, dikkat, gucluUyum, oneri }; +}