Compare commits
4 Commits
feat/remod
...
f4c2e4ed2f
| Author | SHA1 | Date | |
|---|---|---|---|
| f4c2e4ed2f | |||
| e2af1f6a23 | |||
| 68460e061b | |||
| fc1c6177d6 |
20
app/Application/Data/AboutContent.php
Normal file
20
app/Application/Data/AboutContent.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Data;
|
||||
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
final readonly class AboutContent
|
||||
{
|
||||
/**
|
||||
* @param Collection<int, PortfolioCase> $featuredCases
|
||||
*/
|
||||
public function __construct(
|
||||
public SiteSetting $settings,
|
||||
public Collection $featuredCases,
|
||||
) {}
|
||||
}
|
||||
25
app/Application/Queries/Marketing/GetAboutContent.php
Normal file
25
app/Application/Queries/Marketing/GetAboutContent.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Queries\Marketing;
|
||||
|
||||
use App\Application\Data\AboutContent;
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\SiteSetting;
|
||||
|
||||
final class GetAboutContent
|
||||
{
|
||||
public function __invoke(): AboutContent
|
||||
{
|
||||
return new AboutContent(
|
||||
settings: SiteSetting::instance(),
|
||||
featuredCases: PortfolioCase::query()
|
||||
->published()
|
||||
->where('is_featured', true)
|
||||
->orderBy('sort_order')
|
||||
->limit(6)
|
||||
->get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,9 @@ final class MediaGenerateVariantsCommand extends Command
|
||||
if ($settings && filled($settings->about_image_path)) {
|
||||
$paths[] = (string) $settings->about_image_path;
|
||||
}
|
||||
if ($settings && filled($settings->founder_image_path)) {
|
||||
$paths[] = (string) $settings->founder_image_path;
|
||||
}
|
||||
if ($settings && filled($settings->hero_image_path)) {
|
||||
$paths[] = (string) $settings->hero_image_path;
|
||||
}
|
||||
|
||||
@@ -268,8 +268,10 @@ class ManageSiteSettings extends Page
|
||||
->columns(2),
|
||||
Section::make('Página Sobre')
|
||||
->schema([
|
||||
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem da página Sobre', 'content/about'),
|
||||
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem do hero / Sobre', 'content/about'),
|
||||
PublicImageUploadRules::altTextField('about_image_alt', 'about_image_path'),
|
||||
PublicImageUploadRules::fileUpload('founder_image_path', 'Foto da Michele', 'content/about/founder'),
|
||||
PublicImageUploadRules::altTextField('founder_image_alt', 'founder_image_path'),
|
||||
])
|
||||
->columns(2),
|
||||
Section::make('SEO padrão')
|
||||
|
||||
@@ -5,17 +5,20 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\PublicSite;
|
||||
|
||||
use App\Application\Data\PageMeta;
|
||||
use App\Application\Queries\Marketing\GetAboutContent;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
|
||||
final class PageController extends Controller
|
||||
{
|
||||
public function about(): View
|
||||
public function about(GetAboutContent $getAboutContent): View
|
||||
{
|
||||
$settings = SiteSetting::instance();
|
||||
$content = $getAboutContent();
|
||||
$settings = $content->settings;
|
||||
|
||||
return view('pages.about', [
|
||||
'content' => $content,
|
||||
'siteSettings' => $settings,
|
||||
'pageMeta' => PageMeta::forPage(
|
||||
canonical: route('about'),
|
||||
|
||||
@@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* @property string|null $default_og_image_alt
|
||||
* @property string|null $about_image_path
|
||||
* @property string|null $about_image_alt
|
||||
* @property string|null $founder_image_path
|
||||
* @property string|null $founder_image_alt
|
||||
* @property string|null $hero_image_path
|
||||
* @property string|null $hero_image_alt
|
||||
* @property string|null $services_hero_image_path
|
||||
@@ -47,6 +49,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
'about_summary',
|
||||
'about_image_path',
|
||||
'about_image_alt',
|
||||
'founder_image_path',
|
||||
'founder_image_alt',
|
||||
'manifesto_title',
|
||||
'manifesto_lead',
|
||||
'manifesto_body',
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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('founder_image_path')->nullable()->after('about_image_alt');
|
||||
$table->string('founder_image_alt')->nullable()->after('founder_image_path');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('site_settings', function (Blueprint $table): void {
|
||||
$table->dropColumn(['founder_image_path', 'founder_image_alt']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -71,6 +71,8 @@ class ContentSeeder extends Seeder
|
||||
'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_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
||||
'founder_image_path' => $this->copyFixture('about-image.jpg', 'content/about/founder/michele.jpg'),
|
||||
'founder_image_alt' => 'Michele, da Amare',
|
||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
||||
'manifesto_lead' => 'Um evento memorável não nasce apenas de uma boa estética. Ele depende de decisões bem conduzidas, fornecedores alinhados e atenção constante ao que realmente importa.',
|
||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||
|
||||
@@ -52,6 +52,8 @@ class VisualContentSeeder extends Seeder
|
||||
'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_alt' => 'Imagem editorial da página Sobre',
|
||||
'founder_image_path' => $this->writeSolidJpeg('visual/about/founder-michele.jpg', 900, 1200, [210, 205, 192]),
|
||||
'founder_image_alt' => 'Michele, da Amare',
|
||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
||||
'manifesto_lead' => 'Um evento memorável não nasce apenas de uma boa estética. Ele depende de decisões bem conduzidas, fornecedores alinhados e atenção constante ao que realmente importa.',
|
||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||
|
||||
2
openspec/changes/remodel-about-page/.openspec.yaml
Normal file
2
openspec/changes/remodel-about-page/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-12
|
||||
27
openspec/changes/remodel-about-page/design.md
Normal file
27
openspec/changes/remodel-about-page/design.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Design: remodel About page
|
||||
|
||||
## Context
|
||||
|
||||
Mock `amare-sobre(1).html` define composição e copy. O site já tem Heritage Editorial (`tokens.css`, `DESIGN.md`), `about_image_*` no CMS, e `PortfolioCase` featured na home.
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Hero** reusa `about_image_path` com layout split próprio (`x-about.hero`), não o full-viewport `x-public.photo-hero`. Fallback tonal quando sem imagem.
|
||||
2. **Michele** usa `founder_image_*` novos; sem path → coluna tonal, sem request de imagem inventada.
|
||||
3. **Portfólio** lê até 6 casos `published` + `is_featured` via `GetAboutContent` (mesma regra da home).
|
||||
4. **Copy** de pilares/Michele/processo/CTA fixa do mock; `about_summary` alimenta lead/SEO.
|
||||
5. **Visual**: estrutura do mock; radius 0, EB Garamond, botões `btn` existentes — desvios tipográficos/pill do HTML são descartados.
|
||||
|
||||
## Spine
|
||||
|
||||
```
|
||||
PageController::about
|
||||
→ GetAboutContent
|
||||
→ AboutContent
|
||||
→ pages/about.blade.php + x-about.*
|
||||
```
|
||||
|
||||
## Risks
|
||||
|
||||
- Testes que acoplam `/sobre` a `data-photo-hero` full-vh precisam atualizar o contrato.
|
||||
- Home positioning continua usando `about_image_path` — não reaproveitar esse campo como foto da Michele.
|
||||
26
openspec/changes/remodel-about-page/proposal.md
Normal file
26
openspec/changes/remodel-about-page/proposal.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Remodel página Sobre conforme mock aprovado
|
||||
|
||||
## Why
|
||||
|
||||
A rota `/sobre` ainda usa abertura `photo-hero` full-viewport + lista de princípios. O cliente validou o mock `amare-sobre(1).html` com jornada editorial própria: hero split, pilares, bloco Michele, processo, portfólio em destaque e CTA. Sem essa remodelação, a página institucional fica desalinhada da home e do material aprovado.
|
||||
|
||||
## What Changes
|
||||
|
||||
- `/sobre` recomposta em seções `x-about.*` espelhando a ordem do mock (hero, pilares, Michele, processo, portfólio masonry, CTA).
|
||||
- Novo campo CMS `founder_image_path` / `founder_image_alt` no singleton `site_settings` para a foto da Michele; hero continua em `about_image_path`.
|
||||
- Query `GetAboutContent` + DTO `AboutContent` (featured cases ≤6) no spine Application.
|
||||
- Tokens Heritage Editorial do repo (sem Inter/Cormorant, sem pills do HTML estático).
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- CMS para copy dos pilares/Michele/processo.
|
||||
- Replicar header/footer do HTML estático.
|
||||
- Alterar tokens globais assertados por `HeritageEditorialTokensTest`.
|
||||
- Gerar fotografia da Michele (apenas upload).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `public-site-pages`: `/sobre` com composição do mock; motion contract preservado.
|
||||
- `site-settings`: campos `founder_image_path` e `founder_image_alt` administráveis no Filament.
|
||||
@@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: About page follows approved editorial composition
|
||||
|
||||
The `/sobre` route SHALL render the Heritage Editorial public layout with this section order: split hero (eyebrow “Sobre nós”, title “Sobre a Amare”, lead from `about_summary` or editorial default, CTA to `#michele`), three pillars, founder block `#michele` (Michele), process (“Como trabalhamos”), featured portfolio masonry (up to six published featured cases), and a final proposal CTA. The page MUST use shared motion markers (`data-motion="page-open"`, `data-reveal*`) without route transitions. Principles list MUST NOT appear on `/sobre` (home may still show them). Contact, privacy and error surfaces remain unchanged by this requirement.
|
||||
|
||||
#### Scenario: Visitor sees the mock section order
|
||||
|
||||
- **WHEN** a visitor loads `/sobre`
|
||||
- **THEN** the response MUST include the headings for Sobre a Amare, the three pillars, Conheça Michele, Como trabalhamos, Portfólio em destaque, and the proposal CTA copy
|
||||
- **AND** MUST NOT render the numbered principles list formerly used on About
|
||||
|
||||
#### Scenario: Founder photo comes from CMS when configured
|
||||
|
||||
- **GIVEN** `site_settings.founder_image_path` is set with alt text
|
||||
- **WHEN** a visitor loads `/sobre`
|
||||
- **THEN** the Michele section MUST render that image with the configured alt
|
||||
- **AND** MUST use eager loading only for the about hero image, not invent a founder asset path when unset
|
||||
|
||||
#### Scenario: About hero uses about_image with tonal fallback
|
||||
|
||||
- **WHEN** `about_image_path` is configured
|
||||
- **THEN** `/sobre` MUST render a split editorial hero with that media (`loading="eager"` and `fetchpriority="high"`)
|
||||
- **WHEN** `about_image_path` is empty
|
||||
- **THEN** `/sobre` MUST render a tonal hero fallback without an empty image request
|
||||
|
||||
#### Scenario: Featured portfolio tiles use real cases
|
||||
|
||||
- **GIVEN** published featured portfolio cases exist
|
||||
- **WHEN** a visitor loads `/sobre`
|
||||
- **THEN** the masonry MUST link to those cases (or the portfolio index)
|
||||
- **AND** MUST NOT invent decorative photography when no cases exist (tonal slots allowed)
|
||||
@@ -0,0 +1,47 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Site settings singleton is manageable by admin only
|
||||
|
||||
The system SHALL persist site-wide settings in a `site_settings` table as a typed singleton (SPEC WEB-06, §8.2). Fields MUST include brand name, optional logo path and logo alt text, hero copy (eyebrow, title, subtitle, primary CTA label, optional secondary CTA label, optional hero note), manifesto copy (title, lead, body), method steps (structured typed data for four editorial steps), principles (structured typed list), about summary, optional about hero image path and alt text, optional founder image path and alt text (Michele portrait for `/sobre`), contact email/phone/city, social links (jsonb), default meta title/description, default OG image path and alt text, and optional analytics fields disabled by default.
|
||||
|
||||
#### Scenario: Admin updates site settings
|
||||
|
||||
- **WHEN** an admin saves the site settings form in Filament
|
||||
- **THEN** the singleton record is updated
|
||||
- **AND** labels and validation messages are in pt-BR
|
||||
|
||||
#### Scenario: Founder image upload requires alt text
|
||||
|
||||
- **WHEN** an admin uploads a founder image without alt text
|
||||
- **THEN** validation MUST fail with a pt-BR error message
|
||||
- **AND** alt text MUST remain optional when no founder image is present
|
||||
|
||||
#### Scenario: Assistant cannot access site settings
|
||||
|
||||
- **WHEN** an assistant navigates to site settings in Filament
|
||||
- **THEN** access MUST be denied with HTTP 403
|
||||
|
||||
#### Scenario: Default OG image requires alt text
|
||||
|
||||
- **WHEN** an admin uploads a default OG image without alt text
|
||||
- **THEN** validation MUST fail with a pt-BR error message
|
||||
- **AND** alt text MUST remain optional when no default OG image is present
|
||||
|
||||
#### Scenario: Logo upload requires alt text
|
||||
|
||||
- **WHEN** an admin uploads a brand logo without alt text
|
||||
- **THEN** validation MUST fail with a pt-BR error message
|
||||
- **AND** alt text MUST remain optional when no logo is uploaded
|
||||
|
||||
#### Scenario: Singleton avoids generic key-value store
|
||||
|
||||
- **WHEN** site settings are stored
|
||||
- **THEN** the system MUST use typed columns on `site_settings`
|
||||
- **AND** MUST NOT introduce a generic key/value configuration table
|
||||
|
||||
#### Scenario: Editorial defaults remain available when optional fields are empty
|
||||
|
||||
- **GIVEN** manifesto, method steps or principles fields are empty
|
||||
- **WHEN** the home is rendered
|
||||
- **THEN** the page MUST still render those sections using safe editorial defaults
|
||||
- **AND** MUST NOT error
|
||||
8
openspec/changes/remodel-about-page/tasks.md
Normal file
8
openspec/changes/remodel-about-page/tasks.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Tasks: remodel-about-page
|
||||
|
||||
- [x] 1. Migration + SiteSetting fillable/PHPDoc para `founder_image_path` / `founder_image_alt`
|
||||
- [x] 2. Filament ManageSiteSettings: labels hero Sobre + upload Michele; media variants + seeders
|
||||
- [x] 3. AboutContent DTO + GetAboutContent; PageController injeta query
|
||||
- [x] 4. Componentes `x-about.*` + reescrever `pages/about.blade.php`
|
||||
- [x] 5. Feature tests About + atualizar ImmersivePhotoHero / Media / Motion / PublicPages / Filament alt
|
||||
- [ ] 6. PR verde
|
||||
16
resources/views/components/about/cta.blade.php
Normal file
16
resources/views/components/about/cta.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
@props([
|
||||
'settings',
|
||||
])
|
||||
|
||||
<section aria-labelledby="about-cta-heading" class="border-b border-amare-border bg-amare-bg-deep py-20 md:py-24" data-about-cta data-reveal-group>
|
||||
<div class="container-amare max-w-4xl space-y-6" data-reveal data-reveal-from="up">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||
<h2 id="about-cta-heading" class="text-headline font-medium text-amare-text">Vamos criar algo inesquecível juntos?</h2>
|
||||
<p class="max-w-2xl text-amare-muted">Conte com a Amare para transformar seu evento em uma experiência organizada, cuidadosa e memorável.</p>
|
||||
<div>
|
||||
<a href="{{ route('briefing') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
40
resources/views/components/about/founder.blade.php
Normal file
40
resources/views/components/about/founder.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
@props([
|
||||
'settings',
|
||||
])
|
||||
|
||||
@php
|
||||
$hasImage = filled($settings->founder_image_path);
|
||||
@endphp
|
||||
|
||||
<section id="michele" aria-labelledby="founder-heading" class="border-b border-amare-border bg-amare-bg" data-about-founder>
|
||||
<div class="grid min-h-[500px] lg:grid-cols-12" data-reveal-group>
|
||||
@if ($hasImage)
|
||||
<div class="min-h-[420px] overflow-hidden bg-amare-bg-deep max-lg:aspect-[4/5] lg:col-span-6 lg:min-h-full" data-reveal-media>
|
||||
<x-media.image
|
||||
:path="$settings->founder_image_path"
|
||||
:alt="$settings->founder_image_alt ?: 'Michele, da Amare'"
|
||||
sizes="(max-width: 1023px) 100vw, 50vw"
|
||||
class="img-editorial h-full min-h-[420px] w-full object-cover object-[center_35%] lg:min-h-full"
|
||||
/>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex min-h-[420px] items-center justify-center bg-amare-bg-deep lg:col-span-6 lg:min-h-full" data-founder-tonal>
|
||||
<p class="px-4 text-center text-xs font-semibold uppercase tracking-[0.16em] text-amare-muted">Retrato da Michele</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-col justify-center px-6 py-16 lg:col-span-6 lg:px-[clamp(3rem,6vw,7rem)]" data-reveal data-reveal-from="up">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A pessoa por trás da experiência</p>
|
||||
<h2 id="founder-heading" class="mt-3 text-headline font-medium text-amare-text">Conheça Michele</h2>
|
||||
<p class="mt-5 max-w-[520px] text-lg leading-relaxed text-amare-text">Michele representa o olhar humano da Amare: escuta, organização e presença durante todo o processo de construção do evento.</p>
|
||||
<p class="mt-3.5 max-w-[520px] text-lg leading-relaxed text-amare-text-muted">Seu papel é transformar necessidades e escolhas em um planejamento claro, cuidando dos detalhes sem perder de vista a experiência de quem contrata e de quem participa.</p>
|
||||
<div class="mt-8 flex max-w-[500px] flex-col items-start justify-between gap-4 border border-amare-border bg-amare-bg-deep px-5 py-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<strong class="block text-[2rem] font-normal italic text-amare-text">Michele</strong>
|
||||
<small class="text-xs font-semibold uppercase tracking-[0.08em] text-amare-muted">Assessoria e produção de eventos</small>
|
||||
</div>
|
||||
<a href="{{ route('briefing') }}" class="btn btn-outline shrink-0 text-xs font-bold uppercase tracking-[0.11em]">Falar com a Michele</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
50
resources/views/components/about/hero.blade.php
Normal file
50
resources/views/components/about/hero.blade.php
Normal file
@@ -0,0 +1,50 @@
|
||||
@props([
|
||||
'settings',
|
||||
])
|
||||
|
||||
@php
|
||||
$lead = $settings->about_summary ?: 'Eventos que refletem propósito, conectam pessoas e criam memórias.';
|
||||
$hasImage = filled($settings->about_image_path);
|
||||
@endphp
|
||||
|
||||
<section aria-labelledby="about-hero-heading" class="border-b border-amare-border bg-amare-bg" data-about-hero data-motion="page-open">
|
||||
@if ($hasImage)
|
||||
<div class="grid min-h-[440px] lg:grid-cols-12">
|
||||
<div class="flex min-w-0 items-center bg-amare-bg px-6 py-16 lg:col-span-5 lg:px-[clamp(3rem,6vw,7rem)]" data-reveal-group>
|
||||
<div class="w-full max-w-[470px] space-y-6">
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||
<h1 id="about-hero-heading" class="max-w-[470px] whitespace-normal break-normal text-hero-spread font-medium text-amare-text" data-motion-beat="title">Sobre a Amare</h1>
|
||||
<p class="max-w-[450px] text-[clamp(1.25rem,2vw,1.55rem)] italic leading-snug text-amare-text-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||
<span class="block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||
<p class="max-w-[430px] text-lg leading-relaxed text-amare-text">A Amare une sensibilidade, organização e atenção aos detalhes para criar experiências bem planejadas, do primeiro encontro à execução do evento.</p>
|
||||
<div data-motion-beat="cta">
|
||||
<a href="#michele" class="btn btn-outline text-xs font-bold uppercase tracking-[0.11em]">Conhecer mais</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-h-[360px] overflow-hidden bg-amare-bg-deep max-lg:aspect-[4/5] lg:col-span-7 lg:min-h-0" data-motion-beat="media" data-reveal-media>
|
||||
<x-media.image
|
||||
:path="$settings->about_image_path"
|
||||
:alt="$settings->about_image_alt ?: 'Sobre a Amare'"
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
sizes="(max-width: 1023px) 100vw, 58vw"
|
||||
class="img-editorial h-full min-h-[360px] w-full object-cover lg:min-h-[440px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="container-amare py-16 md:py-24" data-tonal-hero>
|
||||
<div class="max-w-[470px] space-y-5" data-motion-beat="heading">
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||
<h1 id="about-hero-heading" class="text-headline font-medium tracking-tight text-amare-text" data-motion-beat="title">Sobre a Amare</h1>
|
||||
<p class="text-lg italic leading-snug text-amare-text-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||
<span class="block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||
<p class="text-lg leading-relaxed text-amare-text">A Amare une sensibilidade, organização e atenção aos detalhes para criar experiências bem planejadas, do primeiro encontro à execução do evento.</p>
|
||||
<div data-motion-beat="cta">
|
||||
<a href="#michele" class="btn btn-outline text-xs font-bold uppercase tracking-[0.11em]">Conhecer mais</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
31
resources/views/components/about/pillars.blade.php
Normal file
31
resources/views/components/about/pillars.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<section aria-label="Pilares da Amare" class="border-b border-amare-border bg-amare-bg" data-about-pillars>
|
||||
<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">
|
||||
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<circle cx="18" cy="16" r="6"/>
|
||||
<circle cx="30" cy="16" r="6"/>
|
||||
<path d="M7 37c1-8 5-12 11-12s10 4 11 12M21 37c1-8 5-12 11-12 5 0 9 4 10 12"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Atendimento próximo</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Escuta atenta para compreender prioridades, contexto e expectativas de cada evento.</p>
|
||||
</article>
|
||||
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<rect x="10" y="7" width="28" height="34" rx="2"/>
|
||||
<path d="M17 15h14M17 22h14M17 29h8M29 29h2"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Planejamento minucioso</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Organização clara das etapas para transformar decisões em uma execução consistente.</p>
|
||||
</article>
|
||||
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<path d="M24 40C10 32 8 20 8 12c8 0 14 4 16 11 2-7 8-11 16-11 0 8-2 20-16 28Z"/>
|
||||
<path d="M24 23v17"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Execução tranquila</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Cuidado com a operação para que anfitriões e convidados possam viver o momento.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
64
resources/views/components/about/portfolio.blade.php
Normal file
64
resources/views/components/about/portfolio.blade.php
Normal file
@@ -0,0 +1,64 @@
|
||||
@props([
|
||||
'cases',
|
||||
])
|
||||
|
||||
@php
|
||||
$slots = collect([
|
||||
['label' => 'Eventos sociais', 'tall' => true],
|
||||
['label' => 'Celebrações', 'tall' => false],
|
||||
['label' => 'Detalhes', 'tall' => false],
|
||||
['label' => 'Sociais', 'tall' => false],
|
||||
['label' => 'Corporativo', 'tall' => false],
|
||||
['label' => 'Produção', 'tall' => false],
|
||||
]);
|
||||
$cases = $cases->take(6)->values();
|
||||
@endphp
|
||||
|
||||
<section id="portfolio" aria-labelledby="about-portfolio-heading" class="border-b border-amare-border bg-amare-bg" data-about-portfolio>
|
||||
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||
<div class="grid gap-10 lg:grid-cols-12 lg:items-end" data-reveal data-reveal-from="up">
|
||||
<div class="lg:col-span-5">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Seleção de trabalhos</p>
|
||||
<h2 id="about-portfolio-heading" class="mt-3 text-headline font-medium text-amare-text">Portfólio em destaque</h2>
|
||||
</div>
|
||||
<div class="lg:col-span-6 lg:col-start-7">
|
||||
<p class="max-w-xl text-amare-text-muted">Uma composição de eventos sociais e corporativos para apresentar a versatilidade da Amare.</p>
|
||||
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost mt-5 min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||
<span class="border-b border-amare-accent pb-1">Ver portfólio completo</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 grid grid-cols-1 gap-[14px] sm:grid-cols-2 lg:grid-cols-[1.2fr_0.8fr_0.8fr]" aria-label="Projetos em destaque" data-editorial-portfolio data-reveal-group>
|
||||
@foreach ($slots as $index => $slot)
|
||||
@php
|
||||
$case = $cases->get($index);
|
||||
@endphp
|
||||
<div
|
||||
@class([
|
||||
'overflow-hidden bg-amare-bg-deep',
|
||||
'sm:col-span-2 sm:min-h-[420px] lg:col-span-1 lg:row-span-2 lg:min-h-[634px]' => $slot['tall'],
|
||||
'min-h-[220px] sm:min-h-[310px]' => ! $slot['tall'],
|
||||
])
|
||||
data-reveal
|
||||
data-reveal-from="up"
|
||||
>
|
||||
@if ($case && filled($case->cover_image_path))
|
||||
<a href="{{ route('portfolio.show', $case) }}" class="block h-full w-full" aria-label="{{ $case->title }}">
|
||||
<x-media.image
|
||||
:path="$case->cover_image_path"
|
||||
:alt="$case->cover_image_alt ?: $case->title"
|
||||
sizes="(max-width: 1023px) 100vw, 33vw"
|
||||
class="img-editorial h-full w-full object-cover transition-opacity hover:opacity-90"
|
||||
/>
|
||||
</a>
|
||||
@else
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<p class="px-4 text-center text-xs font-semibold uppercase tracking-[0.16em] text-amare-muted">{{ $slot['label'] }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
30
resources/views/components/about/process.blade.php
Normal file
30
resources/views/components/about/process.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<section aria-labelledby="process-heading" class="border-b border-amare-border bg-amare-bg" data-about-process>
|
||||
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||
<h2 id="process-heading" class="mb-10 text-center text-headline font-medium text-amare-text" data-reveal data-reveal-from="up">Como trabalhamos</h2>
|
||||
<div class="grid border-l border-t border-amare-border md:grid-cols-3">
|
||||
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<path d="M15 29c-5-4-6-12-2-17 5-7 17-7 22 0 4 6 2 14-4 18-2 1-3 4-3 7H20c0-4-2-6-5-8Z"/>
|
||||
<path d="M20 41h8"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Escuta e entendimento</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Começamos pelo contexto do evento, suas prioridades, estilo e expectativas.</p>
|
||||
</article>
|
||||
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<rect x="8" y="11" width="32" height="29" rx="2"/>
|
||||
<path d="M15 7v8M33 7v8M8 20h32M15 26h4M23 26h4M31 26h3M15 33h4M23 33h4"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Planejamento e curadoria</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Organizamos etapas, fornecedores e decisões para manter cada frente alinhada.</p>
|
||||
</article>
|
||||
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<path d="M24 39S8 30 8 18c0-6 4-10 10-10 4 0 7 2 9 6 2-4 5-6 9-6 6 0 10 4 10 10 0 12-22 21-22 21Z"/>
|
||||
</svg>
|
||||
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Execução e experiência</h3>
|
||||
<p class="mx-auto max-w-[280px] text-amare-text-muted">Coordenamos a operação para que o evento aconteça com fluidez e presença.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,29 +1,10 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$principles = filled($siteSettings->principles)
|
||||
? $siteSettings->principles
|
||||
: \App\Models\SiteSetting::defaultPrinciples();
|
||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
||||
@endphp
|
||||
|
||||
<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">
|
||||
<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>
|
||||
</x-public.photo-hero>
|
||||
|
||||
<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>
|
||||
@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">
|
||||
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||
<span>{{ $principle }}</span>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-home.final-cta :settings="$siteSettings" />
|
||||
<x-about.hero :settings="$content->settings" />
|
||||
<x-about.pillars />
|
||||
<x-about.founder :settings="$content->settings" />
|
||||
<x-about.process />
|
||||
<x-about.portfolio :cases="$content->featuredCases" />
|
||||
<x-about.cta :settings="$content->settings" />
|
||||
@endsection
|
||||
|
||||
@@ -47,8 +47,7 @@
|
||||
:show-intro="false"
|
||||
kicker="tag"
|
||||
:show-subtitle="true"
|
||||
cta-route="contact"
|
||||
:band-cta-href="route('contact')"
|
||||
:band-cta-href="route('briefing')"
|
||||
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"
|
||||
/>
|
||||
|
||||
145
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
145
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?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 AboutPageContentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_about_page_renders_mock_section_order_without_principles_list(): void
|
||||
{
|
||||
SiteSetting::instance()->update([
|
||||
'about_summary' => 'Eventos que refletem propósito, conectam pessoas e criam memórias.',
|
||||
'principles' => ['Princípio que não deve aparecer no sobre'],
|
||||
]);
|
||||
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee('data-motion="page-open"', false)
|
||||
->assertSee('Sobre nós')
|
||||
->assertSee('Sobre a Amare')
|
||||
->assertSee('Eventos que refletem propósito, conectam pessoas e criam memórias.')
|
||||
->assertSee('Atendimento próximo')
|
||||
->assertSee('Planejamento minucioso')
|
||||
->assertSee('Execução tranquila')
|
||||
->assertSee('id="michele"', false)
|
||||
->assertSee('Conheça Michele')
|
||||
->assertSee('Como trabalhamos')
|
||||
->assertSee('Escuta e entendimento')
|
||||
->assertSee('Portfólio em destaque')
|
||||
->assertSee('Vamos criar algo inesquecível juntos?')
|
||||
->assertDontSee('Princípio que não deve aparecer no sobre')
|
||||
->assertDontSee('Personalização sem complicação desnecessária');
|
||||
}
|
||||
|
||||
public function test_about_layout_matches_site_editorial_rhythm(): void
|
||||
{
|
||||
SiteSetting::instance()->update([
|
||||
'about_image_path' => 'content/heroes/about.jpg',
|
||||
'about_image_alt' => 'Estúdio Amare',
|
||||
]);
|
||||
|
||||
$html = $this->get(route('about'))->assertOk()->getContent();
|
||||
|
||||
$this->assertMatchesRegularExpression(
|
||||
'/data-about-hero[\s\S]*?lg:grid-cols-12[\s\S]*?lg:col-span-5[\s\S]*?text-hero-spread[\s\S]*?lg:col-span-7/',
|
||||
$html,
|
||||
);
|
||||
$this->assertStringNotContainsString('data-photo-hero', $html);
|
||||
$this->assertDoesNotMatchRegularExpression(
|
||||
'/data-about-hero[\s\S]*?min-h-\[calc\(100dvh-5rem\)\]/',
|
||||
$html,
|
||||
);
|
||||
|
||||
$this->assertMatchesRegularExpression('/data-about-pillars[\s\S]*?py-16 md:py-24/', $html);
|
||||
$this->assertMatchesRegularExpression('/data-about-process[\s\S]*?py-16 md:py-24/', $html);
|
||||
$this->assertMatchesRegularExpression('/data-about-portfolio[\s\S]*?data-editorial-portfolio/', $html);
|
||||
$this->assertDoesNotMatchRegularExpression('/data-about-portfolio[\s\S]*?rounded-full/', $html);
|
||||
$this->assertMatchesRegularExpression('/<(?:section)[^>]*(?:data-about-cta[^>]*bg-amare-bg-deep|bg-amare-bg-deep[^>]*data-about-cta)/', $html);
|
||||
$this->assertStringContainsString('Próximo passo', $html);
|
||||
}
|
||||
|
||||
public function test_about_renders_founder_image_from_cms_when_configured(): void
|
||||
{
|
||||
SiteSetting::instance()->update([
|
||||
'founder_image_path' => 'content/about/founder/michele.jpg',
|
||||
'founder_image_alt' => 'Michele, da Amare',
|
||||
]);
|
||||
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee('content/about/founder/michele.jpg', false)
|
||||
->assertSee('Michele, da Amare')
|
||||
->assertSee('data-about-founder', false);
|
||||
}
|
||||
|
||||
public function test_about_omits_founder_image_request_when_unset(): void
|
||||
{
|
||||
SiteSetting::instance()->update([
|
||||
'founder_image_path' => null,
|
||||
'founder_image_alt' => null,
|
||||
]);
|
||||
|
||||
$html = $this->get(route('about'))->assertOk()->getContent();
|
||||
|
||||
$this->assertStringContainsString('data-about-founder', $html);
|
||||
$this->assertStringNotContainsString('content/about/founder/', $html);
|
||||
$this->assertStringContainsString('data-founder-tonal', $html);
|
||||
}
|
||||
|
||||
public function test_about_portfolio_links_featured_cases(): void
|
||||
{
|
||||
$case = PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Casamento Ana e Lucas',
|
||||
'slug' => 'casamento-ana-lucas',
|
||||
'is_featured' => true,
|
||||
'cover_image_path' => 'content/cases/ana-lucas.jpg',
|
||||
'cover_image_alt' => 'Cerimônia ao ar livre',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Caso não destacado',
|
||||
'slug' => 'nao-destacado',
|
||||
'is_featured' => false,
|
||||
'cover_image_path' => 'content/cases/other.jpg',
|
||||
'sort_order' => 2,
|
||||
]);
|
||||
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee(route('portfolio.show', $case), false)
|
||||
->assertSee('Cerimônia ao ar livre')
|
||||
->assertDontSee(route('portfolio.show', 'nao-destacado'), false);
|
||||
}
|
||||
|
||||
public function test_founder_image_upload_requires_alt_text(): void
|
||||
{
|
||||
Storage::fake('public');
|
||||
$this->actingAs(User::factory()->admin()->create());
|
||||
|
||||
Livewire::test(ManageSiteSettings::class)
|
||||
->set('data.founder_image_path', [UploadedFile::fake()->create('michele.jpg', 100, 'image/jpeg')])
|
||||
->set('data.founder_image_alt', null)
|
||||
->call('save')
|
||||
->assertHasFormErrors(['founder_image_alt' => 'required']);
|
||||
|
||||
Livewire::test(ManageSiteSettings::class)
|
||||
->set('data.founder_image_path', null)
|
||||
->set('data.founder_image_alt', null)
|
||||
->call('save')
|
||||
->assertHasNoFormErrors();
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,14 @@ class ImmersivePhotoHeroTest extends TestCase
|
||||
->assertDontSee('data-split-hero', false)
|
||||
->assertDontSee('content/heroes/home.jpg', false);
|
||||
}
|
||||
|
||||
if ($route === route('about')) {
|
||||
$response
|
||||
->assertSee('data-motion="page-open"', false)
|
||||
->assertSee('data-about-hero', false)
|
||||
->assertDontSee('data-photo-hero', false)
|
||||
->assertDontSee('min-h-[calc(100dvh-5rem)]', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +107,7 @@ class ImmersivePhotoHeroTest extends TestCase
|
||||
'cover_image_alt' => 'Cerimônia ao ar livre',
|
||||
]);
|
||||
|
||||
foreach ([route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug), route('about')] as $route) {
|
||||
foreach ([route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug)] as $route) {
|
||||
$this->get($route)
|
||||
->assertOk()
|
||||
->assertSee('data-photo-hero', false)
|
||||
@@ -107,6 +115,15 @@ class ImmersivePhotoHeroTest extends TestCase
|
||||
->assertSee('fetchpriority="high"', false)
|
||||
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false);
|
||||
}
|
||||
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee('data-about-hero', false)
|
||||
->assertSee('content/heroes/about.jpg', false)
|
||||
->assertSee('loading="eager"', false)
|
||||
->assertSee('fetchpriority="high"', false)
|
||||
->assertDontSee('data-photo-hero', false)
|
||||
->assertDontSee('data-tonal-hero', false);
|
||||
}
|
||||
|
||||
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
||||
|
||||
@@ -196,9 +196,11 @@ class PublicPagesTest extends TestCase
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee('Sobre a Amare boutique')
|
||||
->assertSee('São Paulo - SP')
|
||||
->assertSee('Sobre a Amare')
|
||||
->assertSee('Conheça Michele')
|
||||
->assertDontSee('Fortaleza')
|
||||
->assertSee('data-tonal-hero', false);
|
||||
->assertSee('data-tonal-hero', false)
|
||||
->assertSee('data-about-hero', false);
|
||||
|
||||
$this->get(route('privacy'))
|
||||
->assertOk()
|
||||
|
||||
53
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
53
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\PublicSite;
|
||||
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\WeddingPackage;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ServicesPageCtaTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_package_cta_uses_whatsapp_when_a_number_is_configured(): void
|
||||
{
|
||||
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
|
||||
|
||||
WeddingPackage::factory()->published()->create([
|
||||
'name' => 'Grand Jour',
|
||||
'cta_label' => 'Quero conhecer a Grand Jour',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('services.index'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee(
|
||||
'https://wa.me/5511988887777?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade Grand Jour para meu casamento.'),
|
||||
false,
|
||||
)
|
||||
->assertSee('target="_blank"', false)
|
||||
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
|
||||
}
|
||||
|
||||
public function test_package_cta_falls_back_to_briefing_without_whatsapp_number(): void
|
||||
{
|
||||
SiteSetting::instance()->update(['whatsapp_number' => null]);
|
||||
|
||||
WeddingPackage::factory()->published()->create([
|
||||
'name' => 'Essenza',
|
||||
'cta_label' => 'Quero conhecer a Essenza',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('services.index'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
|
||||
->assertDontSee('wa.me/', false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user