Compare commits
3 Commits
feat/remod
...
6cc4691094
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cc4691094 | |||
| e2af1f6a23 | |||
| 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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Application\Queries\Marketing;
|
namespace App\Application\Queries\Marketing;
|
||||||
|
|
||||||
|
use App\Domain\Marketing\PortfolioVertical;
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
final class GetPublishedPortfolioCases
|
final class GetPublishedPortfolioCases
|
||||||
@@ -12,12 +15,37 @@ final class GetPublishedPortfolioCases
|
|||||||
/**
|
/**
|
||||||
* @return Collection<int, PortfolioCase>
|
* @return Collection<int, PortfolioCase>
|
||||||
*/
|
*/
|
||||||
public function __invoke(): Collection
|
public function __invoke(?PortfolioVertical $vertical = null): Collection
|
||||||
{
|
{
|
||||||
return PortfolioCase::query()
|
return $this->baseQuery($vertical)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return LengthAwarePaginator<int, PortfolioCase>
|
||||||
|
*/
|
||||||
|
public function paginate(?PortfolioVertical $vertical = null, int $perPage = 9): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
return $this->baseQuery($vertical)->paginate($perPage)->withQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<PortfolioCase>
|
||||||
|
*/
|
||||||
|
private function baseQuery(?PortfolioVertical $vertical): Builder
|
||||||
|
{
|
||||||
|
$query = PortfolioCase::query()
|
||||||
->published()
|
->published()
|
||||||
->with(['images'])
|
->with(['images'])
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order');
|
||||||
->get();
|
|
||||||
|
if ($vertical !== null) {
|
||||||
|
$query->where(function (Builder $builder) use ($vertical): void {
|
||||||
|
foreach ($vertical->eventTypePatterns() as $pattern) {
|
||||||
|
$builder->orWhere('event_type', 'ilike', $pattern);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ 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->founder_image_path)) {
|
||||||
|
$paths[] = (string) $settings->founder_image_path;
|
||||||
|
}
|
||||||
if ($settings && filled($settings->hero_image_path)) {
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
$paths[] = (string) $settings->hero_image_path;
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
}
|
}
|
||||||
|
|||||||
45
app/Domain/Marketing/PortfolioVertical.php
Normal file
45
app/Domain/Marketing/PortfolioVertical.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Marketing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public portfolio verticals used to separate Casamentos and Corporate proof.
|
||||||
|
*
|
||||||
|
* Matching is intentionally tolerant of free-text `event_type` values already
|
||||||
|
* stored in the CMS (e.g. "Casamento", "Mini wedding", "Corporativo").
|
||||||
|
*/
|
||||||
|
enum PortfolioVertical: string
|
||||||
|
{
|
||||||
|
case Casamentos = 'casamentos';
|
||||||
|
case Corporate = 'corporate';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Casamentos => 'Casamentos',
|
||||||
|
self::Corporate => 'Corporate',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public function eventTypePatterns(): array
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::Casamentos => ['%casamento%', '%wedding%', '%social%'],
|
||||||
|
self::Corporate => ['%corporat%', '%empresa%', '%business%'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tryFromQuery(?string $value): ?self
|
||||||
|
{
|
||||||
|
if ($value === null || $value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::tryFrom($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -268,8 +268,10 @@ class ManageSiteSettings extends Page
|
|||||||
->columns(2),
|
->columns(2),
|
||||||
Section::make('Página Sobre')
|
Section::make('Página Sobre')
|
||||||
->schema([
|
->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::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),
|
->columns(2),
|
||||||
Section::make('SEO padrão')
|
Section::make('SEO padrão')
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers\PublicSite;
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
use App\Application\Data\PageMeta;
|
use App\Application\Data\PageMeta;
|
||||||
|
use App\Application\Queries\Marketing\GetAboutContent;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
|
||||||
final class PageController extends Controller
|
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', [
|
return view('pages.about', [
|
||||||
|
'content' => $content,
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('about'),
|
canonical: route('about'),
|
||||||
|
|||||||
@@ -6,33 +6,35 @@ namespace App\Http\Controllers\PublicSite;
|
|||||||
|
|
||||||
use App\Application\Data\PageMeta;
|
use App\Application\Data\PageMeta;
|
||||||
use App\Application\Queries\Marketing\FindPublishedPortfolioCaseBySlug;
|
use App\Application\Queries\Marketing\FindPublishedPortfolioCaseBySlug;
|
||||||
|
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
||||||
|
use App\Domain\Marketing\PortfolioVertical;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\PortfolioCase;
|
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Pagination\LengthAwarePaginator;
|
|
||||||
|
|
||||||
final class PortfolioController extends Controller
|
final class PortfolioController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function index(Request $request, GetPublishedPortfolioCases $getPublishedPortfolioCases): View
|
||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
|
$vertical = PortfolioVertical::tryFromQuery($request->query('vertente'));
|
||||||
/** @var LengthAwarePaginator<int, PortfolioCase> $cases */
|
$cases = $getPublishedPortfolioCases->paginate($vertical);
|
||||||
$cases = PortfolioCase::query()
|
|
||||||
->published()
|
|
||||||
->with(['images'])
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->paginate(9);
|
|
||||||
|
|
||||||
return view('pages.portfolio.index', [
|
return view('pages.portfolio.index', [
|
||||||
'cases' => $cases,
|
'cases' => $cases,
|
||||||
|
'vertical' => $vertical,
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('portfolio.index'),
|
canonical: route('portfolio.index', array_filter([
|
||||||
|
'vertente' => $vertical?->value,
|
||||||
|
])),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: PageMeta::withBrandSuffix('Portfólio', $settings),
|
title: PageMeta::withBrandSuffix(
|
||||||
|
$vertical === null ? 'Portfólio' : 'Portfólio — '.$vertical->label(),
|
||||||
|
$settings,
|
||||||
|
),
|
||||||
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ 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 $founder_image_path
|
||||||
|
* @property string|null $founder_image_alt
|
||||||
* @property string|null $hero_image_path
|
* @property string|null $hero_image_path
|
||||||
* @property string|null $hero_image_alt
|
* @property string|null $hero_image_alt
|
||||||
* @property string|null $services_hero_image_path
|
* @property string|null $services_hero_image_path
|
||||||
@@ -47,6 +49,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'about_summary',
|
'about_summary',
|
||||||
'about_image_path',
|
'about_image_path',
|
||||||
'about_image_alt',
|
'about_image_alt',
|
||||||
|
'founder_image_path',
|
||||||
|
'founder_image_alt',
|
||||||
'manifesto_title',
|
'manifesto_title',
|
||||||
'manifesto_lead',
|
'manifesto_lead',
|
||||||
'manifesto_body',
|
'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_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',
|
||||||
|
'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_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_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.',
|
'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_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',
|
||||||
|
'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_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_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.',
|
'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
|
||||||
@@ -134,7 +134,8 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
[id$='-heading'] {
|
[id$='-heading'],
|
||||||
|
.home-chapter[id] {
|
||||||
scroll-margin-top: 5.5rem;
|
scroll-margin-top: 5.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
resources/views/components/about/cta.blade.php
Normal file
22
resources/views/components/about/cta.blade.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
<section aria-labelledby="about-cta-heading" class="border-b border-amare-border bg-amare-bg py-6 md:py-7" data-about-cta>
|
||||||
|
<div class="container-amare grid items-center gap-7 md:grid-cols-[auto_minmax(0,1fr)_auto]" data-reveal-group>
|
||||||
|
<svg class="mx-auto h-11 w-11 text-amare-accent md:mx-0" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true" data-reveal data-reveal-from="up">
|
||||||
|
<rect x="7" y="12" width="34" height="26" rx="2"/>
|
||||||
|
<path d="m8 15 16 13 16-13"/>
|
||||||
|
<path d="M18 12v-2a6 6 0 0 1 12 0v2"/>
|
||||||
|
</svg>
|
||||||
|
<div class="text-center md:text-left" data-reveal data-reveal-from="up">
|
||||||
|
<h2 id="about-cta-heading" class="text-[clamp(1.75rem,3vw,2rem)] font-medium text-amare-accent-deep">Vamos criar algo inesquecível juntos?</h2>
|
||||||
|
<p class="mt-1 text-base leading-relaxed text-amare-text">Conte com a Amare para transformar seu evento em uma experiência organizada, cuidadosa e memorável.</p>
|
||||||
|
</div>
|
||||||
|
<div class="justify-self-center md:justify-self-end" data-reveal data-reveal-from="up">
|
||||||
|
<a href="{{ route('briefing') }}" class="btn btn-primary text-xs font-bold uppercase tracking-[0.11em]">
|
||||||
|
{{ $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-2" data-reveal-group>
|
||||||
|
@if ($hasImage)
|
||||||
|
<div class="min-h-[420px] overflow-hidden bg-amare-bg-deep 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: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-12 md:px-12 md:py-16 lg:px-[70px]" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent">A pessoa por trás da experiência</p>
|
||||||
|
<h2 id="founder-heading" class="mt-3 text-[clamp(2rem,3vw,2.55rem)] font-medium text-amare-accent-deep">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">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-5 flex max-w-[500px] flex-col items-start justify-between gap-4 bg-amare-bg-deep px-4 py-3.5 sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<strong class="block text-[2rem] font-normal italic text-amare-accent-deep">Michele</strong>
|
||||||
|
<small class="text-[0.65rem] 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>
|
||||||
48
resources/views/components/about/hero.blade.php
Normal file
48
resources/views/components/about/hero.blade.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@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="container-amare grid min-h-[440px] lg:grid-cols-[minmax(0,0.85fr)_minmax(0,1.45fr)]">
|
||||||
|
<div class="flex flex-col items-start justify-center py-12 md:py-14 lg:py-16 lg:pr-10" data-reveal-group>
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||||
|
<h1 id="about-hero-heading" class="mt-3 max-w-[470px] text-[clamp(3rem,5vw,5rem)] font-medium leading-[0.96] tracking-tight text-amare-accent-deep" data-motion-beat="title">Sobre a Amare</h1>
|
||||||
|
<p class="mt-4 max-w-[450px] font-serif text-[clamp(1.25rem,2vw,1.55rem)] italic leading-snug text-amare-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||||
|
<span class="my-6 block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||||
|
<p class="mb-7 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 class="min-h-[360px] overflow-hidden bg-amare-bg-deep lg:min-h-full" 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.16em] 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-accent-deep" data-motion-beat="title">Sobre a Amare</h1>
|
||||||
|
<p class="text-lg italic leading-snug text-amare-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" data-reveal-group>
|
||||||
|
<div class="grid md:grid-cols-3">
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto mb-2.5 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-accent-deep">Atendimento próximo</h3>
|
||||||
|
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Escuta atenta para compreender prioridades, contexto e expectativas de cada evento.</p>
|
||||||
|
</article>
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-[30px] md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto mb-2.5 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-accent-deep">Planejamento minucioso</h3>
|
||||||
|
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Organização clara das etapas para transformar decisões em uma execução consistente.</p>
|
||||||
|
</article>
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-[30px] md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto mb-2.5 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-accent-deep">Execução tranquila</h3>
|
||||||
|
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Cuidado com a operação para que anfitriões e convidados possam viver o momento.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
59
resources/views/components/about/portfolio.blade.php
Normal file
59
resources/views/components/about/portfolio.blade.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
@props([
|
||||||
|
'cases',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$slots = [
|
||||||
|
'Eventos sociais',
|
||||||
|
'Celebrações',
|
||||||
|
'Detalhes',
|
||||||
|
'Sociais',
|
||||||
|
'Corporativo',
|
||||||
|
'Produção',
|
||||||
|
];
|
||||||
|
$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 grid lg:grid-cols-[315px_minmax(0,1fr)]" data-reveal-group>
|
||||||
|
<div class="flex flex-col justify-center px-6 py-11 md:px-10" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent">Seleção de trabalhos</p>
|
||||||
|
<h2 id="about-portfolio-heading" class="mt-3 text-[clamp(1.75rem,3vw,2.05rem)] font-medium leading-tight text-amare-accent-deep">Portfólio em destaque</h2>
|
||||||
|
<p class="mt-4 max-w-sm text-base leading-relaxed text-amare-text">Uma composição de eventos sociais e corporativos para apresentar a versatilidade da Amare.</p>
|
||||||
|
<a href="{{ route('portfolio.index') }}" class="btn btn-outline mt-5 self-start text-xs font-bold uppercase tracking-[0.11em]">Ver portfólio completo</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid auto-rows-[150px] grid-cols-2 gap-1 py-1 lg:auto-rows-[118px] lg:grid-cols-12" aria-label="Projetos em destaque" data-reveal-group>
|
||||||
|
@foreach ($slots as $index => $label)
|
||||||
|
@php
|
||||||
|
$case = $cases->get($index);
|
||||||
|
$span = match ($index) {
|
||||||
|
0 => 'lg:col-span-5',
|
||||||
|
1 => 'lg:col-span-4',
|
||||||
|
2 => 'lg:col-span-3 lg:row-span-2 min-h-[190px] lg:min-h-0',
|
||||||
|
3 => 'lg:col-span-4',
|
||||||
|
4 => 'lg:col-span-3',
|
||||||
|
default => 'lg:col-span-2',
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
|
<div @class(['relative min-h-[150px] overflow-hidden bg-amare-bg-deep', $span]) 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) 50vw, 25vw"
|
||||||
|
class="img-editorial h-full w-full object-cover transition-transform duration-[450ms] ease-out hover:scale-[1.03]"
|
||||||
|
/>
|
||||||
|
<span class="absolute bottom-3 left-3 bg-amare-accent-deep/90 px-3 py-1.5 text-[0.57rem] font-semibold uppercase tracking-[0.06em] text-amare-accent-text">{{ $label }}</span>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="flex h-full w-full items-end justify-start p-3">
|
||||||
|
<span class="bg-amare-accent-deep/90 px-3 py-1.5 text-[0.57rem] font-semibold uppercase tracking-[0.06em] text-amare-accent-text">{{ $label }}</span>
|
||||||
|
</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 py-5 md:py-6" data-about-process>
|
||||||
|
<div class="container-amare" data-reveal-group>
|
||||||
|
<h2 id="process-heading" class="mb-3 text-center text-[clamp(1.75rem,3vw,2rem)] font-medium text-amare-accent-deep" data-reveal data-reveal-from="up">Como trabalhamos</h2>
|
||||||
|
<div class="grid md:grid-cols-3">
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Escuta e entendimento</h3>
|
||||||
|
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Começamos pelo contexto do evento, suas prioridades, estilo e expectativas.</p>
|
||||||
|
</article>
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-3 md:before:left-0 md:before:w-px md:before:bg-amare-border" 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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Planejamento e curadoria</h3>
|
||||||
|
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Organizamos etapas, fornecedores e decisões para manter cada frente alinhada.</p>
|
||||||
|
</article>
|
||||||
|
<article class="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-3 md:before:left-0 md:before:w-px md:before:bg-amare-border" 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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Execução e experiência</h3>
|
||||||
|
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Coordenamos a operação para que o evento aconteça com fluidez e presença.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -58,8 +58,8 @@
|
|||||||
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 backdrop-blur-sm">
|
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 backdrop-blur-sm">
|
||||||
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr] md:h-[74px] md:py-0">
|
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr] md:h-[74px] md:py-0">
|
||||||
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||||
<a href="{{ route('home') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Início</a>
|
<a href="{{ url('/#casamentos') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Casamentos</a>
|
||||||
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Serviços</a>
|
<a href="{{ url('/#corporate') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Corporate</a>
|
||||||
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Portfólio</a>
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Portfólio</a>
|
||||||
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Amare</a>
|
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Amare</a>
|
||||||
<a href="{{ route('briefing') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
<a href="{{ route('briefing') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
||||||
@@ -73,7 +73,6 @@
|
|||||||
<a href="{{ route('briefing') }}" class="btn btn-outline hidden min-h-0 px-6 text-xs font-bold uppercase tracking-[0.09em] md:inline-flex">
|
<a href="{{ route('briefing') }}" class="btn btn-outline hidden min-h-0 px-6 text-xs font-bold uppercase tracking-[0.09em] md:inline-flex">
|
||||||
Conte seu evento
|
Conte seu evento
|
||||||
</a>
|
</a>
|
||||||
</a>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -111,29 +110,39 @@
|
|||||||
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="container-amare flex flex-col gap-8 py-14 md:flex-row md:items-end md:justify-between md:py-[58px]">
|
<div class="container-amare flex flex-col gap-10 py-14 md:py-[58px]">
|
||||||
<div class="space-y-3">
|
<div class="flex flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
||||||
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
<div class="space-y-3">
|
||||||
<p class="text-sm text-amare-muted">Assessoria & produção de eventos • São Paulo</p>
|
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
||||||
|
<p class="text-sm text-amare-muted">Boutique de assessoria & produção • São Paulo</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm text-amare-muted md:text-right">
|
||||||
|
@if ($footerSocials->isEmpty())
|
||||||
|
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
||||||
|
@else
|
||||||
|
@foreach ($footerSocials as $network => $url)
|
||||||
|
<a href="{{ $url }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ $footerSocialLabel((string) $network) }}</a>@unless ($loop->last) · @endunless
|
||||||
|
@endforeach
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-amare-muted md:text-right">
|
<nav aria-label="Rodapé" class="flex flex-col gap-4 border-t border-amare-border pt-8 text-sm text-amare-muted md:flex-row md:flex-wrap md:items-center md:gap-x-6 md:gap-y-2">
|
||||||
@if ($footerSocials->isEmpty())
|
<a href="{{ url('/#casamentos') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Casamentos</a>
|
||||||
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
<a href="{{ url('/#corporate') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Corporate</a>
|
||||||
@else
|
<a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Serviços</a>
|
||||||
@foreach ($footerSocials as $network => $url)
|
<a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Portfólio</a>
|
||||||
<a href="{{ $url }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ $footerSocialLabel((string) $network) }}</a>@unless ($loop->last) · @endunless
|
<a href="{{ route('about') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Sobre</a>
|
||||||
@endforeach
|
<a href="{{ route('briefing') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Briefing</a>
|
||||||
@endif
|
<a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Parcerias</a>
|
||||||
</p>
|
<a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Privacidade</a>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-amare-border">
|
<div class="border-t border-amare-border">
|
||||||
<div class="container-amare flex flex-col gap-2 py-6 text-sm text-amare-muted md:flex-row md:items-center md:justify-between">
|
<div class="container-amare flex flex-col gap-2 py-6 text-sm text-amare-muted md:flex-row md:items-center md:justify-between">
|
||||||
<p>© {{ now()->year }} {{ $siteSettings->brand_name }}. Todos os direitos reservados.</p>
|
<p>© {{ now()->year }} {{ $siteSettings->brand_name }}. Todos os direitos reservados.</p>
|
||||||
<p>
|
|
||||||
<a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Política de privacidade</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,29 +1,10 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
<x-about.hero :settings="$content->settings" />
|
||||||
$principles = filled($siteSettings->principles)
|
<x-about.pillars />
|
||||||
? $siteSettings->principles
|
<x-about.founder :settings="$content->settings" />
|
||||||
: \App\Models\SiteSetting::defaultPrinciples();
|
<x-about.process />
|
||||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
<x-about.portfolio :cases="$content->featuredCases" />
|
||||||
@endphp
|
<x-about.cta :settings="$content->settings" />
|
||||||
|
|
||||||
<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" />
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -5,9 +5,38 @@
|
|||||||
|
|
||||||
<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 space-y-10 py-16 md:py-24">
|
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||||
|
<nav aria-label="Vertentes do portfólio" class="flex flex-wrap gap-x-6 gap-y-2 border-b border-amare-border pb-6 text-sm">
|
||||||
|
<a
|
||||||
|
href="{{ route('portfolio.index') }}"
|
||||||
|
@class([
|
||||||
|
'inline-flex min-h-11 items-center uppercase tracking-[0.08em] transition-colors',
|
||||||
|
'font-semibold text-amare-accent' => $vertical === null,
|
||||||
|
'text-amare-muted hover:text-amare-accent' => $vertical !== null,
|
||||||
|
])
|
||||||
|
>Todos</a>
|
||||||
|
@foreach (\App\Domain\Marketing\PortfolioVertical::cases() as $option)
|
||||||
|
<a
|
||||||
|
href="{{ route('portfolio.index', ['vertente' => $option->value]) }}"
|
||||||
|
@class([
|
||||||
|
'inline-flex min-h-11 items-center uppercase tracking-[0.08em] transition-colors',
|
||||||
|
'font-semibold text-amare-accent' => $vertical === $option,
|
||||||
|
'text-amare-muted hover:text-amare-accent' => $vertical !== $option,
|
||||||
|
])
|
||||||
|
>{{ $option->label() }}</a>
|
||||||
|
@endforeach
|
||||||
|
</nav>
|
||||||
|
|
||||||
@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>
|
@if ($vertical === \App\Domain\Marketing\PortfolioVertical::Corporate)
|
||||||
|
<div class="flex min-h-[280px] flex-col items-center justify-center gap-4 bg-amare-bg p-10 text-center">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Portfólio Corporate</p>
|
||||||
|
<h2 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Conteúdo em construção</h2>
|
||||||
|
<p class="max-w-lg text-amare-text-muted">Ainda não publicamos cases corporativos autorizados. Em vez de inventar prova, mantemos este espaço pronto para projetos reais.</p>
|
||||||
|
<a href="{{ route('briefing') }}" class="btn btn-outline mt-2 text-xs font-bold uppercase tracking-[0.09em]">Falar sobre um evento corporativo</a>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<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>
|
||||||
|
@endif
|
||||||
@else
|
@else
|
||||||
@php
|
@php
|
||||||
$hasPrioritizedImage = false;
|
$hasPrioritizedImage = false;
|
||||||
@@ -53,6 +82,9 @@
|
|||||||
@endphp
|
@endphp
|
||||||
@endif
|
@endif
|
||||||
<div class="space-y-2 border-t border-amare-border pt-4">
|
<div class="space-y-2 border-t border-amare-border pt-4">
|
||||||
|
@if (filled($case->event_type))
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $case->event_type }}</p>
|
||||||
|
@endif
|
||||||
<h2 class="text-2xl font-medium text-amare-text">
|
<h2 class="text-2xl font-medium text-amare-text">
|
||||||
<a href="{{ route('portfolio.show', $case->slug) }}" class="transition-colors hover:text-amare-accent">{{ $case->title }}</a>
|
<a href="{{ route('portfolio.show', $case->slug) }}" class="transition-colors hover:text-amare-accent">{{ $case->title }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -47,8 +47,7 @@
|
|||||||
:show-intro="false"
|
:show-intro="false"
|
||||||
kicker="tag"
|
kicker="tag"
|
||||||
:show-subtitle="true"
|
:show-subtitle="true"
|
||||||
cta-route="contact"
|
:band-cta-href="route('briefing')"
|
||||||
: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."
|
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"
|
:show-note="false"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Feature\Application\Queries\Marketing;
|
namespace Tests\Feature\Application\Queries\Marketing;
|
||||||
|
|
||||||
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
||||||
|
use App\Domain\Marketing\PortfolioVertical;
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -58,4 +59,26 @@ class GetPublishedPortfolioCasesTest extends TestCase
|
|||||||
$this->assertTrue($cases->get(0)?->relationLoaded('images'));
|
$this->assertTrue($cases->get(0)?->relationLoaded('images'));
|
||||||
$this->assertCount(2, $cases->get(0)?->images ?? []);
|
$this->assertCount(2, $cases->get(0)?->images ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_filters_published_cases_by_portfolio_vertical(): void
|
||||||
|
{
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Wedding Case',
|
||||||
|
'event_type' => 'Mini wedding',
|
||||||
|
'sort_order' => 10,
|
||||||
|
]);
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Corporate Case',
|
||||||
|
'event_type' => 'Evento corporativo',
|
||||||
|
'sort_order' => 20,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$weddings = (new GetPublishedPortfolioCases)(PortfolioVertical::Casamentos);
|
||||||
|
$corporate = (new GetPublishedPortfolioCases)(PortfolioVertical::Corporate);
|
||||||
|
|
||||||
|
$this->assertCount(1, $weddings);
|
||||||
|
$this->assertSame('Wedding Case', $weddings->first()?->title);
|
||||||
|
$this->assertCount(1, $corporate);
|
||||||
|
$this->assertSame('Corporate Case', $corporate->first()?->title);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
118
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?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_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('data-split-hero', false)
|
||||||
->assertDontSee('content/heroes/home.jpg', 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',
|
'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)
|
$this->get($route)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('data-photo-hero', false)
|
->assertSee('data-photo-hero', false)
|
||||||
@@ -107,6 +115,15 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
->assertSee('fetchpriority="high"', false)
|
->assertSee('fetchpriority="high"', false)
|
||||||
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', 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
|
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
||||||
|
|||||||
60
tests/Feature/PublicSite/PortfolioVerticalFilterTest.php
Normal file
60
tests/Feature/PublicSite/PortfolioVerticalFilterTest.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use App\Models\PortfolioCase;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PortfolioVerticalFilterTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_portfolio_index_filters_by_vertical_and_shows_corporate_empty_state(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Casamento Jardim',
|
||||||
|
'slug' => 'casamento-jardim',
|
||||||
|
'event_type' => 'Casamento',
|
||||||
|
'sort_order' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Convenção Anual',
|
||||||
|
'slug' => 'convencao-anual',
|
||||||
|
'event_type' => 'Corporativo',
|
||||||
|
'sort_order' => 20,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get(route('portfolio.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Casamento Jardim')
|
||||||
|
->assertSee('Convenção Anual')
|
||||||
|
->assertSee('Vertentes do portfólio')
|
||||||
|
->assertSee('Casamentos')
|
||||||
|
->assertSee('Corporate');
|
||||||
|
|
||||||
|
$this->get(route('portfolio.index', ['vertente' => 'casamentos']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Casamento Jardim')
|
||||||
|
->assertDontSee('Convenção Anual');
|
||||||
|
|
||||||
|
$this->get(route('portfolio.index', ['vertente' => 'corporate']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Convenção Anual')
|
||||||
|
->assertDontSee('Casamento Jardim');
|
||||||
|
|
||||||
|
PortfolioCase::query()->where('slug', 'convencao-anual')->update(['published_at' => null]);
|
||||||
|
|
||||||
|
$this->get(route('portfolio.index', ['vertente' => 'corporate']))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Portfólio Corporate')
|
||||||
|
->assertSee('Conteúdo em construção')
|
||||||
|
->assertDontSee('Convenção Anual');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/Feature/PublicSite/PublicNavigationVerticalsTest.php
Normal file
43
tests/Feature/PublicSite/PublicNavigationVerticalsTest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PublicNavigationVerticalsTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_header_and_footer_expose_casamentos_and_corporate_without_implying_weddings_only(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$response = $this->get(route('about'))->assertOk();
|
||||||
|
|
||||||
|
$homeCasamentos = url('/#casamentos');
|
||||||
|
$homeCorporate = url('/#corporate');
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertSee('id="main-nav"', false)
|
||||||
|
->assertSee('href="'.$homeCasamentos.'"', false)
|
||||||
|
->assertSee('>Casamentos</a>', false)
|
||||||
|
->assertSee('href="'.$homeCorporate.'"', false)
|
||||||
|
->assertSee('>Corporate</a>', false)
|
||||||
|
->assertSee('href="'.route('portfolio.index').'"', false)
|
||||||
|
->assertSee('href="'.route('about').'"', false)
|
||||||
|
->assertSee('href="'.route('services.index').'"', false)
|
||||||
|
->assertSee('href="'.route('privacy').'"', false)
|
||||||
|
->assertSee('aria-label="Rodapé"', false)
|
||||||
|
->assertSeeInOrder([
|
||||||
|
'aria-label="Rodapé"',
|
||||||
|
'href="'.$homeCasamentos.'"',
|
||||||
|
'Casamentos',
|
||||||
|
'href="'.$homeCorporate.'"',
|
||||||
|
'Corporate',
|
||||||
|
], false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -196,9 +196,11 @@ class PublicPagesTest extends TestCase
|
|||||||
$this->get(route('about'))
|
$this->get(route('about'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Sobre a Amare boutique')
|
->assertSee('Sobre a Amare boutique')
|
||||||
->assertSee('São Paulo - SP')
|
->assertSee('Sobre a Amare')
|
||||||
|
->assertSee('Conheça Michele')
|
||||||
->assertDontSee('Fortaleza')
|
->assertDontSee('Fortaleza')
|
||||||
->assertSee('data-tonal-hero', false);
|
->assertSee('data-tonal-hero', false)
|
||||||
|
->assertSee('data-about-hero', false);
|
||||||
|
|
||||||
$this->get(route('privacy'))
|
$this->get(route('privacy'))
|
||||||
->assertOk()
|
->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