88 Commits

Author SHA1 Message Date
Ümit Tunç 097c6719a1 refactor: remove secondary landing page sections to enforce a hero-only product layout 2026-05-11 11:17:18 +03:00
Ümit Tunç 1950e8734c feat: localize landing page content and update icon asset paths 2026-05-11 10:04:58 +03:00
Ümit Tunç 3512858304 feat: internationalize internship application status messages and add Swedish/German language support 2026-05-11 09:23:07 +03:00
Ümit Tunç ebcbc9842c feat: localize career page and add mandatory NDA/contract document upload requirements to application form 2026-05-11 09:21:57 +03:00
Ümit Tunç 81790642c3 feat: add NDA/contract file uploads and technical skill checkboxes to career applications 2026-05-11 09:18:25 +03:00
Ümit Tunç 7cbf872b05 refactor: replace mermaid diagram with static ik-flowchat image on career page 2026-05-11 09:13:12 +03:00
Ümit Tunç 6a7e977330 refactor: overhaul career page with updated UI, NDA information, and issue-based workflow visualization 2026-05-11 09:13:08 +03:00
Ümit Tunç 5f26e76143 feat: add application type field to career applications and implement status filtering in admin panel 2026-05-11 08:28:48 +03:00
Ümit Tunç 932bb89454 refactor: migrate internship page layout to tailwind css styling 2026-05-11 08:05:38 +03:00
Ümit Tunç 2fd3dbb63f refactor: update internship page layout with new application requirements and localization strings 2026-05-11 08:01:41 +03:00
Ümit Tunç 9a606991e3 feat: update internship page with remote work details and application requirements 2026-05-11 08:00:21 +03:00
Ümit Tunç a81f4fdf9e feat: add internship application page and update career menu dropdown 2026-05-11 07:54:27 +03:00
Ümit Tunç 1547d07cb3 docs: update README header image width to percentage-based scaling 2026-05-11 06:37:59 +03:00
Ümit Tunç c5f2b7fc43 feat: Implement AJAX form submission for career application with loading states, SweetAlert2 notifications, and server-side JSON responses. 2026-03-11 09:12:20 +03:00
Ümit Tunç 7af164f3d7 feat: Update career form custom input styles with revised dimensions, box-shadow, responsive width, and placeholder properties. 2026-03-11 08:57:41 +03:00
Truncgil Technology 8f7c5e2102 Merge pull request #32 from truncgil/technical-debt-and-performance
Technical debt and performance
2026-03-11 08:52:15 +03:00
Muhammet Güler 45440f2814 feat: kariyer formu, mobil offcanvas menu ve blog modulu iyilestirmeleri 2026-03-10 19:55:25 +03:00
Muhammet Güler 4924cb0dc8 feat: kariyer formu, mobil offcanvas menu ve blog modulu iyilestirmeleri 2026-03-10 18:50:23 +03:00
Truncgil Technology f4f601a492 Merge pull request #31 from truncgil/feature/image-optimization-and-security
Feature/image optimization and security
2026-02-19 16:58:03 +03:00
Muhammet Güler 42357c4dbb Refactor payment page layout and perform security cleanup 2026-02-18 11:53:39 +03:00
Muhammet Güler 452d05f4a1 Enhance online payment button design: Oval, Red, Animated 2026-02-17 10:57:53 +03:00
Muhammet Güler 477993ec83 Optimize images to WebP and enable lazy loading. Add security headers (HSTS, CSP, etc) for A+ SSL score. 2026-02-17 10:30:36 +03:00
Truncgil Technology 2daa0f5ddd Merge pull request #28 from truncgil/fix/blog-assistant-and-seo
Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi
2026-02-16 17:14:05 +03:00
Muhammet Güler 7101f7878a Fix: AI Blog Assistant retry logic and Subdomain routing for Akademi 2026-02-16 14:55:06 +03:00
Truncgil Technology a5847dcd58 Merge pull request #25 from truncgil/feature/online-payment
Fix payment form validation, styling and blog header visibility
2026-02-13 21:14:32 +03:00
Muhammet Güler 0fa8e4e435 Fix payment form validation, styling and blog header visibility 2026-02-13 12:45:17 +03:00
Truncgil Technology 8f85afd316 Merge pull request #24 from truncgil/feature/partners-and-bank-accounts
feat: add Partners and Bank Accounts pages via Pages module
2026-02-11 07:21:13 +03:00
Muhammet Güler bda19b4b0f feat: add Partners and Bank Accounts pages via Pages module 2026-02-10 19:02:23 +03:00
Truncgil Technology 7c65a4a592 Merge pull request #23 from truncgil/fix-and-ourlogos
Fix and ourlogos
2026-02-09 18:44:16 +03:00
Muhammet Güler b289cfd829 feat: re-enable image editor and aspect ratios for blog and page settings 2026-02-09 15:18:59 +03:00
Muhammet Güler 177ebff119 feat: Add Logos page, refactor Testimonials to RelationManager, and improve designs 2026-02-08 04:54:32 +03:00
Truncgil Technology 33c1ffe779 Merge pull request #21 from truncgil/musteri-gorusleri
Müşteri Görüşleri Sayfası & Genel Düzenlemeler
2026-02-04 19:25:29 +03:00
Muhammet Güler e4ca3b228b Müşteri Görüşleri Sayfası & Genel Düzenlemeler
* Yeni Sayfa: Kurumsal menüsü altına modern tasarımlı 'Müşteri Görüşleri' sayfası eklendi.\n* Düzeltme: Kitaplar sayfasındaki renk döngüsü hatası giderildi ve eksik Google Play linki eklendi.\n* Temizlik: Proje dizinindeki gereksiz bakım dosyaları silindi.
2026-02-04 17:32:38 +03:00
Truncgil Technology 2f9fb14bac Merge pull request #20 from truncgil/muhammetguler
Page errors on the "Our Books" page have been corrected.
2026-02-04 07:41:53 +03:00
Muhammet Güler 43bcca4a5e Page errors on the Our Books page have been corrected. 2026-02-04 00:30:43 +03:00
Muhammet Güler dbd267ca17 pr deneme 2026-02-03 14:20:36 +03:00
Muhammet Güler 74779ef2c8 Update PageSeeder 2026-02-03 13:15:43 +03:00
Muhammet Güler 4a73f7a3a0 Merge pull request #17 from truncgil/muhammetg
Add Books page and minor improvements
2026-02-03 03:36:48 +03:00
Muhammet Güler 03ff01f24d Add Books page and minor improvements 2026-02-03 02:31:54 +03:00
Muhammet Güler 90565412a4 Kitaplarımız sayfası eklendi 2026-02-02 12:51:46 +03:00
Ümit Tunç dc63563e7f refactor: remove highlight spans and simplify description paragraphs in the hero section. 2026-01-30 21:46:01 +03:00
Ümit Tunç 1400ec1ca7 Enhance Search Functionality in Site Translations DataGrid: Introduced a new helper function to extract search values from filters, improving the search capability within the DataGrid. Updated the search logic to utilize this helper, ensuring more effective filtering based on user input. This enhancement aims to provide a more intuitive and efficient user experience when managing site translations. 2026-01-10 23:20:23 +03:00
Ümit Tunç 7946876434 Enhance Site Translations DataGrid Functionality: Updated the DataGrid view to include the wire:ignore directive for improved Livewire compatibility. Adjusted the theme application logic to dynamically respond to changes in the document's class, ensuring a seamless user experience across different themes. Enabled remote operations for the DataGrid, enhancing data handling capabilities. 2026-01-10 23:20:17 +03:00
Ümit Tunç ca942c5140 Refactor Site Translations List Page and Enhance DataGrid Functionality: Updated the ListSiteTranslations page to improve session handling for view toggling between traditional table and DataGrid formats. Enhanced the getHeaderActions method for better readability and session management. Additionally, modified the render method to ensure the latest session value is utilized for rendering the appropriate view. Updated the DataGrid view to dynamically adjust the theme based on the current body class, improving user experience across different themes. 2026-01-10 22:59:58 +03:00
Ümit Tunç e80c94146d Implement Site Translations API and DataGrid View: Introduced a new API controller for managing site translations, including methods for listing, storing, updating, and deleting translations. Added a DataGrid view for the site translations page, allowing for enhanced user interaction and management of translations. Implemented session-based view toggling between traditional table and DataGrid formats, improving usability. Localization support has been integrated for all text elements, ensuring a seamless experience for users across different languages. 2026-01-10 21:54:56 +03:00
Ümit Tunç 2fdf187658 Implement GSAP Features in 'neler-yapariz-gsap' Template: Introduced a comprehensive set of GSAP animations and sections in the 'neler-yapariz-gsap' template, enhancing the visual dynamics of the page. This update includes new JavaScript and CSS files for various animations, such as flip, inertia, marquee, and scrolltrigger, along with a structured layout for improved user interaction. Additionally, localization support has been integrated for all text elements, ensuring a seamless experience for users across different languages. 2026-01-10 17:47:51 +03:00
Ümit Tunç 3ad69677d8 Add New GSAP Images and Update 'neler-yapariz' Template: Introduced new images for GSAP features, enhancing visual content in the 'neler-yapariz' template. Updated the template to include these images in the flip section, improving the overall aesthetic and user engagement. Additionally, made localization adjustments to ensure all text elements are translatable, further enhancing the user experience across different languages. 2026-01-10 17:12:35 +03:00
Ümit Tunç 6caa15b501 Update 'neler-yapariz' Navigation and CSS Styles: Adjusted the sub-navigation background positioning in the 'neler-yapariz' template to improve visual alignment. Updated the CSS to modify the transform property for the sub-navigation background, enhancing the overall layout and user experience. This change aims to create a more polished and cohesive design across the template. 2026-01-09 21:48:55 +03:00
Ümit Tunç 171132263b Update Toggle Styles in Additional CSS: Changed the background and border colors of the toggle ball in the 'neler-yapariz' template's additional CSS file to #ff812d for improved visual consistency with the overall design. This update enhances the user interface and aligns with recent styling changes. 2026-01-09 21:48:31 +03:00
Ümit Tunç 9e79ec28d9 Update 'neler-yapariz' Navigation Template and CSS Styles: Revamped the navigation structure in the 'neler-yapariz' template by replacing the SVG logo with an image and enhancing the sub-navigation layout for better user interaction. Additionally, updated CSS variables for improved color consistency and modified toggle slider styles to align with the new design. This update aims to enhance visual appeal and user experience across the site. 2026-01-09 21:40:01 +03:00
Ümit Tunç 3b852512f0 Refactor 'neler-yapariz' Template and Introduce New Sections: Updated the 'neler-yapariz' template to enhance its structure by adding new sections for footer, hero, inertia, marquee, and scrolltrigger. Introduced external CSS and JS files for improved styling and functionality. This update aims to enhance user experience through better organization and dynamic content integration. 2026-01-09 21:16:13 +03:00
Ümit Tunç b00ca5ea78 Add GSAP Layout and Animated Logo Component: Introduced a new layout file for GSAP integration, enhancing the site's animation capabilities. Added an animated logo component to improve visual engagement. Updated the 'neler-yapariz' template to utilize the new GSAP layout, ensuring a cohesive design and better user experience. 2026-01-09 21:03:54 +03:00
Ümit Tunç c73d448f47 Implement Dynamic Header and Footer Templates in Blog Controller: Enhanced the BlogController to prioritize page-specific header and footer templates based on the 'blog' page settings. If no specific templates are set, it falls back to the default active templates. Updated the blog index view to include a header template variable for better customization. Additionally, added redirects for legacy index URLs to improve user navigation. 2026-01-06 22:03:05 +03:00
Ümit Tunç 374d4a8d7d Enhance Translation Handling in HasTranslations Trait: Improved the translate method to ensure non-empty values are returned from the translations table and original field values. Added JSON handling for original values to support array-based translations. This update enhances localization support and ensures more reliable translation retrieval across different languages. 2026-01-06 21:42:33 +03:00
Ümit Tunç 3a2661cf95 Refactor Landing Page Section and Product Form: Removed required validation from several fields in the LandingPageSection schema to enhance flexibility. Updated the ProductForm to enforce required validation on the hero image field, ensuring essential data is captured for product management. Improved localization support by ensuring all labels utilize the translation function. 2026-01-06 21:36:13 +03:00
Ümit Tunç 36471b5965 Add External URL Field to Product Schema and Localization: Introduced an 'external_url' field in the ProductForm for enhanced product management. Updated the Product model to include this new field and created a migration to add it to the products table. Enhanced localization support by adding relevant translations in both English and Turkish language files. Updated the menu-item Blade component to utilize the external URL, allowing links to open in a new tab if provided, improving user experience. 2026-01-06 21:18:25 +03:00
Ümit Tunç 194e575619 Update Header and Add New Home Template: Enhanced the header layout by including a language selector for improved localization support. Introduced a new 'neler-yapariz' template that integrates various sections of the home page, ensuring a cohesive structure and better user experience. 2026-01-02 23:32:03 +03:00
Ümit Tunç a1d02de86b Update Landing Page Layout and Improve Responsiveness: Adjusted the layout of the landing page by modifying the width of various sections for better responsiveness. Enhanced the styling of the featured image section and added a new divider for visual appeal. Updated the FAQ section header to utilize localization for improved language support, ensuring a consistent user experience across different languages. 2026-01-02 22:58:29 +03:00
Ümit Tunç 1fc23e7bda Update Header Template in Landing Page Blade: Changed the header template from 'Demo29 Header' to 'Demo27 Header' in the landing page view, ensuring consistency with the updated design and layout standards. 2026-01-02 20:58:30 +03:00
Ümit Tunç 97092dd793 Add Landing Page Section and Update Product Schema: Introduced a new LandingPageSection class to manage the layout and fields for the product landing page. Updated the ProductForm to include this new section, enhancing the product management interface. Added migration for landing_page_data in the products table and implemented localization for new fields in English and Turkish language files. Created a new landing Blade view to render the landing page dynamically, improving user experience and modularity. 2026-01-02 20:58:18 +03:00
Ümit Tunç 89eb9f5c4e Refactor Featured Products Section in Menu Item Component: Updated the layout and styling of the featured products section in the menu-item Blade component for improved visual appeal and user experience. Enhanced the structure for better responsiveness and consistency in design. 2026-01-01 20:00:34 +03:00
Ümit Tunç 0ca75829bb frontend style fix 2026-01-01 16:25:27 +03:00
Ümit Tunç e8cdba4fd3 Refactor Product and Category Tables for Enhanced Localization: Updated the ProductCategoriesTable and ProductsTable to utilize the getStateUsing method for retrieving translated titles, improving localization support. Enhanced the search functionality to filter by translations, ensuring consistent display of titles across different languages. Additionally, modified the menu-item Blade component to include featured products, improving the layout and user experience. 2026-01-01 11:13:45 +03:00
Ümit Tunç 12441517f6 Add Custom Header and Footer Blade Selection: Implemented options for selecting custom Blade files for header and footer templates in the Page settings. Enhanced the PageController to render these templates dynamically, improving modularity and user experience. Updated relevant views to support this new functionality, ensuring better localization and flexibility in template management. 2026-01-01 10:54:38 +03:00
Ümit Tunç 10f2e9c9bb Refactor Product Form Localization: Updated the category options in the ProductForm to utilize the translation function for titles, enhancing localization support and ensuring consistent display of category names across different languages. 2026-01-01 09:58:31 +03:00
Ümit Tunç 5aa2a3d6e5 Enhance Settings and PageController Logic: Updated the SettingForm to include dynamic selection for header and footer templates based on active settings, improving modularity and user experience. Refactored PageController to retrieve active settings for rendering templates, ensuring fallback logic is streamlined and enhancing overall functionality. Updated navbar component to dynamically display social media links from settings, contributing to better localization and user engagement. 2026-01-01 09:55:16 +03:00
Ümit Tunç 3e4fc26e4b Remove Unused What We Do Section from Home Template: Commented out the inclusion of the 'what-we-do' component in the home template, streamlining the layout and improving overall organization. This change contributes to a cleaner design and better modularity. 2026-01-01 00:30:14 +03:00
Ümit Tunç 68b7907981 Refactor Header and Why Choose Us Templates: Updated the header template to dynamically retrieve social media links from settings, enhancing functionality and localization support. Modified button styles in the Why Choose Us section for a more consistent rounded appearance, improving overall design coherence. 2026-01-01 00:22:50 +03:00
Ümit Tunç d75434674f Add Dynamic Menu Item Component: Introduced a new Blade component for front-end menu items, enhancing the navigation structure with support for mega menus and nested children. Updated the header template to utilize this component, improving modularity and localization for menu titles. 2026-01-01 00:12:46 +03:00
Ümit Tunç cb0bbf87c7 Update Header and Home Templates: Modified the header template to dynamically retrieve contact information and social media links from settings, enhancing functionality and localization support. Updated the hero section background image for improved visual appeal. Simplified the services cards layout by removing unnecessary padding, contributing to a cleaner design. 2026-01-01 00:04:00 +03:00
Ümit Tunç 77a2713aec Implement Home Page Structure: Refactored the home template to include multiple new components for improved modularity and organization. Added sections for hero, services, solutions, testimonials, and contact information, enhancing the overall layout and user experience. Ensured all new components utilize localization for text content, maintaining consistency across languages. 2025-12-31 23:47:02 +03:00
Ümit Tunç 1fa194260e Refactor Localization in Templates: Updated various templates to utilize the translation function for titles and content, enhancing localization support. Commented out unused fallback logic in the PageController for header templates, streamlining the rendering process. Added new header and navbar components while removing the deprecated navbar file, improving the overall structure and maintainability of the layout. 2025-12-31 23:35:42 +03:00
Ümit Tunç 009d1cbb76 Update Footer Template: Refactored social media links to dynamically retrieve URLs from settings, enhancing the footer's functionality. Improved localization support by ensuring all links are properly formatted and accessible, contributing to a better user experience. 2025-12-31 22:27:10 +03:00
Ümit Tunç 2a1cdafbea Enhance EditSetting Page: Added a method to mutate form data before filling, allowing dynamic retrieval of virtual field values based on the selected type. Updated footer template to utilize localization for contact information and section titles, improving multilingual support and user experience. 2025-12-31 22:16:42 +03:00
Ümit Tunç b345c6f71c Refactor Footer Template Logic: Commented out the fallback logic for footer templates in the PageController to streamline rendering. Updated the site layout to check for non-empty rendered footer content, enhancing the footer display logic. Cleaned up the home template by removing unnecessary characters and ensuring proper formatting. 2025-12-31 22:08:10 +03:00
Ümit Tunç a2827c5d7a a 2025-12-31 22:00:45 +03:00
Ümit Tunç 194beaee1c iniit 2025-12-31 21:37:21 +03:00
Ümit Tunç cf855fdd70 Update Home Template: Translated key sections to Turkish, enhancing localization support. Added new content blocks for services offered, including Web Design and Mobile Design, with corresponding descriptions and links. Improved layout structure for better responsiveness and user experience. 2025-12-31 01:21:30 +03:00
Ümit Tunç 6b561896c6 Enhance SiteTranslationImporter: Added 'Key' label to the import column and simplified validation rules. Improved record resolution logic to handle empty keys and ensured translations are updated correctly during import. Updated CSS paths for assets in the home template for better resource management. 2025-12-31 01:01:41 +03:00
Ümit Tunç 922add1b6f Add Import and Export Functionality for Site Translations: Implemented SiteTranslationImporter and SiteTranslationExporter classes to facilitate the import and export of site translations. Enhanced the SiteTranslationsTable with import and export actions, allowing for efficient management of translation data. Updated the migration files to create necessary tables for handling imports and exports, improving the overall localization workflow in the admin panel. 2025-12-31 00:23:16 +03:00
Ümit Tunç 2c9dcac76c Refactor SettingForm Schema: Removed the image validation from the file input field in the SettingForm schema, ensuring that only specified file types are accepted for upload. This change enhances the flexibility of file handling in the settings management. 2025-12-31 00:01:39 +03:00
Ümit Tunç 064ec327ba Add Site Translation Management: Created SiteTranslation resource with pages for listing, creating, and editing translations. Implemented localization support for translation keys and values, enhancing the admin panel's usability for managing site translations. Introduced a new migration for the site_translations table and updated helper functions for translation retrieval. 2025-12-30 23:41:32 +03:00
Ümit Tunç f381caaa39 Implement Localization for Product and ProductCategory Resources: Refactored resource classes and forms to utilize Laravel's localization features, enhancing user experience by providing translated labels and sections. Added English and Turkish language support for product and category management, ensuring consistency across the admin panel. 2025-12-30 22:49:55 +03:00
Ümit Tunç 54944317b3 Add Language Support to Admin Panel: Implemented dynamic language menu items in the admin panel by retrieving active languages from the database. Enhanced user experience by allowing language selection directly from the user menu, improving localization capabilities. 2025-12-30 22:28:20 +03:00
Ümit Tunç f4cf16f93f Refactor Product Category Form and Product Form: Updated the options method to use \Illuminate\Support\Arr::first for retrieving category titles, ensuring better handling of localization. Added a fallback for category titles to display a default message when no title is available, improving user experience in form submissions. 2025-12-30 22:16:51 +03:00
Ümit Tunç a85e6eebe0 Implement Product and ProductCategory Resources: Created new Filament resources for managing products and product categories, including pages for listing, creating, and editing. Added schemas for forms and tables, integrated translation support, and established relationships between products and categories. Enhanced the navigation structure to include product and service links in the frontend menu, improving overall site organization and user experience. 2025-12-30 22:16:45 +03:00
Ümit Tunç a5a3248c69 Add Frontend Theme Implementation Rules: Introduced a new markdown file outlining the rules for converting static HTML templates in the public/html directory to Laravel Blade structure. This includes guidelines on source and target structure, component organization, code conversion rules, CMS integration, and automatic menu configuration, enhancing the development process for frontend themes. 2025-12-30 21:18:02 +03:00
Ümit Tunç 21ebbfc749 Update Dependencies and Add BlogCategoryPolicy: Upgraded the version of the 'anourvalar/eloquent-serialize' package to 1.3.5 and added a new BlogCategoryPolicy class to manage authorization for blog category actions. This enhances the application's security and maintainability by implementing specific access controls. 2025-12-30 20:53:31 +03:00
1430 changed files with 354747 additions and 8806 deletions
+92
View File
@@ -0,0 +1,92 @@
---
description: Frontend blok ve section mimarisi kuralları. Yeni blok eklerken veya sayfa yapısı kurarken bu kuralları uygula.
globs: resources/views/components/blocks/**/*.blade.php, app/Filament/**/*.php, resources/views/templates/**/*.blade.php
---
# Frontend Architecture Rules
Bu kurallar, projenin Blok (Block) ve Bölüm (Section) tabanlı mimarisini tanımlar. Sayfa oluştururken bu standartlara uyulmalıdır.
## 1. Blok (Block) Yapısı
Sistem, `resources/views/components/blocks/` altında bulunan hazır Blade bileşenleri üzerine kuruludur.
### Mevcut Bloklar ve Kullanımları
- **Hero:** Ana başlık, büyük görsel ve CTA butonları.
- **Features:** İkonlu özellik kutuları grid yapısı.
- **Stats:** Sayısal istatistikler ve sayaçlar.
- **Testimonials:** Müşteri yorumları ve referanslar.
- **Pricing:** Fiyatlandırma tabloları.
- **CTA:** Aksiyon çağrısı bantları.
- **Gallery:** Proje veya görsel galerisi.
- **Team:** Ekip üyeleri listesi.
- **Contact:** İletişim bilgileri ve formu.
### Blok Geliştirme Kuralları
1. **Component Yapısı:** Her blok `resources/views/components/blocks/{type}.blade.php` konumunda olmalıdır.
2. **Veri Alımı:** Bloklar veriyi sadece `$data` prop'u üzerinden almalıdır:
```blade
@props(['data' => []])
{{-- Erişim: $data['title'] --}}
```
3. **Null Check:** Tüm veri alanları için `isset` veya `??` kontrolü yapılmalıdır.
```blade
{{ $data['title'] ?? '' }}
@if(isset($data['image'])) ... @endif
```
## 2. Section Builder ve Veri Yapısı
Admin panelinde "Section Builder" kullanılarak veriler JSON formatında tutulur.
### Veri Formatı (JSON)
Veriler `sections` sütununda veya `parsed_sections` attribute'unda şu formatta işlenir:
```json
[
{
"type": "hero",
"data": {
"title": "Başlık",
"subtitle": "Alt Başlık",
"background_image": "assets/img/bg.jpg"
}
}
]
```
### Key İsimlendirme Kuralları (ÖNEMLİ)
Veri anahtarları (keys) her zaman **İngilizce** ve **snake_case** olmalıdır:
- ✅ `title`, `sub_title`, `button_text`, `image_url`
- ❌ `baslik`, `altBaslik`, `butonYazisi`, `resim`
### Önerilen Value Tipleri
Admin panelinde alan tanımlarken doğru tipleri seçin:
- **Kısa Metinler:** `Text` (title, badge)
- **Uzun Metinler:** `Textarea` (description)
- **Formatlı İçerik:** `Rich Text` veya `HTML` (content)
- **Görseller:** `Image` (background_image, icon)
- **Linkler:** `URL` (button_url)
## 3. Blade Entegrasyonu
Sayfa şablonlarında (örneğin `home.blade.php`) blokları render ederken dinamik yapı kullanılmalıdır:
```blade
@foreach($page->parsed_sections as $section)
@if(view()->exists("components.blocks.{$section['type']}"))
<x-dynamic-component
:component="'blocks.'.$section['type']"
:data="$section['data']"
/>
@endif
@endforeach
```
## 4. Stil Özelleştirme
Bloklar, dışarıdan gelen stil parametrelerini desteklemelidir:
- `bg_class`: Arka plan rengi (örn: `!bg-[#f0f0f8]`)
- `column_class`: Grid yapısı (örn: `md:w-4/12`)
- `padding_class`: Boşluklar (örn: `py-14`)
Bu sınıflar Blade içinde ana kapsayıcıya (wrapper) eklenmelidir.
+138
View File
@@ -0,0 +1,138 @@
---
description: Public HTML şablonlarını Laravel Blade yapısına dönüştürme kuralları
globs: resources/views/**/*.blade.php, public/html/**/*.html
---
# Frontend Theme Implementation Rules
Bu kurallar, `public/html` dizinindeki statik HTML şablonlarının Laravel Blade yapısına dönüştürülmesi sırasında geçerlidir.
## 1. Kaynak ve Hedef Yapısı
- **Kaynak (Source):** `public/html/` dizinindeki `.html` dosyaları referans alınır.
- **Hedef (Target):** `resources/views/front/` dizini altında Blade dosyaları oluşturulur.
- **Assets (Varlıklar):** Statik dosyalar `public/html/assets` altındadır. Blade içinde erişim `asset('html/assets/...')` şeklinde olmalıdır.
## 2. Bileşen (Component) Yapısı
Yeni UI bileşenleri oluştururken aşağıdaki isimlendirme ve klasör yapısını kullan:
- **Layout:** `resources/views/layouts/front.blade.php` (Master layout)
- **Header:** `resources/views/components/front/header.blade.php` -> `<x-front.header />`
- **Footer:** `resources/views/components/front/footer.blade.php` -> `<x-front.footer />`
- **Page Sections:** `resources/views/components/front/sections/` altında:
- Hero/Slider -> `<x-front.sections.hero />`
- Features -> `<x-front.sections.features />`
- About -> `<x-front.sections.about />`
## 3. Kod Dönüştürme Kuralları
### A. CSS ve Sınıflar
- **ÖNEMLİ:** Şablondaki `!` önekli Tailwind sınıflarını (örn: `!bg-[#f0f0f8]`, `!absolute`) KESİNLİKLE KORU. Bunlar temanın çalışması için kritiktir.
- HTML'deki `class="..."` yapılarını birebir kopyala, Tailwind sınıflarını sadeleştirme veya değiştirme.
### B. Asset Bağlantıları
HTML içindeki:
```html
<link rel="stylesheet" href="assets/css/plugins.css">
<img src="assets/img/logo.png">
```
Blade içinde şu şekilde olmalı:
```blade
<link rel="stylesheet" href="{{ asset('html/assets/css/plugins.css') }}">
<img src="{{ asset('html/assets/img/logo.png') }}">
```
### C. Dinamik İçerik
- Metinleri `{{ __('theme.key') }}` şeklinde lokalizasyon fonksiyonu ile sar.
- Tekrarlayan elemanları (örn: slider itemları, hizmet kutuları) `@foreach` döngüsü içine al.
## 4. Layout Şablonu (Örnek)
`front.blade.php` layout dosyası şu yapıda olmalıdır:
```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', config('app.name'))</title>
<!-- CSS Assets from public/html/index.html head -->
@stack('styles')
</head>
<body class="!bg-[#f0f0f8]">
<x-front.header />
<main class="grow shrink-0">
@yield('content')
</main>
<x-front.footer />
<!-- JS Assets from public/html/index.html bottom -->
@stack('scripts')
</body>
</html>
```
## 5. CMS Entegrasyonu (Dinamik Şablon Seçimi)
Kullanıcı, bir prompt ile belirli bir section, header veya footer şablonunun kullanılmasını istediğinde (örn: "Header V1 ve About Section 3 kullan"), aşağıdaki stratejiyi izle:
1. **Veritabanı Araması:** İlgili şablonun veritabanında (genellikle `sections` veya `templates` tablosunda) var olduğunu varsay.
2. **Seeder/Migration Oluşturma:** Kullanıcının istediği yapıyı oluşturmak için bir Laravel Seeder veya Migration kodu üret. Bu kod:
- İlgili şablonu (title veya slug ile) bulmalı.
- Sayfa veya layout ile ilişkilendirmeli.
**Örnek Senaryo:** "Ana sayfada 'Hero Slider V2' ve 'Services Grid' kullan."
**Üretilecek Kod (Örnek):**
```php
// DatabaseSeeder.php veya özel bir seeder içinde
$homePage = Page::firstOrCreate(['slug' => 'home'], ['title' => 'Ana Sayfa']);
// Şablonları bul
$heroSection = Section::where('title', 'like', '%Hero Slider V2%')->first();
$servicesSection = Section::where('title', 'like', '%Services Grid%')->first();
// İlişkilendir (Proje yapısına göre attach veya update)
if ($heroSection) {
// Örnek ilişkilendirme mantığı
$homePage->sections()->attach($heroSection->id, ['order' => 1]);
}
```
## 6. Otomatik Menü ve Sayfa Yapılandırması
Kullanıcı "Şu yapıda bir menü tasarla" dediğinde (örn: "Ana Sayfa, Kurumsal > Hakkımızda, İletişim"):
1. **Page Modeli:** İstenen sayfaların veritabanında var olup olmadığını kontrol eden ve yoksa oluşturan bir kod bloğu hazırla.
2. **Menu Modeli:** Sayfaları hiyerarşik olarak menü yapısına ekleyen bir kod bloğu hazırla.
3. **Çıktı:** Bu işlemleri gerçekleştirecek bir `php artisan tinker` komutu veya `MenuSeeder` sınıfı sun.
**Örnek Prompt:** "Menü: Ana Sayfa, Kurumsal (Hakkımızda, Vizyon), İletişim"
**Beklenen Aksiyon:**
```php
// 1. Sayfaları Oluştur
$pages = [
'Ana Sayfa' => '/',
'Kurumsal' => '#',
'Hakkımızda' => '/kurumsal/hakkimizda',
'Vizyon' => '/kurumsal/vizyon',
'İletişim' => '/iletisim'
];
foreach ($pages as $title => $url) {
Page::firstOrCreate(['title' => $title], ['slug' => Str::slug($title)]);
}
// 2. Menü Yapısını Kur
$mainMenu = Menu::firstOrCreate(['name' => 'Main Menu']);
// ... Menü elemanlarını ekleyen kod ...
```
## 7. Analiz Stratejisi
Bir sayfayı dönüştürürken:
1. Önce `public/html` altındaki ilgili `.html` dosyasını oku.
2. `header` ve `footer` kısımlarını ayır.
3. `main` içeriğindeki her bir `section` veya mantıksal bloğu ayrı bir Blade component olarak çıkar.
+1 -1
View File
@@ -1,4 +1,4 @@
<img width="1920" height="518" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" /> <img width="50%" alt="citrus-yatay-dark" src="https://github.com/user-attachments/assets/e502ad1c-9051-47f7-ae00-869570a5cbea" />
# Citrus Platform # Citrus Platform
@@ -0,0 +1,118 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\BlogTopic;
use App\Models\Blog; // Ensure this model exists and has 'status' column
use App\Models\User;
use App\Services\AiBlogService;
use Illuminate\Support\Str;
use Carbon\Carbon;
class GenerateBlogAssistant extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'blog:generate-assistant {--topic= : Specific topic to generate}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate a new AI blog post draft based on active topics.';
/**
* Execute the console command.
*/
public function handle(AiBlogService $aiService)
{
\Illuminate\Support\Facades\Log::info('GenerateBlogAssistant triggered. Args: ' . json_encode($_SERVER['argv'] ?? []));
$this->info('Starting AI Blog Assistant...');
// 1. Select Topic
$topicName = $this->option('topic');
$topic = null;
if ($topicName) {
$topic = BlogTopic::where('name', $topicName)->first();
if (!$topic) {
$this->error("Topic '{$topicName}' not found.");
return 1;
}
} else {
// Find oldest used active topic
$topic = BlogTopic::active()
->orderBy('last_used_at', 'asc') // Nulls first usually, or old dates
->first();
}
if (!$topic) {
$this->warn('No active topics found. Please add topics to blog_topics table.');
return 0;
}
$this->info("Selected Topic: {$topic->name}");
// 2. Generate Content
$this->info('Generating content with Gemini API...');
try {
$contentData = $aiService->generateContent($topic->name, $topic->context);
} catch (\Exception $e) {
$this->error('Content generation failed: ' . $e->getMessage());
return 1;
}
if (empty($contentData) || !isset($contentData['title']) || !isset($contentData['content'])) {
$this->error('Valid content not returned from AI Service.');
return 1;
}
$this->info('Content generated successfully.');
// 3. Generate/Fetch Image
$this->info('Fetching cover image...');
$imagePath = null;
if (isset($contentData['image_prompt'])) {
$imagePath = $aiService->fetchAndSaveImage($topic->name, $contentData['image_title'] ?? $contentData['image_prompt']); // Pollinations uses prompt
}
if (!$imagePath) {
$this->warn('Could not fetch image, proceeding without cover image.');
} else {
$this->info("Image saved to: {$imagePath}");
}
// 4. Create Draft Blog Post
$author = User::first(); // Default to first user (Admin)
$blog = new Blog();
$blog->title = $contentData['title'];
$blog->slug = Str::slug($contentData['title']);
$blog->content = $contentData['content'];
$blog->excerpt = $contentData['excerpt'] ?? Str::limit(strip_tags($contentData['content']), 150);
$blog->meta_title = $contentData['meta_title'] ?? $contentData['title'];
$blog->meta_description = $contentData['meta_description'] ?? $blog->excerpt;
$blog->status = 'draft'; // Verification required
$blog->author_id = $author ? $author->id : 1;
$blog->featured_image = $imagePath;
$blog->published_at = null; // Draft
$blog->tags = $contentData['tags'] ?? [];
// category_id? We might need to guess or default.
// For now leave null or set default if exists.
$blog->save();
// 5. Update Topic Usage
$topic->last_used_at = Carbon::now();
$topic->save();
$this->info("Blog post '{$blog->title}' created as DRAFT (ID: {$blog->id}).");
return 0;
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ class ImportHtmlTemplates extends Command
'span' => 'text.content', 'span' => 'text.content',
'address' => 'text.address', 'address' => 'text.address',
'button' => 'text.button', 'button' => 'text.button',
'a' => 'url.link', 'a' => 'text.link_text',
'li' => 'text.list_item', 'li' => 'text.list_item',
'label' => 'text.label', 'label' => 'text.label',
]; ];
@@ -260,7 +260,7 @@ class ImportHtmlTemplates extends Command
$links = $xpath->query('.//a', $node); $links = $xpath->query('.//a', $node);
foreach ($links as $link) { foreach ($links as $link) {
$href = $link->getAttribute('href'); $href = $link->getAttribute('href');
if (empty($href) || str_starts_with($href, '#') || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue; if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:') || str_starts_with($href, '{')) continue;
$fixedHref = $this->fixAssetPath($href); $fixedHref = $this->fixAssetPath($href);
if (str_ends_with($fixedHref, '.html')) { if (str_ends_with($fixedHref, '.html')) {
@@ -268,7 +268,7 @@ class ImportHtmlTemplates extends Command
if($fixedHref == 'index') $fixedHref = '/'; if($fixedHref == 'index') $fixedHref = '/';
} }
$key = $this->generateKey('url.link', $counts); $key = $this->generateKey('text.link_url', $counts);
$data[$key] = $fixedHref; $data[$key] = $fixedHref;
$link->setAttribute('href', "{" . $key . "}"); $link->setAttribute('href', "{" . $key . "}");
} }
@@ -21,7 +21,7 @@ class BlogCategoryResource extends Resource
{ {
protected static ?string $model = BlogCategory::class; protected static ?string $model = BlogCategory::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-tag';
protected static UnitEnum|string|null $navigationGroup = 'Blog'; protected static UnitEnum|string|null $navigationGroup = 'Blog';
@@ -20,7 +20,7 @@ class BlogResource extends Resource
{ {
protected static ?string $model = Blog::class; protected static ?string $model = Blog::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedDocumentText; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-text';
public static function getNavigationLabel(): string public static function getNavigationLabel(): string
{ {
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications;
use App\Filament\Admin\Resources\CareerApplications\Pages\CreateCareerApplication;
use App\Filament\Admin\Resources\CareerApplications\Pages\EditCareerApplication;
use App\Filament\Admin\Resources\CareerApplications\Pages\ListCareerApplications;
use App\Filament\Admin\Resources\CareerApplications\Schemas\CareerApplicationForm;
use App\Filament\Admin\Resources\CareerApplications\Tables\CareerApplicationsTable;
use App\Models\CareerApplication;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
class CareerApplicationResource extends Resource
{
protected static ?string $model = CareerApplication::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-user-group';
public static function getNavigationLabel(): string
{
return __('career.navigation_label');
}
public static function getModelLabel(): string
{
return __('career.model_label');
}
public static function getPluralModelLabel(): string
{
return __('career.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return CareerApplicationForm::configure($schema);
}
public static function table(Table $table): Table
{
return CareerApplicationsTable::configure($table);
}
public static function getPages(): array
{
return [
'index' => ListCareerApplications::route('/'),
'create' => CreateCareerApplication::route('/create'),
'edit' => EditCareerApplication::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateCareerApplication extends CreateRecord
{
protected static string $resource = CareerApplicationResource::class;
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditCareerApplication extends EditRecord
{
protected static string $resource = CareerApplicationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications\Pages;
use App\Filament\Admin\Resources\CareerApplications\CareerApplicationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListCareerApplications extends ListRecords
{
protected static string $resource = CareerApplicationResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,55 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications\Schemas;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Schema;
class CareerApplicationForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
TextInput::make('name')
->label(__('career.name'))
->required()
->disabled(),
TextInput::make('email')
->label(__('career.email'))
->email()
->required()
->disabled(),
TextInput::make('phone')
->label(__('career.phone'))
->disabled(),
FileUpload::make('cv_path')
->label(__('career.cv'))
->disk('public')
->directory('cvs')
->required()
->disabled(),
Textarea::make('message')
->label(__('career.message'))
->disabled()
->columnSpanFull(),
Select::make('status')
->label(__('career.status'))
->options([
'pending' => 'Pending',
'reviewed' => 'Reviewed',
'rejected' => 'Rejected',
'accepted' => 'Accepted',
])
->required(),
]);
}
}
@@ -0,0 +1,115 @@
<?php
namespace App\Filament\Admin\Resources\CareerApplications\Tables;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Support\Facades\Storage;
class CareerApplicationsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(__('career.name'))
->searchable()
->sortable(),
TextColumn::make('email')
->label(__('career.email'))
->searchable()
->sortable(),
TextColumn::make('phone')
->label(__('career.phone'))
->searchable(),
TextColumn::make('type')
->label(__('career.type'))
->badge()
->color(fn (string $state): string => match ($state) {
'job' => 'success',
'internship' => 'warning',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => __("career.{$state}")),
TextColumn::make('status')
->label(__('career.status'))
->badge()
->color(fn (string $state): string => match ($state) {
'pending' => 'gray',
'reviewed' => 'info',
'rejected' => 'danger',
'accepted' => 'success',
default => 'gray',
}),
TextColumn::make('git_knowledge')
->label(__('career.git_knowledge'))
->badge()
->color(fn ($state) => $state ? 'success' : 'danger')
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
TextColumn::make('ai_usage')
->label(__('career.ai_usage'))
->badge()
->color(fn ($state) => $state ? 'success' : 'danger')
->formatStateUsing(fn ($state) => $state ? 'Evet' : 'Hayır'),
TextColumn::make('created_at')
->label(__('career.created_at'))
->dateTime('d.m.Y H:i')
->sortable(),
])
->filters([
SelectFilter::make('status')
->label(__('career.status'))
->options([
'pending' => 'Pending',
'reviewed' => 'Reviewed',
'rejected' => 'Rejected',
'accepted' => 'Accepted',
]),
SelectFilter::make('type')
->label(__('career.type'))
->options([
'job' => __('career.job'),
'internship' => __('career.internship'),
]),
])
->actions([
Action::make('download_cv')
->label(__('career.download_cv'))
->icon('heroicon-o-arrow-down-tray')
->url(fn ($record) => Storage::disk('public')->url($record->cv_path))
->openUrlInNewTab(),
Action::make('download_nda')
->label(__('career.nda'))
->icon('heroicon-o-shield-check')
->url(fn ($record) => $record->nda_path ? Storage::disk('public')->url($record->nda_path) : null)
->visible(fn ($record) => $record->nda_path !== null)
->openUrlInNewTab(),
Action::make('download_contract')
->label(__('career.contract'))
->icon('heroicon-o-document-text')
->url(fn ($record) => $record->contract_path ? Storage::disk('public')->url($record->contract_path) : null)
->visible(fn ($record) => $record->contract_path !== null)
->openUrlInNewTab(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}
}
@@ -51,7 +51,7 @@ class PageResource extends Resource
public static function getRelations(): array public static function getRelations(): array
{ {
return [ return [
// RelationManagers\TestimonialsRelationManager::class,
]; ];
} }
@@ -0,0 +1,122 @@
<?php
namespace App\Filament\Admin\Resources\Pages\RelationManagers;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Schema;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class TestimonialsRelationManager extends RelationManager
{
protected static string $relationship = 'testimonials';
protected static ?string $title = 'Müşteri Görüşleri';
protected static ?string $modelLabel = 'Müşteri Görüşü';
protected static ?string $pluralModelLabel = 'Müşteri Görüşleri';
protected static ?string $recordTitleAttribute = 'name';
public static function canViewForRecord(\Illuminate\Database\Eloquent\Model $ownerRecord, string $pageClass): bool
{
// Sadece Müşteri Görüşleri sayfası veya testimonials şablonu kullananlar için göster
return $ownerRecord->slug === 'musteri-gorusleri' ||
$ownerRecord->template === 'corporate.testimonials' ||
$ownerRecord->is_homepage;
}
public function form(Schema $schema): Schema
{
return $schema
->schema([
TextInput::make('name')
->label('Müşteri Adı')
->required()
->maxLength(255),
TextInput::make('position')
->label('Şirket / Ünvan')
->maxLength(255),
Textarea::make('content')
->label('Görüş / Yorum')
->required()
->columnSpanFull(),
Select::make('rating')
->label('Puan')
->options([
1 => '1 Yıldız',
2 => '2 Yıldız',
3 => '3 Yıldız',
4 => '4 Yıldız',
5 => '5 Yıldız',
])
->default(5)
->required(),
FileUpload::make('avatar')
->label('Profil Resmi')
->image()
->directory('testimonials'),
Toggle::make('is_active')
->label('Aktif')
->default(true),
TextInput::make('sort_order')
->label('Sıralama')
->numeric()
->default(0),
]);
}
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('name')
->columns([
Tables\Columns\TextColumn::make('name')
->label('Ad')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('position')
->label('Ünvan')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('rating')
->label('Puan')
->sortable(),
Tables\Columns\IconColumn::make('is_active')
->label('Aktif')
->boolean()
->sortable(),
Tables\Columns\TextColumn::make('sort_order')
->label('Sıra')
->sortable(),
])
->filters([
//
])
->headerActions([
CreateAction::make(),
])
->actions([
EditAction::make(),
DeleteAction::make(),
])
->bulkActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -9,6 +9,9 @@ use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionBuilderSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection; use App\Filament\Admin\Resources\Pages\Schemas\Sections\SectionTemplatesSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection; use App\Filament\Admin\Resources\Pages\Schemas\Sections\SeoSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection; use App\Filament\Admin\Resources\Pages\Schemas\Sections\TranslationsSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\LogosSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\PartnersSection;
use App\Filament\Admin\Resources\Pages\Schemas\Sections\BankAccountsSection;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
@@ -49,6 +52,11 @@ class PageForm
SectionTemplatesSection::make(), SectionTemplatesSection::make(),
FooterTemplateSection::make(), FooterTemplateSection::make(),
// Full Width - Logos (Only for Logolarımız template)
LogosSection::make(),
PartnersSection::make(),
BankAccountsSection::make(),
// Full Width - Translations // Full Width - Translations
TranslationsSection::make(), TranslationsSection::make(),
]); ]);
@@ -0,0 +1,69 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class BankAccountsSection
{
public static function make(): Section
{
return Section::make(__('Banka Hesapları'))
->description(__('Bu sayfada gösterilecek banka hesap bilgilerini buradan yönetebilirsiniz.'))
->schema([
Repeater::make('data.bank_accounts')
->label('Hesaplar')
->schema([
TextInput::make('bank_name')
->label('Banka Adı')
->placeholder('Örn: Ziraat Bankası')
->required(),
TextInput::make('branch_name')
->label('Şube Adı / Kodu')
->placeholder('Örn: Konak Şubesi / 1234'),
TextInput::make('account_holder')
->label('Hesap Sahibi')
->default('Trunçgil Teknoloji')
->required(),
TextInput::make('iban')
->label('IBAN')
->placeholder('TR00 0000 0000 0000 0000 0000 00')
->required(),
Select::make('currency')
->label('Para Birimi')
->options([
'TRY' => 'Türk Lirası (₺)',
'USD' => 'Amerikan Doları ($)',
'EUR' => 'Euro (€)',
'GBP' => 'Sterlin (£)',
])
->default('TRY')
->required(),
FileUpload::make('logo')
->label('Banka Logosu')
->image()
->disk('public')
->directory('banks')
->visibility('public'),
])
->grid(3)
->columnSpanFull()
->reorderableWithButtons()
->collapsible()
->cloneable(),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.bank-accounts')
->columnSpanFull()
->collapsible();
}
}
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Facades\File;
class FooterTemplateSection class FooterTemplateSection
{ {
public static function make(): Section public static function make(): Section
@@ -17,9 +19,35 @@ class FooterTemplateSection
return Section::make(__('pages.form_section_footer_template')) return Section::make(__('pages.form_section_footer_template'))
->description(__('pages.form_section_footer_template_desc')) ->description(__('pages.form_section_footer_template_desc'))
->schema([ ->schema([
Select::make('custom_footer_blade')
->label('Custom Footer Blade')
->options(function () {
$files = File::glob(resource_path('views/partials/*.blade.php'));
$options = [];
foreach ($files as $file) {
$name = basename($file, '.blade.php');
$options["partials.{$name}"] = "Partial: {$name}";
}
// Add other directories if needed
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
foreach ($componentFiles as $file) {
$name = basename($file, '.blade.php');
$options["components.front.{$name}"] = "Component: {$name}";
}
return $options;
})
->searchable()
->preload()
->helperText('Select a physical Blade file to use as footer instead of a database template.')
->live()
->columnSpanFull(),
Select::make('footer_template_id') Select::make('footer_template_id')
->label(__('pages.footer_template_field')) ->label(__('pages.footer_template_field'))
->options(FooterTemplate::where('is_active', true)->pluck('title', 'id')) ->options(FooterTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => empty($get('custom_footer_blade')))
->searchable() ->searchable()
->live() ->live()
->afterStateUpdated(function ($state, Set $set, Get $get) { ->afterStateUpdated(function ($state, Set $set, Get $get) {
@@ -10,6 +10,8 @@ use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\Utilities\Set;
use Illuminate\Support\Facades\File;
class HeaderTemplateSection class HeaderTemplateSection
{ {
public static function make(): Section public static function make(): Section
@@ -17,9 +19,35 @@ class HeaderTemplateSection
return Section::make(__('pages.form_section_header_template')) return Section::make(__('pages.form_section_header_template'))
->description(__('pages.form_section_header_template_desc')) ->description(__('pages.form_section_header_template_desc'))
->schema([ ->schema([
Select::make('custom_header_blade')
->label('Custom Header Blade')
->options(function () {
$files = File::glob(resource_path('views/partials/*.blade.php'));
$options = [];
foreach ($files as $file) {
$name = basename($file, '.blade.php');
$options["partials.{$name}"] = "Partial: {$name}";
}
// Add other directories if needed
$componentFiles = File::glob(resource_path('views/components/front/*.blade.php'));
foreach ($componentFiles as $file) {
$name = basename($file, '.blade.php');
$options["components.front.{$name}"] = "Component: {$name}";
}
return $options;
})
->searchable()
->preload()
->helperText('Select a physical Blade file to use as header instead of a database template.')
->live()
->columnSpanFull(),
Select::make('header_template_id') Select::make('header_template_id')
->label(__('pages.header_template_field')) ->label(__('pages.header_template_field'))
->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id')) ->options(HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => empty($get('custom_header_blade')))
->searchable() ->searchable()
->live() ->live()
->afterStateUpdated(function ($state, Set $set, Get $get) { ->afterStateUpdated(function ($state, Set $set, Get $get) {
@@ -0,0 +1,36 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class LogosSection
{
public static function make(): Section
{
return Section::make(__('Logolar'))
->description(__('Bu sayfada gösterilecek logoları buradan yönetebilirsiniz.'))
->schema([
Grid::make(3)
->schema([
FileUpload::make('data.logo_1')
->label(__('Logo 1'))
->disk('public'),
FileUpload::make('data.logo_2')
->label(__('Logo 2'))
->disk('public'),
FileUpload::make('data.logo_3')
->label(__('Logo 3'))
->disk('public'),
]),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.logos')
->columnSpanFull()
->collapsible();
}
}
@@ -69,6 +69,12 @@ class PageSettingsSection
'landing' => __('pages.template_landing'), 'landing' => __('pages.template_landing'),
'blog' => __('pages.template_blog'), 'blog' => __('pages.template_blog'),
'contact' => __('pages.template_contact'), 'contact' => __('pages.template_contact'),
'home' => 'Home',
'corporate.testimonials' => 'Müşteri Görüşleri (Kurumsal)',
'corporate.logos' => 'Logolarımız (Kurumsal)',
'corporate.partners' => 'Çözüm Ortaklarımız (Kurumsal)',
'corporate.bank-accounts' => 'Banka Bilgilerimiz (Kurumsal)',
'corporate.online-payment' => 'Online Ödeme (Kurumsal)',
]) ])
->default('default') ->default('default')
->columnSpanFull(), ->columnSpanFull(),
@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
class PartnersSection
{
public static function make(): Section
{
return Section::make(__('Çözüm Ortakları'))
->description(__('Bu sayfada gösterilecek çözüm ortaklarını buradan yönetebilirsiniz.'))
->schema([
Repeater::make('data.partners')
->label('Partnerler')
->schema([
FileUpload::make('logo')
->label('Logo')
->image()
->disk('public')
->directory('partners')
->visibility('public')
->required()
->columnSpanFull(),
TextInput::make('url')
->label('Website URL')
->url()
->placeholder('https://example.com')
->columnSpanFull(),
])
->grid(4)
->columnSpanFull()
->reorderableWithButtons()
->collapsible()
->cloneable(),
])
->visible(fn (Get $get): bool => $get('template') === 'corporate.partners')
->columnSpanFull()
->collapsible();
}
}
@@ -233,7 +233,7 @@ class SectionBuilderSection
->image() ->image()
->disk('public') ->disk('public')
->directory('pages/sections') ->directory('pages/sections')
->imageEditor() ->directory('pages/sections')
->live() ->live()
->afterStateUpdated(fn ($state, callable $set) => $set('value', $state)) ->afterStateUpdated(fn ($state, callable $set) => $set('value', $state))
->afterStateHydrated(function ($component, $state, Get $get) { ->afterStateHydrated(function ($component, $state, Get $get) {
@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProductCategory extends CreateRecord
{
protected static string $resource = ProductCategoryResource::class;
protected function afterCreate(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditProductCategory extends EditRecord
{
protected static string $resource = ProductCategoryResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function afterSave(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Pages;
use App\Filament\Admin\Resources\ProductCategories\ProductCategoryResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProductCategories extends ListRecords
{
protected static string $resource = ProductCategoryResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories;
use App\Filament\Admin\Resources\ProductCategories\Pages\CreateProductCategory;
use App\Filament\Admin\Resources\ProductCategories\Pages\EditProductCategory;
use App\Filament\Admin\Resources\ProductCategories\Pages\ListProductCategories;
use App\Filament\Admin\Resources\ProductCategories\Schemas\ProductCategoryForm;
use App\Filament\Admin\Resources\ProductCategories\Tables\ProductCategoriesTable;
use App\Models\ProductCategory;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class ProductCategoryResource extends Resource
{
protected static ?string $model = ProductCategory::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedTag;
public static function getNavigationGroup(): ?string
{
return __('product_categories.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('product_categories.menu_label');
}
public static function getModelLabel(): string
{
return __('product_categories.model_label');
}
public static function getPluralModelLabel(): string
{
return __('product_categories.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return ProductCategoryForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProductCategoriesTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProductCategories::route('/'),
'create' => CreateProductCategory::route('/create'),
'edit' => EditProductCategory::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,57 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Schemas;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Models\ProductCategory;
class ProductCategoryForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Section::make(__('product_categories.general_section'))
->schema([
TextInput::make('slug')
->label(__('product_categories.slug'))
->required()
->unique(ignoreRecord: true),
Select::make('parent_id')
->label(__('product_categories.parent_category'))
->options(function () {
return ProductCategory::all()->mapWithKeys(function ($category) {
$title = is_array($category->title)
? ($category->title[app()->getLocale()] ?? \Illuminate\Support\Arr::first($category->title))
: $category->title;
return [$category->id => $title ?? ('Category #' . $category->id)];
});
})
->searchable(),
TextInput::make('sort_order')
->label(__('product_categories.sort_order'))
->numeric()
->default(0),
Toggle::make('is_active')
->label(__('product_categories.is_active'))
->default(true),
])->columns(2),
Section::make(__('product_categories.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('product_categories.title'),
'required' => true,
],
]),
]),
]);
}
}
@@ -0,0 +1,59 @@
<?php
namespace App\Filament\Admin\Resources\ProductCategories\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\BulkActionGroup;
class ProductCategoriesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('title')
->label(__('product_categories.title'))
->getStateUsing(fn ($record) => $record->translate('title'))
->searchable(query: function ($query, $search) {
return $query->whereHas('translations', function ($q) use ($search) {
$q->where('field_name', 'title')
->where('field_value', 'like', "%{$search}%")
->where('language_code', app()->getLocale());
});
}),
TextColumn::make('slug')
->label(__('product_categories.slug'))
->searchable(),
TextColumn::make('parent.title')
->getStateUsing(fn ($record) => $record->parent?->translate('title'))
->label(__('product_categories.parent')),
TextColumn::make('sort_order')
->label(__('product_categories.sort_order'))
->sortable(),
ToggleColumn::make('is_active')
->label(__('product_categories.is_active')),
TextColumn::make('created_at')
->label(__('product_categories.created_at'))
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Resources\Pages\CreateRecord;
class CreateProduct extends CreateRecord
{
protected static string $resource = ProductResource::class;
protected function afterCreate(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditProduct extends EditRecord
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
protected function mutateFormDataBeforeFill(array $data): array
{
return array_merge($data, TranslationTabs::fillFromRecord($this->record));
}
protected function afterSave(): void
{
$data = $this->form->getState();
TranslationTabs::saveTranslations($this->record, $data);
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Admin\Resources\Products\Pages;
use App\Filament\Admin\Resources\Products\ProductResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListProducts extends ListRecords
{
protected static string $resource = ProductResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\Products;
use App\Filament\Admin\Resources\Products\Pages\CreateProduct;
use App\Filament\Admin\Resources\Products\Pages\EditProduct;
use App\Filament\Admin\Resources\Products\Pages\ListProducts;
use App\Filament\Admin\Resources\Products\Schemas\ProductForm;
use App\Filament\Admin\Resources\Products\Tables\ProductsTable;
use App\Models\Product;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class ProductResource extends Resource
{
protected static ?string $model = Product::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCube;
public static function getNavigationGroup(): ?string
{
return __('products.navigation_group');
}
public static function getNavigationLabel(): string
{
return __('products.menu_label');
}
public static function getModelLabel(): string
{
return __('products.model_label');
}
public static function getPluralModelLabel(): string
{
return __('products.plural_model_label');
}
public static function form(Schema $schema): Schema
{
return ProductForm::configure($schema);
}
public static function table(Table $table): Table
{
return ProductsTable::configure($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => ListProducts::route('/'),
'create' => CreateProduct::route('/create'),
'edit' => EditProduct::route('/{record}/edit'),
];
}
}
@@ -0,0 +1,105 @@
<?php
namespace App\Filament\Admin\Resources\Products\Schemas;
use App\Models\Language;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
use Filament\Schemas\Components\Tabs\Tab;
use Filament\Schemas\Components\Utilities\Get;
class LandingPageSection
{
private static function getIconOptions(): array
{
$icons = [
'cloud-network-2', 'touchscreen', 'lock', 'rocket', 'bar-chart', 'safe',
'controls', 'checked', 'calendar', 'currency', 'server', 'devices',
'search', 'compare', 'smartphone', 'headphone', 'secure', 'verified',
'wallet', 'shopping-cart', 'e-commerce', 'code', 'bulb', 'gears',
'shield', 'star', 'check-circle', 'bolt', 'zap', 'heart', 'gift',
];
return array_combine($icons, array_map(fn($icon) => ucwords(str_replace('-', ' ', $icon)), $icons));
}
private static function getBackgroundOptions(): array
{
$backgrounds = [
// Abstract backgrounds
'assets/img/photos/bg2.jpg' => 'Background 2 (Abstract)',
'assets/img/photos/bg3.jpg' => 'Background 3 (Abstract)',
'assets/img/photos/bg4.jpg' => 'Background 4 (Abstract)',
'assets/img/photos/bg13.jpg' => 'Background 13 (Abstract)',
// Gradient backgrounds
'assets/img/photos/bg14.png' => 'Background 14 (Gradient)',
'assets/img/photos/bg15.png' => 'Background 15 (Gradient)',
'assets/img/photos/bg16.png' => 'Background 16 (Gradient)',
'assets/img/photos/bg17.png' => 'Background 17 (Gradient)',
'assets/img/photos/bg18.png' => 'Background 18 (Gradient)',
'assets/img/photos/bg19.png' => 'Background 19 (Gradient)',
'assets/img/photos/bg20.png' => 'Background 20 (Gradient)',
'assets/img/photos/bg21.png' => 'Background 21 (Gradient)',
'assets/img/photos/bg22.png' => 'Background 22 (Gradient)',
'assets/img/photos/bg23.png' => 'Background 23 (Gradient)',
'assets/img/photos/bg24.png' => 'Background 24 (Gradient)',
'assets/img/photos/bg25.png' => 'Background 25 (Gradient)',
];
return $backgrounds;
}
public static function make(): Section
{
$languages = Language::active()->ordered()->get();
$iconOptions = self::getIconOptions();
$backgroundOptions = self::getBackgroundOptions();
return Section::make(__('products.landing_page_section'))
->schema([
// Hero Section
Section::make(__('products.hero_section'))
->schema([
TextInput::make('landing_page_data.hero.slogan')
->label(__('products.hero_slogan'))
->columnSpanFull(),
Textarea::make('landing_page_data.hero.sub_slogan')
->label(__('products.hero_sub_slogan'))
->rows(2)
->columnSpanFull(),
Select::make('landing_page_data.hero.background_image')
->label(__('products.hero_background_image'))
->options($backgroundOptions)
->searchable()
->placeholder(__('products.select_background'))
->helperText(__('products.hero_background_image_helper'))
->columnSpanFull(),
TextInput::make('landing_page_data.hero.app_store_link')
->label(__('products.app_store_link'))
->url()
->placeholder('https://apps.apple.com/...'),
TextInput::make('landing_page_data.hero.google_play_link')
->label(__('products.google_play_link'))
->url()
->placeholder('https://play.google.com/...'),
FileUpload::make('landing_page_data.hero.featured_image')
->label(__('products.hero_featured_image'))
->image()
->disk('public')
->directory('products/landing')
->columnSpanFull(),
])
->columns(2)
->collapsible()
->collapsed(false),
])
->visible(fn (Get $get) => $get('type') === 'product')
->columnSpanFull();
}
}
@@ -0,0 +1,97 @@
<?php
namespace App\Filament\Admin\Resources\Products\Schemas;
use Filament\Schemas\Schema;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Components\FileUpload;
use Filament\Schemas\Components\Section;
use App\Filament\Admin\Resources\Components\TranslationTabs;
use App\Filament\Admin\Resources\Products\Schemas\LandingPageSection;
use App\Models\ProductCategory;
use Illuminate\Support\Str;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
class ProductForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->schema([
Section::make(__('products.general_section'))
->schema([
TextInput::make('title')
->label(__('products.title'))
->required()
->live(onBlur: true)
->afterStateUpdated(function (Get $get, Set $set, ?string $state) {
$set('slug', Str::slug($state));
$set('translations.' . app()->getLocale() . '.title', $state);
})
->formatStateUsing(fn ($record) => $record?->translate('title'))
->dehydrated(false),
TextInput::make('slug')
->label(__('products.slug'))
->required()
->unique(ignoreRecord: true),
Select::make('type')
->label(__('products.type'))
->options([
'product' => __('products.product'),
'service' => __('products.service'),
])
->required(),
Select::make('product_category_id')
->label(__('products.category'))
->options(function () {
return ProductCategory::all()->mapWithKeys(function ($category) {
return [$category->id => $category->translate('title') ?? ('Category #' . $category->id)];
});
})
->searchable(),
FileUpload::make('hero_image')
->label(__('products.hero_image'))
->required()
->image()
->disk('public')
->directory('products'),
TextInput::make('view_template')
->label(__('products.view_template'))
->placeholder(__('products.view_template_placeholder'))
->helperText(__('products.view_template_helper')),
TextInput::make('external_url')
->label(__('products.external_url'))
->url()
->helperText(__('products.external_url_helper')),
TextInput::make('sort_order')
->label(__('products.sort_order'))
->numeric()
->default(0),
Toggle::make('is_active')
->label(__('products.is_active'))
->default(true),
])->columns(2),
Section::make(__('products.translations_section'))
->schema([
TranslationTabs::make([
'title' => [
'type' => 'text',
'label' => __('products.title'),
'required' => true,
],
'content' => [
'type' => 'richtext',
'label' => __('products.content'),
],
]),
]),
// Landing Page Section (only for products)
LandingPageSection::make(),
]);
}
}
@@ -0,0 +1,68 @@
<?php
namespace App\Filament\Admin\Resources\Products\Tables;
use Filament\Tables\Table;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\ToggleColumn;
use Filament\Actions\EditAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\BulkActionGroup;
class ProductsTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
ImageColumn::make('hero_image')
->label(__('products.hero_image'))
->disk('public'),
TextColumn::make('title')
->label(__('products.title'))
->getStateUsing(fn ($record) => $record->translate('title'))
->searchable(query: function ($query, $search) {
return $query->whereHas('translations', function ($q) use ($search) {
$q->where('field_name', 'title')
->where('field_value', 'like', "%{$search}%")
->where('language_code', app()->getLocale());
});
}),
TextColumn::make('type')
->label(__('products.type'))
->badge()
->color(fn (string $state): string => match ($state) {
'product' => 'info',
'service' => 'success',
default => 'gray',
})
->formatStateUsing(fn (string $state): string => match ($state) {
'product' => __('products.product'),
'service' => __('products.service'),
default => $state,
}),
TextColumn::make('category.title')
->getStateUsing(fn ($record) => $record->category?->translate('title'))
->label(__('products.category')),
ToggleColumn::make('is_active')
->label(__('products.is_active')),
TextColumn::make('sort_order')
->label(__('products.sort_order'))
->sortable(),
])
->filters([
//
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -18,6 +18,25 @@ class EditSetting extends EditRecord
return __('settings.edit'); return __('settings.edit');
} }
protected function mutateFormDataBeforeFill(array $data): array
{
$record = $this->getRecord();
if (isset($data['type'])) {
$type = $data['type'];
$virtualField = 'value_' . $type;
try {
// Model accessor'ını kullanarak değeri al
$data[$virtualField] = $record->{$virtualField};
} catch (\Exception $e) {
// Accessor bulunamazsa devam et
}
}
return $data;
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
@@ -42,6 +42,7 @@ class SettingForm
->regex('/^[a-z0-9_-]+$/') ->regex('/^[a-z0-9_-]+$/')
->maxLength(255) ->maxLength(255)
->disabled(fn ($record) => $record !== null) ->disabled(fn ($record) => $record !== null)
->live(onBlur: true)
->columnSpanFull(), ->columnSpanFull(),
TextInput::make('label') TextInput::make('label')
@@ -114,10 +115,28 @@ class SettingForm
->label(__('settings.value')) ->label(__('settings.value'))
->helperText(__('settings.value_helper')) ->helperText(__('settings.value_helper'))
->required() ->required()
->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json'])) ->visible(fn (Get $get) => in_array($get('type'), ['string', 'text', 'json']) && !in_array($get('key'), ['default_header', 'default_footer']))
->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3) ->rows(fn (Get $get) => $get('type') === 'text' ? 5 : 3)
->columnSpanFull(), ->columnSpanFull(),
Select::make('value_header_template')
->label('Default Header Template')
->options(\App\Models\HeaderTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => $get('key') === 'default_header')
->statePath('value')
->searchable()
->preload()
->columnSpanFull(),
Select::make('value_footer_template')
->label('Default Footer Template')
->options(\App\Models\FooterTemplate::where('is_active', true)->pluck('title', 'id'))
->visible(fn (Get $get) => $get('key') === 'default_footer')
->statePath('value')
->searchable()
->preload()
->columnSpanFull(),
Toggle::make('value_boolean') Toggle::make('value_boolean')
->label(__('settings.value')) ->label(__('settings.value'))
->helperText(__('settings.value_helper')) ->helperText(__('settings.value_helper'))
@@ -134,7 +153,15 @@ class SettingForm
->directory('settings') ->directory('settings')
->acceptedFileTypes(['image/*', 'application/pdf', 'text/*']) ->acceptedFileTypes(['image/*', 'application/pdf', 'text/*'])
->maxSize(10240) // 10MB ->maxSize(10240) // 10MB
->columnSpanFull(), ->openable()
->downloadable()
->previewable(true)
->columnSpanFull()
->afterStateHydrated(function ($component, $state, $record) {
if ($record && $record->type === 'file') {
$component->state($record->value);
}
}),
DatePicker::make('value_date') DatePicker::make('value_date')
->label(__('settings.value')) ->label(__('settings.value'))
@@ -0,0 +1,16 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Resources\Pages\CreateRecord;
class CreateSiteTranslation extends CreateRecord
{
protected static string $resource = SiteTranslationResource::class;
}
@@ -0,0 +1,24 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
class EditSiteTranslation extends EditRecord
{
protected static string $resource = SiteTranslationResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
];
}
}
@@ -0,0 +1,64 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\SiteTranslationResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Actions\Action;
use Illuminate\Contracts\View\View;
class ListSiteTranslations extends ListRecords
{
protected static string $resource = SiteTranslationResource::class;
public bool $useDataGrid = false;
public function mount(): void
{
parent::mount();
// Session'dan görünüm tercihini al, yoksa varsayılan olarak false (eski görünüm)
$this->useDataGrid = session('site_translations_use_datagrid', false);
}
protected function getHeaderActions(): array
{
$useDataGrid = session('site_translations_use_datagrid', false);
return [
Action::make('toggleView')
->label($useDataGrid ? 'Tablo Görünümü' : 'DataGrid Görünümü')
->icon($useDataGrid ? 'heroicon-o-table-cells' : 'heroicon-o-squares-2x2')
->color('gray')
->action(function () {
$currentValue = session('site_translations_use_datagrid', false);
$newValue = !$currentValue;
session(['site_translations_use_datagrid' => $newValue]);
session()->save(); // Session'ı hemen kaydet
// Sayfayı yenile
$this->redirect($this->getResource()::getUrl('index'));
}),
CreateAction::make()
->visible(!$useDataGrid), // Sadece eski görünümde göster
];
}
public function render(): View
{
$useDataGrid = session('site_translations_use_datagrid', false);
if ($useDataGrid) {
return view('filament.admin.resources.site-translations.datagrid')
->layout($this->getLayout());
}
return parent::render();
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Schemas;
use App\Models\Language;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class SiteTranslationForm
{
public static function make(Schema $schema): Schema
{
$languages = Language::where('is_active', true)->get();
$translationFields = [];
foreach ($languages as $language) {
$translationFields[] = Textarea::make($language->code)
->label($language->name . " ({$language->code})")
->rows(2);
}
return $schema
->components([
Section::make()
->schema([
Textarea::make('key')
->label('Key / Original Text')
->required()
->columnSpanFull()
->helperText('The original text to be translated. Use this exact text in t() helper.'),
Group::make()
->schema($translationFields)
->statePath('translations')
->columnSpanFull(),
])
]);
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations;
use App\Filament\Admin\Resources\SiteTranslations\Pages;
use App\Filament\Admin\Resources\SiteTranslations\Schemas\SiteTranslationForm;
use App\Filament\Admin\Resources\SiteTranslations\Tables\SiteTranslationsTable;
use App\Models\SiteTranslation;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;
use UnitEnum;
use BackedEnum;
class SiteTranslationResource extends Resource
{
protected static ?string $model = SiteTranslation::class;
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-language';
protected static UnitEnum|string|null $navigationGroup = 'Settings';
protected static ?int $navigationSort = 100;
public static function form(Schema $schema): Schema
{
return SiteTranslationForm::make($schema);
}
public static function table(Table $table): Table
{
return SiteTranslationsTable::make($table);
}
public static function getRelations(): array
{
return [
//
];
}
public static function getPages(): array
{
return [
'index' => Pages\ListSiteTranslations::route('/'),
'create' => Pages\CreateSiteTranslation::route('/create'),
'edit' => Pages\EditSiteTranslation::route('/{record}/edit'),
];
}
public static function getNavigationLabel(): string
{
return __('Site Translations');
}
public static function getModelLabel(): string
{
return __('Translation');
}
public static function getPluralModelLabel(): string
{
return __('Translations');
}
}
@@ -0,0 +1,93 @@
<?php
namespace App\Filament\Admin\Resources\SiteTranslations\Tables;
use Filament\Actions\DeleteAction;
use Filament\Actions\EditAction;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
use App\Models\Language;
use Filament\Forms\Components\Textarea;
use Filament\Schemas\Components\Group;
use Filament\Schemas\Components\Section;
use Filament\Actions\ImportAction;
use Filament\Actions\ExportAction;
use App\Filament\Imports\SiteTranslationImporter;
use App\Filament\Exports\SiteTranslationExporter;
class SiteTranslationsTable
{
public static function make(Table $table): Table
{
$defaultLocale = app()->getLocale();
return $table
->headerActions([
ImportAction::make()
->importer(SiteTranslationImporter::class),
ExportAction::make()
->exporter(SiteTranslationExporter::class)
])
->columns([
TextColumn::make('key')
->searchable()
->sortable()
->limit(50)
->wrap(),
TextColumn::make("translations.{$defaultLocale}")
->label("Translation ({$defaultLocale})")
->searchable()
->limit(50)
->wrap()
->placeholder('No translation'),
TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->recordActions([
EditAction::make()
->modalWidth('xl')
->form(function () {
$languages = Language::where('is_active', true)->get();
$translationFields = [];
foreach ($languages as $language) {
$translationFields[] = Textarea::make($language->code)
->label($language->name . " ({$language->code})")
->rows(2);
}
return [
Section::make()
->schema([
Textarea::make('key')
->label('Key / Original Text')
->required()
->columnSpanFull()
->helperText('The original text to be translated. Use this exact text in t() helper.'),
Group::make()
->schema($translationFields)
->statePath('translations')
->columnSpanFull(),
])
];
}),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}
@@ -0,0 +1,46 @@
<?php
namespace App\Filament\Exports;
use App\Models\SiteTranslation;
use App\Models\Language;
use Filament\Actions\Exports\ExportColumn;
use Filament\Actions\Exports\Exporter;
use Filament\Actions\Exports\Models\Export;
class SiteTranslationExporter extends Exporter
{
protected static ?string $model = SiteTranslation::class;
public static function getColumns(): array
{
$columns = [
ExportColumn::make('key')->label('Key'),
];
// Veritabanı bağlantısı yoksa veya tablo yoksa hata vermemesi için try-catch veya kontrol
try {
$languages = Language::where('is_active', true)->get();
foreach ($languages as $language) {
$columns[] = ExportColumn::make("translations.{$language->code}")
->label($language->name . " ({$language->code})");
}
} catch (\Exception $e) {
// Migration henüz çalışmadıysa
}
return $columns;
}
public static function getCompletedNotificationBody(Export $export): string
{
$body = 'Your site translation export has completed and ' . number_format($export->successful_rows) . ' ' . str('row')->plural($export->successful_rows) . ' exported.';
if ($failedRowsCount = $export->getFailedRowsCount()) {
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to export.';
}
return $body;
}
}
@@ -0,0 +1,86 @@
<?php
namespace App\Filament\Imports;
use App\Models\SiteTranslation;
use App\Models\Language;
use Filament\Actions\Imports\ImportColumn;
use Filament\Actions\Imports\Importer;
use Filament\Actions\Imports\Models\Import;
class SiteTranslationImporter extends Importer
{
protected static ?string $model = SiteTranslation::class;
public static function getColumns(): array
{
$columns = [
ImportColumn::make('key')
->label('Key')
->requiredMapping()
->rules(['required']),
];
try {
$languages = Language::where('is_active', true)->get();
foreach ($languages as $language) {
$columns[] = ImportColumn::make($language->code)
->label($language->name . " ({$language->code})");
}
} catch (\Exception $e) {
//
}
return $columns;
}
public function resolveRecord(): ?SiteTranslation
{
if (empty($this->data['key'])) {
return null;
}
// Key üzerinden kaydı bul veya yeni oluştur
// Not: key alanı text olduğu için tam eşleşme arıyoruz
return SiteTranslation::firstOrNew([
'key' => $this->data['key'],
]);
}
protected function beforeSave(): void
{
$translations = $this->record->translations ?? [];
$languages = Language::where('is_active', true)->pluck('code')->toArray();
foreach ($languages as $code) {
if (array_key_exists($code, $this->data)) {
$value = $this->data[$code];
// Boş string de olsa güncelleyelim, null ise (CSV'de yoksa) dokunmayalım
if ($value !== null) {
$translations[$code] = $value;
}
}
}
$this->record->translations = $translations;
// Hash'i manuel oluşturmaya gerek yok, model event'i halledecek ama
// new record ise ve event çalışmazsa diye garantiye alalım (import toplu insert yapabilir mi? Hayır record record işler)
if (!$this->record->exists && empty($this->record->hash)) {
$this->record->hash = md5($this->record->key);
}
}
public static function getCompletedNotificationBody(Import $import): string
{
$body = 'Your site translation import has completed and ' . number_format($import->successful_rows) . ' ' . str('row')->plural($import->successful_rows) . ' imported.';
if ($failedRowsCount = $import->getFailedRowsCount()) {
$body .= ' ' . number_format($failedRowsCount) . ' ' . str('row')->plural($failedRowsCount) . ' failed to import.';
}
return $body;
}
}
+32 -2
View File
@@ -1,6 +1,8 @@
<?php <?php
use App\Models\Setting; use App\Models\Setting;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
if (!function_exists('setting')) { if (!function_exists('setting')) {
/** /**
@@ -12,7 +14,35 @@ if (!function_exists('setting')) {
*/ */
function setting(string $key, $default = null) function setting(string $key, $default = null)
{ {
return Setting::get($key, $default); // Cache settings to avoid database queries on every call
// Cache will store the full setting object to access type info
$setting = Cache::remember("app_setting_{$key}", 60 * 60 * 24, function () use ($key) {
return Setting::where('key', $key)
->where('is_active', true)
->first();
});
if (!$setting) {
return $default;
}
$value = $setting->value;
// Handle file/image types
if (in_array($setting->type, ['file', 'image']) || $key === 'home_hero') {
if (empty($value)) {
return $default;
}
// If value is already a URL, return it
if (filter_var($value, FILTER_VALIDATE_URL)) {
return $value;
}
// Return storage URL
return Storage::url($value);
}
return $value;
} }
} }
+17
View File
@@ -2,6 +2,7 @@
use App\Models\Language; use App\Models\Language;
use App\Models\Translation; use App\Models\Translation;
use App\Models\SiteTranslation;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
if (!function_exists('current_language')) { if (!function_exists('current_language')) {
@@ -279,3 +280,19 @@ if (!function_exists('get_translation_value')) {
} }
} }
if (!function_exists('t')) {
/**
* Get site translation for a given key.
*
* @param string $key
* @return string
*/
function t(string $key): string
{
if (empty($key)) {
return '';
}
return SiteTranslation::getTranslation($key);
}
}
@@ -0,0 +1,292 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\SiteTranslation;
use App\Models\Language;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Validator;
class SiteTranslationController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(Request $request): JsonResponse
{
$query = SiteTranslation::query();
// Search
if ($request->has('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('key', 'like', "%{$search}%")
->orWhereRaw("JSON_SEARCH(translations, 'one', ?, NULL, '$[*]') IS NOT NULL", ["%{$search}%"]);
});
}
// Sorting
$sortField = $request->get('sort', 'id');
$sortOrder = $request->get('order', 'desc');
$query->orderBy($sortField, $sortOrder);
// Pagination
$page = $request->get('page', 1);
$pageSize = $request->get('pageSize', 20);
$totalCount = $query->count();
$items = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get();
// Format data for DataGrid
$data = $items->map(function ($item) {
$formatted = [
'id' => $item->id,
'key' => $item->key,
'hash' => $item->hash,
'created_at' => $item->created_at?->toDateTimeString(),
'updated_at' => $item->updated_at?->toDateTimeString(),
];
// Add translation fields for each active language
$languages = Language::where('is_active', true)->get();
$translations = $item->translations ?? [];
foreach ($languages as $language) {
$formatted[$language->code] = $translations[$language->code] ?? '';
}
return $formatted;
});
return response()->json([
'data' => $data,
'totalCount' => $totalCount,
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'key' => 'required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$hash = md5($request->key);
// Check if translation already exists
$exists = SiteTranslation::where('hash', $hash)->first();
if ($exists) {
return response()->json([
'success' => false,
'message' => 'Bu key zaten mevcut.',
], 409);
}
$languages = Language::where('is_active', true)->get();
$translations = [];
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
$translation = SiteTranslation::create([
'hash' => $hash,
'key' => $request->key,
'translations' => $translations,
]);
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'data' => $translation->load('translations'),
'message' => 'Çeviri başarıyla oluşturuldu.',
], 201);
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, $id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$validator = Validator::make($request->all(), [
'key' => 'sometimes|required|string|max:500',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if ($request->has($language->code)) {
$translations[$language->code] = $request->get($language->code, '');
}
}
// Update key if provided
if ($request->has('key') && $request->key !== $translation->key) {
$translation->key = $request->key;
$translation->hash = md5($request->key);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
return response()->json([
'success' => true,
'data' => $translation->fresh(),
'message' => 'Çeviri başarıyla güncellendi.',
]);
}
/**
* Batch update multiple resources.
*/
public function batchUpdate(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'changes' => 'required|array',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$changes = $request->get('changes', []);
$updated = [];
$errors = [];
foreach ($changes as $change) {
try {
// ID can be in 'key' field (integer) or directly in 'key' field
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? null);
if (!$id) {
$errors[] = 'ID bulunamadı.';
continue;
}
$translation = SiteTranslation::find($id);
if (!$translation) {
$errors[] = "ID {$id} için kayıt bulunamadı.";
continue;
}
$data = $change['data'] ?? [];
$languages = Language::where('is_active', true)->get();
$translations = $translation->translations ?? [];
// Update translations
foreach ($languages as $language) {
if (isset($data[$language->code])) {
$translations[$language->code] = $data[$language->code];
}
}
// Update key if provided
if (isset($data['key']) && $data['key'] !== $translation->key) {
$translation->key = $data['key'];
$translation->hash = md5($data['key']);
}
$translation->translations = $translations;
$translation->save();
Cache::forget("site_translation_{$translation->hash}");
$updated[] = $translation->id;
} catch (\Exception $e) {
$id = is_numeric($change['key'] ?? null)
? (int)$change['key']
: ($change['key']['id'] ?? 'unknown');
$errors[] = "ID {$id} için hata: " . $e->getMessage();
}
}
return response()->json([
'success' => count($errors) === 0,
'updated' => $updated,
'errors' => $errors,
'message' => count($updated) . ' kayıt güncellendi.',
]);
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id): JsonResponse
{
$translation = SiteTranslation::findOrFail($id);
$hash = $translation->hash;
$translation->delete();
Cache::forget("site_translation_{$hash}");
return response()->json([
'success' => true,
'message' => 'Çeviri başarıyla silindi.',
]);
}
/**
* Batch delete multiple resources.
*/
public function batchDestroy(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'ids' => 'required|array',
'ids.*' => 'required|integer|exists:site_translations,id',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors(),
], 422);
}
$ids = $request->get('ids', []);
$translations = SiteTranslation::whereIn('id', $ids)->get();
foreach ($translations as $translation) {
Cache::forget("site_translation_{$translation->hash}");
}
SiteTranslation::whereIn('id', $ids)->delete();
return response()->json([
'success' => true,
'message' => count($ids) . ' kayıt başarıyla silindi.',
]);
}
}
+62 -9
View File
@@ -8,6 +8,7 @@ use App\Models\BlogComment;
use App\Models\HeaderTemplate; use App\Models\HeaderTemplate;
use App\Models\FooterTemplate; use App\Models\FooterTemplate;
use App\Services\TemplateService; use App\Services\TemplateService;
use App\Models\Page;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -44,37 +45,80 @@ class BlogController extends Controller
? $query->latest('published_at')->paginate(12) ? $query->latest('published_at')->paginate(12)
: collect(); : collect();
// Render Header Template - En son aktif olan header template'i kullan // Blog sayfa ayarlarını bul (varsa)
$blogPage = Page::where('slug', 'blog')->first();
// Render Header Template
$renderedHeader = null; $renderedHeader = null;
$headerTemplate = null;
// 1. Öncelik: Blog sayfasında seçili header template
if ($blogPage && $blogPage->header_template_id) {
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
}
// 2. Öncelik: Varsayılan aktif template (KALDIRILDI - Default static header kullanılmalı)
/*
if (!$headerTemplate) {
$headerTemplate = HeaderTemplate::where('is_active', true) $headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at') ->latest('updated_at')
->first(); ->first();
}
*/
if ($headerTemplate) { if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? []; $templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders( $renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content, $headerTemplate->html_content,
$templateDefaults $mergedData,
$blogPage
); );
} }
// Render Footer Template - En son aktif olan footer template'i kullan // Render Footer Template
$renderedFooter = null; $renderedFooter = null;
$footerTemplate = null;
// 1. Öncelik: Blog sayfasında seçili footer template
if ($blogPage && $blogPage->footer_template_id) {
$footerTemplate = FooterTemplate::find($blogPage->footer_template_id);
}
// 2. Öncelik: Varsayılan aktif template
if (!$footerTemplate) {
$footerTemplate = FooterTemplate::where('is_active', true) $footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at') ->latest('updated_at')
->first(); ->first();
}
if ($footerTemplate) { if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? []; $templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->footer_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders( $renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content, $footerTemplate->html_content,
$templateDefaults $mergedData,
$blogPage
); );
} }
// Kategorileri al (filtreleme için)
$categories = class_exists(\App\Models\BlogCategory::class) ? \App\Models\BlogCategory::where('is_active', true)->orderBy('sort_order')->get() : collect();
if ($request->ajax()) {
return view('blog.partials.posts', [
'posts' => $posts
]);
}
return view('blog.index', [ return view('blog.index', [
'settings' => $settings, 'settings' => $settings,
'posts' => $posts, 'posts' => $posts,
'categories' => $categories,
'renderedHeader' => $renderedHeader, 'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter, 'renderedFooter' => $renderedFooter,
'meta' => [ 'meta' => [
@@ -132,17 +176,26 @@ class BlogController extends Controller
->orderBy('created_at', 'desc') ->orderBy('created_at', 'desc')
->get(); ->get();
// Render Header Template - En son aktif olan header template'i kullan // Blog sayfa ayarlarını bul (varsa)
$blogPage = Page::where('slug', 'blog')->first();
// Render Header Template
$renderedHeader = null; $renderedHeader = null;
$headerTemplate = HeaderTemplate::where('is_active', true) $headerTemplate = null;
->latest('updated_at')
->first(); // 1. Öncelik: Blog sayfasında seçili header template
if ($blogPage && $blogPage->header_template_id) {
$headerTemplate = HeaderTemplate::find($blogPage->header_template_id);
}
if ($headerTemplate) { if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? []; $templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $blogPage ? ($blogPage->header_data ?? []) : [];
$mergedData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders( $renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content, $headerTemplate->html_content,
$templateDefaults, $mergedData,
$post $post
); );
} }
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers;
use App\Models\CareerApplication;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class CareerController extends Controller
{
public function index()
{
return view('front.career.index', [
'meta' => [
'title' => __('career.job_application_title', ['default' => 'İş Başvurusu']),
'description' => __('career.job_application_description', ['default' => 'Ekibimize katılmak için aşağıdaki formu doldurarak CV\'nizi iletebilirsiniz.']),
]
]);
}
public function internship()
{
return view('front.career.internship', [
'meta' => [
'title' => __('career.internship_title', ['default' => 'Staj Başvurusu']),
'description' => __('career.internship_description', ['default' => 'Uzaktan staj programımız hakkında bilgi alın ve başvurunuzu yapın.']),
]
]);
}
public function store(Request $request)
{
$rules = [
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'nullable|string|max:20',
'cv' => 'required|file|mimes:pdf,doc,docx|max:5120', // Max 5MB
'message' => 'nullable|string',
];
if ($request->type === 'job') {
$rules['nda'] = 'required|file|mimes:pdf|max:5120';
$rules['contract'] = 'required|file|mimes:pdf|max:5120';
$rules['git_knowledge'] = 'accepted';
$rules['ai_usage'] = 'accepted';
}
$request->validate($rules);
$cvPath = $request->file('cv') ? $request->file('cv')->store('cvs', 'public') : null;
$ndaPath = $request->file('nda') ? $request->file('nda')->store('ndas', 'public') : null;
$contractPath = $request->file('contract') ? $request->file('contract')->store('contracts', 'public') : null;
CareerApplication::create([
'name' => $request->name,
'email' => $request->email,
'phone' => $request->phone,
'type' => $request->type ?? 'job',
'cv_path' => $cvPath,
'nda_path' => $ndaPath,
'contract_path' => $contractPath,
'git_knowledge' => $request->has('git_knowledge'),
'ai_usage' => $request->has('ai_usage'),
'message' => $request->message,
'status' => 'pending',
]);
if ($request->ajax()) {
return response()->json([
'success' => true,
'message' => __('career.success_message')
]);
}
return redirect()->back()->with('success', __('career.success_message'));
}
}
+170 -147
View File
@@ -8,6 +8,7 @@ use App\Models\HeaderTemplate;
use App\Models\FooterTemplate; use App\Models\FooterTemplate;
use App\Services\TemplateService; use App\Services\TemplateService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PageController extends Controller class PageController extends Controller
{ {
@@ -16,131 +17,160 @@ class PageController extends Controller
*/ */
public function index() public function index()
{ {
// 1) is_homepage işaretli ve published (en son güncellenen) $settings = $this->getSettings();
$page = Page::with(['headerTemplate', 'footerTemplate']) $page = Page::with(['headerTemplate', 'footerTemplate'])
->where('is_homepage', true) ->where('is_homepage', true)
->where('status', 'published') ->where('status', 'published')
->latest('updated_at') ->latest('updated_at')
->first(); ->first();
// 1. Statik "home" Template Kontrolü
if (view()->exists('templates.home')) {
return $this->renderView('templates.home', $page, $settings);
}
// Hiç sayfa yoksa fallback
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null;
// Hiç sayfa yoksa: direkt home template'inin statik fallback'i ile render et
if (!$page) { if (!$page) {
return view('templates.home', [ return view('templates.home', [
'page' => null, 'page' => null,
'settings' => $settings, 'settings' => $settings,
'meta' => [ 'meta' => $this->getMeta(null, $settings),
'title' => $settings->default_meta_title ?? config('app.name'),
'description' => $settings->default_meta_description ?? null,
'image' => $settings->default_meta_image ?? null,
],
]); ]);
} }
$metaTitle = method_exists($page, 'translate') return $this->renderPage($page, $settings);
? ($page->translate('meta_title') ?: $page->translate('title'))
: ($page->meta_title ?? $page->title ?? null);
$metaDescription = method_exists($page, 'translate')
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
: ($page->meta_description ?? $page->excerpt ?? null);
// Section verisini al - Hem eski hem yeni sistemi destekle
$sections = $page->sections ?? $page->data ?? [];
// Yeni dinamik template sistemi için sections_data varsa onu da al
$templatedSections = $page->templated_sections ?? collect([]);
// Template belirleme mantığı:
// 1. Kullanıcı template seçmişse, onu kullan
// 2. Template seçilmemişse ve homepage ise, 'home' kullan
// 3. Hiçbiri yoksa, 'generic' kullan
$template = $page->template
?? (($page->slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic');
$view = view()->exists("templates.$template")
? "templates.$template"
: 'templates.generic';
// Render Header Template
$renderedHeader = null;
$headerTemplate = $page->headerTemplate;
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan
if (!$headerTemplate) {
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
if ($headerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $page->header_data ?? [];
$mergedHeaderData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$mergedHeaderData,
$page
);
}
// Render Footer Template
$renderedFooter = null;
$footerTemplate = $page->footerTemplate;
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan
if (!$footerTemplate) {
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
}
if ($footerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $page->footer_data ?? [];
$mergedFooterData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$page
);
}
return view($view, [
'page' => $page,
'settings' => $settings,
'sections' => $sections,
'templatedSections' => $templatedSections,
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => [
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $settings->default_meta_image ?? null,
],
]);
} }
/** /**
* Display a specific page by slug * Display a specific page by slug
*/ */
public function show($slug) public function show($slug)
{ {
return $this->handleShow($slug);
}
/**
* Handle nested page routes (e.g. /kurumsal/hakkimizda)
*/
public function showNested($parentSlug, $slug)
{
return $this->handleShow($slug);
}
/**
* Handle akademi.truncgil.com.tr subdomain
*/
public function showAkademi()
{
return $this->handleShow('truncgil-akademi');
}
/**
* Unified show logic
*/
protected function handleShow($slug)
{
$settings = $this->getSettings();
// 3a. Akademi Sayfası Yönlendirmeleri
if ($slug === 'truncgil-akademi') {
$host = request()->getHost();
$path = request()->path();
// Eğer ana domainden geliyorsa -> Subdomain'e yönlendir
if ($host !== 'akademi.truncgil.com.tr') {
return redirect()->to('https://akademi.truncgil.com.tr/', 301);
}
// Eğer subdoman'de ama slug ile geliyorsa (/truncgil-akademi) -> Ana dizine yönlendir (/)
if ($host === 'akademi.truncgil.com.tr' && $path !== '/') {
return redirect()->to('https://akademi.truncgil.com.tr/', 301);
}
}
// 1. Statik View Kontrolü
if (view()->exists("templates.$slug")) {
$page = Page::with(['headerTemplate', 'footerTemplate'])
->where('slug', $slug)
->where('status', 'published')
->first();
return $this->renderView("templates.$slug", $page, $settings);
}
// 2. Normal Dinamik Akış (DB Kaydı Zorunlu)
$page = Page::with(['headerTemplate', 'footerTemplate']) $page = Page::with(['headerTemplate', 'footerTemplate'])
->where('slug', $slug) ->where('slug', $slug)
->where('status', 'published') ->where('status', 'published')
->firstOrFail(); ->firstOrFail();
$settings = class_exists(Setting::class) ? (Setting::query()->first()) : null; return $this->renderPage($page, $settings);
}
/**
* Render a page with full dynamic logic
*/
protected function renderPage(Page $page, $settings)
{
$template = $page->template
?? (($page->is_homepage ?? false) ? 'home' : 'generic');
$view = view()->exists("templates.$template")
? "templates.$template"
: 'templates.generic';
return $this->renderView($view, $page, $settings);
}
/**
* Shared rendering logic for both static and dynamic templates
*/
protected function renderView(string $view, ?Page $page, $settings)
{
$renderedHeader = null;
$renderedFooter = null;
if ($page) {
$renderedHeader = $this->renderHeader($page, $settings);
$renderedFooter = $this->renderFooter($page, $settings);
}
return view($view, [
'page' => $page,
'settings' => $settings,
'sections' => $page ? ($page->parsed_sections ?? $page->sections ?? $page->data ?? []) : [],
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => $this->getMeta($page, $settings),
]);
}
/**
* Get settings as an object
*/
protected function getSettings()
{
if (!class_exists(Setting::class)) {
return (object) [];
}
return (object) Setting::where('is_active', true)
->pluck('value', 'key')
->toArray();
}
/**
* Get meta information for a page
*/
protected function getMeta(?Page $page, $settings)
{
$metaTitle = null;
$metaDescription = null;
$metaImage = $settings->default_meta_image ?? null;
if ($page) {
$metaTitle = method_exists($page, 'translate') $metaTitle = method_exists($page, 'translate')
? ($page->translate('meta_title') ?: $page->translate('title')) ? ($page->translate('meta_title') ?: $page->translate('title'))
: ($page->meta_title ?? $page->title ?? null); : ($page->meta_title ?? $page->title ?? null);
@@ -149,84 +179,80 @@ public function show($slug)
? ($page->translate('meta_description') ?: ($page->excerpt ?? null)) ? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
: ($page->meta_description ?? $page->excerpt ?? null); : ($page->meta_description ?? $page->excerpt ?? null);
// Use parsed_sections for easier template usage (key-value format) if ($page->featured_image) {
$sections = $page->parsed_sections ?? $page->sections ?? $page->data ?? []; $metaImage = asset('storage/' . $page->featured_image);
}
}
// Yeni dinamik template sistemi için sections_data varsa onu da al return [
$templatedSections = $page->templated_sections ?? collect([]); 'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $metaImage,
];
}
// Template belirleme mantığı: /**
// 1. Kullanıcı template seçmişse, onu kullan * Render header template
// 2. Template seçilmemişse ve homepage ise, 'home' kullan */
// 3. Hiçbiri yoksa, 'generic' kullan protected function renderHeader(Page $page, $settings)
$template = $page->template {
?? (($slug === 'home' || ($page->is_homepage ?? false)) ? 'home' : 'generic'); // 1. Önce özel blade dosyası var mı kontrol et
if (!empty($page->custom_header_blade) && view()->exists($page->custom_header_blade)) {
return view($page->custom_header_blade, ['page' => $page])->render();
}
$view = view()->exists("templates.$template") // 2. Yoksa Header Template sistemini kullan
? "templates.$template"
: 'templates.generic';
// Render Header Template
$renderedHeader = null;
$headerTemplate = $page->headerTemplate; $headerTemplate = $page->headerTemplate;
// Eğer sayfa için header seçilmemişse, en son aktif header template'i kullan if (!$headerTemplate && isset($settings->default_header)) {
if (!$headerTemplate) { $headerTemplate = HeaderTemplate::find($settings->default_header);
$headerTemplate = HeaderTemplate::where('is_active', true)
->latest('updated_at')
->first();
} }
if ($headerTemplate) { if ($headerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $headerTemplate->default_data ?? []; $templateDefaults = $headerTemplate->default_data ?? [];
$pageData = $page->header_data ?? []; $pageData = $page->header_data ?? [];
$mergedHeaderData = array_merge($templateDefaults, $pageData); $mergedHeaderData = array_merge($templateDefaults, $pageData);
$renderedHeader = TemplateService::replacePlaceholders( return TemplateService::replacePlaceholders(
$headerTemplate->html_content, $headerTemplate->html_content,
$mergedHeaderData, $mergedHeaderData,
$page $page
); );
} }
// Render Footer Template return null;
$renderedFooter = null; }
/**
* Render footer template
*/
protected function renderFooter(Page $page, $settings)
{
// 1. Önce özel blade dosyası var mı kontrol et
if (!empty($page->custom_footer_blade) && view()->exists($page->custom_footer_blade)) {
return view($page->custom_footer_blade, ['page' => $page])->render();
}
// 2. Yoksa Footer Template sistemini kullan
$footerTemplate = $page->footerTemplate; $footerTemplate = $page->footerTemplate;
// Eğer sayfa için footer seçilmemişse, en son aktif footer template'i kullan if (!$footerTemplate && isset($settings->default_footer)) {
if (!$footerTemplate) { $footerTemplate = FooterTemplate::find($settings->default_footer);
$footerTemplate = FooterTemplate::where('is_active', true)
->latest('updated_at')
->first();
} }
if ($footerTemplate) { if ($footerTemplate) {
// Merge template defaults with page data (page data overrides defaults)
$templateDefaults = $footerTemplate->default_data ?? []; $templateDefaults = $footerTemplate->default_data ?? [];
$pageData = $page->footer_data ?? []; $pageData = $page->footer_data ?? [];
$mergedFooterData = array_merge($templateDefaults, $pageData); $mergedFooterData = array_merge($templateDefaults, $pageData);
$renderedFooter = TemplateService::replacePlaceholders( return TemplateService::replacePlaceholders(
$footerTemplate->html_content, $footerTemplate->html_content,
$mergedFooterData, $mergedFooterData,
$page $page
); );
} }
return view($view, [ return null;
'page' => $page,
'settings' => $settings,
'sections' => $sections,
'templatedSections' => $templatedSections,
'renderedHeader' => $renderedHeader,
'renderedFooter' => $renderedFooter,
'meta' => [
'title' => $metaTitle ?: ($settings->default_meta_title ?? config('app.name')),
'description' => $metaDescription ?: ($settings->default_meta_description ?? null),
'image' => $settings->default_meta_image ?? null,
],
]);
} }
/** /**
@@ -248,6 +274,3 @@ public function show($slug)
->get(); ->get();
} }
} }
+202
View File
@@ -0,0 +1,202 @@
<?php
namespace App\Http\Controllers;
use App\Models\Page;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class PaymentController extends Controller
{
public function process(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'surname' => 'required|string|max:255',
'email' => 'required|email|max:255',
'phone' => 'required|string|max:20',
'price' => 'required|numeric|min:1',
'address' => 'nullable|string',
'agreement' => 'accepted',
], [
'name.required' => 'Lütfen adınızı giriniz.',
'surname.required' => 'Lütfen soyadınızı giriniz.',
'email.required' => 'Lütfen e-posta adresinizi giriniz.',
'email.email' => 'Geçerli bir e-posta adresi giriniz.',
'phone.required' => 'Lütfen telefon numaranızı giriniz.',
'price.required' => 'Lütfen ödeme tutarını giriniz.',
'price.min' => 'Ödeme tutarı en az 1 TL olmalıdır.',
'address.required' => 'Lütfen ödeme açıklamasını giriniz.',
'agreement.accepted' => 'Lütfen sözleşmeyi onaylayınız.',
]);
if ($validator->fails()) {
Log::info('Payment Validation Failed', $validator->errors()->toArray());
return back()->withErrors($validator)->withInput();
}
$merchant_id = '127117';
$merchant_key = 's9GH5FMuxFtqjxyJ';
$merchant_salt = 'qTHTx3j4FjieXPP3';
$price = $request->input('price') * 100; // Krş cinsinden
// Get user IP (handling Cloudflare or standard)
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$ip = $_SERVER['REMOTE_ADDR'];
}
$user_ip = $ip;
$merchant_oid = time(); // Benzersiz sipariş no
$email = $request->input('email');
$payment_amount = $price;
$currency = 'TL'; // Default
$test_mode = 0; // Canlı mod
$user_name = $request->input('name').' '.$request->input('surname');
$user_address = $request->input('address') ?: 'Adres Belirtilmemiş'; // Fallback if not provided
$user_phone = $request->input('phone');
// Callbacks should ideally be routes we define, but user provided specific URLs.
// We will use routes relative to current domain or the ones user specified if they are absolute?
// The user script had: "https://www.truncgil.com.tr/pay.php?ok"
// I should probably use the current app's URL.
$merchant_ok_url = route('payment.success');
$merchant_fail_url = route('payment.fail');
$user_basket = base64_encode(json_encode([
['Web / Mobil Yazılım', $payment_amount, 1],
]));
$no_installment = 0;
$max_installment = 9;
$debug_on = 1;
$timeout_limit = 30;
$hash_str = $merchant_id.$user_ip.$merchant_oid.$email.$payment_amount.$user_basket.$no_installment.$max_installment.$currency.$test_mode;
$paytr_token = base64_encode(hash_hmac('sha256', $hash_str.$merchant_salt, $merchant_key, true));
$post_vals = [
'merchant_id' => $merchant_id,
'user_ip' => $user_ip,
'merchant_oid' => $merchant_oid,
'email' => $email,
'lang' => 'tr',
'payment_amount' => $payment_amount,
'paytr_token' => $paytr_token,
'user_basket' => $user_basket,
'debug_on' => $debug_on,
'no_installment' => $no_installment,
'max_installment' => $max_installment,
'user_name' => $user_name,
'user_address' => $user_address,
'user_phone' => $user_phone,
'merchant_ok_url' => $merchant_ok_url,
'merchant_fail_url' => $merchant_fail_url,
'timeout_limit' => $timeout_limit,
'currency' => $currency,
'test_mode' => $test_mode,
];
// Log request for debugging
Log::info('PayTR Request', [
'merchant_id' => $merchant_id,
'merchant_oid' => $merchant_oid,
'email' => $email,
'payment_amount' => $payment_amount,
'user_ip' => $user_ip,
]);
// Curl request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.paytr.com/odeme/api/get-token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_vals);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
$result = curl_exec($ch);
$curlError = curl_error($ch);
$curlErrno = curl_errno($ch);
curl_close($ch);
if ($curlErrno) {
Log::error('PayTR cURL error', ['errno' => $curlErrno, 'error' => $curlError]);
return back()->with('error', 'PAYTR bağlantı hatası. Lütfen tekrar deneyin.');
}
if ($result === false || empty($result)) {
Log::error('PayTR returned empty response');
return back()->with('error', 'Ödeme sisteminden yanıt alınamadı. Lütfen tekrar deneyin.');
}
// Log the raw response for debugging
Log::info('PayTR Raw Response', ['response' => $result]);
$result = json_decode($result, 1);
// Check if result is valid
if (!$result || !is_array($result)) {
Log::error('PayTR API returned invalid response', ['response' => $result]);
return back()->with('error', 'Ödeme sisteminden geçersiz yanıt alındı. Lütfen tekrar deneyin.');
}
if (!isset($result['status'])) {
Log::error('PayTR API response missing status', ['response' => $result]);
return back()->with('error', 'Ödeme sisteminden beklenmeyen yanıt alındı. Lütfen tekrar deneyin.');
}
if ($result['status'] == 'success') {
$token = $result['token'];
// Re-render the online-payment page but with the token
// We need to fetch the page content again
// Assuming the slug is 'online-odeme' or similar, or we can just pass a dummy page object
// if we are lazy, but correct way is to fetch the page associated with the route.
// Since we are POSTing here, we don't know the Page model context unless we fetch it.
// Let's try to assume 'online-odeme' which is common.
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if (! $page) {
$page = new Page(['title' => 'Online Ödeme', 'content' => '', 'excerpt' => 'Please complete your payment below.']);
}
return view('templates.corporate.online-payment', [
'page' => $page,
'paytr_token' => $token,
]);
} else {
return back()->with('error', 'PAYTR failed. reason:'.$result['reason']);
}
}
public function success(Request $request)
{
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if ($page) {
return redirect()->route('page.show', $page->slug)->with('success', 'Your payment has been charged successfully. This will appear on your credit card statement as "PAYTR ODEME HIZMETLERI"');
}
return redirect()->route('home')->with('success', 'Your payment has been charged successfully.');
}
public function fail(Request $request)
{
$page = Page::where('template', 'online-payment')->orWhere('slug', 'online-odeme')->first();
if ($page) {
return redirect()->route('page.show', $page->slug)->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
}
return redirect()->route('home')->with('error', 'Your payment has not been charged successfully! Please check your credit card information.');
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Models\Setting;
use App\Models\HeaderTemplate;
use App\Models\FooterTemplate;
use App\Services\TemplateService;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function show($slug)
{
$product = Product::where('slug', $slug)
->where('is_active', true)
->with('category')
->firstOrFail();
// Get Settings
$settings = new \stdClass();
$allSettings = Setting::where('is_active', true)->get();
foreach ($allSettings as $setting) {
$settings->{$setting->key} = $setting->value;
}
// --- Header Logic ---
$renderedHeader = null;
$defaultHeaderId = $settings->default_header ?? null;
if ($defaultHeaderId) {
$headerTemplate = HeaderTemplate::find($defaultHeaderId);
if ($headerTemplate) {
$templateDefaults = $headerTemplate->default_data ?? [];
$productData = []; // Ürünlere özel header datası eklenebilir
$mergedHeaderData = array_merge($templateDefaults, $productData);
$renderedHeader = TemplateService::replacePlaceholders(
$headerTemplate->html_content,
$mergedHeaderData,
$product // Model context
);
}
}
// --- Footer Logic ---
$renderedFooter = null;
$defaultFooterId = $settings->default_footer ?? null;
if ($defaultFooterId) {
$footerTemplate = FooterTemplate::find($defaultFooterId);
if ($footerTemplate) {
$templateDefaults = $footerTemplate->default_data ?? [];
$productData = []; // Ürünlere özel footer datası eklenebilir
$mergedFooterData = array_merge($templateDefaults, $productData);
$renderedFooter = TemplateService::replacePlaceholders(
$footerTemplate->html_content,
$mergedFooterData,
$product // Model context
);
}
}
$meta = [
'title' => $product->translate('title'),
'description' => \Str::limit(strip_tags($product->translate('content')), 160),
'image' => $product->hero_image ? asset('storage/' . $product->hero_image) : null,
];
// Use custom view template if specified
if ($product->view_template && view()->exists($product->view_template)) {
return view($product->view_template, compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
}
// Use landing page template (Hero only) for all products
return view('front.products.landing', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta'));
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers;
use App\Models\Page;
use App\Models\Blog;
use App\Models\Product;
use Illuminate\Http\Response;
class SitemapController extends Controller
{
public function index(): Response
{
$urls = [];
// 1. Static & Main Pages
$urls[] = [
'loc' => url('/'),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'daily',
'priority' => '1.0',
];
$urls[] = [
'loc' => route('blog.index'),
'lastmod' => now()->startOfDay()->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.8',
];
$urls[] = [
'loc' => route('career.index'),
'lastmod' => now()->startOfMonth()->toAtomString(),
'changefreq' => 'monthly',
'priority' => '0.5',
];
// 2. Dynamic Pages
$pages = Page::where('status', 'published')
->where('is_homepage', false)
->get();
foreach ($pages as $page) {
$urls[] = [
'loc' => url($page->slug),
'lastmod' => $page->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
];
}
// 3. Blog Posts
if (class_exists(Blog::class)) {
$posts = Blog::published()->get();
foreach ($posts as $post) {
$urls[] = [
'loc' => route('blog.show', $post->slug),
'lastmod' => $post->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.6',
];
}
}
// 4. Products
if (class_exists(Product::class)) {
$products = Product::where('is_active', true)->get();
foreach ($products as $product) {
$urls[] = [
'loc' => route('products.show', $product->slug),
'lastmod' => $product->updated_at->toAtomString(),
'changefreq' => 'weekly',
'priority' => '0.7',
];
}
}
return response()->view('sitemap', compact('urls'))
->header('Content-Type', 'text/xml');
}
}
@@ -0,0 +1,53 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureSecurityHeaders
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// HSTS (Strict-Transport-Security) - Enforce HTTPS for 1 year
// 'includeSubDomains' and 'preload' are optional but often recommended for full security.
// A+ score typically looks for this header with a long max-age.
if ($request->isSecure()) {
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
}
// X-Frame-Options: SAMEORIGIN (prevents clickjacking by allowing framing only by the same site)
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
// X-Content-Type-Options: nosniff (prevents MIME type sniffing)
$response->headers->set('X-Content-Type-Options', 'nosniff');
// Referrer-Policy: stringent check, usually 'strict-origin-when-cross-origin' is good
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// X-XSS-Protection: 1; mode=block (deprecated but still good for older browsers, some scanners check it)
$response->headers->set('X-XSS-Protection', '1; mode=block');
// Permissions-Policy: restrict dangerous features
// This is a bit strict, might need adjustment based on site features.
// For general sites: geolocation=(), microphone=(), camera=() is often safe.
$response->headers->set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// Content-Security-Policy (CSP) - Start with upgrade-insecure-requests to force HTTPS assets
// A full CSP can be tricky and break scripts. 'upgrade-insecure-requests' is safe and good for mixed content.
// We can add 'frame-ancestors' here too if X-Frame-Options is ignored by some.
// If the user wants A+, a basic CSP is often enough.
$csp = "upgrade-insecure-requests; block-all-mixed-content;";
$response->headers->set('Content-Security-Policy', $csp);
return $response;
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SuperAdminMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!auth()->check() || !auth()->user()->hasRole('super_admin')) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Unauthorized'], 403);
}
abort(403);
}
return $next($request);
}
}
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class BlogTopic extends Model
{
use HasFactory;
protected $fillable = [
'name',
'context',
'status',
'last_used_at',
];
protected $casts = [
'last_used_at' => 'datetime',
];
public function scopeActive($query)
{
return $query->where('status', 'active');
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CareerApplication extends Model
{
use HasFactory;
protected $fillable = [
'name',
'email',
'phone',
'type',
'cv_path',
'nda_path',
'contract_path',
'git_knowledge',
'ai_usage',
'message',
'status',
];
}
+7
View File
@@ -34,8 +34,10 @@ class Page extends Model
// Template System // Template System
'header_template_id', 'header_template_id',
'header_data', 'header_data',
'custom_header_blade',
'footer_template_id', 'footer_template_id',
'footer_data', 'footer_data',
'custom_footer_blade',
'sections_data', 'sections_data',
]; ];
@@ -78,6 +80,11 @@ class Page extends Model
return $this->hasMany(Page::class, 'parent_id'); return $this->hasMany(Page::class, 'parent_id');
} }
public function testimonials()
{
return $this->hasMany(Testimonial::class);
}
/** /**
* Get the header template for the page. * Get the header template for the page.
*/ */
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\HasTranslations;
class Product extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
protected $fillable = [
'title',
'slug',
'type',
'product_category_id',
'hero_image',
'content',
'view_template',
'external_url',
'sort_order',
'is_active',
'landing_page_data',
];
protected $translatable = [
'title',
'content',
];
protected $casts = [
'is_active' => 'boolean',
'title' => 'array',
'content' => 'array',
'landing_page_data' => 'array',
];
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
if (empty($model->title)) {
$translations = request()->input('translations');
$defaultLocale = app()->getLocale();
if (is_array($translations)) {
if (!empty($translations[$defaultLocale]['title'])) {
$model->title = $translations[$defaultLocale]['title'];
} else {
foreach ($translations as $locale => $fields) {
if (!empty($fields['title'])) {
$model->title = $fields['title'];
break;
}
}
}
}
if (empty($model->title)) {
$model->title = [];
}
}
});
}
public function category()
{
return $this->belongsTo(ProductCategory::class, 'product_category_id');
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Traits\HasTranslations;
class ProductCategory extends Model
{
use HasFactory, SoftDeletes, HasTranslations;
protected $fillable = [
'title',
'slug',
'parent_id',
'sort_order',
'is_active',
];
protected $translatable = [
'title',
];
protected $casts = [
'is_active' => 'boolean',
'title' => 'array',
];
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// Eğer title boşsa ve translations içinden bir title geliyorsa onu ata
if (empty($model->title)) {
$translations = request()->input('translations');
$defaultLocale = app()->getLocale();
if (is_array($translations)) {
// Önce varsayılan dildeki çeviriye bak
if (!empty($translations[$defaultLocale]['title'])) {
$model->title = $translations[$defaultLocale]['title'];
}
// Yoksa ilk bulduğu dolu çeviriyi al
else {
foreach ($translations as $locale => $fields) {
if (!empty($fields['title'])) {
$model->title = $fields['title'];
break;
}
}
}
}
// Hala boşsa boş bir array veya string ata (veritabanı hatasını önlemek için)
if (empty($model->title)) {
$model->title = [];
}
}
});
}
public function parent()
{
return $this->belongsTo(ProductCategory::class, 'parent_id');
}
public function children()
{
return $this->hasMany(ProductCategory::class, 'parent_id');
}
public function products()
{
return $this->hasMany(Product::class);
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class SiteTranslation extends Model
{
use HasFactory;
protected $fillable = [
'hash',
'key',
'translations',
];
protected $casts = [
'translations' => 'array',
];
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
if (empty($model->hash)) {
$model->hash = md5($model->key);
}
});
static::updating(function ($model) {
// Key değiştiyse hash'i güncelle
if ($model->isDirty('key')) {
$model->hash = md5($model->key);
}
});
static::saved(function ($model) {
Cache::forget("site_translation_{$model->hash}");
});
}
/**
* Get translation for a specific key
*/
public static function getTranslation(string $key): string
{
$hash = md5($key);
$locale = app()->getLocale();
// Cache'ten veya DB'den al
// Cache süresi: 24 saat
$translation = Cache::remember("site_translation_{$hash}", 60 * 60 * 24, function () use ($hash, $key) {
return static::firstOrCreate(
['hash' => $hash],
['key' => $key]
);
});
// Eğer kayıt yeni oluşturulduysa ve key eksikse (concurrency nedeniyle), key'i set et
if ($translation->wasRecentlyCreated && empty($translation->translations)) {
// Yeni oluşturuldu, henüz çeviri yok.
// Varsayılan olarak key'i döndür.
return $key;
}
$translations = $translation->translations ?? [];
if (isset($translations[$locale]) && !empty($translations[$locale])) {
return $translations[$locale];
}
return $key;
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Testimonial extends Model
{
protected $fillable = [
'page_id',
'name',
'position',
'content',
'rating',
'avatar',
'is_active',
'sort_order',
];
public function page()
{
return $this->belongsTo(Page::class);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\BlogCategory;
use Illuminate\Auth\Access\HandlesAuthorization;
class BlogCategoryPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:BlogCategory');
}
public function view(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('View:BlogCategory');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:BlogCategory');
}
public function update(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('Update:BlogCategory');
}
public function delete(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('Delete:BlogCategory');
}
public function restore(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('Restore:BlogCategory');
}
public function forceDelete(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('ForceDelete:BlogCategory');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:BlogCategory');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:BlogCategory');
}
public function replicate(AuthUser $authUser, BlogCategory $blogCategory): bool
{
return $authUser->can('Replicate:BlogCategory');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:BlogCategory');
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\ProductCategory;
use Illuminate\Auth\Access\HandlesAuthorization;
class ProductCategoryPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:ProductCategory');
}
public function view(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('View:ProductCategory');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:ProductCategory');
}
public function update(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('Update:ProductCategory');
}
public function delete(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('Delete:ProductCategory');
}
public function restore(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('Restore:ProductCategory');
}
public function forceDelete(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('ForceDelete:ProductCategory');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:ProductCategory');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:ProductCategory');
}
public function replicate(AuthUser $authUser, ProductCategory $productCategory): bool
{
return $authUser->can('Replicate:ProductCategory');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:ProductCategory');
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\Product;
use Illuminate\Auth\Access\HandlesAuthorization;
class ProductPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:Product');
}
public function view(AuthUser $authUser, Product $product): bool
{
return $authUser->can('View:Product');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:Product');
}
public function update(AuthUser $authUser, Product $product): bool
{
return $authUser->can('Update:Product');
}
public function delete(AuthUser $authUser, Product $product): bool
{
return $authUser->can('Delete:Product');
}
public function restore(AuthUser $authUser, Product $product): bool
{
return $authUser->can('Restore:Product');
}
public function forceDelete(AuthUser $authUser, Product $product): bool
{
return $authUser->can('ForceDelete:Product');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:Product');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:Product');
}
public function replicate(AuthUser $authUser, Product $product): bool
{
return $authUser->can('Replicate:Product');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:Product');
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use Illuminate\Foundation\Auth\User as AuthUser;
use App\Models\SiteTranslation;
use Illuminate\Auth\Access\HandlesAuthorization;
class SiteTranslationPolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:SiteTranslation');
}
public function view(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('View:SiteTranslation');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:SiteTranslation');
}
public function update(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('Update:SiteTranslation');
}
public function delete(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('Delete:SiteTranslation');
}
public function restore(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('Restore:SiteTranslation');
}
public function forceDelete(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('ForceDelete:SiteTranslation');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:SiteTranslation');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:SiteTranslation');
}
public function replicate(AuthUser $authUser, SiteTranslation $siteTranslation): bool
{
return $authUser->can('Replicate:SiteTranslation');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:SiteTranslation');
}
}
+7
View File
@@ -21,7 +21,14 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
setlocale(LC_ALL, 'tr_TR.UTF-8', 'tr_TR', 'turkish');
// Page model için observer kaydet
// Page model için observer kaydet // Page model için observer kaydet
Page::observe(PageObserver::class); Page::observe(PageObserver::class);
if ($this->app->environment('production') || $this->app->environment('staging')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
} }
} }
@@ -19,11 +19,24 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\StartSession; use Illuminate\Session\Middleware\StartSession;
use Illuminate\View\Middleware\ShareErrorsFromSession; use Illuminate\View\Middleware\ShareErrorsFromSession;
use Filament\Navigation\MenuItem;
use App\Models\Language;
use Illuminate\Support\Facades\Schema;
use App\Http\Middleware\SetLocale;
class AdminPanelProvider extends PanelProvider class AdminPanelProvider extends PanelProvider
{ {
public function panel(Panel $panel): Panel public function panel(Panel $panel): Panel
{ {
$languages = [];
try {
if (Schema::hasTable('languages')) {
$languages = Language::where('is_active', true)->get();
}
} catch (\Exception $e) {
// Migration çalışmamış olabilir
}
return $panel return $panel
->id('admin') ->id('admin')
->path('admin') ->path('admin')
@@ -55,11 +68,23 @@ class AdminPanelProvider extends PanelProvider
SubstituteBindings::class, SubstituteBindings::class,
DisableBladeIconComponents::class, DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class, DispatchServingFilamentEvent::class,
SetLocale::class,
]) ])
->authMiddleware([ ->authMiddleware([
Authenticate::class, Authenticate::class,
]) ])
->login() ->login()
->profile() // Profil sayfası ve şifre değiştirme aktif
->userMenuItems([
// Dilleri menüye ekle
...collect($languages)->map(function ($language) {
return MenuItem::make()
->label($language->native_name)
->url(url('/lang/' . $language->code))
->icon('heroicon-m-language')
->sort(100); // En sonda görünsün
})->toArray()
])
->assets([ ->assets([
\Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')), \Filament\Support\Assets\Css::make('citrus', resource_path('css/citrus.css')),
]); ]);
+180
View File
@@ -0,0 +1,180 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Storage;
class AiBlogService
{
protected $geminiKey;
protected $unsplashKey;
public function __construct()
{
$this->geminiKey = config('services.gemini.key', env('GEMINI_API_KEY'));
$this->unsplashKey = config('services.unsplash.access_key', env('UNSPLASH_ACCESS_KEY'));
}
/**
* Generate blog content using Gemini API
*/
public function generateContent(string $topic, ?string $context = null): array
{
$prompt = $this->buildPrompt($topic, $context);
// Debug logging
$keyStatus = empty($this->geminiKey) ? 'MISSING' : 'PRESENT (' . substr($this->geminiKey, 0, 5) . '...)';
Log::info("Gemini Service: Key status: $keyStatus");
try {
$response = Http::withoutVerifying()
->retry(3, 10000, function ($exception, $request) {
if ($exception instanceof \Illuminate\Http\Client\RequestException && $exception->response->status() === 429) {
return false;
}
return true;
})
->withHeaders(['Content-Type' => 'application/json'])
->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={$this->geminiKey}", [
'contents' => [
[
'parts' => [
['text' => $prompt]
]
]
],
'generationConfig' => [
'temperature' => 0.7,
'topK' => 40,
'topP' => 0.95,
'maxOutputTokens' => 8192,
'responseMimeType' => 'application/json',
]
]);
if ($response->failed()) {
Log::error('Gemini API Error Status: ' . $response->status());
Log::error('Gemini API Error Body: ' . $response->body());
throw new \Exception('Gemini API request failed: ' . $response->body());
}
$data = $response->json();
if (!isset($data['candidates'][0]['content']['parts'][0]['text'])) {
Log::error('Gemini API Invalid Response: ' . json_encode($data));
throw new \Exception('Invalid response from Gemini API.');
}
$jsonContent = $data['candidates'][0]['content']['parts'][0]['text'];
// Clean markdown json blocks if present
$jsonContent = str_replace(['```json', '```'], '', $jsonContent);
return json_decode($jsonContent, true);
} catch (\Exception $e) {
Log::error('AI Blog Generation Exception: ' . $e->getMessage());
throw $e;
}
}
/**
* Build the prompt for Gemini
*/
private function buildPrompt(string $topic, ?string $context = null): string
{
$contextStr = $context ? "Additional Context: {$context}" : "";
return <<<EOT
You are an expert content writer for "Trunçgil Teknoloji", a leading technology company specializing in web software, mobile apps, IoT, and digital transformation.
Write a comprehensive, SEO-friendly blog post about the topic: "{$topic}".
{$contextStr}
**IMPORTANT**: All content in the JSON response (title, content, excerpt, meta_data) MUST BE IN TURKISH language. Only the `image_prompt` should be in English.
Requirements:
1. **Format**: Return ONLY a valid JSON object. No markdown formatting outside the JSON structure.
2. **Language**: Turkish (Türkçe).
3. **Tone**: Professional, authoritative, yet accessible. Enthusiastic about technology.
4. **Brand Integration**: Naturally mention "Trunçgil Teknoloji" at least 2-3 times. Position Trunçgil Teknoloji as a solution provider or expert in this field. Use phrases like "Trunçgil Teknoloji olarak biz...", "Deneyimli ekibimizle...", etc.
5. **Structure**:
* `title`: Catchy, SEO-optimized title (Max 60 chars) - IN TURKISH.
* `meta_title`: SEO Meta Title - IN TURKISH.
* `meta_description`: SEO Meta Description (Max 160 chars) - IN TURKISH.
* `content`: Full HTML blog post content.
* Use <h2> and <h3> tags for headings.
* Use <p> for paragraphs.
* Include a bulleted list (<ul>/<li>) if appropriate.
* Do NOT use <h1> or <html>/<body> tags.
* Ensure the content is informative and valuable (min 600 words).
* ALL CONTENT MUST BE TURKISH.
* `excerpt`: A short summary (1-2 sentences) - IN TURKISH.
* `tags`: Array of 5-8 relevant tags (Turkish).
* `image_prompt`: A creative English prompt to generate a cover image for this article using an AI image generator.
JSON Schema:
{
"title": "string",
"meta_title": "string",
"meta_description": "string",
"content": "html_string",
"excerpt": "string",
"tags": ["string"],
"image_prompt": "string"
}
EOT;
}
/**
* Fetch and save a cover image
*/
public function fetchAndSaveImage(string $keyword, string $imagePrompt): ?string
{
$imageUrl = null;
// 1. Try Unsplash if key exists
if ($this->unsplashKey) {
try {
$response = Http::withoutVerifying()->get('https://api.unsplash.com/search/photos', [
'query' => $keyword,
'orientation' => 'landscape',
'per_page' => 1,
'client_id' => $this->unsplashKey
]);
if ($response->successful() && !empty($response['results'])) {
$imageUrl = $response['results'][0]['urls']['regular'];
}
} catch (\Exception $e) {
Log::warning('Unsplash API failed: ' . $e->getMessage());
}
}
// 2. Fallback to Pollinations AI (using the generated prompt)
if (!$imageUrl) {
// Encode the prompt
$encodedPrompt = urlencode($imagePrompt . " high quality, technological, photorealistic, 4k");
$imageUrl = "https://image.pollinations.ai/prompt/{$encodedPrompt}";
}
if (!$imageUrl) {
return null;
}
// Download and Save
try {
$imageContent = Http::withoutVerifying()->get($imageUrl)->body();
$filename = 'blog/ai-generated-' . Str::slug($keyword) . '-' . time() . '.jpg';
Storage::disk('public')->put($filename, $imageContent);
return $filename;
} catch (\Exception $e) {
Log::error('Image download failed: ' . $e->getMessage());
return null;
}
}
}
+1 -4
View File
@@ -107,7 +107,7 @@ class TemplateService
'url' => TextInput::make("{$dataKey}.{$placeholder}") 'url' => TextInput::make("{$dataKey}.{$placeholder}")
->label($label) ->label($label)
->url() // ->url() // Allow #, mailto, tel, relative paths
->maxLength(500), ->maxLength(500),
'tel' => TextInput::make("{$dataKey}.{$placeholder}") 'tel' => TextInput::make("{$dataKey}.{$placeholder}")
@@ -173,8 +173,6 @@ class TemplateService
->image() ->image()
->disk('public') ->disk('public')
->directory('templates/images') ->directory('templates/images')
->imageEditor()
->imageEditorAspectRatios([null, '16:9', '4:3', '1:1'])
->maxSize(5120), ->maxSize(5120),
'images' => FileUpload::make("{$dataKey}.{$placeholder}") 'images' => FileUpload::make("{$dataKey}.{$placeholder}")
@@ -183,7 +181,6 @@ class TemplateService
->disk('public') ->disk('public')
->directory('templates/images') ->directory('templates/images')
->multiple() ->multiple()
->imageEditor()
->maxSize(5120) ->maxSize(5120)
->maxFiles(10), ->maxFiles(10),
+34 -5
View File
@@ -42,22 +42,51 @@ trait HasTranslations
{ {
$languageCode = $languageCode ?? $this->getCurrentLanguageCode(); $languageCode = $languageCode ?? $this->getCurrentLanguageCode();
// 1. Try translations table
$translation = $this->getTranslation($fieldName, $languageCode); $translation = $this->getTranslation($fieldName, $languageCode);
if ($translation) { if ($translation && !empty($translation->field_value)) {
return $translation->field_value; return $translation->field_value;
} }
// Fallback to default language // 2. Fallback to default language from translations table
if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) { if ($fallback && $languageCode !== $this->getDefaultLanguageCode()) {
$defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode()); $defaultTranslation = $this->getTranslation($fieldName, $this->getDefaultLanguageCode());
if ($defaultTranslation) { if ($defaultTranslation && !empty($defaultTranslation->field_value)) {
return $defaultTranslation->field_value; return $defaultTranslation->field_value;
} }
} }
// Fallback to original field value // 3. Fallback to original field value
return $this->{$fieldName} ?? null; $originalValue = $this->{$fieldName} ?? null;
// Handle JSON string if not casted
if (is_string($originalValue) && (str_starts_with($originalValue, '{') || str_starts_with($originalValue, '['))) {
$decoded = json_decode($originalValue, true);
if (json_last_error() === JSON_ERROR_NONE) {
$originalValue = $decoded;
}
}
// If the original value is an array (e.g. JSON cast), try to find translation inside it
if (is_array($originalValue)) {
if (isset($originalValue[$languageCode]) && !empty($originalValue[$languageCode])) {
return $originalValue[$languageCode];
}
if ($fallback && isset($originalValue[$this->getDefaultLanguageCode()]) && !empty($originalValue[$this->getDefaultLanguageCode()])) {
return $originalValue[$this->getDefaultLanguageCode()];
}
// Return first non-empty value if available
foreach ($originalValue as $val) {
if (!empty($val)) {
return $val;
}
}
return '';
}
return $originalValue;
} }
/** /**
+1
View File
@@ -13,6 +13,7 @@ return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware): void { ->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [ $middleware->web(append: [
\App\Http\Middleware\SetLocale::class, \App\Http\Middleware\SetLocale::class,
\App\Http\Middleware\EnsureSecurityHeaders::class,
]); ]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
Generated
+676 -715
View File
File diff suppressed because it is too large Load Diff
+186
View File
@@ -0,0 +1,186 @@
<?php
return [
/*
|---------------------------------------------------------------------------
| Class Namespace
|---------------------------------------------------------------------------
|
| This value sets the root class namespace for Livewire component classes in
| your application. This value will change where component auto-discovery
| finds components. It's also referenced by the file creation commands.
|
*/
'class_namespace' => 'App\\Livewire',
/*
|---------------------------------------------------------------------------
| View Path
|---------------------------------------------------------------------------
|
| This value is used to specify where Livewire component Blade templates are
| stored when running file creation commands like `artisan make:livewire`.
| It is also used if you choose to omit a component's render() method.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|---------------------------------------------------------------------------
| Layout
|---------------------------------------------------------------------------
| The view that will be used as the layout when rendering a single component
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
| In this case, the view returned by CreatePost will render into $slot.
|
*/
'layout' => 'components.layouts.app',
/*
|---------------------------------------------------------------------------
| Lazy Loading Placeholder
|---------------------------------------------------------------------------
| Livewire allows you to lazy load components that would otherwise slow down
| the initial page load. Every component can have a custom placeholder or
| you can define the default placeholder view for all components below.
|
*/
'lazy_placeholder' => null,
/*
|---------------------------------------------------------------------------
| Temporary File Uploads
|---------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is stored permanently. All file uploads are directed to
| a global endpoint for temporary storage. You may configure this below:
|
*/
'temporary_file_upload' => [
'disk' => 'public', // Example: 'local', 's3' | Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => 'livewire-tmp', // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
],
/*
|---------------------------------------------------------------------------
| Render On Redirect
|---------------------------------------------------------------------------
|
| This value determines if Livewire will run a component's `render()` method
| after a redirect has been triggered using something like `redirect(...)`
| Setting this to true will render the view once more before redirecting
|
*/
'render_on_redirect' => false,
/*
|---------------------------------------------------------------------------
| Eloquent Model Binding
|---------------------------------------------------------------------------
|
| Previous versions of Livewire supported binding directly to eloquent model
| properties using wire:model by default. However, this behavior has been
| deemed too "magical" and has therefore been put under a feature flag.
|
*/
'legacy_model_binding' => false,
/*
|---------------------------------------------------------------------------
| Auto-inject Frontend Assets
|---------------------------------------------------------------------------
|
| By default, Livewire automatically injects its JavaScript and CSS into the
| <head> and <body> of pages containing Livewire components. By disabling
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
*/
'inject_assets' => true,
/*
|---------------------------------------------------------------------------
| Navigate (SPA mode)
|---------------------------------------------------------------------------
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
| will prevent the default link handling and instead request those pages
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
*/
'navigate' => [
'show_progress_bar' => true,
'progress_bar_color' => '#2299dd',
],
/*
|---------------------------------------------------------------------------
| HTML Morph Markers
|---------------------------------------------------------------------------
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
| after each update. To make this process more reliable, Livewire injects
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
*/
'inject_morph_markers' => true,
/*
|---------------------------------------------------------------------------
| Smart Wire Keys
|---------------------------------------------------------------------------
|
| Livewire uses loops and keys used within loops to generate smart keys that
| are applied to nested components that don't have them. This makes using
| nested components more reliable by ensuring that they all have keys.
|
*/
'smart_wire_keys' => false,
/*
|---------------------------------------------------------------------------
| Pagination Theme
|---------------------------------------------------------------------------
|
| When enabling Livewire's pagination feature by using the `WithPagination`
| trait, Livewire will use Tailwind templates to render pagination views
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
*/
'pagination_theme' => 'tailwind',
/*
|---------------------------------------------------------------------------
| Release Token
|---------------------------------------------------------------------------
|
| This token is stored client-side and sent along with each request to check
| a users session to see if a new release has invalidated it. If there is
| a mismatch it will throw an error and prompt for a browser refresh.
|
*/
'release_token' => 'a',
];
+8
View File
@@ -35,4 +35,12 @@ return [
], ],
], ],
'gemini' => [
'key' => env('GEMINI_API_KEY'),
],
'unsplash' => [
'access_key' => env('UNSPLASH_ACCESS_KEY'),
],
]; ];
@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ProductCategory>
*/
class ProductCategoryFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Product>
*/
class ProductFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('product_categories', function (Blueprint $table) {
$table->id();
$table->json('title'); // Translatable
$table->string('slug')->unique();
$table->foreignId('parent_id')->nullable()->constrained('product_categories')->nullOnDelete();
$table->integer('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('product_categories');
}
};
@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->json('title'); // Translatable
$table->string('slug')->unique();
$table->enum('type', ['product', 'service'])->default('product');
$table->foreignId('product_category_id')->nullable()->constrained('product_categories')->nullOnDelete();
$table->string('hero_image')->nullable();
$table->json('content')->nullable(); // Translatable
$table->string('view_template')->nullable(); // Custom view path
$table->integer('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->softDeletes();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('site_translations', function (Blueprint $table) {
$table->id();
$table->string('hash')->unique()->index(); // MD5 hash of the key
$table->text('key'); // Original text
$table->json('translations')->nullable(); // JSON data: {"en": "...", "tr": "..."}
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('site_translations');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('imports', function (Blueprint $table): void {
$table->id();
$table->timestamp('completed_at')->nullable();
$table->string('file_name');
$table->string('file_path');
$table->string('importer');
$table->unsignedInteger('processed_rows')->default(0);
$table->unsignedInteger('total_rows');
$table->unsignedInteger('successful_rows')->default(0);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('imports');
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('exports', function (Blueprint $table): void {
$table->id();
$table->timestamp('completed_at')->nullable();
$table->string('file_disk');
$table->string('file_name')->nullable();
$table->string('exporter');
$table->unsignedInteger('processed_rows')->default(0);
$table->unsignedInteger('total_rows');
$table->unsignedInteger('successful_rows')->default(0);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('exports');
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('failed_import_rows', function (Blueprint $table): void {
$table->id();
$table->json('data');
$table->foreignId('import_id')->constrained()->cascadeOnDelete();
$table->text('validation_error')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('failed_import_rows');
}
};
@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->string('custom_header_blade')->nullable()->after('header_data');
$table->string('custom_footer_blade')->nullable()->after('footer_data');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn(['custom_header_blade', 'custom_footer_blade']);
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->json('landing_page_data')->nullable()->after('content');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('landing_page_data');
});
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->string('external_url')->nullable()->after('view_template');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('external_url');
});
}
};
@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('testimonials', function (Blueprint $table) {
$table->id();
$table->foreignId('page_id')->nullable()->constrained()->onDelete('cascade');
$table->string('name');
$table->string('position')->nullable();
$table->text('content');
$table->integer('rating')->default(5);
$table->string('avatar')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('sort_order')->default(0);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('testimonials');
}
};
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('visions', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('visions');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->json('data')->nullable()->after('sections');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('data')->nullable();
});
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('blog_topics', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('context')->nullable(); // Additional context/prompt for AI
$table->string('status')->default('active'); // active, passive
$table->timestamp('last_used_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('blog_topics');
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('career_applications', function (Blueprint $blueprint) {
$blueprint->id();
$blueprint->string('name');
$blueprint->string('email');
$blueprint->string('phone')->nullable();
$blueprint->string('cv_path');
$blueprint->text('message')->nullable();
$blueprint->string('status')->default('pending'); // pending, reviewed, rejected, accepted
$blueprint->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('career_applications');
}
};
@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->string('type')->default('job')->after('phone'); // job, internship
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn('type');
});
}
};
@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->string('nda_path')->nullable()->after('cv_path');
$table->string('contract_path')->nullable()->after('nda_path');
$table->boolean('git_knowledge')->default(false)->after('contract_path');
$table->boolean('ai_usage')->default(false)->after('git_knowledge');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('career_applications', function (Blueprint $table) {
$table->dropColumn(['nda_path', 'contract_path', 'git_knowledge', 'ai_usage']);
});
}
};
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class BlogTopicSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$topics = [
'Yapay Zeka ve İş Dünyası',
'Mobil Uygulama Geliştirme Trendleri',
'E-Ticaretin Geleceği',
'Siber Güvenlik Önlemleri',
'Blockchain Teknolojisi',
'Bulut Bilişim Avantajları',
'Nesnelerin İnterneti (IoT) Uygulamaları',
'Veri Analitiği ve Büyük Veri',
'Dijital Dönüşüm Stratejileri',
'Web Yazılım Teknolojileri'
];
foreach ($topics as $topic) {
DB::table('blog_topics')->insert([
'name' => $topic,
'context' => 'Trunçgil Teknoloji çözümlerine ve uzmanlığına vurgu yapın.',
'status' => 'active',
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
+2
View File
@@ -15,7 +15,9 @@ class DatabaseSeeder extends Seeder
{ {
// Settings seed // Settings seed
$this->call([ $this->call([
LanguageSeeder::class,
SettingSeeder::class, SettingSeeder::class,
PageSeeder::class,
]); ]);
// User::factory(10)->create(); // User::factory(10)->create();
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace Database\Seeders;
use App\Models\Page;
use App\Models\User;
use Illuminate\Database\Seeder;
class LogosPageSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$author = User::first();
if (!$author) return;
$corporate = Page::where('slug', 'kurumsal')->first();
if (!$corporate) return;
Page::updateOrCreate(
['slug' => 'logolarimiz'],
[
'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Logolarımız',
'excerpt' => 'Kurumsal logolarımız ve kullanım standartları',
'content' => null,
'template' => 'corporate.logos',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 6,
'published_at' => now(),
]
);
$this->command->info('✅ Logolarımız sayfası başarıyla eklendi!');
}
}
+196 -241
View File
@@ -4,7 +4,6 @@ namespace Database\Seeders;
use App\Models\Page; use App\Models\Page;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Str;
class PageSeeder extends Seeder class PageSeeder extends Seeder
{ {
@@ -13,7 +12,7 @@ class PageSeeder extends Seeder
*/ */
public function run(): void public function run(): void
{ {
// İlk kullanıcıyı bul veya oluştur (author_id için gerekli) // İlk kullanıcıyı bul veya oluştur
$author = \App\Models\User::first(); $author = \App\Models\User::first();
if (!$author) { if (!$author) {
$author = \App\Models\User::create([ $author = \App\Models\User::create([
@@ -23,99 +22,172 @@ class PageSeeder extends Seeder
]); ]);
} }
$pages = [ // 1. Anasayfa
// Home Page $home = Page::updateOrCreate(
['slug' => 'home'],
[ [
'author_id' => $author->id, 'author_id' => $author->id,
'title' => 'Anasayfa', 'title' => 'Anasayfa',
'slug' => 'home',
'excerpt' => 'Modern ve yenilikçi teknoloji çözümleri', 'excerpt' => 'Modern ve yenilikçi teknoloji çözümleri',
'content' => null, 'content' => null,
'template' => 'home', 'template' => 'home',
'status' => 'published', 'status' => 'published',
'is_homepage' => true, 'is_homepage' => true,
'show_in_menu' => false,
'sort_order' => 1,
'published_at' => now(),
'meta_title' => 'Truncgil Teknoloji - Kurumsal',
'meta_description' => 'Truncgil Teknoloji kurumsal web sitesi.',
'sections' => [], // Sections will be handled by the template view for now or can be added here
]
);
// 2. Kurumsal (Parent)
$corporate = Page::updateOrCreate(
['slug' => 'kurumsal'],
[
'author_id' => $author->id,
'title' => 'Kurumsal',
'excerpt' => 'Kurumsal bilgilerimiz',
'content' => null,
'template' => 'generic',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true, 'show_in_menu' => true,
'sort_order' => 1, 'sort_order' => 1,
'published_at' => now(), 'published_at' => now(),
'meta_title' => 'Truncgil Citrus - Modern Web Çözümleri', ]
'meta_description' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz. Modern web uygulamaları ve dijital dönüşüm hizmetleri.', );
'sections' => [
[
'type' => 'hero',
'data' => [
'background_image' => 'assets/img/photos/blurry.png',
'badge' => 'YENİ PLATFORM',
'title' => 'Modern ve Çok Amaçlı <span class="text-[#e31e24]">Web Çözümleri</span>',
'subtitle' => 'Yenilikçi teknoloji çözümleri ile geleceği şekillendiriyoruz. Projelerinizi en son teknolojilerle hayata geçiriyoruz.',
'primary_button_text' => 'Hemen Başla',
'primary_button_url' => '#features',
'secondary_button_text' => 'Daha Fazla Bilgi',
'secondary_button_url' => '/about',
'hero_image' => 'assets/img/demos/f1.png',
],
],
[
'type' => 'features',
'data' => [
'bg_class' => '!bg-[#f0f0f8]',
'section_badge' => 'ÖZELLİKLER',
'section_title' => 'Neden Truncgil Citrus?',
'section_subtitle' => 'Modern teknolojiler ve uzman ekibimizle projelerinizi hayata geçiriyoruz',
'column_class' => 'md:w-6/12 lg:w-4/12',
'features' => [
[
'icon' => 'assets/img/demos/fi1.png',
'title' => 'Hızlı Çözümler',
'description' => 'Modern teknolojiler kullanarak projelerinizi hızlı ve verimli bir şekilde hayata geçiriyoruz.',
],
[
'icon' => 'assets/img/demos/fi2.png',
'title' => 'Güvenli Altyapı',
'description' => 'En son güvenlik standartlarını kullanarak verilerinizi koruma altına alıyoruz.',
],
[
'icon' => 'assets/img/demos/fi3.png',
'title' => 'Uzman Ekip',
'description' => 'Deneyimli ve uzman ekibimiz ile her zaman yanınızdayız.',
],
],
],
],
[
'type' => 'stats',
'data' => [
'bg_class' => '!bg-[#ffffff]',
'column_class' => 'md:w-6/12 lg:w-3/12',
'stats' => [
['number' => '500+', 'label' => 'Tamamlanan Proje'],
['number' => '300+', 'label' => 'Mutlu Müşteri'],
['number' => '50+', 'label' => 'Kazanılan Ödül'],
['number' => '25+', 'label' => 'Ekip Üyesi'],
],
],
],
[
'type' => 'cta',
'data' => [
'bg_class' => 'overflow-hidden',
'background_image' => 'assets/img/photos/blurry.png',
'icon' => 'assets/img/demos/icon-grape.png',
'title' => 'Benzersiz düşünün ve <span class="text-[#e31e24]">fark yaratın</span>',
'subtitle' => 'Binlerce müşterimiz tarafından güveniliyoruz. Siz de katılın ve projelerinizi hayata geçirin.',
'button_text' => 'İletişime Geç',
'button_url' => '/contact',
'button_icon' => 'uil uil-arrow-up-right',
],
],
],
],
// About Page // 2.1 Hakkımızda (Child of Kurumsal)
Page::updateOrCreate(
['slug' => 'hakkimizda'],
[ [
'author_id' => $author->id, 'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Hakkımızda', 'title' => 'Hakkımızda',
'slug' => 'about', 'excerpt' => 'Biz kimiz?',
'excerpt' => 'Truncgil Citrus olarak yenilikçi teknoloji çözümleri sunuyoruz', 'content' => '<p>Hakkımızda içeriği...</p>',
'template' => 'generic',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 1,
'published_at' => now(),
]
);
// 2.2 Vizyon & Misyon (Child of Kurumsal)
Page::updateOrCreate(
['slug' => 'vizyon-misyon'],
[
'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Vizyon & Misyon',
'excerpt' => 'Gelecek hedeflerimiz',
'content' => '<p>Vizyon ve Misyon içeriği...</p>',
'template' => 'generic',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 2,
'published_at' => now(),
]
);
// 2.3 Kitaplarımız (Child of Kurumsal)
Page::updateOrCreate(
['slug' => 'kitaplarimiz'],
[
'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Kitaplarımız',
'excerpt' => 'Yayınladığımız kitaplar',
'content' => null,
'template' => 'books.books',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 3,
'published_at' => now(),
]
);
// 2.4 Banka Bilgilerimiz (Child of Kurumsal)
Page::updateOrCreate(
['slug' => 'banka-bilgilerimiz'],
[
'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Banka Bilgilerimiz',
'excerpt' => 'Banka hesap bilgilerimiz',
'content' => '<p>Banka hesap bilgilerimiz burada yer alacaktır.</p>',
'template' => 'generic',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 4,
'published_at' => now(),
]
);
// 2.5 Müşteri Görüşleri (Child of Kurumsal)
$testimonialsPage = Page::updateOrCreate(
['slug' => 'musteri-gorusleri'],
[
'author_id' => $author->id,
'parent_id' => $corporate->id,
'title' => 'Müşteri Görüşleri',
'excerpt' => 'Müşterilerimizin hakkımızdaki düşünceleri',
'content' => null,
'template' => 'corporate.testimonials',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 5,
'published_at' => now(),
]
);
// Varsayılan müşteri görüşlerini ekle
$defaultTestimonials = [
[
'name' => 'Kerem Can Azak',
'position' => 'Stellar Construction',
'content' => 'Dijital dönüşüm yolculuğumuzda yanımızda oldukları için mutluyuz. Profesyonel yaklaşımları ve çözüm odaklı çalışmaları ile projelerimize değer kattılar.',
'rating' => 5,
'sort_order' => 1,
],
[
'name' => 'Servet Demir',
'position' => 'Dijimind Akademi',
'content' => 'Eğitim teknolojileri alanındaki vizyoner bakış açıları ve teknik altyapı konusundaki uzmanlıkları sayesinde hedeflediğimiz kitleye çok daha etkili bir şekilde ulaştık.',
'rating' => 5,
'sort_order' => 2,
],
[
'name' => 'A. Cezmi Savaş',
'position' => 'Rhapsode Akademik Yayınevi',
'content' => 'Akademik yayıncılık süreçlerimizi dijitalleştirirken sundukları yenilikçi çözümler ve hızlı destekleri için teşekkür ederiz. Güvenilir bir iş ortağı.',
'rating' => 5,
'sort_order' => 3,
],
];
foreach ($defaultTestimonials as $testimonialData) {
\App\Models\Testimonial::updateOrCreate(
['page_id' => $testimonialsPage->id, 'name' => $testimonialData['name']],
$testimonialData
);
}
// 3. Ürünlerimiz (Mega Menu Target)
Page::updateOrCreate(
['slug' => 'urunlerimiz'],
[
'author_id' => $author->id,
'title' => 'Ürünlerimiz',
'excerpt' => 'Ürün ve hizmetlerimiz',
'content' => null, 'content' => null,
'template' => 'generic', 'template' => 'generic',
'status' => 'published', 'status' => 'published',
@@ -123,194 +195,77 @@ class PageSeeder extends Seeder
'show_in_menu' => true, 'show_in_menu' => true,
'sort_order' => 2, 'sort_order' => 2,
'published_at' => now(), 'published_at' => now(),
'meta_title' => 'Hakkımızda - Truncgil Citrus', ]
'meta_description' => 'Truncgil Citrus olarak yenilikçi teknoloji çözümleri sunuyor, dijital dönüşüm süreçlerinizde yanınızdayız.', );
'sections' => [
[
'type' => 'hero',
'data' => [
'badge' => 'HAKKIMIZDA',
'title' => 'Yenilikçi Teknoloji <span class="text-[#e31e24]">Çözümleri</span>',
'subtitle' => '2010 yılından bu yana dijital dönüşüm süreçlerinde lider konumdayız',
],
],
[
'type' => 'features',
'data' => [
'bg_class' => '!bg-[#ffffff]',
'section_title' => 'Değerlerimiz',
'section_subtitle' => 'İş süreçlerimizi şekillendiren temel prensiplerimiz',
'column_class' => 'md:w-6/12 lg:w-3/12',
'features' => [
['icon' => 'assets/img/demos/fi1.png', 'title' => 'İnovasyon', 'description' => 'Sürekli yenilik ve gelişim'],
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Kalite', 'description' => 'En yüksek standartlarda hizmet'],
['icon' => 'assets/img/demos/fi3.png', 'title' => 'Güvenilirlik', 'description' => 'Zamanında ve eksiksiz teslimat'],
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Destek', 'description' => '7/24 müşteri desteği'],
],
],
],
],
],
// Services Page // 4. Kariyer
Page::updateOrCreate(
['slug' => 'kariyer'],
[ [
'author_id' => $author->id, 'author_id' => $author->id,
'title' => 'Hizmetlerimiz', 'title' => 'Kariyer',
'slug' => 'services', 'excerpt' => 'Kariyer fırsatları',
'excerpt' => 'Geniş yelpazede teknoloji hizmetleri sunuyoruz', 'content' => '<p>Kariyer içeriği...</p>',
'content' => null,
'template' => 'generic', 'template' => 'generic',
'status' => 'published', 'status' => 'published',
'is_homepage' => false, 'is_homepage' => false,
'show_in_menu' => true, 'show_in_menu' => true,
'sort_order' => 3, 'sort_order' => 3,
'published_at' => now(), 'published_at' => now(),
'meta_title' => 'Hizmetlerimiz - Truncgil Citrus', ]
'meta_description' => 'Web tasarım, mobil uygulama geliştirme, e-ticaret çözümleri ve dijital pazarlama hizmetlerimizi keşfedin.', );
'sections' => [
[
'type' => 'hero',
'data' => [
'badge' => 'HİZMETLER',
'title' => 'Dijital Dönüşüm <span class="text-[#e31e24]">Çözümleri</span>',
'subtitle' => 'İşinizi bir üst seviyeye taşıyacak teknoloji hizmetlerimiz',
],
],
[
'type' => 'features',
'data' => [
'bg_class' => '!bg-[#f0f0f8]',
'section_title' => 'Sunduğumuz Hizmetler',
'column_class' => 'md:w-6/12 lg:w-4/12',
'features' => [
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Web Tasarım & Geliştirme', 'description' => 'Modern, responsive ve SEO uyumlu web siteleri'],
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Mobil Uygulama', 'description' => 'iOS ve Android için native ve cross-platform uygulamalar'],
['icon' => 'assets/img/demos/fi3.png', 'title' => 'E-Ticaret Çözümleri', 'description' => 'Entegre ödeme sistemleri ile online satış platformları'],
['icon' => 'assets/img/demos/fi4.png', 'title' => 'Dijital Pazarlama', 'description' => 'SEO, SEM ve sosyal medya yönetimi'],
['icon' => 'assets/img/demos/fi1.png', 'title' => 'Kurumsal Yazılım', 'description' => 'İşletmenize özel CRM, ERP ve özel yazılım çözümleri'],
['icon' => 'assets/img/demos/fi2.png', 'title' => 'Bulut Hizmetleri', 'description' => 'Güvenli ve ölçeklenebilir bulut altyapısı'],
],
],
],
],
],
// Contact Page // 5. Neler Yaparız
Page::updateOrCreate(
['slug' => 'neler-yapariz'],
[ [
'author_id' => $author->id, 'author_id' => $author->id,
'title' => 'İletişim', 'title' => 'Neler Yaparız',
'slug' => 'contact', 'excerpt' => 'Hizmetlerimiz ve çözümlerimiz',
'excerpt' => 'Projeleriniz hakkında bizimle iletişime geçin',
'content' => null, 'content' => null,
'template' => 'generic', 'template' => 'neler-yapariz',
'status' => 'published', 'status' => 'published',
'is_homepage' => false, 'is_homepage' => false,
'show_in_menu' => true, 'show_in_menu' => true,
'sort_order' => 4, 'sort_order' => 4,
'published_at' => now(), 'published_at' => now(),
'meta_title' => 'İletişim - Truncgil Citrus', ]
'meta_description' => 'Projeleriniz hakkında konuşmak için bizimle iletişime geçin. Uzman ekibimiz size yardımcı olmaktan mutluluk duyar.', );
'sections' => [
[
'type' => 'hero',
'data' => [
'badge' => 'İLETİŞİM',
'title' => 'Projelerinizi <span class="text-[#e31e24]">Konuşalım</span>',
'subtitle' => 'Size özel çözümler geliştirmek için bizimle iletişime geçin',
],
],
[
'type' => 'contact',
'data' => [
'bg_class' => '!bg-[#f0f0f8]',
'title' => 'Bizimle İletişime Geçin',
'subtitle' => 'Projeleriniz hakkında konuşmak için bize ulaşın',
'info' => [
'address' => 'Merkez Mah. Teknoloji Cad. No:123 İstanbul',
'phone' => '+90 (212) 555 1234',
'email' => 'info@truncgil.com',
],
'form_action' => '/contact/submit',
'button_text' => 'Gönder',
],
],
],
],
// Pricing Page (örnek) // 6. Blog
Page::updateOrCreate(
['slug' => 'blog'],
[ [
'author_id' => $author->id, 'author_id' => $author->id,
'title' => 'Fiyatlandırma', 'title' => 'Blog',
'slug' => 'pricing', 'excerpt' => 'Güncel haberler ve makaleler',
'excerpt' => 'Size uygun paketi seçin',
'content' => null, 'content' => null,
'template' => 'generic', 'template' => 'blog',
'status' => 'published', 'status' => 'published',
'is_homepage' => false, 'is_homepage' => false,
'show_in_menu' => false, 'show_in_menu' => true,
'sort_order' => 5, 'sort_order' => 5,
'published_at' => now(), 'published_at' => now(),
'meta_title' => 'Fiyatlandırma - Truncgil Citrus', ]
'meta_description' => 'İhtiyaçlarınıza uygun web tasarım ve yazılım geliştirme paketlerimizi inceleyin.',
'sections' => [
[
'type' => 'hero',
'data' => [
'badge' => 'FİYATLANDIRMA',
'title' => 'Size Uygun <span class="text-[#e31e24]">Paketi Seçin</span>',
'subtitle' => 'Tüm paketler 30 gün para iade garantisi ile geliyor',
],
],
[
'type' => 'pricing',
'data' => [
'bg_class' => '!bg-[#f0f0f8]',
'section_title' => 'Web Tasarım Paketleri',
'column_class' => 'md:w-6/12 lg:w-4/12',
'featured_label' => 'Önerilen',
'plans' => [
[
'name' => 'Başlangıç',
'currency' => '₺',
'price' => '999',
'period' => 'ay',
'features' => ['5 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', '7/24 Destek'],
'button_text' => 'Başla',
'button_url' => '/contact',
],
[
'name' => 'Profesyonel',
'currency' => '₺',
'price' => '2499',
'period' => 'ay',
'featured' => true,
'features' => ['15 Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Yönetim Paneli', '7/24 Öncelikli Destek'],
'button_text' => 'Başla',
'button_url' => '/contact',
],
[
'name' => 'Kurumsal',
'currency' => '₺',
'price' => '4999',
'period' => 'ay',
'features' => ['Sınırsız Sayfa', 'Mobil Uyumlu', 'SEO Optimizasyonu', 'Gelişmiş Yönetim Paneli', 'Özel Entegrasyonlar', '7/24 Öncelikli Destek'],
'button_text' => 'Başla',
'button_url' => '/contact',
],
],
],
],
],
],
];
foreach ($pages as $pageData) {
Page::updateOrCreate(
['slug' => $pageData['slug']],
$pageData
); );
}
$this->command->info('✅ ' . count($pages) . ' sayfa başarıyla oluşturuldu!'); // 7. İletişim
Page::updateOrCreate(
['slug' => 'iletisim'],
[
'author_id' => $author->id,
'title' => 'İletişim',
'excerpt' => 'Bize ulaşın',
'content' => null,
'template' => 'contact',
'status' => 'published',
'is_homepage' => false,
'show_in_menu' => true,
'sort_order' => 6,
'published_at' => now(),
]
);
$this->command->info('✅ Menu yapısı ve sayfalar başarıyla oluşturuldu!');
} }
} }
+10
View File
@@ -659,6 +659,16 @@ class SettingSeeder extends Seeder
'is_public' => false, 'is_public' => false,
'is_active' => true, 'is_active' => true,
], ],
[
'key' => 'seo_google_search_console',
'value' => '',
'type' => 'string',
'group' => 'seo',
'label' => 'Google Search Console',
'description' => 'Google Search Console doğrulama kodu',
'is_public' => false,
'is_active' => true,
],
[ [
'key' => 'seo_google_tag_manager_id', 'key' => 'seo_google_tag_manager_id',
'value' => '', 'value' => '',

Some files were not shown because too many files have changed in this diff Show More