Merge pull request #59 from manoel-freitas/feat/services-page-preview

feat: Serviços — página conforme preview (Hero editorial + vertentes + modalidades + compare + corporate)
This commit is contained in:
2026-08-12 09:00:04 -03:00
committed by GitHub
13 changed files with 301 additions and 123 deletions

View File

@@ -24,6 +24,12 @@ class WeddingPackageForm
->label('Número / tipo (ex.: 01 / COMPLETA)') ->label('Número / tipo (ex.: 01 / COMPLETA)')
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('tag')
->label('Etiqueta curta (ex.: Assessoria completa)')
->maxLength(255),
TextInput::make('subtitle')
->label('Frase de apoio')
->maxLength(255),
Textarea::make('summary') Textarea::make('summary')
->label('Resumo') ->label('Resumo')
->required() ->required()
@@ -44,6 +50,12 @@ class WeddingPackageForm
->label('Texto do botão') ->label('Texto do botão')
->required() ->required()
->maxLength(255), ->maxLength(255),
TextInput::make('compare_heading')
->label('Comparação — título (leitura rápida)')
->maxLength(255),
TextInput::make('compare_summary')
->label('Comparação — resumo (leitura rápida)')
->maxLength(255),
TextInput::make('sort_order') TextInput::make('sort_order')
->label('Ordem') ->label('Ordem')
->numeric() ->numeric()

View File

@@ -16,13 +16,17 @@ use Illuminate\Support\Carbon;
/** /**
* @property string $name * @property string $name
* @property string $level * @property string $level
* @property string|null $tag
* @property string|null $subtitle
* @property string $summary * @property string $summary
* @property list<string> $scope_items * @property list<string> $scope_items
* @property string $cta_label * @property string $cta_label
* @property string|null $compare_heading
* @property string|null $compare_summary
* @property int $sort_order * @property int $sort_order
* @property Carbon|null $published_at * @property Carbon|null $published_at
*/ */
#[Fillable(['name', 'level', 'summary', 'scope_items', 'cta_label', 'sort_order', 'published_at'])] #[Fillable(['name', 'level', 'tag', 'subtitle', 'summary', 'scope_items', 'cta_label', 'compare_heading', 'compare_summary', 'sort_order', 'published_at'])]
#[UsePolicy(WeddingPackagePolicy::class)] #[UsePolicy(WeddingPackagePolicy::class)]
class WeddingPackage extends Model class WeddingPackage extends Model
{ {

View File

@@ -18,9 +18,13 @@ class WeddingPackageFactory extends Factory
return [ return [
'name' => fake()->unique()->words(2, true), 'name' => fake()->unique()->words(2, true),
'level' => 'Assessoria', 'level' => 'Assessoria',
'tag' => fake()->words(2, true),
'subtitle' => fake()->sentence(),
'summary' => fake()->sentence(), 'summary' => fake()->sentence(),
'scope_items' => [fake()->sentence()], 'scope_items' => [fake()->sentence()],
'cta_label' => 'Conversar sobre esta modalidade', 'cta_label' => 'Conversar sobre esta modalidade',
'compare_heading' => fake()->words(3, true),
'compare_summary' => fake()->sentence(),
'sort_order' => 0, 'sort_order' => 0,
'published_at' => null, 'published_at' => null,
]; ];

View File

@@ -0,0 +1,27 @@
<?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('wedding_packages', function (Blueprint $table): void {
$table->string('tag')->nullable();
$table->string('subtitle')->nullable();
$table->string('compare_heading')->nullable();
$table->string('compare_summary')->nullable();
});
}
public function down(): void
{
Schema::table('wedding_packages', function (Blueprint $table): void {
$table->dropColumn(['tag', 'subtitle', 'compare_heading', 'compare_summary']);
});
}
};

View File

@@ -9,9 +9,9 @@ use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
/** /**
* Wedding package modalities shown on the home page (03 Amare Casamentos). * Wedding package modalities shown on the home page (03 Amare Casamentos)
* Copy is approved from the structural model preview (preview(1).html) and * and the services page. Copy is approved from the structural model preview
* remains editable via the Filament resource. * (preview(2).html) and remains editable via the Filament resource.
*/ */
class WeddingPackagesSeeder extends Seeder class WeddingPackagesSeeder extends Seeder
{ {
@@ -23,40 +23,55 @@ class WeddingPackagesSeeder extends Seeder
[ [
'name' => 'Essenza', 'name' => 'Essenza',
'level' => '01 / COMPLETA', 'level' => '01 / COMPLETA',
'summary' => 'Para casais que desejam contar com a Amare desde o planejamento até a realização do casamento.', 'tag' => 'Assessoria completa',
'subtitle' => 'Do planejamento ao grande dia.',
'summary' => 'Para casais que desejam contar com a Amare desde o início ou ainda estão estruturando etapas importantes.',
'scope_items' => [ 'scope_items' => [
'Planejamento e organização', 'Estruturação do planejamento',
'Gestão de etapas e prioridades', 'Cronogramas, prazos e prioridades',
'Acompanhamento de fornecedores', 'Orientação e gestão de fornecedores',
'Coordenação do grande dia', 'Alinhamento entre os envolvidos',
'Coordenação da execução',
], ],
'cta_label' => 'Quero conhecer a Essenza', 'cta_label' => 'Quero conhecer a Essenza',
'compare_heading' => 'Começar com a Amare',
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
'sort_order' => 1, 'sort_order' => 1,
], ],
[ [
'name' => 'Conduzione', 'name' => 'Conduzione',
'level' => '02 / PARCIAL', 'level' => '02 / PARCIAL',
'summary' => 'Para quem já começou e deseja estruturar o que falta, organizar fornecedores e seguir com acompanhamento profissional.', 'tag' => 'Assessoria parcial',
'subtitle' => 'Para quem já começou e quer seguir acompanhado.',
'summary' => 'Para casais que já tomaram decisões e contrataram parte dos fornecedores, mas precisam estruturar o que falta.',
'scope_items' => [ 'scope_items' => [
'Diagnóstico do planejamento', 'Diagnóstico do planejamento existente',
'Organização das pendências', 'Organização das pendências',
'Gestão dos próximos passos', 'Gestão dos fornecedores contratados',
'Coordenação do grande dia', 'Orientação para próximas decisões',
'Coordenação da execução',
], ],
'cta_label' => 'Quero conhecer a Conduzione', 'cta_label' => 'Quero conhecer a Conduzione',
'compare_heading' => 'Trazer a Amare para o caminho',
'compare_summary' => 'O planejamento existe, mas ainda há decisões e gestão pela frente.',
'sort_order' => 2, 'sort_order' => 2,
], ],
[ [
'name' => 'Grand Jour', 'name' => 'Grand Jour',
'level' => '03 / FINAL', 'level' => '03 / FINAL',
'summary' => 'Para casais que já planejaram o casamento e precisam de uma equipe para assumir alinhamentos e operação na reta final.', 'tag' => 'Assessoria final',
'subtitle' => 'O planejamento está pronto. Agora é hora de viver.',
'summary' => 'Para casais na reta final que precisam de uma equipe para assumir alinhamentos, cronograma e operação.',
'scope_items' => [ 'scope_items' => [
'Imersão no planejamento existente', 'Imersão no planejamento existente',
'Alinhamento de fornecedores', 'Conferência das informações',
'Cronograma final', 'Alinhamento dos fornecedores',
'Organização do cronograma final',
'Gestão da operação do evento', 'Gestão da operação do evento',
], ],
'cta_label' => 'Quero conhecer a Grand Jour', 'cta_label' => 'Quero conhecer a Grand Jour',
'compare_heading' => 'Entregar a operação para a Amare',
'compare_summary' => 'O projeto está pronto e o foco passa a ser alinhamento e execução.',
'sort_order' => 3, 'sort_order' => 3,
], ],
]; ];

View File

@@ -67,10 +67,13 @@ The home page SHALL render, in order: header/navigation, hero, manifesto, featur
### Requirement: Listing and detail pages exist for catalog content ### Requirement: Listing and detail pages exist for catalog content
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile. The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
#### Scenario: Services listing shows published services #### Scenario: Services listing renders editorial sections with published modalities
- **WHEN** a visitor loads `/servicos` - **WHEN** a visitor loads `/servicos`
- **THEN** every published service MUST be listed with title and summary in `sort_order` - **THEN** the page MUST present the two vertentes (Amare Casamentos and Amare Corporate) with CTAs to their sections
- **AND** every published wedding modality MUST be rendered in `sort_order` with tag, name, subtitle, summary and scope
- **AND** a quick-read comparison of the modalities MUST use each modality's `compare_heading` and `compare_summary`
- **AND** unpublished modalities MUST NOT be rendered
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system) - **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
#### Scenario: Gallery respects stored order #### Scenario: Gallery respects stored order

View File

@@ -1,26 +1,40 @@
@props([ @props([
'settings', 'settings',
'eyebrow' => '04 — Amare Corporate',
'heading' => 'Projetos corporativos tratados como experiência, não apenas operação.',
'meta' => 'No MVP, esta frente pode apresentar claramente o que a Amare oferece mesmo sem inventar um portfólio corporativo que ainda não existe.',
'steps' => null,
'showMeta' => true,
'ctaLabel' => 'Falar sobre um evento corporativo',
'ctaHref' => '#contato',
'showAside' => true,
]) ])
@php
$steps = $steps ?? ($settings->corporate_steps ?? []);
@endphp
<section <section
aria-labelledby="corporate-heading" aria-labelledby="corporate-heading"
class="home-chapter border-b border-amare-border bg-amare-bg" class="home-chapter border-b border-amare-border bg-amare-bg"
data-chapter="corporate" data-chapter="corporate"
id="corporate" id="corporate"
> >
<div class="container-amare grid gap-12 py-16 md:py-24 lg:grid-cols-2 lg:gap-[70px]" data-reveal-group> <div class="container-amare grid gap-12 py-16 md:py-24 @if ($showAside) lg:grid-cols-2 lg:gap-[70px] @endif" data-reveal-group>
<div class="flex flex-col gap-8"> <div class="flex flex-col gap-8">
<div class="space-y-4" data-reveal data-reveal-from="up"> <div class="space-y-4" data-reveal data-reveal-from="up">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">04 Amare Corporate</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
<h2 id="corporate-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Projetos corporativos tratados como experiência, não apenas operação.</h2> <h2 id="corporate-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
</div> </div>
@if ($showMeta && filled($meta))
<p class="max-w-[760px] text-[clamp(1.1875rem,2vw,1.5rem)] leading-[1.55] text-amare-text-muted" data-reveal data-reveal-from="up"> <p class="max-w-[760px] text-[clamp(1.1875rem,2vw,1.5rem)] leading-[1.55] text-amare-text-muted" data-reveal data-reveal-from="up">
No MVP, esta frente pode apresentar claramente o que a Amare oferece mesmo sem inventar um portfólio corporativo que ainda não existe. {{ $meta }}
</p> </p>
@endif
<div class="border-t border-amare-border" data-reveal data-reveal-from="up"> <div class="border-t border-amare-border" data-reveal data-reveal-from="up">
@foreach ($settings->corporate_steps ?? [] as $index => $step) @foreach ($steps as $index => $step)
<div class="flex gap-8 border-b border-amare-border py-[23px]"> <div class="flex gap-8 border-b border-amare-border py-[23px]">
<p class="w-[44px] shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</p> <p class="w-[44px] shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</p>
<div class="space-y-1"> <div class="space-y-1">
@@ -31,15 +45,17 @@
@endforeach @endforeach
</div> </div>
<a href="#contato" class="btn btn-primary mt-7 self-start text-xs font-bold uppercase tracking-[0.09em]" data-reveal data-reveal-from="up"> <a href="{{ $ctaHref }}" class="btn btn-primary mt-7 self-start text-xs font-bold uppercase tracking-[0.09em]" data-reveal data-reveal-from="up">
Falar sobre um evento corporativo {{ $ctaLabel }}
</a> </a>
</div> </div>
@if ($showAside)
<div class="flex min-h-[420px] flex-col items-center justify-center gap-4 bg-amare-bg-deep p-10 text-center" data-reveal data-reveal-from="up"> <div class="flex min-h-[420px] flex-col items-center justify-center gap-4 bg-amare-bg-deep p-10 text-center" data-reveal data-reveal-from="up">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Portfólio Corporate</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Portfólio Corporate</p>
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Conteúdo em construção</h3> <h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Conteúdo em construção</h3>
<p class="max-w-sm text-amare-text-muted">Em vez de usar fotografias genéricas como se fossem cases, a página assume com elegância que o portfólio será construído com projetos reais.</p> <p class="max-w-sm text-amare-text-muted">Em vez de usar fotografias genéricas como se fossem cases, a página assume com elegância que o portfólio será construído com projetos reais.</p>
</div> </div>
@endif
</div> </div>
</section> </section>

View File

@@ -1,6 +1,7 @@
@props([ @props([
'settings', 'settings',
'editorial' => false, 'editorial' => false,
'ctaHref' => null,
]) ])
<section aria-labelledby="final-cta-heading" @class([ <section aria-labelledby="final-cta-heading" @class([
@@ -17,7 +18,7 @@
</p> </p>
<div> <div>
<a <a
href="{{ route('briefing') }}" href="{{ $ctaHref ?? route('briefing') }}"
class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"
> >
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }} {{ $settings->hero_cta_label ?: 'Solicitar proposta' }}

View File

@@ -1,11 +1,23 @@
@props([ @props([
'packages', 'packages',
'settings', 'settings',
'eyebrow' => '03 — Amare Casamentos',
'heading' => 'Um acompanhamento para cada momento.',
'intro' => 'As modalidades são pontos de partida. O site apresenta o conceito e ajuda o casal a reconhecer seu momento; detalhes operacionais e comerciais ficam para a conversa e proposta.',
'kicker' => 'level',
'showSubtitle' => false,
'showIntro' => true,
'showNote' => true,
'ctaRoute' => null,
'bandCtaHref' => null,
'bandBody' => 'Conte um pouco sobre o seu casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático.',
'note' => 'Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.',
]) ])
@php @php
$whatsappDigits = preg_replace('/\D/', '', (string) $settings->whatsapp_number); $whatsappDigits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
$hasWhatsapp = strlen($whatsappDigits) >= 10; $hasWhatsapp = strlen($whatsappDigits) >= 10;
$bandHref = $bandCtaHref ?? route('briefing');
@endphp @endphp
<section <section
@@ -17,10 +29,12 @@
<div class="container-amare py-16 md:py-24" data-reveal-group> <div class="container-amare py-16 md:py-24" data-reveal-group>
<div class="flex flex-col gap-6 md:flex-row md:items-end md:justify-between" data-reveal data-reveal-from="up"> <div class="flex flex-col gap-6 md:flex-row md:items-end md:justify-between" data-reveal data-reveal-from="up">
<div class="max-w-2xl space-y-4"> <div class="max-w-2xl space-y-4">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">03 Amare Casamentos</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
<h2 id="packages-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Um acompanhamento para cada momento.</h2> <h2 id="packages-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
</div> </div>
<p class="max-w-[600px] text-amare-text-muted">As modalidades são pontos de partida. O site apresenta o conceito e ajuda o casal a reconhecer seu momento; detalhes operacionais e comerciais ficam para a conversa e proposta.</p> @if ($showIntro && filled($intro))
<p class="max-w-[600px] text-amare-text-muted">{{ $intro }}</p>
@endif
</div> </div>
@if ($packages->isNotEmpty()) @if ($packages->isNotEmpty())
@@ -28,8 +42,11 @@
@foreach ($packages as $package) @foreach ($packages as $package)
<article class="flex min-h-[480px] flex-col justify-between gap-8 border-b border-r border-amare-border p-8 md:p-10" data-reveal data-reveal-from="up"> <article class="flex min-h-[480px] flex-col justify-between gap-8 border-b border-r border-amare-border p-8 md:p-10" data-reveal data-reveal-from="up">
<div class="space-y-5"> <div class="space-y-5">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ $package->level }}</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ $kicker === 'tag' ? $package->tag : $package->level }}</p>
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">{{ $package->name }}</h3> <h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">{{ $package->name }}</h3>
@if ($showSubtitle && filled($package->subtitle))
<p class="text-[clamp(1.0625rem,1.7vw,1.25rem)] font-medium leading-snug text-amare-text">{{ $package->subtitle }}</p>
@endif
<p class="text-amare-text-muted">{{ $package->summary }}</p> <p class="text-amare-text-muted">{{ $package->summary }}</p>
<ul class="space-y-3 text-amare-text-muted"> <ul class="space-y-3 text-amare-text-muted">
@foreach ($package->scope_items as $item) @foreach ($package->scope_items as $item)
@@ -41,10 +58,13 @@
$waLink = $hasWhatsapp $waLink = $hasWhatsapp
? 'https://wa.me/'.$whatsappDigits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.') ? 'https://wa.me/'.$whatsappDigits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.')
: route('briefing', ['servico_interesse' => $package->name]); : route('briefing', ['servico_interesse' => $package->name]);
$href = $ctaRoute
? route($ctaRoute)
: $waLink;
@endphp @endphp
<a <a
href="{{ $waLink }}" href="{{ $href }}"
@if ($hasWhatsapp) target="_blank" rel="noopener noreferrer" @endif @if ($ctaRoute === null && $hasWhatsapp) target="_blank" rel="noopener noreferrer" @endif
class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]" class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]"
> >
{{ $package->cta_label }} {{ $package->cta_label }}
@@ -57,13 +77,15 @@
<div class="mt-6 flex flex-col justify-between gap-8 bg-amare-accent-deep p-8 text-amare-accent-text md:flex-row md:items-center md:p-12" data-reveal data-reveal-from="up"> <div class="mt-6 flex flex-col justify-between gap-8 bg-amare-accent-deep p-8 text-amare-accent-text md:flex-row md:items-center md:p-12" data-reveal data-reveal-from="up">
<div class="max-w-xl space-y-3"> <div class="max-w-xl space-y-3">
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3> <h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
<p class="text-amare-accent-text/80">Conte um pouco sobre o seu casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático.</p> <p class="text-amare-accent-text/80">{{ $bandBody }}</p>
</div> </div>
<a href="{{ route('briefing') }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto"> <a href="{{ $bandHref }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto">
Conversar com a Amare Conversar com a Amare
</a> </a>
</div> </div>
<p class="mt-6 text-sm text-amare-muted">Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.</p> @if ($showNote && filled($note))
<p class="mt-6 text-sm text-amare-muted">{{ $note }}</p>
@endif
</div> </div>
</section> </section>

View File

@@ -1,4 +1,23 @@
@props([]) @props([
'eyebrow' => '02 — Duas vertentes, uma mesma forma de cuidar',
'heading' => 'Como podemos acompanhar você?',
'cards' => [
[
'label' => 'Amare Casamentos',
'title' => 'Planejamento e condução para viver o processo com mais leveza.',
'body' => 'Do início da organização à reta final, modalidades pensadas para diferentes momentos do casal.',
'href' => '#casamentos',
'cta' => 'Conhecer casamentos',
],
[
'label' => 'Amare Corporate',
'title' => 'Eventos empresariais com organização, intenção e execução segura.',
'body' => 'Uma frente própria para projetos corporativos, com linguagem e necessidades diferentes das celebrações sociais.',
'href' => '#corporate',
'cta' => 'Conhecer Corporate',
],
],
])
<section <section
aria-labelledby="vertentes-heading" aria-labelledby="vertentes-heading"
@@ -8,34 +27,24 @@
> >
<div class="container-amare py-16 md:py-24" data-reveal-group> <div class="container-amare py-16 md:py-24" data-reveal-group>
<div class="max-w-3xl space-y-4" data-reveal data-reveal-from="up"> <div class="max-w-3xl space-y-4" data-reveal data-reveal-from="up">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">02 Duas vertentes, uma mesma forma de cuidar</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
<h2 id="vertentes-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Como podemos acompanhar você?</h2> <h2 id="vertentes-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
</div> </div>
<div class="mt-12 grid gap-6 md:mt-16 md:grid-cols-2 md:gap-[24px]"> <div class="mt-12 grid gap-6 md:mt-16 md:grid-cols-2 md:gap-[24px]">
@foreach ($cards as $card)
<article class="relative flex min-h-[420px] flex-col justify-end bg-amare-bg-deep p-8 md:p-10" data-reveal data-reveal-from="up"> <article class="relative flex min-h-[420px] flex-col justify-end bg-amare-bg-deep p-8 md:p-10" data-reveal data-reveal-from="up">
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-[43%] bg-gradient-to-b from-amare-bg to-transparent"></div> <div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-[43%] bg-gradient-to-b from-amare-bg to-transparent"></div>
<div class="relative flex flex-col gap-5"> <div class="relative flex flex-col gap-5">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Amare Casamentos</p> <p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $card['label'] }}</p>
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Planejamento e condução para viver o processo com mais leveza.</h3> <h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">{{ $card['title'] }}</h3>
<p class="text-amare-text-muted">Do início da organização à reta final, modalidades pensadas para diferentes momentos do casal.</p> <p class="text-amare-text-muted">{{ $card['body'] }}</p>
<a href="#casamentos" class="btn btn-outline mt-2 self-start text-xs font-bold uppercase tracking-[0.09em]"> <a href="{{ $card['href'] }}" class="btn btn-outline mt-2 self-start text-xs font-bold uppercase tracking-[0.09em]">
Conhecer casamentos {{ $card['cta'] }}
</a>
</div>
</article>
<article class="relative flex min-h-[420px] flex-col justify-end bg-amare-bg-deep p-8 md:p-10" data-reveal data-reveal-from="up">
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-[43%] bg-gradient-to-b from-amare-bg to-transparent"></div>
<div class="relative flex flex-col gap-5">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Amare Corporate</p>
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Eventos empresariais com organização, intenção e execução segura.</h3>
<p class="text-amare-text-muted">Uma frente própria para projetos corporativos, com linguagem e necessidades diferentes das celebrações sociais.</p>
<a href="#corporate" class="btn btn-outline mt-2 self-start text-xs font-bold uppercase tracking-[0.09em]">
Conhecer Corporate
</a> </a>
</div> </div>
</article> </article>
@endforeach
</div> </div>
</div> </div>
</section> </section>

View File

@@ -1,55 +1,92 @@
@extends('layouts.public') @extends('layouts.public')
@section('content') @section('content')
<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." /> <x-public.photo-hero
:image-path="$siteSettings->services_hero_image_path"
<div class="border-b border-amare-border bg-amare-bg"> :image-alt="$siteSettings->services_hero_image_alt"
<div class="container-amare space-y-10 py-16 md:py-24"> eyebrow="Serviços"
title="O cuidado certo para cada tipo de evento."
@if ($services->isEmpty()) summary="A Amare acompanha projetos sociais e corporativos com planejamento, organização e condução profissional. O formato muda conforme o momento, o evento e a necessidade de cada cliente."
<p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p> >
@else <div class="flex flex-wrap items-center gap-4">
@php($hasPrioritizedImage = false) <a href="#vertentes" class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]">
<div class="divide-y divide-amare-border border-y border-amare-border" data-editorial-service-list data-reveal-group> Conhecer os serviços
@foreach ($services as $index => $service) </a>
<article @class([ <a href="{{ route('contact') }}" class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
'grid gap-4 py-8 md:grid-cols-[5rem_minmax(0,1fr)_minmax(0,1.2fr)] md:items-start', <span class="border-b border-amare-accent pb-1">Tenho um evento em mente</span>
'md:translate-x-8' => $loop->even, </a>
]) data-reveal data-reveal-from="up">
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
<div class="space-y-3">
<h2 class="text-2xl font-medium text-amare-text md:text-3xl">{{ $service->title }}</h2>
<p class="text-amare-muted">{{ $service->summary }}</p>
</div> </div>
<div class="space-y-4"> </x-public.photo-hero>
@if (filled($service->cover_image_path))
@php($prioritizeImage = ! $hasPrioritizedImage) <x-home.vertentes
<x-media.image eyebrow="Duas vertentes"
:path="$service->cover_image_path" heading="Escolha por contexto, não por uma lista de pacotes."
:alt="$service->cover_image_alt ?: $service->title" :cards="[
:loading="$prioritizeImage ? 'eager' : 'lazy'" [
:fetchpriority="$prioritizeImage ? 'high' : null" 'label' => 'Amare Casamentos',
sizes="(max-width: 768px) calc(100vw - 3rem), 40vw" 'title' => 'Acompanhamento para diferentes momentos do planejamento.',
class="img-editorial aspect-[16/10] w-full object-cover" 'body' => 'Três modalidades para casais que estão começando, já avançaram ou precisam de apoio na reta final.',
'href' => '#casamentos',
'cta' => 'Ver modalidades',
],
[
'label' => 'Amare Corporate',
'title' => 'Planejamento e execução para eventos empresariais.',
'body' => 'Uma frente própria, com linguagem profissional e estrutura adequada a projetos corporativos.',
'href' => '#corporate',
'cta' => 'Ver Corporate',
],
]"
/>
<x-home.packages
:packages="$weddingPackages"
:settings="$siteSettings"
eyebrow="Amare Casamentos"
heading="Três formas de ter a Amare ao lado."
:show-intro="false"
kicker="tag"
:show-subtitle="true"
cta-route="contact"
:band-cta-href="route('contact')"
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
:show-note="false"
/> />
@php($hasPrioritizedImage = true)
@endif
@if (filled($service->description))
<div class="text-amare-muted">
{!! nl2br(e($service->description)) !!}
</div>
@endif
</div>
</article>
@endforeach
</div>
@endif
</div>
</div>
@if ($weddingPackages->isNotEmpty()) @if ($weddingPackages->isNotEmpty())
<section class="border-b border-amare-border bg-amare-bg-deep py-16 md:py-24" aria-labelledby="wedding-packages-heading"><div class="container-amare"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">Casamentos</p><h2 id="wedding-packages-heading" class="mt-3 text-3xl font-medium">Modalidades de assessoria</h2><div class="mt-8 grid gap-6 md:grid-cols-3">@foreach ($weddingPackages as $package)<article class="border-t border-amare-border pt-4"><p class="text-sm text-amare-accent">{{ $package->level }}</p><h3 class="mt-2 text-2xl font-medium">{{ $package->name }}</h3><p class="mt-2 text-amare-muted">{{ $package->summary }}</p><ul class="mt-4 space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li>{{ $item }}</li>@endforeach</ul></article>@endforeach</div></div></section> <section aria-label="Comparação rápida entre as modalidades" class="border-b border-amare-border bg-amare-bg">
<div class="container-amare py-16 md:py-24" data-reveal-group>
<div class="grid border-l border-t border-amare-border md:grid-cols-3">
@foreach ($weddingPackages as $package)
@if (filled($package->compare_heading))
<article class="flex min-h-[300px] flex-col justify-between gap-6 border-b border-r border-amare-border p-8 md:p-10" data-reveal data-reveal-from="up">
<div class="space-y-4">
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ $package->name }}</p>
<h3 class="text-[clamp(1.25rem,2vw,1.5rem)] font-medium leading-tight text-amare-text">{{ $package->compare_heading }}</h3>
<p class="text-amare-text-muted">{{ $package->compare_summary }}</p>
</div>
</article>
@endif
@endforeach
</div>
</div>
</section>
@endif @endif
<x-home.final-cta :settings="$siteSettings" /> <x-home.corporate
:settings="$siteSettings"
eyebrow="Amare Corporate"
heading="Organização, produção e condução com linguagem empresarial."
:show-meta="false"
:steps="[
['title' => 'Planejamento', 'body' => 'Estruturação do evento, escopo, prioridades e cronograma.'],
['title' => 'Produção', 'body' => 'Coordenação dos elementos e fornecedores necessários ao projeto.'],
['title' => 'Execução', 'body' => 'Acompanhamento e condução do evento conforme o planejamento aprovado.'],
]"
cta-label="Falar sobre um evento Corporate"
:cta-href="route('contact')"
:show-aside="false"
/>
<x-home.final-cta :settings="$siteSettings" :cta-href="route('contact')" />
@endsection @endsection

View File

@@ -42,8 +42,8 @@ class MediaPerformanceTest extends TestCase
$about = $this->get(route('about'))->assertOk()->getContent(); $about = $this->get(route('about'))->assertOk()->getContent();
$detail = $this->get(route('portfolio.show', $firstCase->slug))->assertOk()->getContent(); $detail = $this->get(route('portfolio.show', $firstCase->slug))->assertOk()->getContent();
$this->assertMatchesRegularExpression('/service-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $services); $this->assertStringNotContainsString('service-first.jpg', $services);
$this->assertMatchesRegularExpression('/service-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $services); $this->assertStringNotContainsString('service-later.jpg', $services);
$this->assertMatchesRegularExpression('/case-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio); $this->assertMatchesRegularExpression('/case-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
$this->assertMatchesRegularExpression('/case-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $portfolio); $this->assertMatchesRegularExpression('/case-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $portfolio);
$this->assertMatchesRegularExpression('/about\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $about); $this->assertMatchesRegularExpression('/about\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $about);
@@ -89,7 +89,7 @@ class MediaPerformanceTest extends TestCase
$services = $this->get(route('services.index'))->assertOk()->getContent(); $services = $this->get(route('services.index'))->assertOk()->getContent();
$portfolio = $this->get(route('portfolio.index'))->assertOk()->getContent(); $portfolio = $this->get(route('portfolio.index'))->assertOk()->getContent();
$this->assertMatchesRegularExpression('/visible-service\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $services); $this->assertStringNotContainsString('visible-service.jpg', $services);
$this->assertMatchesRegularExpression('/visible-case\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio); $this->assertMatchesRegularExpression('/visible-case\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
} }
} }

View File

@@ -6,8 +6,8 @@ namespace Tests\Feature\PublicSite;
use App\Models\PortfolioCase; use App\Models\PortfolioCase;
use App\Models\PortfolioImage; use App\Models\PortfolioImage;
use App\Models\Service;
use App\Models\SiteSetting; use App\Models\SiteSetting;
use App\Models\WeddingPackage;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -18,17 +18,45 @@ class PublicPagesTest extends TestCase
{ {
use RefreshDatabase; use RefreshDatabase;
public function test_services_index_lists_published_services_in_sort_order(): void public function test_services_index_renders_editorial_sections_with_published_modalities_only(): void
{ {
SiteSetting::instance(); SiteSetting::instance();
Service::factory()->create(['title' => 'Rascunho', 'published_at' => null]); WeddingPackage::factory()->create(['name' => 'Rascunho', 'published_at' => null]);
Service::factory()->published()->create(['title' => 'Serviço B', 'sort_order' => 20]); WeddingPackage::factory()->published()->create([
Service::factory()->published()->create(['title' => 'Serviço A', 'sort_order' => 10]); 'name' => 'Essenza',
'level' => '01 / COMPLETA',
'tag' => 'Assessoria completa',
'subtitle' => 'Do planejamento ao grande dia.',
'summary' => 'Para casais que desejam contar com a Amare desde o início.',
'scope_items' => ['Estruturação do planejamento', 'Coordenação da execução'],
'cta_label' => 'Quero conhecer a Essenza',
'compare_heading' => 'Começar com a Amare',
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
'sort_order' => 10,
]);
$this->get(route('services.index')) $this->get(route('services.index'))
->assertOk() ->assertOk()
->assertSeeInOrder(['Serviço A', 'Serviço B']) ->assertSee('O cuidado certo para cada tipo de evento.')
->assertSee('Conhecer os serviços')
->assertSee('Tenho um evento em mente')
->assertSee('Duas vertentes')
->assertSee('Escolha por contexto, não por uma lista de pacotes.')
->assertSee('Amare Casamentos')
->assertSee('Acompanhamento para diferentes momentos do planejamento.')
->assertSee('Amare Corporate')
->assertSee('Planejamento e execução para eventos empresariais.')
->assertSee('Três formas de ter a Amare ao lado.')
->assertSee('Assessoria completa')
->assertSee('Essenza')
->assertSee('Do planejamento ao grande dia.')
->assertSee('Quero conhecer a Essenza')
->assertSee('Começar com a Amare')
->assertSee('Organização, produção e condução com linguagem empresarial.')
->assertSee('Estruturação do evento, escopo, prioridades e cronograma.')
->assertSee('Falar sobre um evento Corporate')
->assertSee('Do casamento ao evento corporativo, tudo começa com uma boa conversa.')
->assertDontSee('Rascunho'); ->assertDontSee('Rascunho');
} }