Skip to content

Kurs Keşfet ve Arama (Course Discovery)

Kurs listesi (courses.index / courses.all), 3 seviyeli kategorizasyon, dinamik kurs etiket (badge) sistemi, full-text arama, FilterSidebar filtreleme, keşfet bölümleri, anasayfa kurs seçimi (CourseSelection) ve loader veri yapısı.

Bu sayfa Achidemy’de kurs keşfet ve arama akışını açıklar: hangi URL’lerin hangi route’u kullandığı, üç seviyeli kategorizasyon (URL → categoryIds, breadcrumb), dinamik kurs etiket (badge) sistemi (En Çok Satan, Yeni, Popüler), arama sistemi (full-text, relevance sıralama), filtreleme sistemi (FilterSidebar, query parametreleri, loader koşulları), keşfet bölümleri (başlangıç önerileri, öne çıkan kurslar, popüler konular, popüler eğitmenler), anasayfa kurs seçimi (CourseSelection) — alt kategori bazlı en çok satan ve en popüler kurslar — ve loader veri yapısı.


Dosya: app/components/CourseSelection.tsx
Kullanım: Anasayfa (_index.tsx vb.) üzerinde “Kurslar” / kategorilere göre öne çıkan kurslar bölümü.

  • Sekmeler: Alt kategoriler (seviye 2) sekme olarak listelenir; her sekmede o alt kategoriye ait kurs sayısı (badge) gösterilir.
  • Filtreleme: Aktif sekmeye göre kurslar filtrelenir: kursun categoryId’si, ya o alt kategorinin topic (seviye 3) id’lerinden biri ya da doğrudan alt kategori id’si olmalıdır. Sayı (count) hesaplaması da aynı kriterle yapılır.
  • Sıralama: Filtrelenen kurslar önce En çok satan (Bestseller) öncelikli, sonra popülerlik (rating × reviewsCount) yüksekten düşüğe sıralanır; en fazla 10 kurs alınır.
  • Boş durum: Seçili sekmede hiç kurs yoksa “bu kategoride kurs bulunamadı” benzeri varsayılan mesaj gösterilir; diğer kategorilerin kursları fallback olarak listelenmez.
  • Skeleton: Yükleme sırasında 10 adet kurs kartı iskeleti gösterilir.
  • categoryTree (veya eşdeğer) ile ana → alt → konu hiyerarşisi; courses listesi (GraphQL/loader’dan); activeTab (seçili alt kategori slug’ı). Badge bilgisi için Kurs Etiket Sistemi ile uyumlu course.badges, isBestSeller kullanılır.

Route Ayrımı: courses.index vs courses.all

Section titled “Route Ayrımı: courses.index vs courses.all”

Kurs listesi iki farklı route ile sunulur; URL’de kategori olup olmaması hangisinin çalışacağını belirler.

Özellikcourses.index.tsxcourses.all.tsx
URL/:lang/courses/:lang/courses/:category, /:category/:subcategory, /:category/:subcategory/:topic
Route dosyasıapp/routes/courses.index.tsxapp/routes/courses.all.tsx
paramsYokcategory, isteğe bağlı subcategory, topic
Kurs listesiTüm yayındaki kurslar (+ arama/filtre)Seçilen kategori/alt kategori/konuya ait kurslar (+ arama/filtre)
BreadcrumbAna Sayfa → KurslarAna Sayfa → Kurslar → [Ana Kategori] → [Alt Kategori] → [Konu]
Kategori gridVar (categoryTree — “Kategorilere Göz At”)Yok (zaten bir dalın içindesin)
Öne çıkan / popüler verilerPlatform geneliSeçili kategori bağlamında

Örnek URL’ler:

  • /tr/coursescourses.index (tüm kurslar, kategori ağacı gösterilir).
  • /tr/courses/developmentcourses.all (sadece Development ana kategorisi).
  • /tr/courses/development/data-sciencecourses.all (Data Science alt kategorisi).
  • /tr/courses/office-productivity/microsoft-office/excelcourses.all (Excel konusu).

Her iki route’ta da, “Başlangıç için önerilen kurslar”ın hemen altında aynı sırayla dört bölüm render edilir. Veriler loader’dan gelir; sıralama şöyledir:

  1. BeginnerRecommendationsSection — Başlangıç için önerilen kurslar (Popüler / Yeni sekmeleri).
  2. FeaturedCoursesSection — Öne çıkan kurslar (carousel, 1 kart/slide, yatay kart).
  3. PopularTopicsSection — Popüler konular (5x2 grid, konular kurs sayısına göre sıralı).
  4. PopularInstructorsSection — Popüler eğitmenler (kartlar, public profile linki, öğrenci sayısı, eğitmen puanı).
BileşenDosyaAçıklama
BeginnerRecommendationsSectionapp/components/courses/BeginnerRecommendationsSection.tsxBaşlangıç seviye kurslar; “Popüler” ve “Yeni” sekmeleri, grid’de kurs kartları.
FeaturedCoursesSectionapp/components/courses/FeaturedCoursesSection.tsxYatay kurs kartı carousel; 1 kart/slide, önceki/sonraki ok, klavye (Sol/Sağ ok) desteği.
PopularTopicsSectionapp/components/courses/PopularTopicsSection.tsxKonu etiketleri; 5 sütun x 2 satır grid (10 konu), kurs sayısına göre sıralı, link konu sayfasına.
PopularInstructorsSectionapp/components/courses/PopularInstructorsSection.tsxEğitmen kartları; profil resmi, isim, eğitmen puanı, öğrenci sayısı, kurs sayısı; username varsa /:lang/user/:username public profile linki.

courses.index ve courses.all loader’ları aşağıdaki anahtarları döner (bölümler buna göre beslenir):

  • beginnerRecommendations{ popular: CourseCardCourse[], newest: CourseCardCourse[] } (başlangıç seviye, popüler/yeni).
  • featuredCourses — Öne çıkan kurs listesi (FeaturedCourse; yatay kart için alanlar + totalLessons, totalDurationSeconds, displayPrice, badges — tek etiket sistemi ile hesaplanan bestSeller / new / popular).
  • popularTopics{ id, name, slug, courseCount, path }[] (path, konu sayfası URL’i; path dil öneki olmadan, örn. /courses/mainSlug/subSlug/topicSlug).
  • popularInstructors{ id, name, image, username, courseCount, avgRating, studentCount }[] (studentCount, enrollments üzerinden hesaplanır).

courses.all içinde öne çıkan / popüler veriler seçili kategori bağlamına göre filtrelenir; courses.index içinde platform genelinde hesaplanır.


Platformda kurs kartları ve kurs detay sayfasında gösterilen etiketler (badge’ler) tek bir merkezi mantıkla hesaplanır. Udemy / Coursera benzeri, dinamik ve genişletilebilir bir yapı kullanılır; veri kaynağı veritabanı (enrollments, kurs rating/reviewsCount, createdAt) olduğu için etiketler gerçek zamanlı veriye göre güncellenir.

  • Tek kaynak: Tüm etiket kararları app/lib/course-badges.ts içindeki getCourseBadges() fonksiyonu ve BADGE_CONFIG sabitleri ile yapılır.
  • Kullanım yerleri: Kurs listesi (courses.index, courses.all), öne çıkan / başlangıç önerileri, kurs detay sayfası (course.$id) — hepsi aynı badge listesini kullanır.
  • Çıktı: Her kurs için bir badge id listesi ("bestSeller" | "new" | "popular"). Sıra her yerde aynıdır: Best Seller → New → Popular (görsel öncelik ve tutarlılık için).
  • Görüntüleme: Kartlarda (CourseCard, CourseHorizontalCard) thumbnail üzerinde en fazla 3 etiket; detay sayfasında başlık altında aynı etiketler metin olarak gösterilir. Çeviriler course.badges.bestSeller, course.badges.new, course.badges.popular anahtarlarından gelir.
EtiketKoşulAçıklama
En Çok Satan (Best Seller)Konu (3. seviye kategori) bazında satış sayısına göre en yüksek ilk 5 kurs arasında olmak.Kursun categoryId’si (konu) ile gruplanan tüm yayındaki kurslar, enrollments tablosunda refunded_at IS NULL olan kayıt sayısına göre sıralanır; her konuda ilk 5 kurs “Best Seller” alır. Satış sayısı 0 olan kurslar dahil edilmez.
Yeni (New)Kursun yayına alındığı tarihten itibaren 14 gün boyunca.Tarih karşılaştırması için kursun createdAt alanı kullanılır. createdAt >= (bugün - 14 gün) ise “New” etiketi verilir. (İleride publishedAt eklense bile mantık tek yerden BADGE_CONFIG.NEW_DAYS ile yönetilir.)
Popüler (Popular)Aşağıdaki iki koşuldan en az biri sağlanmalı:1) İnceleme sayısı: reviewsCount >= 50 → tek başına popüler. 2) Puan + asgari inceleme: rating >= 4.5 ve reviewsCount >= 10 → popüler.

Dosya: app/lib/course-badges.ts

SabitVarsayılanAçıklama
NEW_DAYS14“Yeni” etiketinin gösterileceği gün sayısı (yayın tarihinden itibaren).
BEST_SELLER_TOP_N5Konu (category_id) başına “En Çok Satan” verilecek kurs sayısı (top 5).
POPULAR_MIN_REVIEWS50Sadece inceleme sayısı ile popüler sayılmak için minimum inceleme.
POPULAR_MIN_RATING4.5Puan kriterinde minimum ortalama puan.
POPULAR_MIN_REVIEWS_FOR_RATING10Puan kriteri (≥4.5) kullanılırken en az kaç inceleme olmalı.

Bu değerler değiştirilerek eşikler tek yerden güncellenir; loader’lara opsiyonel parametre ile farklı değerler de verilebilir.

  1. Best Seller listesi (bestSellerIds):
    Loader’da (courses.index, courses.all, course.$id) bir kez çalışan SQL: courses + enrollments (refunded_at IS NULL), PARTITION BY category_id, satış sayısına göre RANK, rank ≤ BEST_SELLER_TOP_N ve sales_count > 0 olan kurs id’leri bir Set’e alınır.

  2. Badge hesaplama:
    Her kurs için getCourseBadges(course, { bestSellerIds }) çağrılır. Fonksiyon kursun id, categoryId, rating, reviewsCount, createdAt alanlarını kullanır; yukarıdaki koşullara göre ["bestSeller", "new", "popular"] içinden uygun olanları sırayla döndürür.

  3. Loader çıktısı:
    Liste sayfalarında her kurs nesnesine badges dizisi ve (geriye dönük uyum için) isBestSeller eklenir. Kurs detay sayfasında loader doğrudan badges döner; UI’da bu dizi iterate edilerek tüm etiketler gösterilir.

  4. Bileşenler:
    CourseCard ve CourseHorizontalCard course.badges varsa bu diziden etiketleri render eder; yoksa eski davranış (yalnızca isBestSeller / “Önerilen”) korunur. course.$id sayfasında başlık altındaki etiket alanı tamamen badges dizisine göre çizilir.

  • Tek sistem: Tüm platformda etiket kararı getCourseBadges + BADGE_CONFIG ile alınır.
  • En Çok Satan: Konu (category_id) bazında satış sayısına göre ilk 5 kurs (enrollments, refunded_at IS NULL).
  • Yeni: Yayın tarihi (createdAt) üzerinden son 14 gün.
  • Popüler: 50+ inceleme veya (4.5+ puan ve 10+ inceleme).
  • Liste sayfaları ve kurs detay sayfası aynı badge listesini kullanır; çeviriler course.badges.* ile yapılır.

Kurslar üç seviyeli kategori ağacı ile sınıflandırılır. Veri yapısı categories tablosunda tutulur; her kayıt parentId ile üst kategoriye bağlanır.

SeviyeAçıklamaparentIdÖrnek slug
1 – Ana kategoriEn üst kapsayıcı (örn. Geliştirme, Ofis)nulldevelopment, office-productivity
2 – Alt kategoriAna kategorinin alt dalıAna kategorinin id’sidata-science, microsoft-office
3 – KonuKursun doğrudan atandığı yaprakAlt kategorinin id’sipython, excel

Kurs–kategori ilişkisi: Her kurs, kurs tablosundaki categoryId alanı ile tek bir kategoriye (genelde konu, yani 3. seviye) atanır. Listelemede “bu kategori ve altı” mantığı URL’den çıkarılan categoryIds listesi ile uygulanır.

URL → categoryIds Çözümlemesi (courses.all)

Section titled “URL → categoryIds Çözümlemesi (courses.all)”

Dosya: app/routes/courses.all.tsx loader.

  • Path parametreleri: params.category, params.subcategory, params.topic (slug’lar).
  • Akış:
    1. Tüm aktif kategoriler categories tablosundan çekilir.
    2. Ana kategori: slug === params.category && parentId === null ile bulunur. Bulunamazsa categoryIds = [], sayfa boş/404 davranışı.
    3. Alt kategori (varsa): slug === params.subcategory && parentId === mainCategory.id.
    4. Konu (varsa): slug === params.topic && parentId === subCategory.idcategoryIds = [topic.id].
    5. Sadece alt kategori: Tüm konular (parentId === subCategory.id) toplanır → categoryIds = topicIds (veya boşsa [subCategory.id]).
    6. Sadece ana kategori: Tüm alt kategorilerin konu id’leri toplanır → categoryIds = topicIds (veya alt kategori id’leri).
  • Kurs sorgusu: inArray ile categoryId bu liste içinde olan kurslar; sadece bu id’lere ait kurslar listelenir.
  • Breadcrumb: Her adımda name (çeviri: translations[lang]), slug, path eklenir; başlık ve gezinme buna göre oluşturulur.

/:lang/courses sayfasında “Kategorilere Göz At” grid’i categoryTree ile doldurulur.

  • Kaynak: Aynı categories tablosu; orderIndex ile sıralı.
  • Yapı: Ana kategoriler (parentId === null) alınır; her biri için alt kategoriler (parentId === main.id) children olarak eklenir. Çeviriler translations[lang] ile uygulanır.
  • Performans: categoryTree GraphQL resolver’ı:
    • Tüm aktif kategorileri tek sorguda çeker.
    • Tüm topic (3. seviye) kategorileri için kurs sayılarını tek grouped SQL ile hesaplar (group by courses.categoryId).
    • Sonucu Cloudflare Workers global scope’unda in‑memory cache’e (Map) 1 saatlik TTL ile yazar; aynı dil için categoryTree çağrıları DB’ye gitmeden RAM’den döner.
  • Kullanım: Kullanıcı bir ana kategoriye tıklayınca /:lang/courses/:category (courses.all) sayfasına gider; orada breadcrumb ve liste o kategori bağlamında gelir.
  • Kategoriler categories tablosunda, parentId ile ağaç oluşturur; kurslar categoryId ile (çoğunlukla konu seviyesine) bağlıdır.
  • courses.all URL’deki category / subcategory / topic slug’larından categoryIds üretir; liste ve keşfet verileri bu id’lere göre filtrelenir.
  • courses.index tüm kursları listeler ve categoryTree ile kategori grid’ini sunar; kategori seçimi courses.all’a yönlendirir.

Arama altyapısı iki katmanlı bir mimari kullanır:

KatmanKullanım YeriTeknolojiTetiklenme
Anlık arama (instant search)Navbar arama kutusuAlgoliaİstemci tarafı, 150 ms debounce
Listeleme aramasıcourses.index, courses.allPostgreSQL FTSSunucu tarafı, ?search= query parametresi

Navbar’daki anlık arama kullanıcıya hızlı öneri sunar; kullanıcı Enter’a basarak veya “Tümünü Gör” bağlantısını tıklayarak listeleme sayfasına (?search=...) yönlendirilir ve burada PostgreSQL FTS devreye girer.

Dosya: app/components/Navbar.tsx

Navbar bileşeni Algolia’nın hafif istemci SDK’sını kullanır:

import { liteClient as algoliasearch } from "algoliasearch/lite";
const searchClient = useMemo(
() => algoliasearch(
import.meta.env.VITE_ALGOLIA_APP_ID || "",
import.meta.env.VITE_ALGOLIA_SEARCH_KEY || ""
),
[]
);
  • Debounce: Kullanıcı yazmayı bıraktıktan 150 ms sonra istek gönderilir.
  • İstemci önbelleği: searchCache ref’i ile aynı terim tekrar arandığında ağ isteği yapılmaz.
  • Zero‑Result State: Algolia hits boş dönerse Navbar, arka planda query: "" ile yeniden arama yapıp popüler kursları getirir ve dropdown’da “bulunamadı” state’i altında gösterir.
  • Typo toleransı banner’ı: Algolia typo tolerance ile farklı bir terime yakın eşleşme bulduğunda, ilk sonucun başlığı aranan terimi içermiyorsa dropdown’da “Bununla ilgili sonuçlar gösteriliyor” bilgilendirmesi gösterilir.
  • İstek yapısı:
searchClient.search({
requests: [{
indexName: "courses",
query: searchTerm,
hitsPerPage: 6,
attributesToRetrieve: [
"objectID", "title", "slug", "subtitle",
"thumbnailUrl", "instructorName", "price",
"currency", "rating", "reviewsCount"
],
}],
});
  • Ortam değişkenleri (istemci):
DeğişkenAçıklama
VITE_ALGOLIA_APP_IDAlgolia uygulama kimliği
VITE_ALGOLIA_SEARCH_KEYSalt-okunur arama anahtarı (public)

Dosya: app/lib/algolia.ts

Kurslar yayınlandığında veya güncellendiğinde Algolia indeksi otomatik olarak güncellenir.

FonksiyonAçıklama
syncCourseToAlgolia(course, env)Tek bir kursu senkronize eder; kurs yayında değilse indeksten siler
bulkSyncCoursesToAlgolia(courses, env)Tüm yayınlanmış kursları toplu senkronize eder (1000’lik batch’ler halinde)
configureAlgoliaIndex(env)İndeks ayarlarını yapılandırır (aranabilir alanlar, typo toleransı, sıralama)
  • Tetiklenme: syncCourseToAlgolia, GraphQL şemasındaki updateCourse mutation’ından çağrılır (app/graphql/schema.ts).
  • İndeks adı: courses
  • Ortam değişkenleri (backend):
DeğişkenAçıklama
VITE_ALGOLIA_APP_IDAlgolia uygulama kimliği
ALGOLIA_ADMIN_KEYYazma yetkili admin anahtarı (gizli)

Her kurs aşağıdaki alanlarla Algolia’ya gönderilir:

AlanKaynak
objectIDcourse.id
titleKurs başlığı
slugURL slug’ı
subtitleAlt başlık
thumbnailUrlKapak görseli URL’i
priceFiyat
currencyPara birimi (sabit "usd")
instructorNameEğitmen adı
categoryKategori kimliği
languageKurs dili
ratingOrtalama puan
reviewsCountDeğerlendirme sayısı

configureAlgoliaIndex fonksiyonu ile yapılandırılır:

AyarDeğer
searchableAttributestitle, subtitle, instructorName
typoTolerancetrue — 3 harften sonra 1 typo, 6 harften sonra 2 typo toleransı
customRankingdesc(rating), desc(reviewsCount)
attributesForFacetingcategory, language, filterOnly(price)

PostgreSQL Full-Text Arama (Listeleme Sayfaları)

Section titled “PostgreSQL Full-Text Arama (Listeleme Sayfaları)”

Listeleme sayfalarında (courses.index, courses.all) arama URL query parametresi ile tetiklenir ve loader içinde sunucu tarafında uygulanır.

  • Parametre: search (örn. ?search=react+hooks).
  • Okuma: Loader’da url.searchParams.get("search"); boş veya sadece boşluk ise arama yapılmaz (hasSearchQuery = false).

Arama, PostgreSQL to_tsvector + plainto_tsquery ile simple config (dil bağımsız, stemming yok) kullanır. Özel karakterler (/, &, +, -) sorgu ve vektörde boşluğa dönüştürülerek normalize edilir:

to_tsvector('simple',
regexp_replace(
coalesce(title, '') || ' ' || coalesce(description, ''),
'[/&+\-]', ' ', 'g'
)
) @@ plainto_tsquery('simple',
regexp_replace(search_term, '[/&+\-]', ' ', 'g')
)
OR coalesce(title, '') ILIKE '%search_term%'
  • regexp_replace(..., '[/&+\-]', ' ', 'g'): UI/UX Design gibi başlıkları UI UX Design olarak normalize eder; böylece "ui ux design" araması eşleşir.
  • ILIKE fallback: FTS eşleşmezse başlık üzerinde büyük/küçük harf duyarsız alt-dize araması yapılır.
  • Arama varken sıralama relevance (alaka) olur: ts_rank_cd ile başlık ve açıklamaya göre puanlama.
  • Ağırlık: Başlık A, açıklama B (setweight ile) — başlık eşleşmeleri daha yüksek puan alır.
  • Sıralama ifadesinde de aynı regexp_replace normalizasyonu uygulanır.
  • Arama yokken sıralama kullanıcı seçimine göredir (NEWEST, HIGHEST_RATED, BEST_SELLING, fiyat vb.).
  • Ana kurs listesi (courseList).
  • Başlangıç için önerilen kurslar (beginnerRecommendations) — aynı FTS koşulu beginnerBaseFilters içinde kullanılır.
  • Öne çıkan kurslar (featuredCourses) — courses.all’da kategori + arama; courses.index’te sadece arama (varsa).

/:lang/courses?search=... araması sonuç döndürmezse sayfa, kullanıcıyı boş ekranda bırakmak yerine:

  • Popüler kurslar fallback’ini gösterir.
  • Mümkünse “Şunu mu demek istediniz?” benzeri bir öneri banner’ı ile kullanıcıyı daha doğru bir aramaya yönlendirir.

Dosya: app/routes/api.algolia-sync.ts Yol: GET /api/algolia-sync

İlk veri göçü veya indeks yeniden oluşturma için tek seferlik kullanılan endpoint:

  1. Tüm yayınlanmış kursları veritabanından çeker.
  2. Eğitmen isimlerini users tablosundan eşleştirir.
  3. configureAlgoliaIndex(env) ile indeks ayarlarını uygular.
  4. bulkSyncCoursesToAlgolia(courses, env) ile tüm kursları Algolia’ya gönderir.

Yanıt: { settings, sync, totalPublished } — senkronizasyon durumu ve toplam kurs sayısı.

┌─────────────────────────────────────────────────────────────────┐
│ Kullanıcı Arayüzü │
├──────────────────────────┬──────────────────────────────────────┤
│ Navbar Arama Kutusu │ Listeleme Sayfası (?search=) │
│ (anlık öneri) │ (tam sonuç listesi) │
├──────────────────────────┼──────────────────────────────────────┤
│ ▼ │ ▼ │
│ Algolia Lite Client │ PostgreSQL FTS (loader) │
│ ┌────────────────┐ │ ┌──────────────────────┐ │
│ │ 150ms debounce │ │ │ to_tsvector + │ │
│ │ istemci cache │ │ │ regexp_replace │ │
│ │ hitsPerPage: 6 │ │ │ ILIKE fallback │ │
│ └───────┬────────┘ │ │ ts_rank_cd sıralama │ │
│ ▼ │ └──────────────────────┘ │
│ Algolia API │ │
│ (courses indeksi) │ │
└──────────────────────────┴──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Algolia Senkronizasyon │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Kurs Yayınlama/Güncelleme │
│ (GraphQL updateCourse mutation) │
│ │ │
│ ▼ │
│ syncCourseToAlgolia() ──────► Algolia “courses” indeksi │
│ │
│ Toplu Göç (tek seferlik) │
│ GET /api/algolia-sync │
│ │ │
│ ├─► configureAlgoliaIndex() │
│ └─► bulkSyncCoursesToAlgolia() ──► Algolia (1000’lik │
│ batch’ler) │
└─────────────────────────────────────────────────────────────────┘

Filtreleme FilterSidebar bileşeni ve URL query parametreleri ile yapılır. Tüm filtreler loader’da okunur ve SQL koşullarına dönüştürülür; yani sunucu taraflı filtrelemedir.

Dosya: app/components/courses/FilterSidebar.tsx

Sidebar, useSearchParams() ile mevcut query string’i okur ve her filtre değişiminde setSearchParams ile URL’i günceller. Filtre seçildiğinde page parametresi silinir (1. sayfaya dönülür).

Query parametresiAçıklamaDeğerler / Kaynak
minRatingMinimum kurs puanı4.5, 4.0, 3.5, 3.0 (RATING_OPTIONS)
levelKurs seviyesicourseLevels tablosundan (beginner, intermediate, expert vb.); loader levels ile sidebar’a verir
priceÜcretli / ücretsizfree, paid
durationToplam süre aralığı (saniye)0-7200 (0–2 saat), 10800-21600 (3–6 saat), 25200-999999 (7+ saat)
languageKurs dilitr, en, es, de, fr, ja (LANGUAGE_OPTIONS)

courses.index ve courses.all loader’larında:

  • level: Varsa conditions dizisine kurs seviyesi eşitliği (eq) eklenir.
  • language: Varsa conditions dizisine dil eşitliği eklenir.
  • minRating: Varsa conditions dizisine rating büyük-eşit koşulu SQL ile eklenir; parametre parseFloat ile sayıya çevrilir.
  • duration: Ana sorguda uygulanmaz; kurs listesi çekildikten sonra coursesWithStats üzerinde totalDurationSeconds hesaplanır, sonra client-side benzeri bir filtre ile filteredCourses elde edilir:
    • Değer min-max formatında (saniye) ise totalDurationSeconds bu aralıkta mı diye bakılır.
    • Veya sabit aralıklar (0-1, 1-3, 3-6, 6-17, 17+ saat) switch ile kontrol edilir.
  • price: Yine liste sonrası filtre: price “free” ise fiyat 0, “paid” ise fiyat büyük 0.

Sayfalama (getPaginationMeta, sliceForPage) filteredCourses üzerinden yapılır; dolayısıyla filtreler sayfa başına sonuç sayısını da etkiler.

  • Tek seçim: Çoğu filtre tek değer (toggle): aynı parametre tekrar tıklanırsa parametre silinir.
  • Filtreleri temizle: “Filtreleri temizle” tıklanınca tüm filtre parametreleri silinir; istenirse sadece sortBy korunur.
  • Aktif filtre sayısı: minRating, level, price, duration, language dolu olanlar sayılır; badge ile gösterilir.
  • levels: Veritabanından gelen courseLevels listesi; levels loader’dan sidebar’a prop olarak iletilir.
  • sortBy: NEWEST, HIGHEST_RATED, BEST_SELLING, PRICE_LOW_TO_HIGH, PRICE_HIGH_TO_LOW; dropdown ile seçilir, URL’de ?sortBy=....
  • Sayfalama: page (varsayılan 1), COURSES_PAGE_SIZE = 12; getPaginationMeta ve sliceForPage ile sayfa dilimi alınır.

URL: /:lang/courses/search?q=... (veya ilgili query parametresi).

Dosya: app/routes/courses.search.tsx

Arama odaklı sayfa; kullanıcı “courses” bağlamında arama yaptığında bu route kullanılabilir. Detaylar (loader, UI) route dosyası ve tasarıma göre genişletilir.


  • Neon/Postgres uyumu: Kategori ve eğitmen istatistik sorgularında ANY ile dizi parametresi yerine Drizzle inArray(column, array) kullanılır; aksi halde “op ANY/ALL (array) requires array on right side” hatası oluşabilir.
  • Dil: Tüm metinler useTranslation() ve app/locales/*.json içindeki courses.featuredCourses, courses.popularTopics, courses.popularInstructors, courses.beginnerRecommendations anahtarlarından gelir.
  • Erişilebilirlik: Carousel’da klavye (Sol/Sağ ok), kart/link’lerde aria-label, tabIndex, role kullanımı önerilir.

  • courses.index/:lang/courses (tüm kurslar, kategori ağacı, platform geneli keşfet verileri).
  • courses.all/:lang/courses/:category, /:category/:subcategory, /:category/:subcategory/:topic (kategoriye göre filtrelenmiş kurslar ve keşfet verileri).
  • Kurs etiket sistemi: Tek merkezi mantık (app/lib/course-badges.ts, getCourseBadges). Etiketler: En Çok Satan (konu bazında top 5 satış), Yeni (son 14 gün), Popüler (50+ inceleme veya 4.5+ puan ve 10+ inceleme). Liste sayfaları ve kurs detay sayfası aynı badges dizisini kullanır.
  • Kategorizasyon: 3 seviyeli ağaç (categories tablosu, parentId); URL slug’ları loader’da categoryIds’e çözülür; kurs listesi ve keşfet verileri bu id’lere göre filtrelenir.
  • Arama: ?search=... ile başlık + açıklama üzerinde full-text (to_tsvector/plainto_tsquery); arama varken sıralama ts_rank_cd (relevance) ile yapılır.
  • Filtreleme: FilterSidebar ile minRating, level, price, duration, language URL’e yazılır; loader’da koşullara dönüştürülür; duration/price liste sonrası filtre ile uygulanır.
  • Keşfet bölümleri: BeginnerRecommendations → FeaturedCourses → PopularTopics → PopularInstructors (sıra sabit).
  • Sıralama ve sayfalama query parametreleri ile her iki route’ta da geçerli; sayfa başına 12 kurs.