25 Nisan 2020 Cumartesi

ORTA SEVİYE BİR RAİLS TUTORIAL - 5

Önceki bölümü okumadan buraya geldiyseniz hiç kasmayın geri dönün





Tasarım Değişikliği


Bu yazıda kolleksiyon sayfalarında gönderiler için başka bir stil deneyeceğiz. Ana sayfada kart tasarımını kullanmıştık. Kolleksiyonlarda liste tasarımı kullanalım. Böylece kullanıcı çok daha fazla gönderiyi görebilir ve içlerinde daha etkili gezinebilir.

Görsellerin olduğu posts klasöründe post adında bir alt klasör tanımlayalım ve içne _home_page.html.erb adında bir parça görsel dosyası tanımlayalım.

Daha önce yazdığımız _post.html.erb parça görselindeki kodu kesip buraya yapıştıralım.

app/views/posts/post/_home_page.html.erb
<div class="col-sm-3 single-post-card" id=<%= post_path(post.id) %>>
    <div class="card">
        <div class="card-block">
            <h4 class="post-text">
                <%= truncate( post.title, :length => 60 ) %>
            </h4>
            <div class="post-content">
                <div class="posted-by">Gönderen <%= post.user.name %></div>
                <h3><%= post.title %></h3>
                <p><%= post.content %></p>
                <%= link_to "İlgileniyorum", post_path( post.id ),
                    class: "interested" %>
            </div>
        </div>
    </div><!-- card -->
</div><!-- col-sm-3 -->




Şimdi içini boşalttığımız _post.html.erb dosyası içine şu satırı yazalım:

app/views/posts/_post.html.erb
<%= render post_format_partial_path, post: post %>


Burada post_format_partisl_path yardımcı metodunu çağırıyoruz. Bu metod bulunulan sayfaya göre hangi parça görsel ile gönderilerin yayınlanacağını belirleyecek. Eğer ana sayfa gösteriliyorsa az evvel _home_page.html.erb içine taşıdığımız görseli gösterecek. Şimdi neden görseli taşıdığımız anlaşıldı.

Kolleksiyon sayfasındaki görsel için view/posts/post/ klasörü içinde _branch_page.html.erb dosyasını tanımlayalım ve içine şu kodu yazalım:

app/views/posts/post/_branch_page.html.erb
<div class="single-post-list" id=<%= post_path(post.id) %>>
    <%= truncate(post.title, :length => 60) %>
    <div class="post-content">
        <div class="posted-by">Gönderen <%= post.user.name %></div>
        <h3><%= post.title %></h3>
        <p><%= post.content %></p>
        <%= link_to "İlgimi çekti", post_path(post.id), class: "interested" %>
    </div>
</div>

Şimdi posts_helper.rb içinde post_format_partial_path metodunu tanımlayalım:

app/helpers/posts_helper.rb
    ...

    def post_format_partial_path
        current_page?(root_path) ? "posts/post/home_page" : "posts/post/branch_page"
    end

Burada bir eksik var, ana sayfaya gidersek bize post_format_partial_path metodunu bulamadığını söyleyecek çünkü ana sayfa PagesController içindeyken bu metodu başka kontrolör içinde tanımlandığı için göremez. O zaman ana yardımcı sınıfı olan ApplicationHelper içinde PostsHelper sınıfını da dahil edelim.

app/helpers/application_helper.rb
module ApplicationHelper
    include NavigationHelper
    include PostsHelper
end



CSS


Kolleksiyon sayfalarındaki gönderilerin stili için bir CSS dosyası hazırlayalım. Stillere ait posts klasöründe branch_page.scss dosyasını tanımlayalım.

app/assets/stylesheets/partials/posts/branch_page.scss
.single-post-list {
    min-height: 45px;
    max-height: 45px;
    padding: 10px 20px 10px 0;
    margin: 0 10px;
    border-bottom: solid 3px rgba(0,0,0, 0.05);
    border-bottom-right-radius: 10%;
    transition: border-color 0.1s;
    overflow: hidden;
    &:hover {
        cursor: pointer;
    }
}

.page-title {
    margin: 30px 0;
    text-align: center;
    background-color: white !important;
    font-weight: bold;
    a {
        color: black;
    }
    a:hover {
        text-decoration: underline;
    }
}

.categories-list {
    margin: 10px 0;
    padding: 0;
}

.category-item {
    display: inline-block;
    margin: 15px 0;
    a {
        font-size: 16px;
        font-size: 1.6rem;
        color: rgba(0,0,0, 0.7);
        border: solid 2px rgba(0,0,0, 0.4);
        border-radius: 8%;
        padding: 10px;
    }
    a:hover, .selected-item {
        background-color: $navbarColor;
        color: white;
        border: solid 2px white;
        border-radius: 0;
    }
}

.new-post-button-parent {
    text-align: right;
    span {
        font-size: 12px;
        font-size: 1.2rem;
    }
}

.new-post-button {
    display: inline-block;
    background: $navbarColor;
    color: white;
    padding: 8px;
    border-radius: 10px;
    font-weight: bold;
    border: solid 2px $navbarColor;
    margin: 10px 0;
    &:hover, &:active {
        background: white;
        color: black;
    }
}

.login-branch {
    margin: 10px 0;
}

.login-button-branch {
    padding: 5px 10px;
    border-radius: 10px;
    &:hover, &:active, &:visited, &:link {
        color: white;
    }
}

.branch-main-content {
    background: white;
    height: calc(100vh - 50px);
}

#feed {
    background-color: white;
}

Size tavsiyem bu CSS satırlarının herbirinin ne işe yaradığını takip edin.

Bir de base/default.scss dosyasına şunları ekleyelim:

app/assets/stylesheets/base/default.scss
...

.login-button, .sign-up-button {
    background-color: $navbarColor;
    color: white !important;
}

Küçük ekranlarla ilgili olarak düzenleme için de mobile.scss dosyasına şunları ekleyelim.

app/assets/stylesheets/responsive/mobile.scss
...

@media screen and (max-width: 550px) {
    .page-title {
        font-size: 20px;
        font-size: 2rem;
    }
    .new-post-button-parent {
        text-align: center;
        span {
            display: none !important;
        }
    }
    .post-button {
        padding: 5px;
    }
    .category-item {
        a {
            padding: 5px;
        }
    }
}

@media screen and (max-width: 767px) {
    .single-post-list {
        min-height: 65px;
        max-height: 65px;
        padding: 10px 0;
    }
}

Artık kolleksiyon sayfaları böyle görünüyor:







Arama Kutusu


Kolleksiyon sayfalarında sadece tüm listeyi göstermek değil kullanıcıya gönderiler içinde arama yapma kabiliyeti de vermek istiyoruz. _branch.html.erb görsel kalıp dosyasında categories satırı üzerine şunları ekleyelim:

app/views/posts_branch.html.erb

...
    <div class="row">
        <%= render "posts/branch/search_form",
            branch: branch,
            search_placeholder: search_placeholder %>
    </div>
...

Koddanda anlaşıldığı üzere branch klasörü içinde _search_form.html.erb adında bir dosya tanımlayacağız.

app/views/posts/branch/_search_form.html.erb
<div class="col-sm-12">
    <%= form_tag(send("#{branch}_posts_path"),
        :method => "get",
        id: "search-form") do %>
        <i class="fa fa-search" aria-hidden="true"></i>
        <%= text_field_tag :search,
            params[:search],
            placeholder: search_placeholder,
            class: "form-control" %>
        <%= render category_field_partial_path %>
    <% end %>
</div>

Burada send metodu ile bulunulan kolleksiyona bağlı olan PostsController aksiyonuna yönlendiriyoruz. Ayrıca eğer bir kategori seçiliyse onu da extra parametre olarak göndereceğiz. Kategori seçiliyse sadece o kategoriden arama sonuçları gelmeli.

category_field_partial_path yardımcı metodunu posts_helper.rb içine ekleyelim:

app/helpers/posts_helper.rb
...
    def category_field_partial_path
        if params[:category].present?
            "posts/branch/search_form/category_field"
        else
            "shared/empty_partial"
        end
    end

Anlaşılacağı üzere posts/branch/search_form alt klasörünü tanımlayacağız ve içine _category_field.html.erb parça görsel dosyasını koyacağız.

app/views/posts/branch/search_form/_category_field.html.erb
<%= hidden_field_tag :category, params[:category] %>

Arama kutusuna biraz stil vermek için branch_page.scss içine biraz CSS ekleyelim:

app/assets/stylesheets/partials/posts/branch_page.scss
...


.fa-search {
    position: absolute;
    bottom: 14px;
    left: 10px;
    width: 20px;
    height: 10px;
}

#search-form {
    position: relative;
    input {
        border: solid 2px rgba(0,0,0, 0.2);
        border-radius: 10px;
        box-shadow: none;
        outline: 0;
    }
    input:focus {
        border: solid 2px rgba(0,0,0, 0.35);
    }
    input#search {
        padding: 15px;
        width: 100%;
        height: 20px;
        margin: 10px 0;
        padding-left: 30px;
    }
}

Kolleksiyon sayfalarında arama kutusu böyle görünecek:



Formumuzda görüntü var da icraat yok. Arama özelliği kazanmak için bazı gem’ler yükleyebiliriz ama bizim verilerimiz çok karmaşık değil. Bu yüzden Post modeli içinde scope kullanmak bize yeterli olacak.

Önce Post modeli içinde değişik kapsamlar tanımlayarak başlayalım. Isınmak için post.rb model dosyasında default_scope tanımlayalım. Bu kapsam tanımıyla gönderileri tarihlerine göre azalan sıralama yaparak en üste en yeni gönderinin gelmesini sağlayalım.

app/models/post.rb
class Post < ApplicationRecord
    belongs_to :user
    belongs_to :category
    default_scope -> { includes(:user).order(created_at: :desc) }
end



Bu kodun çalışmasından emin olmak için rails console kullanabiliriz. Konsolda

rails console
girdiğimizde karşımıza uygulamamıza özel bir irb uygulaması açılır. Orada

Post.limit(5)
girdiğimizde en üstteki 5 tane gönderi listelenecektir. Eğer default_scope değiştirmemiş olsak zaman olarak ilk girilen 5 gönderi gelir. Ama yukarıda bahsi geçen satırı modele ekleyerek default_scope değiştirdikten sonra en son girilen 5 gönderi gelecektir. Şu anda uygulamada görünen sahte gönderileri seed ile ürettiğimiz için zaman bilgisinin ancak saniye kısmında fark görebiliriz. Deneme yaparken iki durum arasında geçiş yapmadan önce Rails konsoldan exit komutu ile çıkıp tekrar girmeyi unutmayın.

Not : Windows’ta başıma geldi Linux’ta olur mu bilmem ama, eğer Rails konsol utf8 temelli bir hatadan dolayı çalışmamazlık ederse bilgisayarınızın c:\Users<user_name>.irb_history adında bir dosyası var onun içini silin. Bu dosya geçmişteki irb çalışmalarınızda girdiklerinizi kaydeder ve eğer Türkçe karakter girmişseniz ondan dolayı Rails konsol utf8 hatası veriyor.

Şimdi arama kutumuzu çalıştırmak için bir şeyler yapalım. Hatırlar mısınız posts_controller.rb dosyasında bir get_posts metodu tanımlamış ve içine en üstten 30 kaydı getiren tek satır bir kod yazmıştık. Ama daha sonra buraya döneceğimizi söylemiştik. Haydi dönelim:

app/controllers/posts_controller.rb
...
    def get_posts
        branch = params[:action]
        search = params[:search]
        category = params[:category]

        if category.blank? && search.blank?
            posts = Post.by_branch(branch).all
        elsif category.blank? && search.present?
            posts = Post.by_branch(branch).search(search)
        elsif category.present? && search.blank? 
            posts = Post.by_category(branch, category)
        elsif category.present? && search.present?
            posts = Post.by_category(branch, category).search(search)
        else
        end
    end
...

Daha önce lojiğin görseller içinde olmasının tercih edilmediğinden bahsetmiştik. Bu arama kodlarının da kontrolör içinde olması pek yakışık almadı. Önümüzdeki bölümlerde bu kodları düzenleyeceğiz.

Yukarıdaki kodda gördüğümüz gibi kullanıcının yaptığı olası girdilere göre değişik kapsamlar kullanılarak Post modeli sorgulanıyor. Şimdi bunları model koduna ekleyelim.

app/models/post.rb
class Post < ApplicationRecord
    belongs_to :user
    belongs_to :category
    default_scope -> { includes(:user).order(created_at: :desc) }

    scope :by_category, -> (branch, category_name) do
        joins(:category).where(categories: {name: category_name, branch: branch})
    end

    scope :by_branch, -> (branch) do
        joins(:category).where(categories: {branch: branch})
    end 

    scope :search, -> (search) do
        where("title LIKE lower(?) OR content LIKE lower(?)", 
            "%#{search}%", "%#{search}%")
    end
end

Not PostgreSQL kullanırken search sorgusunda ya LIKE yerine ILIKE kullanmak ya da lower(title) şeklinde aranan sütunu da küçük harfe çevirmek gerekebilir.

 Yukarıdaki kodda joins metodu bağlantılı tablolardan kayıtları toplamak için kullanılır. Ayrıca girilen stringe göre kayıtlarda basit bir arama SQL satırı da var.

 Eğer şimdi server’ı yeniden başlatırsak arama kutusunun çalıştığını görürüz. Ayrıca kolleksiyonların kategorilerine ait seçme butonları da çalışmaya başladı.



Sonsuz kaydırma


Kolleksiyon sayfalarında en alta indiğimizde kayıtların pagination ile sayfalara bölünmüş olduğunu görüyoruz. Sonraki linke tıkladığımızda daha eski kayıtların bulunduğu sayfalara doğru gidiyoruz. Burada Tweeter veya Facebook’taki gibi aşağıya gidildikçe eski gönderilerin sayfa yenilemeden aşağıda devam etmesini sağlayabiliriz. Güzel tarafı bunu yapmak çok kolay. Tüm yapmamız gereken biraz JavaScrşpt yazmak. Kullanıcı sayfanın en altına indiğinde bir AJAX çağrısı ile bir sonraki sayfadaki kayıtları alarak bulunulan sayfanın en altına eklenmesini sağlayabiliriz.

AJAX çağrısı ve koşullarını oluşturarak başlayalım. Kullanıcı aşağı scroll ederek belli bir noktayı geçtiğinde AJAX çağrısı ateşlenir. javascripts/posts klasöründe infinite_scroll.js dosyasını ekleyelim ve şu kodu içine yazalım:

app/assets/javascripts/posts/infinite_scroll.js
$(document).on("turbolinks:load", function() {
    var isLoading = false;
    if ($(".infinite-scroll", this).size()) > 0 {
        $(window).on("scroll", function() {
            var more_posts_url = $(".pagination a.next_page").attr("href");
            var threshold_passed = $(window).scrollTop() > $(document).height() - 
                $(window).height() - 60;
            if (!isLoading && more_posts_url && threshold_passed) {
                isLoading = true;
                $.getScript(more_posts_url).done(function(data, textStatus, jqxhr) {
                    isLoading = false;
                }).fail(function() {
                    isLoading = false;
                });
            }
        });
    }
});

isLoading değişkeni aynı anda sadece bir yüklemenin olabilmesini garanti altına alır. Önce pagination var mı diye bakılıyor ve arkada başka sayfa var mı diye. Sonra bir sonraki sayfanın adresi alınıyor. Arkasından sayfa altına 60 piksel kalana kadar kaydırma yapıldıysa AJAX tetikleniyor. Son olarak getScript() fonksiyonu ile tüm koşullar gerçekleşirse sonraki sayfanın kayıtları alınıyor.

getScript() fonksiyonu da aynı JavaScript dosyasını tekrar yükleyeceği için PostsController içinde hangi dosyanın yayınlanacağını seçmemiz gerekiyor. posts_for_branch içinde hangi dosyanın yayınlanacağına karar verelim.

app/controllers/posts_controller.rb
...
    def posts_for_branch(branch)
        @categories = Category.where(branch: branch)
        @posts = get_posts.paginate(page: params[:page])

        respond_to do |format|
            format.html
            format.js { render partial: "posts/posts_pagination_page" }
        end
    end
...

Bu koda göre bir .js dosya aksiyona ulaşırsa posts_pagination_page kalıp dosyası yayınlanacak .html erişim olursa değişen bir şey yok. Bu parça dosya yeni elemanları mevcut listeye ekleyecek. Bu yüzden o da bir JavaScript. Ama görsel kalıpların yanında bulunacak.

app/views/posts/_posts_pagination_page.js.erb
$("#feed").append("<%= j render @posts %>");
<%= render update_pagination_partial_path %>

Yeni yardımcı metodumuz update_pagination_partial_path metodunu da posts_helper.rb içinde tanımlayalım.

app/helpers/posts_helper.rb
...
    def update_pagination_partial_path
        if @posts.next_page
            "posts/posts_pagination_page/update_pagination"
        else
            "posts/posts_pagination_page/remove_pagination"
        end
    end

Burada will_paginate gem’in next_page metodu başka sayfa olup olmadığını algılamak için kullanıldı.

Karşı gelen parça dosyaları tanımlayalım:

app/views/posts/posts_pagination_page/_update_pagination.js.erb
$(".pagination").replaceWith("<%= j will_paginate @posts %>");

app/views/posts/posts_pagination_page/_remove_pagination.js.erb
$(window).off("scroll");
$(".pagination").remove();

Artık herhangi bir kolleksiyon sayfasında aşağı doğru kayarsak sonraki sayfaya ait kayıtların otomatik olarak geldiğini görürüz. Bu pagination menüyü görmeye artık ihtiyaç kalmadı. Onu da görünmez yapalım. branch_page.scss stil dosyasına şunu ekleyelim:

app/assets/stylesheets/partials/posts/branch_page
.infinite-scroll {
    display: none;
}

Bu bölüm de bu kadar. Mola verelim. Sonraki bölümde tekrar ana sayfaya dönüp bazı değişiklikler yapacağız. Görüşmek umuduyla , kalın sağlıcakla..

Sırada şunlar var:







Hiç yorum yok:

Yorum Gönder