From 97092dd793084e62de57b95ff0b4f2a3f3a26113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Fri, 2 Jan 2026 20:58:18 +0300 Subject: [PATCH] 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. --- .../Products/Schemas/LandingPageSection.php | 228 ++++++++++++++ .../Products/Schemas/ProductForm.php | 4 + app/Http/Controllers/ProductController.php | 7 + app/Models/Product.php | 2 + ...dd_landing_page_data_to_products_table.php | 28 ++ lang/en/products.php | 37 +++ lang/tr/products.php | 37 +++ .../views/front/products/landing.blade.php | 295 ++++++++++++++++++ resources/views/layouts/site.blade.php | 89 +++++- resources/views/templates/home.blade.php | 6 +- 10 files changed, 720 insertions(+), 13 deletions(-) create mode 100644 app/Filament/Admin/Resources/Products/Schemas/LandingPageSection.php create mode 100644 database/migrations/2026_01_02_115025_add_landing_page_data_to_products_table.php create mode 100644 resources/views/front/products/landing.blade.php diff --git a/app/Filament/Admin/Resources/Products/Schemas/LandingPageSection.php b/app/Filament/Admin/Resources/Products/Schemas/LandingPageSection.php new file mode 100644 index 0000000..e8c587d --- /dev/null +++ b/app/Filament/Admin/Resources/Products/Schemas/LandingPageSection.php @@ -0,0 +1,228 @@ + 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(), + + // App Features Section + Section::make(__('products.app_features_section')) + ->schema([ + Repeater::make('landing_page_data.features') + ->label(__('products.features')) + ->schema([ + Select::make('icon') + ->label(__('products.feature_icon')) + ->options($iconOptions) + ->searchable() + ->required(), + TextInput::make('title') + ->label(__('products.feature_title')) + ->required() + ->columnSpanFull(), + Textarea::make('description') + ->label(__('products.feature_description')) + ->rows(2) + ->columnSpanFull(), + ]) + ->columns(1) + ->defaultItems(0) + ->addActionLabel(__('products.add_feature')) + ->reorderable() + ->collapsible() + ->itemLabel(fn (array $state): ?string => $state['title'] ?? __('products.feature') . ' #' . ($state['_index'] ?? '')) + ->columnSpanFull(), + + // Note: Translations should match the order of features above + // Each feature translation array index should correspond to the feature index + ]) + ->collapsible() + ->collapsed(), + + // How It Works Section + Section::make(__('products.how_it_works_section')) + ->schema([ + FileUpload::make('landing_page_data.how_it_works.download_image') + ->label(__('products.how_it_works_image')) + ->image() + ->disk('public') + ->directory('products/landing') + ->columnSpanFull(), + TextInput::make('landing_page_data.how_it_works.download_form_label') + ->label(__('products.download_form_label')) + ->placeholder(__('products.download_form_label_placeholder')) + ->columnSpanFull(), + + // Steps (4 steps with translations) + Repeater::make('landing_page_data.how_it_works.steps') + ->label(__('products.steps')) + ->schema([ + TextInput::make('number') + ->label(__('products.step_number')) + ->numeric() + ->default(fn ($get) => ($get('../../../_index') ?? 0) + 1) + ->required(), + TextInput::make('title') + ->label(__('products.step_title')) + ->required() + ->columnSpanFull(), + Textarea::make('description') + ->label(__('products.step_description')) + ->rows(2) + ->columnSpanFull(), + ]) + ->columns(1) + ->defaultItems(4) + ->minItems(4) + ->maxItems(4) + ->reorderable(false) + ->collapsible() + ->itemLabel(fn (array $state): ?string => __('products.step') . ' ' . ($state['number'] ?? '') . ': ' . ($state['title'] ?? '')) + ->columnSpanFull(), + + // Note: Steps translations should match the order of steps above + // Each step translation array index should correspond to the step index + ]) + ->collapsible() + ->collapsed(), + + // Video Section + Section::make(__('products.video_section')) + ->schema([ + TextInput::make('landing_page_data.video.youtube_video_id') + ->label(__('products.youtube_video_id')) + ->placeholder('165101721') + ->helperText(__('products.youtube_video_id_helper')) + ->columnSpanFull(), + ]) + ->collapsible() + ->collapsed(), + + // FAQ Section + Section::make(__('products.faq_section')) + ->schema([ + Repeater::make('landing_page_data.faqs') + ->label(__('products.faqs')) + ->schema([ + TextInput::make('question') + ->label(__('products.faq_question')) + ->required() + ->columnSpanFull(), + Textarea::make('answer') + ->label(__('products.faq_answer')) + ->rows(3) + ->required() + ->columnSpanFull(), + ]) + ->columns(1) + ->defaultItems(0) + ->addActionLabel(__('products.add_faq')) + ->reorderable() + ->collapsible() + ->itemLabel(fn (array $state): ?string => $state['question'] ?? __('products.faq') . ' #' . ($state['_index'] ?? '')) + ->columnSpanFull(), + + // Note: FAQ translations should match the order of FAQs above + // Each FAQ translation array index should correspond to the FAQ index + ]) + ->collapsible() + ->collapsed(), + ]) + ->visible(fn (Get $get) => $get('type') === 'product') + ->columnSpanFull(); + } +} + diff --git a/app/Filament/Admin/Resources/Products/Schemas/ProductForm.php b/app/Filament/Admin/Resources/Products/Schemas/ProductForm.php index 8f4fe3f..5ceeb1c 100644 --- a/app/Filament/Admin/Resources/Products/Schemas/ProductForm.php +++ b/app/Filament/Admin/Resources/Products/Schemas/ProductForm.php @@ -9,6 +9,7 @@ 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; @@ -83,6 +84,9 @@ class ProductForm ], ]), ]), + + // Landing Page Section (only for products) + LandingPageSection::make(), ]); } } diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index f878c34..bfe8e5d 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -68,10 +68,17 @@ class ProductController extends Controller 'description' => \Str::limit(strip_tags($product->translate('content')), 160), ]; + // 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 if landing_page_data exists and product type is 'product' + if ($product->type === 'product' && !empty($product->landing_page_data)) { + return view('front.products.landing', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta')); + } + + // Default view return view('front.products.show', compact('product', 'settings', 'renderedHeader', 'renderedFooter', 'meta')); } } diff --git a/app/Models/Product.php b/app/Models/Product.php index 84e2de6..e6a1ed8 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -21,6 +21,7 @@ class Product extends Model 'view_template', 'sort_order', 'is_active', + 'landing_page_data', ]; protected $translatable = [ @@ -32,6 +33,7 @@ class Product extends Model 'is_active' => 'boolean', 'title' => 'array', 'content' => 'array', + 'landing_page_data' => 'array', ]; public static function boot() diff --git a/database/migrations/2026_01_02_115025_add_landing_page_data_to_products_table.php b/database/migrations/2026_01_02_115025_add_landing_page_data_to_products_table.php new file mode 100644 index 0000000..556d3cd --- /dev/null +++ b/database/migrations/2026_01_02_115025_add_landing_page_data_to_products_table.php @@ -0,0 +1,28 @@ +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'); + }); + } +}; diff --git a/lang/en/products.php b/lang/en/products.php index 11b4d2b..715ed27 100644 --- a/lang/en/products.php +++ b/lang/en/products.php @@ -25,6 +25,43 @@ return [ // Table 'title' => 'Title', 'created_at' => 'Created At', + + // Landing Page + 'landing_page_section' => 'Landing Page Template', + 'hero_section' => 'Hero Section', + 'hero_slogan' => 'Main Slogan', + 'hero_sub_slogan' => 'Sub Slogan', + 'app_store_link' => 'App Store Link', + 'google_play_link' => 'Google Play Link', + 'hero_featured_image' => 'Featured Image', + 'hero_background_image' => 'Hero Background Image', + 'hero_background_image_helper' => 'Select the background image for the hero section', + 'select_background' => 'Select background...', + 'app_features_section' => 'App Features', + 'features' => 'Features', + 'add_feature' => 'Add Feature', + 'feature' => 'Feature', + 'feature_icon' => 'Icon', + 'feature_title' => 'Feature Title', + 'feature_description' => 'Feature Description', + 'how_it_works_section' => 'How It Works', + 'how_it_works_image' => 'Image', + 'download_form_label' => 'Download Form Label', + 'download_form_label_placeholder' => 'Download the app', + 'steps' => 'Steps', + 'step' => 'Step', + 'step_number' => 'Step Number', + 'step_title' => 'Step Title', + 'step_description' => 'Step Description', + 'video_section' => 'Video', + 'youtube_video_id' => 'YouTube Video ID', + 'youtube_video_id_helper' => 'The ID from YouTube video URL (e.g.: 165101721)', + 'faq_section' => 'Frequently Asked Questions', + 'faqs' => 'FAQs', + 'add_faq' => 'Add FAQ', + 'faq' => 'FAQ', + 'faq_question' => 'Question', + 'faq_answer' => 'Answer', ]; diff --git a/lang/tr/products.php b/lang/tr/products.php index 3bcda55..c004e19 100644 --- a/lang/tr/products.php +++ b/lang/tr/products.php @@ -25,6 +25,43 @@ return [ // Table 'title' => 'Başlık', 'created_at' => 'Oluşturulma Tarihi', + + // Landing Page + 'landing_page_section' => 'Landing Page Şablonu', + 'hero_section' => 'Hero Bölümü', + 'hero_slogan' => 'Ana Slogan', + 'hero_sub_slogan' => 'Alt Slogan', + 'app_store_link' => 'App Store Linki', + 'google_play_link' => 'Google Play Linki', + 'hero_featured_image' => 'Öne Çıkan Görsel', + 'hero_background_image' => 'Hero Arka Plan Görseli', + 'hero_background_image_helper' => 'Hero section için kullanılacak arka plan görselini seçin', + 'select_background' => 'Arka plan seçin...', + 'app_features_section' => 'Uygulama Özellikleri', + 'features' => 'Özellikler', + 'add_feature' => 'Özellik Ekle', + 'feature' => 'Özellik', + 'feature_icon' => 'İkon', + 'feature_title' => 'Özellik Başlığı', + 'feature_description' => 'Özellik Açıklaması', + 'how_it_works_section' => 'Nasıl Çalışır', + 'how_it_works_image' => 'Görsel', + 'download_form_label' => 'Download Form Etiketi', + 'download_form_label_placeholder' => 'Download the app', + 'steps' => 'Adımlar', + 'step' => 'Adım', + 'step_number' => 'Adım Numarası', + 'step_title' => 'Adım Başlığı', + 'step_description' => 'Adım Açıklaması', + 'video_section' => 'Video', + 'youtube_video_id' => 'YouTube Video ID', + 'youtube_video_id_helper' => 'YouTube video URL\'sindeki ID (örn: 165101721)', + 'faq_section' => 'Sık Sorulan Sorular', + 'faqs' => 'SSS', + 'add_faq' => 'SSS Ekle', + 'faq' => 'SSS', + 'faq_question' => 'Soru', + 'faq_answer' => 'Cevap', ]; diff --git a/resources/views/front/products/landing.blade.php b/resources/views/front/products/landing.blade.php new file mode 100644 index 0000000..9dad5c4 --- /dev/null +++ b/resources/views/front/products/landing.blade.php @@ -0,0 +1,295 @@ + +@extends('layouts.site', ['headerTemplate' => 'Demo29 Header']) + +@php +use Illuminate\Support\Facades\Storage; +@endphp + +@section('title', $product->translate('title') ?? 'Ürün Detayı') +@section('meta_description', Str::limit(strip_tags($product->translate('content')), 160)) +@php + $landingData = $product->landing_page_data ?? []; + $hero = $landingData['hero'] ?? []; + $features = $landingData['features'] ?? []; + $howItWorks = $landingData['how_it_works'] ?? []; + $steps = $howItWorks['steps'] ?? []; + $video = $landingData['video'] ?? []; + $faqs = $landingData['faqs'] ?? []; + $currentLang = app()->getLocale(); + + // Helper function to get translated value with fallback + $getTranslated = function($item, $key, $default = '') { + global $currentLang; + if (isset($item["{$key}_{$currentLang}"])) { + return $item["{$key}_{$currentLang}"]; + } + return $item[$key] ?? $default; + }; +@endphp + +@section('content') + {{-- Hero Section --}} + @if(!empty($hero)) +
+
+
+
+ @if(!empty($hero['slogan'])) +

+ {!! nl2br(e($hero['slogan'])) !!} +

+ @endif + @if(!empty($hero['sub_slogan'])) +

{{ $hero['sub_slogan'] }}

+ @endif +
+ @if(!empty($hero['app_store_link'])) + + App Store + + @endif + @if(!empty($hero['google_play_link'])) + + Google Play + + @endif +
+
+ + @if(!empty($hero['featured_image'])) +
+
+ {{ $product->translate('title') }} +
+
+ + @endif +
+ +
+ +
+
+ + + + + +
+
+ +
+ + @endif + + {{-- App Features Section --}} + @if(!empty($features)) +
+
+
+
+

App Features

+

+ {{ $product->translate('title') }} makes your experience better for you to have the perfect control. +

+
+ +
+ +
+
+
+ @foreach($features as $index => $feature) +
+ @if(!empty($feature['icon'])) +
+ @php + $iconPath = public_path('docs/styleguide/_/assets/img/icons/solid/' . $feature['icon'] . '.svg'); + $iconUrl = file_exists($iconPath) + ? asset('docs/styleguide/_/assets/img/icons/solid/' . $feature['icon'] . '.svg') + : null; + @endphp + @if($iconUrl) + {{ $feature['title'] ?? '' }} + @else +
+ {{ substr($feature['icon'], 0, 2) }} +
+ @endif +
+ @endif + @if(!empty($feature['title'])) +

{{ $feature['title'] }}

+ @endif +
+ + @endforeach +
+ +
+ +
+ +
+ +
+ + @endif + + {{-- How It Works Section --}} + @if(!empty($howItWorks) && !empty($steps)) +
+
+
+
+

How It Works

+ @if(!empty($howItWorks['download_form_label'])) +

+ {{ $howItWorks['download_form_label'] }} +

+ @endif +
+ +
+ +
+
+
+ @if(!empty($howItWorks['download_image'])) +
+
+ How It Works +
+
+ + @endif +
+
+ @foreach($steps as $index => $step) + @if($index < 2) +
+ {{ str_pad($step['number'] ?? ($index + 1), 2, '0', STR_PAD_LEFT) }} + @if(!empty($step['title'])) +

{{ $step['title'] }}

+ @endif + @if(!empty($step['description'])) +

{{ $step['description'] }}

+ @endif +
+ + @endif + @endforeach +
+ +
+ @foreach($steps as $index => $step) + @if($index >= 2) +
+ {{ str_pad($step['number'] ?? ($index + 1), 2, '0', STR_PAD_LEFT) }} + @if(!empty($step['title'])) +

{{ $step['title'] }}

+ @endif + @if(!empty($step['description'])) +

{{ $step['description'] }}

+ @endif +
+ + @endif + @endforeach +
+ +
+ +
+ +
+ +
+ +
+ + @endif + + {{-- Video Section --}} + @if(!empty($video['youtube_video_id'])) +
+
+
+
+
+
+ +
+
+
+ +
+ +
+ +
+ + @endif + + {{-- FAQ Section --}} + @if(!empty($faqs)) +
+
+
+
+
+

FAQ

+

+ If you don't see an answer to your question, you can send us an email from our contact form. +

+
+
+
+
+ @foreach($faqs as $index => $faq) +
+
+ +
+ +
+
+

{!! nl2br(e($faq['answer'] ?? '')) !!}

+
+ +
+ +
+ + @endforeach +
+ +
+ +
+ +
+ +
+ +
+ +
+ + @endif +@endsection + diff --git a/resources/views/layouts/site.blade.php b/resources/views/layouts/site.blade.php index b561b56..7163d18 100644 --- a/resources/views/layouts/site.blade.php +++ b/resources/views/layouts/site.blade.php @@ -16,6 +16,7 @@ $favicon_path = setting('site_favicon'); +