feat(faq): video tutorial + full-width layout + wider grid

- New tenant setting: settings.faq_video_url. TextInput on Settings
  page → Liste configurabile (accepts YouTube watch URL, youtu.be
  short URL, YouTube shorts/embed URL, or Vimeo URL).
- Faq page has getVideoEmbedUrl() that converts recognized URLs to
  their /embed/ variant. Unknown URLs fall back to a "?" card with
  a direct link. Admins see a "configure in Settings" nudge when no
  URL is set.
- Video renders as a 16:9 iframe capped at 480px height, above the
  hero.
- .faq-shell is now width:100% (was max-width:1200px) so FAQ uses
  all available horizontal space.
- .faq-grid switched from strict 2 columns to
  repeat(auto-fit, minmax(420px, 1fr)) so wide screens get 3 columns
  and narrow screens stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 10:50:40 +00:00
parent cae2904c76
commit 0716b70bfa
5 changed files with 119 additions and 2 deletions
+29
View File
@@ -45,6 +45,35 @@ class Faq extends Page
return Hints::AREAS;
}
/**
* Convert a YouTube / Vimeo watch URL to the embed URL that goes in
* an <iframe src="">. Returns null if the URL is empty or the host
* isn't recognized (in which case we fall back to a plain link).
*/
public function getVideoEmbedUrl(): ?string
{
$tenant = app(\App\Tenancy\TenantManager::class)->current();
$raw = trim((string) ($tenant?->settings['faq_video_url'] ?? ''));
if ($raw === '') return null;
// YouTube: watch?v=ID | youtu.be/ID | shorts/ID
if (preg_match('#(?:youtube\.com/(?:watch\?v=|shorts/|embed/)|youtu\.be/)([A-Za-z0-9_-]{6,})#', $raw, $m)) {
return 'https://www.youtube.com/embed/' . $m[1];
}
// Vimeo: vimeo.com/ID
if (preg_match('#vimeo\.com/(\d+)#', $raw, $m)) {
return 'https://player.vimeo.com/video/' . $m[1];
}
return null;
}
public function getVideoRawUrl(): ?string
{
$tenant = app(\App\Tenancy\TenantManager::class)->current();
$raw = trim((string) ($tenant?->settings['faq_video_url'] ?? ''));
return $raw !== '' ? $raw : null;
}
public function getGrouped(): array
{
$q = mb_strtolower(trim($this->search));
+7
View File
@@ -59,6 +59,7 @@ class Settings extends Page
'labor_rate' => $settings['labor_rate'] ?? 400,
'services' => isset($settings['services']) ? implode(', ', (array) $settings['services']) : '',
'cars' => isset($settings['cars']) ? implode(', ', (array) $settings['cars']) : '',
'faq_video_url' => $settings['faq_video_url'] ?? null,
'notify_wo_ready' => $notify['wo_ready'] ?? true,
'notify_payment' => $notify['payment'] ?? true,
'notify_appointment' => $notify['appointment'] ?? true,
@@ -148,6 +149,11 @@ class Settings extends Page
Forms\Components\Textarea::make('cars')
->label(__('Mărci auto suportate (separate prin virgulă)'))
->rows(2),
Forms\Components\TextInput::make('faq_video_url')
->label(__('Link video tutorial (YouTube / Vimeo)'))
->url()
->placeholder('https://www.youtube.com/watch?v=... sau https://vimeo.com/...')
->helperText(__('Afișat în pagina FAQ ca player embedded. Lasă gol pentru a-l ascunde.')),
]),
Schemas\Components\Section::make(__('Logo & favicon'))
->columns(2)
@@ -268,6 +274,7 @@ class Settings extends Page
'labor_rate' => (float) ($data['labor_rate'] ?? 400),
'services' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['services'] ?? ''))))),
'cars' => array_values(array_filter(array_map('trim', explode(',', (string) ($data['cars'] ?? ''))))),
'faq_video_url' => trim((string) ($data['faq_video_url'] ?? '')) ?: null,
'notify' => [
'wo_ready' => (bool) ($data['notify_wo_ready'] ?? true),
'payment' => (bool) ($data['notify_payment'] ?? true),