İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 3 · Ders 25

Blok 3 projesi: şampiyon ansiklopedisi

Arama, filtre, favoriler ve detay görünümü olan duyarlı bir web uygulaması yazıp GitHub Pages ile internete koyuyorsun.

  • 180–240 dk
  • Önce: Ders 24
  • Proje dersi

Bu dersin sonunda

  • Kod yazmaya başlamadan önce bir web uygulamasını özellik listesi ve tel kafes taslağıyla planlayabilirsin
  • Kodu <script type="module"> ile tarayıcıda çalışan modüllere bölüp birbirine bağlayabilirsin
  • Veriden kart listesi çizip arama, rol filtresi ve localStorage'da saklanan favoriler ekleyebilirsin
  • Adresteki ?id= değerini URLSearchParams ile okuyup ayrı bir detay görünümü gösterebilirsin
  • Sayfanı klavye, odak, alt metin, kontrast ve telefon ekranı için bir kontrol listesiyle denetleyebilirsin
  • Projeni GitHub Pages'te yayına alıp README'ye ekran görüntüsü ve canlı bağlantı ekleyebilirsin
Bu sayfada
  1. Önce plan: ne yapıyoruz?
  2. Klasör ve dosyalar
  3. Modüller tarayıcıda: type="module"
  4. favoriler.js: tarayıcıda saklamak
  5. arayuz.js: ekrana çizen modül
  6. uygulama.js: hepsini yöneten dosya
  7. Yükleniyor, hata ve boş durumlar
  8. Detay görünümü: adreste ?id=
  9. Erişilebilirlik ve mobil kontrol listesi
  10. Yayına almak: GitHub Pages
  11. README: ekran görüntüsü ve canlı bağlantı
  12. Görevler
  13. Kendini sına

Blok 3'ün sonuna geldin. Bu derste HTML, CSS, DOM, formlar, localStorage ve fetch derslerinde öğrendiklerini tek bir gerçek projede birleştiriyorsun. Ders 24'teki şampiyon listesi büyüyüp bir şampiyon ansiklopedisine dönüşecek: arama, rol filtresi, favoriler ve her şampiyon için ayrı bir sayfa. Sonunda onu GitHub Pages ile internete koyacak, bağlantısını istediğin kişiye gönderebileceksin.

Ders uzun; tek oturumda bitirmek zorunda değilsin. Proje adım adım büyüyor ve her büyük adımın sonunda sayfayı tarayıcıda açıp çalıştığını gördüğün bir kontrol noktası var. Bir kontrol noktası çalışmadan sonrakine geçme. Bitince sayfan şöyle görünecek:

127.0.0.1:5500/index.html
Şampiyon Ansiklopedisi'nin liste görünümü. Üstte koyu bir çubukta uygulamanın adı, altında Şampiyonlar başlığı, Ara kutusu, Rol seçimi ve Yalnız favoriler kutusu, Gösterilen: 173 / 173 yazısı ve dört sütunlu kart ızgarası. Kartlarda yuvarlak resim, şampiyonun adı, unvanı, rolleri ve sağ üst köşede yıldız var.
Bitmiş uygulamanın liste görünümü: arama kutusu, rol seçimi, yalnız favoriler kutusu, sayaç ve şampiyon kartları. Ahri'nin yıldızı dolu, yani favorilerde.

Önce plan: ne yapıyoruz?

Boş bir index.html açıp hemen yazmaya başlamak cazip gelir. Ama yarım saat sonra "arama kutusu nereye gelecekti, favoriler nerede saklanacaktı" diye düşünürken aynı kodu üçüncü kez baştan yazarsın. Ders 17'de oyun için yaptığın gibi önce ne yapacağını yazıya dökeceğiz. Bu sefer bir adım daha var: ekranın kaba bir çizimi.

Özellik listesi

Uygulamanın neler yapacağını madde madde yaz. Her maddenin yanına kullanıcıya ne kazandırdığını ve ne zaman yapılacağını ekle. Ders 17'deki en küçük çalışan sürüm fikri burada da geçerli: ilk sürüme neyin gireceğine baştan karar verirsin, gerisi sonraki sürümlerin işi.

ÖzellikKullanıcı için anlamıNe zaman
Şampiyon listesiBütün şampiyonları kart kart görürBu derste
AramaAd ya da unvan yazıp aradığını bulurBu derste
Rol filtresiYalnızca büyücüleri, tankları… görürBu derste
FavorilerYıldıza basar; sayfayı kapatsa da hatırlanırBu derste
Detay görünümüHikâye, yetenekler, ipuçları; her şampiyonun kendi adresi varBu derste
Yükleniyor, hata ve boş mesajlarıEkranda ne olduğunu her an bilirBu derste
Klavye ve telefon uyumuFaresiz de, telefondan da kullanırBu derste
Rastgele şampiyon, zorluk filtresi, sıralama, koyu tema, karşılaştırmaEkstralarGörevlerde

Elimizdeki veri

Veriyi Ders 24'te tanıdığın Data Dragon'dan alacağız. Beş adresten dördünü zaten kullandın: sürüm listesi, champion.json ve kare resim Ders 24'ten, tek şampiyonun dosyası Ders 15'teki "Şampiyonun unvanı" görevinden. Yeni olan yalnızca büyük resim:

NeAdres
Sürüm listesihttps://ddragon.leagueoflegends.com/api/versions.json
Bütün şampiyonlar, kısa bilgi…/cdn/<sürüm>/data/tr_TR/champion.json
Tek şampiyon: hikâye, yetenekler, ipuçları…/cdn/<sürüm>/data/tr_TR/champion/Ahri.json
Kare resim…/cdn/<sürüm>/img/champion/Ahri.png
Büyük resim…/cdn/img/champion/loading/Ahri_0.jpg

Listedeki her şampiyon şuna benzer bir obje. Projede kullanacağımız alanları bıraktık, gerisini kısalttık:

champion.json içinden Ahri (kısaltılmış)
{    "id": "Ahri",    "name": "Ahri",    "title": "Dokuz Kuyruklu Tilki",    "info": { "attack": 3, "defense": 4, "magic": 8, "difficulty": 5 },    "image": { "full": "Ahri.png" },    "tags": ["Mage", "Assassin"],    "stats": { "hp": 590, "armor": 21, "attackrange": 550, "movespeed": 330 }}

Tel kafes

Özellik listesi "ne"yi söyler, tel kafes "nerede"yi. Kâğıt kalem yeter; istersen Excalidraw gibi ücretsiz çizim sitelerini de kullanabilirsin. Önemli olan renk ya da yazı tipi değil, her parçanın yeri.

Liste görünümü Şampiyon Ansiklopedisi Şampiyonlar Ara… Rol ▾ Favoriler Gösterilen: 173 / 173 ☆ ★ ☆ 1 2 3 4 Detay görünümü (?id=Ahri) Şampiyon Ansiklopedisi ← Tüm şampiyonlar Ahri Dokuz Kuyruklu Tilki Büyücü · Suikastçı Hikâye Yetenekler 5 6 7 8 1Arama kutusu: ad ya da unvan 2Rol seçimi ve yalnız favoriler kutusu 3Kartın tamamı bağlantı: ?id=Ahri 4Favori yıldızı: ayrı bir düğme 5Listeye dönüş bağlantısı 678Resim, ölçüler, hikâye ve yetenekler
Şekil 25.1 Masaüstü tel kafes: liste ve detay görünümü. Numaralar alttaki açıklamalara karşılık geliyor.

Aynı iki ekranı bir de telefon için çiz. Telefonda yan yana duran şeyler alt alta iner; çizerken bunun nereleri etkilediğini görürsün.

Şampiyonlar Ara… Rol ▾ Yalnız favoriler 173 / 173 ★ ☆ Liste (telefon) Sığmayan filtre alt satıra iner Kartlar iki sütun Yıldız en az 44 px ← Tüm şampiyonlar Ahri Dokuz Kuyruklu Tilki Detay (telefon) Resim üstte, yazı altında Tek sütun: yatay kaydırma çubuğu yok
Şekil 25.2 Telefon tel kafesi: dar ekranda neyin nasıl değiştiği.

Taslağı çizerken üç karar verdik; kod yazarken bunlara uyacağız:

  • Tek sayfa, iki görünüm. Liste ve detay aynı index.html içinde duracak. Hangisinin görüneceğine adres karar verecek: adreste ?id=Ahri varsa detay, yoksa liste.
  • Kartın tamamı bir bağlantı, yıldız ayrı bir düğme. HTML bir bağlantının içine düğme koymana izin vermez; koyarsan yıldıza basınca hem favori değişir hem detay sayfası açılır. Bu yüzden yıldız kartın içinde ama bağlantının dışında.
  • Telefonda daha az sütun. Detay tek sütuna iner: resim üste çıkar, yazılar altına akar. Bunun için Ders 21'deki @media sorgusunu kullanacağız. Kart listesi ise ekran daraldıkça kendiliğinden iki sütuna iner.

Klasör ve dosyalar

Bu proje C:\kod\ogren'in içinde değil, kendi klasöründe duracak: GitHub Pages bir depoyu site olarak yayınlar, bu yüzden projenin ayrı bir deposu olmalı. VS Code'da File → Open Folder… ile C:\kod içinde ansiklopedi adında yeni bir klasör oluştur ve onu aç. Sonra Explorer'da New Folder ile js klasörünü, New File ile dosyaları oluştur. Bitince yapı şöyle olacak:

C:\kod\ansiklopedi
ansiklopedi/├── index.html        sayfanın iskeleti: iki görünüm, filtreler, mesaj alanı├── stil.css          görünüm: renkler, ızgara, telefon ayarları└── js/    ├── uygulama.js   yönetici: adresi okur, durumu tutar, olayları dinler    ├── api.js        Data Dragon'dan veri getirir    ├── arayuz.js     kartları ve detayı ekrana çizer    └── favoriler.js  favorileri localStorage'da saklar

Ders 24'teki uygulama.js tek dosyada yüz satır kadardı. Bu projenin JavaScript'i 300 satıra yaklaşacak, görevlerden sonra 400'ü geçecek; tek dosyada bir hatayı ararken her şeyi okumak zorunda kalırsın. Dosyalara böldüğünde "favoriler bozuldu" dediğin an nereye bakacağını bilirsin. Ders 14'te Node için öğrendiğin, Ders 17'de oyununda kullandığın modülleri şimdi tarayıcıda kullanacağız.

Ders boyunca Node'da küçük denemeler de yapacağız. Onları proje klasörüne değil, C:\kod\ogren\ders25.js dosyasına yaz; projenin deposuna deneme dosyası girmesin. VS Code'da File → New Text File ile boş bir dosya aç, File → Save As… ile C:\kod\ogren klasörüne ders25.js adıyla kaydet. Çalıştırmak için terminalde önce cd C:\kod\ogren, sonra node ders25.js yaz. Denemeler birbirinden bağımsız: her yenisine başlamadan dosyanın eski içeriğini sil, yoksa aynı adlı iki const yüzünden hata alırsın.

index.html

Önce iskelet. Aşağıdaki dosyayı yaz; altında önemli satırları tek tek açıklıyoruz.

index.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1">    <title>Şampiyon Ansiklopedisi</title>    <link rel="stylesheet" href="stil.css">    <script type="module" src="js/uygulama.js"></script></head><body>    <header class="ust">        <a class="logo" href="./">Şampiyon Ansiklopedisi</a>    </header>     <main>        <section id="liste-gorunumu" hidden>            <h1>Şampiyonlar</h1>            <form class="filtreler" id="filtreler" autocomplete="off">                <div class="alan">                    <label for="arama">Ara</label>                    <input id="arama" type="search" placeholder="Ad ya da unvan">                </div>                <div class="alan">                    <label for="rol">Rol</label>                    <select id="rol">                        <option value="">Hepsi</option>                        <option value="Assassin">Suikastçı</option>                        <option value="Fighter">Dövüşçü</option>                        <option value="Mage">Büyücü</option>                        <option value="Marksman">Nişancı</option>                        <option value="Support">Destek</option>                        <option value="Tank">Tank</option>                    </select>                </div>                <label class="onay">                    <input id="yalniz-favori" type="checkbox">                    Yalnız favoriler                </label>            </form>            <p class="sayac" id="sayac" aria-live="polite"></p>            <ul class="kartlar" id="liste"></ul>        </section>         <section id="detay-gorunumu" hidden>            <a class="geri" href="./">← Tüm şampiyonlar</a>            <article class="detay" id="detay" hidden>                <img class="detay-resim" id="detay-resim" alt="" width="308" height="560">                <div>                    <h1 id="detay-ad"></h1>                    <p class="detay-unvan" id="detay-unvan"></p>                    <p class="roller" id="detay-roller"></p>                    <dl class="olculer" id="detay-olculer"></dl>                    <h2>Hikâye</h2>                    <p id="detay-hikaye"></p>                    <h2>Yetenekler</h2>                    <ul id="detay-yetenekler"></ul>                    <h2>İpuçları</h2>                    <ul id="detay-ipuclari"></ul>                </div>            </article>        </section>         <p class="mesaj" id="mesaj" role="status">Yükleniyor…</p>    </main>     <footer class="alt">        <p>Veriler Riot Games'in Data Dragon servisinden gelir.</p>        <p lang="en">Şampiyon Ansiklopedisi was created under Riot Games' "Legal Jibber Jabber" policy using assets owned by Riot Games. Riot Games does not endorse or sponsor this project.</p>    </footer></body></html>
  • 8. satır: type="module" bu dosyanın bir modül olduğunu söyler; yalnızca böyle yüklenen dosyalar import ve export kullanabilir. Ders 22'de söz ettiğimiz gibi modül betikleri defer gibi davranır: tarayıcı önce HTML'in tamamını okur, sonra betiği çalıştırır. Bu yüzden <head> içinde durması sorun değil.
  • 16 ve 44. satırlar: İki görünümün ikisi de hidden (Ders 23'te Vazgeç düğmesini böyle gizlemiştin). Hangisinin açılacağına JavaScript adrese bakıp karar verecek.
  • Filtreler: Her girdi alanının bir <label for="…">'u var (Ders 19). Etikete tıklayınca kutu seçilir, ekran okuyucu da kutunun adını söyler.
  • 18. satır: autocomplete="off" tarayıcıya bu formdaki kutuları hatırlamamasını söyler: arama kutusunun altında eski aramalar öneri olarak çıkmaz. Daha önemli bir sebebi var; detay görünümünü yazdıktan sonra geri tuşunu denerken göreceksin.
  • 40. satır: aria-live="polite" Ders 24'teki durum yazısında da vardı: "bu yazı değişince sesli oku". Sayaç değiştikçe ekran okuyucu "Gösterilen: 12 / 173" der.
  • 52. satır: <dl> (description list, tanım listesi) "ad: değer" çiftleri için yapılmış bir liste. İçinde her çift bir <dt> (ad, "Saldırı") ve bir <dd> (değer, "3/10") olur. Ölçüleri JavaScript ile bu listeye dolduracağız.
  • 63. satır: Yükleniyor, hata ve boş mesajlarının hepsi bu paragrafta görünecek. role="status" da aria-live="polite" gibi çalışır: içerik değişince sesli okunur. İçindeki "Yükleniyor…" yazısı, JavaScript hiç çalışmazsa ekranda takılı kalır; bir şeylerin ters gittiğini hemen anlarsın.
  • 68. satır: Ders 24'te öğrendiğin Riot hayran projesi notu. lang="en" ekran okuyucuya bu cümleyi İngilizce okumasını söyler.

stil.css

Görünümü Ders 20 ve Ders 21'deki araçlarla yazıyoruz. Dosya uzun ama içindekilerin çoğunu tanıyorsun; yeni olanları altında anlatıyoruz. İstersen renkleri kendi zevkine göre değiştir.

stil.css
:root {    --zemin: #f4f5f9;    --kart: #ffffff;    --yazi: #1c1f2b;    --soluk: #596074;    --vurgu: #6b3fd4;    --cizgi: #d4d7e1;    --yildiz: #9a6700;    font-family: system-ui, "Segoe UI", sans-serif;} * {    box-sizing: border-box;} [hidden] {    display: none !important;} body {    margin: 0;    background: var(--zemin);    color: var(--yazi);    line-height: 1.5;} :focus-visible {    outline: 3px solid var(--vurgu);    outline-offset: 2px;} .ust {    padding: 14px 16px;    background: #1c1f2b;} .logo {    color: #ffffff;    font-weight: 700;    text-decoration: none;} main,.alt {    max-width: 1100px;    margin: 0 auto;    padding: 16px;} /* filtreler */.filtreler {    display: flex;    flex-wrap: wrap;    align-items: flex-end;    gap: 12px 16px;} .alan {    display: flex;    flex-direction: column;} #arama,#rol {    min-height: 44px;    padding: 8px 12px;    border: 1px solid var(--soluk);    border-radius: 8px;    background: var(--kart);    color: var(--yazi);    font: inherit;} .onay {    display: flex;    align-items: center;    gap: 8px;    min-height: 44px;} .onay input {    width: 20px;    height: 20px;} .sayac,.kart-unvan,.roller,.mesaj,.alt {    color: var(--soluk);} /* kartlar */.kartlar {    display: grid;    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));    gap: 12px;    padding: 0;    list-style: none;} .kart {    position: relative;    border: 1px solid var(--cizgi);    border-radius: 12px;    background: var(--kart);    text-align: center;} .kart a {    display: flex;    flex-direction: column;    align-items: center;    padding: 16px 12px 4px;    border-radius: 12px;    color: inherit;    text-decoration: none;} .kart img {    border-radius: 50%;} .kart-ad {    margin-top: 8px;    font-weight: 700;} .kart-unvan,.roller {    font-size: 0.875rem;} .kart .roller {    margin: 4px 8px 12px;} .favori {    position: absolute;    top: 4px;    right: 4px;    width: 44px;    height: 44px;    border: 0;    border-radius: 50%;    background: transparent;    color: var(--yildiz);    font-size: 1.5rem;    cursor: pointer;} /* detay */.geri {    display: inline-block;    margin-bottom: 16px;    color: var(--vurgu);} .detay {    display: grid;    grid-template-columns: 260px 1fr;    gap: 24px;    align-items: start;} .detay-resim {    width: 100%;    height: auto;    border-radius: 12px;} .detay h1,.detay-unvan {    margin: 0;} .olculer {    display: grid;    grid-template-columns: max-content 1fr;    gap: 6px 12px;    max-width: 360px;} .olculer dd {    margin: 0;} /* mesaj ve alt bilgi */.mesaj {    padding: 24px;    text-align: center;} .mesaj button {    min-height: 44px;    padding: 8px 16px;    border: 2px solid var(--vurgu);    border-radius: 8px;    background: transparent;    color: var(--vurgu);    font: inherit;    cursor: pointer;} .alt {    font-size: 0.8rem;} /* telefon */@media (max-width: 600px) {    .detay {        grid-template-columns: 1fr;    }     .detay-resim {        max-width: 220px;    }}
  • Renkler değişkenlerde (Ders 21'deki CSS değişkenleri). Görevlerde koyu tema eklerken yalnızca bu değişkenleri değiştirmek yetecek.
  • :focus-visible: Ders 20'deki :hover gibi bir sözde sınıf. Klavyeyle gezerken seçili öğenin çevresine kalın mor bir çerçeve (outline) çizer; fareyle tıklayınca çıkmaz. Erişilebilirlik bölümünde bunun neden şart olduğunu göreceksin.
  • repeat(auto-fill, minmax(150px, 1fr)): Ders 24'teki ızgaranın aynısı. Kartlar en az 150 piksel genişliğinde; ekran genişledikçe sütun sayısı artar (bizim 1100 piksellik sayfamızda en çok altı), telefonda iki sütun kendiliğinden gelir.
  • min-height: 44px ve 44 piksellik yıldız: Parmakla rahatça basılabilecek en küçük boyut. Telefonda küçük bir yıldızı tutturmaya çalışmak sinir bozucudur.
  • .olculer: <dl>'yi iki sütunlu bir ızgaraya çevirir: solda adlar (dt), sağda değerler (dd). max-content "en uzun ad kadar geniş" demek.
  • [hidden] kuralı: Garip görünebilir; neden gerektiğini aşağıdaki hata kartı anlatıyor.
Sık hatahidden yazdım ama öğe hâlâ görünüyor

Ne zaman çıkar? Bir öğeye hem hidden niteliği verip hem de CSS'te display değeri (grid, flex, block) yazınca. hidden aslında tarayıcının kendi stil dosyasındaki bir display: none kuralıdır. Senin stil dosyandaki kurallar tarayıcınınkilerden üstün gelir (Ders 20'deki basamaklama); .detay { display: grid } kazanır ve öğe gizlenmez.

[hidden] kuralını yazmadan detay sayfasında hatalı bir adres (?id=ahri) açtığımızda gerçekten olan bu: hata mesajı geldi ama boş detay iskeleti de ekranda kaldı.

Gizlenmesi gereken boş detay iskeleti ekranda: içi boş bir resim çerçevesi, yanında içeriksiz Hikâye, Yetenekler ve İpuçları başlıkları; en altta ahri yüklenemedi hata mesajı ve Tekrar dene düğmesi.

Nasıl düzelir? Stil dosyanın başına [hidden] { display: none !important; } kuralını koy. [hidden] "hidden niteliği olan her öğe" demek; !important ise "bu kural her durumda kazansın" (Ders 20'de ondan neden kaçındığımızı gördün). Buradaki iş tam onun işi: hidden yazdığın her öğe kesin gizlensin.

Modüller tarayıcıda: type="module"

Ders 14'te kodu dosyalara bölüp export ve import ile bağladın. Tarayıcıda da aynı sözdizimi geçerli, ama birkaç kural farklı:

  1. <script type="module"> şart. Böyle yüklenmeyen bir dosyada import yazarsan tarayıcı itiraz eder.
  2. Yol ./, ../ ya da / ile başlar ve uzantıyı içerir. import … from "./api.js" doğru; "api.js" ya da "./api" yanlış. Tarayıcı dosyayı tahmin etmeye çalışmaz, ne yazdıysan onu ister.
  3. Yol, import satırını yazan dosyaya göre çözülür, sayfaya göre değil. js/uygulama.js içinde "./api.js" yazınca tarayıcı aynı klasördeki js/api.js'yi arar.
  4. Modüller yalnızca bir sunucu üzerinden çalışır. Dosyaya çift tıklayıp açarsan (adres çubuğunda file:/// görürsün) tarayıcı güvenlik nedeniyle modülleri yüklemez. Sayfayı hep Live Server ile aç.
  5. Her modülün kendi kapsamı var. Bir modülde tanımladığın değişkeni, export etmedikçe başka dosya göremez (Ders 10'daki kapsam kuralının dosya düzeyindeki hâli). Böylece iki dosyada aynı adı kullanmak çakışma yaratmaz.
index.html sayfanın iskeleti <script type="module"> uygulama.js yönetici: durum ve olaylar import import import api.js veri getirir arayuz.js ekrana çizer favoriler.js favorileri saklar resim adresi Data Dragon (internet) Sayfadaki öğeler localStorage
Şekil 25.3 Modüller birbirine nasıl bağlı? Oklar import yönünü gösteriyor; en alttakiler her modülün konuştuğu dış dünya.

Ders 17'deki gibi her dosyanın neyi dışarı verdiğini de baştan yaz. Bu tablo, modüller arasındaki sözleşme: bir fonksiyonun adını değiştirirsen onu içe aktaran dosyayı da değiştirmen gerekir.

DosyaGöreviDışarı verdikleri (export)
js/api.jsData Dragon'a istek atmak, resim adresi üretmeksonSurumuGetir, sampiyonlariGetir, sampiyonGetir, kareResim, buyukResim
js/favoriler.jsFavorileri okumak ve değiştirmekfavorileriOku, favoriDegistir
js/arayuz.jsKartları, detayı ve mesajları çizmeklisteyiCiz, favoriDugmesiniAyarla, mesajGoster, hataGoster, detayiCiz
js/uygulama.jsAdresi okumak, durumu tutmak, olayları dinlemekyok: kimse onu içe aktarmaz, onu sayfa yükler

api.js: veriyi getiren modül

Data Dragon ile konuşan bütün kod bu dosyada. Başka hiçbir dosya fetch çağırmayacak; adresler değişirse yalnızca burayı düzelteceksin.

js/api.js
const ADRES = "https://ddragon.leagueoflegends.com" async function jsonGetir(adres) {    const yanit = await fetch(adres)    if (!yanit.ok) {        throw new Error(`İstek başarısız (${yanit.status}): ${adres}`)    }    const veri = await yanit.json()    return veri} export async function sonSurumuGetir() {    const surumler = await jsonGetir(`${ADRES}/api/versions.json`)    return surumler[0]} export async function sampiyonlariGetir(surum) {    const veri = await jsonGetir(`${ADRES}/cdn/${surum}/data/tr_TR/champion.json`)    return Object.values(veri.data)} export async function sampiyonGetir(surum, id) {    const dosya = encodeURIComponent(id)    const veri = await jsonGetir(`${ADRES}/cdn/${surum}/data/tr_TR/champion/${dosya}.json`)    return veri.data[id]} export function kareResim(surum, sampiyon) {    return `${ADRES}/cdn/${surum}/img/champion/${sampiyon.image.full}`} export function buyukResim(sampiyon) {    return `${ADRES}/cdn/img/champion/loading/${sampiyon.id}_0.jpg`}
  • jsonGetir: Ders 24'teki fonksiyonun aynısı, tek farkla: hata mesajına adresi de ekledik. Konsolda "İstek başarısız (403)" yazınca hangi dosyanın gelmediğini hemen görürsün. Bu fonksiyon dışa aktarılmıyor; yalnızca bu dosyanın içinde kullanılan bir yardımcı (Ders 3'teki "kendini tekrar etme" kuralı).
  • Object.values(veri.data): champion.json şampiyonları { Aatrox: {…}, Ahri: {…} } biçiminde bir obje olarak verir; Ders 24'teki gibi diziye çeviriyoruz.
  • encodeURIComponent(id): Yeni bir fonksiyon. id adresten, yani kullanıcıdan gelecek. İçinde /, ? ya da # gibi adreste özel anlamı olan işaretler varsa onları zararsız kodlara çevirir: encodeURIComponent("a/b") sonucu "a%2Fb". Ders 23'te form gönderilince adres çubuğunda 8%2F2%2F11 görmüştün; tarayıcının orada yaptığını bu fonksiyon senin için yapar. Kullanıcıdan gelen bir metni bir adresin içine koymadan önce hep bunu yap.
  • Resim fonksiyonları adres üretir, istek atmaz. Resmi <img src> yükleyecek.

Kontrol noktası 1: modüller konuşuyor mu?

Arayüzü yazmadan önce modüllerin birbirini bulduğundan ve verinin geldiğinden emin olalım. js/uygulama.js dosyasına geçici olarak şunu yaz. Birazdan tamamen sileceğiz.

js/uygulama.js — ansiklopedi — Visual Studio Code
js/uygulama.js
import { sonSurumuGetir, sampiyonlariGetir } from "./api.js" async function dene() {    const surum = await sonSurumuGetir()    const sampiyonlar = await sampiyonlariGetir(surum)    console.log("Sürüm:", surum)    console.log("Şampiyon sayısı:", sampiyonlar.length)    console.log("İlk şampiyon:", sampiyonlar[0].name, "-", sampiyonlar[0].title)} dene()
Ln 1, Col 1Spaces: 4UTF-8JavaScript

js/uygulama.js içinde geçici deneme kodu. Kaydettikten sonra Explorer'da index.html'e sağ tıkla ve Open with Live Server'ı seç.

Tarayıcıda http://127.0.0.1:5500/index.html açılır. Sayfada şimdilik yalnızca başlık çubuğu ve "Yükleniyor…" yazısı var; önemli olan konsol. F12 ile geliştirici araçlarını aç, Console sekmesine bak:

Tarayıcı konsolu (F12 → Console)örnek, sende farklı çıkabilir
Sürüm: 16.20.1Şampiyon sayısı: 173İlk şampiyon: Aatrox - Darkin Kılıcı

Sürüm numarası ve şampiyon sayısı sende farklı olabilir: Riot her yamada listeyi günceller. Araya Ders 22'den tanıdığın iki satır da karışabilir: Live Server'ın Live reload enabled. mesajı ve sekme simgesi için istenen favicon.ico'nun kırmızı 404'ü. İkisi de zararsız.

Yukarıdaki üç satırı gördüysen index.html modülü buldu, modül api.js'yi buldu ve veri internetten geldi. Görmediysen aşağıdaki hata kartlarına bak; bu aşamada en sık çıkan hatalar bunlar.

Sık hataAccess to script at 'file:///…' from origin 'null' has been blocked by CORS policy

Ne zaman çıkar? index.html dosyasına çift tıklayıp açınca. Adres çubuğunda http://127.0.0.1:5500 yerine file:///C:/kod/ansiklopedi/index.html yazar, sayfada "Yükleniyor…" takılı kalır ve konsolda şu iki satır çıkar:

Tarayıcı konsolu
Access to script at 'file:///C:/kod/ansiklopedi/js/uygulama.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.Failed to load resource: net::ERR_FAILED

Mesaj "CORS politikası bu betiği engelledi; çapraz istekler yalnızca şu adres türlerinde destekleniyor: …, http, https" diyor. Listede file yok. Ders 24'te fetch için gördüğün kural modüllere de uygulanır: tarayıcı, diskteki dosyaları birbirine yükletmez.

Failed to load resource satırlarını Chrome iki biçimde yazabilir. Geliştirici araçları sayfa yüklenirken açıksa satırın başında GET ve istenen dosyanın tam adresi olur (Ders 22'de gördüğün gibi); sayfa açıldıktan sonra açtıysan burada gördüğün biçim çıkar, dosyanın adı da satırın sağında yazar. Bu dersteki bütün Failed to load resource satırları için geçerli; anlamları aynı.

Nasıl düzelir? Sayfayı Live Server ile aç (ya da proje klasöründe npx serve çalıştırıp http://localhost:3000 adresini kullan). Önemli olan adresin http:// ile başlaması.

Sık hataUncaught SyntaxError: Cannot use import statement outside a module

Ne zaman çıkar? <script> etiketinde type="module" yazmayı unutunca. Tarayıcı dosyayı eski usul düz betik sanar ve ilk satırdaki import'u tanımaz:

Tarayıcı konsolu
Uncaught SyntaxError: Cannot use import statement outside a module

Nasıl düzelir? Mesaj "modül dışında import kullanamazsın" diyor. Etiketi düzelt:

HTML✗ Yanlış
<script src="js/uygulama.js"></script>
HTML✓ Doğru
<script type="module" src="js/uygulama.js"></script>
Sık hataFailed to resolve module specifier "api.js" ve 404 (Not Found)

Ne zaman çıkar? import satırındaki yolu yanlış yazınca. Başında ./ olmayan bir yol yazarsan tarayıcı onun bir dosya mı, bir paket adı mı olduğunu bilemez:

Tarayıcı konsolu
Uncaught TypeError: Failed to resolve module specifier "api.js". Relative references must start with either "/", "./", or "../".

Mesaj "göreli yollar /, ./ ya da ../ ile başlamalı" diyor. Başında ./ olan ama başka yanlışı olan bir yol yazarsan tarayıcı istek atar, sunucu da "öyle bir dosya yok" der. "./api" (uzantısız) ve js/uygulama.js içinde "./js/api.js" yazdığımızda konsola düşen satır aynı:

Tarayıcı konsolu
Failed to load resource: the server responded with a status of 404 (Not Found)

Hangi dosyanın gelmediğini görmek için Network sekmesine bak: kırmızı satırda js/api ya da js/js/api.js yazar. İkinci yolun neden js/js/ olduğunu Node'da kendin hesaplayabilirsin. Ders 18'de new URL(...) ile bir adresi parçalarına ayırmıştın; ona ikinci bir değer verirsen "bu yol, şu adrese göre nereye gider?" sorusunu cevaplar, .href de sonucu tam adres olarak verir. Şunu ders25.js deneme dosyana yaz ve çalıştır (dosyayı Klasör ve dosyalar bölümünde açmıştın):

ders25.js
const modul = "https://kullanici.github.io/ansiklopedi/js/uygulama.js"console.log(new URL("./api.js", modul).href)console.log(new URL("./js/api.js", modul).href)console.log(new URL("../stil.css", modul).href)
Çıktı
https://kullanici.github.io/ansiklopedi/js/api.jshttps://kullanici.github.io/ansiklopedi/js/js/api.jshttps://kullanici.github.io/ansiklopedi/stil.css

Yol, import'u yazan dosyanın klasöründen (js/) başlar. ./js/api.js bu yüzden js/js/api.js olur; ../ ise bir üst klasöre çıkar.

Nasıl düzelir? js klasöründeki dosyalar birbirini ./api.js diye çağırır: başta ./, sonda .js, araya klasör adı yok.

favoriler.js: tarayıcıda saklamak

Favoriler, sayfayı kapatıp açınca da durmalı. Ders 23'te maç günlüğünü localStorage'da sakladın; buradaki iki fonksiyon oradaki yukle ve kaydet'in kardeşi. Favorileri bir id dizisi olarak tutuyoruz: ["Ahri", "Lux"].

js/favoriler.js
const ANAHTAR = "ansiklopedi.favoriler" export function favorileriOku() {    const kayit = localStorage.getItem(ANAHTAR)    if (kayit === null) {        return []    }    try {        return JSON.parse(kayit)    } catch (hata) {        console.warn("Favori kaydı bozuk, boş listeyle devam ediyorum:", hata.message)        return []    }} export function favoriDegistir(id) {    let favoriler = favorileriOku()    if (favoriler.includes(id)) {        favoriler = favoriler.filter((f) => f !== id)    } else {        favoriler.push(id)    }    localStorage.setItem(ANAHTAR, JSON.stringify(favoriler))    return favoriler.includes(id)}
  • ANAHTAR: Live Server bütün projelerini aynı 127.0.0.1:5500 adresinden açar ve hepsi aynı depoyu paylaşır (Ders 23). Anahtarın başına proje adını koyunca maç günlüğünün kaydıyla çakışmaz.
  • kayit === null ve try / catch: Ders 23'teki yukle'nin aynısı. İlk ziyarette kayıt yoktur; kayıt bozuksa uygulama çökmez, konsola sarı bir uyarı yazıp boş listeyle devam eder.
  • favoriDegistir varsa çıkarır, yoksa ekler, kaydeder ve son durumu (true ya da false) döndürür. Düğmeyi güncellerken bu dönüş değerini kullanacağız. Kayıt her seferinde baştan okunduğu için favorilerin tek doğru kaynağı localStorage; favorileri ayrı bir değişkende de tutup ikisinin hep aynı kalmasıyla uğraşmıyoruz.

JSON.stringify neden şart? localStorage yalnızca metin saklar; ona bir dizi verirsen kendisi metne çevirir, ama senin istediğin biçimde değil. Ders 23'te objenin [object Object]'e döndüğünü görmüştün; dizide ne olduğunu ders25.js dosyasında gör:

ders25.js
const favoriler = ["Ahri", "Lux"]console.log(String(favoriler))console.log(JSON.stringify(favoriler))console.log(JSON.parse("[\"Ahri\",\"Lux\"]"))
Çıktı
Ahri,Lux["Ahri","Lux"][ 'Ahri', 'Lux' ]

String(favoriler), localStorage'ın kendiliğinden yaptığı çeviri: köşeli parantez ve tırnaklar gitmiş, geriye Ahri,Lux kalmış. JSON.stringify ise diziyi geri okunabilecek biçimde yazar.

Sık hataSyntaxError: Unexpected token 'A', "Ahri,Lux" is not valid JSON

Ne zaman çıkar? Diziyi JSON.stringify olmadan kaydedip sonra JSON.parse ile okumaya çalışınca. Tarayıcıda olanı Node'da taklit edelim:

ders25.js
const kayit = String(["Ahri", "Lux"])console.log(kayit)const favoriler = JSON.parse(kayit)
Hata çıktısı
Ahri,Lux<anonymous_script>:1Ahri,Lux^ SyntaxError: Unexpected token 'A', "Ahri,Lux" is not valid JSON    at JSON.parse (<anonymous>)    at Object.<anonymous> (C:\kod\ogren\ders25.js:3:24)    ... Node.js v24.20.0

Mesaj "beklenmeyen A harfi; Ahri,Lux geçerli bir JSON değil" diyor. JSON'da metinler tırnak içinde olmak zorunda; A ile başlayan çıplak bir sözcük JSON değildir.

Bizim favorileriOku bu hatayı yakaladığı için uygulama çökmez ama sessizce bozulur. setItem satırında JSON.stringify'ı sildik ve tarayıcıda denedik.

Ahri'nin yıldızına basınca kayda Ahri yazıldı. Lux'a basınca favorileriOku bu kaydı okuyamadı, boş listeyle devam etti ve kayda yalnızca Lux yazdı. Ekranda iki yıldız da doluydu, çünkü düğmeler basıldıkları anda güncelleniyor. Sayfayı yenileyince ikisi de boştu: Lux da geçerli bir JSON değil. Konsola iki sarı uyarı düştü: ilki Lux'a basınca, ikincisi yenileyince. (Yenileyince konsol temizlenir; ikisini birlikte görmek istersen Console'un ayarlarında Preserve log'u işaretle.)

Tarayıcı konsolu (sarı uyarılar)
Favori kaydı bozuk, boş listeyle devam ediyorum: Unexpected token 'A', "Ahri" is not valid JSONFavori kaydı bozuk, boş listeyle devam ediyorum: Unexpected token 'L', "Lux" is not valid JSON

Nasıl düzelir? Kaydederken JSON.stringify, okurken JSON.parse. Bozuk kaydı temizlemek için geliştirici araçlarında Application → Local storage → http://127.0.0.1:5500 altında ansiklopedi.favoriler satırını sil. Uyarı satırları boşuna değildi: catch içinde sessizce [] döndürseydik bu hatayı bulmak çok daha uzun sürerdi.

arayuz.js: ekrana çizen modül

Bu dosya veriyi alıp HTML öğelerine çevirir. Veri nereden geldi, nerede saklanıyor, bilmez ve umursamaz; yalnızca çizer. Ders 24'teki kartOlustur ve ROL_ADLARI buraya taşınıyor. Şimdilik liste kısmını yazıyoruz, detay kısmını ilgili bölümde ekleyeceğiz.

js/arayuz.js
import { kareResim } from "./api.js" const ROL_ADLARI = {    Assassin: "Suikastçı",    Fighter: "Dövüşçü",    Mage: "Büyücü",    Marksman: "Nişancı",    Support: "Destek",    Tank: "Tank"} const mesaj = document.querySelector("#mesaj") function oge(etiket, sinif, yazi) {    const yeni = document.createElement(etiket)    if (sinif) {        yeni.className = sinif    }    if (yazi !== undefined) {        yeni.textContent = yazi    }    return yeni} function rolleriYaz(roller) {    return roller.map((rol) => ROL_ADLARI[rol] ?? rol).join(" · ")} export function favoriDugmesiniAyarla(dugme, favori) {    dugme.setAttribute("aria-pressed", String(favori))    dugme.textContent = favori ? "★" : "☆"} function kartOlustur(sampiyon, surum, favori) {    const resim = oge("img")    resim.src = kareResim(surum, sampiyon)    resim.alt = ""    resim.width = 96    resim.height = 96    resim.loading = "lazy"     const baglanti = oge("a")    baglanti.href = `?id=${sampiyon.id}`    baglanti.append(resim, oge("span", "kart-ad", sampiyon.name), oge("span", "kart-unvan", sampiyon.title))     const dugme = oge("button", "favori")    dugme.type = "button"    dugme.dataset.id = sampiyon.id    dugme.setAttribute("aria-label", `Favori: ${sampiyon.name}`)    favoriDugmesiniAyarla(dugme, favori)     const kart = oge("li", "kart")    kart.append(baglanti, oge("p", "roller", rolleriYaz(sampiyon.tags)), dugme)    return kart} export function listeyiCiz(liste, sampiyonlar, surum, favoriler) {    liste.textContent = ""    for (const sampiyon of sampiyonlar) {        liste.append(kartOlustur(sampiyon, surum, favoriler.includes(sampiyon.id)))    }} export function mesajGoster(metin) {    mesaj.textContent = metin    mesaj.hidden = metin === ""} export function hataGoster(metin) {    mesajGoster(`${metin} `)    const dugme = oge("button", "", "Tekrar dene")    dugme.type = "button"    dugme.addEventListener("click", () => location.reload())    mesaj.append(dugme)}

Yukarıdan aşağı:

  • ROL_ADLARI ve rolleriYaz: Sözlük obje Ders 24'tekinin aynısı. ROL_ADLARI[rol] ?? rol (Ders 12'deki boş değer birleştirme): sözlükte varsa Türkçesi, yoksa (Riot ileride yeni bir rol eklerse) İngilizcesi. Ders 24'teki kart böyle bir rolde boş bir rozet gösterirdi; bu hâli bilinmeyen rolü olduğu gibi yazar. join(" · ") rolleri araya nokta koyarak tek metinde birleştirir: "Büyücü · Suikastçı".
  • oge yardımcısı: Her öğe için createElement, className, textContent üçlüsünü tekrar tekrar yazmamak için. oge("span", "kart-ad", "Ahri") bize <span class="kart-ad">Ahri</span> verir. className, class niteliğinin JavaScript'teki adıdır.
  • Hep textContent: Veriyi asla innerHTML ile koymuyoruz (Ders 22'deki XSS). Veri bizim değil, Riot'un.
  • Resim: alt = "", width/height ve loading = "lazy" Ders 24'tekiyle aynı: ad kartta yazdığı için resim süs, yeri baştan ayrılıyor, ekrana yaklaşınca iniyor.
  • href = "?id=Ahri": Başında / olmayan, yalnızca soru işaretiyle başlayan bir göreli bağlantı: "bu sayfanın adresi aynı kalsın, yalnızca sorgu kısmı ?id=Ahri olsun" demek. Detay bölümünde bu adresi okuyacağız.
  • Yıldız düğmesi: type = "button" olmazsa bir formun içindeki düğme formu gönderebilir. dataset.id, data-id="Ahri" niteliğini yazar (Ders 22); tıklanınca hangi şampiyonun yıldızı olduğunu buradan anlayacağız.
  • setAttribute(ad, değer): Bir öğeye istediğin niteliği yazar. src, href, type gibi yaygın niteliklerin resim.src = … diye kısa yolları vardır; aria-… ile başlayanları setAttribute ile yazarız. Nitelik değerleri hep metin olduğu için true'yu String(favori) ile "true"'ya çevirdik.
  • aria-label ve aria-pressed: Düğmede yalnızca bir yıldız karakteri var; ekran okuyucu onu okuyunca kullanıcı hiçbir şey anlamaz. aria-label düğmenin adını, aria-pressed de basılı olup olmadığını söyler. Ekran okuyucu "Favori: Ahri, açma kapama düğmesi, basılı" gibi bir şey der. ★ ve ☆ yalnızca gözle görenler için.
  • mesajGoster("") mesajı gizler; dolu bir metin verince gösterir. hataGoster mesajın yanına bir "Tekrar dene" düğmesi ekler.
  • location.reload(): location, tarayıcının o anki sayfanın adresini tutan hazır objesi; adres çubuğunun JavaScript'teki karşılığı. reload() sayfayı yeniler, tıpkı F5 gibi. Detay bölümünde location'ın başka bir parçasını da kullanacağız.

uygulama.js: hepsini yöneten dosya

Geçici deneme kodunu js/uygulama.js'den tamamen sil. Gerçek dosyayı parça parça yazacağız. Önce merkezdeki fikir: Ders 23'te öğrendiğin durum.

Kullanıcının o an seçtiği her şey (arama kutusundaki yazı, seçili rol, "yalnız favoriler" kutusu) ve gelen veri tek bir objede duracak. Ekranı hep bu objeye bakarak çizeceğiz. Kullanıcı bir şey değiştirdiğinde önce objeyi güncelleyecek, sonra listeyi baştan çizeceğiz.

1. Olay yazar, seçer ya da basarsın 2. Durum değişir durum.arama = "ahri" 3. gorunenleriBul 173 şampiyondan 1'i kalır 4. listeyiCiz kartlar baştan çizilir olay dinleyicisi görünenler ekranda görürsün listeyiGuncelle()
Şekil 25.4 Arayüz döngüsü: olay durumu değiştirir, görünenler bulunur, sonuç çizilir.

Dosyanın başı: içe aktarmalar, durum objesi ve sık kullanacağımız iki öğe.

js/uygulama.js
import { sonSurumuGetir, sampiyonlariGetir } from "./api.js"import { listeyiCiz, favoriDugmesiniAyarla, mesajGoster, hataGoster } from "./arayuz.js"import { favorileriOku, favoriDegistir } from "./favoriler.js" const durum = {    surum: "",    sampiyonlar: [],    arama: "",    rol: "",    yalnizFavori: false} const liste = document.querySelector("#liste")const sayac = document.querySelector("#sayac")

Ders 24'te surum ve sampiyonlar ayrı let değişkenleriydi. Şimdi hepsi tek objede: neyin ekranı etkilediğini görmek için tek bir yere bakarsın. durum bir const, ama içindeki alanları değiştirebiliriz; const yalnızca kutunun başka bir objeyle değiştirilmesini yasaklar (Ders 12'deki referans).

Arama: Ders 24'teki fonksiyonu taşı

Ders 24'te Türkçe I tuzağını çözen aramaBicimi ile süzme fonksiyonu uyuyorMu'yu yazmıştın. İkisini de buraya taşıyoruz, uyuyorMu'yu favori koşuluyla büyütüyoruz:

js/uygulama.js
function aramaBicimi(metin) {    return metin.trim().toLocaleLowerCase("tr").replaceAll("ı", "i")} function uyuyorMu(sampiyon, secim) {    const metinUyuyor = aramaBicimi(`${sampiyon.name} ${sampiyon.title}`).includes(aramaBicimi(secim.arama))    const rolUyuyor = secim.rol === "" || sampiyon.tags.includes(secim.rol)    const favoriUyuyor = !secim.yalnizFavori || secim.favoriler.includes(sampiyon.id)    return metinUyuyor && rolUyuyor && favoriUyuyor} function gorunenleriBul(favoriler) {    const secim = { ...durum, favoriler }    return durum.sampiyonlar.filter((sampiyon) => uyuyorMu(sampiyon, secim))}

Ders 24'ten üç fark var:

  • trim() artık aramaBicimi'nin içinde. Kutunun değerini kırpmayı artık unutamazsın: baştaki ve sondaki boşlukları hem aranan metinden hem şampiyonun adından fonksiyonun kendisi atıyor.
  • uyuyorMu üç ayrı değer yerine tek bir secim objesi alıyor: { arama, rol, yalnizFavori, favoriler }. Koşul ekledikçe parametre listesi uzamıyor; görevlerde dördüncü koşulu eklerken bunun faydasını göreceksin.
  • Favori koşulu: Kutu işaretli değilse herkes geçer; işaretliyse yalnızca favori listesindekiler. Listede id'ler var, bu yüzden sampiyon.id ile bakıyoruz.

gorunenleriBul o anki durumdan ekranda görünecek şampiyonları hesaplar. { ...durum, favoriler }, Ders 12'deki yayma ile "durumun kopyası, yanına favoriler de eklenmiş" demek; favoriler tek başına yazılınca favoriler: favoriler anlamına gelir.

aramaBicimi ve uyuyorMu saf fonksiyonlar (Ders 10): sayfaya dokunmaz, aldığı veriden bir sonuç döndürür. Bu yüzden onları tarayıcıyı açmadan, Node'da birkaç örnekle deneyebilirsin. ders25.js'ye iki fonksiyonu ve dört örnek şampiyonu yaz:

ders25.js
function aramaBicimi(metin) {    return metin.trim().toLocaleLowerCase("tr").replaceAll("ı", "i")} function uyuyorMu(sampiyon, secim) {    const metinUyuyor = aramaBicimi(`${sampiyon.name} ${sampiyon.title}`).includes(aramaBicimi(secim.arama))    const rolUyuyor = secim.rol === "" || sampiyon.tags.includes(secim.rol)    const favoriUyuyor = !secim.yalnizFavori || secim.favoriler.includes(sampiyon.id)    return metinUyuyor && rolUyuyor && favoriUyuyor} const ornekler = [    { id: "Ahri", name: "Ahri", title: "Dokuz Kuyruklu Tilki", tags: ["Mage", "Assassin"] },    { id: "Irelia", name: "Irelia", title: "Bıçakların Dansı", tags: ["Fighter", "Assassin"] },    { id: "Lux", name: "Lux", title: "Işık Leydisi", tags: ["Mage", "Support"] },    { id: "MonkeyKing", name: "Wukong", title: "Maymun Kral", tags: ["Fighter", "Tank"] }] const bos = { arama: "", rol: "", yalnizFavori: false, favoriler: ["Lux", "MonkeyKing"] }const adlar = (secim) => ornekler.filter((s) => uyuyorMu(s, secim)).map((s) => s.name) console.log(adlar(bos))console.log(adlar({ ...bos, arama: "  IRE " }))console.log(adlar({ ...bos, rol: "Assassin" }))console.log(adlar({ ...bos, yalnizFavori: true }))console.log(adlar({ ...bos, yalnizFavori: true, rol: "Mage" }))console.log(adlar({ ...bos, yalnizFavori: true, favoriler: ["Wukong"] }))console.log(adlar({ ...bos, yalnizFavori: true, favoriler: [] }))
Çıktı
[ 'Ahri', 'Irelia', 'Lux', 'Wukong' ][ 'Irelia' ][ 'Ahri', 'Irelia' ][ 'Lux', 'Wukong' ][ 'Lux' ][][]

Her satırı tek tek kontrol et: boş seçim hepsini verdi; başı sonu boşluklu, büyük harfli " IRE " Irelia'yı buldu; "yalnız favoriler" Lux ile Wukong'u bıraktı; iki koşul birlikte çalıştı. Altıncı satır önemli bir sınır: favorilere "Wukong" adı yazılmış olsaydı Wukong görünmezdi, çünkü onun id'si MonkeyKing. Son satırda favori listesi boş ve kutu işaretli: hiçbir şey kalmıyor.

Listeyi güncellemek ve boş durumlar

Döngünün 3. ve 4. adımı tek fonksiyonda: görünenleri bul, çiz, sayacı ve mesajı ayarla.

js/uygulama.js
function listeyiGuncelle() {    const favoriler = favorileriOku()    const gorunenler = gorunenleriBul(favoriler)    listeyiCiz(liste, gorunenler, durum.surum, favoriler)    sayac.textContent = `Gösterilen: ${gorunenler.length} / ${durum.sampiyonlar.length}`    if (gorunenler.length > 0) {        mesajGoster("")    } else if (durum.yalnizFavori && favoriler.length === 0) {        mesajGoster("Henüz favorin yok. Bir kartın yıldızına basarak ekleyebilirsin.")    } else {        mesajGoster("Aramana uyan şampiyon yok. Aramayı kısaltmayı ya da rolü Hepsi yapmayı dene.")    }}

İki ayrı boş durum mesajı var, çünkü sebepleri farklı. "Yalnız favoriler" işaretli ve hiç favori yoksa kullanıcıya ne yapacağını söylüyoruz: yıldıza bas. Arama hiçbir şeye uymuyorsa çıkış yolunu gösteriyoruz: aramayı kısalt. Ders 24'teki kuralı hatırla: boş durum bir hata değildir ve hatayla aynı mesajı almamalıdır.

Veriyi yüklemek

js/uygulama.js
async function listeyiAc() {    document.querySelector("#liste-gorunumu").hidden = false    mesajGoster("Şampiyonlar yükleniyor…")    try {        durum.surum = await sonSurumuGetir()        durum.sampiyonlar = await sampiyonlariGetir(durum.surum)        listeyiGuncelle()    } catch (hata) {        console.error(hata)        hataGoster("Şampiyonlar yüklenemedi. İnternet bağlantını kontrol et.")    }}

Ders 24'teki yukle'nin kardeşi. Önce liste görünümünü açıp "yükleniyor" mesajını gösteriyoruz, sonra veriyi bekliyoruz. Bir şey ters giderse catch iki iş yapar: hatanın aslını geliştirici için konsola yazar (console.error), kullanıcıya da anlayacağı dilde bir mesaj ve "Tekrar dene" düğmesi gösterir.

Olayları dinlemek

js/uygulama.js
document.querySelector("#filtreler").addEventListener("submit", (olay) => {    olay.preventDefault()}) document.querySelector("#arama").addEventListener("input", (olay) => {    durum.arama = olay.target.value    listeyiGuncelle()}) document.querySelector("#rol").addEventListener("change", (olay) => {    durum.rol = olay.target.value    listeyiGuncelle()}) document.querySelector("#yalniz-favori").addEventListener("change", (olay) => {    durum.yalnizFavori = olay.target.checked    listeyiGuncelle()}) liste.addEventListener("click", (olay) => {    const dugme = olay.target.closest(".favori")    if (!dugme) {        return    }    const favori = favoriDegistir(dugme.dataset.id)    favoriDugmesiniAyarla(dugme, favori)    if (durum.yalnizFavori) {        listeyiGuncelle()    }})
  • input olayı her tuş vuruşunda çalışır; kullanıcı yazdıkça liste daralır. select ve onay kutusu için change yeterli. Onay kutusunun değeri value'da değil checked'de durur: işaretliyse true.
  • Her dinleyici aynı kalıpta: durumu güncelle, listeyiGuncelle()'yi çağır. Durumu değiştiren başka bir yer yok; bir şey yanlış görünüyorsa bakacağın yer belli.
  • Yıldızlar için tek dinleyici: 173 düğmenin her birine ayrı dinleyici koymak yerine listenin kendisini dinliyoruz: Ders 22'deki olay yetkilendirmesi. closest(".favori") tıklanan öğeden yukarı doğru çıkıp favori sınıflı ilk öğeyi bulur; kartın başka bir yerine tıklandıysa null döner ve hiçbir şey yapmayız. Kartlar her aramada silinip yeniden yaratıldığı için tek dinleyici en sade yol: düğmelere tek tek takılan dinleyiciler kartlarla birlikte silinir, her çizimde yeniden takman gerekirdi. Listenin kendisi (#liste) ise hiç silinmez.
  • Yıldıza basınca listeyi baştan çizmiyoruz, yalnızca o düğmeyi güncelliyoruz. Bunun bir erişilebilirlik sebebi var; aşağıdaki "Klavye ve odak" başlığında göreceksin. Tek istisna "yalnız favoriler" açıkken: o zaman favoriden çıkan kart listeden kalkmalı.
Sık hataArama kutusunda Enter'a basınca liste sıfırlanıyor

Ne zaman çıkar? 59-61. satırlardaki submit dinleyicisini yazmazsan. Filtreler bir <form> içinde; arama kutusundayken Enter'a basmak formu gönderir. Tarayıcı da formu gönderirken sayfayı yeniden yükler.

Denediğimizde lux yazıp Enter'a bastık: adres http://127.0.0.1:5500/index.html? oldu, arama kutusu boşaldı ve sayaç yeniden Gösterilen: 173 / 173 gösterdi. Hata mesajı yok; sayfa yalnızca "unutuyor".

Nasıl düzelir? Ders 23'teki gibi submit olayında olay.preventDefault() çağır: "formu gönderme, sayfayı yenileme".

Kontrol noktası 2: liste çalışıyor

Dosyanın sonuna, olay dinleyicilerinin altına bir boş satır ve başlatma satırını yaz:

js/uygulama.js
listeyiAc()

Kaydet. Live Server sayfayı kendisi yeniler; kartlar gelmeli. Şunları tek tek dene:

  1. Arama kutusuna ahri yaz: tek kart kalmalı, sayaç Gösterilen: 1 / 173 demeli.
  2. Aramayı sil, rolü Destek yap: yalnızca destekler kalmalı (bu ders yazılırken 43 şampiyon).
  3. Ahri'nin ve Lux'un yıldızına bas, sayfayı yenile (F5): yıldızlar dolu kalmalı.
  4. Yalnız favoriler'i işaretle: yalnızca o iki kart kalmalı. Birinin yıldızına basınca o kart da listeden çıkmalı.
  5. Bir karta tıkla: adres ?id=… ile biter ama ekranda yine liste vardır. Detayı birazdan yazacağız.

Uygulamanın baştaki ekran görüntüsündeki gibi göründüğünü ve konsolda kırmızı satır olmadığını (zararsız favicon.ico 404'ü dışında) gördüysen ikinci kontrol noktasını geçtin.

Yükleniyor, hata ve boş durumlar

Ders 24'te bir sayfanın dört durumunu (yükleniyor, dolu, boş, hata) görmüştün. Projede de üçü aynı mesaj alanında görünüyor:

Üç kesit alt alta. Birinci: filtrelerin altında Şampiyonlar yükleniyor yazısı. İkinci: Şampiyonlar yüklenemedi, İnternet bağlantını kontrol et yazısı ve yanında çerçeveli Tekrar dene düğmesi. Üçüncü: arama kutusunda zzz yazıyor, sayaç Gösterilen: 0 / 173, altında Aramana uyan şampiyon yok mesajı.
Şekil 25.5 Aynı mesaj alanının üç hâli. Hepsi gerçek tarayıcıda çekildi.

Hepsini kendin dene; "bende çalışıyor" yetmez. Yöntemler Ders 24'tekilerle aynı. Önce geliştirici araçlarını aç ve Network sekmesinde Disable cache kutusunun işaretli olduğundan emin ol; boşsa tarayıcı JSON dosyalarını önbellekten verir ve yükleniyor ile hata durumlarını hiç görmezsin.

  • Yükleniyor: Geliştirici araçlarında Network sekmesini aç, No throttling yazan menüden 3G'yi seç ve sayfayı yenile. Mesajı birkaç saniye görürsün. İşin bitince menüyü yeniden No throttling yap.
  • Hata: Wi-Fi'ı kapat (ya da kabloyu çıkar) ve sayfayı yenile. Live Server senin bilgisayarında çalıştığı için sayfa yine açılır; gelmeyen yalnızca Data Dragon verisidir. Tekrar dene'ye bas: internet yokken yine aynı mesaj gelir. İnterneti açıp yeniden basınca kartlar gelir. Konsolda gerçek sebep görünür:
Tarayıcı konsolu
Failed to load resource: net::ERR_INTERNET_DISCONNECTEDTypeError: Failed to fetch    at jsonGetir (http://127.0.0.1:5500/js/api.js:4:25)    at sonSurumuGetir (http://127.0.0.1:5500/js/api.js:13:28)    at listeyiAc (http://127.0.0.1:5500/js/uygulama.js:50:29)    ...
  • Boş: zzz diye ara. Bir de hiç favorin yokken Yalnız favoriler'i işaretle; farklı mesajı görmelisin.
Sık hataTypeError: Cannot read properties of null (reading 'includes')

Ne zaman çıkar? Bu, ekrandaki mesajın yalan söylediği bir hata. favorileriOku'yu kısa yazıp null denetimini atladığımızda tarayıcıda gerçekten olan şu oldu:

JavaScript✗ Yanlış
export function favorileriOku() {    return JSON.parse(localStorage.getItem(ANAHTAR))}

İlk ziyarette kayıt yok; getItem null verir, JSON.parse(null) da null döndürür (Ders 23'te maç günlüğünde aynı tuzağı görmüştün). Ekranda "Şampiyonlar yüklenemedi. İnternet bağlantını kontrol et." yazdı. İnternet çalışıyordu. Gerçek sebep konsoldaydı:

Tarayıcı konsolu
TypeError: Cannot read properties of null (reading 'includes')    at listeyiCiz (http://127.0.0.1:5500/js/arayuz.js:60:61)    at listeyiGuncelle (http://127.0.0.1:5500/js/uygulama.js:35:5)    at listeyiAc (http://127.0.0.1:5500/js/uygulama.js:52:9)

Yığın izini Ders 13'teki gibi aşağıdan yukarı oku: listeyiAc, listeyiGuncelle'yi çağırdı; o da listeyiCiz'i; arayuz.js'nin 60. satırında favoriler.includes(...) çalışırken favoriler null'dı. listeyiAc içindeki catch bu hatayı da yakaladı ve kullanıcıya internet mesajını gösterdi; çünkü try bloğunda hata çıkaran her şeyi yakalar, yalnızca ağ hatalarını değil.

Nasıl düzelir? favorileriOku'da kayit === null denetimini geri koy. Asıl ders şu: kullanıcıya gösterilen mesaj bir tahmindir, konsoldaki satır gerçektir. Bir şey bozulduğunda önce konsola bak; catch içinde console.error(hata) yazmamızın sebebi bu.

Detay görünümü: adreste ?id=

Bir karta tıklayınca detay açılmalı. Seçilen şampiyonu bir değişkende tutup ekranı değiştirmek de bir yol, ama o zaman üç şeyi kaybederiz:

  • Paylaşmak: Arkadaşına "şu sayfaya bak" diyemezsin; gönderdiğin adres hep listeyi açar.
  • Yenilemek: F5'e basınca değişkenler sıfırlanır, listeye dönersin.
  • Geri tuşu: Tarayıcının geri tuşu bir önceki sayfaya gider; değişkenini bilmez.

Seçimi adrese yazarsak üçü de kendiliğinden çalışır. Ders 18'de URL'nin parçalarını görmüştün; soru işaretinden sonraki kısım sorgu (query string). Sayfa adresi …/index.html?id=Ahri olduğunda JavaScript bu kısmı okuyup Ahri'yi çizer.

https:// kullanici.github.io /ansiklopedi/ ?id=Ahri protokol alan adı yol (depo adı) sorgu new URLSearchParams(location.search).get("id") "Ahri" Tarayıcı geçmişi 1. Liste /ansiklopedi/ 2. Detay /ansiklopedi/?id=Ahri karta tıkla geri tuşu
Şekil 25.6 Adresin parçaları, URLSearchParams ile sorguyu okumak ve tarayıcı geçmişi.

Tarayıcıda location.search, Ders 18'deki new URL(...) objesinin search alanı gibi, adresin soru işaretinden başlayan kısmını verir: "?id=Ahri". URLSearchParams bu metni parçalayıp get ile tek tek okumanı sağlar. Node'da da var; ders25.js dosyasında dene:

ders25.js
const parametreler = new URLSearchParams("?id=Ahri&sekme=hikaye")console.log(parametreler.get("id"))console.log(parametreler.get("sekme"))console.log(parametreler.get("rol"))console.log(parametreler.get("ID"))console.log(new URLSearchParams("?id=").get("id") === "")
Çıktı
Ahrihikayenullnulltrue

Üç sınır durumuna dikkat: adreste olmayan bir ad null verir; adlar büyük-küçük harfe duyarlıdır (ID bulunmadı); ?id= yazılıp değer verilmezse sonuç boş metindir. Biz if (secilenId) diye soracağız: hem null'ı hem boş metni "seçim yok" sayar (Ders 12'deki doğru sayılan ve yanlış sayılan değerler).

arayuz.js'ye detay kısmı

js/arayuz.js'nin 1. satırını, büyük resim adresini de alacak şekilde değiştir:

js/arayuz.js
import { kareResim, buyukResim } from "./api.js"

Dosyanın en sonuna, hataGoster'ın altına bir boş satır bırakıp şunu ekle:

js/arayuz.js
const OLCULER = [    ["Saldırı", "attack"],    ["Savunma", "defense"],    ["Büyü", "magic"],    ["Zorluk", "difficulty"]] export function detayiCiz(sampiyon) {    const resim = document.querySelector("#detay-resim")    resim.src = buyukResim(sampiyon)    resim.alt = `${sampiyon.name} çizimi`    document.querySelector("#detay-ad").textContent = sampiyon.name    document.querySelector("#detay-unvan").textContent = sampiyon.title    document.querySelector("#detay-roller").textContent = rolleriYaz(sampiyon.tags)    document.querySelector("#detay-hikaye").textContent = sampiyon.lore     const olculer = document.querySelector("#detay-olculer")    for (const [ad, anahtar] of OLCULER) {        const olcu = oge("meter")        olcu.max = 10        olcu.value = sampiyon.info[anahtar]        const deger = oge("dd")        deger.append(olcu, ` ${sampiyon.info[anahtar]}/10`)        olculer.append(oge("dt", "", ad), deger)    }     const yetenekler = document.querySelector("#detay-yetenekler")    yetenekler.append(oge("li", "", `Pasif: ${sampiyon.passive.name}`))    sampiyon.spells.forEach((yetenek, sira) => {        yetenekler.append(oge("li", "", `${"QWER"[sira]}: ${yetenek.name}`))    })     const ipuclari = document.querySelector("#detay-ipuclari")    if (sampiyon.allytips.length === 0) {        ipuclari.append(oge("li", "", "Bu şampiyon için ipucu yok."))    }    for (const ipucu of sampiyon.allytips) {        ipuclari.append(oge("li", "", ipucu))    }}
  • İskelet HTML'de, veri JavaScript'te. Detayın başlıkları ve kutuları index.html'de zaten duruyor; burada yalnızca içlerini dolduruyoruz. Listede kart sayısı değiştiği için kartları JavaScript ile üretmek zorundaydık; detayda yapı hep aynı.
  • alt: Burada resim süs değil; şampiyonun nasıl göründüğünü anlatıyor. Bu yüzden boş bırakmadık.
  • OLCULER ve for…of ile parçalama: Dört ölçü için dört kez aynı kodu yazmak yerine [ekrandaki ad, verideki anahtar] çiftlerinden bir dizi kurduk ve döngüde Ders 12'deki dizi parçalamayla açtık. Her tur <dl>'ye bir <dt> ve bir <dd> ekler.
  • <meter>: HTML'in hazır ölçü çubuğu; max ve value vermen yeter. Yanına sayıyı da yazdık, çünkü çubuğun doluluğunu herkes aynı rahatlıkla okuyamaz. append'e öğe ile metni birlikte verebilirsin (Ders 23'teki " " gibi).
  • "QWER"[sira]: Metinler de dizi gibi sırayla okunabilir (Ders 9). Data Dragon yetenekleri Q, W, E, R sırasıyla verir; forEach'in ikinci parametresi sıra numarasıdır (Ders 11). sira 0 iken "Q", 3 iken "R" olur.
  • Boş ipucu listesi: Bu ders yazılırken 173 şampiyonun 24'ünün ipucu listesi boştu. Boş bir başlığın altında hiçbir şey göstermemek yerine bunu söylüyoruz.

Detay her açılışta yeni bir sayfa yüklemesiyle çizildiği için listeleri önce boşaltmamız gerekmiyor. Aynı sayfada ikinci kez çağrılsaydı yetenekler iki kez eklenirdi; Ders 24'te liste.textContent = "" satırını unutunca kartların üst üste eklenmesinin kardeşi.

uygulama.js'ye detay kısmı

js/uygulama.js'nin ilk iki satırına detay için gereken iki adı ekle:

js/uygulama.js
import { sonSurumuGetir, sampiyonlariGetir, sampiyonGetir } from "./api.js"import { listeyiCiz, favoriDugmesiniAyarla, mesajGoster, hataGoster, detayiCiz } from "./arayuz.js"

Dosyanın sonundaki listeyiAc() satırını sil ve yerine şunu yaz:

js/uygulama.js
async function detayiAc(id) {    document.querySelector("#detay-gorunumu").hidden = false    mesajGoster("Şampiyon yükleniyor…")    try {        const surum = await sonSurumuGetir()        const sampiyon = await sampiyonGetir(surum, id)        detayiCiz(sampiyon)        document.title = `${sampiyon.name} · Şampiyon Ansiklopedisi`        document.querySelector("#detay").hidden = false        mesajGoster("")    } catch (hata) {        console.error(hata)        hataGoster(`"${id}" yüklenemedi. Adresteki adı (büyük-küçük harf dahil) ve bağlantını kontrol et.`)    }} const parametreler = new URLSearchParams(location.search)const secilenId = parametreler.get("id") if (secilenId) {    detayiAc(secilenId)} else {    listeyiAc()}
  • parametreler satırından dosyanın sonuna kadarki kısım uygulamanın kapısı: adreste id varsa detayı, yoksa listeyi açar.
  • Önce bölümü, sonra içeriği göster: detay-gorunumu hemen açılıyor; böylece "← Tüm şampiyonlar" bağlantısı yüklenirken de, hata çıkarsa da ekranda. Bilgi kartı (#detay) ise ancak veri gelince görünüyor; boş iskelet hiç görünmüyor.
  • document.title: Tarayıcı sekmesindeki başlık "Ahri · Şampiyon Ansiklopedisi" olur. Birden çok sekme açan kullanıcı hangisinin ne olduğunu görür; geçmiş ve yer imleri de bu adı kullanır.
  • Hata mesajında id: Adresi kullanıcı yazabilir, yani id her şey olabilir. Mesajı textContent ile yazdığımız için içine etiket koyan biri de bir şey çalıştıramaz (XSS).

Kontrol noktası 3: detay ve geri tuşu

Sayfayı yenile ve Ahri'nin kartına tıkla:

127.0.0.1:5500/index.html?id=Ahri
Ahri'nin detay sayfası. Üstte Tüm şampiyonlar bağlantısı; solda büyük dikey resim alanı; sağda Ahri başlığı, Dokuz Kuyruklu Tilki unvanı, Büyücü · Suikastçı rolleri, Saldırı 3/10, Savunma 4/10, Büyü 8/10, Zorluk 5/10 ölçü çubukları ve Hikâye başlığıyla paragraf; altta Yetenekler listesi: Pasif Öz Hırsızı, Q Aldatan Küre, W Tilki Ateşi, E Ayartan Öpücük, R Ruh Saldırısı.
Detay görünümü: solda büyük resim, sağda ad, unvan, roller, dört ölçü, hikâye ve yetenekler; ipuçları biraz daha aşağıda. Resim yerinde yine yer tutucu var.

Şimdi tarayıcının geri tuşuna bas: listeye dönersin. Bunun için tek satır kod yazmadık. Kartlar gerçek bağlantı olduğu için her tıklama yeni bir sayfa açar ve tarayıcı geçmişe ekler; geri tuşu da bir önceki sayfayı yeniden açar.

Listeyi nasıl bulacağın tarayıcıya bağlı. Tarayıcı sayfayı bellekte tuttuysa (geri-ileri önbelleği, İngilizcesiyle back/forward cache) aramanı ve filtreni bıraktığın gibi bulursun; tutmadıysa sayfa baştan yüklenir ve filtreler sıfırlanmış gelir. İkisi de tutarlı; formdaki autocomplete="off" tutarsız bir üçüncü hâli önlemek için orada.

Sık hataGeri dönünce kutularda eski filtre yazıyor ama liste hepsini gösteriyor

Ne zaman çıkar? Formda autocomplete="off" yoksa ve tarayıcı geri dönülen sayfayı baştan yüklerse. Tarayıcı baştan yüklerken bir iyilik yapmaya çalışır: form kutularını son hâlleriyle doldurur. Ama JavaScript de baştan başlar ve durum objesi yine boştur.

Niteliği silip denedik: rolü Destek yapıp aramaya lu yazdık; Lulu, Lux ve Nautilus kaldı, sayaç Gösterilen: 3 / 173 dedi. Lux'a tıklayıp geri döndük: kutularda yine lu ve Destek yazıyordu ama sayaç Gösterilen: 173 / 173 diyordu ve bütün kartlar oradaydı. Hata mesajı yok; ekran ile durum yalnızca birbirini tutmuyor. Bunu her denemede de görmeyebilirsin, çünkü sayfa bellekten geri gelirse her şey yerindedir.

Nasıl düzelir? Formun açılış etiketine autocomplete="off" yaz; formdaki bütün kutular için geçerli olur. Doğru sürümde aynı denemede kutular boş ve Hepsi olarak geldi, sayaçla uyumluydu. Kural: ekranda görünen her seçim durum objesinde de olmalı; ikisi ayrı düşerse kullanıcı ekrana inanmaz.

"← Tüm şampiyonlar" bağlantısının adresi ./: "bu klasörün kendisi", yani sorgu olmayan aynı sayfa. Geri tuşundan farkı şu: biri detay sayfasını doğrudan bir bağlantıdan açtıysa geri tuşu onu senin sitenden çıkarır, bu bağlantı ise her zaman listeye götürür.

Son olarak adres çubuğuna elle http://127.0.0.1:5500/index.html?id=ahri (küçük harfle) yazıp aç. Ekranda bizim hata mesajımız çıkar; konsolda da şunlar:

Tarayıcı konsolu
Failed to load resource: the server responded with a status of 403 (Forbidden)Error: İstek başarısız (403): https://ddragon.leagueoflegends.com/cdn/16.20.1/data/tr_TR/champion/ahri.json    at jsonGetir (http://127.0.0.1:5500/js/api.js:6:15)    at async sampiyonGetir (http://127.0.0.1:5500/js/api.js:24:18)    at async detayiAc (http://127.0.0.1:5500/js/uygulama.js:95:26)

Ders 24'te gördüğün gibi Data Dragon'da dosya adları büyük-küçük harfe duyarlı ve olmayan dosya için "bulunamadı" (404) yerine "yasak" (403) dönüyor. Hangisi gelirse gelsin yanit.ok false olur ve jsonGetir hata fırlatır; hata mesajına eklediğimiz adres de tam olarak hangi dosyanın istendiğini gösteriyor. Aynı hatayı ?id=Wukong ile de görürsün: Wukong'un id'si MonkeyKing. Kartlardaki bağlantılar id kullandığı için tıklayınca sorun çıkmaz; elle yazılan adreslerde çıkar.

Sık hataThe requested module './api.js' does not provide an export named 'sampiyonuGetir'

Ne zaman çıkar? import satırına bir adı yanlış yazınca. 1. satıra sampiyonGetir yerine sampiyonuGetir yazdığımızda sayfa hiç açılmadı, "Yükleniyor…" takılı kaldı:

Tarayıcı konsolu
Uncaught SyntaxError: The requested module './api.js' does not provide an export named 'sampiyonuGetir'

Mesaj "./api.js modülü sampiyonuGetir adında bir şey dışa aktarmıyor" diyor. Türü SyntaxError, çünkü tarayıcı bunu kodu çalıştırmadan, dosyaları birbirine bağlarken fark eder; Ders 1'deki gibi hiçbir satır çalışmaz. Aynı hata, api.js'de fonksiyonun başına export yazmayı unutunca da çıkar.

Nasıl düzelir? import'taki adı, export eden dosyadakiyle harfi harfine aynı yap. VS Code süslü parantezin içinde yazarken adları önerir; öneriden seçersen bu hatayı yapmazsın.

Erişilebilirlik ve mobil kontrol listesi

Sayfan senin ekranında, senin farenle çalışıyor. Ama onu açacak herkes senin gibi değil: kimi fareyi kullanamaz ve yalnızca klavyeyle gezer; kimi görme engellidir ve sayfayı bir ekran okuyucu sesli okur; kimi güneşin altında telefondan bakar. Erişilebilirlik onlar için de çalışmak demek. İş görüşmelerinde de sorulur; projeni gösterirken "klavyeyle de kullanılabiliyor" demek fark yaratır.

Aşağıdaki listeyi her projenin sonunda tek tek yap:

KontrolNasıl denersinBizde
Klavyeyle her şeye ulaşılıyorFareyi bırak, Tab ile gez; Enter bağlantıyı açar, Space düğmeye basarKartlar <a>, yıldızlar <button>; ikisi de kendiliğinden odaklanır
Odak görünüyorTab'a basınca nerede olduğunu her an görebiliyor musun?:focus-visible çerçevesi
Her girdi alanının adı varEtikete tıklayınca kutu seçiliyor mu?Her alanın <label for>'u var
Simgeli düğmenin adı varYalnızca ★ olan düğmeye ekran okuyucu ne der?aria-label="Favori: Ahri" ve aria-pressed
Resimlerin alt metni doğruSüs resmi alt="", bilgi taşıyan resim açıklamalı mı?Kartta boş, detayda dolu
Değişen içerik duyuruluyorSayaç ve mesajlar değişince ekran okuyucu söylüyor mu?aria-live ve role="status"
Yazı okunuyorKontrast oranı en az 4,5:1 mi?Aşağıdaki tabloya bak
Telefonda çalışıyor390 piksel genişlikte yatay kaydırma var mı, düğmeler parmağa uygun mu?@media, 44 px düğmeler
Sayfanın dili belli<html lang="tr"> yazıyor mu?Var; İngilizce cümlede lang="en"

Klavye ve odak

Sayfayı aç, fareye dokunmadan Tab'a art arda bas. Bizim sayfamızda gerçek sıra şöyle çıktı:

  1. Logodaki "Şampiyon Ansiklopedisi" bağlantısı
  2. Arama kutusu
  3. Rol seçimi
  4. "Yalnız favoriler" kutusu
  5. Aatrox kartının bağlantısı
  6. Aatrox'un yıldızı ("Favori: Aatrox")
  7. Ahri kartının bağlantısı, sonra Ahri'nin yıldızı ve böyle devam eder

Sıra, öğelerin HTML'deki sırasıdır; bu yüzden kartta bağlantıyı önce, yıldızı sonra yazdık. Odaktaki öğenin çevresinde mor çerçeve görünür:

Yan yana üç kart: Aatrox, Ahri, Akali. Ahri'nin sağ üst köşesindeki dolu yıldızın çevresinde kalın mor yuvarlak bir odak çerçevesi var.
Şekil 25.7 Klavyeyle Ahri'nin yıldızına gelince çıkan odak çerçevesi.
Sık hataYıldıza basınca klavye odağı kayboluyor

Ne zaman çıkar? Yıldız tıklamasında yalnızca o düğmeyi güncellemek yerine bütün listeyi baştan çizince. İlk akla gelen yazım bu:

JavaScript✗ Yanlış
liste.addEventListener("click", (olay) => {    const dugme = olay.target.closest(".favori")    if (!dugme) {        return    }    favoriDegistir(dugme.dataset.id)    listeyiGuncelle()})

Fareyle sorun görünmez. Ama klavyeyle Ahri'nin yıldızına gelip Space'e basınca listeyiCiz eski kartların hepsini silip yenilerini koyar; odaktaki düğme de silinenlerin arasındadır.

Denediğimizde basıştan sonra odak, sayfanın kendisine (<body>) düştü ve sonraki Tab listenin en başına, Aatrox'a gitti. Klavye kullanan biri 50. kartta favori eklediyse yerini kaybeder. Hata mesajı yok; yalnızca klavye kullananlar fark eder.

Nasıl düzelir? Yalnızca değişen düğmeyi güncelle (favoriDugmesiniAyarla). Doğru sürümde aynı denemeden sonra odak yıldızın üstünde kaldı. Ekranda bir şeyi baştan çizdiğinde "odak neredeydi, şimdi nerede?" diye sor.

Kontrast

Ders 20'de kontrast oranını ve 4,5:1 sınırını görmüştün. Açık gri yazı şık görünür ama birçok kişi için okunmaz: ekranı parlak olmayan bir laptopta, güneş altında ya da gözü yorgun biri için. Renklerimizin oranlarını hesapladık; her rengi, sayfada üstünde durduğu zeminle ölçtük:

YazıArka planOranSonuç
#1c1f2b (ana yazı)#f4f5f9 (zemin)15,06:1Geçer
#596074 (soluk yazı: sayaç, mesaj)#f4f5f9 (zemin)5,75:1Geçer
#596074 (soluk yazı: unvan, roller)#ffffff (kart)6,27:1Geçer
#6b3fd4 (bağlantı, düğme)#f4f5f9 (zemin)5,86:1Geçer
#9a6700 (yıldız)#ffffff (kart)4,87:1Geçer
#a0a6b5 (açık gri, kullanmadık)#ffffff (kart)2,44:1Kalır

Kendi renklerini Ders 20'deki gibi denetle: yazının üstüne sağ tıklayıp İncele (Inspect) de, Styles bölümünde renk karesine tıkla; açılan renk seçicideki Contrast ratio satırı oranı gösterir (arayüz Chrome sürümüne göre biraz değişmiş olabilir). Çerçeveler için kural daha gevşek: bir giriş kutusunun kenarı arka planla en az 3:1 olmalı; biz kutu kenarlarında soluk yazı rengini kullandık.

Telefon

Geliştirici araçlarında Ctrl+Shift+M cihaz araç çubuğunu açar; üstteki genişliği 390 yaparsan sayfayı bir telefon ekranında görürsün. Bizim sayfamız 390 pikselde böyle:

Yan yana iki telefon ekranı. Solda liste: arama kutusu ve rol seçimi bir satırda, Yalnız favoriler alt satırda, kartlar iki sütun. Sağda Lux'un detayı: Tüm şampiyonlar bağlantısı, üstte büyük resim, altında Lux başlığı, Işık Leydisi unvanı, roller ve ölçü çubukları.
Şekil 25.8 Telefon genişliğinde (390 px) liste ve detay. Yatay kaydırma çubuğu yok.

Bakacağın üç şey: sayfa yana kaymamalı (alttaki kaydırma çubuğu çıkmamalı), yazılar yakınlaştırmadan okunmalı ve parmakla basılacak her şey en az 44×44 piksel olmalı. Bizim denememizde 390 piksellik pencerede sayfanın genişliği de tam 390 pikseldi ve yıldızlar 44×44 çıktı. <head>'deki <meta name="viewport"> satırını unutursan telefon sayfayı masaüstü genişliğinde çizip küçültür; her şey karınca kadar görünür (Ders 21).

Yayına almak: GitHub Pages

Uygulaman şimdilik yalnızca senin bilgisayarında çalışıyor. Başkalarının da açabilmesi için dosyaları internette sürekli açık duran bir sunucuya koyman gerekiyor.

GitHub Pages, GitHub'ın statik siteler için ücretsiz yayın hizmeti. Depona koyduğun dosyaları https://kullaniciadin.github.io/depoadi/ adresinde yayınlar. Ücretsiz hesapta deponun herkese açık (Public) olması gerekir. Sunucuda kod çalıştırman gerekseydi Pages yetmezdi; Ders 32'de o tür uygulamaları başka yerlerde yayına alacağız.

  1. İlk kaydı al. Proje klasöründe Git'i başlat ve bütün dosyaları kaydet (aşağıdaki kutu).
  2. GitHub'da depo aç. Sağ üstteki + menüsünden New repository'yi seç. Adı ansiklopedi olsun, görünürlüğü Public. README, .gitignore ve lisans seçeneklerini kapalı bırak; README'yi kendimiz yazacağız.
  3. Kodu gönder. GitHub'ın gösterdiği komutlarla deponu bağla ve gönder (aşağıdaki ikinci kutu).
  4. Pages'i aç. Deponun sayfasında Settings'e gir. Soldaki menüde "Code, planning, and automation" başlığının altında Pages'e tıkla. Build and deployment bölümünde Source olarak Deploy from a branch seç, dal olarak main, klasör olarak / (root) seç ve Save'e bas.
  5. Bekle ve aç. GitHub'ın belgesine göre değişikliklerin yayına çıkması 10 dakikayı bulabilir. Deponun Actions sekmesinde bu yayın için bir iş görürsün (adı genelde pages build and deployment). İş yeşil tik alınca Pages ayarlarının en üstünde sitenin adresi ve Visit site düğmesi çıkar.
KULLANICI / ansiklopedi Code Issues Pull requests Actions Settings 1 General Collaborators Code, planning, and automation Branches Actions Pages 2 GitHub Pages Build and deployment Source Deploy from a branch ▾ 3 Branch main ▾ / (root) ▾ Save 4 Birkaç dakika sonra site yayında: https://kullanici.github.io/ansiklopedi/
Şekil 25.9 GitHub'daki Pages ayarının şeması (gerçek ekran biraz farklı görünebilir; GitHub arayüzünü zaman zaman değiştirir).

İlk adımın komutları. VS Code terminali C:\kod\ansiklopedi klasöründe olmalı; Node denemelerinden sonra hâlâ C:\kod\ogren'deysen önce cd C:\kod\ansiklopedi yaz. Bu klasör yeni, bu yüzden Git'i burada ilk kez başlatıyoruz:

GitHub'da depoyu açınca sayfada gösterilen komutların aynısı (Ders 17'de de bunları kullandın). KULLANICI yerine kendi GitHub kullanıcı adını yaz:

Terminal — PowerShell
PS C:\kod\ansiklopedi> git remote add origin https://github.com/KULLANICI/ansiklopedi.gitPS C:\kod\ansiklopedi> git branch -M mainPS C:\kod\ansiklopedi> git push -u origin main# İlk gönderimde tarayıcıda GitHub'a giriş isteyebilir (Ders 7'deki gibi).

git branch -M main dalın adını master'dan main'e çevirir; Pages ayarında seçeceğin dal bu. -u ise "bundan sonra git push yazınca buraya gönder" demek.

Göreli yol tuzağı

Siten açıldı ama tasarım yok, sayfada yalnızca mavi bir bağlantı ve "Yükleniyor…" yazısı var mı? Yayına alırken en sık çıkan sorun bu ve sebebi tek bir karakter: yolun başındaki /.

Bilgisayarında sayfan sunucunun kök klasöründe duruyordu: 127.0.0.1:5500/index.html. Pages'te ise bir alt klasörde: kullanici.github.io/ansiklopedi/. Göreli yol (stil.css) bulunduğun klasörden başlar. Başında / olan bir yol ise "alan adının köküne git" demektir ve alt klasörü atlar. ders25.js ile hesapla:

ders25.js
const sayfa = "https://kullanici.github.io/ansiklopedi/"console.log(new URL("stil.css", sayfa).href)console.log(new URL("/stil.css", sayfa).href)console.log(new URL("?id=Ahri", sayfa).href) const yerel = "http://127.0.0.1:5500/index.html"console.log(new URL("stil.css", yerel).href)console.log(new URL("/stil.css", yerel).href)
Çıktı
https://kullanici.github.io/ansiklopedi/stil.csshttps://kullanici.github.io/stil.csshttps://kullanici.github.io/ansiklopedi/?id=Ahrihttp://127.0.0.1:5500/stil.csshttp://127.0.0.1:5500/stil.css

Son iki satır, hatanın neden senin bilgisayarında hiç görünmediğini açıklıyor: kökte duran bir sayfa için stil.css ile /stil.css aynı yere gider.

Bilgisayarında sayfa: http://127.0.0.1:5500/index.html href="stil.css" 127.0.0.1:5500/stil.css ✓ geldi href="/stil.css" 127.0.0.1:5500/stil.css ✓ geldi GitHub Pages'te sayfa: https://kullanici.github.io/ansiklopedi/ href="stil.css" …github.io/ansiklopedi/stil.css ✓ geldi href="/stil.css" …github.io/stil.css ✗ 404 Baştaki / "alan adının köküne git" demek; depo adını (ansiklopedi/) atlar.
Şekil 25.10 Aynı iki yol, iki farklı yerde. Baştaki / Pages'te depo adını atlar.

Bu derste bütün yolları baştan doğru yazdık: stil.css, js/uygulama.js, ./, ?id=…. Hiçbirinin başında / yok. Ama internetteki örneklerin çoğu / ile başlar ve onları kopyalayınca tuzağa düşersin.

Sık hataPages'te stil ve betik gelmiyor: 404 (Not Found)

Ne zaman çıkar? index.html'de yolları / ile başlatınca: href="/stil.css", src="/js/uygulama.js". Bilgisayarında her şey çalışır; Pages'te sayfa çıplak HTML gibi görünür ve "Yükleniyor…" takılı kalır.

Pages'teki durumu bilgisayarında da görebilirsin: terminalde cd C:\kod ile bir üst klasöre geç, orada npx serve çalıştır ve http://localhost:3000/ansiklopedi/ adresini aç. Böylece sayfan Pages'teki gibi bir alt klasörde durur. Sondaki / önemli: Pages onu kendisi ekler, serve eklemez; /ansiklopedi yazarsan doğru sürüm de bozuk görünür. Bozuk sürümü böyle açtığımızda gördüğümüz:

localhost:3000/ansiklopedi/
Tasarımsız beyaz sayfa: en üstte altı çizili mavi Şampiyon Ansiklopedisi bağlantısı, altında Yükleniyor… yazısı ve Riot Games ile ilgili iki paragraf. Kart, filtre, renk yok.
Stil dosyası gelmediği için sayfa tarayıcının varsayılan görünümünde; betik de gelmediği için Yükleniyor… takılı kaldı.
Tarayıcı konsolu
Failed to load resource: the server responded with a status of 404 (Not Found)Failed to load resource: the server responded with a status of 404 (Not Found)

Network sekmesi iki kırmızı satırda adresleri gösterir: localhost:3000/stil.css ve localhost:3000/js/uygulama.js. İkisinde de ansiklopedi/ yok. (Bir de favicon.ico için üçüncü bir 404 görebilirsin; o her zamanki zararsız satır.)

Nasıl düzelir? Yolların başındaki /'yi sil: href="stil.css", src="js/uygulama.js". Kaydet, git add ., git commit, git push yap; Pages birkaç dakika içinde yeni sürümü yayınlar. Sunucuyu terminalde Ctrl+C ile durdurmayı unutma.

Sık hata404 There isn't a GitHub Pages site here.

Ne zaman çıkar? Site adresini açınca sayfa yerine GitHub'ın kendi 404 sayfası geliyor ve "There isn't a GitHub Pages site here." ("burada bir GitHub Pages sitesi yok") yazıyorsa. Üç olası sebep:

  • Pages ayarında Save'e basmadın ya da yayın daha bitmedi. Actions sekmesindeki iş bitene kadar bekle.
  • Adreste kullanıcı adını ya da depo adını yanlış yazdın (ansiklopedi yerine ansklopedi gibi).
  • Depo gizli (Private). Ücretsiz hesapta Pages yalnızca herkese açık depolarda çalışır.

Nasıl düzelir? Pages ayarlarındaki adresi kopyalayıp aç; kendin yazma. Ayar sayfasında adres görünmüyorsa yayın daha bitmemiştir.

README: ekran görüntüsü ve canlı bağlantı

Deponu açan biri (sen bir yıl sonra, bir arkadaşın ya da bir işveren) ilk olarak README'yi görür. Ders 7'de README'yi ve Markdown'ı, Ders 17'de iyi bir README'nin cevapladığı soruları gördün. Bir web projesinin README'sinde iki şey daha olmalı: tıklanabilir canlı bağlantı ve bir ekran görüntüsü.

Ekran görüntüsü almak: Canlı sitende güzel görünen bir an seç; birkaç kartı favorile ki yıldızlar görünsün. Windows'ta Win+Shift+S ile Ekran Alıntısı Aracı açılır; sayfanın alanını fareyle seç. Köşede çıkan bildirime tıkla ve açılan pencerede Ctrl+S ile proje klasöründe yeni açacağın img klasörüne ekran.png adıyla kaydet. İstersen Chrome'un geliştirici araçlarında Ctrl+Shift+P ile komut menüsünü açıp "screenshot" yazarak da görüntü alabilirsin (menü adları sürüme göre değişebilir).

Sonra proje klasörünün köküne README.md dosyasını oluştur:

README.md
# Şampiyon Ansiklopedisi League of Legends şampiyonlarını listeleyen, arayan, role göre süzen ve favorilere ekleyen küçük bir web uygulaması. Veriler Riot Games'in herkese açık Data Dragon servisinden gelir. **Canlı sürüm:** https://KULLANICI.github.io/ansiklopedi/ ![Liste görünümü: arama kutusu, rol filtresi ve şampiyon kartları](img/ekran.png) ## Özellikler - Ada ya da unvana göre arama (Türkçe I ve ı harfleri doğru eşleşir)- Rol filtresi ve yalnız favorileri gösterme- Favoriler tarayıcıda saklanır (localStorage)- Her şampiyonun kendi adresi var: `?id=Ahri`- Telefonda da çalışır, klavyeyle gezilebilir ## Kullanılanlar HTML, CSS (grid ve flexbox), JavaScript (ES modülleri, fetch, localStorage). Kütüphane yok. ## Bilgisayarında çalıştırmak 1. Depoyu indir ve klasörü VS Code'da aç.2. Sayfayı Live Server ile aç ya da klasörde `npx serve` komutunu çalıştır.3. `index.html`'e çift tıklayarak açma: modüller `file://` adresinde çalışmaz. ## Neler öğrendim? Bir projeyi özellik listesi ve tel kafesle planlamayı, kodu tarayıcıda ES modüllerine bölmeyi, seçimi adreste tutmayı (`URLSearchParams`), erişilebilirlik denetimini ve GitHub Pages ile yayına almayı. --- Şampiyon Ansiklopedisi was created under Riot Games' "Legal Jibber Jabber" policy using assets owned by Riot Games. Riot Games does not endorse or sponsor this project.
  • Canlı bağlantı en üstte. README'yi okuyan çoğu kişi yalnızca ilk ekranı görür. KULLANICI yerine kendi adını yazmayı unutma ve bağlantıya bir kez tıklayıp çalıştığından emin ol.
  • Resim: ![alt metin](yol). Bağlantı yazımının başına ! koyunca Markdown resmi gösterir. Köşeli parantezdeki metin resmin alt metni; README'de de erişilebilirlik geçerli.
  • Resmin yolu img/ekran.png: README'deki yollar da göreli; GitHub onları README dosyasının bulunduğu yere göre çözer. Başına / koymadık.
  • Riot notu en altta. Depo sayfası da projenin bir yüzü; not orada da görünsün.

Bağlantıyı deponun ana sayfasında da göster: sağdaki About kutusunun dişli simgesine tıkla, açılan pencerede Website alanına site adresini yaz ve kaydet (arayüz değişmiş olabilir). Depo listesinde ve aramalarda projenin yanında bu bağlantı görünür.

Son olarak kaydet ve gönder. Pages, her git push'tan sonra siteyi kendiliğinden yeniden yayınlar:

Terminal — PowerShell
PS C:\kod\ansiklopedi> git add .PS C:\kod\ansiklopedi> git commit -m "README: ekran görüntüsü ve canlı bağlantı"[main 28b4eef] README: ekran görüntüsü ve canlı bağlantı 2 files changed, 33 insertions(+) create mode 100644 README.md create mode 100644 img/ekran.pngPS C:\kod\ansiklopedi> git push

Resim dosyası satırlardan oluşmadığı için 33 insertions yalnızca README'nin satırları. Tebrik etmek için erken; önce dene. Canlı adresi telefonunda aç, bir şampiyonu favorile, detayına git, geri dön. Bu dört adım telefonda da çalışıyorsa Blok 3'ün projesini bitirdin: internette, gerçek veriyle çalışan, herkesin açabildiği bir uygulaman var.

Görevler

Her görev projeyi bir özellikle büyütüyor. Görevleri sırayla yap; hepsi aynı dosyalara ekleniyor, her birini bitirince sayfayı tarayıcıda dene. Hepsi bitince dersin sonundaki kutuyla kaydet (istersen her görevden sonra ayrı bir commit de alabilirsin). Takılırsan ipuçlarını sırayla aç; çözüme en son bak.

Görev 1

Rastgele şampiyon

Filtrelerin yanına bir Rastgele şampiyon düğmesi ekle. Basınca o an listede görünen şampiyonlardan birinin detay sayfası açılsın. Beklenen davranış:

  • Hiçbir filtre yokken her basışta farklı (rastgele) bir şampiyon açılır.
  • Arama kutusunda ahri yazıyorken her basışta Ahri açılır.
  • Arama zzz iken (liste boş) sayfa değişmez, mesaj alanında "Seçilecek şampiyon yok. Önce filtreleri gevşet." yazar ve konsolda hata çıkmaz.

Son madde bir sınır değeri. Boş bir listeden rastgele seçmeye kalkınca ne olduğunu, kodu yazmadan önce ders25.js'de gör:

ders25.js
const gorunenler = []const sira = Math.floor(Math.random() * gorunenler.length)console.log(sira, gorunenler[sira])console.log(gorunenler[sira].id)
Hata çıktısı
0 undefinedC:\kod\ogren\ders25.js:4console.log(gorunenler[sira].id)                             ^ TypeError: Cannot read properties of undefined (reading 'id')    at Object.<anonymous> (C:\kod\ogren\ders25.js:4:30)    ... Node.js v24.20.0

Boş dizide sıra numarası 0 çıktı ama 0. eleman yok: gorunenler[0] undefined, onun id'sini okumak da TypeError. Senin düğmen bu durumda hata vermemeli.

İpucu 1

Düğmeyi formun içine koyacaksan type="button" vermeyi unutma; yoksa form gönderilir. Stil için .mesaj button kuralının seçicisine virgülle .rastgele'yi ekleyebilirsin.

İpucu 2

Görünen liste hazır: gorunenleriBul(favorileriOku()). Rastgele sıra numarası için Ders 6'daki Math.floor(Math.random() * uzunluk) kalıbını kullan.

İpucu 3

Başka bir sayfaya JavaScript'ten gitmek için location'ın href alanını değiştir: location.href = "?id=Ahri". Tarayıcı bağlantıya tıklanmış gibi o adrese gider. Listenin boş olduğu durumu bu satırdan önce if (gorunenler.length === 0) ile yakala ve return ile çık (Ders 6'daki erken çıkış).

Çözümü göster

index.html'de "Yalnız favoriler" etiketinin altına, formun içine:

HTML
<button class="rastgele" id="rastgele" type="button">Rastgele şampiyon</button>

stil.css'te düğme stilini paylaştır:

CSS
.mesaj button,.rastgele {

js/uygulama.js'de yıldız dinleyicisinin (liste.addEventListener("click", …)) üstüne:

JavaScript
document.querySelector("#rastgele").addEventListener("click", () => {    const gorunenler = gorunenleriBul(favorileriOku())    if (gorunenler.length === 0) {        mesajGoster("Seçilecek şampiyon yok. Önce filtreleri gevşet.")        return    }    const sira = Math.floor(Math.random() * gorunenler.length)    location.href = `?id=${gorunenler[sira].id}`})

Tarayıcıda denediğimizde: filtresiz dört basış Rammus, Kayle, Ryze ve Neeko'yu açtı; ahri aramasıyla üç basışın üçü de ?id=Ahri'ye gitti; zzz aramasında adres değişmedi, mesaj çıktı ve konsol temiz kaldı.

Math.random() hiçbir zaman tam 1 vermediği için dolu bir listede sıra numarası en fazla uzunluk - 1 olur; taşma yalnızca boş listede çıkar.

Görev 2

Zorluk filtresi

Rol seçiminin yanına bir Zorluk seçimi ekle: Hepsi, Kolay (1–3), Orta (4–7), Zor (8–10). Zorluk değeri her şampiyonun info.difficulty alanında. Seçim, arama ve rol filtreleriyle birlikte çalışmalı. Bitirince şu soruyu cevapla: Kolay, Orta ve Zor'un sayılarını topladığında neden toplam şampiyon sayısını bulamıyorsun?

İpucu 1

Rol filtresini nasıl yaptıysan aynısı: HTML'de bir <select id="zorluk">, durum objesinde zorluk: "", bir change dinleyicisi ve uyuyorMu içinde dördüncü bir koşul. secim objesi durum'un kopyası olduğu için secim.zorluk kendiliğinden gelir.

İpucu 2

Aralık denetimini ayrı bir fonksiyona koy: zorlugaUyuyor(zorluk, secilen). "Kolay" için zorluk >= 1 && zorluk <= 3. Sınırları (3 ve 4, 7 ve 8) Node'da tek tek dene.

Çözümü göster

index.html'de rol seçiminin <div class="alan">'ının altına:

HTML
<div class="alan">    <label for="zorluk">Zorluk</label>    <select id="zorluk">        <option value="">Hepsi</option>        <option value="kolay">Kolay (1–3)</option>        <option value="orta">Orta (4–7)</option>        <option value="zor">Zor (8–10)</option>    </select></div>

stil.css'te #arama, #rol seçicisine #zorluk'u ekle. js/uygulama.js'de durum objesine zorluk: "" alanını (bir önceki satırın sonuna virgül koymayı unutma), uyuyorMu'nun üstüne de bu fonksiyonu ekle:

ders25.js
function zorlugaUyuyor(zorluk, secilen) {    if (secilen === "kolay") {        return zorluk >= 1 && zorluk <= 3    }    if (secilen === "orta") {        return zorluk >= 4 && zorluk <= 7    }    if (secilen === "zor") {        return zorluk >= 8    }    return true} for (const zorluk of [0, 1, 3, 4, 7, 8, 10]) {    const uyanlar = ["kolay", "orta", "zor"].filter((s) => zorlugaUyuyor(zorluk, s))    console.log(zorluk, uyanlar)}
Çıktı
0 []1 [ 'kolay' ]3 [ 'kolay' ]4 [ 'orta' ]7 [ 'orta' ]8 [ 'zor' ]10 [ 'zor' ]

Alttaki döngü yalnızca deneme için; uygulamaya fonksiyonu kopyala, döngüyü değil. Sonra uyuyorMu'nun son satırını değiştir ve dinleyiciyi ekle:

JavaScript
    const zorlukUyuyor = zorlugaUyuyor(sampiyon.info.difficulty, secim.zorluk)    return metinUyuyor && rolUyuyor && favoriUyuyor && zorlukUyuyor
JavaScript
document.querySelector("#zorluk").addEventListener("change", (olay) => {    durum.zorluk = olay.target.value    listeyiGuncelle()})

Tarayıcıda (sürüm 16.20.1) Kolay 24, Orta 104, Zor 41 şampiyon gösterdi; Zor ile Büyücü birlikte 23. Toplam 169, yani 173'ten 4 eksik.

Sebep yukarıdaki çıktının ilk satırında: zorluğu 0 olan şampiyon hiçbir gruba girmiyor. Data Dragon'da Akshan, Rell, Seraphine ve Vex'in bütün info değerleri 0; bu "çok kolay" değil, "bilgi yok" demek. Bu yüzden 0'ı "Kolay"a katmadık. İstersen "Bilinmiyor" diye beşinci bir seçenek ekleyebilirsin.

Görev 3

Sıralama

Bir Sırala seçimi ekle: "Ada göre (A→Z)", "Ada göre (Z→A)" ve "Zorluğa göre". Varsayılan A→Z olsun. Şu sınır durumlarını dene ve doğru çalıştığından emin ol:

  • A→Z sıralamada Wukong nerede? (İpucu: veride id'si MonkeyKing.)
  • "Dr. Mundo" ile "Draven" hangi sırada?
  • Zorluğa göre sıralamada zorluğu bilinmeyen (0) şampiyonlar en başta mı, en sonda mı olmalı? Aynı zorluktakiler hangi sırada?
İpucu 1

Data Dragon şampiyonları id'ye göre sıralı verir; bu yüzden şimdiki listede Wukong, M harfindeki şampiyonların arasında duruyor. Ada göre sıralamak için Ders 11'deki toSorted ve karşılaştırma fonksiyonunu kullan: (a, b) => a.name.localeCompare(b.name, "tr").

İpucu 2

Sıralamayı listeyiGuncelle içinde, görünenleri bulduktan sonra yap: sirala(gorunenleriBul(favoriler), durum.siralama). toSorted diziyi değiştirmeden yeni bir dizi döndürür; durum.sampiyonlar'ın sırası bozulmaz.

İpucu 3

Zorlukta karşılaştırma fonksiyonu iki kademeli olmalı: önce zorluğa, eşitse ada bak. Bilinmeyenleri sona atmak için 0'ı karşılaştırırken 99 gibi büyük bir sayı say.

Çözümü göster

index.html'e zorluk seçiminin altına yeni bir <div class="alan">:

HTML
<div class="alan">    <label for="sirala">Sırala</label>    <select id="sirala">        <option value="ad">Ada göre (A→Z)</option>        <option value="ad-ters">Ada göre (Z→A)</option>        <option value="zorluk">Zorluğa göre</option>    </select></div>

stil.css'te seçiciye #sirala'yı ekle. durum'a siralama: "ad" alanını ekle. Karşılaştırma fonksiyonunu önce Node'da dene:

ders25.js
const ornekler = [    { name: "Wukong", info: { difficulty: 3 } },    { name: "Draven", info: { difficulty: 8 } },    { name: "Dr. Mundo", info: { difficulty: 5 } },    { name: "Akshan", info: { difficulty: 0 } },    { name: "Morgana", info: { difficulty: 1 } },    { name: "Miss Fortune", info: { difficulty: 1 } }] function zorlugaGore(a, b) {    const zorlukA = a.info.difficulty === 0 ? 99 : a.info.difficulty    const zorlukB = b.info.difficulty === 0 ? 99 : b.info.difficulty    if (zorlukA !== zorlukB) {        return zorlukA - zorlukB    }    return a.name.localeCompare(b.name, "tr")} const adlar = (liste) => liste.map((s) => s.name).join(", ")console.log(adlar(ornekler.toSorted((a, b) => a.name.localeCompare(b.name, "tr"))))console.log(adlar(ornekler.toSorted((a, b) => b.name.localeCompare(a.name, "tr"))))console.log(adlar(ornekler.toSorted(zorlugaGore)))console.log(adlar(ornekler))
Çıktı
Akshan, Dr. Mundo, Draven, Miss Fortune, Morgana, WukongWukong, Morgana, Miss Fortune, Draven, Dr. Mundo, AkshanMiss Fortune, Morgana, Wukong, Dr. Mundo, Draven, AkshanWukong, Draven, Dr. Mundo, Akshan, Morgana, Miss Fortune

Son satır önemli: toSorted asıl diziyi değiştirmedi. "Dr. Mundo"nun "Draven"dan önce gelmesinin sebebi noktanın harflerden önce sayılması. Uygulamaya zorlugaGore'yi ve şu fonksiyonu listeyiGuncelle'nin üstüne koy:

JavaScript
function sirala(sampiyonlar, siralama) {    if (siralama === "zorluk") {        return sampiyonlar.toSorted(zorlugaGore)    }    if (siralama === "ad-ters") {        return sampiyonlar.toSorted((a, b) => b.name.localeCompare(a.name, "tr"))    }    return sampiyonlar.toSorted((a, b) => a.name.localeCompare(b.name, "tr"))}

listeyiGuncelle'deki satır şöyle olur: const gorunenler = sirala(gorunenleriBul(favoriler), durum.siralama). #sirala için de diğerleri gibi bir change dinleyicisi ekle (durum.siralama = olay.target.value). Durumdaki alanın adını sira değil siralama koyduk: Ders 5'ten beri sira bizim için "sıra numarası" demek ve Görev 1'de bu anlamda kullandık.

Tarayıcıda A→Z sıralamada Wukong, Warwick ile Xayah arasına geçti; Dr. Mundo 28., Draven 29. sırada.

Zorluğa göre liste Miss Fortune ve Morgana ile (1) başlayıp Rumble ve Yasuo (10) ile bitti; en sonda zorluğu bilinmeyen Akshan, Rell, Seraphine ve Vex var. 0'ı büyük sayı saymasaydık bu dördü "en kolay" diye listenin başına çıkardı.

Görev 4

Koyu tema

Üst çubuğa bir Koyu tema düğmesi ekle. Basınca sayfa koyu renklere geçsin, bir daha basınca açığa dönsün. Seçim sayfa yenilense de hatırlansın. Şu durumları dene:

  • Siteye ilk kez gelen biri (kayıt yok) açık temayı görür.
  • Koyu temadayken sayfayı yenileyince koyu kalır.
  • localStorage'daki kayıt bozuksa (elle mor yazıldıysa) sayfa açık temada açılır, çökmez.
  • Ekran okuyucu düğmenin basılı olup olmadığını bilir.
İpucu 1

Renkleri zaten CSS değişkenlerinde tuttuk (Ders 21). :root[data-tema="koyu"] { … } kuralında aynı değişkenlere koyu değerler ver; JavaScript yalnızca <html> öğesine data-tema="koyu" niteliğini ekleyip kaldırsın. <html> öğesi JavaScript'te document.documentElement'tir: document.documentElement.dataset.tema = "koyu" ekler, delete document.documentElement.dataset.tema siler (Ders 4'teki delete).

İpucu 2

Bu iş ayrı bir modüle yakışır: js/tema.js. İçinde temayiBaslat() adında tek bir fonksiyonu dışa aktar; uygulama.js onu içe aktarıp en başta çağırsın. Düğmenin durumu için favori yıldızındaki gibi aria-pressed kullan; düğme yazısı hep "Koyu tema" kalsın.

Çözümü göster

index.html'de logonun altına:

HTML
<button class="tema" id="tema" type="button" aria-pressed="false">Koyu tema</button>

stil.css'te :root kuralının altına koyu değerleri, .ust kuralının yerine de düğmeli hâlini yaz:

CSS
:root[data-tema="koyu"] {    --zemin: #14161f;    --kart: #1d2030;    --yazi: #e6e8f0;    --soluk: #9aa1b5;    --vurgu: #a98bff;    --cizgi: #343849;    --yildiz: #e6b84a;    color-scheme: dark;} .ust {    display: flex;    justify-content: space-between;    align-items: center;    padding: 8px 16px;    background: #1c1f2b;} .tema {    min-height: 44px;    padding: 6px 14px;    border: 1px solid #ffffff;    border-radius: 8px;    background: transparent;    color: #ffffff;    font: inherit;    cursor: pointer;} .tema[aria-pressed="true"] {    background: #ffffff;    color: #1c1f2b;}

color-scheme: dark tarayıcıya "bu sayfa koyu" der; onay kutusunu, açılır listeyi ve kaydırma çubuğunu da koyu çizer. .tema[aria-pressed="true"] bir nitelik seçicisi: basılıyken düğme beyaz dolar.

Yeni dosya:

js/tema.js
const ANAHTAR = "ansiklopedi.tema"const dugme = document.querySelector("#tema") function temayiUygula(koyu) {    if (koyu) {        document.documentElement.dataset.tema = "koyu"    } else {        delete document.documentElement.dataset.tema    }    dugme.setAttribute("aria-pressed", String(koyu))} export function temayiBaslat() {    temayiUygula(localStorage.getItem(ANAHTAR) === "koyu")    dugme.addEventListener("click", () => {        const koyu = dugme.getAttribute("aria-pressed") === "false"        temayiUygula(koyu)        localStorage.setItem(ANAHTAR, koyu ? "koyu" : "acik")    })}

getAttribute, setAttribute'un okuyan kardeşi: niteliğin o anki değerini metin olarak verir. js/uygulama.js'nin başına, içe aktarmaların altına:

JavaScript
import { temayiBaslat } from "./tema.js" temayiBaslat()

Tarayıcıda ilk ziyarette kayıt null'dı ve sayfa açık açıldı. Düğmeye basınca <html>'e data-tema="koyu" geldi, kayda koyu yazıldı, zemin rgb(20, 22, 31) oldu; yenileyince koyu kaldı.

Tekrar basınca kayıt acik oldu. Kayda elle mor yazıp yenilediğimizde sayfa açık açıldı: === "koyu" karşılaştırması tanımadığı her değeri "koyu değil" sayıyor. Koyu temada renk oranları da yeterli: ana yazı zeminde 14,74:1, soluk yazı kartta 6,25:1, bağlantı zeminde 6,72:1.

Koyu temada liste: koyu lacivert zemin, koyu kartlar, açık renk yazılar. Üst çubukta basılı görünen beyaz Koyu tema düğmesi; filtrelerde Ara, Rol, Zorluk: Orta (4–7), Sırala: Ada göre (A→Z), Yalnız favoriler ve Rastgele şampiyon düğmesi; sayaç Gösterilen: 104 / 173.
Görev 5

Karşılaştırma

Detay sayfasına bir Karşılaştır seçimi ekle: ikinci bir şampiyon seçip Göster'e basınca adres ?id=Ahri&karsi=Lux olsun ve sayfada iki şampiyonun saldırı, savunma, büyü, zorluk, can, zırh, saldırı menzili ve hareket hızı değerlerini yan yana gösteren bir tablo çıksın. Seçim listesinde şampiyonun kendisi olmasın. Şu adresleri de dene:

  • ?id=Ahri&karsi=Ahri: kendisiyle karşılaştırma. Tablo çıkmamalı, açıklayıcı bir mesaj çıkmalı.
  • ?id=Ahri&karsi=lux: olmayan bir id. Detay yine görünmeli, mesaj çıkmalı.
  • ?id=Ahri&karsi=: boş seçim. Ne tablo ne mesaj.
İpucu 1

Adresi değiştirmek için JavaScript'e gerek yok. Ders 23'teki form gönderimini hatırla: <form>'un varsayılan yöntemi GET'tir ve form gönderilince içindeki name'li alanlar adrese yazılır. Formda name="id" olan gizli bir alan (type="hidden") ve name="karsi" olan bir <select> olursa adres kendiliğinden ?id=Ahri&karsi=Lux olur.

İpucu 2

Karşılaştırılacak sayıların hepsi champion.json'da var; ikinci şampiyon için ayrı istek atmana gerek yok. detayiAc içinde listeyi de çek (sampiyonlariGetir), <select>'i doldur, parametreler.get("karsi") ile seçimi oku ve find ile ikinci şampiyonu bul.

İpucu 3

Tablo satırlarını sekiz kez elle yazma. [ekrandaki ad, değeri veren fonksiyon] çiftlerinden bir dizi kur: ["Can", (s) => s.stats.hp]. Fonksiyonlar da değerdir (Ders 10); diziye konabilir ve döngüde çağrılabilir.

Çözümü göster

index.html'de <dl class="olculer" …></dl> satırının altına:

HTML
<form class="karsilastir">    <input type="hidden" name="id" id="karsilastir-id">    <label for="karsi">Karşılaştır:</label>    <select name="karsi" id="karsi">        <option value="">Şampiyon seç</option>    </select>    <button type="submit">Göster</button></form><div id="karsilastirma"></div>

stil.css'te #sirala seçicisine #karsi'yi, .rastgele seçicisine .karsilastir button'u ekle ve tablo için:

CSS
.karsilastir {    display: flex;    flex-wrap: wrap;    align-items: center;    gap: 8px;} .karsilastirma {    margin-top: 12px;    border-collapse: collapse;} .karsilastirma caption {    font-weight: 700;    text-align: left;} .karsilastirma th,.karsilastirma td {    padding: 6px 12px;    border-bottom: 1px solid var(--cizgi);    text-align: left;}

js/arayuz.js'nin en sonuna:

JavaScript
const SATIRLAR = [    ["Saldırı", (s) => s.info.attack],    ["Savunma", (s) => s.info.defense],    ["Büyü", (s) => s.info.magic],    ["Zorluk", (s) => s.info.difficulty],    ["Can", (s) => s.stats.hp],    ["Zırh", (s) => s.stats.armor],    ["Saldırı menzili", (s) => s.stats.attackrange],    ["Hareket hızı", (s) => s.stats.movespeed]] export function karsilastirmaCiz(birinci, ikinci) {    const tablo = oge("table", "karsilastirma")    tablo.append(oge("caption", "", `${birinci.name} ile ${ikinci.name}`))    const baslik = oge("tr")    baslik.append(oge("th", "", "Özellik"), oge("th", "", birinci.name), oge("th", "", ikinci.name))    tablo.append(baslik)    for (const [ad, deger] of SATIRLAR) {        const satir = oge("tr")        satir.append(oge("th", "", ad), oge("td", "", deger(birinci)), oge("td", "", deger(ikinci)))        tablo.append(satir)    }    document.querySelector("#karsilastirma").append(tablo)}

<caption> tablonun başlığıdır (Ders 19); ekran okuyucu tabloya gelince önce onu okur. js/uygulama.js'de 2. satırdaki içe aktarmaya karsilastirmaCiz'i ekle; detayiAc'ın üstüne şu fonksiyonu yaz:

JavaScript
function karsilastirmayiHazirla(tumu, sampiyon) {    document.querySelector("#karsilastir-id").value = sampiyon.id    const secim = document.querySelector("#karsi")    for (const diger of tumu) {        if (diger.id !== sampiyon.id) {            const secenek = document.createElement("option")            secenek.value = diger.id            secenek.textContent = diger.name            secim.append(secenek)        }    }    const karsiId = parametreler.get("karsi")    if (!karsiId) {        return    }    const karsi = tumu.find((s) => s.id === karsiId)    if (!karsi || karsi.id === sampiyon.id) {        mesajGoster(`Karşılaştırma yapılamadı: "${karsiId}" listede yok ya da aynı şampiyon.`)        return    }    secim.value = karsi.id    karsilastirmaCiz(sampiyon, karsi)}

detayiAc içinde mesajGoster("") satırının altına da: karsilastirmayiHazirla(await sampiyonlariGetir(surum), sampiyon).

parametreler dosyanın en altında tanımlı ama sorun değil: bu fonksiyon veri geldikten sonra çalışır, o sırada dosyanın tamamı çoktan okunmuştur. secim.value = karsi.id satırı, sayfa açıldığında seçim kutusunda Lux'un seçili görünmesini sağlar.

Tarayıcıda seçim kutusunda 173 seçenek çıktı: "Şampiyon seç" ve Ahri dışındaki 172 şampiyon. Lux'u seçip Göster'e basınca adres index.html?id=Ahri&karsi=Lux oldu ve tablo geldi:

Ahri'nin detayında ölçülerin altında Karşılaştır: Lux seçimi ve Göster düğmesi; altında Ahri ile Lux başlıklı tablo: Saldırı 3 ve 2, Savunma 4 ve 4, Büyü 8 ve 9, Zorluk 5 ve 5, Can 590 ve 580, Zırh 21 ve 21, Saldırı menzili 550 ve 550, Hareket hızı 330 ve 330.

Sınır denemeleri: karsi=Ahri ve karsi=lux adreslerinde tablo çıkmadı, mesaj alanında "Karşılaştırma yapılamadı: … listede yok ya da aynı şampiyon." yazdı ve detay görünür kaldı. karsi= (boş) adresinde ne tablo ne mesaj çıktı, çünkü boş metin if (!karsiId) ile erken dönüyor.

Kendini sına

Soru 1

Bu kod tarayıcıda çalışınca konsola ne yazar?

JavaScript
const parametreler = new URLSearchParams("?id=Garen&rol=Tank")console.log(parametreler.get("rol"))console.log(parametreler.get("Rol"))console.log(parametreler.get("hp"))
Cevabı göster

Tank, null, null. Adlar büyük-küçük harfe duyarlı olduğu için Rol bulunmaz; adreste hiç olmayan hp de null verir. null gelme ihtimali olan her değeri kullanmadan önce denetlemen gerekir.

Soru 2

Siten https://kullanici.github.io/ansiklopedi/ adresinde. index.html içinde <img src="/img/logo.png" alt=""> yazıyor. Tarayıcı resmi hangi adresten ister, resim gelir mi?

Cevabı göster

https://kullanici.github.io/img/logo.png adresinden ister ve resim gelmez (404). Baştaki / alan adının köküne gider ve depo adını (ansiklopedi/) atlar. Doğrusu src="img/logo.png": o zaman https://kullanici.github.io/ansiklopedi/img/logo.png istenir.

Soru 3

js/uygulama.js dosyasında import { sonSurumuGetir } from "../api.js" yazarsan tarayıcı hangi dosyayı arar?

Cevabı göster

Proje kökündeki api.js'yi (ansiklopedi/api.js) arar. Yol, import'u yazan dosyanın bulunduğu js/ klasöründen başlar; ../ bir üst klasöre çıkar. api.js js/ klasöründe olduğu için doğrusu "./api.js". Dosya orada olmadığı için konsolda 404 (Not Found) görürsün.

Soru 4

favoriDegistir içinde JSON.stringify unutulmuş: localStorage.setItem(ANAHTAR, favoriler). Hiç favorisi olmayan biri önce Ahri'nin, sonra Lux'un yıldızına basıyor. Kayıtta en sonunda ne yazar? Sayfayı yenileyince hangi yıldızlar dolu olur?

Cevabı göster

Kayıtta Lux yazar ve yenileyince hiçbir yıldız dolu olmaz. Ahri'ye basınca dizi ["Ahri"] metne çevrilip Ahri diye kaydedilir. Lux'a basınca favorileriOku bu kaydı JSON.parse ile okuyamaz, catch boş liste döndürür; liste ["Lux"] olur ve kayda Lux yazılır.

Yenileyince Lux metni de geçerli bir JSON olmadığı için yine boş liste gelir. Basış anında iki yıldız da dolu göründüğü için hata ancak yenileyince fark edilir. Bunu ders içinde tarayıcıda gerçekten denedik.

Soru 5

Yıldız için <button> yerine <span class="favori">☆</span> yazıp ona click dinleyicisi koysak fareyle yine çalışır. Neden yine de <button> kullanıyoruz?

Cevabı göster

<span> klavyeyle odaklanamaz: Tab onun üstünde durmaz, Space ya da Enter ona basmaz. Ekran okuyucu da onun bir düğme olduğunu bilmez, yalnızca yıldız karakterini okur. <button> bunların hepsini hazır getirir; biz yalnızca aria-label ve aria-pressed ile adını ve durumunu ekledik.

Soru 6

Kartlardaki resme neden alt="" verdik? alt'ı hiç yazmasaydık ya da alt="Ahri" yazsaydık ne olurdu?

Cevabı göster

Kartta şampiyonun adı resmin hemen altında yazıyor; resim yeni bir bilgi taşımıyor. alt="", ekran okuyucuya "bu süs, atla" der. alt="Ahri" yazsaydık bağlantı okunurken "Ahri" iki kez söylenirdi. alt'ı hiç yazmasaydık ekran okuyucu çoğu zaman dosya adını okurdu: "Ahri.png". Detaydaki büyük resim ise şampiyonun görünüşünü anlattığı için dolu alt aldı.

Soru 7

Sayfayı açınca "Yükleniyor…" yazısı hiç kaybolmuyor ve konsolda Access to script at 'file:///C:/kod/ansiklopedi/js/uygulama.js' from origin 'null' has been blocked by CORS policy yazıyor. Ne yaptın, ne yapmalısın?

Cevabı göster

index.html'i çift tıklayarak file:/// ile açtın. Tarayıcı modül betiklerini diskten yüklemez; uygulama.js hiç çalışmadığı için HTML'deki "Yükleniyor…" yazısı olduğu gibi kalır. Sayfayı Live Server ile (ya da npx serve ile) açıp adresin http:// ile başladığından emin olmalısın.

Soru 8

Pages'teki site adresin "404 There isn't a GitHub Pages site here." diyor. Kontrol edeceğin üç şey ne?

Cevabı göster
  1. Pages ayarında kaynağı seçip Save'e bastın mı ve Actions sekmesindeki yayın işi bitti mi? 2) Adresteki kullanıcı adı ve depo adı doğru mu? En güvenlisi adresi Pages ayar sayfasından kopyalamak. 3) Depo herkese açık (Public) mı? Ücretsiz hesapta Pages yalnızca herkese açık depolarda çalışır.

Bir sonraki derste Blok 4'e, sunucu tarafına geçiyorsun: fetch'in perde arkasında gönderdiği HTTP isteğinin bütün parçalarını (metotlar, durum kodları, başlıklar) inceleyecek ve terminalden kendin istek atacaksın.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.