feat: adicionar heros fotográficos imersivos (#45)

This commit is contained in:
2026-08-11 08:28:09 -03:00
committed by GitHub
parent 338e3c9adb
commit 8afb925e17
16 changed files with 303 additions and 133 deletions

View File

@@ -60,6 +60,15 @@ final class MediaGenerateVariantsCommand extends Command
if ($settings && filled($settings->about_image_path)) { if ($settings && filled($settings->about_image_path)) {
$paths[] = (string) $settings->about_image_path; $paths[] = (string) $settings->about_image_path;
} }
if ($settings && filled($settings->hero_image_path)) {
$paths[] = (string) $settings->hero_image_path;
}
if ($settings && filled($settings->services_hero_image_path)) {
$paths[] = (string) $settings->services_hero_image_path;
}
if ($settings && filled($settings->portfolio_hero_image_path)) {
$paths[] = (string) $settings->portfolio_hero_image_path;
}
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) { foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
$paths[] = (string) $path; $paths[] = (string) $path;

View File

@@ -165,6 +165,17 @@ class ManageSiteSettings extends Page
->rows(3), ->rows(3),
]) ])
->columns(2), ->columns(2),
Section::make('Imagens das aberturas')
->description('Fotos reais e autorizadas para as primeiras dobras. Sem imagem, cada rota mantém a abertura tonal.')
->schema([
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem da home', 'content/heroes'),
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
PublicImageUploadRules::fileUpload('services_hero_image_path', 'Imagem de Serviços', 'content/heroes'),
PublicImageUploadRules::altTextField('services_hero_image_alt', 'services_hero_image_path'),
PublicImageUploadRules::fileUpload('portfolio_hero_image_path', 'Imagem de Portfólio', 'content/heroes'),
PublicImageUploadRules::altTextField('portfolio_hero_image_alt', 'portfolio_hero_image_path'),
])
->columns(2),
Section::make('Manifesto editorial') Section::make('Manifesto editorial')
->schema([ ->schema([
TextInput::make('manifesto_title') TextInput::make('manifesto_title')

View File

@@ -18,6 +18,12 @@ use Illuminate\Database\Eloquent\Model;
* @property string|null $default_og_image_alt * @property string|null $default_og_image_alt
* @property string|null $about_image_path * @property string|null $about_image_path
* @property string|null $about_image_alt * @property string|null $about_image_alt
* @property string|null $hero_image_path
* @property string|null $hero_image_alt
* @property string|null $services_hero_image_path
* @property string|null $services_hero_image_alt
* @property string|null $portfolio_hero_image_path
* @property string|null $portfolio_hero_image_alt
* @property string|null $logo_path * @property string|null $logo_path
* @property string|null $logo_alt * @property string|null $logo_alt
*/ */
@@ -31,6 +37,12 @@ use Illuminate\Database\Eloquent\Model;
'hero_cta_label', 'hero_cta_label',
'hero_secondary_cta_label', 'hero_secondary_cta_label',
'hero_note', 'hero_note',
'hero_image_path',
'hero_image_alt',
'services_hero_image_path',
'services_hero_image_alt',
'portfolio_hero_image_path',
'portfolio_hero_image_alt',
'about_summary', 'about_summary',
'about_image_path', 'about_image_path',
'about_image_alt', 'about_image_alt',

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('site_settings', function (Blueprint $table): void {
$table->string('hero_image_path')->nullable()->after('hero_note');
$table->string('hero_image_alt')->nullable()->after('hero_image_path');
$table->string('services_hero_image_path')->nullable()->after('hero_image_alt');
$table->string('services_hero_image_alt')->nullable()->after('services_hero_image_path');
$table->string('portfolio_hero_image_path')->nullable()->after('services_hero_image_alt');
$table->string('portfolio_hero_image_alt')->nullable()->after('portfolio_hero_image_path');
});
}
public function down(): void
{
Schema::table('site_settings', function (Blueprint $table): void {
$table->dropColumn([
'hero_image_path',
'hero_image_alt',
'services_hero_image_path',
'services_hero_image_alt',
'portfolio_hero_image_path',
'portfolio_hero_image_alt',
]);
});
}
};

View File

@@ -61,6 +61,12 @@ class ContentSeeder extends Seeder
'hero_cta_label' => 'Solicitar proposta', 'hero_cta_label' => 'Solicitar proposta',
'hero_secondary_cta_label' => 'Conheça nosso olhar', 'hero_secondary_cta_label' => 'Conheça nosso olhar',
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.', 'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
'hero_image_path' => $this->copyFixture('case-casamento-ana-lucas.jpg', 'content/heroes/home.jpg'),
'hero_image_alt' => 'Celebração ao ar livre em São Paulo',
'services_hero_image_path' => $this->copyFixture('service-eventos-corporativos.jpg', 'content/heroes/services.jpg'),
'services_hero_image_alt' => 'Mesa preparada para um evento corporativo',
'portfolio_hero_image_path' => $this->copyFixture('case-lancamento-verano.jpg', 'content/heroes/portfolio.jpg'),
'portfolio_hero_image_alt' => 'Ambientação de um evento de lançamento',
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.', 'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'), 'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano', 'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',

View File

@@ -42,6 +42,12 @@ class VisualContentSeeder extends Seeder
'hero_cta_label' => 'Solicitar proposta', 'hero_cta_label' => 'Solicitar proposta',
'hero_secondary_cta_label' => 'Conheça nosso olhar', 'hero_secondary_cta_label' => 'Conheça nosso olhar',
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.', 'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
'hero_image_path' => $this->writeSolidJpeg('visual/heroes/home.jpg', 1600, 1100, [218, 224, 205]),
'hero_image_alt' => 'Imagem editorial da home',
'services_hero_image_path' => $this->writeSolidJpeg('visual/heroes/services.jpg', 1600, 1100, [196, 200, 184]),
'services_hero_image_alt' => 'Imagem editorial de Serviços',
'portfolio_hero_image_path' => $this->writeSolidJpeg('visual/heroes/portfolio.jpg', 1600, 1100, [228, 226, 221]),
'portfolio_hero_image_alt' => 'Imagem editorial de Portfólio',
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.', 'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]), 'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
'about_image_alt' => 'Imagem editorial da página Sobre', 'about_image_alt' => 'Imagem editorial da página Sobre',

View File

@@ -2,68 +2,26 @@
'settings', 'settings',
]) ])
<section <x-public.photo-hero
aria-labelledby="hero-heading" class="home-chapter"
class="home-chapter border-b border-amare-border bg-amare-bg"
data-chapter="hero" data-chapter="hero"
data-motion="page-open" :image-path="$settings->hero_image_path"
:image-alt="$settings->hero_image_alt"
:eyebrow="$settings->hero_eyebrow"
:title="$settings->hero_title ?: 'Celebrações com propósito'"
:summary="$settings->hero_subtitle"
brand-mark
> >
<div class="container-amare pt-8 md:pt-10"> <div class="container-amare pt-8 md:pt-10">
<x-home.folio label="Capa" data-motion-beat="folio" /> <x-home.folio label="Capa" data-motion-beat="folio" />
</div> </div>
<div class="flex flex-wrap items-center gap-4">
<div class="container-amare grid gap-12 pb-20 pt-10 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:pb-28 md:pt-14" data-reveal-group> <a href="{{ route('contact') }}" data-testid="home-primary-cta" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover">{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}</a>
<div class="space-y-8"> @if (filled($settings->hero_secondary_cta_label))
<div data-motion-beat="seal" class="flex items-center gap-4"> <a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span></a>
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
@if (filled($settings->hero_eyebrow))
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
@endif
</div>
<h1 id="hero-heading" data-motion-beat="title" class="max-w-3xl text-display font-medium text-amare-text">
{{ $settings->hero_title ?: 'Celebrações com propósito' }}
</h1>
@if (filled($settings->hero_subtitle))
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
@endif
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
<a
href="{{ route('contact') }}"
data-testid="home-primary-cta"
class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover"
>
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
</a>
@if (filled($settings->hero_secondary_cta_label))
<a
href="{{ route('portfolio.index') }}"
class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep"
>
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
</a>
@endif
</div>
@if (filled($settings->hero_note))
<p class="max-w-xl text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
@endif
</div>
@if (filled($settings->default_og_image_path))
<div data-motion-beat="media" class="min-h-72 overflow-hidden bg-amare-bg-deep">
<x-media.image
:path="$settings->default_og_image_path"
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
loading="eager"
fetchpriority="high"
sizes="(max-width: 768px) calc(100vw - 3rem), 40vw"
class="img-editorial h-full w-full object-cover"
/>
</div>
@endif @endif
</div> </div>
</section> @if (filled($settings->hero_note))
<p class="mt-5 max-w-xl text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
@endif
</x-public.photo-hero>

View File

@@ -58,7 +58,8 @@
@endif @endif
<img <img
src="{{ $src }}" src="{{ $src }}"
@if ($srcset) srcset="{{ $srcset }}" sizes="{{ $sizes }}" @endif @if ($srcset) srcset="{{ $srcset }}" @endif
sizes="{{ $sizes }}"
alt="{{ $alt }}" alt="{{ $alt }}"
@if ($resolvedWidth) width="{{ $resolvedWidth }}" @endif @if ($resolvedWidth) width="{{ $resolvedWidth }}" @endif
@if ($resolvedHeight) height="{{ $resolvedHeight }}" @endif @if ($resolvedHeight) height="{{ $resolvedHeight }}" @endif

View File

@@ -0,0 +1,76 @@
@props([
'imagePath' => null,
'imageAlt' => null,
'eyebrow' => null,
'title',
'summary' => null,
'metadata' => null,
'headingId' => 'hero-heading',
'brandMark' => false,
])
<section aria-labelledby="{{ $headingId }}" {{ $attributes->class(['border-b border-amare-border bg-amare-bg']) }} data-motion="page-open">
@if (filled($imagePath))
<div class="relative flex min-h-[calc(100dvh-5.5rem)] items-end overflow-hidden" data-photo-hero>
<x-media.image
:path="$imagePath"
:alt="$imageAlt ?: $title"
loading="eager"
fetchpriority="high"
sizes="100vw"
class="img-editorial absolute inset-0 h-full w-full object-cover"
data-motion-beat="media"
/>
<div class="container-amare relative z-10 w-full py-8 md:py-12">
<div class="max-w-3xl border border-amare-border bg-amare-bg p-6 text-amare-text md:p-10" data-motion-beat="heading">
@if ($brandMark || filled($eyebrow))
<div class="mb-5 flex items-center gap-4" data-motion-beat="seal">
@if ($brandMark)
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
@endif
@if (filled($eyebrow))
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $eyebrow }}</p>
@endif
</div>
@endif
<h1 id="{{ $headingId }}" data-motion-beat="title" class="max-w-3xl text-display font-medium text-amare-text">{{ $title }}</h1>
@if (filled($summary))
<p class="mt-5 max-w-2xl text-lg text-amare-text-muted">{{ $summary }}</p>
@endif
@if (filled($metadata))
<p class="mt-4 text-sm break-words text-amare-text-muted">{{ $metadata }}</p>
@endif
@if (trim((string) $slot) !== '')
<div class="mt-7" data-motion-beat="cta">{{ $slot }}</div>
@endif
</div>
</div>
</div>
@else
<div class="container-amare py-16 md:py-24" data-tonal-hero>
<div class="max-w-3xl space-y-4" data-motion-beat="heading">
@if ($brandMark || filled($eyebrow))
<div class="flex items-center gap-4" data-motion-beat="seal">
@if ($brandMark)
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
@endif
@if (filled($eyebrow))
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $eyebrow }}</p>
@endif
</div>
@endif
<h1 id="{{ $headingId }}" data-motion-beat="title" class="text-headline font-medium tracking-tight text-amare-text">{{ $title }}</h1>
@if (filled($summary))
<p class="text-lg text-amare-muted">{{ $summary }}</p>
@endif
@if (filled($metadata))
<p class="text-sm break-words text-amare-muted">{{ $metadata }}</p>
@endif
@if (trim((string) $slot) !== '')
<div data-motion-beat="cta">{{ $slot }}</div>
@endif
</div>
</div>
@endif
</section>

View File

@@ -8,31 +8,12 @@
$city = $siteSettings->city ?: 'São Paulo - SP'; $city = $siteSettings->city ?: 'São Paulo - SP';
@endphp @endphp
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg" data-motion="page-open"> <x-public.photo-hero :image-path="$siteSettings->about_image_path" :image-alt="$siteSettings->about_image_alt" eyebrow="A Amare" title="Humana no cuidado. Precisa na entrega." :summary="$siteSettings->about_summary">
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:py-24"> <p class="max-w-xl text-amare-text-muted">A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo, presença no dia do evento e uma condução serena do início ao fim.</p>
<div class="space-y-6" data-motion-beat="heading"> </x-public.photo-hero>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
<h1 class="text-headline font-medium tracking-tight text-amare-text">Humana no cuidado. Precisa na entrega.</h1>
<p class="text-lg text-amare-muted">{{ $siteSettings->about_summary }}</p>
<p class="text-amare-muted">
A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo,
presença no dia do evento e uma condução serena do início ao fim.
</p>
@if (filled($siteSettings->about_image_path))
<div class="overflow-hidden bg-amare-bg-deep">
<x-media.image
:path="$siteSettings->about_image_path"
:alt="$siteSettings->about_image_alt ?: $siteSettings->brand_name"
loading="eager"
fetchpriority="high"
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
class="img-editorial aspect-[4/3] w-full object-cover"
/>
</div>
@endif
</div>
<div class="border-b border-amare-border bg-amare-bg">
<div class="container-amare py-16 md:py-24">
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare" data-reveal-group> <ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare" data-reveal-group>
@foreach ($principles as $index => $principle) @foreach ($principles as $index => $principle)
<li class="grid grid-cols-[3rem_minmax(0,1fr)] gap-4 border-b border-amare-border pb-4 text-amare-text" data-reveal data-reveal-from="up"> <li class="grid grid-cols-[3rem_minmax(0,1fr)] gap-4 border-b border-amare-border pb-4 text-amare-text" data-reveal data-reveal-from="up">

View File

@@ -1,13 +1,10 @@
@extends('layouts.public') @extends('layouts.public')
@section('content') @section('content')
<div class="border-b border-amare-border bg-amare-bg-deep" data-motion="page-open"> <x-public.photo-hero :image-path="$siteSettings->portfolio_hero_image_path" :image-alt="$siteSettings->portfolio_hero_image_alt" eyebrow="Portfólio" title="Atmosferas que contam histórias." summary="Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência." />
<div class="border-b border-amare-border bg-amare-bg-deep">
<div class="container-amare space-y-10 py-16 md:py-24"> <div class="container-amare space-y-10 py-16 md:py-24">
<div class="max-w-2xl space-y-4" data-motion-beat="heading">
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
<h1 class="text-headline font-medium tracking-tight text-amare-text">Atmosferas que contam histórias.</h1>
<p class="text-lg text-amare-muted">Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência.</p>
</div>
@if ($cases->isEmpty()) @if ($cases->isEmpty())
<p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p> <p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p>

View File

@@ -1,35 +1,8 @@
@extends('layouts.public') @extends('layouts.public')
@section('content') @section('content')
<article data-motion="page-open"> <article>
<div class="border-b border-amare-border bg-amare-bg"> <x-public.photo-hero :image-path="$case->cover_image_path" :image-alt="$case->cover_image_alt" :eyebrow="$case->event_type" :title="$case->title" :summary="$case->summary" :metadata="collect([$case->city, $case->venue, $case->event_date?->format('d/m/Y')])->filter()->implode(' · ')" />
<div class="container-amare space-y-8 py-16 md:py-24">
<header class="max-w-3xl space-y-4" data-motion-beat="heading">
@if (filled($case->event_type))
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ $case->event_type }}</p>
@endif
<h1 class="text-headline font-medium tracking-tight text-amare-text">{{ $case->title }}</h1>
<p class="text-lg text-amare-muted">{{ $case->summary }}</p>
<p class="text-sm break-words text-amare-muted">
@if ($case->city){{ $case->city }}@endif
@if ($case->venue) · {{ $case->venue }}@endif
@if ($case->event_date) · {{ $case->event_date->format('d/m/Y') }}@endif
</p>
</header>
@if (filled($case->cover_image_path))
<x-media.image
:path="$case->cover_image_path"
:alt="$case->cover_image_alt ?: $case->title"
loading="eager"
fetchpriority="high"
sizes="(max-width: 1024px) calc(100vw - 3rem), 1120px"
class="img-editorial aspect-[16/9] w-full object-cover"
data-motion-beat="media"
/>
@endif
</div>
</div>
<div class="border-b border-amare-border bg-amare-bg-deep"> <div class="border-b border-amare-border bg-amare-bg-deep">
<div class="container-amare grid gap-10 py-16 md:grid-cols-3" data-reveal-group> <div class="container-amare grid gap-10 py-16 md:grid-cols-3" data-reveal-group>
@@ -57,12 +30,7 @@
<div class="grid gap-6 md:grid-cols-2" data-reveal-group> <div class="grid gap-6 md:grid-cols-2" data-reveal-group>
@foreach ($case->images as $image) @foreach ($case->images as $image)
<figure class="space-y-2" data-reveal data-reveal-from="up" data-reveal-media> <figure class="space-y-2" data-reveal data-reveal-from="up" data-reveal-media>
<x-media.image <x-media.image :path="$image->path" :alt="$image->alt_text" sizes="(max-width: 768px) calc(100vw - 3rem), 50vw" class="img-editorial aspect-[4/3] w-full object-cover" />
:path="$image->path"
:alt="$image->alt_text"
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
class="img-editorial aspect-[4/3] w-full object-cover"
/>
@if (filled($image->caption)) @if (filled($image->caption))
<figcaption class="text-sm text-amare-muted">{{ $image->caption }}</figcaption> <figcaption class="text-sm text-amare-muted">{{ $image->caption }}</figcaption>
@endif @endif

View File

@@ -1,13 +1,10 @@
@extends('layouts.public') @extends('layouts.public')
@section('content') @section('content')
<div class="border-b border-amare-border bg-amare-bg" data-motion="page-open"> <x-public.photo-hero :image-path="$siteSettings->services_hero_image_path" :image-alt="$siteSettings->services_hero_image_alt" eyebrow="Serviços" title="Uma mesma excelência, diferentes ocasiões." summary="O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento." />
<div class="border-b border-amare-border bg-amare-bg">
<div class="container-amare space-y-10 py-16 md:py-24"> <div class="container-amare space-y-10 py-16 md:py-24">
<div class="max-w-2xl space-y-4" data-motion-beat="heading">
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
<h1 class="text-headline font-medium tracking-tight text-amare-text">Uma mesma excelência, diferentes ocasiões.</h1>
<p class="text-lg text-amare-muted">O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento.</p>
</div>
@if ($services->isEmpty()) @if ($services->isEmpty())
<p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p> <p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p>

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Tests\Feature\PublicSite;
use App\Filament\Pages\ManageSiteSettings;
use App\Models\PortfolioCase;
use App\Models\SiteSetting;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;
use Tests\TestCase;
class ImmersivePhotoHeroTest extends TestCase
{
use RefreshDatabase;
public function test_home_hero_uses_its_own_image_without_replacing_open_graph_metadata(): void
{
Storage::fake('public');
Storage::disk('public')->put('content/heroes/home.jpg', 'home image');
$settings = SiteSetting::instance();
$settings->update([
'hero_image_path' => 'content/heroes/home.jpg',
'hero_image_alt' => 'Mesa preparada para uma celebração',
'default_og_image_path' => 'content/og/social.jpg',
'default_og_image_alt' => 'Imagem exclusiva das redes sociais',
]);
$response = $this->get(route('home'));
$response
->assertOk()
->assertSee('data-photo-hero', false)
->assertSee('content/heroes/home.jpg', false)
->assertSee('loading="eager"', false)
->assertSee('fetchpriority="high"', false)
->assertSee('sizes="100vw"', false)
->assertSee('content="http://localhost/storage/content/og/social.jpg"', false);
}
public function test_public_openings_fall_back_to_tonal_layout_when_no_image_is_configured(): void
{
$settings = SiteSetting::instance();
$settings->update([
'hero_image_path' => null,
'hero_image_alt' => null,
'services_hero_image_path' => null,
'services_hero_image_alt' => null,
'portfolio_hero_image_path' => null,
'portfolio_hero_image_alt' => null,
'about_image_path' => null,
'about_image_alt' => null,
]);
$case = PortfolioCase::factory()->published()->create(['cover_image_path' => '', 'cover_image_alt' => '']);
foreach ([route('home'), route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug), route('about')] as $route) {
$this->get($route)
->assertOk()
->assertDontSee('data-photo-hero', false)
->assertSee('data-tonal-hero', false);
}
}
public function test_configured_route_and_case_images_render_as_immersive_heroes(): void
{
$settings = SiteSetting::instance();
$settings->update([
'services_hero_image_path' => 'content/heroes/services.jpg',
'services_hero_image_alt' => 'Detalhe de uma mesa corporativa',
'portfolio_hero_image_path' => 'content/heroes/portfolio.jpg',
'portfolio_hero_image_alt' => 'Ambiente de celebração ao entardecer',
'about_image_path' => 'content/heroes/about.jpg',
'about_image_alt' => 'Caderno e flores no estúdio da Amare',
]);
$case = PortfolioCase::factory()->published()->create([
'cover_image_path' => 'content/cases/capa.jpg',
'cover_image_alt' => 'Cerimônia ao ar livre',
]);
foreach ([route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug), route('about')] as $route) {
$this->get($route)
->assertOk()
->assertSee('data-photo-hero', false)
->assertSee('loading="eager"', false)
->assertSee('fetchpriority="high"', false)
->assertSee('sizes="100vw"', false);
}
}
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
{
Storage::fake('public');
$this->actingAs(User::factory()->admin()->create());
Livewire::test(ManageSiteSettings::class)
->set('data.hero_image_path', [UploadedFile::fake()->create('hero.jpg', 100, 'image/jpeg')])
->set('data.hero_image_alt', null)
->call('save')
->assertHasFormErrors(['hero_image_alt' => 'required']);
Livewire::test(ManageSiteSettings::class)
->set('data.hero_image_path', null)
->set('data.hero_image_alt', null)
->call('save')
->assertHasNoFormErrors();
}
}

View File

@@ -20,8 +20,8 @@ class MotionMarkupTest extends TestCase
$settings = SiteSetting::instance(); $settings = SiteSetting::instance();
$settings->update([ $settings->update([
'hero_title' => 'Celebrações com propósito', 'hero_title' => 'Celebrações com propósito',
'default_og_image_path' => 'media/hero.jpg', 'hero_image_path' => 'media/hero.jpg',
'default_og_image_alt' => 'Capa editorial', 'hero_image_alt' => 'Capa editorial',
]); ]);
Service::factory()->published()->featured()->create(['title' => 'Casamentos']); Service::factory()->published()->featured()->create(['title' => 'Casamentos']);

View File

@@ -120,7 +120,7 @@ class PublicPagesTest extends TestCase
->assertSee('Sobre a Amare boutique') ->assertSee('Sobre a Amare boutique')
->assertSee('São Paulo - SP') ->assertSee('São Paulo - SP')
->assertDontSee('Fortaleza') ->assertDontSee('Fortaleza')
->assertSee('min-h-[calc(100dvh-14rem)]', false); ->assertSee('data-tonal-hero', false);
$this->get(route('privacy')) $this->get(route('privacy'))
->assertOk() ->assertOk()