feat: adicionar heros fotográficos imersivos (#45)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
76
resources/views/components/public/photo-hero.blade.php
Normal file
76
resources/views/components/public/photo-hero.blade.php
Normal 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>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
112
tests/Feature/PublicSite/ImmersivePhotoHeroTest.php
Normal file
112
tests/Feature/PublicSite/ImmersivePhotoHeroTest.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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']);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user