From da0ee2477b7af15222a5cccce2bd8b1405574b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Thu, 21 May 2026 08:53:44 +0300 Subject: [PATCH] feat: implement language selector component with responsive design for improved user experience across devices --- app/Services/TemplateService.php | 61 ++++++++++-- .../custom/language-selector.blade.php | 92 ++++++++++++++----- resources/views/layouts/site.blade.php | 41 ++++++++- resources/views/partials/header.blade.php | 30 ++++-- 4 files changed, 184 insertions(+), 40 deletions(-) diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php index 2ec2360..27973bb 100644 --- a/app/Services/TemplateService.php +++ b/app/Services/TemplateService.php @@ -5,7 +5,10 @@ namespace App\Services; use App\Models\HeaderTemplate; use App\Models\SectionTemplate; use App\Models\FooterTemplate; +use App\Models\Setting; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\View; use Filament\Forms\Components\{ TextInput, Textarea, Select, Checkbox, CheckboxList, Radio, Toggle, ToggleButtons, DateTimePicker, DatePicker, @@ -294,11 +297,15 @@ class TemplateService */ protected static function getSelectOptions(string $fieldName): array { - return config("template-options.{$fieldName}", [ + $defaults = [ 'option_1' => __('Option 1'), 'option_2' => __('Option 2'), 'option_3' => __('Option 3'), - ]); + ]; + + $options = config("template-options.{$fieldName}", $defaults); + + return is_array($options) ? $options : $defaults; } /** @@ -376,6 +383,15 @@ class TemplateService } // Handle custom blade components: {custom.component_name} or {custom.component-name} + if (str_contains($html, '{custom.language-selector}')) { + $desktopLanguage = view('components.custom.language-selector', [ + 'variant' => 'default', + ])->render(); + + $html = str_replace('{custom.language-selector}', $desktopLanguage, $html); + $html = self::injectOffcanvasLanguageSelector($html); + } + preg_match_all('/\{custom\.([a-z][a-z_-]*)\}/i', $html, $customMatches); if (!empty($customMatches[0])) { foreach ($customMatches[0] as $index => $fullMatch) { @@ -383,8 +399,12 @@ class TemplateService if ($componentName) { // Normalize component name to lowercase for consistent file system access $componentName = strtolower($componentName); + + if ($componentName === 'language-selector') { + continue; + } $viewPath = "components.custom.{$componentName}"; - if (view()->exists($viewPath)) { + if (View::exists($viewPath)) { try { $renderedComponent = view($viewPath)->render(); // Ensure rendered component is properly formatted @@ -392,7 +412,7 @@ class TemplateService $html = str_replace($fullMatch, $renderedComponent, $html); } catch (\Exception $e) { // Log the error for debugging - \Log::error("Custom component render error: {$viewPath}", [ + Log::error("Custom component render error: {$viewPath}", [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); @@ -502,10 +522,10 @@ class TemplateService // Handle setting.* placeholders - get value from Setting model if ($type === 'setting' && $field) { $value = setting($field, ''); - + // Get setting type to determine if it's a file/image - $settingModel = \App\Models\Setting::where('key', $field)->where('is_active', true)->first(); - if ($settingModel && in_array($settingModel->type, ['file', 'image'])) { + $settingModel = Setting::findActiveByKey($field); + if ($settingModel && in_array($settingModel->type, ['file', 'image'], true)) { // For file/image settings, use file type for formatting $type = 'file'; } @@ -660,5 +680,32 @@ class TemplateService return $value; } + + /** + * Inject mobile language selector at the top of the first offcanvas menu body. + */ + protected static function injectOffcanvasLanguageSelector(string $html): string + { + if (str_contains($html, 'offcanvas-language-mobile-wrap')) { + return $html; + } + + $mobileLanguage = view('components.custom.language-selector', [ + 'variant' => 'offcanvas', + 'class' => 'offcanvas-language-mobile', + ])->render(); + + $injection = '
' + . $mobileLanguage + . '
'; + + $pattern = '/(]*class="[^"]*offcanvas-body[^"]*"[^>]*>)/i'; + + if (preg_match($pattern, $html)) { + return preg_replace($pattern, '$1' . $injection, $html, 1); + } + + return $html; + } } diff --git a/resources/views/components/custom/language-selector.blade.php b/resources/views/components/custom/language-selector.blade.php index 9736765..69db859 100644 --- a/resources/views/components/custom/language-selector.blade.php +++ b/resources/views/components/custom/language-selector.blade.php @@ -1,31 +1,77 @@ +@props([ + 'variant' => 'default', +]) + @php $languages = available_languages(); $currentLang = current_language_code(); $currentLanguage = $languages->firstWhere('code', $currentLang); + $languageCollapseId = 'offcanvas-language-collapse'; + + $wrapperClass = $variant === 'offcanvas' + ? 'language-select-offcanvas w-full' + : 'navbar-other !ml-5 header-language-desktop !hidden lg:!flex xl:!flex shrink-0'; + + $linkClass = 'nav-link dropdown-item dropdown-toggle xl:!text-[.85rem] lg:!text-[.85rem] max-lg:!text-[1.05rem]'; @endphp - \ No newline at end of file + +@endif diff --git a/resources/views/layouts/site.blade.php b/resources/views/layouts/site.blade.php index 48ae890..18aed61 100644 --- a/resources/views/layouts/site.blade.php +++ b/resources/views/layouts/site.blade.php @@ -85,9 +85,44 @@ $favicon_path = setting('site_favicon'); max-width: 100%; } - header .container, - .wrapper > .container { - max-width: 100%; + + + header .header-language-desktop { + display: none !important; + } + + @media (min-width: 992px) { + header .header-language-desktop { + display: flex !important; + } + } + + @media (max-width: 991.98px) { + header nav .container > .navbar-other:has(> .navbar-nav > .language-select) { + display: none !important; + } + + .offcanvas .language-select-offcanvas__toggle { + width: 100%; + text-align: left; + } + + .offcanvas .language-select-offcanvas__panel { + position: static !important; + inset: auto !important; + transform: none !important; + background: transparent !important; + box-shadow: none !important; + border: 0 !important; + } + + .offcanvas .language-select-offcanvas__panel .nav-link { + padding-left: 0 !important; + } + + .offcanvas .language-select-offcanvas__toggle[aria-expanded="true"] .language-select-offcanvas__icon { + transform: rotate(180deg); + } } diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php index ac05b68..761091d 100644 --- a/resources/views/partials/header.blade.php +++ b/resources/views/partials/header.blade.php @@ -51,11 +51,14 @@ $isHomepage = $currentRoute === 'homepage' || $currentPath === '/' || request()- Truncgil Truncgil -
+
+
+ @include('components.custom.language-selector', ['variant' => 'offcanvas']) +
@@ -149,12 +154,12 @@ $isHomepage = $currentRoute === 'homepage' || $currentPath === '/' || request()- {{-- Mobile menu is dark, so we only need the light (white) logo here --}} Truncgil -
- @include("components.custom.language-selector") - -
+ -
+
+
+ @include('components.custom.language-selector', ['variant' => 'offcanvas']) +