Vue ve Laravel ile oluşturulan yerel uygulamalar genellikle “İşte Inertia ile bir sayfayı nasıl render edeceksiniz” aşamasında kalıyor. Ancak gerçek SaaS uygulamaları, kullanıcı türüne göre dinamik düzenlere, overlay yönetimine, filtre durumunu koruyan sayfalama sistemlerine ve tüm bunları tam bir durum yönetim kütüphanesi karmaşıklığı olmadan sağlamalıdır.
<p>Kohana.io için tam bir ön uç modülü geliştirdim - bir üretim CRM/ERP'si - ve bunu Laravel için açık kaynak bir SaaS çerçevesine olan <strong>LaraFoundry</strong>ye aktarıyorum.</p>
<p>Bu yazı, tam ön uç mimarisini kapsamaktadır: düzen değişimi, overlay sistemi, sayfalama + filtreler, modal desenleri, Inertia bağlantıları ve test işlemleri.</p>
<h2>
<a name="the-problem" href="#the-problem"></a> Problem
</h2>
<p>Çok kiracılı bir SaaS uygulaması birden fazla düzen gerektirir. Kohana.io için beş düzen gereklidir:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Düzen</th>
<th>Ne zaman</th>
<th>İçerikleri</th>
</tr>
</thead>
<tbody>
<tr>
<td>GuestLayout</td>
<td>Giriş yapılmamış</td>
<td>Landing page, giriş/kayıt modalları</td>
</tr>
<tr>
<td>AuthLayout</td>
<td>Normal kullanıcı</td>
<td>Tam uygulama: başlık, menüler, bildirimler</td>
</tr>
<tr>
<td>AdminLayout</td>
<td>Admin kullanıcı</td>
<td>Farklı navigasyona sahip admin paneli</td>
</tr>
<tr>
<td>AuthBlockedLayout</td>
<td>Engellenmiş hesap</td>
<td>Sınırlı erişim, destek talepleri</td>
</tr>
<tr>
<td>AuthDeletedLayout</td>
<td>Silinmiş hesap</td>
<td>Veri talepleri için minimal UI</td>
</tr>
</tbody>
</table>
</div>
<p>Her düzen tamamen farklı bir bileşen ağacına sahiptir. Farklı başlıklar, farklı menüler ve farklı modal sistemleri. Inertia'nın sürekli düzenleri, sayfa gezintileri boyunca sabit kalır - sadece bir CSS sınıfını değiştiremezsiniz.</p>
<h2>
<a name="architecture-overview" href="#architecture-overview"></a> Mimari Genel Bakış
</h2>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext">
<p>Her sayfa, kendi düzenini açıkça tanımlamadığı sürece otomatik olarak <code>LayoutSwitcher</code>'ı alır. Sayfalar düzenleri import etmez. Sayfalar hangi düzenin onları sardığını bilmez.</p>
<h2>
<a name="the-overlay-system" href="#the-overlay-system"></a> Overlay Sistemi
</h2>
<p>Bir üretim SaaS düzeni yalnızca bir başlık ve yan çubuk değildir. Kohana.io'daki doğrulanmış düzen 7 açılır panel içermektedir:</p>
<ol>
<li>Mobil menü (soldan kayar)</li>
<li>Sağ profil menüsü (sağdan kayar)</li>
<li>Bildirim paneli</li>
<li>Dil seçici</li>
<li>Şirket değiştirici</li>
<li>Karşı taraf filtre çekmecesi</li>
<li>Ürün filtre çekmecesi</li>
</ol>
<p>Ve bunlar üst üste yığılır. Mobil menü açıkken dil seçiciyi açmak çift üst katman oluşturur - iki arka plan katmanı, bağımsız olarak kapatılabilir.</p>
<h3>
<a name="state-management-no-library" href="#state-management-no-library"></a> Durum Yönetimi (Kütüphane yok)
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
<p>Ham reaktif referanslar. Vuex, Pinia, veya etkinlik otobüsü yok.</p>
<h3>
<a name="overlay-logic" href="#overlay-logic"></a> Overlay Mantığı
</h3>
<ul>
<li>Açık herhangi bir açılır → tek overlay gösterilir ve <code>document.body.classList.add('non-overflow')</code> ile gövde kaydırma devre dışı bırakılır.</li>
<li>Üstte ikinci bir panel açıldığında → çift overlay gösterilir.</li>
<li>ESC tuşu → <code>onKeyStroke</code> ile üst katmanı önce kapatır.</li>
<li>Arka plana tıklamak → hepsini kapatır.</li>
<li>CSS geçişleri: tek overlay için 0.3s, çift için 0.1s (hızlı hissettirir).</li>
</ul>
<h3>
<a name="child-component-communication" href="#child-component-communication"></a> Çocuk Bileşen İletişimi
</h3>
<p>Bileşen ağacında derin sayfalar düzen overleylere <code>provide/inject</code> aracılığıyla tetikler:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
<p>Sayfa <code>showFilter()</code> çağrısını yapar. Düzen z-index'leri, arka planlar, gövde kaydırması ve geçiş zamanlamasını yönetir. Temiz bir ayrım sağlar.</p>
<h3>
<a name="transition-animations" href="#transition-animations"></a> Geçiş Animasyonları
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight css">
<p><code>.withQueryString()</code>, sayfalama bağlantıları oluşturulurken tüm filtre parametrelerini saklamak için kritik öneme sahiptir.</p>
<h3>
<a name="frontend-pagepaginator-component" href="#frontend-pagepaginator-component"></a> Ön Uç: PagePaginator Bileşeni
</h3>
<p>Paginator bileşeni sayfalara değil, düzen içerisinde yer alır. <code>AppMainContentLayout.vue</code>, sayfalama verisi mevcut olduğunda otomatik olarak render eder:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
<p>Ülke filtresi aktif mi? Saklandı. Sıralama yönü? Saklandı. Arama sorgusu? Saklandı. Durum yönetiminde bir gereklilik yok.</p>
<h3>
<a name="filter-autodiscovery-pattern" href="#filter-autodiscovery-pattern"></a> Filtre Otomatik Keşif Deseni
</h3>
<p>Filtre sistemi yöntem otomatik keşfine dayanıyor:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php">
abstract class Filter { public function apply(Builder $builder) { $this->builder = $builder; foreach ($this->request->all() as $name => $value) { if (method_exists($this, $name)) { call_user_func_array([$this, $name], [$value]); } } return $this->builder; } }
<p>İstek parametresi <code>country=DE</code> → <code>$filter->country('DE')</code> çağrısını tetikler. Rota yapılandırması yok. Hiçbir switch ifadesi yok. Yöntem mevcutsa, filtre uygulanır.</p>
<p>Somut bir filtre örneği:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php">
class ContragentsFilter extends Filter { public function search_string($value = null) { $words = preg_split('/\s+/', trim($value)); return $this->builder->where(function($query) use ($words) { foreach ($words as $word) { $query->whereRaw('LOWER(name) LIKE ?', ['%' . mb_strtolower($word) . '%']); } }); }
public function sort_by($value = null) {
$allowed = ['name', 'country', 'balance', 'created_at'];
if (in_array($value, $allowed)) {
$direction = $this->request->input('sort_direction', 'asc');
return $this->builder->orderBy($value, $direction);
}
return $this->builder->orderBy('name', 'asc');
}
}
<p>Beyaz listeye alınmış sıralama sütunları SQL enjeksiyonunu engeller. Kelime bazında arama eşleştirme. <code>starts_with()</code> yöntemi ile alfabetik navigasyon. Ürünler için hızlı filtre ön ayarları (<code>min_stock</code>, <code>has_reserved</code>).</p>
<h3>
<a name="frontend-filter-integration" href="#frontend-filter-integration"></a> Ön Uç Filtre Entegrasyonu
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
// Inertia yönlendirme navigasyonu ile durum koruma const performSearch = () => { const params = {}; if (filters.value.search) params.search_string = filters.value.search; if (filters.value.country) params.country = filters.value.country; // ... tüm filtre parametreleri
router.get(route(routeName.value), params, {
preserveState: true, // Bileşen durumu korunur
preserveScroll: true, // Kaydırma pozisyonu korunur
replace: true, // Tek bir geçmiş girişi
});
};
<p><code>preserveState: true</code> anahtardır. Bileşen reaktif durumu Inertia yönlendirmeleri sırasında korunur. Filtreleri bir Vuex store veya yerel depolama kullanarak saklamaya ihtiyaç yok.</p>
<h2>
<a name="modal-amp-popup-system" href="#modal-amp-popup-system"></a> Modal & Popup Sistemi
</h2>
<p>LaraFoundry, karmaşık etkileşimler için özel modallar ile onaylar için SweetAlert2 kullanarak bir hibrit yaklaşım benimsemektedir.</p>
<h3>
<a name="custom-form-modal-inertia-useform" href="#custom-form-modal-inertia-useform"></a> Özel Form Modal (Inertia useForm)
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight vue">
<p><code>form.processing</code> butonu devre dışı bırakır. <code>form.errors</code> sunucu tarafı doğrulamayı gösterir. <code>onSuccess</code> modali kapatır. Üç özellik, sıfır özelleştirilmiş kod.</p>
<h3>
<a name="async-dataloading-modal" href="#async-dataloading-modal"></a> Asenkron Veri Yükleme Modalı
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
<p>Modal görünür hale geldiğinde veri çekilir. Yükleme döngüsü. Hata durumu. Farklı veri görünümleri için sekmeler. ESC tuşu ile kapama.</p>
<h3>
<a name="sweetalert2-for-confirmations" href="#sweetalert2-for-confirmations"></a> Onaylar için SweetAlert2
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight javascript">
const result = await Swal.fire({ title: t('Remove Employee?'), text: t('This action will immediately remove the employee.'), icon: 'error', showCancelButton: true, confirmButtonColor: '#ef4444', });
if (result.isConfirmed) { router.delete(route('my_company.employees.remove', employee.id)); }
<p>Ana kararlar:</p>
<ul>
<li><strong><code>import.meta.glob</code></strong> - Vite her sayfayı kod parçalarına ayırır. Sadece mevcut sayfa yüklenir.</li>
<li><strong>Küresel <code>t()</code> fonksiyonu</strong> - hem <code>globalProperties</code> (Options API) hem de <code>globalThis</code> (Composition API) üzerinde kaydedilir. Herhangi bir yerde içe aktarma gerekir.</li>
<li><strong>İlerleme çubuğu gecikmesi: 0</strong> - navigasyonda hemen görünür. Kullanıcılar anlık geri bildirim görür.</li>
</ul>
<h3>
<a name="shared-props-middleware" href="#shared-props-middleware"></a> Paylaşılan Özellikler (Middleware)
</h3>
<div class="highlight js-code-highlight">
<pre class="highlight php">
<p><code>@/components/PagePaginator.vue</code> yerine ilişkisel yol zincirleri kullanılır.</p>
<h2>
<a name="testing" href="#testing"></a> Test
</h2>
<p>Ön uç davranışı, Inertia özelliklerini doğrulayan Pest özellik testleri aracılığıyla test edilir:<br/></p>
<div class="highlight js-code-highlight">
<pre class="highlight php">
it('serves auth layout for authenticated users', function () { actingAs(User::factory()->create()) ->get('/dashboard') ->assertInertia(fn($page) => $page->where('visitor_status', 'auth') ); });
it('returns pagination data with filter state', function () { Product::factory(30)->create(['company_id' => $company->id]);
<p>Şablon: sunucunun ne gönderdiğini test et. Eğer Inertia özellikleri doğruysa, Vue doğru render eder. Bu, düzen değiştirme, sayfalama, filtreler, flaş mesajlar ve modal verilerini kapsar.</p>
<p>Kompleks etkileşimli bileşenler (filtre izleyicileri, URL manipülasyonu) için birim testleri Vitest ile desteklenmelidir. Ancak çoğu ön uç davranışı için Pest özellik testleri yeterlidir.</p>
<h2>
<a name="directory-structure" href="#directory-structure"></a> Dizin Yapısı
</h2>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext">