Compare commits
28 Commits
a834f5ff01
...
8f4669e31d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f4669e31d | ||
|
|
d7c29ac4a6 | ||
|
|
bf6aee1fbe | ||
|
|
f05f3e0c69 | ||
|
|
271e01e1dc | ||
|
|
977db40099 | ||
|
|
f7ed8f2f0b | ||
|
|
1e9598ac93 | ||
|
|
5853b7d091 | ||
|
|
0400aa5d77 | ||
|
|
d79758adc0 | ||
|
|
294fb2cf6a | ||
|
|
f17ee48a88 | ||
|
|
97952f6e06 | ||
|
|
899d29af04 | ||
|
|
9a446df572 | ||
|
|
343fcf610a | ||
|
|
1a87d89b5a | ||
|
|
f1abedccee | ||
|
|
4dfb24bb13 | ||
|
|
79bc7fa8be | ||
|
|
ea3ba5f335 | ||
|
|
17046c830f | ||
|
|
2af4a9f781 | ||
|
|
5992cf3637 | ||
|
|
cae2aaab4d | ||
|
|
16c450827b | ||
|
|
aac33e7533 |
@@ -26,11 +26,18 @@ jobs:
|
||||
GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
RESEND_API_KEY=${{ secrets.RESEND_API_KEY }}
|
||||
EMAIL_FROM=${{ secrets.EMAIL_FROM }}
|
||||
# İsteğe bağlı: secret yoksa boş gelir → kod destek@kolaytercih.com'a düşer.
|
||||
EMAIL_REPLY_TO=${{ secrets.EMAIL_REPLY_TO }}
|
||||
IYZICO_API_KEY=${{ secrets.IYZICO_API_KEY }}
|
||||
IYZICO_SECRET_KEY=${{ secrets.IYZICO_SECRET_KEY }}
|
||||
IYZICO_BASE_URL=${{ secrets.IYZICO_BASE_URL }}
|
||||
OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}
|
||||
OPENROUTER_MODEL=${{ secrets.OPENROUTER_MODEL }}
|
||||
# Sunucu tarafı Rybbit olayları (odeme_tamamlandi, giris_basarili —
|
||||
# src/lib/analitik-sunucu.ts). Üçünden biri boşsa gönderim no-op.
|
||||
RYBBIT_HOST=${{ secrets.RYBBIT_HOST }}
|
||||
RYBBIT_SITE_ID=${{ secrets.RYBBIT_SITE_ID }}
|
||||
RYBBIT_API_KEY=${{ secrets.RYBBIT_API_KEY }}
|
||||
EOF
|
||||
|
||||
# BuildKit'te "docker build --memory" YOK SAYILIR (moby/buildkit#1362);
|
||||
|
||||
@@ -25,5 +25,5 @@ Funnel CTA butonlarında ("Listemi oluştur", "Tercihlerimi belirle", giriş/öd
|
||||
|
||||
# Meraklısına'daki mermaid şeması mimariyle senkron kalmalı
|
||||
|
||||
`/meraklisina` sayfasındaki sihirbaz akış şeması (`src/features/pazarlama/components/huni-semasi.tsx` içindeki `SEMA` sabiti) ürünün gerçek davranışının belgesidir, dekor değildir. Sihirbaz/listeleme mimarisinde davranış değiştiren her değişiklikte bu şema da aynı PR/commit içinde güncellenmeli: sihirbaz adımlarının sayısı-sırası-içeriği (alan → il → devlet/vakıf), aday havuzu kuralları, 24'lük liste iskeleti (hayal/dengeli/garanti dağılımı), havuz 24'ün altına düşünce filtre gevşetme sırası ve yapay zekânın havuz-dışına-çıkamama kuralı. Şemayı güncellerken çevresindeki unsurları da eşitle: bölümdeki anlatım metni (`src/features/pazarlama/components/meraklisina-icerik.tsx`, 4. bölüm), kabın `aria-label`'ı ve figcaption. Şema öğrenci diliyle kalmalı (SQL/prompt/teknik detay yok) ve ürünün risk renk dilini korumalı — kırmızı/sarı/yeşil her zaman metin etiketiyle birlikte, renk tek başına anlam taşımaz.
|
||||
`/meraklisina` sayfasındaki sihirbaz akış şeması (`src/features/pazarlama/components/huni-semasi.tsx` içindeki `SEMA` sabiti) ürünün gerçek davranışının belgesidir, dekor değildir. Sihirbaz/listeleme mimarisinde davranış değiştiren her değişiklikte bu şema da aynı PR/commit içinde güncellenmeli: sihirbaz adımlarının sayısı-sırası-içeriği (alan → il → devlet/vakıf), aday havuzu kuralları, 24'lük liste iskeleti (hayal/dengeli/güvenli dağılımı — üçüncü dilimin kullanıcıya görünen adı "Güvenli"dir; iç anahtar, tip, DB değeri, mermaid `classDef` ve analitik değeri `garanti` olarak kalır, görünen etiketin tek kaynağı `src/lib/risk.ts` içindeki `DILIM_ETIKET`), havuz 24'ün altına düşünce filtre gevşetme sırası ve yapay zekânın havuz-dışına-çıkamama kuralı. Şemayı güncellerken çevresindeki unsurları da eşitle: bölümdeki anlatım metni (`src/features/pazarlama/components/meraklisina-icerik.tsx`, 4. bölüm), kabın `aria-label`'ı ve figcaption. Şema öğrenci diliyle kalmalı (SQL/prompt/teknik detay yok) ve ürünün risk renk dilini korumalı — kırmızı/sarı/yeşil her zaman metin etiketiyle birlikte, renk tek başına anlam taşımaz.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
|
||||
@@ -51,8 +51,8 @@ flowchart TD
|
||||
|
||||
1. **Her satırın koşulunu okudun mu?** Yaş, cinsiyet, sağlık raporu gibi [özel koşullar](/rehber/ozel-kosullu-programlar) ve [başarı sırası şartları](/rehber/siralama-sartlari-2026) sağlanmıyorsa o satır ya boşa gider ya da yerleşsen bile kayıtta yanar.
|
||||
2. **Üst sıralarda ölü tercih var mı?** Tabanı senden çok iyi programı listenin *sonuna* yazmak satırı çöpe atar ([ölü tercih nedir](/rehber/olu-tercih-nedir)).
|
||||
3. **Listenin sonu gerçekten garanti mi?** Son 4-6 satır, yerleşirsen kayıt yaptıracağın programlar olmalı ([hayal/dengeli/garanti dağılımı](/rehber/tercih-listesi-nasil-yapilir)).
|
||||
3. **Listenin sonu gerçekten güvenli mi?** Son 4-6 satır — halk arasındaki adıyla "garanti tercihler" — yerleşirsen kayıt yaptıracağın programlar olmalı ([hayal/dengeli/güvenli dağılımı](/rehber/tercih-listesi-nasil-yapilir)).
|
||||
4. **Kayıt yaptırmayacağın satır var mı?** Varsa sil: oraya yerleşirsen hem yılın gider hem [ek yerleştirmeye](/rehber/ek-yerlestirme-2026) başvuramazsın.
|
||||
5. **Kodları tek tek doğruladın mı?** Ekrandaki program adı + üniversite + şehir üçlüsünü kendi listenle satır satır karşılaştır.
|
||||
|
||||
Listen henüz oturmadıysa önce evreni gör: [tercih robotuna](/tercih-robotu) sıralamanı gir, sıralamana düşen programları hayal/dengeli/garanti dilimleriyle çıkar, sonra AİS'e kodları bu listeden aktar. AİS'te düşünülmez — AİS'e *bitmiş* liste girilir.
|
||||
Listen henüz oturmadıysa önce evreni gör: [tercih robotuna](/tercih-robotu) sıralamanı gir, sıralamana düşen programları hayal/dengeli/güvenli dilimleriyle çıkar, sonra AİS'e kodları bu listeden aktar. AİS'te düşünülmez — AİS'e *bitmiş* liste girilir.
|
||||
|
||||
@@ -68,6 +68,6 @@ En son adım bu, çünkü ilk dördü olmadan sorduğun sorular yüzeysel kalır
|
||||
|
||||
## Cevap belirsiz kaldıysa ne yapmalı?
|
||||
|
||||
Bazı alanlarda dört yıl sonrası dürüstçe belirsizdir — ve bu, o bölümü kötü yapmaz. Belirsizlikle baş etmenin tercih listesindeki karşılığı şu: **listeye alternatif de koy.** İstediğin bölümü üst bloğa yaz, ama alt bloğa da seni memnun edecek ikinci ve üçüncü seçenekleri diz. [Tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–garanti dağılımı tam olarak bu belirsizliği yönetmek için var.
|
||||
Bazı alanlarda dört yıl sonrası dürüstçe belirsizdir — ve bu, o bölümü kötü yapmaz. Belirsizlikle baş etmenin tercih listesindeki karşılığı şu: **listeye alternatif de koy.** İstediğin bölümü üst bloğa yaz, ama alt bloğa da seni memnun edecek ikinci ve üçüncü seçenekleri diz. [Tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–güvenli (halk arasında "garanti tercih") dağılımı tam olarak bu belirsizliği yönetmek için var.
|
||||
|
||||
Bir de bölümlerin kendi sayfalarına bak: [bölüm kataloğunda](/bolumler) her bölümün hangi üniversitelerde açık olduğunu, taban sıralamalarını ve program sayısını görebilir, araştırmanın dördüncü adımını dakikalar içinde tamamlayabilirsin. Sıralamana hangi bölümlerin düştüğünü görmek içinse [tercih robotuna](/tercih-robotu) sıralamanı gir.
|
||||
|
||||
@@ -8,13 +8,13 @@ guncelleme: 2026-09-21
|
||||
"Sıralamam X, nereyi kazanırım?" tercih döneminin en çok sorulan sorusu. Aşağıdaki bantlar 2025 YKS yerleştirme verilerine (YÖK Atlas) dayanır ve **kabaca bir harita** çizer — kesin sınır değildir; aynı bölümün tabanı üniversiteye, şehre ve burs durumuna göre çok geniş bir aralığa yayılır.
|
||||
|
||||
```mermaid
|
||||
%% aria: Sıralama bandı okuma şeması: önce kendi puan türünün sıralama bandını bulursun; bant sana hangi bölümlere bakmaya değer olduğunu söyler ama hangi üniversitede tutacağını söylemez. Sonra bölüm sayfasındaki tabloya inip kendi sıralamanın düştüğü programları görürsün; en hızlı yol tercih robotuna sıralamanı girip hayal, dengeli ve garanti dilimlerini tek bakışta almaktır. Bant pusuladır, garanti değildir.
|
||||
%% aria: Sıralama bandı okuma şeması: önce kendi puan türünün sıralama bandını bulursun; bant sana hangi bölümlere bakmaya değer olduğunu söyler ama hangi üniversitede tutacağını söylemez. Sonra bölüm sayfasındaki tabloya inip kendi sıralamanın düştüğü programları görürsün; en hızlı yol tercih robotuna sıralamanı girip hayal, dengeli ve güvenli dilimlerini tek bakışta almaktır. Bant pusuladır, garanti değildir.
|
||||
%% altyazi: Bant "nereye bakayım" der; kesin satırı bölüm tablosu ve tercih robotu söyler.
|
||||
flowchart TD
|
||||
B("Sıralama bandını bul<br/>SAY, EA, sözel veya dil") --> N("Bant sana hangi bölümlere<br/>bakmaya değer olduğunu söyler")
|
||||
N --> T{"Nasıl netleşir?"}
|
||||
T -- "bölüm tablosuna in" --> TAB("Aynı bölümde tabanlar<br/>üniversiteye göre çok açılır")
|
||||
T -- "tercih robotuna gir" --> R("Hayal / dengeli / garanti<br/>dilimleri saniyede çıkar")
|
||||
T -- "tercih robotuna gir" --> R("Hayal / dengeli / güvenli<br/>dilimleri saniyede çıkar")
|
||||
TAB --> K("Bandına düşen satırları<br/>istek sırana göre diz")
|
||||
R --> K
|
||||
|
||||
@@ -50,7 +50,7 @@ Sözel ve Dil tarafında bantlar daha da geniştir; öğretmenlikler için ayrı
|
||||
|
||||
1. **Bandını bul, ama banda güvenme.** Bant sana "hangi bölümlere bakmaya değer" der; hangi üniversitede tutacağını söylemez. Aynı bölüm içinde tabanlar 10 kat oynayabilir.
|
||||
2. **Bölüm sayfasının tablosuna in.** Örneğin [Psikoloji taban puanları](/bolum/psikoloji) sayfasında 380'e yakın programın tek tek tabanını görürsün — senin sıralaman listenin neresine düşüyorsa gerçekçi bölgen orasıdır.
|
||||
3. **Kendi sıralamanla tara.** [Tercih robotuna](/tercih-robotu) sıralamanı gir; robot 23.000+ programın içinden senin bandına düşenleri hayal/dengeli/garanti dilimleriyle ayırıp önüne koyar. Tablo ezberlemekten hızlıdır.
|
||||
3. **Kendi sıralamanla tara.** [Tercih robotuna](/tercih-robotu) sıralamanı gir; robot 23.000+ programın içinden senin bandına düşenleri hayal, dengeli ve güvenli (halk arasında "garanti tercih") dilimleriyle ayırıp önüne koyar. Tablo ezberlemekten hızlıdır.
|
||||
4. **Henüz sıralaman yoksa tersinden git.** Seneye hazırlanıyorsan istediğin bölümden başlayıp kaçıncı olman gerektiğini [hedef sıralama nasıl belirlenir?](/rehber/hedef-siralama-nasil-belirlenir) yazısındaki yöntemle çıkar.
|
||||
|
||||
## Unutma
|
||||
|
||||
@@ -31,7 +31,7 @@ flowchart TD
|
||||
|
||||
## Bu mantıktan çıkan üç altın kural
|
||||
|
||||
**1) Üst sıradaki "hayal" tercihi alttakilere zarar veremez.** Sistem 1. tercihinde yer bulamazsa hiç duraksamadan 2.'ye geçer. Hayal bölümünü en üste yazmanın tek "maliyeti" bir satırdır; alttaki dengeli ve garanti tercihlerinin ihtimali kılını bile kıpırdatmaz. Bu yüzden listenin başı gönlüne göre, sonu aklına göre kurulur — dilim mantığının tamamı [tercih listesi rehberinde](/rehber/tercih-listesi-nasil-yapilir) var.
|
||||
**1) Üst sıradaki "hayal" tercihi alttakilere zarar veremez.** Sistem 1. tercihinde yer bulamazsa hiç duraksamadan 2.'ye geçer. Hayal bölümünü en üste yazmanın tek "maliyeti" bir satırdır; alttaki dengeli ve güvenli (halk arasında "garanti") tercihlerinin ihtimali kılını bile kıpırdatmaz. Bu yüzden listenin başı gönlüne göre, sonu aklına göre kurulur — dilim mantığının tamamı [tercih listesi rehberinde](/rehber/tercih-listesi-nasil-yapilir) var.
|
||||
|
||||
**2) Yerleştiğin satırın altı yok hükmündedir.** 3. tercihine yerleştiysen 4-24 arası hiç okunmaz. Bu yüzden listeyi *yerleşme ihtimaline göre değil, istek sırana göre* dizmelisin: en çok istediğin hep daha üstte durmalı. "Daha çok istediğim bölüm alttaydı, keşke" cümlesinin telafisi yoktur.
|
||||
|
||||
@@ -44,7 +44,7 @@ Sistemin yukarıdan aşağı okuması, sıralamanın çok üstünde kapatan bir
|
||||
## Pratik sonuç
|
||||
|
||||
- Listenin başına puanın "yetmeyecek gibi" duran ama çok istediğin programları çekinmeden yaz.
|
||||
- Ortayı sıralamana yakın programlarla doldur, sona gerçekten razı olacağın garantileri koy.
|
||||
- Ortayı sıralamana yakın programlarla doldur, sona gerçekten razı olacağın güvenli tercihleri koy.
|
||||
- Sıralamanla hangi dilime neyin girdiğini görmek için [tercih robotuna](/tercih-robotu) sıralamanı girmen yeterli.
|
||||
|
||||
Sistemi bilen aday, listeyi korkuyla değil stratejiyle kurar. Korku listesi hep aşağı yığılır; strateji listesi yukarıdan kazanır.
|
||||
|
||||
@@ -39,7 +39,7 @@ Pratik kontrol yöntemi:
|
||||
- Tabanı, senin sıralamandan iyiyse → o satır ancak listenin *üst bloğunda* (hayal) anlamlıdır.
|
||||
- Tabanı üstündeki satırdan da iyiyse → sıra hatası var; satırları taban gevşekliğine göre yeniden diz.
|
||||
|
||||
[Tercih robotuna](/tercih-robotu) sıralamanı girdiğinde programlar hayal/dengeli/garanti dilimleriyle etiketlenir; bir programın senin sıralamana göre hangi bölgede olduğunu tek bakışta görürsün. Popüler bölümlerde tabanların ne kadar geniş bir aralığa yayıldığını görmek için [Psikoloji](/bolum/psikoloji) veya [Hukuk](/bolum/hukuk) tablolarına bak — aynı bölümün tabanı üniversiteye göre yüz binlerce sıra oynayabilir; yani bölüm ölü değildir, *yanlış üniversite-satır eşleşmesi* ölüdür.
|
||||
[Tercih robotuna](/tercih-robotu) sıralamanı girdiğinde programlar hayal, dengeli ve güvenli (halk arasında "garanti tercih") dilimleriyle etiketlenir; bir programın senin sıralamana göre hangi bölgede olduğunu tek bakışta görürsün. Popüler bölümlerde tabanların ne kadar geniş bir aralığa yayıldığını görmek için [Psikoloji](/bolum/psikoloji) veya [Hukuk](/bolum/hukuk) tablolarına bak — aynı bölümün tabanı üniversiteye göre yüz binlerce sıra oynayabilir; yani bölüm ölü değildir, *yanlış üniversite-satır eşleşmesi* ölüdür.
|
||||
|
||||
## Ölü tercihten temizlenmiş liste nasıl görünür?
|
||||
|
||||
|
||||
@@ -45,4 +45,4 @@ flowchart TD
|
||||
|
||||
24 tercih hakkı lisans ve önlisansta **ortaktır** — aynı listeye ikisini de yazabilirsin. Ama farklı puan türlerinin sıralamaları karışacağı için düzeni şöyle kur: önce gitmeye razı olduğun lisans programları (kendi türünün sıralamasıyla), sonra önlisans bloğu (TYT sıralamasıyla). Her iki blokta da [ölü tercih](/rehber/olu-tercih-nedir) kuralları aynen geçerli.
|
||||
|
||||
TYT sıralamanı [tercih robotuna](/tercih-robotu) girip türü "TYT" seçersen önlisans evrenini hayal/dengeli/garanti dilimleriyle görürsün; lisans sıralamanla da ayrı bir tarama yapıp iki listeyi yan yana koyman en sağlıklısı.
|
||||
TYT sıralamanı [tercih robotuna](/tercih-robotu) girip türü "TYT" seçersen önlisans evrenini hayal, dengeli ve güvenli (halk arasında "garanti tercih") dilimleriyle görürsün; lisans sıralamanla da ayrı bir tarama yapıp iki listeyi yan yana koyman en sağlıklısı.
|
||||
|
||||
@@ -69,4 +69,4 @@ Pratik sonuç: özel yetenek hedefliyorsan **iki paralel süreç** yürütürsü
|
||||
- Koşulunu sağlamadığın programı hiç yazma. Bu satır, [ölü tercihten](/rehber/olu-tercih-nedir) daha kötüdür: ölü tercih sadece yer kaplar, koşulu sağlanmayan tercih ise yerleşirsen yılını götürür.
|
||||
- Özel yetenek programlarını merkezi listene yazmaya çalışma; ayrı süreçtir.
|
||||
|
||||
Kılavuz koşullarını okuduktan sonra sıralamana hangi programların gerçekten açık olduğunu görmek için [tercih robotuna](/tercih-robotu) sıralamanı gir; listeyi kurarken [tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–garanti dağılımını temel al.
|
||||
Kılavuz koşullarını okuduktan sonra sıralamana hangi programların gerçekten açık olduğunu görmek için [tercih robotuna](/tercih-robotu) sıralamanı gir; listeyi kurarken [tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–güvenli (halk arasında "garanti tercih") dağılımını temel al.
|
||||
|
||||
@@ -14,7 +14,7 @@ flowchart TD
|
||||
S -- "hayır" --> K("Listeye yazamazsın:<br/>alternatif rotaya yönel")
|
||||
S -- "evet" --> A("Kapı açık — ama bu yerleşmek değil")
|
||||
A --> T("Gözünü tabana çevir:<br/>kendi sıralamanın düştüğü bandı bul")
|
||||
T --> L("Gerçekçi satırları<br/>hayal / dengeli / garanti diz")
|
||||
T --> L("Gerçekçi satırları<br/>hayal / dengeli / güvenli diz")
|
||||
|
||||
classDef hayal fill:#fee2e2,stroke:#ef4444,color:#991b1b
|
||||
classDef uyari fill:#fef3c7,stroke:#f59e0b,color:#92400e
|
||||
|
||||
@@ -48,4 +48,4 @@ Liste kurarken açtığın her tabloda gözün **sıralama kolonunda** olmalı.
|
||||
|
||||
Seneye hazırlanıyorsan aynı mantık hedef koyarken de geçerli: hedefini netle ya da puanla değil sıralamayla koy — yöntemi [hedef sıralama nasıl belirlenir?](/rehber/hedef-siralama-nasil-belirlenir) yazısında.
|
||||
|
||||
Sıralamanı biliyorsan gerisi mekanik: [tercih robotuna](/tercih-robotu) gir, sıralamana uygun programları hayal/dengeli/garanti dilimleriyle gör, listeni veriyle kur.
|
||||
Sıralamanı biliyorsan gerisi mekanik: [tercih robotuna](/tercih-robotu) gir, sıralamana uygun programları hayal, dengeli ve güvenli (halk arasında "garanti tercih") dilimleriyle gör, listeni veriyle kur.
|
||||
|
||||
@@ -41,10 +41,10 @@ flowchart TD
|
||||
|
||||
Tek yılın tabanı **fotoğraf**, 5 yılın (2021–2025) tabanı **film**dir. Film izle:
|
||||
|
||||
- **İstikrarlı taban** (5 yıl üst üste dar bantta): güvenilir referans. Sıralaman tabana yakınsa "dengeli", belirgin gerisindeyse "garanti" davranabilirsin.
|
||||
- **İstikrarlı taban** (5 yıl üst üste dar bantta): güvenilir referans. Sıralaman tabana yakınsa "dengeli", belirgin gerisindeyse "güvenli" (halk arasında "garanti tercih") davranabilirsin.
|
||||
- **Tek yönlü trend** (her yıl sıkılaşıyor): geçen yılın tabanına güvenme; bu yıl daha da sıkılaşacağını varsayıp payla yaklaş. Popüler bölümlerde sık görülür — [Psikoloji](/bolum/psikoloji) ve [Bilgisayar Mühendisliği](/bolum/bilgisayar-muhendisligi) tablolarında 2024-2025 kolonlarını yan yana koyup bak.
|
||||
- **Testere dişi** (bir sıkı bir gevşek): sürü etkisi işliyor demektir; ortalamayı referans al, uç yıla değil.
|
||||
- **Veri boşluğu** (bazı yıllar "—"): yeni ya da o yıl dolmamış program; belirsizlik yüksek, listende dengeli değil garanti bölgesine koyma, hayal bölgesine koy.
|
||||
- **Veri boşluğu** (bazı yıllar "—"): yeni ya da o yıl dolmamış program; belirsizlik yüksek, listende dengeli değil güvenli bölgeye koyma, hayal bölgesine koy.
|
||||
|
||||
Bölüm sayfalarımızdaki tablolarda 2025 sıralamasının yanında 2024 kolonu ve değişim yönü işareti (▲/▼) tam bu iş için var.
|
||||
|
||||
@@ -53,6 +53,6 @@ Bölüm sayfalarımızdaki tablolarda 2025 sıralamasının yanında 2024 kolonu
|
||||
1. Her satır için son 5 yılın (2021–2025) tabanına bak; tek yıl asla yeterli değil.
|
||||
2. Sıkılaşma trendindeki programları bir kademe yukarı say ("dengeli" sandığın aslında "hayal" olabilir).
|
||||
3. Gevşeme trendindeki programlara erken sevinme; sürü geri dönebilir.
|
||||
4. Belirsizliği [dengeli bir listeyle](/rehber/tercih-listesi-nasil-yapilir) yönet: trend ne yaparsa yapsın, hayal/dengeli/garanti dağılımı düzgün kurulmuş bir liste boşta bırakmaz.
|
||||
4. Belirsizliği [dengeli bir listeyle](/rehber/tercih-listesi-nasil-yapilir) yönet: trend ne yaparsa yapsın, hayal/dengeli/güvenli dağılımı düzgün kurulmuş bir liste boşta bırakmaz.
|
||||
|
||||
[Tercih robotu](/tercih-robotu) işin mekanik kısmını üstlenir: sıralamanı girdiğinde her programı **son yılın tabanına** göre (2025; o yıl veri yoksa 2024) hayal, dengeli ya da güvenli (halk arasında "garanti") dilimine yerleştirir ve satırda geçmiş yılların tabanlarını da gösterir. Dilim tek yılın fotoğrafıdır; filmi yukarıdaki kalıplarla sen okursun. Aynı okumayı seneye tek bir hedef bölüm için yapmak istersen [hedef sıralama nasıl belirlenir?](/rehber/hedef-siralama-nasil-belirlenir) yazısına bak.
|
||||
[Tercih robotu](/tercih-robotu) işin mekanik kısmını üstlenir: sıralamanı girdiğinde her programı **son yılın tabanına** göre (2025; o yıl veri yoksa 2024) hayal, dengeli ya da güvenli dilimine yerleştirir ve satırda geçmiş yılların tabanlarını da gösterir. Dilim tek yılın fotoğrafıdır; filmi yukarıdaki kalıplarla sen okursun. Aynı okumayı seneye tek bir hedef bölüm için yapmak istersen [hedef sıralama nasıl belirlenir?](/rehber/hedef-siralama-nasil-belirlenir) yazısına bak.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
baslik: YKS Tercih Listesi Nasıl Yapılır? (2026 Rehberi)
|
||||
aciklama: 24 tercihlik listeyi dilim dilim kurmanın adım adım yöntemi — hayal, dengeli ve garanti bölgeleri tek şemada.
|
||||
aciklama: 24 tercihlik listeyi dilim dilim kurmanın adım adım yöntemi — hayal, dengeli ve güvenli ("garanti") bölgeleri tek şemada.
|
||||
tarih: 2026-07-25
|
||||
---
|
||||
|
||||
Tercih listesi kurmanın tek doğru yolu yok; ama boşta bırakan yanlış yolları belli. Bu rehber, ÖSYM'nin tanıdığı 24 tercih hakkını **hayal / dengeli / garanti** mantığıyla nasıl dağıtacağını anlatıyor.
|
||||
Tercih listesi kurmanın tek doğru yolu yok; ama boşta bırakan yanlış yolları belli. Bu rehber, ÖSYM'nin tanıdığı 24 tercih hakkını **hayal / dengeli / güvenli** mantığıyla nasıl dağıtacağını anlatıyor. Güvenli dilim, halk arasında "garanti tercih" diye anılan bölgedir.
|
||||
|
||||
## Önce ölçüyü düzelt: puan değil, sıralama
|
||||
|
||||
@@ -13,12 +13,12 @@ Tercihin tek sağlam ölçüsü **başarı sıralamandır**. Puanlar her yıl s
|
||||
## Listenin üç bölgesi
|
||||
|
||||
```mermaid
|
||||
%% aria: Tercih listesi dilim şeması: 24 satırlık liste yukarıdan aşağı üç bölgeye ayrılır. İlk 4-8 satır hayal tercihleri — tabanı senden daha iyi kapatan ama çok istediğin programlar; tutmazsa alttakilere zarar vermez. Orta 8-12 satır dengeli tercihler — sıralamana yakın programlar, yerleşme ihtimalinin en yüksek olduğu bölge. Son 4-6 satır garanti tercihler — tabanı belirgin geniş programlar; buraya yalnızca gerçekten kayıt yaptıracağın programları yazarsın.
|
||||
%% altyazi: Üstte hayal (kırmızı), ortada dengeli (sarı), altta garanti (yeşil) — her satır istek sırana göre dizilir, yerleşme ihtimaline göre değil.
|
||||
%% aria: Tercih listesi dilim şeması: 24 satırlık liste yukarıdan aşağı üç bölgeye ayrılır. İlk 4-8 satır hayal tercihleri — tabanı senden daha iyi kapatan ama çok istediğin programlar; tutmazsa alttakilere zarar vermez. Orta 8-12 satır dengeli tercihler — sıralamana yakın programlar, yerleşme ihtimalinin en yüksek olduğu bölge. Son 4-6 satır güvenli tercihler — tabanı belirgin geniş programlar; buraya yalnızca gerçekten kayıt yaptıracağın programları yazarsın.
|
||||
%% altyazi: Üstte hayal (kırmızı), ortada dengeli (sarı), altta güvenli (yeşil) — her satır istek sırana göre dizilir, yerleşme ihtimaline göre değil.
|
||||
flowchart TD
|
||||
L("24 tercihlik listen<br/>yukarıdan aşağı okunur") --> H("Hayal · ilk 4-8 satır<br/>çok istediğin, tabanı daha sıkı programlar")
|
||||
H --> D("Dengeli · orta 8-12 satır<br/>sıralamana yakın programlar")
|
||||
D --> G("Garanti · son 4-6 satır<br/>gerçekten razı olacağın programlar")
|
||||
D --> G("Güvenli · son 4-6 satır<br/>gerçekten razı olacağın programlar")
|
||||
H -. "tutmazsa sıradakine geçer<br/>alttakilere zarar vermez" .-> D
|
||||
G --> K{"Kayıt yaptırır mısın?"}
|
||||
K -- "evet, severek giderim" --> OK("Listeye yaz")
|
||||
@@ -37,7 +37,7 @@ flowchart TD
|
||||
|
||||
**2) Dengeli tercihler (orta blok, 8-12 sıra).** Taban sıralaması senin sıralamana yakın programlar. Listenin kalbi burasıdır; yerleşme ihtimalinin en yüksek olduğu bölge. Aynı bölümün farklı şehirlerdeki/üniversitelerdeki versiyonlarını sıralama sırasıyla diz.
|
||||
|
||||
**3) Garanti tercihleri (son 4-6 sıra).** Taban sıralaması seninkinden belirgin şekilde geniş programlar. Buradaki altın kural: **yerleşsen kayıt yaptırmayacağın hiçbir programı yazma.** Garanti dilimi "razı olacağın en makul seçenek" demektir, "ne olursa olsun bir yere gireyim" demek değil. Kayıt yaptırmayacağın bir yere yerleşirsen hem bir yılın gider hem de ek yerleştirmeye başvuramazsın.
|
||||
**3) Güvenli tercihler (son 4-6 sıra).** Taban sıralaması seninkinden belirgin şekilde geniş programlar. Buradaki altın kural: **yerleşsen kayıt yaptırmayacağın hiçbir programı yazma.** Güvenli dilim "razı olacağın en makul seçenek" demektir, "ne olursa olsun bir yere gireyim" demek değil. Kayıt yaptırmayacağın bir yere yerleşirsen hem bir yılın gider hem de ek yerleştirmeye başvuramazsın.
|
||||
|
||||
## Sık yapılan hatalar
|
||||
|
||||
@@ -49,9 +49,9 @@ flowchart TD
|
||||
## Pratik akış
|
||||
|
||||
1. Sıralamanı ve puan türünü not et.
|
||||
2. Sıralamana uygun program evrenini çıkar — [tercih robotuna](/tercih-robotu) sıralamanı girerek hayal/dengeli/garanti dilimlerini saniyede görebilirsin.
|
||||
2. Sıralamana uygun program evrenini çıkar — [tercih robotuna](/tercih-robotu) sıralamanı girerek hayal/dengeli/güvenli dilimlerini saniyede görebilirsin.
|
||||
3. İstemediklerini ele, kalanları *istek sırana* göre diz (yerleşme ihtimaline göre değil).
|
||||
4. Son kontrol: listenin başı hayal mi, ortası dengeli mi, sonu gerçekten garanti mi? Sonunda kayıt yaptırmayacağın program var mı?
|
||||
4. Son kontrol: listenin başı hayal mi, ortası dengeli mi, sonu gerçekten güvenli mi? Sonunda kayıt yaptırmayacağın program var mı?
|
||||
5. ÖSYM AİS'e girerken listeyi bir kez daha kod kod doğrula.
|
||||
|
||||
Listeyi kurarken her satırın "neden burada" sorusuna cevabı olmalı. Cevabı yoksa o satır ya yanlış yerde ya da listede fazla.
|
||||
|
||||
@@ -11,7 +11,7 @@ Tercih döneminin en çok aranan araçlarından biri "tercih robotu". Ama iki fa
|
||||
Özünde her tercih robotu aynı işi yapar: **başarı sıralamanı** alır, geçmiş yılların taban sıralamalarıyla karşılaştırır ve 23.000'i aşkın programın içinden senin bandına düşenleri süzer. Elle günler süren tarama işini saniyelere indirir. İyi bir robot bunun üstüne üç şey daha katar:
|
||||
|
||||
- Programları tek yılın tabanına göre değil, **çok yıllı trende** göre dilimlemek ([tabanlar yıldan yıla nasıl oynuyor](/rehber/taban-siralamalar-nasil-degisir)),
|
||||
- Sonuçları **hayal / dengeli / garanti** mantığıyla ayırmak ([listenin üç bölgesi](/rehber/tercih-listesi-nasil-yapilir)),
|
||||
- Sonuçları **hayal / dengeli / güvenli** (halk arasında "garanti tercih") mantığıyla ayırmak ([listenin üç bölgesi](/rehber/tercih-listesi-nasil-yapilir)),
|
||||
- Kılavuzdaki [özel koşulları ve sıralama şartlarını](/rehber/siralama-sartlari-2026) süzgece dahil etmek.
|
||||
|
||||
Robotun yapamayacağı şey ise senin yerine **istemek**: hangi bölümü gerçekten okuyacağını, hangi şehirde dört yıl yaşayabileceğini, bütçenin neye yettiğini robot bilemez. Robot evreni daraltır; sırayı sen kurarsın.
|
||||
@@ -28,11 +28,11 @@ YÖK Atlas (yokatlas.yok.gov.tr), YÖK'ün resmî yerleştirme verilerini açtı
|
||||
## Robot çıktısını okurken dört tuzak
|
||||
|
||||
```mermaid
|
||||
%% aria: Tercih robotu kullanım akışı şeması. Doğru akış şöyledir: başarı sıralamanı girersin, filtrelerle aday havuzunu daraltırsın, robot programları hayal, dengeli ve garanti dilimlerine ayırır, sonra son kontrol sana geçer: kılavuz koşullarını, çok yıllı taban trendini ve kendi isteklerini kendin doğrularsın; bu kontrol sarı ile işaretlidir çünkü atlanması en sık hatadır. Akışın dışında dört tuzak kutusu kırmızı ile işaretlidir: puanla arama yapmak, tek yılın tabanına güvenmek, listeyi robotun verdiği sırayla göndermek ve kılavuz koşullarını hiç okumamak. Bu dört hatadan herhangi biri listeyi çürütür. Son kontrolden geçen liste yeşil ile işaretlenmiş sağlam liste kutusuna ulaşır.
|
||||
%% aria: Tercih robotu kullanım akışı şeması. Doğru akış şöyledir: başarı sıralamanı girersin, filtrelerle aday havuzunu daraltırsın, robot programları hayal, dengeli ve güvenli dilimlerine ayırır, sonra son kontrol sana geçer: kılavuz koşullarını, çok yıllı taban trendini ve kendi isteklerini kendin doğrularsın; bu kontrol sarı ile işaretlidir çünkü atlanması en sık hatadır. Akışın dışında dört tuzak kutusu kırmızı ile işaretlidir: puanla arama yapmak, tek yılın tabanına güvenmek, listeyi robotun verdiği sırayla göndermek ve kılavuz koşullarını hiç okumamak. Bu dört hatadan herhangi biri listeyi çürütür. Son kontrolden geçen liste yeşil ile işaretlenmiş sağlam liste kutusuna ulaşır.
|
||||
%% altyazi: Robot evreni daraltır, sırayı sen kurarsın — son kontrol devredilemez.
|
||||
flowchart TD
|
||||
A("Başarı sıralamanı gir<br/>puanı değil") --> B("Filtrele:<br/>şehir, devlet/vakıf,<br/>öğretim dili")
|
||||
B --> C("Robot dilimler:<br/>hayal / dengeli / garanti")
|
||||
B --> C("Robot dilimler:<br/>hayal / dengeli / güvenli")
|
||||
C --> D{"Son kontrol sende:<br/>kılavuz koşulları,<br/>çok yıllı trend,<br/>gerçek isteklerin"}
|
||||
D -- "kontrolü yaptın" --> E("Sağlam liste:<br/>her satırın gerekçesi var")
|
||||
D -- "atladın" --> F("Tuzaklar devrede")
|
||||
@@ -59,4 +59,4 @@ flowchart TD
|
||||
|
||||
## YÖK Atlas mı, tercih robotu mu?
|
||||
|
||||
İkisi rakip değil, ardışık: robot tarama işini saniyeye indirir, YÖK Atlas tek programın derinine iner. Verimli akış şöyle: [tercih robotuna](/tercih-robotu) sıralamanı gir, hayal/dengeli/garanti dilimlerine ayrılmış aday havuzunu çıkar; sonra listende ciddi yer verdiğin programların YÖK Atlas sayfalarını açıp kadro, doluluk geçmişi ve koşulları tek tek doğrula. Robotla dakikada evreni gör, Atlas'la satır satır emin ol — liste ikisinin kesişiminden çıkar.
|
||||
İkisi rakip değil, ardışık: robot tarama işini saniyeye indirir, YÖK Atlas tek programın derinine iner. Verimli akış şöyle: [tercih robotuna](/tercih-robotu) sıralamanı gir, hayal/dengeli/güvenli dilimlerine ayrılmış aday havuzunu çıkar; sonra listende ciddi yer verdiğin programların YÖK Atlas sayfalarını açıp kadro, doluluk geçmişi ve koşulları tek tek doğrula. Robotla dakikada evreni gör, Atlas'la satır satır emin ol — liste ikisinin kesişiminden çıkar.
|
||||
|
||||
@@ -60,4 +60,4 @@ Ek yerleştirmede en büyük hata kılavuzu bekleyip son gün panikle liste kurm
|
||||
|
||||
## Beklerken yapılacak en akıllıca iş
|
||||
|
||||
Sonuç ekranı açılana kadar elindeki en değerli veri hâlâ **başarı sıralaman**. Hangi senaryoda olursan ol — ek yerleştirme ihtimaline hazırlık ya da sadece "nereye yerleşmiş olabilirim" merakı — [tercih robotuna](/tercih-robotu) sıralamanı girerek sıralamana düşen programları hayal/dengeli/garanti dilimleriyle görebilirsin. Sonuç günü geldiğinde sürpriz değil, doğrulama yaşarsın.
|
||||
Sonuç ekranı açılana kadar elindeki en değerli veri hâlâ **başarı sıralaman**. Hangi senaryoda olursan ol — ek yerleştirme ihtimaline hazırlık ya da sadece "nereye yerleşmiş olabilirim" merakı — [tercih robotuna](/tercih-robotu) sıralamanı girerek sıralamana düşen programları hayal, dengeli ve güvenli (halk arasında "garanti tercih") dilimleriyle görebilirsin. Sonuç günü geldiğinde sürpriz değil, doğrulama yaşarsın.
|
||||
|
||||
@@ -49,7 +49,7 @@ Velinin listeyi "düzeltmesi" genelde şuraya varır: üst sıralara ailenin pre
|
||||
Öğrenci bir bölümü çok istiyor, siz riskli buluyorsunuz. Tartışmak yerine:
|
||||
|
||||
- **Adım 1 — İddiaları yazıya dökün.** "Bu bölüm iş bulamaz" ya da "bu bölüm harika" cümlelerinin ikisi de kanıtsız. İkiniz de iddianızı somut bir kaynağa dayandırın: ders katalogu, akreditasyon, mezunların gittiği yerler. [Bir bölümün iş imkânını nasıl araştırırsın](/rehber/bolumun-is-imkanini-nasil-arastirirsin) yazısı bunun yöntemini veriyor.
|
||||
- **Adım 2 — Listede ikisine de yer açın.** 24 satırlık liste tam da bunun için var. Öğrencinin çok istediği bölüm üst bloğa, sizin daha güvenli bulduğunuz alternatifler alt bloğa girebilir. İkisi çatışmaz, sıralanır. [Tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–garanti dağılımı bu pazarlığın zeminidir.
|
||||
- **Adım 2 — Listede ikisine de yer açın.** 24 satırlık liste tam da bunun için var. Öğrencinin çok istediği bölüm üst bloğa, sizin daha güvenli bulduğunuz alternatifler alt bloğa girebilir. İkisi çatışmaz, sıralanır. [Tercih listesi nasıl yapılır](/rehber/tercih-listesi-nasil-yapilir) yazısındaki hayal–dengeli–güvenli (halk arasında "garanti tercih") dağılımı bu pazarlığın zeminidir.
|
||||
- **Adım 3 — Kararı öğrenciye bırakın, gerekçenizi bir kez söyleyin.** Bir kez. Tekrar etmek ikna etmez, sadece kararı duygusallaştırır.
|
||||
|
||||
## Son iki hatırlatma
|
||||
|
||||
@@ -77,4 +77,4 @@ Tek cümleyle: **hesaba katılmaz.** Listeyi, "buraya yerleşirsem burada okurum
|
||||
|
||||
Somut kural: kayıt yaptırmayacağın bir programı "nasıl olsa geçerim" diye listeye yazma — bu tam olarak [ölü tercih](/rehber/olu-tercih-nedir) mantığındaki hatadır ve yerleşirsen ek yerleştirme hakkın da yanar.
|
||||
|
||||
Sıralamanın gerçekten nereye yettiğini görüp listeyi baştan doğru kurmak için [tercih robotuna](/tercih-robotu) sıralamanı gir; hayal, dengeli ve garanti bloklarını tek ekranda görürsün.
|
||||
Sıralamanın gerçekten nereye yettiğini görüp listeyi baştan doğru kurmak için [tercih robotuna](/tercih-robotu) sıralamanı gir; hayal, dengeli ve güvenli (halk arasında "garanti tercih") bloklarını tek ekranda görürsün.
|
||||
|
||||
@@ -53,4 +53,4 @@ Gelecek yıl tekrar sınava girmek meşru bir plan. Kayıt olup denemek, kaydı
|
||||
|
||||
## Bir dahaki listeye ders
|
||||
|
||||
Bu durumların neredeyse tamamı, liste kurulurken önlenebilir: sonuna gerçekten razı olacağın garantiler koymak, kayıt yaptırmayacağın satırı hiç yazmamak ve [ölü tercihleri](/rehber/olu-tercih-nedir) temizlemek. Ek yerleştirme listesi kuracaksan da aynı disiplin geçerli — sıralamana göre gerçekçi seçenekleri [tercih robotunda](/tercih-robotu) saniyeler içinde görebilirsin.
|
||||
Bu durumların neredeyse tamamı, liste kurulurken önlenebilir: sonuna gerçekten razı olacağın güvenli tercihleri (halk arasında "garanti tercih") koymak, kayıt yaptırmayacağın satırı hiç yazmamak ve [ölü tercihleri](/rehber/olu-tercih-nedir) temizlemek. Ek yerleştirme listesi kuracaksan da aynı disiplin geçerli — sıralamana göre gerçekçi seçenekleri [tercih robotunda](/tercih-robotu) saniyeler içinde görebilirsin.
|
||||
|
||||
@@ -46,7 +46,7 @@ Boş kalan kontenjanlar için ikinci tur. Kritik kural: ilk yerleştirmede bir p
|
||||
|
||||
- **1. gün:** Sıralamanı doğrula, puan türünü netleştir.
|
||||
- **2-3. gün:** Program evrenini çıkar. [Bölüm taban puanları](/bolumler) tablolarında ilgilendiğin bölümlerin son yıllardaki tabanlarını incele; [Psikoloji](/bolum/psikoloji) gibi geniş bölümlerde aralığın ne kadar açıldığını görmek ufuk açar.
|
||||
- **4-5. gün:** Taslak listeyi kur: üstte hayal, ortada dengeli, altta gerçekten gideceğin garanti.
|
||||
- **4-5. gün:** Taslak listeyi kur: üstte hayal, ortada dengeli, altta gerçekten gideceğin güvenli tercihler (halk arasında "garanti tercih").
|
||||
- **6. gün:** Ölü tercih taraması yap ([nasıl yapılır](/rehber/olu-tercih-nedir)), aile onayını al, üniversitelerin özel şartlarını (kılavuz dipnotlarını) oku.
|
||||
- **Son gün değil, son günden önce:** AİS'e gir, listeyi kod kod işle, onayla, çıktısını sakla. Sistem son saatlerde yoğunluktan yavaşlar — son güne bırakma.
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# iyzico ödeme entegrasyonu
|
||||
|
||||
Ödeme akışı iyzico **Checkout Form (CF)** üzerinden yürür: kart bilgisi hiçbir
|
||||
zaman bize ulaşmaz, kullanıcı iyzico'nun barındırdığı ödeme sayfasına gider.
|
||||
zaman bize ulaşmaz; kart formu iyzico iframe'i olarak kendi `/odeme` sayfamıza
|
||||
gömülür.
|
||||
|
||||
## Ortam değişkenleri
|
||||
|
||||
@@ -29,8 +30,13 @@ Ayrıntı: `src/lib/app-url.ts`.
|
||||
|
||||
1. `baslatOdeme` (`src/features/odeme/odeme-actions.ts`) — `orders` satırını
|
||||
`pending` olarak yazar, `checkoutFormInitialize` çağırır, dönen `token`'ı
|
||||
siparişe iliştirir ve kullanıcıyı `paymentPageUrl`'e yönlendirir.
|
||||
siparişe iliştirir ve `/odeme?siparis=<id>` adresini **döndürür** (`redirect()`
|
||||
fırlatmaz); yönlendirmeyi `SatinAlForm` client'ta `router.push` ile yapar.
|
||||
`conversationId` = `basketId` = sipariş id'miz.
|
||||
Oturum yoksa `/giris?callback=/paket` döner. Sunucu koruması: paketi olan
|
||||
yeniden `paket`, paketi olmayan `topup` başlatamaz; `/odeme` sayfası da
|
||||
paket alınmışken eski bekleyen paket siparişinin formunu göstermez
|
||||
(bayat sekme / ikinci cihazdan çift çekim önlemi).
|
||||
2. Kullanıcı ödemeyi bitirince iyzico `/api/odeme/callback` adresine
|
||||
**cross-site POST** atar; gövdede yalnızca `token` vardır. SameSite=Lax
|
||||
nedeniyle session çerezi gelmez, bu yüzden kullanıcı token'dan çözülür.
|
||||
|
||||
@@ -82,8 +82,9 @@ rozet, buton varyantı tasarlanmaz.
|
||||
- **Tamamlanınca:** `tercihProfiliYaz` → çekmece kapanır → `sonucHref` ile
|
||||
`/sonuc` (girişliye `sihirbaz: "1"`, girişsize `hazir: "1"`). Giriş duvarı
|
||||
sihirbazın hemen arkasında değildir; değer önce gösterilir.
|
||||
- **Lenis:** çekmece içeriği ve kaydırma kabı `data-lenis-prevent` taşır;
|
||||
sayfadaki smooth-scroll çekmece içi kaydırmayı ezmez.
|
||||
- **Kaydırma:** sayfa yerel tarayıcı kaydırmasını kullanır (Lenis/skew
|
||||
performans nedeniyle 21.09.2026'da kaldırıldı); çekmece içi kaydırma için
|
||||
ek bir önlem gerekmez.
|
||||
|
||||
## 6. Erişilebilirlik
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"iyzipay": "^2.0.69",
|
||||
"lenis": "^1.3.25",
|
||||
"lucide-react": "^1.25.0",
|
||||
"marked": "^18.0.7",
|
||||
"mermaid": "^11.16.0",
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -35,9 +35,6 @@ importers:
|
||||
iyzipay:
|
||||
specifier: ^2.0.69
|
||||
version: 2.0.69
|
||||
lenis:
|
||||
specifier: ^1.3.25
|
||||
version: 1.3.25(react@19.2.4)
|
||||
lucide-react:
|
||||
specifier: ^1.25.0
|
||||
version: 1.25.0(react@19.2.4)
|
||||
@@ -4195,20 +4192,6 @@ packages:
|
||||
layout-base@2.0.1:
|
||||
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
|
||||
|
||||
lenis@1.3.25:
|
||||
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
|
||||
peerDependencies:
|
||||
'@nuxt/kit': '>=3.0.0'
|
||||
react: '>=17.0.0'
|
||||
vue: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@nuxt/kit':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
|
||||
levn@0.4.1:
|
||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -9688,10 +9671,6 @@ snapshots:
|
||||
|
||||
layout-base@2.0.1: {}
|
||||
|
||||
lenis@1.3.25(react@19.2.4):
|
||||
optionalDependencies:
|
||||
react: 19.2.4
|
||||
|
||||
levn@0.4.1:
|
||||
dependencies:
|
||||
prelude-ls: 1.2.1
|
||||
|
||||
@@ -67,9 +67,12 @@ KURALLAR (kesin):
|
||||
- METİN DİYETİ: her alan TEK kısa cümle, dolgu söz yok.
|
||||
- gerekce: bu programın bu aday için neden mantıklı olduğu (TEK cümle,
|
||||
en fazla ~15 kelime, Türkçe).
|
||||
- riskNotu: yerleşme ihtimali değerlendirmesi (TEK kısa cümle). ASLA "garanti",
|
||||
"kesin", "yüzde yüz" deme; "yüksek/orta/düşük ihtimal" dili kullan.
|
||||
- trendOzeti: son 4 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
|
||||
- riskNotu: geçmiş taban verisine dayalı risk değerlendirmesi (TEK kısa cümle).
|
||||
ASLA garanti/kesin/yüzde/olasılık/ihtimal deme; "güvenli / az riskli /
|
||||
riskli" etiketleriyle konuş.
|
||||
- Programın "dilim" alanındaki "garanti" değeri teknik anahtardır; adaya dönük
|
||||
hiçbir metinde bu kelimeyi kullanma, o dilimi "güvenli" diye an.
|
||||
- trendOzeti: son 5 yılın taban sıralaması hareketi, TEK cümle (ör. "2022'den
|
||||
beri taban 45B'den 38B'ye indi, bölüm zorlaşıyor").
|
||||
- Adayın sıralaması yaklaşıktır; kesin sıra telaffuz etme, banda göre konuş.`;
|
||||
|
||||
@@ -82,9 +85,9 @@ function mockMetin(p: Program, temsiliSira: number, kategori: string | null) {
|
||||
return {
|
||||
gerekce: `(Dev mock) ${p.isim}, ${kategori ? `${kategori} ilgisiyle ve ` : ""}~${temsiliSira.toLocaleString("tr-TR")} bandındaki sıralamanla uyumlu bir aday.`,
|
||||
riskNotu:
|
||||
"(Dev mock) Taban sıralaması senin civarında; orta-yüksek ihtimalle yerleşilebilir.",
|
||||
"(Dev mock) Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.",
|
||||
trendOzeti:
|
||||
"(Dev mock) Son 4 yılda taban sıralaması görece stabil seyretti.",
|
||||
"(Dev mock) Son 5 yılda taban sıralaması görece stabil seyretti.",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,11 +41,15 @@ iş imkânları, tercih sırası) net ve dürüst yanıtlamak.
|
||||
KURALLAR:
|
||||
- Cevaplarını adayın 24'lük listesi ve seçimleri bağlamında ver; listedeki
|
||||
programlara referans verebilirsin.
|
||||
- Dilimlerin adı Hayal, Dengeli ve Güvenli'dir. Üçüncü dilime her zaman
|
||||
"Güvenli" de; "garanti dilimi", "garanti tercih" gibi ifadeler KULLANMA
|
||||
(aday ya da eski mesajlar böyle dese bile sen "güvenli" diye düzelt).
|
||||
- Listedeki her satırda dilim ve risk etiketi var: Güvenli (yeşil) = taban
|
||||
sıralaması adayın belirgin altında, Az riskli (sarı) = adayın sıralaması
|
||||
civarında, Riskli (kırmızı) = geçen yıl adayın sıralamasının üzerinde
|
||||
kapandı. Risk sorularında bu etiketleri ve taban sıralarını kullan.
|
||||
- ASLA yerleşme garantisi verme; "yüksek/orta/düşük ihtimal" dili kullan.
|
||||
- ASLA yerleşme garantisi verme; olasılık, yüzde ya da "ihtimal" dili kullanma —
|
||||
güvenli / az riskli / riskli etiketleri ve taban sıralarıyla konuş.
|
||||
- Bilmediğin güncel veriyi uydurma; emin değilsen YÖK Atlas'a bakmasını söyle.
|
||||
- Taban sıralamalarının her yıl değiştiğini gerektiğinde hatırlat.
|
||||
- Cevapları kısa ve öz tut (genelde 3-6 cümle); istenirse detaylandır.
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function GirisPage({
|
||||
searchParams: Promise<{ callback?: string; hata?: string }>;
|
||||
}) {
|
||||
return (
|
||||
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler —
|
||||
// -mb-28: layout'taki içerik sarmalayıcısının pb-28'ini nötrler —
|
||||
// kart tam dvh içinde ortalanır, sayfa scroll etmez (sığmazsa scroll açılır).
|
||||
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
||||
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">
|
||||
|
||||
@@ -100,7 +100,10 @@ export default function GizlilikPage() {
|
||||
ölçen, çerez kullanmayan bir analitik aracı (Rybbit) çalıştırıyoruz;
|
||||
bu araç kendi sunucumuzda barındırılır, veriler üçüncü taraf bir
|
||||
reklam ağına gitmez. Giriş yaptığında ziyaretin hesabınla
|
||||
eşleştirilir. Başarı sıralaman analitiğe ham hâliyle
|
||||
eşleştirilir; bunun için e-posta adresin ve hesabındaki adın
|
||||
(varsa) kendi sunucumuzdaki bu analitik aracına kaydedilir ve
|
||||
yalnızca sorun giderme ile destek amacıyla kullanılır. Başarı
|
||||
sıralaman analitiğe ham hâliyle
|
||||
gönderilmez; yalnızca “50-100 bin” gibi geniş bir
|
||||
aralık olarak kaydedilir.
|
||||
</p>
|
||||
|
||||
@@ -252,7 +252,5 @@
|
||||
}
|
||||
html {
|
||||
@apply font-sans;
|
||||
/* scroll-behavior: smooth KULLANMA — Lenis (ScrollFlow, anchors: true)
|
||||
kaydırmayı zaten yumuşatıyor; ikisi birlikte çift yumuşatma yapar. */
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { TercihProfiliKapisiLazy } from "@/features/liste/components/tercih-prof
|
||||
import { HeaderGate } from "@/components/header-gate";
|
||||
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
|
||||
import { DevPanelMount } from "@/components/dev/dev-panel-mount";
|
||||
import { ScrollFlow } from "@/components/scroll-flow";
|
||||
import { JsonLd, organizationJsonLd, webSiteJsonLd, SITE_URL } from "@/lib/seo";
|
||||
import "./globals.css";
|
||||
|
||||
@@ -52,7 +51,7 @@ export const metadata: Metadata = {
|
||||
template: "%s — KolayTercih",
|
||||
},
|
||||
description:
|
||||
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmanın onda bir fiyatına, yapay zekâ destekli tercih danışmanlığı.",
|
||||
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste. İnsan danışmandan çok daha düşük fiyata, yapay zekâ destekli tercih danışmanlığı.",
|
||||
applicationName: "KolayTercih",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
@@ -79,10 +78,8 @@ export default function RootLayout({
|
||||
return (
|
||||
<html
|
||||
lang="tr"
|
||||
// `h-full` VERME — Lenis içerik yüksekliğini <html>'e bağlı ResizeObserver
|
||||
// ile izliyor; html sabit %100 kalınca lazy yüklenen içerik sonrası limit
|
||||
// bayatlıyor ve sayfa sonunda scroll kilitleniyor. html auto yükseklikte
|
||||
// içerikle büyümeli, viewport tabanı body'deki min-h-dvh'den gelir.
|
||||
// `h-full` verme: html auto yükseklikte içerikle büyür, viewport tabanı
|
||||
// body'deki min-h-dvh'den gelir.
|
||||
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`}
|
||||
>
|
||||
<body className="min-h-dvh flex flex-col bg-slate-50">
|
||||
@@ -107,9 +104,9 @@ export default function RootLayout({
|
||||
<SiteTopBanner />
|
||||
<SiteHeader />
|
||||
</HeaderGate>
|
||||
<ScrollFlow className="flex min-h-0 flex-1 flex-col pb-28 print:pb-0">
|
||||
<div className="flex min-h-0 flex-1 flex-col pb-28 print:pb-0">
|
||||
{children}
|
||||
</ScrollFlow>
|
||||
</div>
|
||||
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
|
||||
<ListeCekmecesiLazy />
|
||||
{/* Girişsiz + 3 seçimli kullanıcıya "listen yalnızca bu tarayıcıda" banner'ı */}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function OdemePage({
|
||||
searchParams: Promise<{ siparis?: string }>;
|
||||
}) {
|
||||
return (
|
||||
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler — o
|
||||
// -mb-28: layout'taki içerik sarmalayıcısının pb-28'ini nötrler — o
|
||||
// padding gizlenen alt blur katmanı içindi, checkout'ta krom kapalı.
|
||||
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
||||
<OdemeUstSerit />
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
TrendingUp,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Suspense } from "react";
|
||||
import { ComparisonCell } from "@/features/pazarlama/components/landing-parcalari";
|
||||
@@ -30,6 +29,7 @@ import {
|
||||
SectionEyebrow,
|
||||
} from "@/components/pixel-decor";
|
||||
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
@@ -54,9 +54,11 @@ import {
|
||||
import type { Metadata } from "next";
|
||||
import {
|
||||
JsonLd,
|
||||
KATALOG_VERI_TARIHI,
|
||||
faqPageJsonLd,
|
||||
softwareApplicationJsonLd,
|
||||
} from "@/lib/seo";
|
||||
import { KATALOG_TARIH_METNI } from "@/features/katalog/components/veri-kaynagi";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -87,7 +89,7 @@ const problems = [
|
||||
icon: Wallet,
|
||||
title: "Danışman lüks haline geldi",
|
||||
description:
|
||||
"İyi bir tercih danışmanı 2.000–10.000 TL istiyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.",
|
||||
"İyi bir tercih danışmanı 1.000–3.000 TL istiyor. Ücretsiz robotlar ise sadece liste filtreliyor, akıl vermiyor.",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -103,23 +105,21 @@ const steps = [
|
||||
icon: BarChart3,
|
||||
step: "2",
|
||||
title: "Yapay zekâ veriyi tarasın",
|
||||
description:
|
||||
"Son 4 yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.",
|
||||
description: `Son ${VERI_YIL_SAYISI} yılın YÖK Atlas verisi — taban sıralaması trendleri, kontenjan değişimleri, doluluk oranları — senin için analiz edilsin.`,
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
step: "3",
|
||||
title: "Dengeli 24'lük listeni al",
|
||||
description:
|
||||
"Hayal, dengeli ve garanti dilimlerine dağıtılmış, her satırı gerekçeli tercih listeni indir; aklına takılanı listen üzerinden yapay zekâya sor.",
|
||||
"Hayal, dengeli ve güvenli dilimlerine dağıtılmış, her satırı gerekçeli tercih listeni indir; aklına takılanı listen üzerinden yapay zekâya sor.",
|
||||
},
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
question: "KolayTercih'in önerileri neye dayanıyor?",
|
||||
answer:
|
||||
"Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son 4 yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.",
|
||||
answer: `Tüm öneriler YÖK Atlas'ın resmî verisine dayanır: son ${VERI_YIL_SAYISI} yılın taban başarı sıralamaları, kontenjanlar, yerleşme istatistikleri ve doluluk oranları. Yapay zekâ bu veriyi yorumlar; veri olmadan tahmin yürütmez.`,
|
||||
},
|
||||
{
|
||||
question: "Ücretsiz tercih robotlarından farkı ne?",
|
||||
@@ -129,7 +129,7 @@ const faqs = [
|
||||
{
|
||||
question: "Yerleşme garantisi veriyor musunuz?",
|
||||
answer:
|
||||
"Hayır — ve verdiğini iddia eden herkesten uzak dur. Taban sıralamaları her yıl adayların davranışına göre değişir; kimse garanti veremez. Biz riski şeffaf gösterir, listeyi hayal/dengeli/garanti dilimleriyle kurarak boşta kalma ihtimalini en aza indiririz. Tercih listenin son hali ve ÖSYM başvurusu her zaman senin sorumluluğundadır.",
|
||||
"Hayır — ve verdiğini iddia eden herkesten uzak dur. Taban sıralamaları her yıl adayların davranışına göre değişir; kimse garanti veremez. Biz riski şeffaf gösterir, listeyi hayal/dengeli/güvenli dilimleriyle kurarak boşta kalma ihtimalini en aza indiririz. Tercih listenin son hali ve ÖSYM başvurusu her zaman senin sorumluluğundadır.",
|
||||
},
|
||||
{
|
||||
question: "Fiyatlandırma nasıl çalışıyor?",
|
||||
@@ -145,7 +145,7 @@ const comparison = [
|
||||
tercihai: true,
|
||||
},
|
||||
{
|
||||
feature: "Son 4 yılın trend analizi",
|
||||
feature: `Son ${VERI_YIL_SAYISI} yılın trend analizi`,
|
||||
robot: false,
|
||||
danisman: true,
|
||||
tercihai: true,
|
||||
@@ -165,7 +165,7 @@ const comparison = [
|
||||
{
|
||||
feature: "Fiyat",
|
||||
robot: "Ücretsiz",
|
||||
danisman: "2.000–10.000 TL",
|
||||
danisman: "1.000–3.000 TL",
|
||||
tercihai: `${PAKET_FIYATI} TL · tek seferlik`,
|
||||
},
|
||||
];
|
||||
@@ -177,7 +177,7 @@ const paketOzellikleri = [
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 yıl)",
|
||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||
},
|
||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||
{ icon: RefreshCcw, text: `${MAX_REVIZYON} liste revizyon hakkı` },
|
||||
@@ -222,11 +222,6 @@ export default function Home() {
|
||||
className="mx-auto flex min-h-[calc(100svh-5.5rem)] max-w-6xl flex-col items-center justify-center px-4 pb-4 pt-12 text-center sm:pb-36 sm:pt-4"
|
||||
>
|
||||
<HeroBaslik />
|
||||
{/* <p className="mt-6 max-w-2xl text-lg leading-relaxed text-slate-600">
|
||||
KolayTercih, gerçek YÖK Atlas verisiyle çalışan yapay zekâ tercih
|
||||
danışmanın. İnsan danışmanın binlerce lira aldığı işi — dengeli
|
||||
liste, risk analizi, soru-cevap — onda bir fiyatına yapar.
|
||||
</p> */}
|
||||
<div
|
||||
id="hero-form"
|
||||
className="mt-6 flex w-full scroll-mt-28 justify-center"
|
||||
@@ -234,19 +229,14 @@ export default function Home() {
|
||||
<HeroForm />
|
||||
</div>
|
||||
<div className="mt-3 inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-white px-3 py-2 text-left text-xs text-slate-700 shadow-xs">
|
||||
<Image
|
||||
src="/yok-logo.svg"
|
||||
alt="Yükseköğretim Kurulu"
|
||||
width={68}
|
||||
height={33}
|
||||
className="h-7 w-auto shrink-0"
|
||||
/>
|
||||
<span className="h-5 w-px shrink-0 bg-slate-200" aria-hidden />
|
||||
<span>
|
||||
<strong className="font-semibold text-slate-900">
|
||||
YÖK Atlas'ın gerçek verileriyle
|
||||
Veri: YÖK Atlas
|
||||
</strong>{" "}
|
||||
güvenilir sonuçlar
|
||||
· son güncelleme{" "}
|
||||
<time dateTime={KATALOG_VERI_TARIHI}>
|
||||
{KATALOG_TARIH_METNI}
|
||||
</time>
|
||||
</span>
|
||||
</div>
|
||||
</Parallax>
|
||||
@@ -362,7 +352,7 @@ export default function Home() {
|
||||
{[
|
||||
{ value: "23.000+", label: "Taranan lisans programı" },
|
||||
{
|
||||
value: "4 yıl",
|
||||
value: `${VERI_YIL_SAYISI} yıl`,
|
||||
label: "Taban sıralaması trend verisi",
|
||||
},
|
||||
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
|
||||
@@ -481,10 +471,13 @@ export default function Home() {
|
||||
Ne kadar? Ne dahil?
|
||||
</h2>
|
||||
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
|
||||
İnsan danışmanın 2.000–10.000 TL aldığı işi tek seferlik{" "}
|
||||
İnsan danışmanın 1.000–3.000 TL aldığı işi tek seferlik{" "}
|
||||
{PAKET_FIYATI} TL'ye alırsın. Abonelik yok, gizli ücret
|
||||
yok.
|
||||
</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Danışman fiyat aralığı: Armut ortalama fiyatı, Eylül 2026.
|
||||
</p>
|
||||
</Reveal>
|
||||
</Parallax>
|
||||
|
||||
|
||||
@@ -46,12 +46,15 @@ export default function PaketPage({
|
||||
<SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow>
|
||||
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
|
||||
İnsan danışmanın işini,{" "}
|
||||
<span className="text-primary">onda bir fiyatına</span>
|
||||
<span className="text-primary">çok daha düşük fiyata</span>
|
||||
</h1>
|
||||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
||||
Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih
|
||||
Tercih danışmanları bu dönemde 1.000–3.000 TL alıyor. KolayTercih
|
||||
aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
Danışman fiyat aralığı: Armut ortalama fiyatı, Eylül 2026.
|
||||
</p>
|
||||
</Parallax>
|
||||
|
||||
<Suspense fallback={<PaketSatinAlSkeleton />}>
|
||||
|
||||
@@ -35,7 +35,7 @@ const FUNNEL_METINLERI = [
|
||||
"Sıralaman ne söylüyor? Birlikte bakalım.",
|
||||
"Her satırı senin için hesaplıyoruz.",
|
||||
"Akıllı tercih, içi rahat bir bekleyiş.",
|
||||
"Yerleşme ihtimalini görerek seç.",
|
||||
"Yerleşme riskini görerek seç.",
|
||||
"Sana özel liste birkaç dakikada hazır.",
|
||||
"İlgi alanların ve şehrin, tek listede.",
|
||||
"Boş kalan tercih, kaçan fırsattır.",
|
||||
|
||||
@@ -178,7 +178,6 @@ export function DevPanel() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
data-lenis-prevent
|
||||
className="flex flex-col gap-4 overflow-y-auto p-4 text-sm"
|
||||
>
|
||||
{/* Oturum */}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { type ReactNode } from "react";
|
||||
import { ReactLenis } from "lenis/react";
|
||||
import {
|
||||
motion,
|
||||
useReducedMotion,
|
||||
useScroll,
|
||||
useSpring,
|
||||
useTransform,
|
||||
useVelocity,
|
||||
} from "motion/react";
|
||||
|
||||
/**
|
||||
* Vitrin sayfalarının akışkan scroll efekti: Lenis ile lerp'li (yavaşlatılmış,
|
||||
* momentumlu) kaydırma + scroll hızına bağlı içerik bükülmesi (skew).
|
||||
* Hızlı kaydırdıkça sayfa hafifçe yana yatar, durunca yayla yerine oturur.
|
||||
*
|
||||
* Sohbet ve liste gibi iç scroll alanı olan uygulama sayfalarında KULLANMA —
|
||||
* Lenis tekerlek olayını sayfaya yönlendirdiği için iç scroll'u bozar,
|
||||
* transform da `fixed` konumlu çocukları koparır.
|
||||
*
|
||||
* Hareket azaltılmışsa hem yumuşatma hem bükülme tamamen kapalıdır.
|
||||
*/
|
||||
export function ScrollFlow({
|
||||
children,
|
||||
className,
|
||||
maxSkew = 3,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Derece cinsinden en fazla bükülme açısı. */
|
||||
maxSkew?: number;
|
||||
}) {
|
||||
const reduceMotion = useReducedMotion();
|
||||
const { scrollY } = useScroll();
|
||||
const velocity = useVelocity(scrollY);
|
||||
|
||||
// Önce hızı kesintisiz bir hedef açıya dönüştür, sonra doğrudan görünen
|
||||
// açıyı yumuşat. Böylece eşik/clamp kaynaklı ani hız değişimleri yaya girmez.
|
||||
const targetSkew = useTransform(velocity, (v) => {
|
||||
const RESPONSE = 1800;
|
||||
return Math.tanh(v / RESPONSE) * maxSkew;
|
||||
});
|
||||
const smoothSkew = useSpring(targetSkew, {
|
||||
stiffness: 240,
|
||||
damping: 26,
|
||||
mass: 0.7,
|
||||
restDelta: 0.005,
|
||||
restSpeed: 0.01,
|
||||
});
|
||||
|
||||
// Yuvarlama görsel basamaklanma üretir. Tam hassasiyetle çiz; yalnızca açı
|
||||
// algılanamayacak kadar küçüldüğünde transform'u kaldırarak dev sayfa
|
||||
// katmanının GPU belleğinde boşuna tutulmasını önle.
|
||||
const transform = useTransform(smoothSkew, (v) => {
|
||||
if (Math.abs(v) < 0.01) return "none";
|
||||
return `skewY(${v}deg)`;
|
||||
});
|
||||
|
||||
// Hareket azaltılmışsa da AYNI ağaç render edilir (server/client farklı
|
||||
// ağaç hydration uyumsuzluğu yaratıyordu); efektler koşullu kapatılır:
|
||||
// Lenis tekerlek yumuşatması kapalı, skew transform'u hiç bağlanmaz.
|
||||
return (
|
||||
<ReactLenis
|
||||
root
|
||||
options={{ lerp: 0.09, anchors: true, smoothWheel: !reduceMotion }}
|
||||
>
|
||||
<motion.div
|
||||
className={className}
|
||||
style={reduceMotion ? undefined : { transform }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</ReactLenis>
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,6 @@ function ChatContainerRoot({
|
||||
return (
|
||||
<StickToBottom
|
||||
className={cn("flex overflow-y-auto", className)}
|
||||
data-lenis-prevent
|
||||
resize="smooth"
|
||||
initial="instant"
|
||||
role="log"
|
||||
|
||||
@@ -60,7 +60,6 @@ function DialogContent({
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
data-lenis-prevent
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
|
||||
@@ -68,7 +68,6 @@ function SelectContent({
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
data-lenis-prevent
|
||||
data-align-trigger={position === "item-aligned"}
|
||||
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
|
||||
position={position}
|
||||
|
||||
@@ -8,9 +8,9 @@ const getClientSnapshot = () => true;
|
||||
const getServerSnapshot = () => false;
|
||||
|
||||
/**
|
||||
* Viewport'a sabit kalması gereken öğeleri, transform uygulanan sayfa
|
||||
* katmanının dışına taşır. Böylece üst katmandaki scroll skew animasyonu
|
||||
* `position: fixed` koordinat sistemini değiştirmez.
|
||||
* Viewport'a sabit kalması gereken öğeleri sayfa katmanının dışına, doğrudan
|
||||
* body'ye taşır. Böylece üst katmanlardaki bir transform (ör. Parallax/Reveal)
|
||||
* `position: fixed` koordinat sistemini değiştiremez.
|
||||
*/
|
||||
export function ViewportPortal({ children }: { children: ReactNode }) {
|
||||
const mounted = useSyncExternalStore(
|
||||
|
||||
@@ -14,9 +14,15 @@ import {
|
||||
sayfaMetadata,
|
||||
ustSegmentOgGorseli,
|
||||
} from "@/lib/seo";
|
||||
import { SAYFA_BOYU } from "./sayfalama";
|
||||
import {
|
||||
KATALOG_CTA_ACIKLAMA,
|
||||
SAYFA_BOYU,
|
||||
sayfaDilimi,
|
||||
siraYiliEtiketi,
|
||||
} from "./sayfalama";
|
||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
@@ -42,14 +48,94 @@ export function bolumSayfaStaticParams(): { slug: string; no: string }[] {
|
||||
});
|
||||
}
|
||||
|
||||
type BolumDetay = NonNullable<ReturnType<typeof getBolumBySlug>>;
|
||||
|
||||
/**
|
||||
* Sayfanın satır dilimi + o dilimin taban sırası aralığı. Liste efektif sıraya
|
||||
* göre dizili (lisans, sonra önlisans) olduğundan her derin sayfa belirli bir
|
||||
* sıralama aralığını kapsar. Metadata ve gövde aynı hesabı buradan alır; ikisi
|
||||
* ayrı ayrı dilimlerse başlık ile tablo birbirinden kayabilir.
|
||||
*/
|
||||
function bolumSayfaDilimi(bolum: BolumDetay, sayfa: number) {
|
||||
const tumProgramlar = [
|
||||
...bolum.lisansProgramlari,
|
||||
...bolum.onlisansProgramlari,
|
||||
];
|
||||
const dilim = sayfaDilimi(tumProgramlar, sayfa);
|
||||
let minSira: number | null = null;
|
||||
let maxSira: number | null = null;
|
||||
let verisiz = 0;
|
||||
// Sırası 2025'te olmayıp 2024'ten gelen satırlar: aralık metni yılı dürüstçe
|
||||
// söylesin (tablonun Sıralama sütunu bu satırlarda "—" gösterir).
|
||||
let yalniz2024 = 0;
|
||||
for (const p of dilim) {
|
||||
const efektif = p.sira2025 ?? p.sira2024;
|
||||
if (efektif == null) {
|
||||
verisiz += 1;
|
||||
continue;
|
||||
}
|
||||
if (p.sira2025 == null) yalniz2024 += 1;
|
||||
if (minSira == null || efektif < minSira) minSira = efektif;
|
||||
if (maxSira == null || efektif > maxSira) maxSira = efektif;
|
||||
}
|
||||
return { tumProgramlar, dilim, minSira, maxSira, verisiz, yalniz2024 };
|
||||
}
|
||||
|
||||
/** <title> için kısa sıra: 2.425.061 → "2,43 mn", 95.400 → "95 bin". */
|
||||
function kisaSira(n: number): string {
|
||||
if (n >= 1_000_000) {
|
||||
return `${(n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 })} mn`;
|
||||
}
|
||||
return n >= 10_000 ? `${Math.round(n / 1000)} bin` : sayi(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dilimin okunur özeti ("taban sırası 95.000–180.000 arası programlar").
|
||||
* Dilim boşsa null (çağıran sayfa numaralı eski metne düşer); dilimde hiç
|
||||
* taban verisi yoksa aralık yerine bunu söyler.
|
||||
*/
|
||||
function siraAraligiMetni({
|
||||
dilim,
|
||||
minSira,
|
||||
maxSira,
|
||||
verisiz,
|
||||
yalniz2024,
|
||||
}: ReturnType<typeof bolumSayfaDilimi>): string | null {
|
||||
if (dilim.length === 0) return null;
|
||||
if (minSira == null || maxSira == null) {
|
||||
return "taban verisi olmayan programlar";
|
||||
}
|
||||
const yil = siraYiliEtiketi(yalniz2024, dilim.length - verisiz);
|
||||
const aralik =
|
||||
minSira === maxSira
|
||||
? `${yil} taban sırası ${sayi(minSira)} olan programlar`
|
||||
: `${yil} taban sırası ${sayi(minSira)}–${sayi(maxSira)} arası programlar`;
|
||||
return verisiz > 0 ? `${aralik} ile taban verisi olmayanlar` : aralik;
|
||||
}
|
||||
|
||||
export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||
const bolum = getBolumBySlug(slug);
|
||||
if (!bolum) return {};
|
||||
const { stats } = bolum;
|
||||
if (sayfa > 1) {
|
||||
return sayfaMetadata({
|
||||
title: `${bolum.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
||||
description: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
|
||||
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
|
||||
const aralik = siraAraligiMetni(sayfaDilim);
|
||||
const { minSira, maxSira } = sayfaDilim;
|
||||
// <title> kısa kalsın (arama sonucunda ayırt edici kısım kesilmesin):
|
||||
// yuvarlanmış rakamlar, kök şablonun " — KolayTercih" eki yok; açıklama tam
|
||||
// cümleyi taşır. "s. N" yuvarlamada çakışan iki sayfayı yine ayırır.
|
||||
const baslikEki =
|
||||
minSira != null && maxSira != null
|
||||
? minSira === maxSira
|
||||
? `Sıra ${kisaSira(minSira)}, s. ${sayfa}`
|
||||
: `Sıra ${kisaSira(minSira)}–${kisaSira(maxSira)}, s. ${sayfa}`
|
||||
: `Sayfa ${sayfa}`;
|
||||
const baslik = `${bolum.ad} Taban Puanları (${baslikEki})`;
|
||||
const meta = sayfaMetadata({
|
||||
title: baslik,
|
||||
description: aralik
|
||||
? `${bolum.ad} taban puanları, sayfa ${sayfa}: ${aralik}. Toplam ${stats.toplam} programın son yerleştirme verileri (2025; 2025'te verisi olmayanlarda 2024) — taban puanı, başarı sıralaması ve kontenjan.`
|
||||
: `${bolum.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${stats.toplam} programın 2025 yerleştirme verileri.`,
|
||||
path: `/bolum/${slug}/sayfa/${sayfa}`,
|
||||
openGraph: {
|
||||
images: ustSegmentOgGorseli(
|
||||
@@ -58,6 +144,7 @@ export function bolumSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||
),
|
||||
},
|
||||
});
|
||||
return { ...meta, title: { absolute: baslik } };
|
||||
}
|
||||
const enIyi =
|
||||
stats.minSira != null
|
||||
@@ -74,13 +161,12 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
const bolum = getBolumBySlug(slug);
|
||||
if (!bolum) notFound();
|
||||
|
||||
const tumProgramlar = [
|
||||
...bolum.lisansProgramlari,
|
||||
...bolum.onlisansProgramlari,
|
||||
];
|
||||
const sayfaDilim = bolumSayfaDilimi(bolum, sayfa);
|
||||
const { tumProgramlar } = sayfaDilim;
|
||||
const toplamSayfa = bolumToplamSayfa(tumProgramlar.length);
|
||||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||||
const ilkSayfa = sayfa === 1;
|
||||
const aralik = siraAraligiMetni(sayfaDilim);
|
||||
const buYol = ilkSayfa ? `/bolum/${slug}` : `/bolum/${slug}/sayfa/${sayfa}`;
|
||||
|
||||
return (
|
||||
@@ -113,20 +199,57 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
Bölümler
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<span className="text-foreground">
|
||||
{ilkSayfa ? bolum.ad : `Sayfa ${sayfa}`}
|
||||
</span>
|
||||
{ilkSayfa ? (
|
||||
<span className="text-foreground">{bolum.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{bolum.ad}
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<span className="text-foreground">Sayfa {sayfa}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<header className="border-b pb-7 sm:pb-9">
|
||||
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||
{bolum.ad}
|
||||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||
{!ilkSayfa && aralik ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="mt-2 block text-lg font-semibold tracking-normal text-muted-foreground sm:text-2xl">
|
||||
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</h1>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
|
||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele
|
||||
{!ilkSayfa ? ` — sayfa ${sayfa}` : ""}.
|
||||
</p>
|
||||
{ilkSayfa ? (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{sayi(tumProgramlar.length)} üniversite programının 2025 taban
|
||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{bolum.ad} taban puanları tablosunun {sayfa}. sayfası (
|
||||
{sayi(tumProgramlar.length)} programın{" "}
|
||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
|
||||
satırları, taban sırasına göre)
|
||||
{aralik ? `: ${aralik}` : ""}. Listenin başı{" "}
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
||||
@@ -153,6 +276,14 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
/>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
baslik={`Sıralaman ${bolum.ad} tabanlarının neresinde?`}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,31 @@ import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
/** Programatik tablo sayfaları için satır dilimi boyutu. */
|
||||
export const SAYFA_BOYU = 50;
|
||||
|
||||
/**
|
||||
* Düz listenin `sayfa` numaralı dilimi. Metadata (title/description) ile sayfa
|
||||
* gövdesi aynı satırları anlatsın diye dilim hesabı tek yerde durur.
|
||||
*/
|
||||
export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] {
|
||||
return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU);
|
||||
}
|
||||
|
||||
/** Aralıktaki sıraların hangi yıldan geldiği: hepsi 2025 / hepsi 2024 / karışık. */
|
||||
export function siraYiliEtiketi(yalniz2024: number, siraliSayi: number): string {
|
||||
if (yalniz2024 === 0) return "2025";
|
||||
return yalniz2024 === siraliSayi ? "2024" : "2024–2025";
|
||||
}
|
||||
|
||||
/**
|
||||
* Üniversite ve bölüm tablo sayfalarının altındaki CtaSiraForm kartının köprü
|
||||
* cümlesi — iki sayfa birebir aynı metni kullansın diye tek kaynak. Sunucu
|
||||
* bileşenlerinden okunduğu için "use client" olan cta-sira-form.tsx'te
|
||||
* duramaz (istemci modülünün sabitleri sunucuda değer olarak okunamaz).
|
||||
* Vaat içermez: tablo geçmiş veridir, ürün sıralamaya yakın programları
|
||||
* risk seviyesine göre ayırıp listeler.
|
||||
*/
|
||||
export const KATALOG_CTA_ACIKLAMA =
|
||||
"Bu tablodaki tabanlar geçmiş yerleştirmelerin sonucu; bu yıl değişebilir. Sıralamanı gir, tabanı sıralamana yakın programları risk seviyesine göre ayrılmış hâlde listeleyelim. Ücretsiz, üyelik gerekmez.";
|
||||
|
||||
/**
|
||||
* Statik path tabanlı sayfalama navigasyonu. 1. sayfa taban URL'in
|
||||
* kendisidir (/bolum/x), sonrakiler /bolum/x/sayfa/2 ... — hepsi gerçek
|
||||
|
||||
@@ -18,7 +18,13 @@ import {
|
||||
} from "@/lib/seo";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
||||
import {
|
||||
KATALOG_CTA_ACIKLAMA,
|
||||
Sayfalama,
|
||||
SAYFA_BOYU,
|
||||
sayfaDilimi,
|
||||
siraYiliEtiketi,
|
||||
} from "./sayfalama";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||
@@ -51,13 +57,69 @@ export function uniSayfaStaticParams(): { slug: string; no: string }[] {
|
||||
});
|
||||
}
|
||||
|
||||
type UniDetay = NonNullable<ReturnType<typeof getUniversiteBySlug>>;
|
||||
|
||||
/**
|
||||
* Sayfanın satır dilimi + o dilimde geçen fakülte/birim adları (tablo
|
||||
* sırasıyla, tekrarsız). Liste fakülteye göre dizili olduğundan her derin
|
||||
* sayfa belirli birimleri kapsar. Metadata ve gövde aynı hesabı buradan alır.
|
||||
* Fakültesi boş satırlar tabloda "Diğer" grubuna düşer; ad listesine girmez.
|
||||
*/
|
||||
function uniSayfaDilimi(uni: UniDetay, sayfa: number) {
|
||||
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
|
||||
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
|
||||
const dilim = sayfaDilimi(tumProgramlar, sayfa);
|
||||
const fakulteAdlari: string[] = [];
|
||||
for (const p of dilim) {
|
||||
if (p.fakulte && !fakulteAdlari.includes(p.fakulte)) {
|
||||
fakulteAdlari.push(p.fakulte);
|
||||
}
|
||||
}
|
||||
return { tumProgramlar, dilim, fakulteAdlari };
|
||||
}
|
||||
|
||||
/**
|
||||
* İlk 2 ad + "ve N birim daha"; ad yoksa null (çağıran eski metne düşer).
|
||||
* Dilim tek birimden ibaretse (50'den çok programlı MYO'lar art arda iki
|
||||
* sayfayı doldurabiliyor) sayfalar ayırt edilsin diye birim içi taban sırası
|
||||
* aralığı eklenir — birim içinde satırlar zaten taban sırasına göre dizili.
|
||||
*/
|
||||
function fakulteOzeti({
|
||||
dilim,
|
||||
fakulteAdlari: adlar,
|
||||
}: ReturnType<typeof uniSayfaDilimi>): string | null {
|
||||
if (adlar.length === 0) return null;
|
||||
if (adlar.length === 1) {
|
||||
const siralar = dilim
|
||||
.map((p) => p.sira2025 ?? p.sira2024)
|
||||
.filter((n): n is number => n != null);
|
||||
if (siralar.length === 0) return adlar[0];
|
||||
const min = Math.min(...siralar);
|
||||
const max = Math.max(...siralar);
|
||||
const yil = siraYiliEtiketi(
|
||||
dilim.filter((p) => p.sira2025 == null && p.sira2024 != null).length,
|
||||
siralar.length,
|
||||
);
|
||||
return min === max
|
||||
? `${adlar[0]} (${yil} taban sırası ${sayi(min)})`
|
||||
: `${adlar[0]} (${yil} taban sırası ${sayi(min)}–${sayi(max)} arası)`;
|
||||
}
|
||||
if (adlar.length === 2) return `${adlar[0]} ve ${adlar[1]}`;
|
||||
return `${adlar[0]}, ${adlar[1]} ve ${adlar.length - 2} birim daha`;
|
||||
}
|
||||
|
||||
export function uniSayfaMetadata(slug: string, sayfa: number): Metadata {
|
||||
const uni = getUniversiteBySlug(slug);
|
||||
if (!uni) return {};
|
||||
if (sayfa > 1) {
|
||||
// Üniversite adları uzun: <title> sayfa numarasıyla kalır (~60 karakter),
|
||||
// farklılaştırıcı (o sayfadaki birimler) açıklamaya ve gövdeye gider.
|
||||
const birimler = fakulteOzeti(uniSayfaDilimi(uni, sayfa));
|
||||
return sayfaMetadata({
|
||||
title: `${uni.ad} Taban Puanları 2026 (Sayfa ${sayfa})`,
|
||||
description: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
|
||||
description: birimler
|
||||
? `${uni.ad} taban puanları, sayfa ${sayfa}: ${birimler}. Toplam ${uni.programSayisi} programın 2025 yerleştirme verileri — taban puanı, başarı sıralaması ve kontenjan.`
|
||||
: `${uni.ad} taban puanları ve başarı sıralamaları tablosunun ${sayfa}. sayfası — toplam ${uni.programSayisi} programın 2025 yerleştirme verileri.`,
|
||||
path: `/universite/${slug}/sayfa/${sayfa}`,
|
||||
openGraph: {
|
||||
images: ustSegmentOgGorseli(
|
||||
@@ -84,14 +146,11 @@ export function UniversiteIcerik({
|
||||
const uni = getUniversiteBySlug(slug);
|
||||
if (!uni) notFound();
|
||||
|
||||
// Sayfalama fakülte sıralı düz liste üzerinden; dilim içi yeniden gruplanır
|
||||
const tumProgramlar = uni.fakulteler.flatMap((f) => f.programlar);
|
||||
const sayfaDilim = uniSayfaDilimi(uni, sayfa);
|
||||
const { tumProgramlar, dilim } = sayfaDilim;
|
||||
const toplamSayfa = uniToplamSayfa(tumProgramlar.length);
|
||||
if (sayfa < 1 || sayfa > toplamSayfa) notFound();
|
||||
const dilim = tumProgramlar.slice(
|
||||
(sayfa - 1) * SAYFA_BOYU,
|
||||
sayfa * SAYFA_BOYU,
|
||||
);
|
||||
const birimler = fakulteOzeti(sayfaDilim);
|
||||
const gruplar: {
|
||||
fakulte: string;
|
||||
programlar: (Program & { bolumSlug: string | null })[];
|
||||
@@ -263,7 +322,9 @@ export function UniversiteIcerik({
|
||||
{sayi(tumProgramlar.length)} programın{" "}
|
||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
|
||||
satırları, fakülte sırasıyla). Üniversite özeti ve istatistikler{" "}
|
||||
satırları, fakülte sırasıyla).
|
||||
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite
|
||||
özeti ve istatistikler{" "}
|
||||
<Link
|
||||
href={`/universite/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
@@ -300,7 +361,8 @@ export function UniversiteIcerik({
|
||||
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
baslik={`Sıralamanla ${uni.ad} programlarına yerleşir misin?`}
|
||||
baslik={`Sıralaman ${uni.ad} tabanlarının neresinde?`}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { KATALOG_VERI_TARIHI } from "@/lib/seo";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TARIH_METNI = new Date(
|
||||
// Veri tarihinin görünür biçimi ("9 Ağustos 2026") — tek biçim kaynağı; ana
|
||||
// sayfa hero'sundaki veri satırı da bunu kullanır.
|
||||
export const KATALOG_TARIH_METNI = new Date(
|
||||
`${KATALOG_VERI_TARIHI}T00:00:00Z`,
|
||||
).toLocaleDateString("tr-TR", {
|
||||
day: "numeric",
|
||||
@@ -23,7 +25,7 @@ export function VeriKaynagi({
|
||||
<p className={cn("mt-3 text-xs leading-5 text-slate-500", className)}>
|
||||
{children}
|
||||
Kaynak: YÖK Atlas 2025 yerleştirme sonuçları. Son güncelleme:{" "}
|
||||
<time dateTime={KATALOG_VERI_TARIHI}>{TARIH_METNI}</time>.
|
||||
<time dateTime={KATALOG_VERI_TARIHI}>{KATALOG_TARIH_METNI}</time>.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Mail } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
@@ -40,18 +40,28 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
||||
|
||||
// 60 sn: sunucunun adres başına gönderim soğumasıyla hizalı; daha kısa
|
||||
// sayaçta "Yeniden gönder" sessizce yutulur, kullanıcı gelmeyen maili bekler.
|
||||
// Tek sayaç: "E-postayı düzelt" → yeniden gönderimde eski interval durmazsa
|
||||
// iki sayaç birlikte sayar, buton sunucu soğuması bitmeden açılır.
|
||||
const sayacRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
function geriSayimBaslat() {
|
||||
if (sayacRef.current) clearInterval(sayacRef.current);
|
||||
setBekleme(60);
|
||||
const sayac = setInterval(() => {
|
||||
sayacRef.current = setInterval(() => {
|
||||
setBekleme((s) => {
|
||||
if (s <= 1) {
|
||||
clearInterval(sayac);
|
||||
if (sayacRef.current) clearInterval(sayacRef.current);
|
||||
sayacRef.current = null;
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (sayacRef.current) clearInterval(sayacRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function googleIleGiris() {
|
||||
setBusy("google");
|
||||
|
||||
@@ -13,7 +13,10 @@ export async function GirisKarti({
|
||||
callback?: string;
|
||||
hata?: string;
|
||||
}) {
|
||||
const callbackURL = callback && callback.startsWith("/") ? callback : "/";
|
||||
// Yalnız site içi yol: "//evil.com" ve "/\\evil.com" protokol-göreli adres
|
||||
// sayılır, açık yönlendirmeye izin verme (dogrula-karti.tsx ile aynı kalkan).
|
||||
const callbackURL =
|
||||
callback && /^\/(?![/\\])/.test(callback) ? callback : "/";
|
||||
// Magic link doğrulaması başarısız olunca errorCallbackURL buraya düşürür
|
||||
// (bkz. giris-form.tsx ve /giris/dogrula) — ham INVALID_TOKEN yerine
|
||||
// dostça bir mesaj gösterip yeni bağlantı istemeye yönlendiriyoruz.
|
||||
|
||||
@@ -46,7 +46,11 @@ import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { bolumBazAdi, turkishSlugify } from "@/lib/slug";
|
||||
import { olay } from "@/lib/analitik";
|
||||
import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari";
|
||||
import { sonucaGitIste } from "../hooks/use-tercih-profili";
|
||||
import {
|
||||
profilToplamaAc,
|
||||
sonucaGitIste,
|
||||
useTercihProfili,
|
||||
} from "../hooks/use-tercih-profili";
|
||||
import { useOturumDurumu } from "../liste-oturum-sinyali";
|
||||
import { useGirisUrl } from "../liste-giris-url";
|
||||
import {
|
||||
@@ -91,6 +95,11 @@ export function ListeCekmecesi() {
|
||||
const [oturdu, setOturdu] = useState(false);
|
||||
|
||||
const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100);
|
||||
// Profilsiz + ile eklenenlerin riski henüz hesaplanmamıştır; alt kart
|
||||
// olmayan bir analizi "hazır" diye duyurmasın. Profil varken null risk,
|
||||
// programın taban sırası olmadığındandır — sıra istemek yanlış olur.
|
||||
const profil = useTercihProfili();
|
||||
const riskEksik = !profil && liste.some((t) => t.risk == null);
|
||||
|
||||
function programaEkleGit() {
|
||||
cekmeceKapat();
|
||||
@@ -112,7 +121,6 @@ export function ListeCekmecesi() {
|
||||
onAnimationEnd={(open) => setOturdu(open)}
|
||||
>
|
||||
<DrawerContent
|
||||
data-lenis-prevent
|
||||
className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
||||
>
|
||||
<DrawerHeader className="border-b border-slate-100 text-left md:text-left">
|
||||
@@ -179,7 +187,6 @@ export function ListeCekmecesi() {
|
||||
</DrawerHeader>
|
||||
|
||||
<div
|
||||
data-lenis-prevent
|
||||
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4"
|
||||
>
|
||||
{liste.length === 0 ? (
|
||||
@@ -269,22 +276,38 @@ export function ListeCekmecesi() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="font-heading text-2xl font-bold">
|
||||
Risk analizini biz yaptık
|
||||
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
|
||||
</p>
|
||||
<p className="mt-1 truncate text-sm leading-relaxed text-white">
|
||||
Her seçimin yerleşme riski hazır — son kararı sen ver.
|
||||
{riskEksik
|
||||
? "Sıralamanı girince her seçimin riskini gösteririz."
|
||||
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href="/listem" onClick={cekmeceKapat}>
|
||||
Listemde aç
|
||||
{riskEksik ? (
|
||||
<Button
|
||||
size="lg"
|
||||
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
onClick={() => {
|
||||
cekmeceKapat();
|
||||
profilToplamaAc(null, true, "arti_toast");
|
||||
}}
|
||||
>
|
||||
Sıralamamı gir
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Link>
|
||||
</Button>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href="/listem" onClick={cekmeceKapat}>
|
||||
Listemde aç
|
||||
<ArrowRight data-icon="inline-end" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Check, Plus } from "lucide-react";
|
||||
import { ArrowRight, Check, Plus } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { Program } from "@/types/yokatlas";
|
||||
import { olay } from "@/lib/analitik";
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
useListedeMi,
|
||||
type ManuelKaynak,
|
||||
} from "../liste-store";
|
||||
import { programEkleIste } from "../hooks/use-tercih-profili";
|
||||
import {
|
||||
profilToplamaAc,
|
||||
programEkleIste,
|
||||
} from "../hooks/use-tercih-profili";
|
||||
import { preloadTercihProfiliKapisi } from "./tercih-profili-kapisi-lazy";
|
||||
|
||||
type ProgramOzet = Pick<
|
||||
@@ -25,6 +28,8 @@ type ProgramOzet = Pick<
|
||||
| "sira2024"
|
||||
>;
|
||||
|
||||
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";
|
||||
|
||||
export function ProgramEkleButonu({
|
||||
program,
|
||||
kaynak = "sonuc",
|
||||
@@ -53,10 +58,32 @@ export function ProgramEkleButonu({
|
||||
} else if (sonuc.zatenVar) {
|
||||
toast.info("Bu program zaten seçtiklerinde.");
|
||||
} else if (sonuc.eklendi) {
|
||||
// Yalnız gerçek eklemede: profilsiz + kapıyı açar (kapiAcildi) ve
|
||||
// program o an listeye girmez — o yolun olayı kapı tamamlanınca
|
||||
// profilTamamlandi içinden {yol: "kapi"} ile atılır.
|
||||
olay("program_eklendi", { kaynak, yol: "dogrudan" });
|
||||
olay("program_eklendi", {
|
||||
kaynak,
|
||||
yol: "dogrudan",
|
||||
profilli: sonuc.profilsiz ? "hayir" : "evet",
|
||||
});
|
||||
if (sonuc.profilsiz) {
|
||||
// Önce ver, sonra iste: program eklendi; sıralama sorusu değer
|
||||
// karşılığında gelir. Sabit id → art arda +'larda toast yığılmaz,
|
||||
// aynı toast tazelenir.
|
||||
toast.success("Seçtiklerine eklendi", {
|
||||
id: PROFILSIZ_EKLEME_TOAST_ID,
|
||||
duration: 8000,
|
||||
description:
|
||||
"Sıralamanı girersen bu programın risk seviyesini de gösteririz.",
|
||||
action: {
|
||||
label: (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
Sıralamamı gir
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</span>
|
||||
),
|
||||
// Tanıtım ekranını atla, doğrudan sıralama adımı
|
||||
onClick: () => profilToplamaAc(null, true, "arti_toast"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +92,8 @@ export function ProgramEkleButonu({
|
||||
type="button"
|
||||
disabled={listede || doldu}
|
||||
onClick={handleClick}
|
||||
// Profilsiz kullanıcıda + kapıyı açar — chunk'ı niyet anında ısıt
|
||||
// Profilsiz eklemenin toast aksiyonu kapıyı açar — chunk'ı niyet
|
||||
// anında ısıt
|
||||
onPointerEnter={preloadTercihProfiliKapisi}
|
||||
onFocus={preloadTercihProfiliKapisi}
|
||||
title={
|
||||
|
||||
@@ -20,7 +20,7 @@ import type {
|
||||
PuanTuruKey,
|
||||
RankResultsListe,
|
||||
} from "@/types/yokatlas";
|
||||
import { riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
||||
import {
|
||||
ProgramSiraGecmisi,
|
||||
@@ -51,21 +51,21 @@ const DILIMLER: {
|
||||
}[] = [
|
||||
{
|
||||
key: "hayal",
|
||||
etiket: "Hayal",
|
||||
etiket: DILIM_ETIKET.hayal,
|
||||
icon: Rocket,
|
||||
renk: "text-red-500",
|
||||
renkAktif: "text-red-400",
|
||||
},
|
||||
{
|
||||
key: "dengeli",
|
||||
etiket: "Dengeli",
|
||||
etiket: DILIM_ETIKET.dengeli,
|
||||
icon: Scale,
|
||||
renk: "text-amber-500",
|
||||
renkAktif: "text-amber-400",
|
||||
},
|
||||
{
|
||||
key: "garanti",
|
||||
etiket: "Garanti",
|
||||
etiket: DILIM_ETIKET.garanti,
|
||||
icon: ShieldCheck,
|
||||
renk: "text-emerald-600",
|
||||
renkAktif: "text-emerald-400",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { DILIM_ETIKET, RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
|
||||
import { sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import {
|
||||
tercihDegerlendir,
|
||||
@@ -156,9 +156,12 @@ export function SecimlerimPaneli() {
|
||||
|
||||
{ozet ? (
|
||||
<dl className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||
<Istatistik etiket="Hayal" deger={ozet.hayal} />
|
||||
<Istatistik etiket="Dengeli" deger={ozet.dengeli} />
|
||||
<Istatistik etiket="Daha güvenli" deger={ozet.dahaGuvenli} />
|
||||
<Istatistik etiket={DILIM_ETIKET.hayal} deger={ozet.hayal} />
|
||||
<Istatistik etiket={DILIM_ETIKET.dengeli} deger={ozet.dengeli} />
|
||||
<Istatistik
|
||||
etiket={DILIM_ETIKET.garanti}
|
||||
deger={ozet.dahaGuvenli}
|
||||
/>
|
||||
<Istatistik etiket="Dikkat" deger={ozet.dikkat} />
|
||||
</dl>
|
||||
) : null}
|
||||
@@ -272,7 +275,9 @@ const SecimSatiri = memo(function SecimSatiri({
|
||||
/>
|
||||
) : null}
|
||||
{degerlendirme.riskEtiketi}
|
||||
{risk ? (
|
||||
{/* Dilim adı ile risk etiketi aynı kelimeyse ("Güvenli")
|
||||
parantez yazılmaz — "Güvenli (Güvenli)" olmasın. */}
|
||||
{risk && RISK_ETIKET[risk] !== degerlendirme.riskEtiketi ? (
|
||||
<span className="font-normal text-slate-500">
|
||||
({RISK_ETIKET[risk]})
|
||||
</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
// Global "+" kapısı: profil yokken bekleyen programı tutar, modalı açar.
|
||||
// Global profil kapısı deposu. + profil yokken de programı hemen ekler
|
||||
// (prototip); kapı, ekleme toast'ındaki aksiyondan ve diğer CTA'lardan açılır.
|
||||
// Layout'ta bir kez monte edilen TercihProfiliKapisi bu store'a abone olur.
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
@@ -45,7 +46,15 @@ export type BekleyenHedef = "sonuc" | "sihirbaz";
|
||||
* `kapi_kapatildi` olaylarının `tetik` özelliği), davranışı etkilemez.
|
||||
* "arti": + butonu · "sonuc": sonuca gitmek isteyen CTA'lar ·
|
||||
* "sihirbaz": Yapay Zeka niyetli CTA'lar · "duzenle": profil düzenleme. */
|
||||
export type KapiTetik = "arti" | "sonuc" | "sihirbaz" | "duzenle" | "diger";
|
||||
export type KapiTetik =
|
||||
| "arti"
|
||||
// Profilsiz + artık anında ekliyor; kapı, ekleme toast'ındaki
|
||||
// "Sıralamamı gir" aksiyonundan açılır ("arti" yeni akışta atılmaz).
|
||||
| "arti_toast"
|
||||
| "sonuc"
|
||||
| "sihirbaz"
|
||||
| "duzenle"
|
||||
| "diger";
|
||||
|
||||
type KapıDurumu = {
|
||||
acik: boolean;
|
||||
@@ -230,32 +239,35 @@ export function programEkleIste(opts: {
|
||||
eklendi: boolean;
|
||||
zatenVar: boolean;
|
||||
doldu: boolean;
|
||||
/** Artık hep false: + kapıyı açmıyor (çağıranlar kırılmasın diye duruyor). */
|
||||
kapiAcildi: boolean;
|
||||
/** Program profil (sıralama) olmadan eklendi → risk henüz hesaplanamadı. */
|
||||
profilsiz: boolean;
|
||||
} {
|
||||
const { program, kaynak = "sonuc", adaySira, adayTur } = opts;
|
||||
const { program, kaynak = "sonuc", adaySira } = opts;
|
||||
|
||||
if (listedeMi(program.id)) {
|
||||
return { eklendi: false, zatenVar: true, doldu: false, kapiAcildi: false };
|
||||
return {
|
||||
eklendi: false,
|
||||
zatenVar: true,
|
||||
doldu: false,
|
||||
kapiAcildi: false,
|
||||
profilsiz: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (!tercihProfiliTamamlandiMi()) {
|
||||
kapiyiAc(
|
||||
{
|
||||
duzenle: false,
|
||||
dogrudanSira: false,
|
||||
bekleyen: { program, kaynak, adaySira, adayTur },
|
||||
bekleyenHedef: null,
|
||||
},
|
||||
"arti",
|
||||
);
|
||||
return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true };
|
||||
}
|
||||
// [PROTOTİP] Önce ver, sonra iste: profil yokken de program HEMEN listeye
|
||||
// girer (sıra bilinmiyorsa risk null), kapı açılmaz. Sıralama sorusu
|
||||
// butondaki toast aksiyonuyla gelir; profil girilince profilTamamlandi →
|
||||
// riskleriYenile tüm listedeki null riskleri doldurur. Eski davranış
|
||||
// (bekleyen program + kapı) için bu commit'i geri almak yeterli.
|
||||
const profilsiz = !tercihProfiliTamamlandiMi();
|
||||
|
||||
if (!durum.profilYuklendi) {
|
||||
if (!profilsiz && !durum.profilYuklendi) {
|
||||
profiliTazele();
|
||||
}
|
||||
|
||||
const profil = durum.profil ?? tercihProfiliOku();
|
||||
const profil = profilsiz ? null : (durum.profil ?? tercihProfiliOku());
|
||||
const tercih = programdanManuelTercih(
|
||||
program,
|
||||
adaySira ?? profil?.sira ?? null,
|
||||
@@ -263,9 +275,21 @@ export function programEkleIste(opts: {
|
||||
);
|
||||
const ok = ekle(tercih);
|
||||
if (!ok) {
|
||||
return { eklendi: false, zatenVar: false, doldu: true, kapiAcildi: false };
|
||||
return {
|
||||
eklendi: false,
|
||||
zatenVar: false,
|
||||
doldu: true,
|
||||
kapiAcildi: false,
|
||||
profilsiz,
|
||||
};
|
||||
}
|
||||
return { eklendi: true, zatenVar: false, doldu: false, kapiAcildi: false };
|
||||
return {
|
||||
eklendi: true,
|
||||
zatenVar: false,
|
||||
doldu: false,
|
||||
kapiAcildi: false,
|
||||
profilsiz,
|
||||
};
|
||||
}
|
||||
|
||||
/** Profil yazıldıktan sonra; bekleyen hedef varsa yönlendirme href'i döner.
|
||||
|
||||
@@ -8,7 +8,10 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import type { Program } from "@/types/yokatlas";
|
||||
import { riskHesapla } from "@/lib/risk";
|
||||
import { type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import {
|
||||
PROFIL_DEGISTI_EVENT,
|
||||
type TercihProfili,
|
||||
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
@@ -49,6 +52,11 @@ function yukle() {
|
||||
const ham = localStorage.getItem(STORAGE_KEY);
|
||||
if (ham) liste = dogrula(JSON.parse(ham));
|
||||
} catch {}
|
||||
// Profilsiz + ile eklenenlerin riski null'dır. Profil sonradan hangi yoldan
|
||||
// yazılırsa yazılsın (kapı, hero/CTA formu, sihirbaz, giriş senkronu) riskler
|
||||
// dolsun: açılışta bir kez, sonra her profil değişiminde.
|
||||
if (liste.some((t) => t.risk == null)) riskleriYenile();
|
||||
window.addEventListener(PROFIL_DEGISTI_EVENT, () => riskleriYenile());
|
||||
// Başka sekmede yapılan değişiklikleri de yansıt
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key !== STORAGE_KEY) return;
|
||||
@@ -144,6 +152,9 @@ export function sunucuylaBirlestir(sunucuItems: ManuelTercih[]): ManuelTercih[]
|
||||
liste = [...liste, ...ekler].slice(0, MANUEL_LISTE_MAX);
|
||||
kaydet();
|
||||
yayinla();
|
||||
// Başka cihazda profilsiz eklenmiş kayıtlar risksiz gelir; profil lokalde
|
||||
// zaten varsa PROFIL_DEGISTI_EVENT yayılmaz, riski burada doldur.
|
||||
if (liste.some((t) => t.risk == null)) riskleriYenile();
|
||||
return liste;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Lock } from "lucide-react";
|
||||
import { verifySession } from "@/lib/session";
|
||||
import { getCurrentUser, verifySession } from "@/lib/session";
|
||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { siparisGetir } from "../odeme-queries";
|
||||
@@ -28,6 +28,12 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
|
||||
if (order.status === "failed" || !order.iyzicoFormContent) {
|
||||
redirect("/paket");
|
||||
}
|
||||
// Paket bu arada alındıysa (başka sekme/cihaz, geri tuşu) eski bekleyen
|
||||
// paket siparişinin formu yeniden çizilmesin — ikinci kez ücret çekilmesin.
|
||||
if (order.product === "paket") {
|
||||
const kullanici = await getCurrentUser();
|
||||
if (kullanici?.hasPaket) redirect("/paket");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -28,7 +29,7 @@ const OZELLIKLER = [
|
||||
},
|
||||
{
|
||||
icon: ShieldCheck,
|
||||
text: "Her tercih için yerleşme olasılığı ve risk analizi (son 4 yıl trend)",
|
||||
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
|
||||
},
|
||||
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
|
||||
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState, useEffect } from "react";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useLayoutEffect, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Loader2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { olay } from "@/lib/analitik";
|
||||
import { baslatOdeme, type OdemeBaslatDurum } from "../odeme-actions";
|
||||
import { baslatOdeme } from "../odeme-actions";
|
||||
|
||||
const BAGLANTI_HATASI =
|
||||
"Ödeme başlatılamadı; kartından çekim yapılmadı. Bağlantını kontrol edip tekrar dener misin?";
|
||||
|
||||
export function SatinAlForm({
|
||||
urun,
|
||||
@@ -16,24 +20,50 @@ export function SatinAlForm({
|
||||
label: string;
|
||||
vurgulu?: boolean;
|
||||
}) {
|
||||
const [state, formAction, pending] = useActionState<
|
||||
OdemeBaslatDurum,
|
||||
FormData
|
||||
>(baslatOdeme, undefined);
|
||||
const router = useRouter();
|
||||
const [pending, startTransition] = useTransition();
|
||||
// Action bitti, /odeme'ye geçiş sürüyor: buton o arada yeniden açılıp ikinci
|
||||
// bir sipariş başlatmasın.
|
||||
const [gidiyor, setGidiyor] = useState(false);
|
||||
const mesgul = pending || gidiyor;
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.error) toast.error(state.error);
|
||||
}, [state]);
|
||||
// cacheComponents açıkken /paket, /odeme'ye geçince unmount olmaz; gizli
|
||||
// <Activity> içinde state'iyle saklanır. Geri dönen kullanıcı kilitli değil
|
||||
// taze bir buton bulsun diye sayfa gizlenirken sıfırlanır (layout effect
|
||||
// cleanup'ı Activity gizlenince çalışır).
|
||||
useLayoutEffect(() => {
|
||||
return () => setGidiyor(false);
|
||||
}, []);
|
||||
|
||||
// Yönlendirme client'ta: action `redirect()` fırlatmaz, hedefi döndürür
|
||||
// (nedeni odeme-actions.ts'te). Hata toast'ı da burada, effect'te değil —
|
||||
// effect'ler Activity geri açılınca yeniden koştuğu için eski hatayı
|
||||
// tekrar gösteriyordu.
|
||||
function gonder() {
|
||||
olay("odeme_baslatildi", { urun });
|
||||
startTransition(async () => {
|
||||
let sonuc;
|
||||
try {
|
||||
sonuc = await baslatOdeme(urun);
|
||||
} catch {
|
||||
toast.error(BAGLANTI_HATASI);
|
||||
return;
|
||||
}
|
||||
if ("error" in sonuc) {
|
||||
toast.error(sonuc.error);
|
||||
return;
|
||||
}
|
||||
setGidiyor(true);
|
||||
router.push(sonuc.yonlendir);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
action={formAction}
|
||||
onSubmit={() => olay("odeme_baslatildi", { urun })}
|
||||
>
|
||||
<input type="hidden" name="urun" value={urun} />
|
||||
<form action={gonder}>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
disabled={mesgul}
|
||||
aria-busy={mesgul}
|
||||
className={
|
||||
vurgulu
|
||||
? "h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
@@ -41,11 +71,15 @@ export function SatinAlForm({
|
||||
}
|
||||
variant={vurgulu ? "default" : "outline"}
|
||||
>
|
||||
{/* Tıklamada buton içeriği değişmez (metin/spinner takası yok); dar
|
||||
top-up kartında taşmaya yol açıyordu. Çift gönderim `disabled`
|
||||
ile engellenir. */}
|
||||
{/* Tıklamada metin değişmez (dar top-up kartında taşıyordu); geri
|
||||
bildirim aynı boyuttaki ikon takasıyla verilir: ok → dönen halka.
|
||||
Çift gönderim `disabled` ile engellenir. */}
|
||||
{label}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
{mesgul ? (
|
||||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
||||
) : (
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { verifySession } from "@/lib/session";
|
||||
import { getCurrentUser, getSession } from "@/lib/session";
|
||||
import { appUrl as siteKok } from "@/lib/app-url";
|
||||
import { appDb, schema } from "@/lib/appdb";
|
||||
import { URUNLER } from "@/lib/credits";
|
||||
import { initializeCheckoutForm, initImzaDurumu } from "@/lib/iyzico";
|
||||
|
||||
export type OdemeBaslatDurum = { error?: string } | undefined;
|
||||
// Action bilerek `redirect()` FIRLATMAZ, hedefi döndürür; yönlendirmeyi client
|
||||
// yapar (bkz. satin-al-form.tsx). Fırlatılan redirect, client'ta action
|
||||
// promise'ini reddedip formu RedirectBoundary üzerinden yeniden mount ediyor;
|
||||
// cacheComponents altında /paket gizli <Activity> içinde canlı tutulduğu için
|
||||
// bu yarım kalmış durum geri dönüşte butonu tıklamalara sağır bırakıyordu.
|
||||
export type OdemeBaslatDurum = { error: string } | { yonlendir: string };
|
||||
|
||||
const GENEL_HATA =
|
||||
"Ödeme şu anda başlatılamıyor; kartından çekim yapılmadı. Birazdan tekrar dener misin?";
|
||||
@@ -23,15 +27,26 @@ function adSoyadAyir(tamAd: string): [string, string] {
|
||||
}
|
||||
|
||||
export async function baslatOdeme(
|
||||
_prev: OdemeBaslatDurum,
|
||||
formData: FormData,
|
||||
product: "paket" | "topup",
|
||||
): Promise<OdemeBaslatDurum> {
|
||||
// Ucuz senkron doğrulama pahalı oturum turundan önce
|
||||
const product = formData.get("urun");
|
||||
// Ucuz senkron doğrulama pahalı oturum turundan önce (argüman client'tan
|
||||
// gelir; tipine güvenilmez)
|
||||
if (product !== "paket" && product !== "topup") {
|
||||
return { error: "Geçersiz ürün." };
|
||||
}
|
||||
const session = await verifySession("/paket");
|
||||
const session = await getSession();
|
||||
if (!session) {
|
||||
return { yonlendir: `/giris?callback=${encodeURIComponent("/paket")}` };
|
||||
}
|
||||
// Arayüz bu kuralları zaten uygular ama bayat sekmeden (ör. paket başka
|
||||
// sekmede alındı) gelen istek ikinci kez ücret çekmesin.
|
||||
const kullanici = await getCurrentUser();
|
||||
if (product === "paket" && kullanici?.hasPaket) {
|
||||
return { error: "Paketin zaten aktif; yeniden ödeme alınmadı." };
|
||||
}
|
||||
if (product === "topup" && !kullanici?.hasPaket) {
|
||||
return { error: "Ek kredi için önce paketi alman gerekiyor." };
|
||||
}
|
||||
const urun = URUNLER[product];
|
||||
|
||||
const orderId = crypto.randomUUID();
|
||||
@@ -100,5 +115,5 @@ export async function baslatOdeme(
|
||||
.set({ iyzicoToken: init.token, iyzicoFormContent: init.checkoutFormContent })
|
||||
.where(eq(schema.orders.id, orderId));
|
||||
|
||||
redirect(`/odeme?siparis=${orderId}`);
|
||||
return { yonlendir: `/odeme?siparis=${orderId}` };
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ElCizimiSema } from "@/features/rehber/components/el-cizimi-sema";
|
||||
|
||||
// Renkler ürünün risk dilinden (kırmızı/sarı/yeşil) ve slate paletinden;
|
||||
// renk tek başına anlam taşımaz, her düğüm metinle etiketlidir.
|
||||
// Üçüncü dilimin görünen adı "güvenli"dir; `classDef garanti` yalnızca iç
|
||||
// renk sınıfıdır (render edilmez), iç anahtarla aynı kalır.
|
||||
const SEMA = `---
|
||||
config:
|
||||
look: handDrawn
|
||||
@@ -25,7 +27,7 @@ flowchart TD
|
||||
R --> L("En fazla 24 tercihlik liste")
|
||||
L --> L1("üstte hayal — hedef 5, havuz elverdiğince")
|
||||
L --> L2("ortada dengeli — hedef 13, havuz elverdiğince")
|
||||
L --> L3("sonda garanti — hedef 6, havuz elverdiğince")
|
||||
L --> L3("sonda güvenli — hedef 6, havuz elverdiğince")
|
||||
|
||||
classDef havuz fill:#eff6ff,stroke:#3b82f6,color:#1e3a8a
|
||||
classDef uyari fill:#fffbeb,stroke:#f59e0b,color:#92400e
|
||||
@@ -42,7 +44,7 @@ export function HuniSemasi() {
|
||||
return (
|
||||
<ElCizimiSema
|
||||
tanim={SEMA}
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda garanti olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||
ariaLabel="Sihirbaz akış şeması: sıralamanla açılan programlar alan, il ve devlet/vakıf adımlarıyla aday havuzuna daraltılır; alanlar bölümün adına ve bağlı olduğu fakülteye göre belirlenir, il adımında programı olan tüm iller listelenir; havuz 24'ün altına düşerse filtreler gevşetilir, yine de dar kalırsa liste eldeki programlarla kurulur ve dağılım havuza uyarlanır; yapay zekâ havuzdan üstte hayal, ortada dengeli, sonda güvenli olacak şekilde en fazla 24 tercih seçip her satıra gerekçe yazar; risk notu ve trend özeti gerçek sıralama geçmişinden hesaplanır."
|
||||
figcaption={
|
||||
<>
|
||||
Her adım havuzu biraz daha daraltır; yapay zekâ listeyi bu havuzun{" "}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
||||
import { PUAN_TURU_ETIKET, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import type { DilimKey, SihirbazFacetleri } from "@/types/yokatlas";
|
||||
import { onbellekliJsonGetir } from "@/lib/istek-cache";
|
||||
import { DILIM_ETIKET } from "@/lib/risk";
|
||||
|
||||
export type DemoVeri = {
|
||||
facetler: SihirbazFacetleri;
|
||||
@@ -146,7 +147,7 @@ export function DilimSeridiCanli() {
|
||||
<svg
|
||||
viewBox="0 0 640 168"
|
||||
role="img"
|
||||
aria-label={`Sıralama ekseni: ${fmt(sira)}. sıradaki aday için Hayal diliminde ${fmt(t.hayal)}, Dengeli diliminde ${fmt(t.dengeli)}, Garanti diliminde ${fmt(t.garanti)} program var.`}
|
||||
aria-label={`Sıralama ekseni: ${fmt(sira)}. sıradaki aday için ${DILIM_ETIKET.hayal} diliminde ${fmt(t.hayal)}, ${DILIM_ETIKET.dengeli} diliminde ${fmt(t.dengeli)}, ${DILIM_ETIKET.garanti} diliminde ${fmt(t.garanti)} program var.`}
|
||||
className="w-full"
|
||||
>
|
||||
{/* eksen */}
|
||||
@@ -174,7 +175,7 @@ export function DilimSeridiCanli() {
|
||||
{/* hayal */}
|
||||
<rect x="20" y="70" width="150" height="32" rx="4" className="fill-red-100" />
|
||||
<text x="95" y="40" textAnchor="middle" className="fill-red-600 text-[12px] font-semibold">
|
||||
Hayal
|
||||
{DILIM_ETIKET.hayal}
|
||||
</text>
|
||||
<text x="95" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||||
{fmt(t.hayal)} program
|
||||
@@ -192,7 +193,7 @@ export function DilimSeridiCanli() {
|
||||
{/* dengeli */}
|
||||
<rect x="174" y="70" width="120" height="32" rx="4" className="fill-amber-100" />
|
||||
<text x="234" y="40" textAnchor="middle" className="fill-amber-600 text-[12px] font-semibold">
|
||||
Dengeli
|
||||
{DILIM_ETIKET.dengeli}
|
||||
</text>
|
||||
<text x="234" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||||
{fmt(t.dengeli)} program
|
||||
@@ -204,7 +205,7 @@ export function DilimSeridiCanli() {
|
||||
{/* garanti */}
|
||||
<rect x="296" y="70" width="324" height="32" rx="4" className="fill-emerald-100" />
|
||||
<text x="458" y="40" textAnchor="middle" className="fill-emerald-700 text-[12px] font-semibold">
|
||||
Garanti
|
||||
{DILIM_ETIKET.garanti}
|
||||
</text>
|
||||
<text x="458" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||||
{fmt(t.garanti)} program
|
||||
|
||||
@@ -68,12 +68,12 @@ export function MeraklisinaIcerik() {
|
||||
(listenin bel kemiği — sıralamanın yaklaşık yüzde kırk fazlasına
|
||||
kadar pay bırakılır), tabanı{" "}
|
||||
<strong>belirgin şekilde altında</strong> kalan her şey ise{" "}
|
||||
<span className="font-semibold text-emerald-600">Garanti</span>{" "}
|
||||
<span className="font-semibold text-emerald-600">Güvenli</span>{" "}
|
||||
dilimidir.
|
||||
</p>
|
||||
<DilimSeridiCanli />
|
||||
<p className="mt-4">
|
||||
Garanti diliminin bir sonu yok: 87. sıradaki bir adayın da 500
|
||||
Güvenli diliminin bir sonu yok: 87. sıradaki bir adayın da 500
|
||||
bininci sıradaki bir adayın da ulaşabildiği <em>bütün</em>{" "}
|
||||
programlar listededir. Liste sana en yakın tabandan başlar; aşağı
|
||||
indikçe ve “Daha fazla göster”e bastıkça senden
|
||||
@@ -131,7 +131,7 @@ export function MeraklisinaIcerik() {
|
||||
<p className="mt-2">
|
||||
Tüm bu hesaplar <strong>geçen yılların</strong>{" "}
|
||||
yerleştirme sonuçlarına dayanır. Kontenjanlar, tercih davranışları
|
||||
ve puanlar her yıl değişir; geçen yıl “garanti”
|
||||
ve puanlar her yıl değişir; geçen yıl “güvenli”
|
||||
görünen bir bölüm bu yıl dolabilir. Bu yüzden satırlarda son beş
|
||||
yılın taban seyrini de gösteririz — çizgi yukarı tırmanıyorsa
|
||||
bölüm zorlaşıyor demektir. KolayTercih karar <em>destek</em>{" "}
|
||||
|
||||
@@ -29,17 +29,14 @@ import {
|
||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||
import {
|
||||
DILIM_ETIKET,
|
||||
RISK_ETIKET,
|
||||
dilimdenFarkliRiskEtiketi,
|
||||
dilimdenRisk,
|
||||
riskHesapla,
|
||||
type RiskSeviyesi,
|
||||
} from "@/lib/risk";
|
||||
|
||||
export const DILIM_ETIKET: Record<string, string> = {
|
||||
hayal: "Hayal",
|
||||
dengeli: "Dengeli",
|
||||
garanti: "Garanti",
|
||||
};
|
||||
import type { Dilim } from "@/lib/rapor-havuzu";
|
||||
|
||||
// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli.
|
||||
// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket dar ekranda sr-only
|
||||
@@ -75,12 +72,16 @@ export function RaporSatirDetayi({
|
||||
trendOzeti: string;
|
||||
}) {
|
||||
const stil = RISK_STIL[risk];
|
||||
// Risk etiketi yalnızca dilimin varsayılanından farklıysa yazılır; yoksa
|
||||
// üçüncü dilimde başlık "Güvenli · Güvenli" olurdu (bkz. risk.ts).
|
||||
const farkliRisk = dilimdenFarkliRiskEtiketi(dilim, risk);
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
||||
<stil.icon className="size-3.5" aria-hidden />
|
||||
<span className="font-medium">
|
||||
{DILIM_ETIKET[dilim]} · {RISK_ETIKET[risk]}
|
||||
{DILIM_ETIKET[dilim as Dilim] ?? dilim}
|
||||
{farkliRisk ? ` · ${farkliRisk}` : null}
|
||||
</span>
|
||||
{unitur ? <span>· {unitur}</span> : null}
|
||||
</div>
|
||||
@@ -345,7 +346,7 @@ export function RaporListesi({
|
||||
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
||||
Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu; listen{" "}
|
||||
{rapor.kapsam.dagilim.hayal} hayal · {rapor.kapsam.dagilim.dengeli}{" "}
|
||||
dengeli · {rapor.kapsam.dagilim.garanti} garanti olarak kuruldu. Daha
|
||||
dengeli · {rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu. Daha
|
||||
fazla seçenek için alan veya il filtreni genişletebilirsin.
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
@@ -2,16 +2,19 @@ import Image from "next/image";
|
||||
import { redirect } from "next/navigation";
|
||||
import { verifySession, getCurrentUser } from "@/lib/session";
|
||||
import { kullanicininRaporu } from "../rapor-queries";
|
||||
import { PUAN_TURLERI } from "@/types/yokatlas";
|
||||
import { PUAN_TURLERI, type DilimKey } from "@/types/yokatlas";
|
||||
import { DILIM_ETIKET, RISK_ETIKET, dilimdenRisk } from "@/lib/risk";
|
||||
import type { RaporParams, RaporSonuc } from "../types/rapor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { YazdirButonu } from "./yazdir-butonu";
|
||||
|
||||
const DILIM_LABEL: Record<string, string> = {
|
||||
hayal: "Hayal (Riskli)",
|
||||
dengeli: "Dengeli (Az riskli)",
|
||||
garanti: "Garanti (Güvenli)",
|
||||
};
|
||||
// Dilim sütunu: dilim adı + varsayılan riski. Üçüncü dilimde ikisi aynı
|
||||
// kelime ("Güvenli") olduğu için parantez düşer — "Güvenli (Güvenli)" yazmaz.
|
||||
function dilimLabel(dilim: DilimKey): string {
|
||||
const ad = DILIM_ETIKET[dilim] ?? dilim;
|
||||
const risk = RISK_ETIKET[dilimdenRisk(dilim)];
|
||||
return ad === risk ? ad : `${ad} (${risk})`;
|
||||
}
|
||||
|
||||
export async function RaporYazdir() {
|
||||
const session = await verifySession("/rapor/yazdir");
|
||||
@@ -50,7 +53,7 @@ export async function RaporYazdir() {
|
||||
Not: Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu;
|
||||
liste {rapor.kapsam.dagilim.hayal} hayal ·{" "}
|
||||
{rapor.kapsam.dagilim.dengeli} dengeli ·{" "}
|
||||
{rapor.kapsam.dagilim.garanti} garanti olarak kuruldu.
|
||||
{rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu.
|
||||
</p>
|
||||
) : null}
|
||||
</header>
|
||||
@@ -91,7 +94,7 @@ export async function RaporYazdir() {
|
||||
{p?.universite} · {p?.il ?? "—"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 pr-2">{DILIM_LABEL[t.dilim]}</td>
|
||||
<td className="py-2 pr-2">{dilimLabel(t.dilim)}</td>
|
||||
<td className="py-2 pr-2">{t.gerekce}</td>
|
||||
<td className="py-2 pr-2">{t.riskNotu}</td>
|
||||
<td className="py-2">{t.trendOzeti}</td>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function RevizyonKutusu({ kalanHak }: { kalanHak: number }) {
|
||||
onChange={(e) => setFeedback(e.target.value)}
|
||||
rows={2}
|
||||
maxLength={1000}
|
||||
placeholder="ör. İlk 5 tercihte daha fazla İstanbul olsun, garanti dilimini genişlet…"
|
||||
placeholder="ör. İlk 5 tercihte daha fazla İstanbul olsun, güvenli dilimi genişlet…"
|
||||
disabled={pending}
|
||||
className="w-full rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import {
|
||||
PUAN_TURU_ETIKET,
|
||||
UNIVERSITE_TIPI_ETIKET,
|
||||
oncelikNormalize,
|
||||
type TercihProfili,
|
||||
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import type { RaporParams } from "./types/rapor";
|
||||
@@ -69,7 +70,9 @@ export function tercihFarki(
|
||||
) {
|
||||
farklar.push("universiteTipi");
|
||||
}
|
||||
if (kumeFarkli(params.oncelikler, s.oncelikler)) farklar.push("oncelikler");
|
||||
if (kumeFarkli(params.oncelikler?.map(oncelikNormalize), s.oncelikler)) {
|
||||
farklar.push("oncelikler");
|
||||
}
|
||||
|
||||
return farklar;
|
||||
}
|
||||
@@ -107,7 +110,8 @@ export function farkSatirlari(
|
||||
yeni: UNIVERSITE_TIPI_ETIKET[s.universiteTipi],
|
||||
};
|
||||
case "oncelikler":
|
||||
return { ...ortak, eski: liste(params.oncelikler), yeni: liste(s.oncelikler) };
|
||||
return { ...ortak, eski: liste(params.oncelikler?.map(oncelikNormalize)),
|
||||
yeni: liste(s.oncelikler), };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
// sayfa paketine girmez.
|
||||
|
||||
import { useEffect, useId, useRef, useState } from "react";
|
||||
import localFont from "next/font/local";
|
||||
import { Patrick_Hand } from "next/font/google";
|
||||
|
||||
// Şemanın "el çizimi" görünümüne uyan el yazısı fontu (Sweet Cucumber
|
||||
// Mocktail, Misti's Fonts). Türkçe glifler doğrulandı (ş, ğ, ı, İ… hepsi
|
||||
// var). DİKKAT: lisans "kişisel kullanım için ücretsiz" — canlıya çıkmadan
|
||||
// mistifonts.com/sweet-cucumber-mocktail adresinden ticari lisans alınmalı.
|
||||
const elYazisi = localFont({
|
||||
src: "../../../fonts/SweetCucumberMocktail.woff2",
|
||||
// Şemanın "el çizimi" görünümüne uyan el yazısı fontu. Patrick Hand, SIL Open
|
||||
// Font License ile ticari kullanıma açık; Türkçe glifler (ş, ğ, ı, İ…)
|
||||
// latin-ext alt kümesinde.
|
||||
const elYazisi = Patrick_Hand({
|
||||
weight: "400",
|
||||
subsets: ["latin", "latin-ext"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ export function ElCizimiSema({
|
||||
lineColor: "#94a3b8",
|
||||
edgeLabelBackground: "#ffffff",
|
||||
fontFamily: `${elYazisi.style.fontFamily}, cursive`,
|
||||
fontSize: "17px", // script fontun x-yüksekliği düşük; okunurluk için büyütüldü
|
||||
fontSize: "16px",
|
||||
},
|
||||
});
|
||||
const sonuc = await mermaid.render(semaId, tanim);
|
||||
|
||||
@@ -60,8 +60,19 @@ const KANITLAR = [
|
||||
* Profilsiz CTA adayı ana sayfaya göndermez: buton alttan bir çekmece açar,
|
||||
* sıralama girişi ve sihirbaz adayın bulunduğu sayfanın bağlamında akar.
|
||||
* Tasarım gerekçesi için bkz. docs/tasarim/cta-cekmece-tasarimi.md.
|
||||
*
|
||||
* `baslik` iki kartın (profilli/profilsiz) başlığını, `aciklama` yalnız
|
||||
* profilsiz karttaki paragrafı değiştirir — katalog sayfaları kartı üstündeki
|
||||
* tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar
|
||||
* yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler.
|
||||
*/
|
||||
export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
export function CtaSiraForm({
|
||||
baslik,
|
||||
aciklama,
|
||||
}: {
|
||||
baslik?: string;
|
||||
aciklama?: string;
|
||||
}) {
|
||||
const profil = useTercihProfili();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -146,11 +157,11 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8">
|
||||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
||||
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
|
||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
Sıralaman kayıtlı ({profil.sira.toLocaleString("tr-TR")}. sıra) —
|
||||
gerçek YÖK Atlas verisiyle hayal, dengeli ve garanti dilimlerindeki
|
||||
gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
|
||||
programların hazır.
|
||||
</p>
|
||||
<Link
|
||||
@@ -170,12 +181,11 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8"
|
||||
>
|
||||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
||||
{baslik ?? "Sıralamanla nereye yerleşirsin? Saniyede gör."}
|
||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun
|
||||
hayal, dengeli ve garanti dilimlerindeki programları anında listele.
|
||||
Ücretsiz, üyelik gerekmez.
|
||||
{aciklama ??
|
||||
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -190,12 +200,11 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
|
||||
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
|
||||
<DrawerContent
|
||||
data-lenis-prevent
|
||||
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
||||
>
|
||||
{/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */}
|
||||
<DrawerTitle className="sr-only">
|
||||
Sıralamanla nereye yerleşirsin?
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</DrawerTitle>
|
||||
<DrawerDescription className="sr-only">
|
||||
YKS başarı sıralamanı gir; sıralamana uygun programları bu sayfadan
|
||||
@@ -207,7 +216,6 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
</DrawerClose>
|
||||
|
||||
<div
|
||||
data-lenis-prevent
|
||||
className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"
|
||||
>
|
||||
{adim === "sira" ? (
|
||||
@@ -215,11 +223,11 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
|
||||
<div>
|
||||
<SectionEyebrow>Saniyede gör</SectionEyebrow>
|
||||
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900">
|
||||
Sıralamanla nereye yerleşirsin?
|
||||
Sıralamana yakın programlar hangileri?
|
||||
</h2>
|
||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600">
|
||||
Puan türünü seç, başarı sıralamanı yaz; hayal, dengeli ve
|
||||
garanti dilimlerindeki programları bu sayfadan ayrılmadan
|
||||
güvenli dilimlerindeki programları bu sayfadan ayrılmadan
|
||||
gör. Ücretsiz, üyelik gerekmez.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -322,7 +322,7 @@ export function SihirbazBolumu({
|
||||
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
|
||||
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
|
||||
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli
|
||||
ve garanti dilimlerine dağıtılmış 24 tercihi — her satırı
|
||||
ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı
|
||||
yerleşme riski ve gerekçesiyle — senin yerine dizer.
|
||||
</p>
|
||||
<Button
|
||||
|
||||
@@ -8,7 +8,7 @@ import { KATEGORILER } from "@/lib/kategoriler";
|
||||
import type { UniturGrubu } from "@/types/yokatlas";
|
||||
|
||||
export const ONCELIKLER = [
|
||||
"İş garantisi",
|
||||
"İş bulma kolaylığı",
|
||||
"Yüksek maaş potansiyeli",
|
||||
"Sevdiğim işi yapmak",
|
||||
"Akademik kariyer",
|
||||
@@ -108,11 +108,15 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
|
||||
const oncelikler = Array.isArray(p.oncelikler)
|
||||
? [
|
||||
...new Set(
|
||||
p.oncelikler.filter(
|
||||
(x): x is string =>
|
||||
typeof x === "string" &&
|
||||
(ONCELIKLER as readonly string[]).includes(x),
|
||||
),
|
||||
p.oncelikler
|
||||
// Eski kayıtlı profiller: çip adı "İş garantisi" idi (vaat diliyle
|
||||
// çeliştiği için değişti); seçim kaybolmasın diye yeni ada eşlenir.
|
||||
.map(oncelikNormalize)
|
||||
.filter(
|
||||
(x): x is string =>
|
||||
typeof x === "string" &&
|
||||
(ONCELIKLER as readonly string[]).includes(x),
|
||||
),
|
||||
),
|
||||
]
|
||||
: [];
|
||||
@@ -120,6 +124,15 @@ export function sihirbazDogrula(girdi: unknown): SihirbazSecimleri | null {
|
||||
return { kategoriler, iller, universiteTipi, oncelikler };
|
||||
}
|
||||
|
||||
/**
|
||||
* Eski çip adı "İş garantisi" → "İş bulma kolaylığı". Kayıtlı profil ve eski
|
||||
* rapor parametreleri eski adı taşıyabilir; karşılaştırma/gösterim öncesi
|
||||
* yeni ada eşlenir ki ad değişikliği "tercihlerin değişmiş" diye okunmasın.
|
||||
*/
|
||||
export function oncelikNormalize<T>(x: T): T | string {
|
||||
return x === "İş garantisi" ? "İş bulma kolaylığı" : x;
|
||||
}
|
||||
|
||||
/** 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;
|
||||
|
||||
Binary file not shown.
@@ -18,7 +18,14 @@ import {
|
||||
type AdayProgram,
|
||||
} from "../rapor-havuzu";
|
||||
import { secimOzeti } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
import { RISK_ETIKET, dilimdenRisk, riskHesapla } from "../risk";
|
||||
import {
|
||||
DILIM_ETIKET,
|
||||
RISK_ETIKET,
|
||||
dilimdenRisk,
|
||||
riskHesapla,
|
||||
riskNotuMetni,
|
||||
} from "../risk";
|
||||
import { trendOzetiMetni } from "../trend";
|
||||
|
||||
// Yapay zekâdan istenen küçük şema (kullanıcı kararı, 6 Ağu 2026): riskNotu
|
||||
// ve trendOzeti gerçek sıra geçmişinden deterministik üretilir; dilim iskeleti
|
||||
@@ -71,6 +78,11 @@ KURALLAR (kesin):
|
||||
- METİN DİYETİ: aday ${toplam} satır okuyacak; gerekce TEK kısa cümle (en fazla ~15
|
||||
kelime, Türkçe): bu programın bu aday için neden mantıklı olduğu. Dolgu söz
|
||||
yok; ASLA "garanti", "kesin", "yüzde yüz" deme.
|
||||
- SÖZLÜK: "garanti" yalnızca üçüncü listenin teknik JSON alan adı ve havuzdaki
|
||||
"dilim" değeridir. Adaya dönük HİÇBİR metinde (gerekce, genelDegerlendirme,
|
||||
uyarilar) "garanti" kelimesini kullanma; bu dilimi adaya "güvenli dilim" /
|
||||
"güvenli tercihler" diye an. Aday ya da önceki liste özeti "güvenli dilim"
|
||||
derse kastedilen "garanti" listesidir.
|
||||
- Adayın notlarındaki şehir/bölüm tercihlerine saygı göster ama dengeyi bozma.
|
||||
- genelDegerlendirme: listenin genel stratejisi, EN FAZLA 2 cümle. Uyarıları
|
||||
buraya yazma.
|
||||
@@ -127,50 +139,19 @@ function normalizeEt(
|
||||
// geçmişinden hesaplanır; model bu alanlarda jenerik dolgu yazıyordu, kod
|
||||
// kesin rakam verir ("45B düzeyinden 38B düzeyine indi").
|
||||
|
||||
/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */
|
||||
function siraKisa(n: number): string {
|
||||
if (n >= 1_000_000) {
|
||||
return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`;
|
||||
}
|
||||
if (n >= 1000) return `${Math.round(n / 1000)}B`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function trendOzetiUret(p: AdayProgram): string {
|
||||
const ham: Array<[number, number | null | undefined]> = [
|
||||
[2021, p.sira2021],
|
||||
[2022, p.sira2022],
|
||||
[2023, p.sira2023],
|
||||
[2024, p.sira2024],
|
||||
[2025, p.sira2025],
|
||||
];
|
||||
const seri = ham.filter((cift): cift is [number, number] => cift[1] != null);
|
||||
if (seri.length < 2) {
|
||||
return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku.";
|
||||
}
|
||||
const [ilkYil, ilkSira] = seri[0];
|
||||
const [sonYil, sonSira] = seri[seri.length - 1];
|
||||
const oran = (sonSira - ilkSira) / ilkSira;
|
||||
if (oran <= -0.1) {
|
||||
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; bölüm zorlaşıyor.`;
|
||||
}
|
||||
if (oran >= 0.1) {
|
||||
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; yerleşmek görece kolaylaşıyor.`;
|
||||
}
|
||||
return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`;
|
||||
// Cümle tadımlıkla ortak saf fonksiyondan gelir (lib/trend.ts)
|
||||
return trendOzetiMetni([
|
||||
p.sira2021,
|
||||
p.sira2022,
|
||||
p.sira2023,
|
||||
p.sira2024,
|
||||
p.sira2025,
|
||||
]);
|
||||
}
|
||||
|
||||
function riskNotuUret(p: AdayProgram, adaySira: number): string {
|
||||
switch (riskHesapla(p.sira2025 ?? p.sira2024, adaySira)) {
|
||||
case "guvenli":
|
||||
return "Taban sıralaması senin sıranın belirgin altında; yüksek ihtimalle yerleşirsin.";
|
||||
case "az-riskli":
|
||||
return "Taban sıralaması sırana yakın; orta ihtimalle yerleşilebilir.";
|
||||
case "riskli":
|
||||
return "Taban sıralaması senin sıranın üstünde kapandı; yerleşme ihtimali düşük.";
|
||||
default:
|
||||
return "Güncel taban sıralaması verisi yok; ihtimal öngörülemez, temkinli yaklaş.";
|
||||
}
|
||||
return riskNotuMetni(riskHesapla(p.sira2025 ?? p.sira2024, adaySira));
|
||||
}
|
||||
|
||||
/** Havuz programını render için gereken bilgiye indirger. */
|
||||
@@ -234,12 +215,12 @@ function mockRapor(
|
||||
tercihler,
|
||||
genelDegerlendirme: `(Dev mock — Yapay Zeka anahtarı tanımlı değil, bu rapor gerçek Yapay Zeka çıktısı değildir.) ${params.sira.toLocaleString(
|
||||
"tr-TR",
|
||||
)}. sıra için hayal/dengeli/garanti dağılımlı ${toplamHedef} tercihlik örnek liste kuruldu.${
|
||||
)}. sıra için hayal/dengeli/güvenli dağılımlı ${toplamHedef} tercihlik örnek liste kuruldu.${
|
||||
revizyonNotu ? ` Revizyon isteğin not alındı: "${revizyonNotu}".` : ""
|
||||
}`,
|
||||
uyarilar: [
|
||||
"(Dev mock) Tercih dönemi kapanmadan listeyi ÖSYM sistemine kendin gir.",
|
||||
"(Dev mock) Garanti dilimindeki son tercihleri asla silme.",
|
||||
"(Dev mock) Güvenli dilimindeki son tercihleri asla silme.",
|
||||
],
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
@@ -431,7 +412,7 @@ export async function raporUret(
|
||||
tercihler,
|
||||
genelDegerlendirme:
|
||||
genel.trim() ||
|
||||
`Listen ${toplamHedef} tercihten oluşuyor; hayal, dengeli ve garanti dilimleri sıralamana göre dengelendi.`,
|
||||
`Listen ${toplamHedef} tercihten oluşuyor; hayal, dengeli ve güvenli dilimleri sıralamana göre dengelendi.`,
|
||||
uyarilar: uyarilar.length > 0 ? uyarilar : undefined,
|
||||
programlar,
|
||||
kapsam: kapsamUret(hedef),
|
||||
@@ -449,7 +430,10 @@ export function listeOzetiCikar(rapor: RaporSonuc, adaySira?: number): string {
|
||||
p?.efektifSira != null
|
||||
? ` (taban sırası ~${p.efektifSira.toLocaleString("tr-TR")})`
|
||||
: "";
|
||||
return `${t.sira}. [${t.dilim} | ${RISK_ETIKET[risk]}] ${p?.isim ?? "?"} — ${p?.universite ?? "?"}${taban}`;
|
||||
// Modele ham `garanti` anahtarı değil görünen dilim adı gider; yoksa
|
||||
// sohbet cevaplarına "garanti dilimi" sızıyor. Dilim ve risk aynı kelime
|
||||
// olabildiği için ("Güvenli") alanlar adlandırılır.
|
||||
return `${t.sira}. [dilim: ${DILIM_ETIKET[t.dilim] ?? t.dilim} | risk: ${RISK_ETIKET[risk]}] ${p?.isim ?? "?"} — ${p?.universite ?? "?"}${taban}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import "server-only";
|
||||
/**
|
||||
* Rybbit'e sunucudan olay gönderimi (POST /api/track). Yalnızca sunucu
|
||||
* tarafından çağrılır (lib/odeme.ts) — anahtar NEXT_PUBLIC_ değil.
|
||||
* tarafından çağrılır (lib/odeme.ts, lib/auth.ts) — anahtar NEXT_PUBLIC_ değil.
|
||||
*
|
||||
* Neden sunucu tarafı: ödeme dönüşümü tarayıcıdan gönderilirse reklam
|
||||
* engelleyiciler ve iyzico dönüşünde kapatılan sekmeler yüzünden kaybolur.
|
||||
@@ -10,11 +10,13 @@ import "server-only";
|
||||
* Env yoksa (dev, self-host anahtarı tanımsız) hiçbir istek yapılmaz.
|
||||
*/
|
||||
|
||||
const HOST = process.env.RYBBIT_HOST;
|
||||
// Sondaki "/" atılır: secret "https://…/" girilirse `//api/track` 404 olup
|
||||
// olaylar sessizce kaybolmasın.
|
||||
const HOST = process.env.RYBBIT_HOST?.replace(/\/+$/, "");
|
||||
const SITE_ID = process.env.RYBBIT_SITE_ID;
|
||||
const API_KEY = process.env.RYBBIT_API_KEY;
|
||||
|
||||
export type SunucuOlayAdi = "odeme_tamamlandi";
|
||||
export type SunucuOlayAdi = "odeme_tamamlandi" | "giris_basarili";
|
||||
|
||||
export async function sunucuOlayi(
|
||||
ad: SunucuOlayAdi,
|
||||
|
||||
@@ -2,10 +2,11 @@ import "server-only";
|
||||
import { betterAuth } from "better-auth";
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { magicLink } from "better-auth/plugins";
|
||||
import { sunucuOlayi } from "./analitik-sunucu";
|
||||
import { appDb, schema } from "./appdb";
|
||||
import { grantCredits, DENEME_KREDISI } from "./credits";
|
||||
import { magicLinkEpostasi } from "./eposta";
|
||||
import { epostaGonder } from "./eposta-gonder";
|
||||
import { epostaGonder, epostaKonsolModu } from "./eposta-gonder";
|
||||
|
||||
/**
|
||||
* Verify URL'ini maile konacak onay sayfası URL'ine çevirir. Doğrulama linki
|
||||
@@ -22,7 +23,67 @@ function onaySayfasiUrl(verifyUrl: string): string {
|
||||
return hedef.toString();
|
||||
}
|
||||
|
||||
/** Aynı adrese bu süre içinde en çok bir giriş e-postası gider. */
|
||||
const GIRIS_SOGUMA_MS = 60_000;
|
||||
|
||||
/**
|
||||
* Adres → son gönderim zamanı. Süreç içi Map: tek konteyner çalışıyoruz,
|
||||
* şema/DB yazımı gerektirmez; yeniden başlatmada sıfırlanır (ikinci katman
|
||||
* better-auth'un IP limiti). `verification` tablosundan türetilmez — orada
|
||||
* identifier token'dır ve satır sendMagicLink'ten ÖNCE yazılır. globalThis:
|
||||
* modül birden çok kez yüklense de tek kayıt (appdb/index.ts deseni).
|
||||
*/
|
||||
function girisSogumaKaydi(): Map<string, number> {
|
||||
const g = globalThis as { __girisEpostaSon?: Map<string, number> };
|
||||
return (g.__girisEpostaSon ??= new Map());
|
||||
}
|
||||
|
||||
function sogumaAnahtari(email: string): string {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adres soğumada değilse pencereyi başlatır ve `true` döner; soğumadaysa
|
||||
* `false` (pencere kaymaz — süre ilk gönderimden sayılır). Her çağrıda süresi
|
||||
* dolmuş kayıtlar budanır, Map büyümez.
|
||||
*/
|
||||
export function girisSogumasiAl(email: string, simdi = Date.now()): boolean {
|
||||
const kayit = girisSogumaKaydi();
|
||||
for (const [adres, zaman] of kayit) {
|
||||
if (simdi - zaman >= GIRIS_SOGUMA_MS) kayit.delete(adres);
|
||||
}
|
||||
const anahtar = sogumaAnahtari(email);
|
||||
if (kayit.has(anahtar)) return false;
|
||||
kayit.set(anahtar, simdi);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Gönderim başarısızsa pencereyi geri verir — kullanıcı hemen yeniden deneyebilsin. */
|
||||
export function girisSogumasiBirak(email: string): void {
|
||||
girisSogumaKaydi().delete(sogumaAnahtari(email));
|
||||
}
|
||||
|
||||
async function sendMagicLinkEmail(email: string, url: string) {
|
||||
// Soğuma yalnızca prod'da: dev paneli "anında giriş" aynı adrese art arda
|
||||
// istek atıyor. Konsol modunda da işler (lokal docker testi görsün).
|
||||
const sogumaAcik = process.env.NODE_ENV === "production";
|
||||
if (sogumaAcik && !girisSogumasiAl(email)) {
|
||||
// Sessizce çık: istek 200 döner, form "gönderildi" gösterir, mail çıkmaz —
|
||||
// üçüncü kişiye mail bombardımanı ve Resend kotası yakılması engellenir.
|
||||
console.info(
|
||||
"[giris] Soğuma penceresinde tekrar istek — e-posta gönderilmedi.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await girisLinkiniIlet(email, url);
|
||||
} catch (err) {
|
||||
if (sogumaAcik) girisSogumasiBirak(email);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function girisLinkiniIlet(email: string, url: string) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// Dev paneli "anında giriş" için son üretilen linki yakalar (bkz.
|
||||
// src/lib/dev/actions.ts) — panel onay adımını atlayıp doğrudan verify
|
||||
@@ -30,13 +91,15 @@ async function sendMagicLinkEmail(email: string, url: string) {
|
||||
(globalThis as { __sonMagicLink?: string }).__sonMagicLink = url;
|
||||
}
|
||||
const mailLinki = onaySayfasiUrl(url);
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
// Dev fallback: Resend anahtarı yoksa linki terminale yaz
|
||||
if (epostaKonsolModu()) {
|
||||
// Konsol modu (dev ya da EPOSTA_KONSOL_FALLBACK=1): linki terminale yaz.
|
||||
// Prod'da anahtar yoksa buraya girilmez; epostaGonder hata fırlatır —
|
||||
// token'lı link prod loglarına düşmez.
|
||||
console.log(`\n[giris] Magic link for ${email}:\n${mailLinki}\n`);
|
||||
return;
|
||||
}
|
||||
// Gönderim hatası better-auth'a fırlatılır — aksi halde kullanıcı
|
||||
// "gönderildi" ekranını görür ama mail hiç çıkmaz.
|
||||
// Gönderim/yapılandırma hatası better-auth'a fırlatılır — aksi halde
|
||||
// kullanıcı "gönderildi" ekranını görür ama mail hiç çıkmaz.
|
||||
await epostaGonder(email, magicLinkEpostasi(mailLinki));
|
||||
}
|
||||
|
||||
@@ -95,6 +158,28 @@ export const auth = betterAuth({
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
create: {
|
||||
after: async (session, ctx) => {
|
||||
// Huninin son adımı: oturum gerçekten açıldı. Sunucudan gider —
|
||||
// reklam engelleyici ve kapanan sekme kaybettirmez. `await` YOK:
|
||||
// Rybbit'in 3 sn'lik timeout'u girişi geciktirmesin (sunucuOlayi
|
||||
// hata fırlatmaz). E-posta adresi özelliklere girmez.
|
||||
const yol = ctx?.path ?? "";
|
||||
void sunucuOlayi("giris_basarili", {
|
||||
kullaniciId: session.userId,
|
||||
pathname: "/giris",
|
||||
ozellikler: {
|
||||
yontem: yol.startsWith("/magic-link")
|
||||
? "eposta"
|
||||
: yol.startsWith("/callback")
|
||||
? "google"
|
||||
: "diger",
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -4,23 +4,62 @@ import type { EpostaIcerik } from "./eposta";
|
||||
/**
|
||||
* Ortak e-posta göndericisi (Resend). Şablonlar eposta.ts'te, gönderim
|
||||
* burada — magic link (auth.ts) ve kredi hatırlatması aynı yoldan çıkar.
|
||||
* RESEND_API_KEY yoksa dev fallback: içerik terminale yazılır.
|
||||
*
|
||||
* Env okumalarının tamamı ÇAĞRI ANINDA yapılır, modül seviyesinde değil:
|
||||
* `next build` NODE_ENV=production + env'siz koşar; modül yüklenirken hata
|
||||
* fırlatmak build'i kırar.
|
||||
*/
|
||||
|
||||
/** Maile yanıt verilirse düşeceği adres — mail.kolaytercih.com'un MX'i yok. */
|
||||
const VARSAYILAN_YANIT_ADRESI = "destek@kolaytercih.com";
|
||||
|
||||
/** Prod'da konsol fallback'ini bilinçli açan kaçış bayrağı (lokal docker testi). */
|
||||
function kacisBayragi(): boolean {
|
||||
return process.env.EPOSTA_KONSOL_FALLBACK === "1";
|
||||
}
|
||||
|
||||
/**
|
||||
* Konsol modu: gönderim yerine içerik terminale yazılır. Yalnızca Resend
|
||||
* anahtarı yokken ve (dev'de ya da EPOSTA_KONSOL_FALLBACK=1 ile) geçerlidir.
|
||||
* Production'da anahtar yoksa konsol moduna DÜŞÜLMEZ — giriş token'ı taşıyan
|
||||
* link prod loglarına yazılmasın, kullanıcı da "gönderildi" sanmasın diye
|
||||
* epostaGonder net hata fırlatır.
|
||||
*/
|
||||
export function epostaKonsolModu(): boolean {
|
||||
return (
|
||||
!process.env.RESEND_API_KEY &&
|
||||
(process.env.NODE_ENV !== "production" || kacisBayragi())
|
||||
);
|
||||
}
|
||||
|
||||
export async function epostaGonder(
|
||||
to: string,
|
||||
icerik: EpostaIcerik,
|
||||
): Promise<void> {
|
||||
if (!process.env.RESEND_API_KEY) {
|
||||
if (epostaKonsolModu()) {
|
||||
console.log(
|
||||
`\n[eposta] (dev) ${to} adresine gönderilecekti — ${icerik.subject}\n${icerik.text}\n`,
|
||||
`\n[eposta] (konsol modu) ${to} adresine gönderilecekti — ${icerik.subject}\n${icerik.text}\n`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const anahtar = process.env.RESEND_API_KEY;
|
||||
if (!anahtar) {
|
||||
throw new Error("[eposta] RESEND_API_KEY tanımlı değil");
|
||||
}
|
||||
// EMAIL_FROM yoksa resend.dev adresi yalnızca Resend hesap sahibine teslim
|
||||
// eder — prod'da sessizce buna düşmek yerine yapılandırma hatasını göster.
|
||||
const gonderen = process.env.EMAIL_FROM;
|
||||
if (!gonderen && process.env.NODE_ENV === "production" && !kacisBayragi()) {
|
||||
throw new Error("[eposta] EMAIL_FROM tanımlı değil");
|
||||
}
|
||||
const { Resend } = await import("resend");
|
||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
||||
const resend = new Resend(anahtar);
|
||||
const { error } = await resend.emails.send({
|
||||
from: process.env.EMAIL_FROM ?? "KolayTercih <onboarding@resend.dev>",
|
||||
from: gonderen || "KolayTercih <onboarding@resend.dev>",
|
||||
to,
|
||||
// `||`: CI'da secret tanımsızsa env boş string gelir; boş Reply-To ile
|
||||
// Resend'e gitmek yerine varsayılana düş.
|
||||
replyTo: process.env.EMAIL_REPLY_TO || VARSAYILAN_YANIT_ADRESI,
|
||||
subject: icerik.subject,
|
||||
html: icerik.html,
|
||||
// Düz metin alternatifi spam puanını düşürür ve HTML'i engelleyen
|
||||
|
||||
@@ -6,8 +6,9 @@ import "server-only";
|
||||
* desteğini güvenilir biçimde sunmaz — bu yüzden düzen tablo tabanlı ve tüm
|
||||
* stiller satır içi. Logo işareti sitedeki SVG'nin raster kopyası olan
|
||||
* `/kolay-tercih-mark.png` ile gösterilir (SVG'yi Gmail/Outlook engeller).
|
||||
* Web fontları (Bricolage Grotesque, Cal Sans) <link> ile progresif olarak
|
||||
* yüklenir; desteklemeyen istemciler sistem yazı tipine düşer.
|
||||
* Harici font <link>'i yok: çoğu istemci yok sayıyor, bazı spam filtreleri
|
||||
* harici kaynağı eksi puan sayıyor. Cal Sans / Bricolage Grotesque yalnızca
|
||||
* cihazda kuruluysa görünür; aksi hâlde sistem yazı tipine düşer.
|
||||
*/
|
||||
|
||||
import { SITE_URL } from "./seo";
|
||||
@@ -25,6 +26,11 @@ const BASLIK_TIPI = `'Cal Sans', ${YAZI_TIPI}`;
|
||||
/** Sitedeki kelime markası Bricolage Grotesque (font-bricolage). */
|
||||
const MARKA_TIPI = `'Bricolage Grotesque', ${YAZI_TIPI}`;
|
||||
|
||||
/** Alt bilgideki iletişim satırı — HTML footer ve düz metin gövdeler ortak kullanır. */
|
||||
const SITE_ALANI = "kolaytercih.com";
|
||||
const DESTEK_EPOSTA = "destek@kolaytercih.com";
|
||||
const METIN_ALT_BILGI = ["", "—", `${SITE_ALANI} · ${DESTEK_EPOSTA}`];
|
||||
|
||||
export type EpostaIcerik = {
|
||||
subject: string;
|
||||
html: string;
|
||||
@@ -76,8 +82,6 @@ function kabuk({
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>${kacir(baslikEtiketi)}</title>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,300;12..96,600&family=Cal+Sans&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background-color:${ZEMIN};">
|
||||
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;font-size:1px;line-height:1px;">${onizleme}</div>
|
||||
@@ -96,7 +100,8 @@ function kabuk({
|
||||
<tr>
|
||||
<td style="padding:20px 8px 0;font-family:${YAZI_TIPI};font-size:12px;line-height:1.6;color:#94a3b8;">
|
||||
KolayTercih · YKS tercih listeni dakikalar içinde hazırla<br>
|
||||
Bu e-postayı hesabınla ilgili bir işlem yaptığın için aldın.
|
||||
Bu e-postayı hesabınla ilgili bir işlem yaptığın için aldın.<br>
|
||||
<a href="https://${SITE_ALANI}" style="color:#94a3b8;">${SITE_ALANI}</a> · <a href="mailto:${DESTEK_EPOSTA}" style="color:#94a3b8;">${DESTEK_EPOSTA}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -155,6 +160,7 @@ export function magicLinkEpostasi(url: string): EpostaIcerik {
|
||||
"",
|
||||
"Bağlantı 15 dakika geçerli ve yalnızca bir kez kullanılabilir.",
|
||||
"Bu isteği sen yapmadıysan bu e-postayı yok sayabilirsin.",
|
||||
...METIN_ALT_BILGI,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
@@ -201,6 +207,7 @@ export function krediHatirlatmaEpostasi(): EpostaIcerik {
|
||||
"",
|
||||
"Listenin tamamını açmak ya da danışmana soru sormak istersen kredi",
|
||||
"yükleyebilirsin — listen ve sohbet geçmişin olduğu gibi durur.",
|
||||
...METIN_ALT_BILGI,
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,18 @@ export const RISK_ETIKET: Record<RiskSeviyesi, string> = {
|
||||
riskli: "Riskli",
|
||||
};
|
||||
|
||||
/**
|
||||
* Dilimin kullanıcıya GÖRÜNEN adı — tek kaynak. Üçüncü dilimin iç anahtarı
|
||||
* (tip, DB değeri, API parametresi, mermaid classDef, analitik) `garanti`
|
||||
* olarak kalır; ekranda "Güvenli" yazar. Ham anahtarı kullanıcıya basma,
|
||||
* buradan oku.
|
||||
*/
|
||||
export const DILIM_ETIKET: Record<Dilim, string> = {
|
||||
hayal: "Hayal",
|
||||
dengeli: "Dengeli",
|
||||
garanti: "Güvenli",
|
||||
};
|
||||
|
||||
/** Harita pinlerinin SVG dolgu renkleri — risk renk dilinin oklch karşılığı. */
|
||||
export const RISK_PIN_RENK: Record<RiskSeviyesi, string> = {
|
||||
guvenli: "oklch(0.696 0.17 162.5)", // emerald-500
|
||||
@@ -42,3 +54,35 @@ export function dilimdenRisk(dilim: Dilim | string): RiskSeviyesi {
|
||||
if (dilim === "dengeli") return "az-riskli";
|
||||
return "riskli";
|
||||
}
|
||||
|
||||
/**
|
||||
* Dilim başlığının yanına yazılacak risk etiketi. Yalnızca satırın riski
|
||||
* dilimin varsayılanından FARKLIYSA döner; aynıysa null. Üçüncü dilimin adı
|
||||
* risk etiketiyle aynı kelime olduğu için aksi hâlde "Güvenli · Güvenli"
|
||||
* görünürdü.
|
||||
*/
|
||||
export function dilimdenFarkliRiskEtiketi(
|
||||
dilim: Dilim | string,
|
||||
risk: RiskSeviyesi,
|
||||
): string | null {
|
||||
return dilimdenRisk(dilim) === risk ? null : RISK_ETIKET[risk];
|
||||
}
|
||||
|
||||
/**
|
||||
* Risk seviyesinin tek cümlelik açıklaması (rapor satırları + tadımlık).
|
||||
* Ürün yerleşme olasılığı HESAPLAMAZ; yalnızca son taban sıralamasını adayın
|
||||
* sırasıyla kıyaslar. Bu yüzden metin olasılık/ihtimal/"yerleşirsin" demez.
|
||||
* "Geçen yıl" da denmez: efektif sıra 2025 yoksa 2024'e düşüyor.
|
||||
*/
|
||||
export function riskNotuMetni(risk: RiskSeviyesi | null): string {
|
||||
switch (risk) {
|
||||
case "guvenli":
|
||||
return "Son taban sıralaması senin sıranın belirgin altında; geçmiş veriye göre güvenli aralıkta.";
|
||||
case "az-riskli":
|
||||
return "Son taban sıralaması sırana yakın; küçük bir kayma sonucu değiştirebilir.";
|
||||
case "riskli":
|
||||
return "Son taban sıralaması senin sıranın üstünde kapandı; hayal tercihi olarak düşün.";
|
||||
default:
|
||||
return "Güncel taban verisi yok; bu satırı temkinli değerlendir.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { turkishSlugify } from "./slug";
|
||||
import type { PuanTuruKey } from "@/types/yokatlas";
|
||||
import type { Dilim } from "./rapor-havuzu";
|
||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||
import { dilimdenRisk, riskHesapla, riskNotuMetni } from "./risk";
|
||||
import { trendOzetiMetni } from "./trend";
|
||||
|
||||
/** Sihirbaz kategorisi seçilmemiş kullanıcı için kova×tür başına fallback satır. */
|
||||
export const GENEL_KATEGORI = "genel";
|
||||
@@ -48,6 +50,22 @@ export interface TadimlikSatir {
|
||||
|
||||
export type TadimlikTip = "genel" | "devlet" | "vakif";
|
||||
|
||||
/**
|
||||
* gerekce, satırın servis edilen TEK LLM metnidir (batch'te temsili sıraya
|
||||
* göre yazıldı). Ürün yerleşme olasılığı hesaplamaz; bu yüzden olasılık /
|
||||
* ihtimal / şans / garanti / kesin / yüzde / "yerleşirsin" diyen gerekçe
|
||||
* servis edilmez. Eski batch'in "son 4 yıl" kalıbı da takılır (veri 5 yılı
|
||||
* kapsıyor, bkz. veri-kapsami.ts). Kalıp küçük harfledir: metin önce Türkçe
|
||||
* kurallarıyla küçültülür ("İhtimal" → "ihtimal"; düz /i bayrağı İ/ı'yı
|
||||
* eşlemez).
|
||||
*/
|
||||
const YASAK_GEREKCE_DILI =
|
||||
/olasılı|ihtimal|şans|garanti|kesin|yüzde|yerleşirsin|yerleşebil|son (4|dört) yıl/;
|
||||
|
||||
export function gerekceServisEdilebilir(gerekce: string): boolean {
|
||||
return !YASAK_GEREKCE_DILI.test(gerekce.toLocaleLowerCase("tr-TR"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Kova × tür × kategori × tip anahtarıyla tadımlık satırı seçer. Kategoriye
|
||||
* özel satır yoksa "genel" kategoriye düşer; o da yoksa null — çağıran taraf
|
||||
@@ -55,6 +73,9 @@ export type TadimlikTip = "genel" | "devlet" | "vakif";
|
||||
*
|
||||
* Tip fallback'i YOKTUR: kullanıcı devlet/vakıf seçtiyse yanlış tipte örnek
|
||||
* asla gösterilmez — uygun satır yoksa tadımlık gizlenir.
|
||||
*
|
||||
* Gerekçesi dil filtresine takılan satır YOK sayılır: aynı sırayla "genel"
|
||||
* kategoriye düşülür, o da takılırsa null (tadımlık gizlenir).
|
||||
*/
|
||||
export async function tadimlikSec(
|
||||
sira: number,
|
||||
@@ -75,14 +96,23 @@ export async function tadimlikSec(
|
||||
eq(schema.tadimlikHavuzu.tip, tip),
|
||||
),
|
||||
});
|
||||
if (satir) {
|
||||
if (satir && gerekceServisEdilebilir(satir.gerekce)) {
|
||||
const program = satir.program as RaporSonuc["programlar"][string];
|
||||
return {
|
||||
programId: satir.programId,
|
||||
dilim: satir.dilim,
|
||||
// Servis edilen tek LLM metni; yukarıdaki dil filtresinden geçti.
|
||||
gerekce: satir.gerekce,
|
||||
riskNotu: satir.riskNotu,
|
||||
trendOzeti: satir.trendOzeti,
|
||||
program: satir.program as RaporSonuc["programlar"][string],
|
||||
// DB'deki riskNotu ve trendOzeti batch'te LLM'in yazdığı cümlelerdir;
|
||||
// ikisi de servis EDİLMEZ. Risk notu, rozetle (tadimlik-satiri.tsx)
|
||||
// aynı formülden adayın gerçek sırasına göre; trend, rapordakiyle aynı
|
||||
// saf fonksiyondan programın sıra geçmişinden okuma anında üretilir —
|
||||
// metin rozetle çelişemez, "son 4 yıl" gibi bayat kalıp taşınmaz.
|
||||
riskNotu: riskNotuMetni(
|
||||
riskHesapla(program.efektifSira, sira) ?? dilimdenRisk(satir.dilim),
|
||||
),
|
||||
trendOzeti: trendOzetiMetni(program.siraGecmisi),
|
||||
program,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { kategoriEslesir } from "./kategoriler";
|
||||
import { riskHesapla, type RiskSeviyesi } from "./risk";
|
||||
import { DILIM_ETIKET, riskHesapla, type RiskSeviyesi } from "./risk";
|
||||
import type { TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||
|
||||
export type DegerlendirilecekProgram = {
|
||||
@@ -11,7 +11,8 @@ export type DegerlendirilecekProgram = {
|
||||
|
||||
export type TercihDegerlendirmesi = {
|
||||
risk: RiskSeviyesi | null;
|
||||
riskEtiketi: "Hayal" | "Dengeli" | "Daha güvenli" | "Veri yetersiz";
|
||||
/** Görünen dilim adı (DILIM_ETIKET) ya da "Veri yetersiz". */
|
||||
riskEtiketi: string;
|
||||
riskMetni: string;
|
||||
olumlu: string[];
|
||||
celiskiler: string[];
|
||||
@@ -65,11 +66,11 @@ export function tercihDegerlendir(
|
||||
|
||||
const riskEtiketi =
|
||||
risk === "riskli"
|
||||
? "Hayal"
|
||||
? DILIM_ETIKET.hayal
|
||||
: risk === "az-riskli"
|
||||
? "Dengeli"
|
||||
? DILIM_ETIKET.dengeli
|
||||
: risk === "guvenli"
|
||||
? "Daha güvenli"
|
||||
? DILIM_ETIKET.garanti
|
||||
: "Veri yetersiz";
|
||||
const riskMetni =
|
||||
risk === "riskli"
|
||||
|
||||
51
src/lib/trend.ts
Normal file
51
src/lib/trend.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Deterministik trend cümlesi: rapor satırları (lib/ai/rapor.ts) ve girişsiz
|
||||
// tadımlık (lib/tadimlik-havuzu.ts) AYNI saf fonksiyondan okur. Cümle gerçek
|
||||
// YÖK Atlas sıra geçmişinden kesin rakamla kurulur ("45B düzeyinden 38B
|
||||
// düzeyine indi"); yıl aralığı sabit bir "son N yıl" kalıbından değil,
|
||||
// serideki dolu yıllardan gelir.
|
||||
|
||||
/**
|
||||
* Sıra geçmişi dizisinin ilk elemanının yılı. Dizi her yerde 2021'den başlar
|
||||
* (bkz. programBilgisi, raporSiraSerisi); yeni veri yılı sona eklenir, bu
|
||||
* çapa değişmez.
|
||||
*/
|
||||
const ILK_VERI_YILI = 2021;
|
||||
|
||||
/** 45000 -> "45B", 1_200_000 -> "1,2M" (şemadaki öğrenci dili). */
|
||||
function siraKisa(n: number): string {
|
||||
if (n >= 1_000_000) {
|
||||
return `${(n / 1_000_000).toLocaleString("tr-TR", { maximumFractionDigits: 1 })}M`;
|
||||
}
|
||||
if (n >= 1000) return `${Math.round(n / 1000)}B`;
|
||||
return String(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* [2021..] taban sıralaması dizisinden tek cümlelik trend özeti.
|
||||
*
|
||||
* Cümle yalnızca OLANI betimler: ilk ve son dolu yılın tabanını kıyaslar
|
||||
* (±%10 eşiği). "Zorlaşıyor / kolaylaşıyor" gibi geleceğe dönük yargı
|
||||
* kurmaz — iki uç yılın kıyası aradaki seyri ve gelecek yılı söylemez.
|
||||
* Küçülen sıra = taban sıkılaştı, büyüyen sıra = taban gevşedi.
|
||||
*/
|
||||
export function trendOzetiMetni(
|
||||
gecmis: readonly (number | null | undefined)[] | undefined,
|
||||
): string {
|
||||
const seri: Array<[number, number]> = [];
|
||||
(gecmis ?? []).forEach((sira, i) => {
|
||||
if (sira != null) seri.push([ILK_VERI_YILI + i, sira]);
|
||||
});
|
||||
if (seri.length < 2) {
|
||||
return "Karşılaştırma için yeterli geçmiş yıl verisi yok; trendi temkinli oku.";
|
||||
}
|
||||
const [ilkYil, ilkSira] = seri[0];
|
||||
const [sonYil, sonSira] = seri[seri.length - 1];
|
||||
const oran = (sonSira - ilkSira) / ilkSira;
|
||||
if (oran <= -0.1) {
|
||||
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine indi; taban sıkılaştı.`;
|
||||
}
|
||||
if (oran >= 0.1) {
|
||||
return `${ilkYil}-${sonYil} arasında taban ${siraKisa(ilkSira)} düzeyinden ${siraKisa(sonSira)} düzeyine çıktı; taban gevşedi.`;
|
||||
}
|
||||
return `${ilkYil}-${sonYil} arasında taban sıralaması ${siraKisa(sonSira)} düzeyinde stabil seyretti.`;
|
||||
}
|
||||
6
src/lib/veri-kapsami.ts
Normal file
6
src/lib/veri-kapsami.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Taban sıralaması geçmişinin kapsadığı yıl sayısı. Veritabanında
|
||||
* sira2021–sira2025 kolonları var (5 yıl); "son N yıl" diyen tüm metinler bu
|
||||
* sabitten okur. Yeni veri yılı eklenince (ör. sira2026) burayı güncelle.
|
||||
*/
|
||||
export const VERI_YIL_SAYISI = 5;
|
||||
Reference in New Issue
Block a user