feat: implement page template hero functionality with dynamic image handling and localization support for enhanced visual presentation
This commit is contained in:
@@ -35,7 +35,6 @@ class ContentSection
|
||||
|
||||
RichEditor::make('content')
|
||||
->label(__('pages.content_field'))
|
||||
->required()
|
||||
->fileAttachmentsDisk('public')
|
||||
->fileAttachmentsDirectory('pages')
|
||||
->fileAttachmentsVisibility('public')
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
namespace App\Filament\Admin\Resources\Pages\Schemas\Sections;
|
||||
|
||||
use App\Support\PageTemplateHero;
|
||||
use Filament\Forms\Components\Checkbox;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Schemas\Components\Section;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
|
||||
class PageSettingsSection
|
||||
{
|
||||
@@ -15,6 +17,13 @@ class PageSettingsSection
|
||||
{
|
||||
return Section::make(__('pages.form_section_page_settings'))
|
||||
->schema([
|
||||
Select::make('template')
|
||||
->label(__('pages.template_field'))
|
||||
->options(PageTemplateHero::templateFormOptions())
|
||||
->default('default')
|
||||
->live()
|
||||
->columnSpanFull(),
|
||||
|
||||
FileUpload::make('featured_image')
|
||||
->label(__('pages.featured_image_field'))
|
||||
->image()
|
||||
@@ -27,7 +36,13 @@ class PageSettingsSection
|
||||
'4:3',
|
||||
'1:1',
|
||||
])
|
||||
->helperText(__('pages.featured_image_helper_text'))
|
||||
->helperText(function (Get $get): string {
|
||||
if (PageTemplateHero::hasHero($get('template'))) {
|
||||
return __('pages.featured_image_template_hero_helper');
|
||||
}
|
||||
|
||||
return __('pages.featured_image_helper_text');
|
||||
})
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('author_id')
|
||||
@@ -62,24 +77,7 @@ class PageSettingsSection
|
||||
->helperText(__('pages.parent_helper_text'))
|
||||
->columnSpanFull(),
|
||||
|
||||
Select::make('template')
|
||||
->label(__('pages.template_field'))
|
||||
->options([
|
||||
'default' => __('pages.template_default'),
|
||||
'landing' => __('pages.template_landing'),
|
||||
'blog' => __('pages.template_blog'),
|
||||
'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')
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('sort_order')
|
||||
TextInput::make('sort_order')
|
||||
->label(__('pages.sort_order_field'))
|
||||
->numeric()
|
||||
->default(0)
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\HeaderTemplate;
|
||||
use App\Models\FooterTemplate;
|
||||
use App\Services\TemplateService;
|
||||
use App\Support\PageStructuredData;
|
||||
use App\Support\PageTemplateHero;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
@@ -145,6 +146,7 @@ class PageController extends Controller
|
||||
|
||||
return view($view, [
|
||||
'page' => $page,
|
||||
'pageHero' => $page ? PageTemplateHero::resolveForPage($page) : null,
|
||||
'settings' => $settings,
|
||||
'sections' => $page ? ($page->parsed_sections ?? $page->sections ?? $page->data ?? []) : [],
|
||||
'templatedSections' => $page ? ($page->templated_sections ?? collect([])) : collect([]),
|
||||
@@ -192,8 +194,8 @@ class PageController extends Controller
|
||||
? ($page->translate('meta_description') ?: ($page->excerpt ?? null))
|
||||
: ($page->meta_description ?? $page->excerpt ?? null);
|
||||
|
||||
if ($page->featured_image) {
|
||||
$metaImage = asset('storage/' . $page->featured_image);
|
||||
if ($page->featured_image_url) {
|
||||
$metaImage = $page->featured_image_url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-5
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\SectionTemplate;
|
||||
use App\Support\PageTemplateHero;
|
||||
use App\Traits\HasTranslations;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -115,13 +116,14 @@ class Page extends Model
|
||||
return '/' . $this->slug;
|
||||
}
|
||||
|
||||
public function getFeaturedImageUrlAttribute()
|
||||
public function getFeaturedImageUrlAttribute(): ?string
|
||||
{
|
||||
if ($this->featured_image) {
|
||||
return asset('storage/' . $this->featured_image);
|
||||
}
|
||||
return PageTemplateHero::urlForPage($this);
|
||||
}
|
||||
|
||||
return null;
|
||||
public function usesTemplateHeroImage(): bool
|
||||
{
|
||||
return ! $this->featured_image && PageTemplateHero::hasHero($this->template);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Page;
|
||||
|
||||
class PageTemplateHero
|
||||
{
|
||||
public static function config(?string $template): ?array
|
||||
{
|
||||
if (! filled($template)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$heroes = config('page_templates.heroes', []);
|
||||
|
||||
return $heroes[$template] ?? null;
|
||||
}
|
||||
|
||||
public static function hasHero(?string $template): bool
|
||||
{
|
||||
return filled(static::imagePath($template));
|
||||
}
|
||||
|
||||
public static function imagePath(?string $template): ?string
|
||||
{
|
||||
return static::config($template)['image'] ?? null;
|
||||
}
|
||||
|
||||
public static function webpPath(?string $template): ?string
|
||||
{
|
||||
return static::config($template)['webp'] ?? null;
|
||||
}
|
||||
|
||||
public static function url(?string $template): ?string
|
||||
{
|
||||
$path = static::imagePath($template);
|
||||
|
||||
return $path ? asset($path) : null;
|
||||
}
|
||||
|
||||
public static function webpUrl(?string $template): ?string
|
||||
{
|
||||
$path = static::webpPath($template);
|
||||
|
||||
return $path ? asset($path) : null;
|
||||
}
|
||||
|
||||
public static function featuredStoragePath(Page $page): ?string
|
||||
{
|
||||
$value = $page->featured_image;
|
||||
|
||||
if (blank($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$value = $value[0] ?? null;
|
||||
}
|
||||
|
||||
return is_string($value) && $value !== '' ? $value : null;
|
||||
}
|
||||
|
||||
public static function urlForPage(Page $page): ?string
|
||||
{
|
||||
if ($path = static::featuredStoragePath($page)) {
|
||||
return asset('storage/' . $path);
|
||||
}
|
||||
|
||||
return static::url($page->template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hero alanı için görsel verisi (yüklenen öne çıkan görsel veya şablon varsayılanı).
|
||||
*
|
||||
* @return array{image: string, webp?: string, width?: int, height?: int, from_upload: bool}|null
|
||||
*/
|
||||
public static function resolveForPage(?Page $page, ?string $templateFallback = null): ?array
|
||||
{
|
||||
if ($page && ($path = static::featuredStoragePath($page))) {
|
||||
return [
|
||||
'image' => asset('storage/' . $path),
|
||||
'from_upload' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$template = $page?->template ?? $templateFallback;
|
||||
$config = static::config($template);
|
||||
|
||||
if (! $config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_merge($config, ['from_upload' => false]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function templateFormOptions(): array
|
||||
{
|
||||
$base = [
|
||||
'default' => __('pages.template_default'),
|
||||
'landing' => __('pages.template_landing'),
|
||||
'blog' => __('pages.template_blog'),
|
||||
'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)',
|
||||
];
|
||||
|
||||
$fromConfig = collect(config('page_templates.labels', []))
|
||||
->mapWithKeys(fn (string $label, string $key) => [$key => $label])
|
||||
->all();
|
||||
|
||||
return array_merge($base, $fromConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Statik sayfa şablonları için hero görselleri.
|
||||
* Blade şablonları ve Page modeli aynı kaynağı kullanır.
|
||||
*/
|
||||
return [
|
||||
'heroes' => [
|
||||
'services.app-development' => [
|
||||
'image' => 'assets/img/photos/truncgil-mobile-app.png',
|
||||
'webp' => 'assets/img/photos/truncgil-mobile-app.webp',
|
||||
'width' => 735,
|
||||
'height' => 735,
|
||||
],
|
||||
'neler-yapariz' => [
|
||||
'image' => 'assets/img/illustrations/3d11.png',
|
||||
'width' => 800,
|
||||
'height' => 1080,
|
||||
],
|
||||
],
|
||||
|
||||
'labels' => [
|
||||
'services.app-development' => 'Uygulama Geliştirme',
|
||||
'neler-yapariz' => 'Neler Yaparız',
|
||||
],
|
||||
];
|
||||
@@ -117,6 +117,7 @@ class AppDevelopmentPageSeeder extends Seeder
|
||||
'excerpt' => 'Trunçgil Teknoloji olarak Android ve iOS uygulamalarını Google Play ve App Store\'da yayınlanmaya hazır şekilde titizlikle geliştiriyoruz.',
|
||||
'content' => '',
|
||||
'template' => 'services.app-development',
|
||||
'featured_image' => null,
|
||||
'custom_header_blade' => 'partials.header-app-development',
|
||||
'status' => 'published',
|
||||
'is_homepage' => false,
|
||||
|
||||
@@ -58,6 +58,9 @@ return [
|
||||
'slug_helper_text' => 'The part that will appear in the URL. Example: about-us',
|
||||
'excerpt_helper_text' => 'Page excerpt (maximum 500 characters)',
|
||||
'featured_image_helper_text' => 'Select a featured image for the page',
|
||||
'featured_image_template_hero_helper' => 'If left empty, the selected template\'s hero image is used automatically. Upload a custom image to override.',
|
||||
'template_hero_preview_label' => 'Template hero image',
|
||||
'template_hero_preview_helper' => 'This image is used as the page featured image and in SEO/meta fields.',
|
||||
'published_at_helper_text' => 'If left empty, current date will be used',
|
||||
'parent_helper_text' => 'Select to make this page a sub-page of another page',
|
||||
'sort_order_helper_text' => 'Display order in menu',
|
||||
|
||||
@@ -58,6 +58,9 @@ return [
|
||||
'slug_helper_text' => 'URL\'de görünecek kısım. Örnek: hakkimizda',
|
||||
'excerpt_helper_text' => 'Sayfa özeti (maksimum 500 karakter)',
|
||||
'featured_image_helper_text' => 'Sayfa için öne çıkan görsel seçin',
|
||||
'featured_image_template_hero_helper' => 'Boş bırakılırsa seçili şablonun hero görseli otomatik kullanılır. Özel görsel yükleyerek geçersiz kılabilirsiniz.',
|
||||
'template_hero_preview_label' => 'Şablon hero görseli',
|
||||
'template_hero_preview_helper' => 'Bu görsel sayfa öne çıkan görseli olarak ve SEO/meta alanlarında kullanılır.',
|
||||
'published_at_helper_text' => 'Boş bırakılırsa şu anki tarih kullanılır',
|
||||
'parent_helper_text' => 'Bu sayfayı başka bir sayfanın alt sayfası yapmak için seçin',
|
||||
'sort_order_helper_text' => 'Menüde görünme sırası',
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@props([
|
||||
'hero' => null,
|
||||
'alt' => '',
|
||||
])
|
||||
|
||||
@if($hero)
|
||||
@if(!empty($hero['from_upload']))
|
||||
<img
|
||||
{{ $attributes->merge(['class' => 'w-full max-w-full !h-auto']) }}
|
||||
src="{{ $hero['image'] }}"
|
||||
alt="{{ $alt }}"
|
||||
@if(!empty($hero['width'])) width="{{ $hero['width'] }}" @endif
|
||||
@if(!empty($hero['height'])) height="{{ $hero['height'] }}" @endif
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
>
|
||||
@else
|
||||
<picture>
|
||||
@if(!empty($hero['webp']))
|
||||
<source srcset="{{ asset($hero['webp']) }}" type="image/webp">
|
||||
@endif
|
||||
<img
|
||||
{{ $attributes->merge(['class' => 'w-full max-w-full !h-auto']) }}
|
||||
src="{{ asset($hero['image']) }}"
|
||||
alt="{{ $alt }}"
|
||||
width="{{ $hero['width'] ?? 735 }}"
|
||||
height="{{ $hero['height'] ?? 735 }}"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
>
|
||||
</picture>
|
||||
@endif
|
||||
@endif
|
||||
@@ -1,7 +1,17 @@
|
||||
@extends('layouts.site', ['header' => 'partials.header-app-development', 'footer' => 'partials.footer'])
|
||||
|
||||
@php
|
||||
$pageHero = $pageHero ?? \App\Support\PageTemplateHero::resolveForPage($page ?? null, 'services.app-development');
|
||||
@endphp
|
||||
|
||||
@push('head')
|
||||
<link rel="preload" as="image" href="{{ asset('assets/img/photos/truncgil-mobile-app.webp') }}" type="image/webp" fetchpriority="high">
|
||||
@if($pageHero)
|
||||
@if(!empty($pageHero['from_upload']))
|
||||
<link rel="preload" as="image" href="{{ $pageHero['image'] }}" fetchpriority="high">
|
||||
@elseif(!empty($pageHero['webp']))
|
||||
<link rel="preload" as="image" href="{{ asset($pageHero['webp']) }}" type="image/webp" fetchpriority="high">
|
||||
@endif
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
@@ -264,10 +274,10 @@
|
||||
</div>
|
||||
<div class="xl:w-6/12 lg:w-6/12 w-full flex-[0_0_auto] max-w-full !ml-auto !mb-[-10rem] xxl:!mb-[-15rem] !mt-[50px]" data-cues="slideInDown" data-delay="600">
|
||||
<figure class="m-0 p-0">
|
||||
<picture>
|
||||
<source srcset="{{ asset('assets/img/photos/truncgil-mobile-app.webp') }}" type="image/webp">
|
||||
<img class="w-full max-w-full !h-auto" src="{{ asset('assets/img/photos/truncgil-mobile-app.png') }}" alt="{!! t('Trunçgil Teknoloji çoklu platform uygulama geliştirme') !!}" width="735" height="735" fetchpriority="high" decoding="async">
|
||||
</picture>
|
||||
<x-front.page-hero-image
|
||||
:hero="$pageHero"
|
||||
:alt="t('Trunçgil Teknoloji çoklu platform uygulama geliştirme')"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user