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ı.
Anasayfa Kurs Seçimi (CourseSelection)
Section titled “Anasayfa Kurs Seçimi (CourseSelection)”Dosya: app/components/CourseSelection.tsx
Kullanım: Anasayfa (_index.tsx vb.) üzerinde “Kurslar” / kategorilere göre öne çıkan kurslar bölümü.
Mantık
Section titled “Mantık”- 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.
Veri Kaynağı
Section titled “Veri Kaynağı”- 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,isBestSellerkullanı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.
| Özellik | courses.index.tsx | courses.all.tsx |
|---|---|---|
| URL | /:lang/courses | /:lang/courses/:category, /:category/:subcategory, /:category/:subcategory/:topic |
| Route dosyası | app/routes/courses.index.tsx | app/routes/courses.all.tsx |
| params | Yok | category, isteğe bağlı subcategory, topic |
| Kurs listesi | Tüm yayındaki kurslar (+ arama/filtre) | Seçilen kategori/alt kategori/konuya ait kurslar (+ arama/filtre) |
| Breadcrumb | Ana Sayfa → Kurslar | Ana Sayfa → Kurslar → [Ana Kategori] → [Alt Kategori] → [Konu] |
| Kategori grid | Var (categoryTree — “Kategorilere Göz At”) | Yok (zaten bir dalın içindesin) |
| Öne çıkan / popüler veriler | Platform geneli | Seçili kategori bağlamında |
Örnek URL’ler:
/tr/courses→ courses.index (tüm kurslar, kategori ağacı gösterilir)./tr/courses/development→ courses.all (sadece Development ana kategorisi)./tr/courses/development/data-science→ courses.all (Data Science alt kategorisi)./tr/courses/office-productivity/microsoft-office/excel→ courses.all (Excel konusu).
Keşfet Bölümleri (Discovery Sections)
Section titled “Keşfet Bölümleri (Discovery Sections)”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:
- BeginnerRecommendationsSection — Başlangıç için önerilen kurslar (Popüler / Yeni sekmeleri).
- FeaturedCoursesSection — Öne çıkan kurslar (carousel, 1 kart/slide, yatay kart).
- PopularTopicsSection — Popüler konular (5x2 grid, konular kurs sayısına göre sıralı).
- PopularInstructorsSection — Popüler eğitmenler (kartlar, public profile linki, öğrenci sayısı, eğitmen puanı).
Bileşen Dosyaları
Section titled “Bileşen Dosyaları”| Bileşen | Dosya | Açıklama |
|---|---|---|
| BeginnerRecommendationsSection | app/components/courses/BeginnerRecommendationsSection.tsx | Başlangıç seviye kurslar; “Popüler” ve “Yeni” sekmeleri, grid’de kurs kartları. |
| FeaturedCoursesSection | app/components/courses/FeaturedCoursesSection.tsx | Yatay kurs kartı carousel; 1 kart/slide, önceki/sonraki ok, klavye (Sol/Sağ ok) desteği. |
| PopularTopicsSection | app/components/courses/PopularTopicsSection.tsx | Konu etiketleri; 5 sütun x 2 satır grid (10 konu), kurs sayısına göre sıralı, link konu sayfasına. |
| PopularInstructorsSection | app/components/courses/PopularInstructorsSection.tsx | Eğitmen kartları; profil resmi, isim, eğitmen puanı, öğrenci sayısı, kurs sayısı; username varsa /:lang/user/:username public profile linki. |
Loader Verileri
Section titled “Loader Verileri”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;pathdil ö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.
Kurs Etiket Sistemi (Badge Sistemi)
Section titled “Kurs Etiket Sistemi (Badge Sistemi)”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.
Genel Mantık
Section titled “Genel Mantık”- Tek kaynak: Tüm etiket kararları
app/lib/course-badges.tsiçindekigetCourseBadges()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.popularanahtarlarından gelir.
Etiket Türleri ve Koşulları
Section titled “Etiket Türleri ve Koşulları”| Etiket | Koşul | Açı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. |
Yapılandırma Sabitleri (BADGE_CONFIG)
Section titled “Yapılandırma Sabitleri (BADGE_CONFIG)”Dosya: app/lib/course-badges.ts
| Sabit | Varsayılan | Açıklama |
|---|---|---|
| NEW_DAYS | 14 | “Yeni” etiketinin gösterileceği gün sayısı (yayın tarihinden itibaren). |
| BEST_SELLER_TOP_N | 5 | Konu (category_id) başına “En Çok Satan” verilecek kurs sayısı (top 5). |
| POPULAR_MIN_REVIEWS | 50 | Sadece inceleme sayısı ile popüler sayılmak için minimum inceleme. |
| POPULAR_MIN_RATING | 4.5 | Puan kriterinde minimum ortalama puan. |
| POPULAR_MIN_REVIEWS_FOR_RATING | 10 | Puan 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.
Teknik Akış
Section titled “Teknik Akış”-
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. -
Badge hesaplama:
Her kurs içingetCourseBadges(course, { bestSellerIds })çağrılır. Fonksiyon kursunid,categoryId,rating,reviewsCount,createdAtalanlarını kullanır; yukarıdaki koşullara göre["bestSeller", "new", "popular"]içinden uygun olanları sırayla döndürür. -
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. -
Bileşenler:
CourseCard ve CourseHorizontalCardcourse.badgesvarsa 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.
Kategorizasyon Sistemi
Section titled “Kategorizasyon Sistemi”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.
Seviye Yapısı
Section titled “Seviye Yapısı”| Seviye | Açıklama | parentId | Örnek slug |
|---|---|---|---|
| 1 – Ana kategori | En üst kapsayıcı (örn. Geliştirme, Ofis) | null | development, office-productivity |
| 2 – Alt kategori | Ana kategorinin alt dalı | Ana kategorinin id’si | data-science, microsoft-office |
| 3 – Konu | Kursun doğrudan atandığı yaprak | Alt kategorinin id’si | python, 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ış:
- Tüm aktif kategoriler
categoriestablosundan çekilir. - Ana kategori:
slug === params.category && parentId === nullile bulunur. BulunamazsacategoryIds = [], sayfa boş/404 davranışı. - Alt kategori (varsa):
slug === params.subcategory && parentId === mainCategory.id. - Konu (varsa):
slug === params.topic && parentId === subCategory.id→categoryIds = [topic.id]. - Sadece alt kategori: Tüm konular (
parentId === subCategory.id) toplanır →categoryIds = topicIds(veya boşsa[subCategory.id]). - Sadece ana kategori: Tüm alt kategorilerin konu id’leri toplanır →
categoryIds = topicIds(veya alt kategori id’leri).
- Tüm aktif kategoriler
- 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,patheklenir; başlık ve gezinme buna göre oluşturulur.
Kategori Ağacı (courses.index)
Section titled “Kategori Ağacı (courses.index)”/:lang/courses sayfasında “Kategorilere Göz At” grid’i categoryTree ile doldurulur.
- Kaynak: Aynı
categoriestablosu;orderIndexile sıralı. - Yapı: Ana kategoriler (
parentId === null) alınır; her biri için alt kategoriler (parentId === main.id) children olarak eklenir. Çevirilertranslations[lang]ile uygulanır. - Performans:
categoryTreeGraphQL 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çincategoryTreeç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/topicslug’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 Sistemi
Section titled “Arama Sistemi”Arama altyapısı iki katmanlı bir mimari kullanır:
| Katman | Kullanım Yeri | Teknoloji | Tetiklenme |
|---|---|---|---|
| Anlık arama (instant search) | Navbar arama kutusu | Algolia | İstemci tarafı, 150 ms debounce |
| Listeleme araması | courses.index, courses.all | PostgreSQL FTS | Sunucu 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.
Algolia Entegrasyonu (Anlık Arama)
Section titled “Algolia Entegrasyonu (Anlık Arama)”İstemci Tarafı — Navbar
Section titled “İstemci Tarafı — Navbar”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:
searchCacheref’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şken | Açıklama |
|---|---|
VITE_ALGOLIA_APP_ID | Algolia uygulama kimliği |
VITE_ALGOLIA_SEARCH_KEY | Salt-okunur arama anahtarı (public) |
Backend Senkronizasyon Servisi
Section titled “Backend Senkronizasyon Servisi”Dosya: app/lib/algolia.ts
Kurslar yayınlandığında veya güncellendiğinde Algolia indeksi otomatik olarak güncellenir.
| Fonksiyon | Açı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ındakiupdateCoursemutation’ından çağrılır (app/graphql/schema.ts). - İndeks adı:
courses - Ortam değişkenleri (backend):
| Değişken | Açıklama |
|---|---|
VITE_ALGOLIA_APP_ID | Algolia uygulama kimliği |
ALGOLIA_ADMIN_KEY | Yazma yetkili admin anahtarı (gizli) |
Algolia İndeks Kaydı (Record)
Section titled “Algolia İndeks Kaydı (Record)”Her kurs aşağıdaki alanlarla Algolia’ya gönderilir:
| Alan | Kaynak |
|---|---|
objectID | course.id |
title | Kurs başlığı |
slug | URL slug’ı |
subtitle | Alt başlık |
thumbnailUrl | Kapak görseli URL’i |
price | Fiyat |
currency | Para birimi (sabit "usd") |
instructorName | Eğitmen adı |
category | Kategori kimliği |
language | Kurs dili |
rating | Ortalama puan |
reviewsCount | Değerlendirme sayısı |
Algolia İndeks Ayarları
Section titled “Algolia İndeks Ayarları”configureAlgoliaIndex fonksiyonu ile yapılandırılır:
| Ayar | Değer |
|---|---|
| searchableAttributes | title, subtitle, instructorName |
| typoTolerance | true — 3 harften sonra 1 typo, 6 harften sonra 2 typo toleransı |
| customRanking | desc(rating), desc(reviewsCount) |
| attributesForFaceting | category, 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.
Query Parametresi
Section titled “Query Parametresi”- Parametre:
search(örn.?search=react+hooks). - Okuma: Loader’da
url.searchParams.get("search"); boş veya sadece boşluk ise arama yapılmaz (hasSearchQuery = false).
FTS Koşulu ve Separator Normalizasyonu
Section titled “FTS Koşulu ve Separator Normalizasyonu”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 Designgibi başlıklarıUI UX Designolarak normalize eder; böylece"ui ux design"araması eşleşir.ILIKEfallback: 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)
Section titled “Arama Varken Sıralama (Relevance)”- Arama varken sıralama relevance (alaka) olur:
ts_rank_cdile başlık ve açıklamaya göre puanlama. - Ağırlık: Başlık A, açıklama B (
setweightile) — başlık eşleşmeleri daha yüksek puan alır. - Sıralama ifadesinde de aynı
regexp_replacenormalizasyonu uygulanır. - Arama yokken sıralama kullanıcı seçimine göredir (NEWEST, HIGHEST_RATED, BEST_SELLING, fiyat vb.).
Aramanın Uygulandığı Yerler
Section titled “Aramanın Uygulandığı Yerler”- Ana kurs listesi (courseList).
- Başlangıç için önerilen kurslar (beginnerRecommendations) — aynı FTS koşulu
beginnerBaseFiltersiçinde kullanılır. - Öne çıkan kurslar (featuredCourses) —
courses.all’da kategori + arama;courses.index’te sadece arama (varsa).
Zero‑Result (Listeleme Sayfası)
Section titled “Zero‑Result (Listeleme Sayfası)”/: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.
Toplu Senkronizasyon Endpoint’i
Section titled “Toplu Senkronizasyon Endpoint’i”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:
- Tüm yayınlanmış kursları veritabanından çeker.
- Eğitmen isimlerini
userstablosundan eşleştirir. configureAlgoliaIndex(env)ile indeks ayarlarını uygular.bulkSyncCoursesToAlgolia(courses, env)ile tüm kursları Algolia’ya gönderir.
Yanıt: { settings, sync, totalPublished } — senkronizasyon durumu ve toplam kurs sayısı.
Teknik Akış Diyagramı
Section titled “Teknik Akış Diyagramı”┌─────────────────────────────────────────────────────────────────┐│ 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 Sistemi
Section titled “Filtreleme Sistemi”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.
Bileşen ve Parametreler
Section titled “Bileşen ve Parametreler”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 parametresi | Açıklama | Değerler / Kaynak |
|---|---|---|
| minRating | Minimum kurs puanı | 4.5, 4.0, 3.5, 3.0 (RATING_OPTIONS) |
| level | Kurs seviyesi | courseLevels tablosundan (beginner, intermediate, expert vb.); loader levels ile sidebar’a verir |
| price | Ücretli / ücretsiz | free, paid |
| duration | Toplam süre aralığı (saniye) | 0-7200 (0–2 saat), 10800-21600 (3–6 saat), 25200-999999 (7+ saat) |
| language | Kurs dili | tr, en, es, de, fr, ja (LANGUAGE_OPTIONS) |
Loader’da Filtre Uygulaması
Section titled “Loader’da Filtre Uygulaması”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.
FilterSidebar Davranışı
Section titled “FilterSidebar Davranışı”- 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
sortBykorunur. - Aktif filtre sayısı:
minRating,level,price,duration,languagedolu olanlar sayılır; badge ile gösterilir. - levels: Veritabanından gelen
courseLevelslistesi;levelsloader’dan sidebar’a prop olarak iletilir.
Sıralama ve Sayfalama
Section titled “Sıralama ve Sayfalama”- 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;getPaginationMetavesliceForPageile sayfa dilimi alınır.
courses/search Sayfası
Section titled “courses/search Sayfası”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.
Teknik Notlar
Section titled “Teknik Notlar”- 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,rolekullanı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.