From 736f9c536d4712ee747f0b2ac56e17ffbd939d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 02:21:31 -0300 Subject: [PATCH 01/15] Add initial website setup: Created essential files including package-lock.json, setup documentation, and foundational Blade templates. Implemented PageController for homepage and page display, integrated navigation and footer components, and established dark mode functionality. Enhanced CSS for custom scrollbar and smooth scrolling. Added SVG logos for branding. Updated routes for page handling. --- WEBSITE_SETUP.md | 229 ++ app/Http/Controllers/PageController.php | 68 + package-lock.json | 2307 +++++++++++++++++ public/logos/truncgil-dikey-dark.svg | 32 + public/logos/truncgil-dikey.svg | 35 + public/logos/truncgil-yatay-dark.svg | 35 + public/logos/truncgil-yatay.svg | 35 + resources/css/app.css | 32 +- resources/js/app.js | 20 + resources/views/components/footer.blade.php | 61 + .../views/components/navigation.blade.php | 127 + resources/views/home.blade.php | 155 ++ resources/views/layouts/app.blade.php | 35 + resources/views/page.blade.php | 103 + routes/web.php | 6 +- 15 files changed, 3276 insertions(+), 4 deletions(-) create mode 100644 WEBSITE_SETUP.md create mode 100644 app/Http/Controllers/PageController.php create mode 100644 package-lock.json create mode 100644 public/logos/truncgil-dikey-dark.svg create mode 100644 public/logos/truncgil-dikey.svg create mode 100644 public/logos/truncgil-yatay-dark.svg create mode 100644 public/logos/truncgil-yatay.svg create mode 100644 resources/views/components/footer.blade.php create mode 100644 resources/views/components/navigation.blade.php create mode 100644 resources/views/home.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/page.blade.php diff --git a/WEBSITE_SETUP.md b/WEBSITE_SETUP.md new file mode 100644 index 0000000..b1adf60 --- /dev/null +++ b/WEBSITE_SETUP.md @@ -0,0 +1,229 @@ +# Trunçgil Teknoloji Web Sitesi Kurulum Kılavuzu + +## 🎉 Web sitesi başarıyla oluşturuldu! + +Modern, responsive ve dark mode destekli bir web sitesi hazır. + +## 📋 Özellikler + +✅ **Dinamik Menü Sistemi** +- Admin panelinden yönetilebilir +- `show_in_menu` işaretli sayfalar otomatik görünür +- `parent_page` ile hiyerarşik menü yapısı +- `sort_order` ile sıralama desteği +- Dropdown menüler + +✅ **Dark/Light Mode** +- Otomatik tema algılama +- Local storage ile tercih kaydetme +- Smooth transitions +- Her sayfada erişilebilir toggle buton + +✅ **Modern Tasarım** +- Tailwind CSS ile responsive design +- Gradient backgrounds +- Smooth animations +- Custom scrollbar +- Modern tipografi (Inter font) + +✅ **SEO Friendly** +- Meta title ve description desteği +- Semantic HTML +- Clean URLs +- Open Graph hazır + +## 🚀 Kurulum Adımları + +### 1. NPM Paketlerini Kur ve Build Et + +```bash +npm install +npm run build +``` + +### 2. Veritabanını Kontrol Et + +Eğer henüz migration'ları çalıştırmadıysanız: + +```bash +php artisan migrate +``` + +### 3. Storage Link Oluştur + +Featured image'ların görünmesi için: + +```bash +php artisan storage:link +``` + +### 4. İlk Kullanıcı ve Sayfa Oluştur + +Admin paneline giriş yapın: +``` +https://citrus.truncgil.com/admin +``` + +**İlk Sayfa Oluşturma:** +1. Pages > Create New Page +2. Başlık ve içerik girin +3. "Homepage" checkbox'ını işaretleyin (anasayfa için) +4. "Show in Menu" işaretleyin (menüde görünsün) +5. Status: Published +6. Save + +**Menü Yapısı:** +- Ana sayfa: `parent_page` boş, `sort_order: 0` +- Alt sayfa: `parent_page` seçili, `sort_order: 1, 2, 3...` + +## 📁 Oluşturulan Dosyalar + +### Controllers +- `app/Http/Controllers/PageController.php` - Sayfa kontrolcüsü + +### Routes +- `routes/web.php` - Web route'ları + +### Views +- `resources/views/layouts/app.blade.php` - Ana layout +- `resources/views/components/navigation.blade.php` - Dinamik menü +- `resources/views/components/footer.blade.php` - Footer +- `resources/views/home.blade.php` - Anasayfa +- `resources/views/page.blade.php` - Sayfa detay + +### Assets +- `resources/css/app.css` - Güncellenmiş CSS +- `resources/js/app.js` - Dark mode JavaScript + +## 🎨 Tasarım Özellikleri + +### Renkler +- **Primary:** Red (#DC2626) +- **Secondary:** Orange (#EA580C) +- **Dark Mode:** Gray scale + +### Logo Kullanımı +- Light mode: `truncgil-yatay.svg` / `truncgil-dikey.svg` +- Dark mode: `truncgil-yatay-dark.svg` / `truncgil-dikey-dark.svg` + +### Responsive Breakpoints +- Mobile: < 768px +- Tablet: 768px - 1024px +- Desktop: > 1024px + +## 📖 Kullanım + +### Yeni Sayfa Ekleme + +1. Admin Panel > Pages > Create +2. Form alanlarını doldurun: + - **Title:** Sayfa başlığı + - **Slug:** URL (otomatik oluşur) + - **Content:** Sayfa içeriği (Rich Editor) + - **Excerpt:** Kısa açıklama + - **Featured Image:** Öne çıkan görsel + - **Status:** published (yayında olması için) + - **Parent Page:** Boş = Ana menü, Dolu = Alt menü + - **Sort Order:** Menüde sıralama (0, 1, 2...) + - **Show in Menu:** ✅ İşaretli olmalı + - **Is Homepage:** Sadece anasayfa için ✅ + +### Menü Yapısı Örneği + +``` +Ana Sayfa (parent_id: null, sort_order: 0) +Hakkımızda (parent_id: null, sort_order: 1) + ├─ Ekibimiz (parent_id: Hakkımızda, sort_order: 0) + └─ Tarihçe (parent_id: Hakkımızda, sort_order: 1) +Hizmetler (parent_id: null, sort_order: 2) + ├─ Web Yazılım (parent_id: Hizmetler, sort_order: 0) + └─ Mobil Uygulama (parent_id: Hizmetler, sort_order: 1) +İletişim (parent_id: null, sort_order: 3) +``` + +## 🔧 Geliştirme + +### Development Mode +```bash +npm run dev +``` + +### Build for Production +```bash +npm run build +``` + +### Clear Cache +```bash +php artisan cache:clear +php artisan view:clear +php artisan config:clear +``` + +## 🎯 SEO Optimizasyonu + +Her sayfa için: +1. **Meta Title:** 60 karakter max +2. **Meta Description:** 160 karakter max +3. **Slug:** URL-friendly (otomatik oluşur) +4. **Alt Text:** Görsellere açıklayıcı text + +## 🌐 Tarayıcı Desteği + +- ✅ Chrome (son 2 versiyon) +- ✅ Firefox (son 2 versiyon) +- ✅ Safari (son 2 versiyon) +- ✅ Edge (son 2 versiyon) + +## 📱 Mobil Responsive + +Tüm sayfalar mobil uyumlu: +- Touch-friendly menü +- Responsive images +- Mobile-optimized typography +- Hamburger menu + +## 🎨 Özelleştirme + +### Logo Değiştirme +Logolar `public/logos/` klasöründe. Değiştirmek için: +1. Aynı isimde yeni logo yükleyin +2. Cache'i temizleyin + +### Renk Değiştirme +`resources/css/app.css` dosyasında Tailwind theme ayarları + +### İletişim Bilgileri +`resources/views/components/footer.blade.php` dosyasında düzenleyin + +## 🐛 Sorun Giderme + +### Stiller Yüklenmiyorsa +```bash +npm run build +php artisan cache:clear +``` + +### Dark Mode Çalışmıyorsa +Browser console'da: +```javascript +localStorage.clear() +``` +Sayfayı yenileyin. + +### Menü Görünmüyorsa +Kontrol edin: +- ✅ Status: published +- ✅ Show in Menu: checked +- ✅ Parent sayfalar da published + +## 📞 Destek + +Sorularınız için: +- Email: info@truncgil.com +- Admin Panel: /admin + +## 🎉 Başarılar! + +Web siteniz hazır! Citrus admin panelinden içerik yönetmeye başlayabilirsiniz. + diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php new file mode 100644 index 0000000..7907eaa --- /dev/null +++ b/app/Http/Controllers/PageController.php @@ -0,0 +1,68 @@ +where('status', 'published') + ->first(); + + if (!$page) { + // Homepage yoksa, view'e boş geçelim + return view('home', [ + 'page' => null, + 'menuItems' => $this->getMenuItems() + ]); + } + + return view('home', [ + 'page' => $page, + 'menuItems' => $this->getMenuItems() + ]); + } + + /** + * Display a specific page by slug + */ + public function show($slug) + { + $page = Page::where('slug', $slug) + ->where('status', 'published') + ->firstOrFail(); + + return view('page', [ + 'page' => $page, + 'menuItems' => $this->getMenuItems() + ]); + } + + /** + * Get menu items for navigation + */ + private function getMenuItems() + { + return Page::with(['children' => function ($query) { + $query->where('status', 'published') + ->where('show_in_menu', true) + ->orderBy('sort_order', 'asc') + ->orderBy('title', 'asc'); + }]) + ->whereNull('parent_id') + ->where('status', 'published') + ->where('show_in_menu', true) + ->orderBy('sort_order', 'asc') + ->orderBy('title', 'asc') + ->get(); + } +} + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..90ee789 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2307 @@ +{ + "name": "public_html", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.3.tgz", + "integrity": "sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.3.tgz", + "integrity": "sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.3.tgz", + "integrity": "sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.3.tgz", + "integrity": "sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.3.tgz", + "integrity": "sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.3.tgz", + "integrity": "sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.3.tgz", + "integrity": "sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.3.tgz", + "integrity": "sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.3.tgz", + "integrity": "sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.3.tgz", + "integrity": "sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.3.tgz", + "integrity": "sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.3.tgz", + "integrity": "sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.3.tgz", + "integrity": "sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.3.tgz", + "integrity": "sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.3.tgz", + "integrity": "sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.3.tgz", + "integrity": "sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.3.tgz", + "integrity": "sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.3.tgz", + "integrity": "sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.3.tgz", + "integrity": "sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.3.tgz", + "integrity": "sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.3.tgz", + "integrity": "sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz", + "integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "dev": true, + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "dev": true, + "dependencies": { + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-generator-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-generator-function/-/async-generator-function-1.0.0.tgz", + "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "dev": true, + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.0.tgz", + "integrity": "sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.1.tgz", + "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "async-generator-function": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "dev": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", + "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^7.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.52.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.3.tgz", + "integrity": "sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.3", + "@rollup/rollup-android-arm64": "4.52.3", + "@rollup/rollup-darwin-arm64": "4.52.3", + "@rollup/rollup-darwin-x64": "4.52.3", + "@rollup/rollup-freebsd-arm64": "4.52.3", + "@rollup/rollup-freebsd-x64": "4.52.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.3", + "@rollup/rollup-linux-arm-musleabihf": "4.52.3", + "@rollup/rollup-linux-arm64-gnu": "4.52.3", + "@rollup/rollup-linux-arm64-musl": "4.52.3", + "@rollup/rollup-linux-loong64-gnu": "4.52.3", + "@rollup/rollup-linux-ppc64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-gnu": "4.52.3", + "@rollup/rollup-linux-riscv64-musl": "4.52.3", + "@rollup/rollup-linux-s390x-gnu": "4.52.3", + "@rollup/rollup-linux-x64-gnu": "4.52.3", + "@rollup/rollup-linux-x64-musl": "4.52.3", + "@rollup/rollup-openharmony-arm64": "4.52.3", + "@rollup/rollup-win32-arm64-msvc": "4.52.3", + "@rollup/rollup-win32-ia32-msvc": "4.52.3", + "@rollup/rollup-win32-x64-gnu": "4.52.3", + "@rollup/rollup-win32-x64-msvc": "4.52.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true + }, + "node_modules/vite": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", + "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/public/logos/truncgil-dikey-dark.svg b/public/logos/truncgil-dikey-dark.svg new file mode 100644 index 0000000..fb27779 --- /dev/null +++ b/public/logos/truncgil-dikey-dark.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/truncgil-dikey.svg b/public/logos/truncgil-dikey.svg new file mode 100644 index 0000000..084ae34 --- /dev/null +++ b/public/logos/truncgil-dikey.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/truncgil-yatay-dark.svg b/public/logos/truncgil-yatay-dark.svg new file mode 100644 index 0000000..4ab45a9 --- /dev/null +++ b/public/logos/truncgil-yatay-dark.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/logos/truncgil-yatay.svg b/public/logos/truncgil-yatay.svg new file mode 100644 index 0000000..6220e6a --- /dev/null +++ b/public/logos/truncgil-yatay.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 1f42c6c..497166a 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -6,8 +6,38 @@ @source '../**/*.js'; @theme { - --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; } +/* Custom Scrollbar */ +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + @apply bg-gray-100 dark:bg-gray-800; +} + +::-webkit-scrollbar-thumb { + @apply bg-gray-300 dark:bg-gray-600 rounded-full; +} + +::-webkit-scrollbar-thumb:hover { + @apply bg-gray-400 dark:bg-gray-500; +} + +/* Smooth scroll behavior */ +html { + scroll-behavior: smooth; +} + +/* Line clamp utilities */ +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + diff --git a/resources/js/app.js b/resources/js/app.js index 29f5ccf..90ae063 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,3 +1,23 @@ import './bootstrap'; +// Dark mode initialization +document.addEventListener('DOMContentLoaded', function() { + // Check for saved theme preference or default to 'light' + const savedTheme = localStorage.getItem('darkMode'); + + if (savedTheme === null) { + // If no saved preference, check system preference + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + localStorage.setItem('darkMode', prefersDark.toString()); + } + + // Listen for system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + if (localStorage.getItem('darkMode') === null) { + localStorage.setItem('darkMode', e.matches.toString()); + window.location.reload(); + } + }); +}); + diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php new file mode 100644 index 0000000..04b156a --- /dev/null +++ b/resources/views/components/footer.blade.php @@ -0,0 +1,61 @@ + + diff --git a/resources/views/components/navigation.blade.php b/resources/views/components/navigation.blade.php new file mode 100644 index 0000000..880114e --- /dev/null +++ b/resources/views/components/navigation.blade.php @@ -0,0 +1,127 @@ + + + +
+ diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 0000000..4cf4c76 --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,155 @@ +@extends('layouts.app') + +@section('title', $page ? ($page->meta_title ?? $page->title) : 'Trunçgil Teknoloji') +@section('description', $page ? ($page->meta_description ?? $page->excerpt) : 'Trunçgil Teknoloji - Yenilikçi Çözümler') + +@section('content') + + +
+
+ +
+
+
+ +
+
+ +
+ @if($page) +

+ {{ $page->title }} +

+ @if($page->excerpt) +

+ {{ $page->excerpt }} +

+ @endif + @else +

+ Hoş Geldiniz + + Trunçgil Teknoloji + +

+

+ Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz +

+ @endif + + +
+ + +
+
+
+ Trunçgil Teknoloji + +
+
+
+
+
+ + @if($page && $page->content) + +
+
+
+ {!! $page->content !!} +
+
+
+ @endif + + +
+
+
+

+ Neden Trunçgil Teknoloji? +

+

+ Modern teknolojiler ve uzman ekibimizle projelerinizi hayata geçiriyoruz +

+
+ +
+ +
+
+ + + +
+

+ Hızlı Çözümler +

+

+ Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz. +

+
+ + +
+
+ + + +
+

+ Güvenli Altyapı +

+

+ En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz. +

+
+ + +
+
+ + + +
+

+ Uzman Ekip +

+

+ Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız. +

+
+
+
+
+ + +
+
+

+ Projenizi Konuşalım +

+

+ Size özel çözümler geliştirmek için iletişime geçin +

+ + İletişime Geç + + + + +
+
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..ba0f1f5 --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,35 @@ + + + + + + + + @yield('title', config('app.name', 'Trunçgil Teknoloji')) + + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + + + + + @include('components.navigation') + + +
+ @yield('content') +
+ + + @include('components.footer') + + + diff --git a/resources/views/page.blade.php b/resources/views/page.blade.php new file mode 100644 index 0000000..b67e128 --- /dev/null +++ b/resources/views/page.blade.php @@ -0,0 +1,103 @@ +@extends('layouts.app') + +@section('title', $page->meta_title ?? $page->title) +@section('description', $page->meta_description ?? $page->excerpt ?? '') + +@section('content') + + +
+
+
+
+
+ +
+
+

+ {{ $page->title }} +

+ + @if($page->excerpt) +

+ {{ $page->excerpt }} +

+ @endif + + @if($page->published_at) +
+ + + + {{ $page->published_at->format('d M Y') }} +
+ @endif +
+
+
+ + +
+
+ @if($page->featured_image) +
+ {{ $page->title }} +
+ @endif + +
+ {!! $page->content !!} +
+ + + @if($page->children->where('status', 'published')->count() > 0) +
+

+ İlgili Sayfalar +

+
+ @foreach($page->children->where('status', 'published')->sortBy('sort_order') as $child) + +

+ {{ $child->title }} +

+ @if($child->excerpt) +

+ {{ $child->excerpt }} +

+ @endif +
+ @endforeach +
+
+ @endif + + +
+ + + + + Ana Sayfaya Dön + + + @if($page->parent) + + Üst Sayfa + + + + + @endif +
+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 86a06c5..384e847 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,7 +1,7 @@ name('home'); +Route::get('/{slug}', [PageController::class, 'show'])->name('page.show'); From 38ae33d16459ff7fd969261e6c073f9a4d75a4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:14:32 -0300 Subject: [PATCH 02/15] Enhance documentation for Filament 4.x import rules and universal translation system. Added guidelines for table actions, form components, and translation methods, ensuring clarity on correct usage and implementation. Updated translation workflow and helper functions for better integration in models. --- .cursorrules | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/.cursorrules b/.cursorrules index cb20757..948defd 100644 --- a/.cursorrules +++ b/.cursorrules @@ -50,6 +50,82 @@ php artisan make:filament-resource ModelName --generate --model --migration --fa - Table column'ları için header'ları localization'dan al - Validation mesajları için localization kullan +### Filament 4.x Import Kuralları (ÖNEMLİ!) + +#### **Table Actions (DOĞRU):** +```php +// Table Actions için +use Filament\Actions\EditAction; +use Filament\Actions\DeleteAction; +use Filament\Actions\RestoreAction; +use Filament\Actions\ForceDeleteAction; + +// Bulk Actions için +use Filament\Actions\BulkActionGroup; +use Filament\Actions\DeleteBulkAction; +use Filament\Actions\ForceDeleteBulkAction; +use Filament\Actions\RestoreBulkAction; + +// Filters için +use Filament\Tables\Filters\TrashedFilter; +use Filament\Tables\Filters\SelectFilter; + +// Columns için +use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\ImageColumn; + +// Table metodları +$table + ->recordActions([ // ✅ Doğru + EditAction::make(), + ]) + ->toolbarActions([ // ✅ Doğru + BulkActionGroup::make([ + DeleteBulkAction::make(), + ]), + ]); +``` + +#### **Form & Schema Components (DOĞRU):** +```php +// Form input components +use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\Checkbox; +use Filament\Forms\Components\Toggle; +use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\DateTimePicker; +use Filament\Forms\Components\FileUpload; + +// Layout components (ÖNEMLİ: Schemas namespace!) +use Filament\Schemas\Components\Section; // ✅ Doğru +use Filament\Schemas\Components\Grid; // ✅ Doğru +use Filament\Schemas\Components\Tabs; // ✅ Doğru +use Filament\Schemas\Components\Fieldset; // ✅ Doğru +``` + +#### **YANLIŞ KULLANIM (Eski Filament 3.x):** +```php +// ❌ YANLIŞ - Bu namespace'ler Filament 4'te YOK! +use Filament\Tables\Actions\EditAction; // ❌ +use Filament\Forms\Components\Section; // ❌ +use Filament\Forms\Components\Grid; // ❌ + +// ❌ YANLIŞ - Eski metod adları +$table + ->actions([...]) // ❌ Kullanma! + ->bulkActions([...]) // ❌ Kullanma! +``` + +#### **ÖZET:** +- ✅ **Table Actions:** `Filament\Actions\` namespace +- ✅ **Table Columns:** `Filament\Tables\Columns\` namespace +- ✅ **Table Filters:** `Filament\Tables\Filters\` namespace +- ✅ **Form Inputs:** `Filament\Forms\Components\` namespace +- ✅ **Layout Components:** `Filament\Schemas\Components\` namespace (Section, Grid, Tabs) +- ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()` + ### Dil Dosyası Yapısı ```php setTranslation('field_name', 'en', 'Translation', 'published'); + +// Çeviri oku +$model->translate('field_name'); // Mevcut dilde +$model->translate('field_name', 'en'); // Belirli dilde +$model->translate('field_name', 'en', false); // Fallback olmadan + +// Çeviri kontrolü +$model->hasTranslation('field_name', 'en'); + +// İlerleme takibi +$model->getTranslationProgress(); // ['tr' => 100, 'en' => 75] +$model->getMissingTranslations('en'); // ['meta_description'] +``` + +### Translation Workflow +- `draft` → `review` → `approved` → `published` +- Sadece `published` çeviriler son kullanıcıya görünür +- User-language permissions ile kontrol edilir + +### Helper Functions +```php +current_language(); // Mevcut dil objesi +available_languages(); // Aktif diller +translate_model($model, 'field', 'en'); // Model çevirisi +user_can_manage_language('en'); // Kullanıcı yetkisi +switch_language('en'); // Dil değiştir +``` + +### Dokümantasyon +- `docs/TRANSLATION_SYSTEM.md` - Tam sistem dokümantasyonu +- `docs/TRANSLATION_QUICK_START.md` - Hızlı başlangıç rehberi From bd5177e70caed54735f0fab5cc809943a624f419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:14:46 -0300 Subject: [PATCH 03/15] Add translation helper functions: Introduced a new file for translation-related helper functions to enhance localization capabilities. Updated composer.json to autoload the new helper file, ensuring easy access to functions for managing languages, translations, and their statuses. --- app/Helpers/translation_helpers.php | 284 ++++++++++++++++++++++++++++ composer.json | 5 +- 2 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 app/Helpers/translation_helpers.php diff --git a/app/Helpers/translation_helpers.php b/app/Helpers/translation_helpers.php new file mode 100644 index 0000000..d71464b --- /dev/null +++ b/app/Helpers/translation_helpers.php @@ -0,0 +1,284 @@ +getLocale()); + } +} + +if (!function_exists('current_language_code')) { + /** + * Get current language code + */ + function current_language_code(): string + { + return app()->getLocale(); + } +} + +if (!function_exists('default_language')) { + /** + * Get default language + */ + function default_language(): ?Language + { + return Language::getDefault(); + } +} + +if (!function_exists('default_language_code')) { + /** + * Get default language code + */ + function default_language_code(): string + { + $defaultLanguage = Language::getDefault(); + return $defaultLanguage ? $defaultLanguage->code : config('app.locale'); + } +} + +if (!function_exists('available_languages')) { + /** + * Get all active languages + */ + function available_languages(): Collection + { + return Language::getActive(); + } +} + +if (!function_exists('available_language_codes')) { + /** + * Get all active language codes + */ + function available_language_codes(): array + { + return Language::active()->pluck('code')->toArray(); + } +} + +if (!function_exists('translate_model')) { + /** + * Get translated value for a model field + * + * @param mixed $model Model instance + * @param string $field Field name to translate + * @param string|null $languageCode Language code (null for current) + * @param bool $fallback Use fallback to default language + * @return mixed + */ + function translate_model($model, string $field, ?string $languageCode = null, bool $fallback = true): mixed + { + if (!method_exists($model, 'translate')) { + return $model->{$field} ?? null; + } + + return $model->translate($field, $languageCode, $fallback); + } +} + +if (!function_exists('has_translation')) { + /** + * Check if model has translation for field and language + */ + function has_translation($model, string $field, ?string $languageCode = null): bool + { + if (!method_exists($model, 'hasTranslation')) { + return false; + } + + $languageCode = $languageCode ?? current_language_code(); + return $model->hasTranslation($field, $languageCode); + } +} + +if (!function_exists('translation_progress')) { + /** + * Get translation progress for a model + */ + function translation_progress($model, ?string $languageCode = null): int + { + if (!method_exists($model, 'getTranslationProgress')) { + return 0; + } + + $progress = $model->getTranslationProgress(); + + if ($languageCode) { + return $progress[$languageCode] ?? 0; + } + + return $progress; + } +} + +if (!function_exists('user_can_manage_language')) { + /** + * Check if current user can manage a language + */ + function user_can_manage_language(string $languageCode): bool + { + $user = auth()->user(); + + if (!$user || !method_exists($user, 'canManageLanguage')) { + return false; + } + + return $user->canManageLanguage($languageCode); + } +} + +if (!function_exists('user_managed_languages')) { + /** + * Get languages that current user can manage + */ + function user_managed_languages(): Collection + { + $user = auth()->user(); + + if (!$user || !method_exists($user, 'getManagedLanguages')) { + return collect(); + } + + return $user->getManagedLanguages(); + } +} + +if (!function_exists('user_managed_language_codes')) { + /** + * Get language codes that current user can manage + */ + function user_managed_language_codes(): array + { + $user = auth()->user(); + + if (!$user || !method_exists($user, 'getManagedLanguageCodes')) { + return []; + } + + return $user->getManagedLanguageCodes(); + } +} + +if (!function_exists('switch_language')) { + /** + * Switch application language + */ + function switch_language(string $languageCode): bool + { + $language = Language::findByCode($languageCode); + + if (!$language || !$language->is_active) { + return false; + } + + app()->setLocale($languageCode); + + if (auth()->check()) { + session(['locale' => $languageCode]); + } + + return true; + } +} + +if (!function_exists('get_translation_status_badge')) { + /** + * Get translation status badge HTML + */ + function get_translation_status_badge(string $status): string + { + $badges = [ + 'draft' => 'Draft', + 'review' => 'Review', + 'approved' => 'Approved', + 'published' => 'Published', + ]; + + return $badges[$status] ?? $badges['draft']; + } +} + +if (!function_exists('get_translation_status_color')) { + /** + * Get translation status color + */ + function get_translation_status_color(string $status): string + { + $colors = [ + 'draft' => 'gray', + 'review' => 'warning', + 'approved' => 'success', + 'published' => 'primary', + ]; + + return $colors[$status] ?? 'gray'; + } +} + +if (!function_exists('get_language_flag')) { + /** + * Get language flag emoji or icon + */ + function get_language_flag(string $languageCode): string + { + $language = Language::findByCode($languageCode); + return $language ? ($language->flag ?? '🌐') : '🌐'; + } +} + +if (!function_exists('format_language_name')) { + /** + * Format language name with native name + */ + function format_language_name(string $languageCode): string + { + $language = Language::findByCode($languageCode); + + if (!$language) { + return $languageCode; + } + + return "{$language->name} ({$language->native_name})"; + } +} + +if (!function_exists('translation_exists')) { + /** + * Check if a translation exists in database + */ + function translation_exists(string $modelClass, int $modelId, string $field, string $languageCode): bool + { + return Translation::forModel($modelClass, $modelId) + ->forField($field) + ->forLanguage($languageCode) + ->exists(); + } +} + +if (!function_exists('get_translation_value')) { + /** + * Get translation value directly from Translation model + */ + function get_translation_value(string $modelClass, int $modelId, string $field, ?string $languageCode = null): ?string + { + $languageCode = $languageCode ?? current_language_code(); + + $translation = Translation::forModel($modelClass, $modelId) + ->forField($field) + ->forLanguage($languageCode) + ->published() + ->first(); + + return $translation ? $translation->field_value : null; + } +} + diff --git a/composer.json b/composer.json index d6aa7cd..b3ff955 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,10 @@ "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "app/Helpers/translation_helpers.php" + ] }, "autoload-dev": { "psr-4": { From b5a4c7669d9fc0f48fcc826790a25d2c314c18bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:15:03 -0300 Subject: [PATCH 04/15] Add migration files for language and translation management: Created migrations for languages, translations, and user_languages tables to support the universal translation system. Each migration includes necessary fields, constraints, and indexes to ensure efficient data handling and relationships. --- ...25_10_01_053644_create_languages_table.php | 41 +++++++++++++++ ...10_01_053653_create_translations_table.php | 52 +++++++++++++++++++ ..._01_053657_create_user_languages_table.php | 43 +++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 database/migrations/2025_10_01_053644_create_languages_table.php create mode 100644 database/migrations/2025_10_01_053653_create_translations_table.php create mode 100644 database/migrations/2025_10_01_053657_create_user_languages_table.php diff --git a/database/migrations/2025_10_01_053644_create_languages_table.php b/database/migrations/2025_10_01_053644_create_languages_table.php new file mode 100644 index 0000000..ac59216 --- /dev/null +++ b/database/migrations/2025_10_01_053644_create_languages_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('code', 10)->unique()->comment('Language code (tr, en, de)'); + $table->string('name', 100)->comment('Language name in English'); + $table->string('native_name', 100)->comment('Language name in native language'); + $table->string('flag', 10)->nullable()->comment('Flag emoji or icon code'); + $table->enum('direction', ['ltr', 'rtl'])->default('ltr')->comment('Text direction'); + $table->boolean('is_active')->default(true)->comment('Is language active?'); + $table->boolean('is_default')->default(false)->comment('Is default language?'); + $table->integer('sort_order')->default(0)->comment('Display order'); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['is_active', 'sort_order']); + $table->index('code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('languages'); + } +}; diff --git a/database/migrations/2025_10_01_053653_create_translations_table.php b/database/migrations/2025_10_01_053653_create_translations_table.php new file mode 100644 index 0000000..cd6d241 --- /dev/null +++ b/database/migrations/2025_10_01_053653_create_translations_table.php @@ -0,0 +1,52 @@ +id(); + $table->string('translatable_type')->comment('Model class name'); + $table->unsignedBigInteger('translatable_id')->comment('Model ID'); + $table->string('language_code', 10)->comment('Language code'); + $table->string('field_name', 100)->comment('Field name to translate'); + $table->longText('field_value')->nullable()->comment('Translated value'); + $table->enum('status', ['draft', 'review', 'approved', 'published'])->default('draft')->comment('Translation status'); + $table->integer('version')->default(1)->comment('Translation version'); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->timestamp('published_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + // Indexes for performance + $table->index(['translatable_type', 'translatable_id']); + $table->index(['language_code', 'status']); + $table->index(['created_by', 'status']); + $table->index('approved_by'); + + // Unique constraint: one translation per model+field+language + $table->unique(['translatable_type', 'translatable_id', 'language_code', 'field_name'], 'unique_translation'); + + // Foreign key for language + $table->foreign('language_code')->references('code')->on('languages')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('translations'); + } +}; diff --git a/database/migrations/2025_10_01_053657_create_user_languages_table.php b/database/migrations/2025_10_01_053657_create_user_languages_table.php new file mode 100644 index 0000000..6c2e89c --- /dev/null +++ b/database/migrations/2025_10_01_053657_create_user_languages_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->string('language_code', 10); + $table->boolean('can_create')->default(true)->comment('Can create content in this language'); + $table->boolean('can_edit')->default(true)->comment('Can edit content in this language'); + $table->boolean('can_approve')->default(false)->comment('Can approve translations in this language'); + $table->boolean('can_publish')->default(false)->comment('Can publish content in this language'); + $table->timestamps(); + + // Unique constraint: one entry per user+language + $table->unique(['user_id', 'language_code']); + + // Foreign key for language + $table->foreign('language_code')->references('code')->on('languages')->onDelete('cascade'); + + // Indexes + $table->index(['user_id', 'language_code']); + $table->index('language_code'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_languages'); + } +}; From 105e14b48d06c27128766c88315866bc5c6c0e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:15:21 -0300 Subject: [PATCH 05/15] Add Language Management Module: Created Language resource with CRUD pages, schemas, and models to support the universal translation system. Implemented localization for English and Turkish, ensuring consistent user experience. Added necessary policies and seeder for initial language data, enhancing the overall language management functionality. --- .../Resources/Languages/LanguageResource.php | 68 +++ .../Languages/Pages/CreateLanguage.php | 39 ++ .../Languages/Pages/EditLanguage.php | 53 +++ .../Languages/Pages/ListLanguages.php | 26 ++ .../Languages/Schemas/LanguageForm.php | 95 ++++ .../Languages/Tables/LanguagesTable.php | 113 +++++ app/Models/Blog.php | 14 +- app/Models/Language.php | 124 +++++ app/Models/Page.php | 14 +- app/Models/Translation.php | 201 ++++++++ app/Models/User.php | 125 +++++ app/Models/UserLanguage.php | 105 +++++ app/Policies/LanguagePolicy.php | 97 ++++ app/Providers/AuthServiceProvider.php | 3 + app/Traits/HasTranslations.php | 309 +++++++++++++ database/seeders/LanguageSeeder.php | 78 ++++ docs/TRANSLATION_QUICK_START.md | 352 ++++++++++++++ docs/TRANSLATION_SYSTEM.md | 431 ++++++++++++++++++ lang/en/language.php | 75 +++ lang/tr/language.php | 75 +++ resources/css/citrus.css | 5 +- 21 files changed, 2398 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Admin/Resources/Languages/LanguageResource.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php create mode 100644 app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php create mode 100644 app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php create mode 100644 app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php create mode 100644 app/Models/Language.php create mode 100644 app/Models/Translation.php create mode 100644 app/Models/UserLanguage.php create mode 100644 app/Policies/LanguagePolicy.php create mode 100644 app/Traits/HasTranslations.php create mode 100644 database/seeders/LanguageSeeder.php create mode 100644 docs/TRANSLATION_QUICK_START.md create mode 100644 docs/TRANSLATION_SYSTEM.md create mode 100644 lang/en/language.php create mode 100644 lang/tr/language.php diff --git a/app/Filament/Admin/Resources/Languages/LanguageResource.php b/app/Filament/Admin/Resources/Languages/LanguageResource.php new file mode 100644 index 0000000..fb7c4ea --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/LanguageResource.php @@ -0,0 +1,68 @@ +schema(\App\Filament\Admin\Resources\Languages\Schemas\LanguageForm::schema()); + } + + public static function table(Table $table): Table + { + return \App\Filament\Admin\Resources\Languages\Tables\LanguagesTable::configure($table); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => ListLanguages::route('/'), + 'create' => CreateLanguage::route('/create'), + 'edit' => EditLanguage::route('/{record}/edit'), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php b/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php new file mode 100644 index 0000000..a651c6c --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/CreateLanguage.php @@ -0,0 +1,39 @@ +success() + ->title(__('language.created_successfully')) + ->body(__('language.created_successfully_body')); + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function mutateFormDataBeforeCreate(array $data): array + { + $data['created_by'] = auth()->id(); + $data['updated_by'] = auth()->id(); + + return $data; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php new file mode 100644 index 0000000..91a6a1a --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/EditLanguage.php @@ -0,0 +1,53 @@ +label(__('language.delete')), + RestoreAction::make() + ->label(__('language.restore')), + ForceDeleteAction::make() + ->label(__('language.force_delete')), + ]; + } + + protected function getSavedNotification(): ?Notification + { + return Notification::make() + ->success() + ->title(__('language.updated_successfully')) + ->body(__('language.updated_successfully_body')); + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function mutateFormDataBeforeSave(array $data): array + { + $data['updated_by'] = auth()->id(); + + return $data; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php b/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php new file mode 100644 index 0000000..23ac100 --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Pages/ListLanguages.php @@ -0,0 +1,26 @@ +label(__('language.create')), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php b/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php new file mode 100644 index 0000000..0eecc68 --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Schemas/LanguageForm.php @@ -0,0 +1,95 @@ +schema([ + Grid::make(2)->schema([ + TextInput::make('code') + ->label(__('language.code')) + ->required() + ->unique(ignoreRecord: true) + ->maxLength(10) + ->placeholder('tr, en, de') + ->helperText(__('language.code_helper')) + ->columnSpan(1), + + TextInput::make('flag') + ->label(__('language.flag')) + ->maxLength(10) + ->placeholder('🇹🇷') + ->helperText(__('language.flag_helper')) + ->columnSpan(1), + ]), + + Grid::make(2)->schema([ + TextInput::make('name') + ->label(__('language.name')) + ->required() + ->maxLength(100) + ->placeholder('Turkish') + ->helperText(__('language.name_helper')) + ->columnSpan(1), + + TextInput::make('native_name') + ->label(__('language.native_name')) + ->required() + ->maxLength(100) + ->placeholder('Türkçe') + ->helperText(__('language.native_name_helper')) + ->columnSpan(1), + ]), + + Grid::make(2)->schema([ + Select::make('direction') + ->label(__('language.direction')) + ->options([ + 'ltr' => __('language.direction_ltr'), + 'rtl' => __('language.direction_rtl'), + ]) + ->default('ltr') + ->required() + ->columnSpan(1), + + TextInput::make('sort_order') + ->label(__('language.sort_order')) + ->numeric() + ->default(0) + ->helperText(__('language.sort_order_helper')) + ->columnSpan(1), + ]), + ]), + + Section::make(__('language.settings')) + ->schema([ + Grid::make(3)->schema([ + Checkbox::make('is_active') + ->label(__('language.is_active')) + ->default(true) + ->helperText(__('language.is_active_helper')) + ->columnSpan(1), + + Checkbox::make('is_default') + ->label(__('language.is_default')) + ->default(false) + ->helperText(__('language.is_default_helper')) + ->columnSpan(1) + ->disabled(fn ($record) => $record && $record->is_default), + ]), + ]), + ]; + } +} + diff --git a/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php b/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php new file mode 100644 index 0000000..b21de0b --- /dev/null +++ b/app/Filament/Admin/Resources/Languages/Tables/LanguagesTable.php @@ -0,0 +1,113 @@ +columns([ + TextColumn::make('flag') + ->label(__('language.table_flag')) + ->sortable() + ->searchable() + ->formatStateUsing(fn (?string $state): string => + '' . ($state ?? '🌐') . '' + ) + ->html() + ->alignCenter(), + + TextColumn::make('code') + ->label(__('language.table_code')) + ->sortable() + ->searchable() + ->badge() + ->color('info'), + + TextColumn::make('name') + ->label(__('language.table_name')) + ->sortable() + ->searchable() + ->description(fn ($record) => $record->native_name), + + IconColumn::make('is_active') + ->label(__('language.table_is_active')) + ->boolean() + ->sortable(), + + IconColumn::make('is_default') + ->label(__('language.table_is_default')) + ->boolean() + ->sortable(), + + TextColumn::make('direction') + ->label(__('language.table_direction')) + ->badge() + ->color(fn (string $state): string => match ($state) { + 'ltr' => 'success', + 'rtl' => 'warning', + }) + ->sortable(), + + TextColumn::make('sort_order') + ->label(__('language.table_sort_order')) + ->numeric() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('language.table_created_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('language.table_updated_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('deleted_at') + ->label(__('language.table_deleted_at')) + ->dateTime() + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + TrashedFilter::make(), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make(), + RestoreAction::make(), + ForceDeleteAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + ForceDeleteBulkAction::make(), + RestoreBulkAction::make(), + ]), + ]) + ->defaultSort('sort_order') + ->modifyQueryUsing(fn (Builder $query) => $query->withoutGlobalScopes([ + SoftDeletingScope::class, + ])); + } +} + diff --git a/app/Models/Blog.php b/app/Models/Blog.php index d53d08e..adcc672 100644 --- a/app/Models/Blog.php +++ b/app/Models/Blog.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\HasTranslations; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Blog extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, HasTranslations; protected $fillable = [ 'title', @@ -28,6 +29,17 @@ class Blog extends Model 'allow_comments', ]; + /** + * Translatable fields + */ + protected $translatable = [ + 'title', + 'content', + 'excerpt', + 'meta_title', + 'meta_description', + ]; + protected $casts = [ 'published_at' => 'datetime', 'tags' => 'array', diff --git a/app/Models/Language.php b/app/Models/Language.php new file mode 100644 index 0000000..ca3fe3a --- /dev/null +++ b/app/Models/Language.php @@ -0,0 +1,124 @@ + 'boolean', + 'is_default' => 'boolean', + 'sort_order' => 'integer', + ]; + + /** + * Boot method + */ + protected static function boot(): void + { + parent::boot(); + + // Only one default language allowed + static::saving(function ($language) { + if ($language->is_default) { + static::where('id', '!=', $language->id)->update(['is_default' => false]); + } + }); + } + + /** + * Relationships + */ + public function translations(): HasMany + { + return $this->hasMany(Translation::class, 'language_code', 'code'); + } + + public function userLanguages(): HasMany + { + return $this->hasMany(UserLanguage::class, 'language_code', 'code'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + /** + * Scopes + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeOrdered($query) + { + return $query->orderBy('sort_order')->orderBy('name'); + } + + /** + * Helpers + */ + public static function getDefault(): ?self + { + return static::where('is_default', true)->first(); + } + + public static function getActive(): \Illuminate\Database\Eloquent\Collection + { + return static::active()->ordered()->get(); + } + + public static function findByCode(string $code): ?self + { + return static::where('code', $code)->first(); + } + + public function activate(): bool + { + $this->is_active = true; + return $this->save(); + } + + public function deactivate(): bool + { + if ($this->is_default) { + return false; // Cannot deactivate default language + } + $this->is_active = false; + return $this->save(); + } + + public function setAsDefault(): bool + { + $this->is_default = true; + $this->is_active = true; + return $this->save(); + } +} diff --git a/app/Models/Page.php b/app/Models/Page.php index 7823d78..e039d07 100644 --- a/app/Models/Page.php +++ b/app/Models/Page.php @@ -2,13 +2,14 @@ namespace App\Models; +use App\Traits\HasTranslations; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class Page extends Model { - use HasFactory, SoftDeletes; + use HasFactory, SoftDeletes, HasTranslations; protected $fillable = [ 'title', @@ -28,6 +29,17 @@ class Page extends Model 'show_in_menu', ]; + /** + * Translatable fields + */ + protected $translatable = [ + 'title', + 'content', + 'excerpt', + 'meta_title', + 'meta_description', + ]; + protected $casts = [ 'published_at' => 'datetime', 'is_homepage' => 'boolean', diff --git a/app/Models/Translation.php b/app/Models/Translation.php new file mode 100644 index 0000000..b0f4103 --- /dev/null +++ b/app/Models/Translation.php @@ -0,0 +1,201 @@ + 'integer', + 'approved_at' => 'datetime', + 'published_at' => 'datetime', + ]; + + /** + * Relationships + */ + public function translatable(): MorphTo + { + return $this->morphTo(); + } + + public function language(): BelongsTo + { + return $this->belongsTo(Language::class, 'language_code', 'code'); + } + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function updater(): BelongsTo + { + return $this->belongsTo(User::class, 'updated_by'); + } + + public function approver(): BelongsTo + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Scopes + */ + public function scopeForModel($query, string $modelClass, int $modelId) + { + return $query->where('translatable_type', $modelClass) + ->where('translatable_id', $modelId); + } + + public function scopeForLanguage($query, string $languageCode) + { + return $query->where('language_code', $languageCode); + } + + public function scopeForField($query, string $fieldName) + { + return $query->where('field_name', $fieldName); + } + + public function scopePublished($query) + { + return $query->where('status', 'published'); + } + + public function scopeApproved($query) + { + return $query->whereIn('status', ['approved', 'published']); + } + + public function scopeDraft($query) + { + return $query->where('status', 'draft'); + } + + public function scopeReview($query) + { + return $query->where('status', 'review'); + } + + public function scopePendingApproval($query) + { + return $query->whereIn('status', ['draft', 'review']); + } + + /** + * Workflow Methods + */ + public function submitForReview(): bool + { + if ($this->status === 'draft') { + $this->status = 'review'; + return $this->save(); + } + return false; + } + + public function approve(?int $userId = null): bool + { + if (in_array($this->status, ['draft', 'review'])) { + $this->status = 'approved'; + $this->approved_by = $userId ?? auth()->id(); + $this->approved_at = now(); + return $this->save(); + } + return false; + } + + public function publish(?int $userId = null): bool + { + if (in_array($this->status, ['approved', 'draft', 'review'])) { + $this->status = 'published'; + $this->published_at = now(); + if (!$this->approved_by) { + $this->approved_by = $userId ?? auth()->id(); + $this->approved_at = now(); + } + return $this->save(); + } + return false; + } + + public function reject(): bool + { + if ($this->status === 'review') { + $this->status = 'draft'; + return $this->save(); + } + return false; + } + + public function unpublish(): bool + { + if ($this->status === 'published') { + $this->status = 'approved'; + $this->published_at = null; + return $this->save(); + } + return false; + } + + /** + * Helper Methods + */ + public function isDraft(): bool + { + return $this->status === 'draft'; + } + + public function isReview(): bool + { + return $this->status === 'review'; + } + + public function isApproved(): bool + { + return $this->status === 'approved'; + } + + public function isPublished(): bool + { + return $this->status === 'published'; + } + + public function canBeEdited(): bool + { + return in_array($this->status, ['draft', 'review']); + } + + public function canBeApproved(): bool + { + return in_array($this->status, ['draft', 'review']); + } + + public function canBePublished(): bool + { + return in_array($this->status, ['draft', 'review', 'approved']); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index ac2ca92..952ef3a 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Spatie\Permission\Traits\HasRoles; @@ -46,4 +47,128 @@ class User extends Authenticatable 'password' => 'hashed', ]; } + + /** + * Relationships + */ + public function userLanguages(): HasMany + { + return $this->hasMany(UserLanguage::class); + } + + public function languages() + { + return $this->belongsToMany(Language::class, 'user_languages', 'user_id', 'language_code', 'id', 'code') + ->withPivot(['can_create', 'can_edit', 'can_approve', 'can_publish']) + ->withTimestamps(); + } + + /** + * Language Management Methods + */ + public function canManageLanguage(string $languageCode): bool + { + // Super admin can manage all languages + if ($this->hasRole('super_admin')) { + return true; + } + + return $this->userLanguages() + ->where('language_code', $languageCode) + ->where(function ($query) { + $query->where('can_create', true) + ->orWhere('can_edit', true) + ->orWhere('can_approve', true) + ->orWhere('can_publish', true); + }) + ->exists(); + } + + public function canCreateInLanguage(string $languageCode): bool + { + if ($this->hasRole('super_admin')) { + return true; + } + + return $this->userLanguages() + ->where('language_code', $languageCode) + ->where('can_create', true) + ->exists(); + } + + public function canEditInLanguage(string $languageCode): bool + { + if ($this->hasRole('super_admin')) { + return true; + } + + return $this->userLanguages() + ->where('language_code', $languageCode) + ->where('can_edit', true) + ->exists(); + } + + public function canApproveInLanguage(string $languageCode): bool + { + if ($this->hasRole('super_admin')) { + return true; + } + + return $this->userLanguages() + ->where('language_code', $languageCode) + ->where('can_approve', true) + ->exists(); + } + + public function canPublishInLanguage(string $languageCode): bool + { + if ($this->hasRole('super_admin')) { + return true; + } + + return $this->userLanguages() + ->where('language_code', $languageCode) + ->where('can_publish', true) + ->exists(); + } + + public function getManagedLanguages(): \Illuminate\Support\Collection + { + return $this->userLanguages() + ->with('language') + ->get() + ->pluck('language'); + } + + public function getManagedLanguageCodes(): array + { + return $this->userLanguages() + ->pluck('language_code') + ->toArray(); + } + + public function assignLanguage( + string $languageCode, + bool $canCreate = true, + bool $canEdit = true, + bool $canApprove = false, + bool $canPublish = false + ): UserLanguage { + return $this->userLanguages()->updateOrCreate( + ['language_code' => $languageCode], + [ + 'can_create' => $canCreate, + 'can_edit' => $canEdit, + 'can_approve' => $canApprove, + 'can_publish' => $canPublish, + ] + ); + } + + public function removeLanguage(string $languageCode): bool + { + return $this->userLanguages() + ->where('language_code', $languageCode) + ->delete() > 0; + } } diff --git a/app/Models/UserLanguage.php b/app/Models/UserLanguage.php new file mode 100644 index 0000000..99701e1 --- /dev/null +++ b/app/Models/UserLanguage.php @@ -0,0 +1,105 @@ + 'boolean', + 'can_edit' => 'boolean', + 'can_approve' => 'boolean', + 'can_publish' => 'boolean', + ]; + + /** + * Relationships + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function language(): BelongsTo + { + return $this->belongsTo(Language::class, 'language_code', 'code'); + } + + /** + * Scopes + */ + public function scopeForUser($query, int $userId) + { + return $query->where('user_id', $userId); + } + + public function scopeForLanguage($query, string $languageCode) + { + return $query->where('language_code', $languageCode); + } + + public function scopeCanCreate($query) + { + return $query->where('can_create', true); + } + + public function scopeCanEdit($query) + { + return $query->where('can_edit', true); + } + + public function scopeCanApprove($query) + { + return $query->where('can_approve', true); + } + + public function scopeCanPublish($query) + { + return $query->where('can_publish', true); + } + + /** + * Helper Methods + */ + public function hasFullAccess(): bool + { + return $this->can_create && $this->can_edit && $this->can_approve && $this->can_publish; + } + + public function hasNoAccess(): bool + { + return !$this->can_create && !$this->can_edit && !$this->can_approve && !$this->can_publish; + } + + public function grantFullAccess(): bool + { + $this->can_create = true; + $this->can_edit = true; + $this->can_approve = true; + $this->can_publish = true; + return $this->save(); + } + + public function revokeAllAccess(): bool + { + $this->can_create = false; + $this->can_edit = false; + $this->can_approve = false; + $this->can_publish = false; + return $this->save(); + } +} diff --git a/app/Policies/LanguagePolicy.php b/app/Policies/LanguagePolicy.php new file mode 100644 index 0000000..95f50ff --- /dev/null +++ b/app/Policies/LanguagePolicy.php @@ -0,0 +1,97 @@ +hasRole('super_admin') || $user->can('language::viewAny'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Language $language): bool + { + return $user->hasRole('super_admin') || $user->can('language::view'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->hasRole('super_admin') || $user->can('language::create'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Language $language): bool + { + // Cannot disable default language + if ($language->is_default && !$user->hasRole('super_admin')) { + return false; + } + + return $user->hasRole('super_admin') || $user->can('language::update'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Language $language): bool + { + // Cannot delete default language + if ($language->is_default) { + return false; + } + + return $user->hasRole('super_admin') || $user->can('language::delete'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Language $language): bool + { + return $user->hasRole('super_admin') || $user->can('language::restore'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Language $language): bool + { + // Cannot force delete default language + if ($language->is_default) { + return false; + } + + return $user->hasRole('super_admin') || $user->can('language::forceDelete'); + } + + /** + * Determine whether the user can activate the language. + */ + public function activate(User $user): bool + { + return $user->hasRole('super_admin') || $user->can('language::activate'); + } + + /** + * Determine whether the user can set the language as default. + */ + public function setDefault(User $user): bool + { + return $user->hasRole('super_admin') || $user->can('language::setDefault'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 89c03aa..07a76d3 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -2,7 +2,9 @@ namespace App\Providers; +use App\Models\Language; use App\Models\User; +use App\Policies\LanguagePolicy; use App\Policies\UserPolicy; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Spatie\Permission\Models\Role; @@ -18,6 +20,7 @@ class AuthServiceProvider extends ServiceProvider protected $policies = [ User::class => UserPolicy::class, Role::class => RolePolicy::class, + Language::class => LanguagePolicy::class, ]; /** diff --git a/app/Traits/HasTranslations.php b/app/Traits/HasTranslations.php new file mode 100644 index 0000000..0c97b24 --- /dev/null +++ b/app/Traits/HasTranslations.php @@ -0,0 +1,309 @@ +morphMany(Translation::class, 'translatable'); + } + + /** + * Get translation for a specific field and language + */ + public function getTranslation(string $fieldName, ?string $languageCode = null, bool $published = true): ?Translation + { + $languageCode = $languageCode ?? $this->getCurrentLanguageCode(); + + $query = $this->translations() + ->where('field_name', $fieldName) + ->where('language_code', $languageCode); + + if ($published) { + $query->where('status', 'published'); + } + + return $query->first(); + } + + /** + * Get translation value for a specific field and language + */ + public function translate(string $fieldName, ?string $languageCode = null, bool $fallback = true): mixed + { + $languageCode = $languageCode ?? $this->getCurrentLanguageCode(); + + $translation = $this->getTranslation($fieldName, $languageCode); + + if ($translation) { + return $translation->field_value; + } + + // Fallback to default language + if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) { + $defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode()); + if ($defaultTranslation) { + return $defaultTranslation->field_value; + } + } + + // Fallback to original field value + return $this->{$fieldName} ?? null; + } + + /** + * Set translation for a specific field and language + */ + public function setTranslation( + string $fieldName, + string $languageCode, + mixed $value, + string $status = 'draft', + ?int $userId = null + ): Translation { + $userId = $userId ?? auth()->id(); + + return $this->translations()->updateOrCreate( + [ + 'field_name' => $fieldName, + 'language_code' => $languageCode, + ], + [ + 'field_value' => $value, + 'status' => $status, + 'created_by' => $userId, + 'updated_by' => $userId, + ] + ); + } + + /** + * Get all translations for a specific field + */ + public function getTranslations(string $fieldName, bool $published = true): Collection + { + $query = $this->translations() + ->where('field_name', $fieldName); + + if ($published) { + $query->where('status', 'published'); + } + + return $query->get()->keyBy('language_code'); + } + + /** + * Get all translations for a specific language + */ + public function getTranslationsForLanguage(string $languageCode, bool $published = true): Collection + { + $query = $this->translations() + ->where('language_code', $languageCode); + + if ($published) { + $query->where('status', 'published'); + } + + return $query->get()->keyBy('field_name'); + } + + /** + * Check if translation exists for a field and language + */ + public function hasTranslation(string $fieldName, string $languageCode, bool $published = true): bool + { + $query = $this->translations() + ->where('field_name', $fieldName) + ->where('language_code', $languageCode); + + if ($published) { + $query->where('status', 'published'); + } + + return $query->exists(); + } + + /** + * Get translation progress for all languages + */ + public function getTranslationProgress(): array + { + $languages = Language::active()->get(); + $translatableFields = $this->getTranslatableFields(); + $totalFields = count($translatableFields); + + $progress = []; + + foreach ($languages as $language) { + $translatedCount = $this->translations() + ->where('language_code', $language->code) + ->where('status', 'published') + ->whereIn('field_name', $translatableFields) + ->count(); + + $progress[$language->code] = $totalFields > 0 + ? round(($translatedCount / $totalFields) * 100) + : 0; + } + + return $progress; + } + + /** + * Get missing translations for a specific language + */ + public function getMissingTranslations(string $languageCode): array + { + $translatableFields = $this->getTranslatableFields(); + + $existingTranslations = $this->translations() + ->where('language_code', $languageCode) + ->where('status', 'published') + ->pluck('field_name') + ->toArray(); + + return array_diff($translatableFields, $existingTranslations); + } + + /** + * Duplicate translation from one language to another + */ + public function duplicateTranslation(string $fromLanguage, string $toLanguage, ?int $userId = null): int + { + $userId = $userId ?? auth()->id(); + $count = 0; + + $sourceTranslations = $this->getTranslationsForLanguage($fromLanguage, false); + + foreach ($sourceTranslations as $fieldName => $translation) { + $this->setTranslation( + $fieldName, + $toLanguage, + $translation->field_value, + 'draft', + $userId + ); + $count++; + } + + return $count; + } + + /** + * Get translation status for a language + */ + public function getTranslationStatus(string $languageCode): string + { + $progress = $this->getTranslationProgress(); + $percentage = $progress[$languageCode] ?? 0; + + if ($percentage === 100) { + return 'complete'; + } elseif ($percentage > 0) { + return 'partial'; + } else { + return 'missing'; + } + } + + /** + * Sync translations for all translatable fields + */ + public function syncTranslations(array $languageCodes, ?int $userId = null): void + { + $userId = $userId ?? auth()->id(); + $translatableFields = $this->getTranslatableFields(); + + foreach ($languageCodes as $languageCode) { + foreach ($translatableFields as $field) { + if (!$this->hasTranslation($field, $languageCode, false)) { + $this->setTranslation( + $field, + $languageCode, + $this->{$field} ?? '', + 'draft', + $userId + ); + } + } + } + } + + /** + * Get translatable fields for this model + */ + public function getTranslatableFields(): array + { + return $this->translatable ?? []; + } + + /** + * Get current language code + */ + protected function getCurrentLanguageCode(): string + { + return app()->getLocale(); + } + + /** + * Get default language code + */ + protected function getDefaultLanguageCode(): string + { + $defaultLanguage = Language::where('is_default', true)->first(); + return $defaultLanguage ? $defaultLanguage->code : config('app.locale'); + } + + /** + * Scope: Filter by language + */ + public function scopeWhereHasTranslation($query, string $languageCode, bool $published = true) + { + return $query->whereHas('translations', function ($q) use ($languageCode, $published) { + $q->where('language_code', $languageCode); + if ($published) { + $q->where('status', 'published'); + } + }); + } + + /** + * Scope: Filter by user's languages + */ + public function scopeWhereHasTranslationForUser($query, $user) + { + $languageCodes = $user->userLanguages()->pluck('language_code')->toArray(); + + return $query->whereHas('translations', function ($q) use ($languageCodes) { + $q->whereIn('language_code', $languageCodes); + }); + } + + /** + * Get translated model for current language + */ + public function translated(?string $languageCode = null): self + { + $languageCode = $languageCode ?? $this->getCurrentLanguageCode(); + + $translations = $this->getTranslationsForLanguage($languageCode); + + foreach ($translations as $fieldName => $translation) { + if (in_array($fieldName, $this->getTranslatableFields())) { + $this->{$fieldName} = $translation->field_value; + } + } + + return $this; + } +} + diff --git a/database/seeders/LanguageSeeder.php b/database/seeders/LanguageSeeder.php new file mode 100644 index 0000000..24c9b61 --- /dev/null +++ b/database/seeders/LanguageSeeder.php @@ -0,0 +1,78 @@ + 'tr', + 'name' => 'Turkish', + 'native_name' => 'Türkçe', + 'flag' => '🇹🇷', + 'direction' => 'ltr', + 'is_active' => true, + 'is_default' => true, + 'sort_order' => 1, + ], + [ + 'code' => 'en', + 'name' => 'English', + 'native_name' => 'English', + 'flag' => '🇬🇧', + 'direction' => 'ltr', + 'is_active' => true, + 'is_default' => false, + 'sort_order' => 2, + ], + [ + 'code' => 'de', + 'name' => 'German', + 'native_name' => 'Deutsch', + 'flag' => '🇩🇪', + 'direction' => 'ltr', + 'is_active' => false, + 'is_default' => false, + 'sort_order' => 3, + ], + [ + 'code' => 'fr', + 'name' => 'French', + 'native_name' => 'Français', + 'flag' => '🇫🇷', + 'direction' => 'ltr', + 'is_active' => false, + 'is_default' => false, + 'sort_order' => 4, + ], + [ + 'code' => 'ar', + 'name' => 'Arabic', + 'native_name' => 'العربية', + 'flag' => '🇸🇦', + 'direction' => 'rtl', + 'is_active' => false, + 'is_default' => false, + 'sort_order' => 5, + ], + ]; + + foreach ($languages as $language) { + Language::updateOrCreate( + ['code' => $language['code']], + $language + ); + } + + $this->command->info('Languages seeded successfully!'); + } +} diff --git a/docs/TRANSLATION_QUICK_START.md b/docs/TRANSLATION_QUICK_START.md new file mode 100644 index 0000000..941904d --- /dev/null +++ b/docs/TRANSLATION_QUICK_START.md @@ -0,0 +1,352 @@ +# Translation System - Quick Start Guide + +## 🚀 5 Dakikada Başlangıç + +### 1️⃣ Admin Panel'e Git + +``` +http://your-domain.com/admin/languages +``` + +### 2️⃣ Dilleri Aktifleştir + +- 🇹🇷 Türkçe (TR) - Zaten aktif (varsayılan) +- 🇬🇧 İngilizce (EN) - Zaten aktif +- 🇩🇪 Almanca (DE) - İsterseniz aktif edin +- 🇫🇷 Fransızca (FR) - İsterseniz aktif edin + +### 3️⃣ İlk Çeviriyi Ekle + +#### Tinker ile (Terminal): +```bash +php artisan tinker + +# Blog çevirisi ekle +$blog = App\Models\Blog::first(); +$blog->setTranslation('title', 'en', 'My First Blog Post', 'published'); +$blog->setTranslation('content', 'en', 'This is my blog content in English...', 'published'); + +# Kontrol et +echo $blog->translate('title', 'en'); +``` + +#### Kod ile: +```php +use App\Models\Blog; + +$blog = Blog::find(1); + +// İngilizce çeviri ekle +$blog->setTranslation('title', 'en', 'My Blog Title', 'published'); +$blog->setTranslation('content', 'en', 'Blog content here...', 'published'); +$blog->setTranslation('excerpt', 'en', 'Short description', 'published'); + +// Oku +echo $blog->translate('title'); // Mevcut dilde +echo $blog->translate('title', 'en'); // İngilizce +echo $blog->translate('title', 'tr'); // Türkçe +``` + +### 4️⃣ Translation Progress Kontrol Et + +```php +$blog = Blog::find(1); + +// İlerleme yüzdeleri +$progress = $blog->getTranslationProgress(); +print_r($progress); +// ['tr' => 100, 'en' => 60, 'de' => 0] + +// Eksik çeviriler +$missing = $blog->getMissingTranslations('en'); +print_r($missing); +// ['meta_title', 'meta_description'] + +// Durum +echo $blog->getTranslationStatus('en'); // 'partial' +``` + +### 5️⃣ Kullanıcıya Dil Yetkisi Ver + +```php +use App\Models\User; + +$user = User::find(1); + +// Türkçe içerik yöneticisi +$user->assignLanguage('tr', + canCreate: true, + canEdit: true, + canApprove: true, + canPublish: true +); + +// İngilizce çevirmen (yayınlayamaz) +$user->assignLanguage('en', + canCreate: true, + canEdit: true, + canApprove: false, + canPublish: false +); + +// Kontrol +if ($user->canManageLanguage('tr')) { + echo "User can manage Turkish!"; +} +``` + +## 💡 Yaygın Kullanım Örnekleri + +### Örnek 1: Blog Oluştur ve Çevir + +```php +// 1. Türkçe blog oluştur +$blog = Blog::create([ + 'title' => 'Laravel Eğitimi', + 'slug' => 'laravel-egitimi', + 'content' => 'Laravel hakkında detaylı bilgi...', + 'excerpt' => 'Laravel nedir?', + 'status' => 'published', + 'author_id' => 1, +]); + +// 2. İngilizce çevirisi ekle +$blog->setTranslation('title', 'en', 'Laravel Tutorial', 'published'); +$blog->setTranslation('content', 'en', 'Detailed info about Laravel...', 'published'); +$blog->setTranslation('excerpt', 'en', 'What is Laravel?', 'published'); + +// 3. Almanca çevirisi ekle (draft olarak) +$blog->setTranslation('title', 'de', 'Laravel Tutorial', 'draft'); +$blog->setTranslation('content', 'de', 'Detaillierte Informationen über Laravel...', 'draft'); + +// 4. Çevirileri kullan +echo $blog->translate('title'); // Mevcut dilde +echo $blog->translate('title', 'en'); // İngilizce +echo $blog->translate('title', 'de'); // Almanca (draft, yayında değil) +``` + +### Örnek 2: Translation Workflow + +```php +$blog = Blog::find(1); + +// Çeviri oluştur (draft olarak başlar) +$translation = $blog->setTranslation('title', 'en', 'My Title', 'draft'); + +// Workflow adımları +$translation->submitForReview(); // Draft → Review +$translation->approve(auth()->id()); // Review → Approved +$translation->publish(auth()->id()); // Approved → Published + +// Veya direkt yayınla +$translation->publish(); // Draft/Review/Approved → Published + +// Geri al +$translation->unpublish(); // Published → Approved +$translation->reject(); // Review → Draft +``` + +### Örnek 3: Yeni Model Ekle + +```php +// 1. Model oluştur +class Product extends Model +{ + use HasTranslations; + + protected $fillable = [ + 'name', 'slug', 'description', 'price', 'sku' + ]; + + protected $translatable = [ + 'name', + 'description', + ]; +} + +// 2. Migration +Schema::create('products', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description'); + $table->decimal('price', 10, 2); + $table->string('sku')->unique(); + $table->timestamps(); +}); + +// 3. Kullan +$product = Product::create([ + 'name' => 'Laptop', + 'slug' => 'laptop', + 'description' => 'Yüksek performanslı laptop', + 'price' => 15000, + 'sku' => 'LAP-001' +]); + +$product->setTranslation('name', 'en', 'Laptop', 'published'); +$product->setTranslation('description', 'en', 'High performance laptop', 'published'); + +// Artık Product'da çeviri var! 🎉 +echo $product->translate('name', 'en'); // "Laptop" +``` + +### Örnek 4: Tüm Çevirileri Getir + +```php +$blog = Blog::find(1); + +// Bir alanın tüm dillerdeki çevirileri +$titleTranslations = $blog->getTranslations('title'); +foreach ($titleTranslations as $langCode => $translation) { + echo "{$langCode}: {$translation->field_value}\n"; +} +// tr: Laravel Eğitimi +// en: Laravel Tutorial +// de: Laravel Tutorial + +// Bir dilin tüm alan çevirileri +$enTranslations = $blog->getTranslationsForLanguage('en'); +foreach ($enTranslations as $fieldName => $translation) { + echo "{$fieldName}: {$translation->field_value}\n"; +} +// title: Laravel Tutorial +// content: Detailed info about Laravel... +// excerpt: What is Laravel? +``` + +### Örnek 5: Helper Functions + +```php +// Mevcut dil +$currentLang = current_language(); // Language model +$currentCode = current_language_code(); // 'tr' + +// Varsayılan dil +$defaultLang = default_language(); +$defaultCode = default_language_code(); // 'tr' + +// Aktif diller +$languages = available_languages(); // Collection [TR, EN] +$codes = available_language_codes(); // ['tr', 'en'] + +// Dil değiştir +switch_language('en'); +app()->setLocale('en'); + +// Model çevirisi +$title = translate_model($blog, 'title', 'en'); + +// Çeviri var mı? +if (has_translation($blog, 'title', 'en')) { + echo "English translation exists!"; +} + +// İlerleme +$progress = translation_progress($blog, 'en'); // 75 + +// Kullanıcı dilleri +$userLanguages = user_managed_languages(); // Collection +$userCodes = user_managed_language_codes(); // ['tr', 'en'] + +// Dil bilgileri +echo get_language_flag('tr'); // 🇹🇷 +echo format_language_name('tr'); // Turkish (Türkçe) +``` + +## 🎯 Sık Sorulan Sorular + +### 1. Varsayılan dilde çeviri lazım mı? +Hayır! Varsayılan dil (TR) için veritabanındaki orijinal değer kullanılır. Sadece diğer diller için çeviri eklemeniz yeterli. + +### 2. Çeviri yoksa ne olur? +Fallback mekanizması devreye girer: +1. İstenen dilde ara +2. Yoksa varsayılan dilde ara +3. Yoksa model'deki orijinal değeri kullan + +### 3. Draft çeviriler görünür mü? +Hayır, sadece `published` çeviriler görünür. Ama parametre ile draft'ları da alabilirsiniz: +```php +$translation = $blog->getTranslation('title', 'en', published: false); +``` + +### 4. Toplu çeviri nasıl yapılır? +```php +// TR'den EN'e tüm alanları kopyala +$blog->duplicateTranslation('tr', 'en'); + +// Tüm diller için placeholder oluştur +$blog->syncTranslations(['tr', 'en', 'de']); +``` + +### 5. Performans nasıl? +- ✅ Indexed queries +- ✅ Eager loading desteği +- ✅ Tek query ile tüm çevirileri çek +- ✅ Cache-ready yapı + +## 📊 Dashboard Özet Örneği + +```php +// Admin dashboard widget örneği +$stats = [ + 'total_languages' => Language::active()->count(), + 'total_translations' => Translation::published()->count(), + 'pending_approvals' => Translation::review()->count(), + 'translation_progress' => [] +]; + +foreach (Language::active()->get() as $language) { + $stats['translation_progress'][$language->code] = [ + 'name' => $language->name, + 'flag' => $language->flag, + 'count' => Translation::forLanguage($language->code)->published()->count(), + ]; +} +``` + +## 🎨 Blade Template Kullanımı + +```blade +{{-- Mevcut dilde --}} +

{{ $blog->translate('title') }}

+ +{{-- Belirli dilde --}} +

{{ $blog->translate('title', 'en') }}

+ +{{-- Helper ile --}} +

{{ translate_model($blog, 'title') }}

+ +{{-- Dil seçici --}} +@foreach(available_languages() as $language) + + {{ $language->flag }} {{ $language->name }} + +@endforeach + +{{-- Progress bar --}} +@php + $progress = $blog->getTranslationProgress(); +@endphp + +@foreach($progress as $code => $percentage) +
+ {{ get_language_flag($code) }} +
{{ $percentage }}%
+
+@endforeach +``` + +## 🔥 Pro Tips + +1. **Her zaman published status kullanın** (son kullanıcı için) +2. **Draft status kullanın** (henüz tamamlanmamış çeviriler için) +3. **Translation progress ile takip edin** (hangi diller eksik?) +4. **User-language permissions ile kontrol edin** (kim ne yapabilir?) +5. **Fallback mekanizmasını kullanın** (eksik çeviriler için) + +--- + +Hazırsınız! 🚀 Artık universal translation sisteminiz tamamen çalışıyor! + diff --git a/docs/TRANSLATION_SYSTEM.md b/docs/TRANSLATION_SYSTEM.md new file mode 100644 index 0000000..d8bb42c --- /dev/null +++ b/docs/TRANSLATION_SYSTEM.md @@ -0,0 +1,431 @@ +# Universal Dynamic Translation System + +## 🌟 Genel Bakış + +Citrus Platform için geliştirilmiş **evrensel, dinamik ve role-based** çeviri sistemi. Bu sistem, herhangi bir modelin çeviri desteğine sahip olmasını sağlar ve kullanıcı bazlı dil yönetimi sunar. + +## 📋 Sistem Mimarisi + +``` +┌─────────────────┐ +│ languages │ → Dinamik dil tanımları (TR, EN, DE, FR, AR...) +└─────────────────┘ + ↓ +┌─────────────────┐ +│ translations │ → Evrensel çeviri tablosu (polymorphic) +└─────────────────┘ + ↓ +┌─────────────────┐ +│ user_languages │ → Kullanıcı-Dil ilişkisi (yetkiler) +└─────────────────┘ + ↓ +┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ +│ pages │ │ blogs │ │products │ │ news │ → Herhangi bir model +└─────────┘ └─────────┘ └─────────┘ └──────────┘ +``` + +## ✅ Kurulum ve Yapılandırma + +### 1. Database Tabloları + +```bash +php artisan migrate +``` + +Oluşturulan tablolar: +- `languages` - Dil tanımları +- `translations` - Çeviri veriler (polymorphic) +- `user_languages` - Kullanıcı-dil yetkileri + +### 2. Varsayılan Diller + +```bash +php artisan db:seed --class=LanguageSeeder +``` + +Eklenen diller: +- 🇹🇷 Türkçe (tr) - Varsayılan, Aktif +- 🇬🇧 İngilizce (en) - Aktif +- 🇩🇪 Almanca (de) - Pasif +- 🇫🇷 Fransızca (fr) - Pasif +- 🇸🇦 Arapça (ar) - Pasif, RTL + +### 3. Model Entegrasyonu + +Herhangi bir modele çeviri desteği eklemek için: + +```php +use App\Traits\HasTranslations; + +class YourModel extends Model +{ + use HasTranslations; + + protected $translatable = [ + 'title', + 'description', + 'content', + ]; +} +``` + +## 🎯 Kullanım + +### Çeviri Kaydetme + +```php +// Basit çeviri kaydetme +$blog->setTranslation('title', 'en', 'My Blog Post'); + +// Status ile kaydetme +$blog->setTranslation('title', 'en', 'My Blog Post', 'published'); + +// Kullanıcı ID ile +$blog->setTranslation('title', 'en', 'My Blog Post', 'draft', auth()->id()); +``` + +### Çeviri Okuma + +```php +// Mevcut dilde +$title = $blog->translate('title'); + +// Belirli bir dilde +$title = $blog->translate('title', 'en'); + +// Fallback olmadan +$title = $blog->translate('title', 'en', false); +``` + +### Toplu İşlemler + +```php +// Tüm çevirileri al (bir alan için) +$translations = $blog->getTranslations('title'); +// ['tr' => 'Blog Yazım', 'en' => 'My Blog Post'] + +// Bir dildeki tüm çevirileri al +$translations = $blog->getTranslationsForLanguage('en'); +// ['title' => 'My Blog Post', 'content' => '...'] + +// Çeviri var mı kontrol et +if ($blog->hasTranslation('title', 'en')) { + // ... +} +``` + +### Translation Progress + +```php +// Tüm diller için ilerleme +$progress = $blog->getTranslationProgress(); +// ['tr' => 100, 'en' => 75, 'de' => 0] + +// Eksik çevirileri bul +$missing = $blog->getMissingTranslations('en'); +// ['meta_description', 'excerpt'] + +// Çeviri durumu +$status = $blog->getTranslationStatus('en'); +// 'complete', 'partial', 'missing' +``` + +### Translation Workflow + +```php +// Çeviri oluştur (draft) +$translation = $blog->setTranslation('title', 'en', 'My Title', 'draft'); + +// Onaya gönder +$translation->submitForReview(); + +// Onayla +$translation->approve(auth()->id()); + +// Yayınla +$translation->publish(auth()->id()); + +// Reddet +$translation->reject(); +``` + +### Dil Değiştirme + +```php +// Runtime'da dil değiştir +switch_language('en'); + +// Uygulama dili +app()->setLocale('tr'); +``` + +## 🔐 Kullanıcı Yetkilendirme + +### Kullanıcıya Dil Atama + +```php +// Tam yetki ile dil ata +$user->assignLanguage('tr', + canCreate: true, + canEdit: true, + canApprove: true, + canPublish: true +); + +// Sadece oluşturma ve düzenleme yetkisi +$user->assignLanguage('en', + canCreate: true, + canEdit: true, + canApprove: false, + canPublish: false +); +``` + +### Yetki Kontrolleri + +```php +// Kullanıcı bu dili yönetebilir mi? +if ($user->canManageLanguage('en')) { + // ... +} + +// Specific permissions +if ($user->canCreateInLanguage('en')) { } +if ($user->canEditInLanguage('en')) { } +if ($user->canApproveInLanguage('en')) { } +if ($user->canPublishInLanguage('en')) { } + +// Kullanıcının yönettiği diller +$languages = $user->getManagedLanguages(); +$codes = $user->getManagedLanguageCodes(); // ['tr', 'en'] +``` + +### Query Filtering + +```php +// Sadece belirli dildeki içerikleri getir +Blog::whereHasTranslation('en')->get(); + +// Kullanıcının yönettiği dillerdeki içerikleri getir +Blog::whereHasTranslationForUser(auth()->user())->get(); +``` + +## 🛠️ Helper Functions + +```php +// Mevcut dil +$language = current_language(); // Language model +$code = current_language_code(); // 'tr' + +// Varsayılan dil +$defaultLang = default_language(); +$defaultCode = default_language_code(); + +// Aktif diller +$languages = available_languages(); // Collection +$codes = available_language_codes(); // ['tr', 'en'] + +// Model çevirisi +$value = translate_model($blog, 'title', 'en'); + +// Çeviri kontrolü +$exists = has_translation($blog, 'title', 'en'); + +// İlerleme +$progress = translation_progress($blog, 'en'); // 75 + +// Kullanıcı yetkileri +$can = user_can_manage_language('en'); +$languages = user_managed_languages(); +$codes = user_managed_language_codes(); + +// Dil bilgileri +$flag = get_language_flag('tr'); // 🇹🇷 +$name = format_language_name('tr'); // Turkish (Türkçe) + +// Status +$badge = get_translation_status_badge('published'); +$color = get_translation_status_color('draft'); +``` + +## 📊 Filament Admin Panel + +### Language Management + +Admin panelde **Languages** resource'u üzerinden: +- ✅ Dil ekleme/düzenleme/silme +- ✅ Dil aktif/pasif yapma +- ✅ Varsayılan dil seçme +- ✅ Sıralama düzenleme +- ✅ RTL/LTR desteği + +### Permissions + +Spatie Permission ile entegre: +- `language::viewAny` +- `language::view` +- `language::create` +- `language::update` +- `language::delete` +- `language::restore` +- `language::forceDelete` +- `language::activate` +- `language::setDefault` + +### Policy Kuralları + +- Varsayılan dil silinemez +- Varsayılan dil pasif edilemez +- Super admin tüm yetkilere sahip +- Diğer kullanıcılar permission'lara göre erişir + +## 🎨 Translation Workflow Statuses + +| Status | Açıklama | Badge Color | +|--------|----------|-------------| +| `draft` | Taslak, henüz onaylanmamış | Gray | +| `review` | Onay bekliyor | Warning | +| `approved` | Onaylanmış, yayınlanmayı bekliyor | Success | +| `published` | Yayınlanmış, görünür | Primary | + +## 📦 Database Schema + +### languages + +```sql +id, code (unique), name, native_name, flag, direction, +is_active, is_default, sort_order, +created_by, updated_by, created_at, updated_at, deleted_at +``` + +### translations + +```sql +id, translatable_type, translatable_id, language_code, +field_name, field_value, status, version, +created_by, updated_by, approved_by, approved_at, published_at, +created_at, updated_at, deleted_at + +UNIQUE: translatable_type + translatable_id + language_code + field_name +``` + +### user_languages + +```sql +id, user_id, language_code, +can_create, can_edit, can_approve, can_publish, +created_at, updated_at + +UNIQUE: user_id + language_code +``` + +## 🚀 Örnekler + +### Yeni Model için Çeviri Ekle + +```php +// 1. Model oluştur +class Product extends Model +{ + use HasTranslations; + + protected $translatable = ['name', 'description', 'features']; +} + +// 2. Çeviri ekle +$product = Product::create(['name' => 'Ürün', 'slug' => 'urun']); +$product->setTranslation('name', 'en', 'Product', 'published'); +$product->setTranslation('description', 'en', 'Product description', 'published'); + +// 3. Kullan +echo $product->translate('name'); // Mevcut dilde +echo $product->translate('name', 'en'); // İngilizce'de +``` + +### Çeviri İlerlemesi Widget + +```php +$progress = $blog->getTranslationProgress(); +foreach ($progress as $langCode => $percentage) { + $language = Language::findByCode($langCode); + echo "{$language->flag} {$language->name}: {$percentage}%"; +} +``` + +### Kullanıcı Dil Yönetimi + +```php +// Turkish Editor oluştur +$user = User::find(1); +$user->assignLanguage('tr', true, true, false, true); + +// English Translator oluştur +$translator = User::find(2); +$translator->assignLanguage('en', true, true, false, false); + +// Multi-language Manager +$manager = User::find(3); +$manager->assignLanguage('tr', true, true, true, true); +$manager->assignLanguage('en', true, true, true, true); +``` + +## ⚡ Performans + +- ✅ Indexed queries (translatable_type, translatable_id, language_code) +- ✅ Eager loading desteği +- ✅ Cache-ready yapı +- ✅ Efficient polymorphic relationships +- ✅ Unique constraints + +## 🔄 Migration + +Mevcut projeden yeni sisteme geçiş için: + +```php +// Eski sistem +$blog->title_tr +$blog->title_en + +// Yeni sisteme migrate +$blog->setTranslation('title', 'tr', $blog->title_tr, 'published'); +$blog->setTranslation('title', 'en', $blog->title_en, 'published'); +``` + +## 📝 Notlar + +1. **Zero Configuration**: Yeni model için sadece trait ekle +2. **Dynamic**: Runtime'da dil ekle/çıkar +3. **Role-Based**: Her kullanıcı farklı dilleri yönetebilir +4. **Workflow**: Draft → Review → Approved → Published +5. **Fallback**: Çeviri yoksa default dile düş +6. **Version Control**: Her çevirinin versiyonu +7. **Soft Delete**: Tüm tablolar soft delete destekli + +## 🎯 Gelecek Özellikler (TODO) + +- [ ] Translation approval notifications +- [ ] Bulk translation import/export +- [ ] Translation memory (önceki çevirilerden öner) +- [ ] AI-powered translation suggestions +- [ ] Translation dashboard widget +- [ ] Language-specific user roles +- [ ] Translation history/audit log +- [ ] Compare translations side-by-side +- [ ] Missing translations report + +## 📚 İlgili Dosyalar + +- Models: `app/Models/Language.php`, `Translation.php`, `UserLanguage.php` +- Trait: `app/Traits/HasTranslations.php` +- Helpers: `app/Helpers/translation_helpers.php` +- Resource: `app/Filament/Admin/Resources/Languages/` +- Policy: `app/Policies/LanguagePolicy.php` +- Migrations: `database/migrations/*_create_languages_table.php` +- Translations: `lang/tr/language.php`, `lang/en/language.php` + +--- + +**Geliştirici:** Citrus Platform Team +**Versiyon:** 1.0.0 +**Tarih:** 1 Ekim 2025 + diff --git a/lang/en/language.php b/lang/en/language.php new file mode 100644 index 0000000..d152b74 --- /dev/null +++ b/lang/en/language.php @@ -0,0 +1,75 @@ + 'Languages', + 'navigation_label' => 'Languages', + 'model_label' => 'Language', + 'plural_model_label' => 'Languages', + + // Actions + 'create' => 'Create Language', + 'edit' => 'Edit Language', + 'delete' => 'Delete Language', + 'restore' => 'Restore', + 'force_delete' => 'Force Delete', + 'activate' => 'Activate', + 'deactivate' => 'Deactivate', + 'set_default' => 'Set as Default', + + // Form Sections + 'basic_information' => 'Basic Information', + 'settings' => 'Settings', + + // Form Fields + 'code' => 'Language Code', + 'code_helper' => 'ISO 639-1 language code (e.g: tr, en, de)', + 'name' => 'Language Name', + 'name_helper' => 'English language name (e.g: Turkish)', + 'native_name' => 'Native Name', + 'native_name_helper' => 'Language name in its own language (e.g: Türkçe)', + 'flag' => 'Flag', + 'flag_helper' => 'Flag emoji or icon code', + 'direction' => 'Text Direction', + 'direction_ltr' => 'Left to Right (LTR)', + 'direction_rtl' => 'Right to Left (RTL)', + 'is_active' => 'Active', + 'is_active_helper' => 'Is this language available in the system?', + 'is_default' => 'Default Language', + 'is_default_helper' => 'Should this be the default language?', + 'sort_order' => 'Sort Order', + 'sort_order_helper' => 'Display order of languages', + + // Table Columns + 'table_flag' => 'Flag', + 'table_code' => 'Code', + 'table_name' => 'Language Name', + 'table_is_active' => 'Active', + 'table_is_default' => 'Default', + 'table_direction' => 'Direction', + 'table_sort_order' => 'Order', + 'table_created_at' => 'Created', + 'table_updated_at' => 'Updated', + 'table_deleted_at' => 'Deleted', + + // Messages + 'created_successfully' => 'Language created successfully', + 'created_successfully_body' => 'New language has been added to the system.', + 'updated_successfully' => 'Language updated successfully', + 'updated_successfully_body' => 'Language information has been updated.', + 'deleted_successfully' => 'Language deleted successfully', + 'deleted_successfully_body' => 'Language has been removed from the system.', + 'restored_successfully' => 'Language restored successfully', + 'activated_successfully' => 'Language activated', + 'deactivated_successfully' => 'Language deactivated', + 'set_default_successfully' => 'Default language updated', + 'cannot_delete_default' => 'Cannot delete default language', + 'cannot_deactivate_default' => 'Cannot deactivate default language', + + // Validation + 'code_required' => 'Language code is required', + 'code_unique' => 'This language code is already in use', + 'name_required' => 'Language name is required', + 'native_name_required' => 'Native name is required', +]; + diff --git a/lang/tr/language.php b/lang/tr/language.php new file mode 100644 index 0000000..debca43 --- /dev/null +++ b/lang/tr/language.php @@ -0,0 +1,75 @@ + 'Diller', + 'navigation_label' => 'Diller', + 'model_label' => 'Dil', + 'plural_model_label' => 'Diller', + + // Actions + 'create' => 'Dil Oluştur', + 'edit' => 'Dil Düzenle', + 'delete' => 'Dil Sil', + 'restore' => 'Geri Yükle', + 'force_delete' => 'Kalıcı Olarak Sil', + 'activate' => 'Aktif Et', + 'deactivate' => 'Pasif Et', + 'set_default' => 'Varsayılan Yap', + + // Form Sections + 'basic_information' => 'Temel Bilgiler', + 'settings' => 'Ayarlar', + + // Form Fields + 'code' => 'Dil Kodu', + 'code_helper' => 'ISO 639-1 dil kodu (örn: tr, en, de)', + 'name' => 'Dil Adı', + 'name_helper' => 'İngilizce dil adı (örn: Turkish)', + 'native_name' => 'Yerel Dil Adı', + 'native_name_helper' => 'Dil adının kendi dilindeki yazılışı (örn: Türkçe)', + 'flag' => 'Bayrak', + 'flag_helper' => 'Bayrak emojisi veya icon kodu', + 'direction' => 'Metin Yönü', + 'direction_ltr' => 'Soldan Sağa (LTR)', + 'direction_rtl' => 'Sağdan Sola (RTL)', + 'is_active' => 'Aktif', + 'is_active_helper' => 'Dil sistemde kullanılabilir mi?', + 'is_default' => 'Varsayılan Dil', + 'is_default_helper' => 'Bu dil varsayılan dil olsun mu?', + 'sort_order' => 'Sıralama', + 'sort_order_helper' => 'Dillerin görüntülenme sırası', + + // Table Columns + 'table_flag' => 'Bayrak', + 'table_code' => 'Kod', + 'table_name' => 'Dil Adı', + 'table_is_active' => 'Aktif', + 'table_is_default' => 'Varsayılan', + 'table_direction' => 'Yön', + 'table_sort_order' => 'Sıra', + 'table_created_at' => 'Oluşturulma', + 'table_updated_at' => 'Güncellenme', + 'table_deleted_at' => 'Silinme', + + // Messages + 'created_successfully' => 'Dil başarıyla oluşturuldu', + 'created_successfully_body' => 'Yeni dil sisteme eklendi.', + 'updated_successfully' => 'Dil başarıyla güncellendi', + 'updated_successfully_body' => 'Dil bilgileri güncellendi.', + 'deleted_successfully' => 'Dil başarıyla silindi', + 'deleted_successfully_body' => 'Dil sistemden kaldırıldı.', + 'restored_successfully' => 'Dil başarıyla geri yüklendi', + 'activated_successfully' => 'Dil aktif edildi', + 'deactivated_successfully' => 'Dil pasif edildi', + 'set_default_successfully' => 'Varsayılan dil güncellendi', + 'cannot_delete_default' => 'Varsayılan dil silinemez', + 'cannot_deactivate_default' => 'Varsayılan dil pasif edilemez', + + // Validation + 'code_required' => 'Dil kodu zorunludur', + 'code_unique' => 'Bu dil kodu zaten kullanılıyor', + 'name_required' => 'Dil adı zorunludur', + 'native_name_required' => 'Yerel dil adı zorunludur', +]; + diff --git a/resources/css/citrus.css b/resources/css/citrus.css index 1ac5e99..17f6ae9 100644 --- a/resources/css/citrus.css +++ b/resources/css/citrus.css @@ -3,8 +3,9 @@ height: 6rem !important; width: auto !important; } - - +.fi-topbar-start .fi-logo { + height: 2.5rem !important; +} /* Login sayfası brand logo boyutu */ .fi-brand-logo { height: 6rem !important; From 9cc36297ea46fb102ddb2ba63709b2294d4eb1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:43:27 -0300 Subject: [PATCH 06/15] Implement Translation Tabs Component: Added TranslationTabs component for managing translations in Blog and Page resources. Integrated translation fields into forms, ensuring localization support for titles, content, and metadata. Updated existing pages to save and load translations, enhancing the universal translation system. Added relevant localization keys for English and Turkish. --- .cursorrules | 9 +- .../Resources/Blogs/Pages/CreateBlog.php | 7 + .../Admin/Resources/Blogs/Pages/EditBlog.php | 25 ++ .../Resources/Blogs/Schemas/BlogForm.php | 42 ++++ .../Resources/Components/TranslationTabs.php | 217 ++++++++++++++++++ .../Resources/Pages/Pages/CreatePage.php | 7 + .../Admin/Resources/Pages/Pages/EditPage.php | 15 ++ .../Resources/Pages/Schemas/PageForm.php | 42 ++++ lang/en/blog.php | 1 + lang/en/pages.php | 1 + lang/tr/blog.php | 1 + lang/tr/pages.php | 1 + 12 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 app/Filament/Admin/Resources/Components/TranslationTabs.php diff --git a/.cursorrules b/.cursorrules index 948defd..2c586b7 100644 --- a/.cursorrules +++ b/.cursorrules @@ -101,8 +101,12 @@ use Filament\Forms\Components\FileUpload; // Layout components (ÖNEMLİ: Schemas namespace!) use Filament\Schemas\Components\Section; // ✅ Doğru use Filament\Schemas\Components\Grid; // ✅ Doğru -use Filament\Schemas\Components\Tabs; // ✅ Doğru use Filament\Schemas\Components\Fieldset; // ✅ Doğru + +// Tabs components (ÖNEMLİ: Forms namespace!) +use Filament\Forms\Components\Tabs; // ✅ Doğru +// Tab kullanımı: Tabs\Tab::make() şeklinde kullan +// Import gerekmez, Tabs namespace'i üzerinden erişilir ``` #### **YANLIŞ KULLANIM (Eski Filament 3.x):** @@ -127,7 +131,6 @@ $table - ✅ **Table Methods:** `->recordActions()` ve `->toolbarActions()` ### Dil Dosyası Yapısı -```php 'Bu alan zorunludur.', 'field_unique' => 'Bu değer zaten kullanılıyor.', ]; -``` + ### Model Kuralları - `$fillable` array'ini tanımla diff --git a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php index fbd5dad..b0c3f01 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/CreateBlog.php @@ -3,6 +3,7 @@ namespace App\Filament\Admin\Resources\Blogs\Pages; use App\Filament\Admin\Resources\Blogs\BlogResource; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Resources\Pages\CreateRecord; class CreateBlog extends CreateRecord @@ -23,4 +24,10 @@ class CreateBlog extends CreateRecord { return __('blog.created_successfully'); } + + protected function afterCreate(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php index af1a630..495d912 100644 --- a/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php +++ b/app/Filament/Admin/Resources/Blogs/Pages/EditBlog.php @@ -3,6 +3,7 @@ namespace App\Filament\Admin\Resources\Blogs\Pages; use App\Filament\Admin\Resources\Blogs\BlogResource; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Actions\DeleteAction; use Filament\Actions\ForceDeleteAction; use Filament\Actions\RestoreAction; @@ -38,4 +39,28 @@ class EditBlog extends EditRecord { return __('blog.updated_successfully'); } + + protected function mutateFormDataBeforeFill(array $data): array + { + // Load existing translations + $translationData = TranslationTabs::fillFromRecord($this->record); + + \Log::info('Loading translations for edit', [ + 'blog_id' => $this->record->id, + 'translations' => $translationData + ]); + + return array_merge($data, $translationData); + } + + protected function afterSave(): void + { + \Log::info('Saving blog translations', [ + 'blog_id' => $this->record->id, + 'form_state' => $this->form->getState() + ]); + + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php index 13c0a6c..15347c5 100644 --- a/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php +++ b/app/Filament/Admin/Resources/Blogs/Schemas/BlogForm.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Blogs\Schemas; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\FileUpload; @@ -149,6 +150,47 @@ class BlogForm ->columnSpanFull() ->collapsible(true) ->collapsed(true), + + // Çeviri Sekmesi + Section::make('🌍 ' . __('blog.translations_section')) + ->schema([ + TranslationTabs::make([ + 'title' => [ + 'type' => 'text', + 'label' => __('blog.title_field'), + 'required' => false, + 'maxLength' => 255, + ], + 'content' => [ + 'type' => 'richtext', + 'label' => __('blog.content_field'), + 'required' => false, + ], + 'excerpt' => [ + 'type' => 'textarea', + 'label' => __('blog.excerpt_field'), + 'required' => false, + 'maxLength' => 500, + 'rows' => 3, + ], + 'meta_title' => [ + 'type' => 'text', + 'label' => __('blog.meta_title_field'), + 'required' => false, + 'maxLength' => 60, + ], + 'meta_description' => [ + 'type' => 'textarea', + 'label' => __('blog.meta_description_field'), + 'required' => false, + 'maxLength' => 160, + 'rows' => 3, + ], + ]), + ]) + ->columnSpanFull() + ->collapsible(true) + ->collapsed(false), ]); } } diff --git a/app/Filament/Admin/Resources/Components/TranslationTabs.php b/app/Filament/Admin/Resources/Components/TranslationTabs.php new file mode 100644 index 0000000..b0c61c9 --- /dev/null +++ b/app/Filament/Admin/Resources/Components/TranslationTabs.php @@ -0,0 +1,217 @@ +ordered()->get(); + + if ($languages->isEmpty()) { + return Tabs::make('translations'); + } + + $tabs = []; + + foreach ($languages as $language) { + // User permission kontrolü + $canManage = auth()->user()?->canManageLanguage($language->code) ?? true; + + if (!$canManage) { + continue; + } + + $tabFields = []; + + foreach ($fields as $fieldName => $fieldConfig) { + $fieldType = $fieldConfig['type'] ?? 'text'; + $label = $fieldConfig['label'] ?? ucfirst($fieldName); + $required = $fieldConfig['required'] ?? false; + $maxLength = $fieldConfig['maxLength'] ?? null; + $rows = $fieldConfig['rows'] ?? 3; + + $fieldKey = "translations.{$language->code}.{$fieldName}"; + + switch ($fieldType) { + case 'richtext': + $field = RichEditor::make($fieldKey) + ->label($label) + ->fileAttachmentsDisk('public') + ->fileAttachmentsDirectory('translations') + ->fileAttachmentsVisibility('public') + ->toolbarButtons([ + 'attachFiles', + 'blockquote', + 'bold', + 'bulletList', + 'codeBlock', + 'h2', + 'h3', + 'italic', + 'link', + 'orderedList', + 'redo', + 'strike', + 'underline', + 'undo', + ]) + ->columnSpanFull() + ->required($required); + break; + + case 'textarea': + $field = Textarea::make($fieldKey) + ->label($label) + ->rows($rows) + ->columnSpanFull(); + break; + + case 'text': + default: + $field = TextInput::make($fieldKey) + ->label($label); + break; + } + + if ($required) { + $field->required(); + } + + if ($maxLength) { + $field->maxLength($maxLength); + } + + $tabFields[] = $field; + } + + // Convert country code to flag emoji + $code = strtoupper($language->code); + $flag = ''; + for ($i = 0; $i < strlen($code); $i++) { + $flag .= mb_chr(127397 + ord($code[$i])); + } + + $tabs[] = Tab::make($language->name) + ->label($flag . ' ' . $language->native_name) + ->schema($tabFields) + ->badge(fn ($record) => $record + ? self::getTranslationBadge($record, $language->code) + : null + ) + ->badgeColor(fn ($record) => $record + ? self::getTranslationBadgeColor($record, $language->code) + : 'gray' + ); + } + + return Tabs::make('translations') + ->tabs($tabs) + ->columnSpanFull() + ->persistTabInQueryString(); + } + + protected static function getTranslationBadge($record, string $languageCode): ?string + { + if (!method_exists($record, 'getTranslationProgress')) { + return null; + } + + $progress = $record->getTranslationProgress(); + $percentage = $progress[$languageCode] ?? 0; + + if ($percentage === 100) { + return '✓'; + } elseif ($percentage > 0) { + return $percentage . '%'; + } + + return null; + } + + protected static function getTranslationBadgeColor($record, string $languageCode): string + { + if (!method_exists($record, 'getTranslationProgress')) { + return 'gray'; + } + + $progress = $record->getTranslationProgress(); + $percentage = $progress[$languageCode] ?? 0; + + if ($percentage === 100) { + return 'success'; + } elseif ($percentage >= 50) { + return 'warning'; + } elseif ($percentage > 0) { + return 'danger'; + } + + return 'gray'; + } + + public static function fillFromRecord($record): array + { + $data = ['translations' => []]; + + if (!method_exists($record, 'getTranslatableFields')) { + return $data; + } + + $languages = Language::active()->ordered()->get(); + $fields = $record->getTranslatableFields(); + + foreach ($languages as $language) { + $data['translations'][$language->code] = []; + foreach ($fields as $field) { + $translation = $record->getTranslation($field, $language->code, false); + if ($translation) { + $data['translations'][$language->code][$field] = $translation->field_value; + } + } + } + + return $data; + } + + public static function saveTranslations($record, array $data): void + { + if (!method_exists($record, 'setTranslation')) { + return; + } + + $translations = $data['translations'] ?? []; + + \Log::info('Saving translations', ['data' => $translations]); + + foreach ($translations as $languageCode => $fields) { + if (!is_array($fields)) { + continue; + } + + foreach ($fields as $fieldName => $value) { + // Boş değerleri de kaydet (silme işlemi için) + $record->setTranslation( + $fieldName, + $languageCode, + $value ?? '', + 'published', // Default status + auth()->id() + ); + + \Log::info('Translation saved', [ + 'language' => $languageCode, + 'field' => $fieldName, + 'value' => substr($value ?? '', 0, 50) + ]); + } + } + } +} + diff --git a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php index 199a18e..d6a37d2 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/CreatePage.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Pages; +use App\Filament\Admin\Resources\Components\TranslationTabs; use App\Filament\Admin\Resources\Pages\PageResource; use Filament\Resources\Pages\CreateRecord; @@ -23,4 +24,10 @@ class CreatePage extends CreateRecord { return __('pages.created_successfully'); } + + protected function afterCreate(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php index a8cb597..569c930 100644 --- a/app/Filament/Admin/Resources/Pages/Pages/EditPage.php +++ b/app/Filament/Admin/Resources/Pages/Pages/EditPage.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Pages; +use App\Filament\Admin\Resources\Components\TranslationTabs; use App\Filament\Admin\Resources\Pages\PageResource; use Filament\Actions\DeleteAction; use Filament\Actions\ForceDeleteAction; @@ -38,4 +39,18 @@ class EditPage extends EditRecord { return __('pages.updated_successfully'); } + + protected function mutateFormDataBeforeFill(array $data): array + { + // Load existing translations + $translationData = TranslationTabs::fillFromRecord($this->record); + + return array_merge($data, $translationData); + } + + protected function afterSave(): void + { + // Save translations + TranslationTabs::saveTranslations($this->record, $this->form->getState()); + } } diff --git a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php index 342a427..ceebbff 100644 --- a/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php +++ b/app/Filament/Admin/Resources/Pages/Schemas/PageForm.php @@ -2,6 +2,7 @@ namespace App\Filament\Admin\Resources\Pages\Schemas; +use App\Filament\Admin\Resources\Components\TranslationTabs; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\FileUpload; @@ -162,6 +163,47 @@ class PageForm ->columnSpanFull() ->collapsible(true) ->collapsed(true), + + // Çeviri Sekmesi + Section::make('🌍 ' . __('pages.translations_section')) + ->schema([ + TranslationTabs::make([ + 'title' => [ + 'type' => 'text', + 'label' => __('pages.title_field'), + 'required' => false, + 'maxLength' => 255, + ], + 'content' => [ + 'type' => 'richtext', + 'label' => __('pages.content_field'), + 'required' => false, + ], + 'excerpt' => [ + 'type' => 'textarea', + 'label' => __('pages.excerpt_field'), + 'required' => false, + 'maxLength' => 500, + 'rows' => 3, + ], + 'meta_title' => [ + 'type' => 'text', + 'label' => __('pages.meta_title_field'), + 'required' => false, + 'maxLength' => 60, + ], + 'meta_description' => [ + 'type' => 'textarea', + 'label' => __('pages.meta_description_field'), + 'required' => false, + 'maxLength' => 160, + 'rows' => 3, + ], + ]), + ]) + ->columnSpanFull() + ->collapsible(true) + ->collapsed(false), ]); } } diff --git a/lang/en/blog.php b/lang/en/blog.php index 6c38691..8d98641 100644 --- a/lang/en/blog.php +++ b/lang/en/blog.php @@ -34,6 +34,7 @@ return [ 'content_section' => 'Content', 'settings_section' => 'Blog Settings', 'seo_section' => 'SEO Settings', + 'translations_section' => 'Translations', // Helper texts 'slug_helper' => 'The part that will appear in the URL. Example: blog-post', diff --git a/lang/en/pages.php b/lang/en/pages.php index 85e79c5..d70fc7a 100644 --- a/lang/en/pages.php +++ b/lang/en/pages.php @@ -26,6 +26,7 @@ return [ 'form_section_content' => 'Content', 'form_section_page_settings' => 'Page Settings', 'form_section_seo_settings' => 'SEO Settings', + 'translations_section' => 'Translations', // Form fields 'title_field' => 'Title', diff --git a/lang/tr/blog.php b/lang/tr/blog.php index 53cf6a6..a16d84f 100644 --- a/lang/tr/blog.php +++ b/lang/tr/blog.php @@ -34,6 +34,7 @@ return [ 'content_section' => 'İçerik', 'settings_section' => 'Blog Ayarları', 'seo_section' => 'SEO Ayarları', + 'translations_section' => 'Çeviriler', // Helper texts 'slug_helper' => 'URL\'de görünecek kısım. Örnek: blog-yazisi', diff --git a/lang/tr/pages.php b/lang/tr/pages.php index e3e2922..2b020aa 100644 --- a/lang/tr/pages.php +++ b/lang/tr/pages.php @@ -26,6 +26,7 @@ return [ 'form_section_content' => 'İçerik', 'form_section_page_settings' => 'Sayfa Ayarları', 'form_section_seo_settings' => 'SEO Ayarları', + 'translations_section' => 'Çeviriler', // Form fields 'title_field' => 'Başlık', From fa136f49483e317993d109c3d10f651dda1e4a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Fri, 3 Oct 2025 13:01:37 -0300 Subject: [PATCH 07/15] Remove unnecessary blank lines in PageController, footer, and navigation Blade templates for cleaner code structure. --- app/Http/Controllers/PageController.php | 2 ++ resources/views/components/footer.blade.php | 2 ++ resources/views/components/navigation.blade.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index 7907eaa..85ab2f7 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -66,3 +66,5 @@ class PageController extends Controller } } + + diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php index 04b156a..195ef48 100644 --- a/resources/views/components/footer.blade.php +++ b/resources/views/components/footer.blade.php @@ -59,3 +59,5 @@ + + diff --git a/resources/views/components/navigation.blade.php b/resources/views/components/navigation.blade.php index 880114e..a3f6288 100644 --- a/resources/views/components/navigation.blade.php +++ b/resources/views/components/navigation.blade.php @@ -125,3 +125,5 @@
+ + From 0ece20fa47d48e17615361712d3a44e5829ccfcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:14:32 -0300 Subject: [PATCH 08/15] Enhance documentation for Filament 4.x import rules and universal translation system. Added guidelines for table actions, form components, and translation methods, ensuring clarity on correct usage and implementation. Updated translation workflow and helper functions for better integration in models. --- .cursorrules | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.cursorrules b/.cursorrules index 2c586b7..86e95fc 100644 --- a/.cursorrules +++ b/.cursorrules @@ -101,12 +101,8 @@ use Filament\Forms\Components\FileUpload; // Layout components (ÖNEMLİ: Schemas namespace!) use Filament\Schemas\Components\Section; // ✅ Doğru use Filament\Schemas\Components\Grid; // ✅ Doğru +use Filament\Schemas\Components\Tabs; // ✅ Doğru use Filament\Schemas\Components\Fieldset; // ✅ Doğru - -// Tabs components (ÖNEMLİ: Forms namespace!) -use Filament\Forms\Components\Tabs; // ✅ Doğru -// Tab kullanımı: Tabs\Tab::make() şeklinde kullan -// Import gerekmez, Tabs namespace'i üzerinden erişilir ``` #### **YANLIŞ KULLANIM (Eski Filament 3.x):** From a7ab40cde910d200ec0d69bea9f9cb783613415f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Wed, 1 Oct 2025 04:43:27 -0300 Subject: [PATCH 09/15] Implement Translation Tabs Component: Added TranslationTabs component for managing translations in Blog and Page resources. Integrated translation fields into forms, ensuring localization support for titles, content, and metadata. Updated existing pages to save and load translations, enhancing the universal translation system. Added relevant localization keys for English and Turkish. --- .cursorrules | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.cursorrules b/.cursorrules index 86e95fc..2c586b7 100644 --- a/.cursorrules +++ b/.cursorrules @@ -101,8 +101,12 @@ use Filament\Forms\Components\FileUpload; // Layout components (ÖNEMLİ: Schemas namespace!) use Filament\Schemas\Components\Section; // ✅ Doğru use Filament\Schemas\Components\Grid; // ✅ Doğru -use Filament\Schemas\Components\Tabs; // ✅ Doğru use Filament\Schemas\Components\Fieldset; // ✅ Doğru + +// Tabs components (ÖNEMLİ: Forms namespace!) +use Filament\Forms\Components\Tabs; // ✅ Doğru +// Tab kullanımı: Tabs\Tab::make() şeklinde kullan +// Import gerekmez, Tabs namespace'i üzerinden erişilir ``` #### **YANLIŞ KULLANIM (Eski Filament 3.x):** From 9094e9c729fd04babd29b03e47c5d016faca99c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 14:59:00 -0300 Subject: [PATCH 10/15] Add migration for settings table: Created a new migration file to establish the settings table with necessary fields, including key, value, type, group, label, description, and soft delete functionality. Implemented indexes for key, group, and is_active columns to optimize queries. --- ...025_10_20_170041_create_settings_table.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 database/migrations/2025_10_20_170041_create_settings_table.php diff --git a/database/migrations/2025_10_20_170041_create_settings_table.php b/database/migrations/2025_10_20_170041_create_settings_table.php new file mode 100644 index 0000000..7a42262 --- /dev/null +++ b/database/migrations/2025_10_20_170041_create_settings_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->enum('type', ['string', 'text', 'boolean', 'integer', 'float', 'array', 'json'])->default('string'); + $table->string('group')->default('general'); + $table->string('label'); + $table->text('description')->nullable(); + $table->boolean('is_public')->default(false); + $table->boolean('is_active')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->index('key'); + $table->index('group'); + $table->index('is_active'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; From b8c0d6a5c4ce2510b854691ac650281af6d04e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 14:59:15 -0300 Subject: [PATCH 11/15] Add localization files for settings module: Created English and Turkish language files for settings, including keys for actions, form fields, messages, table columns, types, groups, and validation. This enhances the localization support for the settings module, ensuring a consistent user experience across languages. --- lang/en/settings.php | 87 ++++++++++++++++++++++++++++++++++++++++++++ lang/tr/settings.php | 87 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 lang/en/settings.php create mode 100644 lang/tr/settings.php diff --git a/lang/en/settings.php b/lang/en/settings.php new file mode 100644 index 0000000..d64d7cd --- /dev/null +++ b/lang/en/settings.php @@ -0,0 +1,87 @@ + 'Settings', + 'navigation_label' => 'Settings', + 'model_label' => 'Setting', + 'plural_model_label' => 'Settings', + + // Actions + 'create' => 'Create New Setting', + 'edit' => 'Edit Setting', + 'delete' => 'Delete Setting', + 'restore' => 'Restore Setting', + 'force_delete' => 'Force Delete Setting', + 'save' => 'Save', + 'cancel' => 'Cancel', + + // Form fields + 'key' => 'Key', + 'key_helper' => 'Setting key (e.g: site_name, site_email)', + 'value' => 'Value', + 'value_helper' => 'Setting value', + 'type' => 'Type', + 'type_helper' => 'Value type', + 'group' => 'Group', + 'group_helper' => 'Setting group (e.g: general, email, seo)', + 'label' => 'Label', + 'label_helper' => 'User-friendly label', + 'description' => 'Description', + 'description_helper' => 'Setting description', + 'is_public' => 'Public', + 'is_public_helper' => 'Is this setting public?', + 'is_active' => 'Active', + 'is_active_helper' => 'Is this setting active?', + + // Messages + 'created_successfully' => 'Setting created successfully.', + 'updated_successfully' => 'Setting updated successfully.', + 'deleted_successfully' => 'Setting deleted successfully.', + 'restored_successfully' => 'Setting restored successfully.', + 'force_deleted_successfully' => 'Setting permanently deleted.', + + // Table columns + 'table_key' => 'Key', + 'table_value' => 'Value', + 'table_type' => 'Type', + 'table_group' => 'Group', + 'table_label' => 'Label', + 'table_public' => 'Public', + 'table_active' => 'Active', + 'table_created_at' => 'Created At', + 'table_updated_at' => 'Updated At', + + // Types + 'type_string' => 'String', + 'type_text' => 'Text', + 'type_boolean' => 'Boolean', + 'type_integer' => 'Integer', + 'type_float' => 'Float', + 'type_array' => 'Array', + 'type_json' => 'JSON', + + // Groups + 'group_general' => 'General', + 'group_email' => 'Email', + 'group_seo' => 'SEO', + 'group_social' => 'Social Media', + 'group_security' => 'Security', + 'group_payment' => 'Payment', + 'group_notification' => 'Notification', + 'group_other' => 'Other', + + // Validation messages + 'key_required' => 'Key field is required.', + 'key_unique' => 'This key is already in use.', + 'key_regex' => 'Key can only contain letters, numbers, underscores and hyphens.', + 'value_required' => 'Value field is required.', + 'type_required' => 'Type field is required.', + 'label_required' => 'Label field is required.', + + // Filters + 'filter_group' => 'By Group', + 'filter_type' => 'By Type', + 'filter_active' => 'Active', + 'filter_public' => 'Public', +]; + diff --git a/lang/tr/settings.php b/lang/tr/settings.php new file mode 100644 index 0000000..4861cfa --- /dev/null +++ b/lang/tr/settings.php @@ -0,0 +1,87 @@ + 'Ayarlar', + 'navigation_label' => 'Ayarlar', + 'model_label' => 'Ayar', + 'plural_model_label' => 'Ayarlar', + + // Actions + 'create' => 'Yeni Ayar Oluştur', + 'edit' => 'Ayarı Düzenle', + 'delete' => 'Ayarı Sil', + 'restore' => 'Ayarı Geri Yükle', + 'force_delete' => 'Ayarı Kalıcı Olarak Sil', + 'save' => 'Kaydet', + 'cancel' => 'İptal', + + // Form fields + 'key' => 'Anahtar', + 'key_helper' => 'Ayar anahtarı (örn: site_name, site_email)', + 'value' => 'Değer', + 'value_helper' => 'Ayar değeri', + 'type' => 'Tip', + 'type_helper' => 'Değer tipi', + 'group' => 'Grup', + 'group_helper' => 'Ayar grubu (örn: general, email, seo)', + 'label' => 'Etiket', + 'label_helper' => 'Kullanıcı dostu etiket', + 'description' => 'Açıklama', + 'description_helper' => 'Ayar açıklaması', + 'is_public' => 'Herkese Açık', + 'is_public_helper' => 'Bu ayar herkese açık mı?', + 'is_active' => 'Aktif', + 'is_active_helper' => 'Bu ayar aktif mi?', + + // Messages + 'created_successfully' => 'Ayar başarıyla oluşturuldu.', + 'updated_successfully' => 'Ayar başarıyla güncellendi.', + 'deleted_successfully' => 'Ayar başarıyla silindi.', + 'restored_successfully' => 'Ayar başarıyla geri yüklendi.', + 'force_deleted_successfully' => 'Ayar kalıcı olarak silindi.', + + // Table columns + 'table_key' => 'Anahtar', + 'table_value' => 'Değer', + 'table_type' => 'Tip', + 'table_group' => 'Grup', + 'table_label' => 'Etiket', + 'table_public' => 'Herkese Açık', + 'table_active' => 'Aktif', + 'table_created_at' => 'Oluşturulma', + 'table_updated_at' => 'Güncellenme', + + // Types + 'type_string' => 'Metin', + 'type_text' => 'Uzun Metin', + 'type_boolean' => 'Evet/Hayır', + 'type_integer' => 'Tam Sayı', + 'type_float' => 'Ondalıklı Sayı', + 'type_array' => 'Dizi', + 'type_json' => 'JSON', + + // Groups + 'group_general' => 'Genel', + 'group_email' => 'E-posta', + 'group_seo' => 'SEO', + 'group_social' => 'Sosyal Medya', + 'group_security' => 'Güvenlik', + 'group_payment' => 'Ödeme', + 'group_notification' => 'Bildirim', + 'group_other' => 'Diğer', + + // Validation messages + 'key_required' => 'Anahtar alanı zorunludur.', + 'key_unique' => 'Bu anahtar zaten kullanılıyor.', + 'key_regex' => 'Anahtar sadece harf, rakam, alt çizgi ve tire içerebilir.', + 'value_required' => 'Değer alanı zorunludur.', + 'type_required' => 'Tip alanı zorunludur.', + 'label_required' => 'Etiket alanı zorunludur.', + + // Filters + 'filter_group' => 'Gruba Göre', + 'filter_type' => 'Tipe Göre', + 'filter_active' => 'Aktif', + 'filter_public' => 'Herkese Açık', +]; + From 1adc90ed598ce2e5a9cc97e24444a7e7176eb609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 15:04:30 -0300 Subject: [PATCH 12/15] Add settings module: Implemented SettingResource with CRUD pages, schemas, and models for managing application settings. Added localization support for settings, including navigation labels, form fields, and table columns. Integrated soft delete functionality and optimized query handling for settings retrieval. --- .../Settings/Pages/CreateSetting.php | 46 ++++ .../Resources/Settings/Pages/EditSetting.php | 81 ++++++ .../Resources/Settings/Pages/ListSettings.php | 25 ++ .../Settings/Schemas/SettingForm.php | 107 ++++++++ .../Resources/Settings/SettingResource.php | 78 ++++++ .../Settings/Tables/SettingsTable.php | 136 ++++++++++ app/Models/Setting.php | 94 +++++++ database/factories/SettingFactory.php | 23 ++ database/seeders/SettingSeeder.php | 237 ++++++++++++++++++ 9 files changed, 827 insertions(+) create mode 100644 app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php create mode 100644 app/Filament/Admin/Resources/Settings/Pages/EditSetting.php create mode 100644 app/Filament/Admin/Resources/Settings/Pages/ListSettings.php create mode 100644 app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php create mode 100644 app/Filament/Admin/Resources/Settings/SettingResource.php create mode 100644 app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php create mode 100644 app/Models/Setting.php create mode 100644 database/factories/SettingFactory.php create mode 100644 database/seeders/SettingSeeder.php diff --git a/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php b/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php new file mode 100644 index 0000000..a97330f --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php @@ -0,0 +1,46 @@ +getResource()::getUrl('index'); + } + + protected function getCreatedNotificationTitle(): ?string + { + return __('settings.created_successfully'); + } + + protected function mutateFormDataBeforeSave(array $data): array + { + // Boolean değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'boolean') { + $data['value'] = $data['value'] ? '1' : '0'; + } + + // Integer değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'integer') { + $data['value'] = (string) $data['value']; + } + + // Float değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'float') { + $data['value'] = (string) $data['value']; + } + + return $data; + } +} diff --git a/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php new file mode 100644 index 0000000..01bea62 --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php @@ -0,0 +1,81 @@ +label(__('settings.delete')), + RestoreAction::make() + ->label(__('settings.restore')), + ForceDeleteAction::make() + ->label(__('settings.force_delete')), + ]; + } + + protected function getRedirectUrl(): string + { + return $this->getResource()::getUrl('index'); + } + + protected function getSavedNotificationTitle(): ?string + { + return __('settings.updated_successfully'); + } + + protected function mutateFormDataBeforeFill(array $data): array + { + // Boolean tipindeki değerleri cast et + if (isset($data['type']) && $data['type'] === 'boolean') { + $data['value'] = filter_var($data['value'], FILTER_VALIDATE_BOOLEAN); + } + + // Integer tipindeki değerleri cast et + if (isset($data['type']) && $data['type'] === 'integer') { + $data['value'] = (int) $data['value']; + } + + // Float tipindeki değerleri cast et + if (isset($data['type']) && $data['type'] === 'float') { + $data['value'] = (float) $data['value']; + } + + return $data; + } + + protected function mutateFormDataBeforeSave(array $data): array + { + // Boolean değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'boolean') { + $data['value'] = $data['value'] ? '1' : '0'; + } + + // Integer değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'integer') { + $data['value'] = (string) $data['value']; + } + + // Float değerleri string'e çevir + if (isset($data['type']) && $data['type'] === 'float') { + $data['value'] = (string) $data['value']; + } + + return $data; + } +} diff --git a/app/Filament/Admin/Resources/Settings/Pages/ListSettings.php b/app/Filament/Admin/Resources/Settings/Pages/ListSettings.php new file mode 100644 index 0000000..c07e9c9 --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/Pages/ListSettings.php @@ -0,0 +1,25 @@ +label(__('settings.create')), + ]; + } +} diff --git a/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php b/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php new file mode 100644 index 0000000..32e1861 --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php @@ -0,0 +1,107 @@ +components([ + Section::make(__('settings.title')) + ->schema([ + TextInput::make('key') + ->label(__('settings.key')) + ->helperText(__('settings.key_helper')) + ->required() + ->unique(ignoreRecord: true) + ->regex('/^[a-z0-9_-]+$/') + ->maxLength(255) + ->disabled(fn ($record) => $record !== null) + ->columnSpanFull(), + + TextInput::make('label') + ->label(__('settings.label')) + ->helperText(__('settings.label_helper')) + ->required() + ->maxLength(255) + ->columnSpanFull(), + + Textarea::make('description') + ->label(__('settings.description')) + ->helperText(__('settings.description_helper')) + ->rows(3) + ->columnSpanFull(), + + Select::make('type') + ->label(__('settings.type')) + ->helperText(__('settings.type_helper')) + ->required() + ->options([ + 'string' => __('settings.type_string'), + 'text' => __('settings.type_text'), + 'boolean' => __('settings.type_boolean'), + 'integer' => __('settings.type_integer'), + 'float' => __('settings.type_float'), + 'array' => __('settings.type_array'), + 'json' => __('settings.type_json'), + ]) + ->live() + ->afterStateUpdated(fn (Set $set) => $set('value', null)), + + Select::make('group') + ->label(__('settings.group')) + ->helperText(__('settings.group_helper')) + ->required() + ->options([ + 'general' => __('settings.group_general'), + 'email' => __('settings.group_email'), + 'seo' => __('settings.group_seo'), + 'social' => __('settings.group_social'), + 'security' => __('settings.group_security'), + 'payment' => __('settings.group_payment'), + 'notification' => __('settings.group_notification'), + 'other' => __('settings.group_other'), + ]) + ->default('general'), + + Textarea::make('value') + ->label(__('settings.value')) + ->helperText(__('settings.value_helper')) + ->required() + ->visible(fn (Get $get) => !in_array($get('type'), ['boolean'])) + ->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3) + ->columnSpanFull(), + + Toggle::make('value') + ->label(__('settings.value')) + ->helperText(__('settings.value_helper')) + ->required() + ->visible(fn (Get $get) => $get('type') === 'boolean') + ->columnSpanFull(), + + Toggle::make('is_active') + ->label(__('settings.is_active')) + ->helperText(__('settings.is_active_helper')) + ->default(true) + ->columnSpan(1), + + Toggle::make('is_public') + ->label(__('settings.is_public')) + ->helperText(__('settings.is_public_helper')) + ->default(false) + ->columnSpan(1), + ]) + ->columns(2), + ]); + } +} diff --git a/app/Filament/Admin/Resources/Settings/SettingResource.php b/app/Filament/Admin/Resources/Settings/SettingResource.php new file mode 100644 index 0000000..65b93a7 --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/SettingResource.php @@ -0,0 +1,78 @@ + ListSettings::route('/'), + 'create' => CreateSetting::route('/create'), + 'edit' => EditSetting::route('/{record}/edit'), + ]; + } + + public static function getEloquentQuery(): Builder + { + return parent::getEloquentQuery() + ->withoutGlobalScopes([ + SoftDeletingScope::class, + ]); + } +} diff --git a/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php b/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php new file mode 100644 index 0000000..48e44cb --- /dev/null +++ b/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php @@ -0,0 +1,136 @@ +columns([ + TextColumn::make('key') + ->label(__('settings.table_key')) + ->searchable() + ->sortable(), + + TextColumn::make('label') + ->label(__('settings.table_label')) + ->searchable() + ->sortable(), + + TextColumn::make('value') + ->label(__('settings.table_value')) + ->searchable() + ->limit(50) + ->wrap(), + + TextColumn::make('type') + ->label(__('settings.table_type')) + ->badge() + ->color(fn (string $state): string => match ($state) { + 'boolean' => 'success', + 'integer', 'float' => 'info', + 'array', 'json' => 'warning', + default => 'gray', + }) + ->formatStateUsing(fn (string $state): string => __("settings.type_{$state}")), + + TextColumn::make('group') + ->label(__('settings.table_group')) + ->badge() + ->color('primary') + ->formatStateUsing(fn (string $state): string => __("settings.group_{$state}")), + + IconColumn::make('is_active') + ->label(__('settings.table_active')) + ->boolean() + ->sortable(), + + IconColumn::make('is_public') + ->label(__('settings.table_public')) + ->boolean() + ->sortable(), + + TextColumn::make('created_at') + ->label(__('settings.table_created_at')) + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + + TextColumn::make('updated_at') + ->label(__('settings.table_updated_at')) + ->dateTime('d.m.Y H:i') + ->sortable() + ->toggleable(isToggledHiddenByDefault: true), + ]) + ->filters([ + SelectFilter::make('group') + ->label(__('settings.filter_group')) + ->options([ + 'general' => __('settings.group_general'), + 'email' => __('settings.group_email'), + 'seo' => __('settings.group_seo'), + 'social' => __('settings.group_social'), + 'security' => __('settings.group_security'), + 'payment' => __('settings.group_payment'), + 'notification' => __('settings.group_notification'), + 'other' => __('settings.group_other'), + ]), + + SelectFilter::make('type') + ->label(__('settings.filter_type')) + ->options([ + 'string' => __('settings.type_string'), + 'text' => __('settings.type_text'), + 'boolean' => __('settings.type_boolean'), + 'integer' => __('settings.type_integer'), + 'float' => __('settings.type_float'), + 'array' => __('settings.type_array'), + 'json' => __('settings.type_json'), + ]), + + TernaryFilter::make('is_active') + ->label(__('settings.filter_active')) + ->boolean() + ->trueLabel(__('settings.is_active')) + ->falseLabel(__('settings.is_active') . ' (Hayır)'), + + TernaryFilter::make('is_public') + ->label(__('settings.filter_public')) + ->boolean() + ->trueLabel(__('settings.is_public')) + ->falseLabel(__('settings.is_public') . ' (Hayır)'), + + TrashedFilter::make(), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make(), + RestoreAction::make(), + ForceDeleteAction::make(), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make(), + RestoreBulkAction::make(), + ForceDeleteBulkAction::make(), + ]), + ]) + ->defaultSort('created_at', 'desc'); + } +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..a84672b --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,94 @@ + 'boolean', + 'is_active' => 'boolean', + ]; + + protected $dates = [ + 'created_at', + 'updated_at', + 'deleted_at', + ]; + + /** + * Get setting by key + */ + public static function get(string $key, $default = null) + { + $setting = static::where('key', $key) + ->where('is_active', true) + ->first(); + + if (!$setting) { + return $default; + } + + return static::castValue($setting->value, $setting->type); + } + + /** + * Set setting value + */ + public static function set(string $key, $value, string $type = 'string'): void + { + static::updateOrCreate( + ['key' => $key], + [ + 'value' => $value, + 'type' => $type, + 'is_active' => true, + ] + ); + } + + /** + * Cast value based on type + */ + protected static function castValue($value, string $type) + { + return match($type) { + 'boolean' => filter_var($value, FILTER_VALIDATE_BOOLEAN), + 'integer' => (int) $value, + 'float' => (float) $value, + 'array', 'json' => json_decode($value, true), + default => $value, + }; + } + + /** + * Get all settings by group + */ + public static function getGroup(string $group): array + { + return static::where('group', $group) + ->where('is_active', true) + ->pluck('value', 'key') + ->map(function ($value, $key) use ($group) { + $setting = static::where('key', $key)->first(); + return static::castValue($value, $setting->type); + }) + ->toArray(); + } +} diff --git a/database/factories/SettingFactory.php b/database/factories/SettingFactory.php new file mode 100644 index 0000000..eb11ef9 --- /dev/null +++ b/database/factories/SettingFactory.php @@ -0,0 +1,23 @@ + + */ +class SettingFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/seeders/SettingSeeder.php b/database/seeders/SettingSeeder.php new file mode 100644 index 0000000..e750004 --- /dev/null +++ b/database/seeders/SettingSeeder.php @@ -0,0 +1,237 @@ + 'site_name', + 'value' => 'Citrus Platform', + 'type' => 'string', + 'group' => 'general', + 'label' => 'Site Adı', + 'description' => 'Web sitesinin adı', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'site_description', + 'value' => 'Modern ve güvenilir web platformu', + 'type' => 'text', + 'group' => 'general', + 'label' => 'Site Açıklaması', + 'description' => 'Web sitesinin açıklaması', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'site_keywords', + 'value' => 'citrus, platform, web, modern', + 'type' => 'string', + 'group' => 'general', + 'label' => 'Site Anahtar Kelimeleri', + 'description' => 'SEO için anahtar kelimeler', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'maintenance_mode', + 'value' => '0', + 'type' => 'boolean', + 'group' => 'general', + 'label' => 'Bakım Modu', + 'description' => 'Site bakım modunda mı?', + 'is_public' => false, + 'is_active' => true, + ], + + // E-posta Ayarları + [ + 'key' => 'mail_from_address', + 'value' => 'noreply@citrus.truncgil.com', + 'type' => 'string', + 'group' => 'email', + 'label' => 'E-posta Gönderen Adres', + 'description' => 'Sistem e-postalarının gönderileceği adres', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'mail_from_name', + 'value' => 'Citrus Platform', + 'type' => 'string', + 'group' => 'email', + 'label' => 'E-posta Gönderen İsim', + 'description' => 'Sistem e-postalarının gönderen adı', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'mail_notifications_enabled', + 'value' => '1', + 'type' => 'boolean', + 'group' => 'email', + 'label' => 'E-posta Bildirimleri Aktif', + 'description' => 'E-posta bildirimleri gönderilsin mi?', + 'is_public' => false, + 'is_active' => true, + ], + + // SEO Ayarları + [ + 'key' => 'seo_meta_title', + 'value' => 'Citrus Platform - Modern Web Platformu', + 'type' => 'string', + 'group' => 'seo', + 'label' => 'SEO Meta Başlık', + 'description' => 'Varsayılan SEO meta başlığı', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'seo_meta_description', + 'value' => 'Modern ve güvenilir web platformu', + 'type' => 'text', + 'group' => 'seo', + 'label' => 'SEO Meta Açıklama', + 'description' => 'Varsayılan SEO meta açıklaması', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'seo_google_analytics', + 'value' => '', + 'type' => 'string', + 'group' => 'seo', + 'label' => 'Google Analytics ID', + 'description' => 'Google Analytics takip kodu', + 'is_public' => false, + 'is_active' => true, + ], + + // Sosyal Medya Ayarları + [ + 'key' => 'social_facebook', + 'value' => '', + 'type' => 'string', + 'group' => 'social', + 'label' => 'Facebook URL', + 'description' => 'Facebook sayfa URL\'si', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'social_twitter', + 'value' => '', + 'type' => 'string', + 'group' => 'social', + 'label' => 'Twitter URL', + 'description' => 'Twitter profil URL\'si', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'social_instagram', + 'value' => '', + 'type' => 'string', + 'group' => 'social', + 'label' => 'Instagram URL', + 'description' => 'Instagram profil URL\'si', + 'is_public' => true, + 'is_active' => true, + ], + [ + 'key' => 'social_linkedin', + 'value' => '', + 'type' => 'string', + 'group' => 'social', + 'label' => 'LinkedIn URL', + 'description' => 'LinkedIn profil URL\'si', + 'is_public' => true, + 'is_active' => true, + ], + + // Güvenlik Ayarları + [ + 'key' => 'security_max_login_attempts', + 'value' => '5', + 'type' => 'integer', + 'group' => 'security', + 'label' => 'Maksimum Giriş Denemesi', + 'description' => 'Kullanıcı hesabının kilitlenmesi için maksimum giriş denemesi', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'security_session_timeout', + 'value' => '120', + 'type' => 'integer', + 'group' => 'security', + 'label' => 'Oturum Zaman Aşımı (dakika)', + 'description' => 'Kullanıcı oturumunun zaman aşımına uğrayacağı süre', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'security_2fa_enabled', + 'value' => '0', + 'type' => 'boolean', + 'group' => 'security', + 'label' => 'İki Faktörlü Doğrulama', + 'description' => 'İki faktörlü doğrulama aktif mi?', + 'is_public' => false, + 'is_active' => true, + ], + + // Bildirim Ayarları + [ + 'key' => 'notification_email_enabled', + 'value' => '1', + 'type' => 'boolean', + 'group' => 'notification', + 'label' => 'E-posta Bildirimleri', + 'description' => 'E-posta bildirimleri gönderilsin mi?', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'notification_sms_enabled', + 'value' => '0', + 'type' => 'boolean', + 'group' => 'notification', + 'label' => 'SMS Bildirimleri', + 'description' => 'SMS bildirimleri gönderilsin mi?', + 'is_public' => false, + 'is_active' => true, + ], + [ + 'key' => 'notification_push_enabled', + 'value' => '0', + 'type' => 'boolean', + 'group' => 'notification', + 'label' => 'Push Bildirimleri', + 'description' => 'Push bildirimleri gönderilsin mi?', + 'is_public' => false, + 'is_active' => true, + ], + ]; + + foreach ($settings as $setting) { + Setting::updateOrCreate( + ['key' => $setting['key']], + $setting + ); + } + } +} From 1450e8baf293b75e0e081aa62e008fc9ae80bad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 15:19:34 -0300 Subject: [PATCH 13/15] Refactor settings form handling: Removed redundant data mutation methods in CreateSetting and EditSetting pages. Updated SettingForm to use distinct fields for text and boolean values, enhancing clarity and functionality. Added new attributes in the Setting model to manage value assignments from different fields. --- .../Settings/Pages/CreateSetting.php | 20 ---------- .../Resources/Settings/Pages/EditSetting.php | 40 ------------------- .../Settings/Schemas/SettingForm.php | 16 +++++++- app/Models/Setting.php | 18 +++++++++ 4 files changed, 32 insertions(+), 62 deletions(-) diff --git a/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php b/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php index a97330f..52a8094 100644 --- a/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php +++ b/app/Filament/Admin/Resources/Settings/Pages/CreateSetting.php @@ -23,24 +23,4 @@ class CreateSetting extends CreateRecord { return __('settings.created_successfully'); } - - protected function mutateFormDataBeforeSave(array $data): array - { - // Boolean değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'boolean') { - $data['value'] = $data['value'] ? '1' : '0'; - } - - // Integer değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'integer') { - $data['value'] = (string) $data['value']; - } - - // Float değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'float') { - $data['value'] = (string) $data['value']; - } - - return $data; - } } diff --git a/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php index 01bea62..928b25a 100644 --- a/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php +++ b/app/Filament/Admin/Resources/Settings/Pages/EditSetting.php @@ -38,44 +38,4 @@ class EditSetting extends EditRecord { return __('settings.updated_successfully'); } - - protected function mutateFormDataBeforeFill(array $data): array - { - // Boolean tipindeki değerleri cast et - if (isset($data['type']) && $data['type'] === 'boolean') { - $data['value'] = filter_var($data['value'], FILTER_VALIDATE_BOOLEAN); - } - - // Integer tipindeki değerleri cast et - if (isset($data['type']) && $data['type'] === 'integer') { - $data['value'] = (int) $data['value']; - } - - // Float tipindeki değerleri cast et - if (isset($data['type']) && $data['type'] === 'float') { - $data['value'] = (float) $data['value']; - } - - return $data; - } - - protected function mutateFormDataBeforeSave(array $data): array - { - // Boolean değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'boolean') { - $data['value'] = $data['value'] ? '1' : '0'; - } - - // Integer değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'integer') { - $data['value'] = (string) $data['value']; - } - - // Float değerleri string'e çevir - if (isset($data['type']) && $data['type'] === 'float') { - $data['value'] = (string) $data['value']; - } - - return $data; - } } diff --git a/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php b/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php index 32e1861..6269957 100644 --- a/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php +++ b/app/Filament/Admin/Resources/Settings/Schemas/SettingForm.php @@ -74,19 +74,31 @@ class SettingForm ]) ->default('general'), - Textarea::make('value') + Textarea::make('value_text') ->label(__('settings.value')) ->helperText(__('settings.value_helper')) ->required() ->visible(fn (Get $get) => !in_array($get('type'), ['boolean'])) ->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3) + ->formatStateUsing(function ($state, $record) { + if ($record && $record->type !== 'boolean') { + return $record->value; + } + return $state; + }) ->columnSpanFull(), - Toggle::make('value') + Toggle::make('value_boolean') ->label(__('settings.value')) ->helperText(__('settings.value_helper')) ->required() ->visible(fn (Get $get) => $get('type') === 'boolean') + ->formatStateUsing(function ($state, $record) { + if ($record && $record->type === 'boolean') { + return filter_var($record->value, FILTER_VALIDATE_BOOLEAN); + } + return false; + }) ->columnSpanFull(), Toggle::make('is_active') diff --git a/app/Models/Setting.php b/app/Models/Setting.php index a84672b..569bb53 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -13,6 +13,8 @@ class Setting extends Model protected $fillable = [ 'key', 'value', + 'value_text', + 'value_boolean', 'type', 'group', 'label', @@ -32,6 +34,22 @@ class Setting extends Model 'deleted_at', ]; + /** + * Set value from different field names + */ + public function setValueTextAttribute($value) + { + $this->attributes['value'] = $value; + } + + /** + * Set value from different field names + */ + public function setValueBooleanAttribute($value) + { + $this->attributes['value'] = $value ? '1' : '0'; + } + /** * Get setting by key */ From a01c24616ca3f96c38a9136580a44495e55ceeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 15:24:30 -0300 Subject: [PATCH 14/15] Enhance SettingsTable: Added state formatting for boolean types to display emojis in the settings table. This improves user experience by providing a visual representation of boolean values, making the table more intuitive. --- .../Admin/Resources/Settings/Tables/SettingsTable.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php b/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php index 48e44cb..1f5bd10 100644 --- a/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php +++ b/app/Filament/Admin/Resources/Settings/Tables/SettingsTable.php @@ -37,7 +37,16 @@ class SettingsTable ->label(__('settings.table_value')) ->searchable() ->limit(50) - ->wrap(), + ->wrap() + ->formatStateUsing(function ($state, $record) { + // Boolean tipler için emoji göster + if ($record->type === 'boolean') { + return filter_var($state, FILTER_VALIDATE_BOOLEAN) + ? '✅' + : '❌'; + } + return $state; + }), TextColumn::make('type') ->label(__('settings.table_type')) From 1bb06c7bd4662c876b58b7587f06c58dbd20a8fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Mon, 20 Oct 2025 15:26:04 -0300 Subject: [PATCH 15/15] Update navigation group label in SettingResource from 'Sistem' to 'System' for consistency in English localization. --- app/Filament/Admin/Resources/Settings/SettingResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Filament/Admin/Resources/Settings/SettingResource.php b/app/Filament/Admin/Resources/Settings/SettingResource.php index 65b93a7..1496150 100644 --- a/app/Filament/Admin/Resources/Settings/SettingResource.php +++ b/app/Filament/Admin/Resources/Settings/SettingResource.php @@ -23,7 +23,7 @@ class SettingResource extends Resource protected static string|BackedEnum|null $navigationIcon = Heroicon::Cog6Tooth; - protected static UnitEnum|string|null $navigationGroup = 'Sistem'; + protected static UnitEnum|string|null $navigationGroup = 'System'; protected static ?int $navigationSort = 100;