Compare commits
2 Commits
fix/seed-t
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a5c55d90f | |||
| 9c43ad1d65 |
@@ -10,6 +10,8 @@ use BackedEnum;
|
|||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
use Filament\Forms\Components\KeyValue;
|
use Filament\Forms\Components\KeyValue;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
@@ -135,6 +137,8 @@ class ManageSiteSettings extends Page
|
|||||||
->label('Nome da marca')
|
->label('Nome da marca')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
PublicImageUploadRules::fileUpload('logo_path', 'Logo da marca', 'content/logo'),
|
||||||
|
PublicImageUploadRules::altTextField('logo_alt', 'logo_path', 'Texto alternativo do logo'),
|
||||||
TextInput::make('hero_eyebrow')
|
TextInput::make('hero_eyebrow')
|
||||||
->label('Eyebrow do hero')
|
->label('Eyebrow do hero')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
@@ -147,14 +151,62 @@ class ManageSiteSettings extends Page
|
|||||||
->required()
|
->required()
|
||||||
->rows(3),
|
->rows(3),
|
||||||
TextInput::make('hero_cta_label')
|
TextInput::make('hero_cta_label')
|
||||||
->label('Texto do CTA')
|
->label('Texto do CTA principal')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('hero_secondary_cta_label')
|
||||||
|
->label('Texto do CTA secundário')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('hero_note')
|
||||||
|
->label('Nota do hero')
|
||||||
|
->rows(2),
|
||||||
Textarea::make('about_summary')
|
Textarea::make('about_summary')
|
||||||
->label('Resumo institucional')
|
->label('Resumo institucional')
|
||||||
->rows(3),
|
->rows(3),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
|
Section::make('Manifesto editorial')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('manifesto_title')
|
||||||
|
->label('Título do manifesto')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('manifesto_lead')
|
||||||
|
->label('Lead do manifesto')
|
||||||
|
->rows(3),
|
||||||
|
Textarea::make('manifesto_body')
|
||||||
|
->label('Corpo do manifesto')
|
||||||
|
->rows(4),
|
||||||
|
]),
|
||||||
|
Section::make('Método')
|
||||||
|
->schema([
|
||||||
|
Textarea::make('method_intro')
|
||||||
|
->label('Introdução do método')
|
||||||
|
->rows(2),
|
||||||
|
Repeater::make('method_steps')
|
||||||
|
->label('Passos do método')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('title')
|
||||||
|
->label('Título')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('body')
|
||||||
|
->label('Descrição')
|
||||||
|
->required()
|
||||||
|
->rows(2),
|
||||||
|
])
|
||||||
|
->defaultItems(0)
|
||||||
|
->maxItems(4)
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
Section::make('Princípios')
|
||||||
|
->schema([
|
||||||
|
TagsInput::make('principles')
|
||||||
|
->label('Princípios')
|
||||||
|
->placeholder('Adicionar princípio')
|
||||||
|
->helperText('Até quatro princípios editoriais.')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
Section::make('Contato')
|
Section::make('Contato')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('email')
|
TextInput::make('email')
|
||||||
|
|||||||
@@ -11,17 +11,31 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @property array<string, string|null> $social_links
|
* @property array<string, string|null> $social_links
|
||||||
|
* @property list<array{title?: string, body?: string}>|null $method_steps
|
||||||
|
* @property list<string>|null $principles
|
||||||
* @property bool $analytics_enabled
|
* @property bool $analytics_enabled
|
||||||
* @property string|null $default_og_image_path
|
* @property string|null $default_og_image_path
|
||||||
* @property string|null $default_og_image_alt
|
* @property string|null $default_og_image_alt
|
||||||
|
* @property string|null $logo_path
|
||||||
|
* @property string|null $logo_alt
|
||||||
*/
|
*/
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'brand_name',
|
'brand_name',
|
||||||
|
'logo_path',
|
||||||
|
'logo_alt',
|
||||||
'hero_eyebrow',
|
'hero_eyebrow',
|
||||||
'hero_title',
|
'hero_title',
|
||||||
'hero_subtitle',
|
'hero_subtitle',
|
||||||
'hero_cta_label',
|
'hero_cta_label',
|
||||||
|
'hero_secondary_cta_label',
|
||||||
|
'hero_note',
|
||||||
'about_summary',
|
'about_summary',
|
||||||
|
'manifesto_title',
|
||||||
|
'manifesto_lead',
|
||||||
|
'manifesto_body',
|
||||||
|
'method_intro',
|
||||||
|
'method_steps',
|
||||||
|
'principles',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
'city',
|
'city',
|
||||||
@@ -40,21 +54,67 @@ class SiteSetting extends Model
|
|||||||
{
|
{
|
||||||
return static::query()->firstOrCreate([], [
|
return static::query()->firstOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique em Fortaleza.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique em São Paulo - SP.',
|
||||||
'city' => 'Fortaleza, CE',
|
'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.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => self::defaultMethodSteps(),
|
||||||
|
'principles' => self::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [],
|
'social_links' => [],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{title: string, body: string}>
|
||||||
|
*/
|
||||||
|
public static function defaultMethodSteps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Escuta',
|
||||||
|
'body' => 'Entendimento do contexto, das prioridades, do público e do que o evento precisa comunicar.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Direção',
|
||||||
|
'body' => 'Definição de escopo, próximos passos, responsabilidades e critérios para orientar decisões.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Produção',
|
||||||
|
'body' => 'Coordenação de cronograma, fornecedores, detalhes, alinhamentos e contingências.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Execução',
|
||||||
|
'body' => 'Presença atenta no evento para que o planejado aconteça com ritmo, cuidado e segurança.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function defaultPrinciples(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Personalização sem complicação desnecessária',
|
||||||
|
'Comunicação clara e decisões bem orientadas',
|
||||||
|
'Atenção à experiência de clientes e convidados',
|
||||||
|
'Execução responsável do início ao fim',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string|class-string>
|
* @return array<string, string|class-string>
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +122,8 @@ class SiteSetting extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'social_links' => 'array',
|
'social_links' => 'array',
|
||||||
|
'method_steps' => 'array',
|
||||||
|
'principles' => 'array',
|
||||||
'analytics_enabled' => 'boolean',
|
'analytics_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class PortfolioCaseFactory extends Factory
|
|||||||
'slug' => str($title)->slug()->toString(),
|
'slug' => str($title)->slug()->toString(),
|
||||||
'summary' => fake()->sentence(),
|
'summary' => fake()->sentence(),
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => fake()->company(),
|
'venue' => fake()->company(),
|
||||||
'event_date' => fake()->date(),
|
'event_date' => fake()->date(),
|
||||||
'challenge' => fake()->paragraph(),
|
'challenge' => fake()->paragraph(),
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?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('logo_path')->nullable()->after('brand_name');
|
||||||
|
$table->string('logo_alt')->nullable()->after('logo_path');
|
||||||
|
$table->string('hero_secondary_cta_label')->nullable()->after('hero_cta_label');
|
||||||
|
$table->text('hero_note')->nullable()->after('hero_secondary_cta_label');
|
||||||
|
$table->string('manifesto_title')->nullable()->after('about_summary');
|
||||||
|
$table->text('manifesto_lead')->nullable()->after('manifesto_title');
|
||||||
|
$table->text('manifesto_body')->nullable()->after('manifesto_lead');
|
||||||
|
$table->text('method_intro')->nullable()->after('manifesto_body');
|
||||||
|
$table->jsonb('method_steps')->nullable()->after('method_intro');
|
||||||
|
$table->jsonb('principles')->nullable()->after('method_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn([
|
||||||
|
'logo_path',
|
||||||
|
'logo_alt',
|
||||||
|
'hero_secondary_cta_label',
|
||||||
|
'hero_note',
|
||||||
|
'manifesto_title',
|
||||||
|
'manifesto_lead',
|
||||||
|
'manifesto_body',
|
||||||
|
'method_intro',
|
||||||
|
'method_steps',
|
||||||
|
'principles',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -30,19 +30,27 @@ class ContentSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
SiteSetting::query()->updateOrCreate([], [
|
SiteSetting::query()->updateOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em Fortaleza.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em São Paulo.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'city' => 'Fortaleza, CE',
|
'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.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [
|
'social_links' => [
|
||||||
'instagram' => 'https://instagram.com/amare',
|
'instagram' => 'https://instagram.com/amare',
|
||||||
],
|
],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'content/og/og-default.jpg'),
|
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'content/og/og-default.jpg'),
|
||||||
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
@@ -98,9 +106,9 @@ class ContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Casamento Ana e Lucas',
|
'title' => 'Casamento Ana e Lucas',
|
||||||
'slug' => 'casamento-ana-lucas',
|
'slug' => 'casamento-ana-lucas',
|
||||||
'summary' => 'Cerimônia ao ar livre em Fortaleza.',
|
'summary' => 'Cerimônia ao ar livre em São Paulo.',
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Espaço Jardim Atlântico',
|
'venue' => 'Espaço Jardim Atlântico',
|
||||||
'event_date' => '2025-11-20',
|
'event_date' => '2025-11-20',
|
||||||
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
||||||
@@ -113,7 +121,7 @@ class ContentSeeder extends Seeder
|
|||||||
'slug' => 'lancamento-verano',
|
'slug' => 'lancamento-verano',
|
||||||
'summary' => 'Evento corporativo de lançamento de coleção.',
|
'summary' => 'Evento corporativo de lançamento de coleção.',
|
||||||
'event_type' => 'Corporativo',
|
'event_type' => 'Corporativo',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Centro de Convenções',
|
'venue' => 'Centro de Convenções',
|
||||||
'event_date' => '2025-09-10',
|
'event_date' => '2025-09-10',
|
||||||
'challenge' => 'Ativar marca em ambiente multiestação.',
|
'challenge' => 'Ativar marca em ambiente multiestação.',
|
||||||
@@ -124,10 +132,10 @@ class ContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Mini wedding Marina',
|
'title' => 'Mini wedding Marina',
|
||||||
'slug' => 'mini-wedding-marina',
|
'slug' => 'mini-wedding-marina',
|
||||||
'summary' => 'Celebração intimista à beira-mar.',
|
'summary' => 'Celebração intimista em São Paulo.',
|
||||||
'event_type' => 'Mini wedding',
|
'event_type' => 'Mini wedding',
|
||||||
'city' => 'Caucaia',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Pousada da Praia',
|
'venue' => 'Espaço intimista',
|
||||||
'event_date' => '2025-06-02',
|
'event_date' => '2025-06-02',
|
||||||
'challenge' => 'Clima e logística em área externa.',
|
'challenge' => 'Clima e logística em área externa.',
|
||||||
'solution' => 'Plano B estruturado e fornecedores locais alinhados.',
|
'solution' => 'Plano B estruturado e fornecedores locais alinhados.',
|
||||||
@@ -164,40 +172,59 @@ class ContentSeeder extends Seeder
|
|||||||
|
|
||||||
private function seedTestimonials(): void
|
private function seedTestimonials(): void
|
||||||
{
|
{
|
||||||
|
// Real couples from depoimentos.md. Production publication still requires
|
||||||
|
// explicit couple authorization before setting published_at outside local/demo seeds.
|
||||||
$testimonials = [
|
$testimonials = [
|
||||||
[
|
[
|
||||||
'quote' => 'A Amare transformou nosso casamento em uma experiência inesquecível.',
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
'author_name' => 'Ana Souza',
|
'author_name' => 'Jeniffer e Maick',
|
||||||
'context' => 'Noiva',
|
'context' => 'Casamento · 06/12/2025',
|
||||||
'sort_order' => 1,
|
'sort_order' => 1,
|
||||||
'is_featured' => true,
|
'is_featured' => true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'quote' => 'Profissionalismo do início ao fim no lançamento da nossa coleção.',
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
'author_name' => 'Marcos Lima',
|
'author_name' => 'Quesia e Jhonata',
|
||||||
'context' => 'Diretor de marketing',
|
'context' => 'Casamento · 21/12/2025',
|
||||||
'sort_order' => 2,
|
'sort_order' => 2,
|
||||||
'is_featured' => true,
|
'is_featured' => true,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'quote' => 'Cuidaram de cada detalhe com sensibilidade e precisão.',
|
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||||
'author_name' => 'Marina Costa',
|
'author_name' => 'Milena e Weslley',
|
||||||
'context' => 'Anfitriã',
|
'context' => 'Casamento · 13/02/2026',
|
||||||
'sort_order' => 3,
|
'sort_order' => 3,
|
||||||
'is_featured' => false,
|
'is_featured' => false,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||||
|
'author_name' => 'Raquel e Pedro',
|
||||||
|
'context' => 'Casamento · 09/05/2026',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||||
|
'author_name' => 'Victoria e Pedro',
|
||||||
|
'context' => 'Casamento · 24/06/2026',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$keepAuthors = array_column($testimonials, 'author_name');
|
||||||
|
|
||||||
|
Testimonial::query()
|
||||||
|
->whereNotIn('author_name', $keepAuthors)
|
||||||
|
->delete();
|
||||||
|
|
||||||
foreach ($testimonials as $testimonial) {
|
foreach ($testimonials as $testimonial) {
|
||||||
Testimonial::query()->updateOrCreate(
|
Testimonial::query()->updateOrCreate(
|
||||||
[
|
['author_name' => $testimonial['author_name']],
|
||||||
'author_name' => $testimonial['author_name'],
|
|
||||||
'quote' => $testimonial['quote'],
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
...$testimonial,
|
...$testimonial,
|
||||||
'photo_path' => $this->copyFixture('testimonial.jpg', 'content/testimonials/'.str($testimonial['author_name'])->slug().'.jpg'),
|
'photo_path' => null,
|
||||||
'photo_alt' => 'Foto de '.$testimonial['author_name'],
|
'photo_alt' => null,
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -37,19 +37,27 @@ class VisualContentSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
SiteSetting::query()->updateOrCreate([], [
|
SiteSetting::query()->updateOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em Fortaleza.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em São Paulo.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'city' => 'Fortaleza, CE',
|
'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.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [
|
'social_links' => [
|
||||||
'instagram' => 'https://instagram.com/amare',
|
'instagram' => 'https://instagram.com/amare',
|
||||||
],
|
],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'visual/og/og-default.jpg'),
|
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'visual/og/og-default.jpg'),
|
||||||
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
@@ -95,9 +103,9 @@ class VisualContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Casamento Ana e Lucas',
|
'title' => 'Casamento Ana e Lucas',
|
||||||
'slug' => 'casamento-ana-lucas',
|
'slug' => 'casamento-ana-lucas',
|
||||||
'summary' => 'Cerimônia ao ar livre em Fortaleza.',
|
'summary' => 'Cerimônia ao ar livre em São Paulo.',
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Espaço Jardim Atlântico',
|
'venue' => 'Espaço Jardim Atlântico',
|
||||||
'event_date' => '2025-11-20',
|
'event_date' => '2025-11-20',
|
||||||
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
||||||
@@ -110,7 +118,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
'slug' => 'lancamento-verano',
|
'slug' => 'lancamento-verano',
|
||||||
'summary' => 'Evento corporativo de lançamento de coleção.',
|
'summary' => 'Evento corporativo de lançamento de coleção.',
|
||||||
'event_type' => 'Corporativo',
|
'event_type' => 'Corporativo',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Centro de Convenções',
|
'venue' => 'Centro de Convenções',
|
||||||
'event_date' => '2025-09-10',
|
'event_date' => '2025-09-10',
|
||||||
'challenge' => 'Ativar marca em ambiente multiestação.',
|
'challenge' => 'Ativar marca em ambiente multiestação.',
|
||||||
@@ -146,20 +154,36 @@ class VisualContentSeeder extends Seeder
|
|||||||
|
|
||||||
private function seedTestimonials(): void
|
private function seedTestimonials(): void
|
||||||
{
|
{
|
||||||
Testimonial::query()->updateOrCreate(
|
Testimonial::query()->whereNotIn('author_name', [
|
||||||
|
'Jeniffer e Maick',
|
||||||
|
'Quesia e Jhonata',
|
||||||
|
])->delete();
|
||||||
|
|
||||||
|
foreach ([
|
||||||
[
|
[
|
||||||
'author_name' => 'Ana Souza',
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
'quote' => 'A Amare transformou nosso casamento em uma experiência inesquecível.',
|
'author_name' => 'Jeniffer e Maick',
|
||||||
],
|
'context' => 'Casamento · 06/12/2025',
|
||||||
[
|
|
||||||
'context' => 'Noiva',
|
|
||||||
'sort_order' => 1,
|
'sort_order' => 1,
|
||||||
'is_featured' => true,
|
|
||||||
'photo_path' => $this->copyFixture('testimonial.jpg', 'visual/testimonials/ana-souza.jpg'),
|
|
||||||
'photo_alt' => 'Foto de Ana Souza',
|
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
|
||||||
],
|
],
|
||||||
);
|
[
|
||||||
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
],
|
||||||
|
] as $testimonial) {
|
||||||
|
Testimonial::query()->updateOrCreate(
|
||||||
|
['author_name' => $testimonial['author_name']],
|
||||||
|
[
|
||||||
|
...$testimonial,
|
||||||
|
'is_featured' => true,
|
||||||
|
'photo_path' => null,
|
||||||
|
'photo_alt' => null,
|
||||||
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function copyFixture(string $fixtureName, string $destination): string
|
private function copyFixture(string $fixtureName, string $destination): string
|
||||||
|
|||||||
30
depoimentos.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.
|
||||||
|
Era nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.
|
||||||
|
Que você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!! ❤️
|
||||||
|
Muito obrigada!
|
||||||
|
Jeniffer e Maick
|
||||||
|
Casamento - 06/12/2025
|
||||||
|
----
|
||||||
|
Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.
|
||||||
|
Obrigada por tudo !
|
||||||
|
Quesia e Jhonata
|
||||||
|
casamento - 21/12/2025
|
||||||
|
----
|
||||||
|
@_amareassessoria
|
||||||
|
Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!! 😍😍
|
||||||
|
Milena e Weslley
|
||||||
|
casamento - 13/02/2026
|
||||||
|
---
|
||||||
|
Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.
|
||||||
|
Desde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.
|
||||||
|
Também adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.
|
||||||
|
Obrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.
|
||||||
|
Raquel e Pedro
|
||||||
|
casamento - 09/05/2026
|
||||||
|
----
|
||||||
|
Miiii, meu amor… você e sua equipe foram impecáveis.
|
||||||
|
Superou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.
|
||||||
|
Sempre muito atenciosa e paciente.
|
||||||
|
Adoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia. ♥️🙏🏻
|
||||||
|
Victoria e Pedro
|
||||||
|
casamento -24/06/2026
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-02
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Notes — recreate-public-frontend
|
||||||
|
|
||||||
|
## Remaining unresolved (do not invent)
|
||||||
|
|
||||||
|
- **Official contacts:** production e-mail is `amareassessoriaeventos@gmail.com`; WhatsApp and Instagram handles still placeholder until the owner confirms.
|
||||||
|
- **Testimonial authorization:** five real couples from `depoimentos.md` are seeded for local/demo/visual; production publish still requires explicit couple authorization before `published_at` goes live.
|
||||||
|
- **Authorized photography:** public images remain fixture/demo with editorial disclosure notes until Amare supplies an authorized portfolio archive.
|
||||||
|
- **WEB-05 briefing form:** `/contato` stays presentation-only (channels + CTA); lead capture is a future change.
|
||||||
|
|
||||||
|
## Adaptation note
|
||||||
|
|
||||||
|
Mockup `amare-home-editorial.html` was single-page with anchors. Implementation keeps Laravel multipage routes; home carries the editorial narrative, internal pages are chapters with the same Heritage Editorial grammar.
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
O site público da Fase 1 já entrega rotas, CMS, SEO, mídia responsiva, regressão visual e acessibilidade. A identidade visual, porém, ainda é o placeholder da fundação: Instrument Sans, acento ouro, raios arredondados e cartões com sombra. `DESIGN.md` (“Heritage Editorial”), o mockup `amare-home-editorial.html`, o logo fornecido (coração facetado) e `depoimentos.md` (cinco casais reais) definem a marca a materializar.
|
||||||
|
|
||||||
|
Restrições que condicionam o desenho:
|
||||||
|
|
||||||
|
- Arquitetura multipágina Laravel/Blade/CMS já aprovada; o mockup HTML é single-page com âncoras — adaptar, não portar literalmente.
|
||||||
|
- Contato permanece placeholder (WEB-05 fora de escopo); CTAs levam a `/contato` sem criar leads.
|
||||||
|
- Fontes self-hosted via Vite (determinismo visual); sem Google Fonts CDN.
|
||||||
|
- Imagens públicas via disco configurado + variantes; Unsplash do mockup não entra no app.
|
||||||
|
- `PRODUCT.md`: São Paulo capital; seeders atuais ainda usam Fortaleza e depoimentos fictícios.
|
||||||
|
- Change paralela `complete-foundation-parity` não bloqueia nem é bloqueada por esta.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Materializar Heritage Editorial em todas as rotas públicas (home, serviços, portfólio, caso, sobre, contato, privacidade, 404, 500).
|
||||||
|
- Home como capa do “Dossiê Editorial do Evento”: hero, manifesto, serviços, portfólio, método (4 passos), depoimentos reais, perfil Amare, CTA final.
|
||||||
|
- Tokens centralizados alinhados a `DESIGN.md`; EB Garamond única família; radius 0; elevação por campos tonais.
|
||||||
|
- Logo oficial otimizado (selo + lockup) em fundos claros/escuros, geometria preservada.
|
||||||
|
- Cinco depoimentos de `depoimentos.md` no CMS/seed, multipárrafo, com nota de autorização.
|
||||||
|
- Manter publicação dinâmica, paginação, eager loading, SEO, axe, teclado, contraste AA e baselines determinísticas.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Briefing funcional / lead (WEB-05), WhatsApp automatizado, inventar provas corporativas.
|
||||||
|
- Redesign do Filament, page builder, single-page navigation como modelo primário.
|
||||||
|
- Fotografia proprietária real (permanece ilustrativa e marcada até acervo autorizado).
|
||||||
|
- Staging/deploy/auth parity (`complete-foundation-parity`).
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1 — Multipágina editorial, não single-page literal
|
||||||
|
|
||||||
|
Preservar rotas do SPEC §5.1. Home concentra a narrativa do mockup; páginas internas herdam a mesma gramática (eyebrow, títulos, linhas 1px, spreads assimétricos no desktop, sequência linear no mobile). Header usa links de rota (não `#âncoras` como navegação primária), com CTA “Solicitar proposta” → `contact`.
|
||||||
|
|
||||||
|
*Alternativas:* home quase idêntica com âncoras (rejeitada: conflita com SEO/CMS/rotas já testadas); portar HTML estático (rejeitada: perde CMS e determinismo).
|
||||||
|
|
||||||
|
### D2 — Tokens Heritage Editorial como única fonte visual
|
||||||
|
|
||||||
|
Reescrever `resources/css/tokens.css` e o mapeamento `@theme` em `app.css`:
|
||||||
|
|
||||||
|
| Papel | Token | Valor |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| Fundo | `--amare-color-bg` | `#FBF9F4` (Papel Marfim) |
|
||||||
|
| Fundo profundo | `--amare-color-bg-deep` | `#F0EEE9` |
|
||||||
|
| Arquivo | `--amare-color-bg-archive` | `#E4E2DD` |
|
||||||
|
| Oliva | `--amare-color-accent` | `#556B2F` |
|
||||||
|
| Oliva profunda | `--amare-color-accent-deep` | `#3E5219` |
|
||||||
|
| Sálvia | `--amare-color-sage` | `#8B9D77` |
|
||||||
|
| Tinta | `--amare-color-text` | `#1B1C19` |
|
||||||
|
| Tinta suave | `--amare-color-muted` | `#5D6155` |
|
||||||
|
| Linha | `--amare-color-border` | `#C5C8B8` |
|
||||||
|
| Radius | `--amare-radius-*` | `0` |
|
||||||
|
| Container | `--amare-container-max` | `1120px` |
|
||||||
|
| Sombra | removida / não usada em conteúdo | — |
|
||||||
|
|
||||||
|
Tipografia: EB Garamond (400/500/600) self-hosted via Vite; escala display/headline/title/body/label conforme `DESIGN.md`. Componentes públicos deixam de usar `rounded-*` e shadows de cartão.
|
||||||
|
|
||||||
|
*Alternativa:* CSS inline do mockup (rejeitada: foge de `design-tokens` e quebra Tailwind/`@theme`).
|
||||||
|
|
||||||
|
### D3 — Composição da home e omissão de seções vazias
|
||||||
|
|
||||||
|
Ordem canônica:
|
||||||
|
|
||||||
|
1. Hero (settings + imagem OG/hero se houver)
|
||||||
|
2. Manifesto (copy de settings)
|
||||||
|
3. Serviços em destaque (lista editorial, não grid de cartões)
|
||||||
|
4. Portfólio em destaque (bloco escuro oliva; funde proof+cases atuais)
|
||||||
|
5. Método (4 passos: Escuta, Direção, Produção, Execução)
|
||||||
|
6. Depoimentos publicados
|
||||||
|
7. Perfil / posicionamento Amare (about + princípios)
|
||||||
|
8. CTA final → `/contato`
|
||||||
|
|
||||||
|
Seções alimentadas por collections (serviços, casos, depoimentos) **omitidas** quando vazias. Manifesto, método, perfil e CTA final permanecem (copy de settings / defaults editoriais). Um único `h1` no hero; demais seções usam `h2`/`h3`.
|
||||||
|
|
||||||
|
### D4 — Contato continua presentation-only
|
||||||
|
|
||||||
|
`/contato` e o CTA da home mostram canais de `site_settings` (e-mail, telefone, cidade, sociais). Nenhum `<form>` funcional, nenhum lead. O mockup de formulário serve só como referência visual futura para WEB-05; nesta change o bloco de contato da home é CTA editorial + link para a página de contato, não formulário embutido.
|
||||||
|
|
||||||
|
### D5 — Extensão mínima tipada de `site_settings`
|
||||||
|
|
||||||
|
Novos campos tipados (não key/value genérico):
|
||||||
|
|
||||||
|
- `logo_path` / `logo_alt` (opcional; fallback para lockup estático em `public/`)
|
||||||
|
- `hero_secondary_cta_label` (opcional)
|
||||||
|
- `hero_note` (texto curto sob CTAs)
|
||||||
|
- `manifesto_title`, `manifesto_lead`, `manifesto_body`
|
||||||
|
- `method_intro` (opcional; passos estruturados em JSON tipado ou colunas `method_step_{1..4}_{title,body}` — preferir JSONB `method_steps` validado no Filament)
|
||||||
|
- `principles` (JSONB lista de até 4 strings) **ou** quatro colunas `principle_1..4`
|
||||||
|
- Manter `about_summary`, hero atual, contato, SEO, analytics
|
||||||
|
|
||||||
|
Filament `ManageSiteSettings` ganha seções editoriais em pt-BR. Defaults no seeder alinhados ao mockup + São Paulo.
|
||||||
|
|
||||||
|
*Alternativa:* hardcode de manifesto/método nas Blade (rejeitada parcialmente: método/princípios podem ter default no view, mas copy institucional deve ser editável como o hero).
|
||||||
|
|
||||||
|
### D6 — Logo: ativo estático + campo CMS opcional
|
||||||
|
|
||||||
|
1. Converter/otimizar o PNG fornecido para WebP/SVG derivados em `public/brand/` (selo coração + lockup completo), com versões para fundo claro (oliva) e fundo escuro (papel/branco).
|
||||||
|
2. Componente `<x-brand.logo>` escolhe variante por contexto (`on-dark` / `on-light`) e expõe `alt` acessível.
|
||||||
|
3. Se `logo_path` em settings estiver preenchido, usa o upload; senão, o estático versionado.
|
||||||
|
|
||||||
|
Não redesenhar o coração facetado; não inventar polígonos decorativos genéricos.
|
||||||
|
|
||||||
|
### D7 — Depoimentos reais multipárrafo
|
||||||
|
|
||||||
|
- Seedar os 5 casais de `depoimentos.md` em `quote` (texto completo com quebras `\n\n`), `author_name`, `context` (ex.: `Casamento · 06/12/2025`), `sort_order`, `is_featured`, `published_at` conforme ambiente.
|
||||||
|
- Blade renderiza parágrafos a partir de quebras de linha; tipografia editorial (aspas, offset).
|
||||||
|
- Nota discreta no markup/admin: autorização final dos casais antes de publicação em produção.
|
||||||
|
- Remover depoimentos fictícios dos seeders de demo/visual ou substituí-los pelos reais (visual seeder usa subset determinístico, tipicamente 2 featured).
|
||||||
|
|
||||||
|
### D8 — Navegação responsiva com JS mínimo
|
||||||
|
|
||||||
|
`resources/js/app.js` ganha toggle de menu mobile (aria-expanded, `menu-open`, fechar ao navegar), espelhando o mockup. Sem framework novo. `prefers-reduced-motion` continua a anular transições não essenciais. Hover de imagem (scale leve) só quando motion permitido.
|
||||||
|
|
||||||
|
### D9 — Páginas internas como capítulos
|
||||||
|
|
||||||
|
| Rota | Tratamento |
|
||||||
|
|------|------------|
|
||||||
|
| `/servicos` | Lista editorial (número + nome + resumo), não cartões |
|
||||||
|
| `/portfolio` | Grade assimétrica / stack com captions; fundo pode usar papel profundo |
|
||||||
|
| `/portfolio/{slug}` | Caderno de caso: metadados, desafio/solução/resultado, galeria |
|
||||||
|
| `/sobre` | Perfil editorial + princípios |
|
||||||
|
| `/contato` | Canais + CTA textual (sem form) |
|
||||||
|
| `/privacidade` | Tipografia editorial sobre papel |
|
||||||
|
| 404/500 | Mesma linguagem; 500 sem internals |
|
||||||
|
|
||||||
|
### D10 — Testes e baselines
|
||||||
|
|
||||||
|
- Atualizar feature tests de home (ordem de seções, omissão, CTA → contact, `data-testid="home-primary-cta"`).
|
||||||
|
- Browser: axe nas rotas cobertas; Tab até CTA; console limpo.
|
||||||
|
- `composer visual:update` após aprovação visual local; timezone permanece `America/Fortaleza` (SPEC); copy de cidade pública passa a São Paulo.
|
||||||
|
- Fotos fixture locais continuam; filtro CSS de saturação contida via classe utilitária, não via URL externa.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Regeneração de 8+ snapshots** → risco de ruído no PR; mitigar com seeder visual estável e revisão humana do diff.
|
||||||
|
- **EB Garamond em forms/UI** → legibilidade de labels uppercase; mitigar com letter-spacing e peso 600 conforme DESIGN.md; validar contraste AA.
|
||||||
|
- **Depoimentos longos** → layout quebra em mobile; mitigar com tipografia responsiva e subset featured na home.
|
||||||
|
- **Autorização de depoimentos** → risco legal/reputacional; mitigar com nota explícita e `published_at` null até autorização.
|
||||||
|
- **Campos novos em site_settings** → migração + Filament; mitigar com defaults e nullable.
|
||||||
|
- **Paridade com mockup single-page** → expectativa visual vs rotas; documentar adaptação multipágina na proposta e no surface brief.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Migrar tokens/fontes/logo estático (sem breaking de rotas).
|
||||||
|
2. Migrar `site_settings` (colunas novas nullable + backfill de defaults).
|
||||||
|
3. Atualizar seeders (SP + depoimentos reais).
|
||||||
|
4. Trocar layout e páginas; manter contratos de testes passando incrementalmente.
|
||||||
|
5. Regenerar baselines com `composer visual:update`.
|
||||||
|
6. Rollback: reverter deploy/commit; migração down remove colunas novas; assets estáticos são aditivos.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Formato exato de `method_steps` / `principles` (JSONB vs colunas) — default recomendado: JSONB validado no Filament.
|
||||||
|
- WhatsApp/e-mail/Instagram oficiais ainda ausentes — settings continuam placeholder até o dono informar.
|
||||||
|
- Subconjunto de depoimentos na home (2 vs 5) — default: featured first, até 2 na home estilo mockup; listagem completa só se houver página dedicada (não há); home mostra todos published featured ou os N primeiros por `sort_order` (cap 2–3 para ritmo editorial).
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
O site público já existe com rotas, CMS e gates de qualidade, mas a identidade visual ainda é o placeholder da Fase 0 (Instrument Sans, ouro, cantos arredondados, cartões genéricos). `DESIGN.md` e o mockup editorial já definem o sistema Heritage Editorial; `depoimentos.md` e o logo fornecido finalmente permitem prova e marca reais. Sem recriar o frontend agora, o critério de saída da Fase 1 (“site público aprovado visualmente”) permanece ligado a uma UI que não representa a marca, e a Fase 2 (WEB-05) herdaria um shell genérico.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Substituir tokens, tipografia e composição do site público pelo sistema **Heritage Editorial** de `DESIGN.md` (EB Garamond, oliva/papel, cantos retos, campos tonais, sem sombras de cartão SaaS).
|
||||||
|
- Adaptar o mockup `amare-home-editorial.html` à arquitetura **multipágina** Laravel/CMS existente: home como capa editorial; serviços, portfólio, caso, sobre, contato, privacidade e erros como capítulos coerentes.
|
||||||
|
- Incorporar o logo fornecido (coração facetado + lockup) como ativo otimizado com variantes para fundos claros/escuros, sem redesenhar a geometria.
|
||||||
|
- Seedar e renderizar os **cinco depoimentos reais** de `depoimentos.md` (texto, casal, data/contexto), com autorização final como requisito de publicação.
|
||||||
|
- Reestruturar a home na narrativa editorial: hero → manifesto → serviços → portfólio → método → depoimentos → perfil Amare → CTA final (WEB-01).
|
||||||
|
- Atualizar layout público (header/footer, navegação responsiva, selo), componentes Blade e páginas internas para a mesma linguagem visual.
|
||||||
|
- Estender `site_settings` o mínimo necessário para copy editorial (manifesto, nota do hero, CTA secundário, passos do método, princípios) mantendo singleton tipado (WEB-06).
|
||||||
|
- Regenerar baselines visuais desktop/mobile e manter axe, teclado, landmarks, SEO e publicação dinâmica (SPEC §6.5, §13.5, §13.8).
|
||||||
|
- Atualizar seeders/conteúdo demonstrativo para São Paulo e marcar fotografias ilustrativas até existir acervo autorizado.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
Conforme [SPEC.md §4.2](../../SPEC.md) e decisões desta proposta:
|
||||||
|
|
||||||
|
- Formulário funcional de briefing / criação de lead (WEB-05) — permanece placeholder em `/contato`; change futura `build-lead-capture`.
|
||||||
|
- Integração WhatsApp, portal do cliente, page builder, i18n, PWA.
|
||||||
|
- Inventar cases corporativos, credenciais, números, imprensa ou provas não autorizadas.
|
||||||
|
- Redesign do painel Filament / área interna.
|
||||||
|
- Deploy staging / paridade de fundação — change paralela `complete-foundation-parity`, independente desta.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
<!-- Nenhuma capability nova: a recriação altera requisitos de capabilities já existentes. -->
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `design-tokens`: substituir tokens placeholder (Instrument Sans, ouro, raios, sombras) pelos tokens Heritage Editorial (EB Garamond, oliva/papel, radius 0, elevação tonal).
|
||||||
|
- `public-site-pages`: reestruturar home editorial e páginas públicas para a composição “Dossiê Editorial do Evento”, preservando rotas e publicação.
|
||||||
|
- `site-settings`: campos tipados adicionais para copy editorial da home (manifesto, CTAs, método, princípios, logo).
|
||||||
|
- `testimonials`: suporte a depoimentos multipárrafo reais com contexto/data e regra de autorização antes da publicação.
|
||||||
|
- `content-media`: logo da marca e tratamento editorial de imagens demonstrativas (marcação, filtros contidos) sem inventar acervo.
|
||||||
|
- `visual-regression`: baselines regeneradas sob a nova identidade; determinismo e cobertura de telas mantidos.
|
||||||
|
- `web-accessibility`: preservação/reforço de landmarks, foco, teclado, contraste AA e movimento reduzido após a troca tipográfica/cromática.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Altera**: `resources/css/tokens.css`, `resources/css/app.css`, `vite.config.js`, `resources/views/layouts/public.blade.php`, `resources/views/pages/**`, `resources/views/components/home/**`, `resources/js/app.js`, seeders (`ContentSeeder`, `VisualContentSeeder`), possivelmente migração + model/Filament de `site_settings`, testes Feature/Browser e snapshots em `tests/.pest/snapshots/`.
|
||||||
|
- **Cria**: ativo de logo em `public/` (ou storage CMS), componentes Blade de marca/manifesto/positioning, campos tipados novos em `site_settings` se necessário.
|
||||||
|
- **Depende de**: specs atuais `design-tokens`, `public-site-pages`, `site-settings`, `testimonials`, `content-media`, `visual-regression`, `web-accessibility`, `public-seo`, `service-catalog`, `portfolio-cases`.
|
||||||
|
- **Independente de**: `complete-foundation-parity` (auth/staging/coverage).
|
||||||
|
- **Risco**: regeneração ampla de snapshots; mitigado por seed determinístico, fontes self-hosted e `composer visual:update` com revisão humana. Depoimentos reais exigem confirmação de autorização antes de publicar em produção.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Brand logo assets are available to the public layout
|
||||||
|
|
||||||
|
The system SHALL provide optimized Amare brand logo assets derived from the official faceted-heart lockup for use in the public header, footer and institutional pages. Assets MUST preserve the original geometry, include an accessible text alternative, and provide variants suitable for light and dark tonal fields. When `site_settings.logo_path` is present, the uploaded logo MUST be used; otherwise the versioned static brand asset MUST be used.
|
||||||
|
|
||||||
|
#### Scenario: Header renders brand mark with alt text
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** the brand mark image or equivalent MUST expose accessible alternative text identifying Amare Assessoria
|
||||||
|
|
||||||
|
#### Scenario: Dark portfolio field uses a legible logo variant
|
||||||
|
|
||||||
|
- **WHEN** the brand mark is rendered on an olive-deep or otherwise dark public surface
|
||||||
|
- **THEN** the chosen logo variant MUST remain legible against that background
|
||||||
|
|
||||||
|
#### Scenario: Uploaded logo overrides static fallback
|
||||||
|
|
||||||
|
- **GIVEN** an admin has saved `logo_path` and `logo_alt` in site settings
|
||||||
|
- **WHEN** the public layout renders the brand mark
|
||||||
|
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Decorative saturation/contrast treatment for editorial mood MUST be applied via CSS on self-hosted images and MUST NOT introduce external image CDN dependencies that break deterministic visual tests.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Design tokens are centralized for the public site
|
||||||
|
|
||||||
|
The system SHALL define design tokens in a single source consumed by the public site layout and components. Tokens MUST implement the Heritage Editorial system from `DESIGN.md`: typography family EB Garamond (self-hosted), font scale (display/headline/title/body/label), spacing on an 8px rhythm, border radius `0` for interactive and content surfaces, container max width `1120px`, paper/olive/sage/ink color roles, transition duration/easing, and MUST NOT rely on card shadows as a hierarchy mechanism for regular content.
|
||||||
|
|
||||||
|
#### Scenario: Public layout uses shared Heritage Editorial tokens
|
||||||
|
|
||||||
|
- **WHEN** a public page is rendered
|
||||||
|
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
||||||
|
- **AND** the primary typeface MUST be EB Garamond (or the declared serif fallback stack)
|
||||||
|
- **AND** public content surfaces MUST use `0` border radius from tokens
|
||||||
|
|
||||||
|
#### Scenario: Palette commits paper and olive regions
|
||||||
|
|
||||||
|
- **WHEN** the public site is rendered
|
||||||
|
- **THEN** background regions MUST use paper ivory / paper deep / olive deep tokens rather than pure white card stacks on a white page
|
||||||
|
- **AND** primary interactive emphasis MUST use olive heritage (`#556B2F`) / olive deep (`#3E5219`) tokens
|
||||||
|
|
||||||
|
### Requirement: Public site respects reduced motion preference
|
||||||
|
|
||||||
|
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site, including image hover scales and menu transitions.
|
||||||
|
|
||||||
|
#### Scenario: User prefers reduced motion
|
||||||
|
|
||||||
|
- **WHEN** a visitor has `prefers-reduced-motion: reduce` enabled
|
||||||
|
- **THEN** the public site MUST NOT play non-essential motion effects
|
||||||
|
|
||||||
|
### Requirement: Public site meets baseline accessibility contrast
|
||||||
|
|
||||||
|
The system SHALL use Heritage Editorial color combinations that meet WCAG AA contrast requirements for text and interactive elements. Long-form text MUST use ink on paper; sage MUST NOT replace reading color when contrast would fall below AA.
|
||||||
|
|
||||||
|
#### Scenario: Primary text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary body text is rendered on its background color
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
#### Scenario: Olive on paper interactive text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary buttons or links use olive tokens on paper backgrounds (or paper text on olive)
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Elevation comes from tonal fields not card shadows
|
||||||
|
|
||||||
|
The public site SHALL express hierarchy through tonal paper fields, 1px botanical rules, and editorial overlap. Regular content components MUST NOT use short grey SaaS card shadows.
|
||||||
|
|
||||||
|
#### Scenario: Content cards omit drop shadows
|
||||||
|
|
||||||
|
- **WHEN** home services, testimonials, or portfolio items are rendered
|
||||||
|
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
||||||
|
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
|
||||||
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition (asymmetric spreads on desktop, linear sequence on mobile) rather than rounded card grids.
|
||||||
|
|
||||||
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
|
- **GIVEN** published services, cases and testimonials exist
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
|
||||||
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
|
- **GIVEN** no published services, cases or testimonials
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the response MUST be 200
|
||||||
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
|
- **WHEN** the home is loaded in a real browser at desktop and mobile viewports
|
||||||
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
|
|
||||||
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
|
||||||
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
||||||
|
|
||||||
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
|
- **GIVEN** a published case with multiple gallery images
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the images MUST be rendered ordered by `sort_order`
|
||||||
|
|
||||||
|
#### Scenario: Listings paginate open-ended growth
|
||||||
|
|
||||||
|
- **WHEN** the number of published cases exceeds the page size
|
||||||
|
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
||||||
|
|
||||||
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
|
||||||
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
|
- **WHEN** a visitor requests a non-existent public URL
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
- **AND** the page MUST use the public layout and offer navigation back to the home
|
||||||
|
|
||||||
|
#### Scenario: Server error hides internals in production
|
||||||
|
|
||||||
|
- **GIVEN** `APP_DEBUG` is false
|
||||||
|
- **WHEN** an unhandled exception occurs on a public route
|
||||||
|
- **THEN** the response MUST be a generic branded error page
|
||||||
|
- **AND** MUST NOT contain a stack trace, file path, or environment variable
|
||||||
|
|
||||||
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
|
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads and MUST NOT submit a functional briefing form in this change.
|
||||||
|
|
||||||
|
#### Scenario: Contact page shows configured contact data
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/contato`
|
||||||
|
- **THEN** the e-mail and phone stored in `site_settings` MUST be displayed
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public header exposes brand mark and responsive navigation
|
||||||
|
|
||||||
|
The public layout SHALL render the Amare brand mark (faceted-heart logo lockup or configured logo), primary route navigation, and a contact CTA. On narrow viewports the navigation MUST be operable via a disclosure control with accessible name and `aria-expanded` state.
|
||||||
|
|
||||||
|
#### Scenario: Desktop header shows navigation and CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads any public page at a desktop viewport
|
||||||
|
- **THEN** the header MUST include brand mark, links to home/services/portfolio/about/contact, and a contact CTA
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu toggles accessibly
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the menu button on a narrow viewport
|
||||||
|
- **THEN** the primary navigation MUST become available
|
||||||
|
- **AND** the control MUST expose an updated `aria-expanded` value
|
||||||
|
- **AND** activating a navigation link MUST close the menu
|
||||||
|
|
||||||
|
### Requirement: Demonstrative photography is labeled until authorized assets exist
|
||||||
|
|
||||||
|
When public pages render illustrative/demo photography that is not an authorized Amare asset, the system SHALL mark that imagery as demonstrative in visible copy or accessible labeling so visitors are not misled.
|
||||||
|
|
||||||
|
#### Scenario: Portfolio demo imagery is disclosed
|
||||||
|
|
||||||
|
- **WHEN** the home or portfolio renders placeholder photography
|
||||||
|
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## 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, 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: 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
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public geography defaults to São Paulo
|
||||||
|
|
||||||
|
Demo and visual seed content for site settings SHALL present the Amare operating city as São Paulo (capital), matching `PRODUCT.md`, instead of unrelated cities.
|
||||||
|
|
||||||
|
#### Scenario: Seeded settings use São Paulo
|
||||||
|
|
||||||
|
- **WHEN** content seeders populate `site_settings`
|
||||||
|
- **THEN** the city field MUST be São Paulo (or equivalent capital wording)
|
||||||
|
- **AND** MUST NOT present Fortaleza as the operating city
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
|
- **WHEN** a testimonial has `published_at` null
|
||||||
|
- **THEN** the `published()` scope MUST exclude it
|
||||||
|
|
||||||
|
#### Scenario: Published testimonial is queryable
|
||||||
|
|
||||||
|
- **WHEN** an admin sets `published_at` with required quote and author name
|
||||||
|
- **THEN** the testimonial MUST be included in the `published()` scope
|
||||||
|
|
||||||
|
#### Scenario: Assistant cannot manage testimonials
|
||||||
|
|
||||||
|
- **WHEN** an assistant attempts to access the testimonials Resource
|
||||||
|
- **THEN** access MUST be denied with HTTP 403
|
||||||
|
|
||||||
|
#### Scenario: Featured testimonials are filterable
|
||||||
|
|
||||||
|
- **WHEN** content is queried with featured filter
|
||||||
|
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
||||||
|
|
||||||
|
#### Scenario: Multi-paragraph quotes render as paragraphs
|
||||||
|
|
||||||
|
- **GIVEN** a published testimonial whose quote contains blank-line separated paragraphs
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
||||||
|
|
||||||
|
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
||||||
|
|
||||||
|
#### Scenario: Seed loads the five real couples
|
||||||
|
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** testimonials for Jeniffer e Maick, Quesia e Jhonata, Milena e Weslley, Raquel e Pedro, and Victoria e Pedro MUST exist with their source quotes and marriage context/dates
|
||||||
|
|
||||||
|
#### Scenario: Fictional demo quotes are removed
|
||||||
|
|
||||||
|
- **WHEN** the content seeder completes
|
||||||
|
- **THEN** previously invented placeholder testimonial authors MUST NOT remain as the published demo set
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
|
||||||
|
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
||||||
|
|
||||||
|
#### Scenario: Unintended visual change fails the suite
|
||||||
|
|
||||||
|
- **GIVEN** approved baselines exist
|
||||||
|
- **WHEN** a code change alters the rendering of a covered screen
|
||||||
|
- **THEN** the visual assertion MUST fail and report the diff
|
||||||
|
|
||||||
|
#### Scenario: Both viewports are covered
|
||||||
|
|
||||||
|
- **WHEN** the visual suite runs
|
||||||
|
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
||||||
|
|
||||||
|
#### Scenario: Heritage Editorial identity is captured
|
||||||
|
|
||||||
|
- **WHEN** approved baselines for the home are reviewed after this change
|
||||||
|
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
||||||
|
|
||||||
|
### Requirement: Visual runs are deterministic
|
||||||
|
|
||||||
|
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, self-hosted fonts installed/bundled for the suite, frozen clock, deterministic seed (including real testimonial subset and São Paulo settings), animations and transitions disabled, and no dependency on external network.
|
||||||
|
|
||||||
|
#### Scenario: Repeated run without code change produces no diff
|
||||||
|
|
||||||
|
- **WHEN** the visual suite runs twice against the same commit and seed
|
||||||
|
- **THEN** both runs MUST pass with no pixel diff
|
||||||
|
|
||||||
|
#### Scenario: Time-dependent content does not cause drift
|
||||||
|
|
||||||
|
- **GIVEN** the clock is frozen and the seed is deterministic
|
||||||
|
- **WHEN** the suite runs on a different calendar day
|
||||||
|
- **THEN** rendered dates MUST remain identical to the baseline
|
||||||
|
|
||||||
|
#### Scenario: Motion is disabled during capture
|
||||||
|
|
||||||
|
- **WHEN** a screenshot is captured
|
||||||
|
- **THEN** CSS animations and transitions MUST be disabled
|
||||||
|
|
||||||
|
### Requirement: Baseline updates are explicit and reviewed
|
||||||
|
|
||||||
|
Baselines SHALL only be updated through the explicit `composer visual:update` command, and the resulting diff MUST be reviewed by a human before merge. Baselines MUST NOT be regenerated automatically to make CI pass.
|
||||||
|
|
||||||
|
#### Scenario: CI does not regenerate baselines
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** it MUST run in assertion mode
|
||||||
|
- **AND** MUST NOT write new baselines
|
||||||
|
|
||||||
|
#### Scenario: Developer updates baselines intentionally
|
||||||
|
|
||||||
|
- **WHEN** a developer runs `composer visual:update`
|
||||||
|
- **THEN** the updated baseline files MUST be written to the versioned baseline directory for review
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial redesign. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
|
- **WHEN** the automated accessibility check reports a critical or serious issue on a covered route
|
||||||
|
- **THEN** the browser suite MUST fail and report the offending rule and selector
|
||||||
|
|
||||||
|
#### Scenario: Covered routes are checked
|
||||||
|
|
||||||
|
- **WHEN** the accessibility suite runs
|
||||||
|
- **THEN** the home, services listing, portfolio listing and case detail MUST each be checked
|
||||||
|
|
||||||
|
### Requirement: Public pages use accessible semantic structure
|
||||||
|
|
||||||
|
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image and brand mark, and visible focus on interactive elements (SPEC §6.5).
|
||||||
|
|
||||||
|
#### Scenario: Single h1 per page
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** exactly one `h1` element MUST be present
|
||||||
|
|
||||||
|
#### Scenario: Landmarks are present
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** `header`, `main`, `nav` and `footer` landmarks MUST be present
|
||||||
|
|
||||||
|
#### Scenario: Content images expose alt text
|
||||||
|
|
||||||
|
- **WHEN** a page renders a cover or gallery image
|
||||||
|
- **THEN** the `alt` attribute MUST contain the stored alt text
|
||||||
|
|
||||||
|
#### Scenario: Brand mark exposes accessible name
|
||||||
|
|
||||||
|
- **WHEN** the public header brand mark is rendered
|
||||||
|
- **THEN** it MUST expose an accessible name identifying Amare Assessoria
|
||||||
|
|
||||||
|
### Requirement: Public pages are fully keyboard operable
|
||||||
|
|
||||||
|
Visitors SHALL be able to reach and activate every interactive element with the keyboard, including the mobile navigation disclosure when visible, with a visible focus indicator and a skip link to the main content.
|
||||||
|
|
||||||
|
#### Scenario: Keyboard reaches the primary CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor navigates the home with the Tab key
|
||||||
|
- **THEN** the primary CTA MUST receive focus with a visible indicator
|
||||||
|
- **AND** activating it with the keyboard MUST navigate to the contact route
|
||||||
|
|
||||||
|
#### Scenario: Skip link bypasses navigation
|
||||||
|
|
||||||
|
- **WHEN** a visitor focuses the first element of a public page
|
||||||
|
- **THEN** a skip link to the main content MUST be available
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu is keyboard operable
|
||||||
|
|
||||||
|
- **WHEN** the mobile menu button is focused and activated with the keyboard
|
||||||
|
- **THEN** the navigation links MUST become reachable by subsequent Tab stops
|
||||||
|
- **AND** the button MUST expose the correct `aria-expanded` state
|
||||||
|
|
||||||
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including editorial hover scales and menu transitions introduced by the redesign.
|
||||||
|
|
||||||
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
|
- **GIVEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
|
- **WHEN** a public page is loaded
|
||||||
|
- **THEN** decorative transitions and animations MUST NOT run
|
||||||
|
|
||||||
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including pages that load the mobile navigation script.
|
||||||
|
|
||||||
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
|
- **WHEN** a covered public route is loaded in the browser suite
|
||||||
|
- **THEN** the console MUST contain no error-level messages
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
## 1. Tokens, fonts and brand assets
|
||||||
|
|
||||||
|
- [x] 1.1 Write failing feature/CSS contract tests (or extend existing token/layout assertions) for Heritage Editorial palette, EB Garamond family, radius 0 and container `1120px`
|
||||||
|
- [x] 1.2 Rewrite `resources/css/tokens.css` + `@theme` mapping in `app.css` to Heritage Editorial; remove card-shadow hierarchy for public content
|
||||||
|
- [x] 1.3 Swap Vite font from Instrument Sans to self-hosted EB Garamond (400/500/600) and update `components/fonts.blade.php` consumption
|
||||||
|
- [x] 1.4 Add optimized logo assets under `public/brand/` (light/dark variants) from the provided lockup without redrawing geometry; add `<x-brand.logo>` with accessible alt
|
||||||
|
- [x] 1.5 Run focused feature/static checks covering tokens/fonts and `npm run build`
|
||||||
|
|
||||||
|
## 2. Site settings and CMS editorial fields
|
||||||
|
|
||||||
|
- [x] 2.1 Add failing feature tests for new typed `site_settings` fields (logo, hero secondary CTA/note, manifesto, method steps, principles) and São Paulo seed city
|
||||||
|
- [x] 2.2 Create migration + model fillable/casts for the new typed columns (JSONB for method steps/principles preferred)
|
||||||
|
- [x] 2.3 Update Filament `ManageSiteSettings` with pt-BR editorial sections and logo alt validation
|
||||||
|
- [x] 2.4 Update `ContentSeeder` / factory defaults with editorial copy and São Paulo; keep optional fields nullable with safe view defaults
|
||||||
|
- [x] 2.5 Run `composer test:feature` for site-settings coverage
|
||||||
|
|
||||||
|
## 3. Real testimonials
|
||||||
|
|
||||||
|
- [x] 3.1 Add failing tests for multi-paragraph quote rendering and seeder expectations for the five couples from `depoimentos.md`
|
||||||
|
- [x] 3.2 Replace fictional testimonials in content/visual seeders with the real quotes, author names and context/dates
|
||||||
|
- [x] 3.3 Update testimonials Blade to render paragraph breaks; keep unpublished-by-default path documented for production authorization
|
||||||
|
- [x] 3.4 Run testimonials feature tests
|
||||||
|
|
||||||
|
## 4. Public layout and navigation
|
||||||
|
|
||||||
|
- [x] 4.1 Add/extend failing tests for brand mark in header, landmarks, skip link, and mobile menu `aria-expanded` behavior
|
||||||
|
- [x] 4.2 Redesign `layouts/public.blade.php` header/footer for Heritage Editorial (logo, route nav, contact CTA, footer groups)
|
||||||
|
- [x] 4.3 Implement minimal mobile menu toggle in `resources/js/app.js` with reduced-motion safety
|
||||||
|
- [x] 4.4 Run layout/accessibility structure feature tests
|
||||||
|
|
||||||
|
## 5. Home editorial reconstruction
|
||||||
|
|
||||||
|
- [x] 5.1 Update failing `HomePageContentTest` (and related) for new section order: hero → manifesto → services → portfolio → method → testimonials → positioning → final CTA; empty catalog sections omitted; CTA → contact; preserve `data-testid="home-primary-cta"`
|
||||||
|
- [x] 5.2 Rebuild home components/pages to match editorial composition (merge proof+cases into one portfolio block; add manifesto + positioning; four method steps)
|
||||||
|
- [x] 5.3 Wire settings-driven copy and demo-image disclosure note; keep single `h1`
|
||||||
|
- [x] 5.4 Run home feature tests
|
||||||
|
|
||||||
|
## 6. Internal public pages as chapters
|
||||||
|
|
||||||
|
- [x] 6.1 Extend/adjust public page feature tests for services, portfolio index/show, about, contact (presentation-only), privacy and branded errors under the new layout
|
||||||
|
- [x] 6.2 Restyle `pages/services`, `pages/portfolio/*`, `pages/about`, `pages/contact`, `pages/privacy`, `errors/404`, `errors/500` to Heritage Editorial without changing route contracts or inventing proof
|
||||||
|
- [x] 6.3 Confirm contact page shows settings channels and creates no leads
|
||||||
|
- [x] 6.4 Run `PublicPagesTest` and related SEO/N+1 tests
|
||||||
|
|
||||||
|
## 7. Accessibility, browser and visual gates
|
||||||
|
|
||||||
|
- [x] 7.1 Update browser accessibility tests for brand mark name, keyboard path to primary CTA, mobile menu operability, axe on covered routes, reduced motion and clean console
|
||||||
|
- [x] 7.2 Update `VisualContentSeeder` for deterministic Heritage Editorial content (SP + real testimonial subset + fixtures)
|
||||||
|
- [x] 7.3 Run browser a11y/smoke suites; fix regressions
|
||||||
|
- [x] 7.4 Run `composer visual:update` and review desktop `1440×1000` / mobile `390×844` diffs for home, services, portfolio, portfolio detail before committing baselines
|
||||||
|
|
||||||
|
## 8. Quality closeout
|
||||||
|
|
||||||
|
- [x] 8.1 Run `composer pint` and `composer phpstan` on touched PHP
|
||||||
|
- [x] 8.2 Run `composer test:feature` and `composer test:browser`
|
||||||
|
- [x] 8.3 Run `composer quality` (or equivalent full gate) and fix remaining failures
|
||||||
|
- [x] 8.4 Update surface brief / note remaining unresolved items (official contacts, final testimonial authorization, authorized photography) without inventing facts
|
||||||
@@ -104,3 +104,33 @@ Public content image uploads (Filament FileUpload via `PublicImageUploadRules`)
|
|||||||
|
|
||||||
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
|
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
|
||||||
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`
|
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`
|
||||||
|
|
||||||
|
### Requirement: Brand logo assets are available to the public layout
|
||||||
|
|
||||||
|
The system SHALL provide optimized Amare brand logo assets derived from the official faceted-heart lockup for use in the public header, footer and institutional pages. Assets MUST preserve the original geometry, include an accessible text alternative, and provide variants suitable for light and dark tonal fields. When `site_settings.logo_path` is present, the uploaded logo MUST be used; otherwise the versioned static brand asset MUST be used.
|
||||||
|
|
||||||
|
#### Scenario: Header renders brand mark with alt text
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** the brand mark image or equivalent MUST expose accessible alternative text identifying Amare Assessoria
|
||||||
|
|
||||||
|
#### Scenario: Dark portfolio field uses a legible logo variant
|
||||||
|
|
||||||
|
- **WHEN** the brand mark is rendered on an olive-deep or otherwise dark public surface
|
||||||
|
- **THEN** the chosen logo variant MUST remain legible against that background
|
||||||
|
|
||||||
|
#### Scenario: Uploaded logo overrides static fallback
|
||||||
|
|
||||||
|
- **GIVEN** an admin has saved `logo_path` and `logo_alt` in site settings
|
||||||
|
- **WHEN** the public layout renders the brand mark
|
||||||
|
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Decorative saturation/contrast treatment for editorial mood MUST be applied via CSS on self-hosted images and MUST NOT introduce external image CDN dependencies that break deterministic visual tests.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
|
|||||||
@@ -5,16 +5,24 @@ TBD - created by archiving change setup-foundation. Update Purpose after archive
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Design tokens are centralized for the public site
|
### Requirement: Design tokens are centralized for the public site
|
||||||
|
|
||||||
The system SHALL define minimum design tokens in a single source consumed by the public site layout and components. Tokens MUST cover typography families, font scale, spacing, border radius, container width, background/text/border/accent/state colors, shadows, and transition duration/easing.
|
The system SHALL define design tokens in a single source consumed by the public site layout and components. Tokens MUST implement the Heritage Editorial system from `DESIGN.md`: typography family EB Garamond (self-hosted), font scale (display/headline/title/body/label), spacing on an 8px rhythm, border radius `0` for interactive and content surfaces, container max width `1120px`, paper/olive/sage/ink color roles, transition duration/easing, and MUST NOT rely on card shadows as a hierarchy mechanism for regular content.
|
||||||
|
|
||||||
#### Scenario: Public layout uses shared tokens
|
#### Scenario: Public layout uses shared Heritage Editorial tokens
|
||||||
|
|
||||||
- **WHEN** a public page is rendered
|
- **WHEN** a public page is rendered
|
||||||
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
||||||
|
- **AND** the primary typeface MUST be EB Garamond (or the declared serif fallback stack)
|
||||||
|
- **AND** public content surfaces MUST use `0` border radius from tokens
|
||||||
|
|
||||||
|
#### Scenario: Palette commits paper and olive regions
|
||||||
|
|
||||||
|
- **WHEN** the public site is rendered
|
||||||
|
- **THEN** background regions MUST use paper ivory / paper deep / olive deep tokens rather than pure white card stacks on a white page
|
||||||
|
- **AND** primary interactive emphasis MUST use olive heritage (`#556B2F`) / olive deep (`#3E5219`) tokens
|
||||||
|
|
||||||
### Requirement: Public site respects reduced motion preference
|
### Requirement: Public site respects reduced motion preference
|
||||||
|
|
||||||
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site.
|
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site, including image hover scales and menu transitions.
|
||||||
|
|
||||||
#### Scenario: User prefers reduced motion
|
#### Scenario: User prefers reduced motion
|
||||||
|
|
||||||
@@ -23,10 +31,25 @@ The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-e
|
|||||||
|
|
||||||
### Requirement: Public site meets baseline accessibility contrast
|
### Requirement: Public site meets baseline accessibility contrast
|
||||||
|
|
||||||
The system SHALL use color combinations on the public site that meet WCAG AA contrast requirements for text and interactive elements defined in the token palette.
|
The system SHALL use Heritage Editorial color combinations that meet WCAG AA contrast requirements for text and interactive elements. Long-form text MUST use ink on paper; sage MUST NOT replace reading color when contrast would fall below AA.
|
||||||
|
|
||||||
#### Scenario: Primary text is readable
|
#### Scenario: Primary text is readable
|
||||||
|
|
||||||
- **WHEN** primary body text is rendered on its background color
|
- **WHEN** primary body text is rendered on its background color
|
||||||
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
#### Scenario: Olive on paper interactive text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary buttons or links use olive tokens on paper backgrounds (or paper text on olive)
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
### Requirement: Elevation comes from tonal fields not card shadows
|
||||||
|
|
||||||
|
The public site SHALL express hierarchy through tonal paper fields, 1px botanical rules, and editorial overlap. Regular content components MUST NOT use short grey SaaS card shadows.
|
||||||
|
|
||||||
|
#### Scenario: Content cards omit drop shadows
|
||||||
|
|
||||||
|
- **WHEN** home services, testimonials, or portfolio items are rendered
|
||||||
|
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
||||||
|
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ The system SHALL expose the public routes of SPEC §5.1: `home` (`/`), `services
|
|||||||
|
|
||||||
### Requirement: Home renders the editorial structure from CMS content
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
|
||||||
The home page SHALL render, in the order defined by SPEC §6.2, header/navigation, hero, featured visual proof, services summary, working method, selected cases, testimonials, final briefing CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name and contact data MUST come from `site_settings`; services, cases and testimonials MUST come from published records.
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition (asymmetric spreads on desktop, linear sequence on mobile) rather than rounded card grids.
|
||||||
|
|
||||||
#### Scenario: Published content is displayed in configured order
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
@@ -43,18 +43,21 @@ The home page SHALL render, in the order defined by SPEC §6.2, header/navigatio
|
|||||||
- **WHEN** a visitor loads the home
|
- **WHEN** a visitor loads the home
|
||||||
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
- **AND** the hero MUST show the values stored in `site_settings`
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
|
||||||
#### Scenario: CTA leads to the briefing page
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
- **WHEN** a visitor activates the primary or final CTA on the home
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
- **THEN** the visitor MUST be taken to the `contact` route
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
#### Scenario: Empty content does not break the home
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
- **GIVEN** no published services, cases or testimonials
|
- **GIVEN** no published services, cases or testimonials
|
||||||
- **WHEN** a visitor loads the home
|
- **WHEN** a visitor loads the home
|
||||||
- **THEN** the response MUST be 200
|
- **THEN** the response MUST be 200
|
||||||
- **AND** the affected sections MUST be omitted instead of rendering empty containers
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
#### Scenario: Home has no console errors
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
@@ -63,12 +66,13 @@ The home page SHALL render, in the order defined by SPEC §6.2, header/navigatio
|
|||||||
|
|
||||||
### Requirement: Listing and detail pages exist for catalog content
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
|
||||||
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03). The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
||||||
|
|
||||||
#### Scenario: Services listing shows published services
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
- **WHEN** a visitor loads `/servicos`
|
- **WHEN** a visitor loads `/servicos`
|
||||||
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
#### Scenario: Gallery respects stored order
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ The system SHALL render a services listing (WEB-02) and a portfolio listing plus
|
|||||||
|
|
||||||
### Requirement: Institutional and error pages have brand identity
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
|
||||||
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07). The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
#### Scenario: Unknown URL renders branded 404
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
@@ -100,7 +104,7 @@ The system SHALL provide the Sobre and Política de privacidade pages and brande
|
|||||||
|
|
||||||
### Requirement: Contact page presents contact data as briefing placeholder
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads in this change.
|
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads and MUST NOT submit a functional briefing form in this change.
|
||||||
|
|
||||||
#### Scenario: Contact page shows configured contact data
|
#### Scenario: Contact page shows configured contact data
|
||||||
|
|
||||||
@@ -117,3 +121,28 @@ Public pages SHALL load related content with explicit eager loading through dedi
|
|||||||
- **WHEN** a case detail page with many gallery images is rendered
|
- **WHEN** a case detail page with many gallery images is rendered
|
||||||
- **THEN** the gallery MUST be loaded with eager loading
|
- **THEN** the gallery MUST be loaded with eager loading
|
||||||
- **AND** the query count MUST NOT grow with the number of images
|
- **AND** the query count MUST NOT grow with the number of images
|
||||||
|
|
||||||
|
### Requirement: Public header exposes brand mark and responsive navigation
|
||||||
|
|
||||||
|
The public layout SHALL render the Amare brand mark (faceted-heart logo lockup or configured logo), primary route navigation, and a contact CTA. On narrow viewports the navigation MUST be operable via a disclosure control with accessible name and `aria-expanded` state.
|
||||||
|
|
||||||
|
#### Scenario: Desktop header shows navigation and CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads any public page at a desktop viewport
|
||||||
|
- **THEN** the header MUST include brand mark, links to home/services/portfolio/about/contact, and a contact CTA
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu toggles accessibly
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the menu button on a narrow viewport
|
||||||
|
- **THEN** the primary navigation MUST become available
|
||||||
|
- **AND** the control MUST expose an updated `aria-expanded` value
|
||||||
|
- **AND** activating a navigation link MUST close the menu
|
||||||
|
|
||||||
|
### Requirement: Demonstrative photography is labeled until authorized assets exist
|
||||||
|
|
||||||
|
When public pages render illustrative/demo photography that is not an authorized Amare asset, the system SHALL mark that imagery as demonstrative in visible copy or accessible labeling so visitors are not misled.
|
||||||
|
|
||||||
|
#### Scenario: Portfolio demo imagery is disclosed
|
||||||
|
|
||||||
|
- **WHEN** the home or portfolio renders placeholder photography
|
||||||
|
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define the typed site-wide settings singleton and its administration rules.
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Site settings singleton is manageable by admin only
|
### 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, hero copy (eyebrow, title, subtitle, CTA label), about summary, 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.
|
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, 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
|
#### Scenario: Admin updates site settings
|
||||||
|
|
||||||
@@ -25,8 +25,31 @@ The system SHALL persist site-wide settings in a `site_settings` table as a type
|
|||||||
- **THEN** validation MUST fail with a pt-BR error message
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
- **AND** alt text MUST remain optional when no default OG image is present
|
- **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
|
#### Scenario: Singleton avoids generic key-value store
|
||||||
|
|
||||||
- **WHEN** site settings are stored
|
- **WHEN** site settings are stored
|
||||||
- **THEN** the system MUST use typed columns on `site_settings`
|
- **THEN** the system MUST use typed columns on `site_settings`
|
||||||
- **AND** MUST NOT introduce a generic key/value configuration table
|
- **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
|
||||||
|
|
||||||
|
### Requirement: Public geography defaults to São Paulo
|
||||||
|
|
||||||
|
Demo and visual seed content for site settings SHALL present the Amare operating city as São Paulo (capital), matching `PRODUCT.md`, instead of unrelated cities.
|
||||||
|
|
||||||
|
#### Scenario: Seeded settings use São Paulo
|
||||||
|
|
||||||
|
- **WHEN** content seeders populate `site_settings`
|
||||||
|
- **THEN** the city field MUST be São Paulo (or equivalent capital wording)
|
||||||
|
- **AND** MUST NOT present Fortaleza as the operating city
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define testimonial management, publication, authorization, and featured filterin
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Testimonials are managed with publication control
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text, author name, optional context, optional photo with alt text, sort order, featured flag, and `published_at`.
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
#### Scenario: Unpublished testimonial is excluded
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
@@ -27,3 +27,23 @@ The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote te
|
|||||||
|
|
||||||
- **WHEN** content is queried with featured filter
|
- **WHEN** content is queried with featured filter
|
||||||
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
||||||
|
|
||||||
|
#### Scenario: Multi-paragraph quotes render as paragraphs
|
||||||
|
|
||||||
|
- **GIVEN** a published testimonial whose quote contains blank-line separated paragraphs
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
||||||
|
|
||||||
|
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
||||||
|
|
||||||
|
#### Scenario: Seed loads the five real couples
|
||||||
|
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** testimonials for Jeniffer e Maick, Quesia e Jhonata, Milena e Weslley, Raquel e Pedro, and Victoria e Pedro MUST exist with their source quotes and marriage context/dates
|
||||||
|
|
||||||
|
#### Scenario: Fictional demo quotes are removed
|
||||||
|
|
||||||
|
- **WHEN** the content seeder completes
|
||||||
|
- **THEN** previously invented placeholder testimonial authors MUST NOT remain as the published demo set
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define deterministic visual baselines for public screens and the explicit baseli
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public screens have desktop and mobile visual baselines
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
|
||||||
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed.
|
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
||||||
|
|
||||||
#### Scenario: Unintended visual change fails the suite
|
#### Scenario: Unintended visual change fails the suite
|
||||||
|
|
||||||
@@ -19,9 +19,14 @@ The system SHALL keep versioned screenshot baselines for the public screens avai
|
|||||||
- **WHEN** the visual suite runs
|
- **WHEN** the visual suite runs
|
||||||
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
||||||
|
|
||||||
|
#### Scenario: Heritage Editorial identity is captured
|
||||||
|
|
||||||
|
- **WHEN** approved baselines for the home are reviewed after this change
|
||||||
|
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
||||||
|
|
||||||
### Requirement: Visual runs are deterministic
|
### Requirement: Visual runs are deterministic
|
||||||
|
|
||||||
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, fonts installed in the image, frozen clock, deterministic seed, animations and transitions disabled, and no dependency on external network.
|
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, self-hosted fonts installed/bundled for the suite, frozen clock, deterministic seed (including real testimonial subset and São Paulo settings), animations and transitions disabled, and no dependency on external network.
|
||||||
|
|
||||||
#### Scenario: Repeated run without code change produces no diff
|
#### Scenario: Repeated run without code change produces no diff
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define automated accessibility checks, semantic structure, keyboard operability,
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public routes have no critical or serious accessibility issues
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8). A critical or serious issue MUST fail the suite.
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial redesign. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
#### Scenario: Critical issue blocks the suite
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ The system SHALL run automated accessibility checks on the public routes covered
|
|||||||
|
|
||||||
### Requirement: Public pages use accessible semantic structure
|
### Requirement: Public pages use accessible semantic structure
|
||||||
|
|
||||||
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image, and visible focus on interactive elements (SPEC §6.5).
|
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image and brand mark, and visible focus on interactive elements (SPEC §6.5).
|
||||||
|
|
||||||
#### Scenario: Single h1 per page
|
#### Scenario: Single h1 per page
|
||||||
|
|
||||||
@@ -37,9 +37,14 @@ Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a cohe
|
|||||||
- **WHEN** a page renders a cover or gallery image
|
- **WHEN** a page renders a cover or gallery image
|
||||||
- **THEN** the `alt` attribute MUST contain the stored alt text
|
- **THEN** the `alt` attribute MUST contain the stored alt text
|
||||||
|
|
||||||
|
#### Scenario: Brand mark exposes accessible name
|
||||||
|
|
||||||
|
- **WHEN** the public header brand mark is rendered
|
||||||
|
- **THEN** it MUST expose an accessible name identifying Amare Assessoria
|
||||||
|
|
||||||
### Requirement: Public pages are fully keyboard operable
|
### Requirement: Public pages are fully keyboard operable
|
||||||
|
|
||||||
Visitors SHALL be able to reach and activate every interactive element with the keyboard, with a visible focus indicator and a skip link to the main content.
|
Visitors SHALL be able to reach and activate every interactive element with the keyboard, including the mobile navigation disclosure when visible, with a visible focus indicator and a skip link to the main content.
|
||||||
|
|
||||||
#### Scenario: Keyboard reaches the primary CTA
|
#### Scenario: Keyboard reaches the primary CTA
|
||||||
|
|
||||||
@@ -52,9 +57,15 @@ Visitors SHALL be able to reach and activate every interactive element with the
|
|||||||
- **WHEN** a visitor focuses the first element of a public page
|
- **WHEN** a visitor focuses the first element of a public page
|
||||||
- **THEN** a skip link to the main content MUST be available
|
- **THEN** a skip link to the main content MUST be available
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu is keyboard operable
|
||||||
|
|
||||||
|
- **WHEN** the mobile menu button is focused and activated with the keyboard
|
||||||
|
- **THEN** the navigation links MUST become reachable by subsequent Tab stops
|
||||||
|
- **AND** the button MUST expose the correct `aria-expanded` state
|
||||||
|
|
||||||
### Requirement: Reduced motion preference is honored
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`.
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including editorial hover scales and menu transitions introduced by the redesign.
|
||||||
|
|
||||||
#### Scenario: Reduced motion disables transitions
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
@@ -64,7 +75,7 @@ The system SHALL suppress non-essential animation and transition when the user a
|
|||||||
|
|
||||||
### Requirement: Public pages emit no console errors
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19).
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including pages that load the mobile navigation script.
|
||||||
|
|
||||||
#### Scenario: Console stays clean on covered routes
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
|
|||||||
BIN
public/brand/lockup-on-dark.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/brand/lockup-on-dark.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/brand/lockup-on-light.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
public/brand/lockup-on-light.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/brand/lockup-source.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
public/brand/mark-on-dark.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/brand/mark-on-dark.webp
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
public/brand/mark-on-light.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
public/brand/mark-on-light.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
@@ -34,21 +34,22 @@
|
|||||||
--radius-full: var(--amare-radius-full);
|
--radius-full: var(--amare-radius-full);
|
||||||
|
|
||||||
--color-amare-bg: var(--amare-color-bg);
|
--color-amare-bg: var(--amare-color-bg);
|
||||||
|
--color-amare-bg-deep: var(--amare-color-bg-deep);
|
||||||
|
--color-amare-bg-archive: var(--amare-color-bg-archive);
|
||||||
--color-amare-bg-muted: var(--amare-color-bg-muted);
|
--color-amare-bg-muted: var(--amare-color-bg-muted);
|
||||||
--color-amare-text: var(--amare-color-text);
|
--color-amare-text: var(--amare-color-text);
|
||||||
|
--color-amare-muted: var(--amare-color-muted);
|
||||||
--color-amare-text-muted: var(--amare-color-text-muted);
|
--color-amare-text-muted: var(--amare-color-text-muted);
|
||||||
--color-amare-border: var(--amare-color-border);
|
--color-amare-border: var(--amare-color-border);
|
||||||
--color-amare-accent: var(--amare-color-accent);
|
--color-amare-accent: var(--amare-color-accent);
|
||||||
|
--color-amare-accent-deep: var(--amare-color-accent-deep);
|
||||||
--color-amare-accent-hover: var(--amare-color-accent-hover);
|
--color-amare-accent-hover: var(--amare-color-accent-hover);
|
||||||
|
--color-amare-sage: var(--amare-color-sage);
|
||||||
--color-amare-accent-text: var(--amare-color-accent-text);
|
--color-amare-accent-text: var(--amare-color-accent-text);
|
||||||
--color-amare-success: var(--amare-color-success);
|
--color-amare-success: var(--amare-color-success);
|
||||||
--color-amare-warning: var(--amare-color-warning);
|
--color-amare-warning: var(--amare-color-warning);
|
||||||
--color-amare-error: var(--amare-color-error);
|
--color-amare-error: var(--amare-color-error);
|
||||||
|
|
||||||
--shadow-amare-sm: var(--amare-shadow-sm);
|
|
||||||
--shadow-amare-md: var(--amare-shadow-md);
|
|
||||||
--shadow-amare-lg: var(--amare-shadow-lg);
|
|
||||||
|
|
||||||
--ease-amare: var(--amare-ease-standard);
|
--ease-amare: var(--amare-ease-standard);
|
||||||
--default-transition-duration: var(--amare-duration-normal);
|
--default-transition-duration: var(--amare-duration-normal);
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
body {
|
body {
|
||||||
background-color: var(--amare-color-bg);
|
background-color: var(--amare-color-bg);
|
||||||
color: var(--amare-color-text);
|
color: var(--amare-color-text);
|
||||||
font-family: var(--amare-font-sans);
|
font-family: var(--amare-font-serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:focus-visible,
|
a:focus-visible,
|
||||||
@@ -78,3 +79,23 @@
|
|||||||
margin-inline: auto;
|
margin-inline: auto;
|
||||||
padding-inline: var(--amare-container-padding);
|
padding-inline: var(--amare-container-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility img-editorial {
|
||||||
|
filter: saturate(0.88) contrast(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.main-nav.is-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.main-nav {
|
||||||
|
transition: opacity var(--amare-duration-normal) var(--amare-ease-standard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.menu-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
/* Typography */
|
/* Typography — Heritage Editorial single voice */
|
||||||
--amare-font-sans: var(--font-instrument-sans, 'Instrument Sans'), sans-serif;
|
--amare-font-serif: var(--font-eb-garamond, 'EB Garamond'), Garamond, Georgia, serif;
|
||||||
--amare-font-serif: var(--font-instrument-sans, 'Instrument Sans'), sans-serif;
|
--amare-font-sans: var(--amare-font-serif);
|
||||||
|
|
||||||
/* Font scale */
|
/* Font scale */
|
||||||
--amare-text-xs: 0.75rem;
|
--amare-text-xs: 0.75rem;
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
--amare-text-3xl: 1.875rem;
|
--amare-text-3xl: 1.875rem;
|
||||||
--amare-text-4xl: 2.25rem;
|
--amare-text-4xl: 2.25rem;
|
||||||
|
|
||||||
/* Spacing scale */
|
/* Spacing scale (8px rhythm) */
|
||||||
--amare-space-1: 0.25rem;
|
--amare-space-1: 0.25rem;
|
||||||
--amare-space-2: 0.5rem;
|
--amare-space-2: 0.5rem;
|
||||||
--amare-space-3: 0.75rem;
|
--amare-space-3: 0.75rem;
|
||||||
@@ -23,35 +23,35 @@
|
|||||||
--amare-space-12: 3rem;
|
--amare-space-12: 3rem;
|
||||||
--amare-space-16: 4rem;
|
--amare-space-16: 4rem;
|
||||||
|
|
||||||
/* Border radius */
|
/* Border radius — sharp editorial edges */
|
||||||
--amare-radius-sm: 0.375rem;
|
--amare-radius-sm: 0;
|
||||||
--amare-radius-md: 0.5rem;
|
--amare-radius-md: 0;
|
||||||
--amare-radius-lg: 0.75rem;
|
--amare-radius-lg: 0;
|
||||||
--amare-radius-xl: 1rem;
|
--amare-radius-xl: 0;
|
||||||
--amare-radius-full: 9999px;
|
--amare-radius-full: 9999px;
|
||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
--amare-container-max: 72rem;
|
--amare-container-max: 1120px;
|
||||||
--amare-container-padding: 1.5rem;
|
--amare-container-padding: 1.5rem;
|
||||||
|
|
||||||
/* Colors — WCAG AA contrast pairs */
|
/* Colors — Heritage Editorial (WCAG AA pairs) */
|
||||||
--amare-color-bg: #fffdf8;
|
--amare-color-bg: #FBF9F4;
|
||||||
--amare-color-bg-muted: #f5f0e8;
|
--amare-color-bg-deep: #F0EEE9;
|
||||||
--amare-color-text: #1a1410;
|
--amare-color-bg-archive: #E4E2DD;
|
||||||
--amare-color-text-muted: #4a4038;
|
--amare-color-bg-muted: var(--amare-color-bg-deep);
|
||||||
--amare-color-border: #d9cfc0;
|
--amare-color-text: #1B1C19;
|
||||||
--amare-color-accent: #8a6500;
|
--amare-color-muted: #5D6155;
|
||||||
--amare-color-accent-hover: #6f5200;
|
--amare-color-text-muted: var(--amare-color-muted);
|
||||||
--amare-color-accent-text: #fffdf8;
|
--amare-color-border: #C5C8B8;
|
||||||
|
--amare-color-accent: #556B2F;
|
||||||
|
--amare-color-accent-deep: #3E5219;
|
||||||
|
--amare-color-accent-hover: var(--amare-color-accent-deep);
|
||||||
|
--amare-color-sage: #8B9D77;
|
||||||
|
--amare-color-accent-text: #FFFFFF;
|
||||||
--amare-color-success: #166534;
|
--amare-color-success: #166534;
|
||||||
--amare-color-warning: #92400e;
|
--amare-color-warning: #92400e;
|
||||||
--amare-color-error: #991b1b;
|
--amare-color-error: #991b1b;
|
||||||
|
|
||||||
/* Shadows */
|
|
||||||
--amare-shadow-sm: 0 1px 2px rgb(26 20 16 / 0.06);
|
|
||||||
--amare-shadow-md: 0 4px 12px rgb(26 20 16 / 0.08);
|
|
||||||
--amare-shadow-lg: 0 12px 32px rgb(26 20 16 / 0.12);
|
|
||||||
|
|
||||||
/* Transitions */
|
/* Transitions */
|
||||||
--amare-duration-fast: 150ms;
|
--amare-duration-fast: 150ms;
|
||||||
--amare-duration-normal: 250ms;
|
--amare-duration-normal: 250ms;
|
||||||
|
|||||||
@@ -1 +1,40 @@
|
|||||||
//
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const menuButton = document.querySelector('[data-menu-button]');
|
||||||
|
const navigation = document.querySelector('[data-main-nav]');
|
||||||
|
|
||||||
|
if (!menuButton || !navigation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOpen = (isOpen) => {
|
||||||
|
navigation.classList.toggle('is-open', isOpen);
|
||||||
|
navigation.classList.toggle('hidden', !isOpen && !window.matchMedia('(min-width: 768px)').matches);
|
||||||
|
navigation.classList.toggle('flex', isOpen || window.matchMedia('(min-width: 768px)').matches);
|
||||||
|
document.body.classList.toggle('menu-open', isOpen);
|
||||||
|
menuButton.setAttribute('aria-expanded', String(isOpen));
|
||||||
|
menuButton.setAttribute('aria-label', isOpen ? 'Fechar menu' : 'Abrir menu');
|
||||||
|
};
|
||||||
|
|
||||||
|
menuButton.addEventListener('click', () => {
|
||||||
|
const isOpen = menuButton.getAttribute('aria-expanded') !== 'true';
|
||||||
|
setOpen(isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
navigation.querySelectorAll('a').forEach((link) => {
|
||||||
|
link.addEventListener('click', () => setOpen(false));
|
||||||
|
});
|
||||||
|
|
||||||
|
window.matchMedia('(min-width: 768px)').addEventListener('change', (event) => {
|
||||||
|
if (event.matches) {
|
||||||
|
setOpen(false);
|
||||||
|
navigation.classList.remove('hidden');
|
||||||
|
navigation.classList.add('flex');
|
||||||
|
} else {
|
||||||
|
navigation.classList.remove('is-open', 'flex');
|
||||||
|
navigation.classList.add('hidden');
|
||||||
|
document.body.classList.remove('menu-open');
|
||||||
|
menuButton.setAttribute('aria-expanded', 'false');
|
||||||
|
menuButton.setAttribute('aria-label', 'Abrir menu');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
41
resources/views/components/brand/logo.blade.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@props([
|
||||||
|
'variant' => 'on-light',
|
||||||
|
'mark' => false,
|
||||||
|
'alt' => null,
|
||||||
|
'class' => '',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$settings = $siteSettings ?? null;
|
||||||
|
$uploadedPath = is_object($settings) ? ($settings->logo_path ?? null) : null;
|
||||||
|
$uploadedAlt = is_object($settings) ? ($settings->logo_alt ?? null) : null;
|
||||||
|
|
||||||
|
$resolvedAlt = $alt
|
||||||
|
?? (filled($uploadedAlt) ? $uploadedAlt : null)
|
||||||
|
?? ((is_object($settings) && filled($settings->brand_name ?? null))
|
||||||
|
? $settings->brand_name
|
||||||
|
: 'Amare Assessoria');
|
||||||
|
|
||||||
|
$variant = $variant === 'on-dark' ? 'on-dark' : 'on-light';
|
||||||
|
$kind = $mark ? 'mark' : 'lockup';
|
||||||
|
$staticSrc = asset("brand/{$kind}-{$variant}.webp");
|
||||||
|
$staticFallback = asset("brand/{$kind}-{$variant}.png");
|
||||||
|
|
||||||
|
$src = filled($uploadedPath)
|
||||||
|
? \Illuminate\Support\Facades\Storage::disk('public')->url($uploadedPath)
|
||||||
|
: $staticSrc;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<img
|
||||||
|
{{ $attributes->merge([
|
||||||
|
'src' => $src,
|
||||||
|
'alt' => $resolvedAlt,
|
||||||
|
'class' => trim('brand-logo '.$class),
|
||||||
|
'decoding' => 'async',
|
||||||
|
'loading' => 'eager',
|
||||||
|
]) }}
|
||||||
|
@if (! filled($uploadedPath))
|
||||||
|
data-brand-fallback="{{ $staticFallback }}"
|
||||||
|
@endif
|
||||||
|
data-brand-variant="{{ $variant }}"
|
||||||
|
/>
|
||||||
@@ -2,16 +2,17 @@
|
|||||||
'settings',
|
'settings',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section aria-labelledby="final-cta-heading" class="py-16">
|
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20">
|
||||||
<div class="container-amare rounded-xl border border-amare-border bg-amare-bg-muted px-8 py-12 text-center">
|
<div class="container-amare space-y-6 text-center">
|
||||||
<h2 id="final-cta-heading" class="text-3xl font-semibold text-amare-text">Vamos planejar o seu evento?</h2>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||||
<p class="mx-auto mt-3 max-w-2xl text-amare-text-muted">
|
<h2 id="final-cta-heading" class="text-3xl font-medium text-amare-text md:text-4xl">Todo grande encontro começa com uma boa conversa.</h2>
|
||||||
Conte um pouco do que você imagina. A próxima conversa começa no briefing.
|
<p class="mx-auto max-w-2xl text-amare-muted">
|
||||||
|
Compartilhe as primeiras informações do seu evento. A Amare retorna para entender o contexto e orientar os próximos passos.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-8">
|
<div>
|
||||||
<a
|
<a
|
||||||
href="{{ route('contact') }}"
|
href="{{ route('contact') }}"
|
||||||
class="inline-flex items-center rounded-md bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover"
|
class="inline-flex items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-deep"
|
||||||
>
|
>
|
||||||
{{ $settings->hero_cta_label }}
|
{{ $settings->hero_cta_label }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
'settings',
|
'settings',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section aria-labelledby="hero-heading" class="relative overflow-hidden border-b border-amare-border bg-amare-bg">
|
<section aria-labelledby="hero-heading" class="border-b border-amare-border bg-amare-bg">
|
||||||
<div class="container-amare grid gap-10 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:py-24">
|
<div class="container-amare grid gap-12 py-20 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:py-28">
|
||||||
<div class="space-y-6">
|
<div class="space-y-8">
|
||||||
@if (filled($settings->hero_eyebrow))
|
@if (filled($settings->hero_eyebrow))
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<h1 id="hero-heading" class="max-w-3xl text-4xl font-semibold tracking-tight text-amare-text md:text-5xl">
|
<h1 id="hero-heading" class="max-w-3xl text-4xl font-medium leading-none text-amare-text md:text-5xl">
|
||||||
{{ $settings->hero_title }}
|
{{ $settings->hero_title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -17,25 +17,38 @@
|
|||||||
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div>
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="{{ route('contact') }}"
|
href="{{ route('contact') }}"
|
||||||
data-testid="home-primary-cta"
|
data-testid="home-primary-cta"
|
||||||
class="inline-flex items-center rounded-md bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-amare-accent"
|
class="inline-flex items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover"
|
||||||
>
|
>
|
||||||
{{ $settings->hero_cta_label }}
|
{{ $settings->hero_cta_label }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@if (filled($settings->hero_secondary_cta_label))
|
||||||
|
<a
|
||||||
|
href="{{ route('portfolio.index') }}"
|
||||||
|
class="inline-flex items-center border-b border-amare-accent pb-1 text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep"
|
||||||
|
>
|
||||||
|
{{ $settings->hero_secondary_cta_label }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (filled($settings->hero_note))
|
||||||
|
<p class="max-w-xl text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filled($settings->default_og_image_path))
|
@if (filled($settings->default_og_image_path))
|
||||||
<div class="min-h-72 overflow-hidden rounded-xl bg-amare-bg-muted">
|
<div class="min-h-72 bg-amare-bg-deep">
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$settings->default_og_image_path"
|
:path="$settings->default_og_image_path"
|
||||||
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
|
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
sizes="(max-width: 768px) 100vw, 40vw"
|
sizes="(max-width: 768px) 100vw, 40vw"
|
||||||
class="h-full w-full object-cover"
|
class="img-editorial h-full w-full object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
20
resources/views/components/home/manifesto.blade.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$title = $settings->manifesto_title ?: 'Sofisticação que também se traduz em organização.';
|
||||||
|
$lead = $settings->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.';
|
||||||
|
$body = $settings->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.';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section aria-labelledby="manifesto-heading" class="border-b border-amare-border bg-amare-bg-deep py-20">
|
||||||
|
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Manifesto</p>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<h2 id="manifesto-heading" class="max-w-3xl text-3xl font-medium leading-tight text-amare-text md:text-4xl">{{ $title }}</h2>
|
||||||
|
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $lead }}</p>
|
||||||
|
<p class="max-w-2xl text-amare-text-muted">{{ $body }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -2,20 +2,29 @@
|
|||||||
'settings',
|
'settings',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section aria-labelledby="method-heading" class="border-b border-amare-border bg-amare-bg-muted py-16">
|
@php
|
||||||
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] md:items-start">
|
$steps = filled($settings->method_steps) ? $settings->method_steps : \App\Models\SiteSetting::defaultMethodSteps();
|
||||||
|
$intro = $settings->method_intro ?: 'Clareza em cada etapa. Tranquilidade durante todo o processo.';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section aria-labelledby="method-heading" class="border-b border-amare-border bg-amare-bg-archive py-20">
|
||||||
|
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start">
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h2 id="method-heading" class="text-3xl font-semibold text-amare-text">Método de trabalho</h2>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Método</p>
|
||||||
<p class="text-amare-text-muted">Do briefing ao dia do evento, com clareza e acompanhamento próximo.</p>
|
<h2 id="method-heading" class="text-3xl font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
||||||
|
<p class="text-amare-text-muted">{{ $intro }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 text-amare-text-muted">
|
<ol class="grid gap-5 border-t border-amare-border">
|
||||||
<p>{{ $settings->about_summary }}</p>
|
@foreach ($steps as $index => $step)
|
||||||
<ol class="grid gap-3">
|
<li class="grid gap-2 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,1fr)]">
|
||||||
<li>1. Escuta e briefing inicial</li>
|
<span class="text-sm text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||||
<li>2. Planejamento e curadoria</li>
|
<div class="space-y-2">
|
||||||
<li>3. Coordenação no dia do evento</li>
|
<h3 class="text-2xl font-medium text-amare-text">{{ $step['title'] ?? '' }}</h3>
|
||||||
|
<p class="text-amare-text-muted">{{ $step['body'] ?? '' }}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
48
resources/views/components/home/portfolio.blade.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
@props([
|
||||||
|
'cases',
|
||||||
|
])
|
||||||
|
|
||||||
|
@if ($cases->isNotEmpty())
|
||||||
|
<section aria-labelledby="portfolio-heading" class="border-b border-amare-accent-deep bg-amare-accent-deep py-20 text-amare-accent-text">
|
||||||
|
<div class="container-amare space-y-10">
|
||||||
|
<div class="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent-text/80">Portfólio</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h2 id="portfolio-heading" class="text-3xl font-medium md:text-4xl">Celebrações que ganham forma com intenção.</h2>
|
||||||
|
<p class="max-w-2xl text-amare-accent-text/80">Recortes de eventos conduzidos com escuta, direção e presença em cada etapa.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-10 md:grid-cols-2">
|
||||||
|
@foreach ($cases as $case)
|
||||||
|
<article class="space-y-4 border-t border-amare-accent-text/30 pt-4">
|
||||||
|
@if (filled($case->cover_image_path))
|
||||||
|
<x-media.image
|
||||||
|
:path="$case->cover_image_path"
|
||||||
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
class="img-editorial aspect-[4/3] w-full object-cover"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
<div class="space-y-2">
|
||||||
|
<h3 class="text-2xl font-medium">{{ $case->title }}</h3>
|
||||||
|
<p class="text-amare-accent-text/80">{{ $case->summary }}</p>
|
||||||
|
<a href="{{ route('portfolio.show', $case->slug) }}" class="inline-flex border-b border-amare-accent-text pb-1 text-sm font-semibold transition-colors hover:text-amare-accent-text">
|
||||||
|
Ver caso
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 border-t border-amare-accent-text/30 pt-5 md:flex-row md:items-end md:justify-between">
|
||||||
|
<p class="max-w-2xl text-sm text-amare-accent-text/75">
|
||||||
|
Imagens demonstrativas enquanto o acervo autorizado da Amare está em organização.
|
||||||
|
</p>
|
||||||
|
<a href="{{ route('portfolio.index') }}" class="border-b border-amare-accent-text pb-1 text-sm font-semibold transition-colors hover:text-amare-accent-text">
|
||||||
|
Conhecer o portfólio
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
30
resources/views/components/home/positioning.blade.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$summary = $settings->about_summary ?: 'Assessoria para eventos em que cada escolha precisa fazer sentido para quem celebra e para quem recebe.';
|
||||||
|
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section aria-labelledby="positioning-heading" class="border-b border-amare-border py-20">
|
||||||
|
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">A Amare</p>
|
||||||
|
<h2 id="positioning-heading" class="text-3xl font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-8">
|
||||||
|
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
||||||
|
<ul class="grid gap-3 border-t border-amare-border">
|
||||||
|
@foreach ($principles as $principle)
|
||||||
|
<li class="border-b border-amare-border py-3 text-amare-text-muted">{{ $principle }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
<a href="{{ route('about') }}" class="border-b border-amare-accent pb-1 text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
||||||
|
Conhecer a Amare
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -3,24 +3,26 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($services->isNotEmpty())
|
@if ($services->isNotEmpty())
|
||||||
<section aria-labelledby="services-heading" class="border-b border-amare-border py-16">
|
<section aria-labelledby="services-heading" class="border-b border-amare-border py-20">
|
||||||
<div class="container-amare space-y-8">
|
<div class="container-amare space-y-10">
|
||||||
<div class="max-w-2xl space-y-3">
|
<div class="max-w-2xl space-y-3">
|
||||||
<h2 id="services-heading" class="text-3xl font-semibold text-amare-text">Serviços</h2>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Atuação</p>
|
||||||
<p class="text-amare-text-muted">Um resumo do que a assessoria pode conduzir com você.</p>
|
<h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2>
|
||||||
|
<p class="text-amare-text-muted">Assessoria sob medida para decisões importantes e celebrações bem conduzidas.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
<ol class="border-t border-amare-border">
|
||||||
@foreach ($services as $service)
|
@foreach ($services as $index => $service)
|
||||||
<article class="space-y-3 border-t border-amare-border pt-4">
|
<li class="grid gap-3 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,0.8fr)_minmax(0,1.2fr)] md:gap-6">
|
||||||
<h3 class="text-xl font-semibold text-amare-text">{{ $service->title }}</h3>
|
<span class="text-sm text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||||
|
<h3 class="text-2xl font-medium text-amare-text">{{ $service->title }}</h3>
|
||||||
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
||||||
</article>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</ol>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url('/servicos') }}" class="text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-hover">
|
<a href="{{ route('services.index') }}" class="border-b border-amare-accent pb-1 text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
||||||
Ver todos os serviços
|
Ver todos os serviços
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -12,8 +12,16 @@
|
|||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
@foreach ($testimonials as $testimonial)
|
@foreach ($testimonials as $testimonial)
|
||||||
|
@php
|
||||||
|
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
||||||
|
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
||||||
|
@endphp
|
||||||
<blockquote class="space-y-4 border-t border-amare-border pt-4">
|
<blockquote class="space-y-4 border-t border-amare-border pt-4">
|
||||||
<p class="text-lg text-amare-text">“{{ $testimonial->quote }}”</p>
|
<div class="space-y-3 text-lg text-amare-text">
|
||||||
|
@foreach ($paragraphs as $index => $paragraph)
|
||||||
|
<p>@if ($index === 0)“@endif{{ $paragraph }}@if ($index === count($paragraphs) - 1)”@endif</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
<footer class="text-sm text-amare-text-muted">
|
<footer class="text-sm text-amare-text-muted">
|
||||||
<cite class="not-italic font-semibold text-amare-text">{{ $testimonial->author_name }}</cite>
|
<cite class="not-italic font-semibold text-amare-text">{{ $testimonial->author_name }}</cite>
|
||||||
@if (filled($testimonial->context))
|
@if (filled($testimonial->context))
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare max-w-2xl space-y-6 py-8 text-center">
|
<div class="container-amare max-w-2xl space-y-6 py-20 text-center">
|
||||||
<p class="text-sm uppercase tracking-[0.18em] text-amare-accent">Erro 404</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Erro 404</p>
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Página não encontrada</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text">Página não encontrada</h1>
|
||||||
<p class="text-amare-text-muted">O endereço que você tentou abrir não existe ou foi movido.</p>
|
<p class="text-amare-muted">O endereço que você tentou abrir não existe ou foi movido.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('home') }}" class="inline-flex rounded-md bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text hover:bg-amare-accent-hover">
|
<a href="{{ route('home') }}" class="inline-flex bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare max-w-2xl space-y-6 py-8 text-center">
|
<div class="container-amare max-w-2xl space-y-6 py-20 text-center">
|
||||||
<p class="text-sm uppercase tracking-[0.18em] text-amare-accent">Erro 500</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Erro 500</p>
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Algo deu errado</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text">Algo deu errado</h1>
|
||||||
<p class="text-amare-text-muted">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
<p class="text-amare-muted">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('home') }}" class="inline-flex rounded-md bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text hover:bg-amare-accent-hover">
|
<a href="{{ route('home') }}" class="inline-flex bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -11,61 +11,101 @@
|
|||||||
<x-fonts />
|
<x-fonts />
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen antialiased">
|
<body class="min-h-screen bg-amare-bg font-serif text-amare-text antialiased">
|
||||||
<a href="#conteudo" class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:rounded-md focus:bg-amare-accent focus:px-4 focus:py-2 focus:text-amare-accent-text">
|
<a href="#conteudo" class="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-50 focus:bg-amare-accent focus:px-4 focus:py-2 focus:text-amare-accent-text">
|
||||||
Ir para o conteúdo
|
Ir para o conteúdo
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<header class="border-b border-amare-border bg-amare-bg">
|
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 backdrop-blur-sm">
|
||||||
<div class="container-amare flex items-center justify-between gap-6 py-4">
|
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr]">
|
||||||
<a href="{{ url('/') }}" class="text-lg font-semibold text-amare-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:text-amare-accent">
|
<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:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||||
{{ $siteSettings->brand_name }}
|
<a href="{{ route('home') }}" class="text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent">Início</a>
|
||||||
|
<a href="{{ route('services.index') }}" class="text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent">Serviços</a>
|
||||||
|
<a href="{{ route('portfolio.index') }}" class="text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent">Portfólio</a>
|
||||||
|
<a href="{{ route('about') }}" class="text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent">Amare</a>
|
||||||
|
<a href="{{ route('contact') }}" class="text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:hidden">Contato</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<a href="{{ route('home') }}" class="order-1 justify-self-start md:order-2 md:justify-self-center" aria-label="{{ $siteSettings->brand_name }} — página inicial">
|
||||||
|
<x-brand.logo variant="on-light" class="h-10 w-auto md:h-12" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav aria-label="Principal" class="flex flex-wrap items-center gap-4 text-sm text-amare-text-muted">
|
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
||||||
<a href="{{ route('home') }}" class="transition-colors hover:text-amare-accent">Início</a>
|
<a href="{{ route('contact') }}" class="hidden text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent transition-colors hover:text-amare-accent-deep md:inline-flex">
|
||||||
<a href="{{ route('services.index') }}" class="transition-colors hover:text-amare-accent">Serviços</a>
|
Solicitar proposta
|
||||||
<a href="{{ route('portfolio.index') }}" class="transition-colors hover:text-amare-accent">Portfólio</a>
|
</a>
|
||||||
<a href="{{ route('about') }}" class="transition-colors hover:text-amare-accent">Sobre</a>
|
|
||||||
<a href="{{ route('contact') }}" class="transition-colors hover:text-amare-accent">Contato</a>
|
<button
|
||||||
</nav>
|
type="button"
|
||||||
|
class="menu-button inline-flex h-10 w-10 items-center justify-center border border-amare-border text-amare-text md:hidden"
|
||||||
|
aria-label="Abrir menu"
|
||||||
|
aria-controls="main-nav"
|
||||||
|
aria-expanded="false"
|
||||||
|
data-menu-button
|
||||||
|
>
|
||||||
|
<span class="sr-only">Menu</span>
|
||||||
|
<span aria-hidden="true" class="flex w-4 flex-col gap-1">
|
||||||
|
<span class="block h-px w-full bg-current"></span>
|
||||||
|
<span class="block h-px w-full bg-current"></span>
|
||||||
|
<span class="block h-px w-full bg-current"></span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="conteudo" class="py-12">
|
<main id="conteudo">
|
||||||
@yield('content')
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="border-t border-amare-border bg-amare-bg-muted">
|
<footer class="border-t border-amare-border bg-amare-bg-deep">
|
||||||
<div class="container-amare flex flex-col gap-4 py-8 text-sm text-amare-text-muted md:flex-row md:items-start md:justify-between">
|
<div class="container-amare grid gap-10 py-12 md:grid-cols-[minmax(0,1.4fr)_repeat(2,minmax(0,1fr))]">
|
||||||
<div class="space-y-2">
|
<div class="space-y-4">
|
||||||
<p class="font-medium text-amare-text">{{ $siteSettings->brand_name }}</p>
|
<x-brand.logo variant="on-light" class="h-12 w-auto" />
|
||||||
|
<p class="max-w-md text-amare-muted">
|
||||||
|
{{ $siteSettings->about_summary ?: 'Assessoria, produção e organização de eventos sociais e corporativos em São Paulo - SP.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Navegação</h2>
|
||||||
|
<p><a href="{{ route('services.index') }}" class="text-amare-muted transition-colors hover:text-amare-accent">Serviços</a></p>
|
||||||
|
<p><a href="{{ route('portfolio.index') }}" class="text-amare-muted transition-colors hover:text-amare-accent">Portfólio</a></p>
|
||||||
|
<p><a href="{{ route('about') }}" class="text-amare-muted transition-colors hover:text-amare-accent">A Amare</a></p>
|
||||||
|
<p><a href="{{ route('contact') }}" class="text-amare-muted transition-colors hover:text-amare-accent">Contato</a></p>
|
||||||
|
<p><a href="{{ route('privacy') }}" class="text-amare-muted transition-colors hover:text-amare-accent">Política de privacidade</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-3 text-sm">
|
||||||
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Contato</h2>
|
||||||
|
@if ($siteSettings->city)
|
||||||
|
<p class="text-amare-muted">{{ $siteSettings->city }}</p>
|
||||||
|
@endif
|
||||||
@if ($siteSettings->email)
|
@if ($siteSettings->email)
|
||||||
<p>
|
<p>
|
||||||
<a href="mailto:{{ $siteSettings->email }}" class="transition-colors hover:text-amare-accent">{{ $siteSettings->email }}</a>
|
<a href="mailto:{{ $siteSettings->email }}" class="text-amare-muted transition-colors hover:text-amare-accent">{{ $siteSettings->email }}</a>
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@if ($siteSettings->phone)
|
@if ($siteSettings->phone)
|
||||||
<p>{{ $siteSettings->phone }}</p>
|
<p class="text-amare-muted">{{ $siteSettings->phone }}</p>
|
||||||
@endif
|
@endif
|
||||||
@if ($siteSettings->city)
|
|
||||||
<p>{{ $siteSettings->city }}</p>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<p class="font-medium text-amare-text">Links</p>
|
|
||||||
<p><a href="{{ route('privacy') }}" class="transition-colors hover:text-amare-accent">Política de privacidade</a></p>
|
|
||||||
@foreach ($siteSettings->social_links ?? [] as $network => $url)
|
@foreach ($siteSettings->social_links ?? [] as $network => $url)
|
||||||
@if (filled($url))
|
@if (filled($url))
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ $url }}" class="transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ ucfirst((string) $network) }}</a>
|
<a href="{{ $url }}" class="text-amare-muted transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ ucfirst((string) $network) }}</a>
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>© {{ now()->year }} {{ $siteSettings->brand_name }}. Todos os direitos reservados.</p>
|
<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">
|
||||||
|
<p>© {{ now()->year }} {{ $siteSettings->brand_name }}. Todos os direitos reservados.</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{ route('privacy') }}" class="transition-colors hover:text-amare-accent">Política de privacidade</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,33 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare max-w-3xl space-y-6">
|
@php
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Sobre</h1>
|
$principles = filled($siteSettings->principles)
|
||||||
<p class="text-lg text-amare-text-muted">{{ $siteSettings->about_summary }}</p>
|
? $siteSettings->principles
|
||||||
<p class="text-amare-text-muted">
|
: \App\Models\SiteSetting::defaultPrinciples();
|
||||||
A {{ $siteSettings->brand_name }} atua em {{ $siteSettings->city }} com foco em planejamento completo,
|
$city = $siteSettings->city ?: 'São Paulo - SP';
|
||||||
presença no dia do evento e uma condução serena do início ao fim.
|
@endphp
|
||||||
</p>
|
|
||||||
|
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg">
|
||||||
|
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:py-24">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
||||||
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Humana no cuidado. Precisa na entrega.</h1>
|
||||||
|
<p class="text-lg text-amare-muted">{{ $siteSettings->about_summary }}</p>
|
||||||
|
<p class="text-amare-muted">
|
||||||
|
A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo,
|
||||||
|
presença no dia do evento e uma condução serena do início ao fim.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare">
|
||||||
|
@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">
|
||||||
|
<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>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,28 +1,34 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare space-y-6">
|
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg">
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Contato</h1>
|
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)] md:py-24">
|
||||||
<p class="max-w-2xl text-amare-text-muted">
|
<div class="space-y-6">
|
||||||
Em breve você poderá enviar um briefing por aqui. Enquanto isso, fale conosco pelos canais abaixo.
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Vamos conversar</p>
|
||||||
</p>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Todo grande encontro começa com uma boa conversa.</h1>
|
||||||
<div class="space-y-2 text-amare-text-muted">
|
<p class="max-w-2xl text-lg text-amare-muted">
|
||||||
@if ($siteSettings->email)
|
Em breve você poderá enviar um briefing por aqui. Enquanto isso, fale conosco pelos canais abaixo.
|
||||||
<p><a href="mailto:{{ $siteSettings->email }}" class="text-amare-accent hover:text-amare-accent-hover">{{ $siteSettings->email }}</a></p>
|
</p>
|
||||||
@endif
|
</div>
|
||||||
@if ($siteSettings->phone)
|
|
||||||
<p>{{ $siteSettings->phone }}</p>
|
<div class="space-y-4 border-t border-amare-border pt-6 text-amare-muted md:border-t-0 md:border-l md:pt-0 md:pl-10">
|
||||||
@endif
|
<p>{{ $siteSettings->city ?: 'São Paulo - SP' }}</p>
|
||||||
@if ($siteSettings->city)
|
@if ($siteSettings->email)
|
||||||
<p>{{ $siteSettings->city }}</p>
|
|
||||||
@endif
|
|
||||||
@foreach ($siteSettings->social_links ?? [] as $network => $url)
|
|
||||||
@if (filled($url))
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ $url }}" class="text-amare-accent hover:text-amare-accent-hover" rel="noopener noreferrer" target="_blank">{{ ucfirst((string) $network) }}</a>
|
<a href="mailto:{{ $siteSettings->email }}" class="text-amare-accent transition-colors hover:text-amare-accent-deep">{{ $siteSettings->email }}</a>
|
||||||
</p>
|
</p>
|
||||||
@endif
|
@endif
|
||||||
@endforeach
|
@if ($siteSettings->phone)
|
||||||
|
<p>{{ $siteSettings->phone }}</p>
|
||||||
|
@endif
|
||||||
|
@foreach ($siteSettings->social_links ?? [] as $network => $url)
|
||||||
|
@if (filled($url))
|
||||||
|
<p>
|
||||||
|
<a href="{{ $url }}" class="text-amare-accent transition-colors hover:text-amare-accent-deep" rel="noopener noreferrer" target="_blank">{{ ucfirst((string) $network) }}</a>
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<x-home.hero :settings="$content->settings" />
|
<x-home.hero :settings="$content->settings" />
|
||||||
<x-home.proof :cases="$content->featuredCases" />
|
<x-home.manifesto :settings="$content->settings" />
|
||||||
<x-home.services :services="$content->featuredServices" />
|
<x-home.services :services="$content->featuredServices" />
|
||||||
|
<x-home.portfolio :cases="$content->featuredCases" />
|
||||||
<x-home.method :settings="$content->settings" />
|
<x-home.method :settings="$content->settings" />
|
||||||
<x-home.cases :cases="$content->featuredCases" />
|
|
||||||
<x-home.testimonials :testimonials="$content->testimonials" />
|
<x-home.testimonials :testimonials="$content->testimonials" />
|
||||||
|
<x-home.positioning :settings="$content->settings" />
|
||||||
<x-home.final-cta :settings="$content->settings" />
|
<x-home.final-cta :settings="$content->settings" />
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,39 +1,46 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare space-y-10">
|
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||||
<div class="max-w-2xl space-y-3">
|
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Portfólio</h1>
|
<div class="max-w-2xl space-y-4">
|
||||||
<p class="text-lg text-amare-text-muted">Casos reais de celebrações conduzidas com atenção a cada detalhe.</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
||||||
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Atmosferas que contam histórias.</h1>
|
||||||
|
<p class="text-lg text-amare-muted">Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($cases->isEmpty())
|
||||||
|
<p class="text-amare-muted">Em breve publicaremos novos casos.</p>
|
||||||
|
@else
|
||||||
|
<div class="grid gap-10 md:grid-cols-2">
|
||||||
|
@foreach ($cases as $case)
|
||||||
|
<article class="space-y-4">
|
||||||
|
@if (filled($case->cover_image_path))
|
||||||
|
<a href="{{ route('portfolio.show', $case->slug) }}" class="block overflow-hidden">
|
||||||
|
<x-media.image
|
||||||
|
:path="$case->cover_image_path"
|
||||||
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
class="img-editorial aspect-[4/3] w-full object-cover transition-transform duration-(--amare-duration-slow) ease-(--amare-ease-standard) motion-safe:hover:scale-[1.02]"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
<div class="space-y-2 border-t border-amare-border pt-4">
|
||||||
|
<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>
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-amare-muted">{{ $case->summary }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{{ $cases->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<p class="text-sm text-amare-accent">Imagens demonstrativas até existir acervo autorizado da Amare.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($cases->isEmpty())
|
|
||||||
<p class="text-amare-text-muted">Em breve publicaremos novos casos.</p>
|
|
||||||
@else
|
|
||||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
@foreach ($cases as $case)
|
|
||||||
<article class="space-y-3">
|
|
||||||
@if (filled($case->cover_image_path))
|
|
||||||
<a href="{{ route('portfolio.show', $case->slug) }}">
|
|
||||||
<x-media.image
|
|
||||||
:path="$case->cover_image_path"
|
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
|
||||||
class="aspect-[4/3] w-full object-cover"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
<h2 class="text-xl font-semibold text-amare-text">
|
|
||||||
<a href="{{ route('portfolio.show', $case->slug) }}" class="hover:text-amare-accent">{{ $case->title }}</a>
|
|
||||||
</h2>
|
|
||||||
<p class="text-sm text-amare-text-muted">{{ $case->summary }}</p>
|
|
||||||
</article>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{{ $cases->links() }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,62 +1,70 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<article class="container-amare space-y-10">
|
<article>
|
||||||
<header class="max-w-3xl space-y-4">
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
<p class="text-sm uppercase tracking-[0.18em] text-amare-accent">{{ $case->event_type }}</p>
|
<div class="container-amare space-y-8 py-16 md:py-24">
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">{{ $case->title }}</h1>
|
<header class="max-w-3xl space-y-4">
|
||||||
<p class="text-lg text-amare-text-muted">{{ $case->summary }}</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ $case->event_type }}</p>
|
||||||
<p class="text-sm text-amare-text-muted">
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">{{ $case->title }}</h1>
|
||||||
@if ($case->city){{ $case->city }}@endif
|
<p class="text-lg text-amare-muted">{{ $case->summary }}</p>
|
||||||
@if ($case->venue) · {{ $case->venue }}@endif
|
<p class="text-sm text-amare-muted">
|
||||||
@if ($case->event_date) · {{ $case->event_date->format('d/m/Y') }}@endif
|
@if ($case->city){{ $case->city }}@endif
|
||||||
</p>
|
@if ($case->venue) · {{ $case->venue }}@endif
|
||||||
</header>
|
@if ($case->event_date) · {{ $case->event_date->format('d/m/Y') }}@endif
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
@if (filled($case->cover_image_path))
|
@if (filled($case->cover_image_path))
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$case->cover_image_path"
|
:path="$case->cover_image_path"
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
sizes="(max-width: 1024px) 100vw, 72rem"
|
sizes="(max-width: 1024px) 100vw, 1120px"
|
||||||
class="aspect-[16/9] w-full object-cover"
|
class="img-editorial aspect-[16/9] w-full object-cover"
|
||||||
/>
|
/>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-8 md:grid-cols-3">
|
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||||
<section class="space-y-2">
|
<div class="container-amare grid gap-10 py-16 md:grid-cols-3">
|
||||||
<h2 class="text-xl font-semibold text-amare-text">Desafio</h2>
|
<section class="space-y-3">
|
||||||
<p class="text-amare-text-muted">{!! nl2br(e($case->challenge)) !!}</p>
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Desafio</h2>
|
||||||
</section>
|
<p class="text-amare-muted">{!! nl2br(e($case->challenge)) !!}</p>
|
||||||
<section class="space-y-2">
|
|
||||||
<h2 class="text-xl font-semibold text-amare-text">Solução</h2>
|
|
||||||
<p class="text-amare-text-muted">{!! nl2br(e($case->solution)) !!}</p>
|
|
||||||
</section>
|
|
||||||
@if (filled($case->result))
|
|
||||||
<section class="space-y-2">
|
|
||||||
<h2 class="text-xl font-semibold text-amare-text">Resultado</h2>
|
|
||||||
<p class="text-amare-text-muted">{!! nl2br(e($case->result)) !!}</p>
|
|
||||||
</section>
|
</section>
|
||||||
@endif
|
<section class="space-y-3">
|
||||||
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Solução</h2>
|
||||||
|
<p class="text-amare-muted">{!! nl2br(e($case->solution)) !!}</p>
|
||||||
|
</section>
|
||||||
|
@if (filled($case->result))
|
||||||
|
<section class="space-y-3">
|
||||||
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Resultado</h2>
|
||||||
|
<p class="text-amare-muted">{!! nl2br(e($case->result)) !!}</p>
|
||||||
|
</section>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($case->images->isNotEmpty())
|
@if ($case->images->isNotEmpty())
|
||||||
<section aria-labelledby="gallery-heading" class="space-y-6">
|
<section aria-labelledby="gallery-heading" class="bg-amare-bg">
|
||||||
<h2 id="gallery-heading" class="text-2xl font-semibold text-amare-text">Galeria</h2>
|
<div class="container-amare space-y-8 py-16">
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<h2 id="gallery-heading" class="text-3xl font-medium text-amare-text">Galeria</h2>
|
||||||
@foreach ($case->images as $image)
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<figure class="space-y-2">
|
@foreach ($case->images as $image)
|
||||||
<x-media.image
|
<figure class="space-y-2">
|
||||||
:path="$image->path"
|
<x-media.image
|
||||||
:alt="$image->alt_text"
|
:path="$image->path"
|
||||||
sizes="(max-width: 768px) 100vw, 33vw"
|
:alt="$image->alt_text"
|
||||||
class="aspect-square w-full object-cover"
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
/>
|
class="img-editorial aspect-[4/3] w-full object-cover"
|
||||||
@if (filled($image->caption))
|
/>
|
||||||
<figcaption class="text-sm text-amare-text-muted">{{ $image->caption }}</figcaption>
|
@if (filled($image->caption))
|
||||||
@endif
|
<figcaption class="text-sm text-amare-muted">{{ $image->caption }}</figcaption>
|
||||||
</figure>
|
@endif
|
||||||
@endforeach
|
</figure>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare max-w-3xl space-y-6">
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Política de privacidade</h1>
|
<div class="container-amare max-w-3xl space-y-6 py-16 md:py-24">
|
||||||
<p class="text-amare-text-muted">
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Privacidade</p>
|
||||||
A {{ $siteSettings->brand_name }} trata dados pessoais com responsabilidade e somente para finalidades
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text">Política de privacidade</h1>
|
||||||
relacionadas ao atendimento de interessados e à operação do site.
|
<p class="text-amare-muted">
|
||||||
</p>
|
A {{ $siteSettings->brand_name }} trata dados pessoais com responsabilidade e somente para finalidades
|
||||||
<p class="text-amare-text-muted">
|
relacionadas ao atendimento de interessados e à operação do site.
|
||||||
Para dúvidas sobre privacidade, escreva para
|
</p>
|
||||||
<a href="mailto:{{ $siteSettings->email }}" class="text-amare-accent hover:text-amare-accent-hover">{{ $siteSettings->email }}</a>.
|
<p class="text-amare-muted">
|
||||||
</p>
|
Para dúvidas sobre privacidade, escreva para
|
||||||
|
<a href="mailto:{{ $siteSettings->email }}" class="text-amare-accent transition-colors hover:text-amare-accent-deep">{{ $siteSettings->email }}</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -1,36 +1,44 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container-amare space-y-10">
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
<div class="max-w-2xl space-y-3">
|
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||||
<h1 class="text-4xl font-semibold text-amare-text">Serviços</h1>
|
<div class="max-w-2xl space-y-4">
|
||||||
<p class="text-lg text-amare-text-muted">Assessoria completa para casamentos e eventos corporativos.</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
|
||||||
</div>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Uma mesma excelência, diferentes ocasiões.</h1>
|
||||||
|
<p class="text-lg text-amare-muted">O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento.</p>
|
||||||
@if ($services->isEmpty())
|
|
||||||
<p class="text-amare-text-muted">Em breve publicaremos o catálogo de serviços.</p>
|
|
||||||
@else
|
|
||||||
<div class="grid gap-8 md:grid-cols-2">
|
|
||||||
@foreach ($services as $service)
|
|
||||||
<article class="space-y-3 border-t border-amare-border pt-6">
|
|
||||||
@if (filled($service->cover_image_path))
|
|
||||||
<x-media.image
|
|
||||||
:path="$service->cover_image_path"
|
|
||||||
:alt="$service->cover_image_alt ?: $service->title"
|
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
|
||||||
class="aspect-[16/10] w-full object-cover"
|
|
||||||
/>
|
|
||||||
@endif
|
|
||||||
<h2 class="text-2xl font-semibold text-amare-text">{{ $service->title }}</h2>
|
|
||||||
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
|
||||||
@if (filled($service->description))
|
|
||||||
<div class="prose prose-amare max-w-none text-amare-text-muted">
|
|
||||||
{!! nl2br(e($service->description)) !!}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</article>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
</div>
|
||||||
@endif
|
|
||||||
|
@if ($services->isEmpty())
|
||||||
|
<p class="text-amare-muted">Em breve publicaremos o catálogo de serviços.</p>
|
||||||
|
@else
|
||||||
|
<div class="divide-y divide-amare-border border-y border-amare-border">
|
||||||
|
@foreach ($services as $index => $service)
|
||||||
|
<article class="grid gap-4 py-8 md:grid-cols-[5rem_minmax(0,1fr)_minmax(0,1.2fr)] md:items-start">
|
||||||
|
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h2 class="text-2xl font-medium text-amare-text md:text-3xl">{{ $service->title }}</h2>
|
||||||
|
<p class="text-amare-muted">{{ $service->summary }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
@if (filled($service->cover_image_path))
|
||||||
|
<x-media.image
|
||||||
|
:path="$service->cover_image_path"
|
||||||
|
:alt="$service->cover_image_alt ?: $service->title"
|
||||||
|
sizes="(max-width: 768px) 100vw, 40vw"
|
||||||
|
class="img-editorial aspect-[16/10] w-full object-cover"
|
||||||
|
/>
|
||||||
|
@endif
|
||||||
|
@if (filled($service->description))
|
||||||
|
<div class="text-amare-muted">
|
||||||
|
{!! nl2br(e($service->description)) !!}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ it('has no critical or serious accessibility issues on covered public routes', f
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('exposes brand mark and operable mobile menu toggle', function (): void {
|
||||||
|
$page = $this->visit('/');
|
||||||
|
|
||||||
|
$brandAlt = $page->script('() => document.querySelector("img.brand-logo")?.getAttribute("alt")');
|
||||||
|
expect(strtolower((string) $brandAlt))->toContain('amare');
|
||||||
|
|
||||||
|
$page->resize(390, 844);
|
||||||
|
|
||||||
|
$expandedBefore = $page->script('() => document.querySelector("[data-menu-button]")?.getAttribute("aria-expanded")');
|
||||||
|
expect($expandedBefore)->toBe('false');
|
||||||
|
|
||||||
|
$page->click('[data-menu-button]');
|
||||||
|
|
||||||
|
$expandedAfter = $page->script('() => document.querySelector("[data-menu-button]")?.getAttribute("aria-expanded")');
|
||||||
|
expect($expandedAfter)->toBe('true');
|
||||||
|
|
||||||
|
$navVisible = $page->script('() => {
|
||||||
|
const nav = document.querySelector("[data-main-nav]");
|
||||||
|
if (!nav) return false;
|
||||||
|
const style = getComputedStyle(nav);
|
||||||
|
return style.display !== "none" && style.visibility !== "hidden";
|
||||||
|
}');
|
||||||
|
expect($navVisible)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
it('reaches the primary CTA by keyboard and activates it', function (): void {
|
it('reaches the primary CTA by keyboard and activates it', function (): void {
|
||||||
$page = $this->visit('/');
|
$page = $this->visit('/');
|
||||||
|
|
||||||
|
|||||||
@@ -126,4 +126,106 @@ class SiteSettingsTest extends TestCase
|
|||||||
$this->assertSame('content/og/og-default.jpg', $settings->default_og_image_path);
|
$this->assertSame('content/og/og-default.jpg', $settings->default_og_image_path);
|
||||||
Storage::disk('public')->assertExists('content/og/og-default.jpg');
|
Storage::disk('public')->assertExists('content/og/og-default.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_content_seeder_uses_sao_paulo_and_editorial_fields(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
$this->seed(ContentSeeder::class);
|
||||||
|
|
||||||
|
$settings = SiteSetting::query()->sole();
|
||||||
|
|
||||||
|
$this->assertStringContainsStringIgnoringCase('São Paulo', (string) $settings->city);
|
||||||
|
$this->assertStringNotContainsStringIgnoringCase('Fortaleza', (string) $settings->city);
|
||||||
|
$this->assertNotEmpty($settings->manifesto_title);
|
||||||
|
$this->assertNotEmpty($settings->manifesto_lead);
|
||||||
|
$this->assertNotEmpty($settings->manifesto_body);
|
||||||
|
$this->assertIsArray($settings->method_steps);
|
||||||
|
$this->assertCount(4, $settings->method_steps);
|
||||||
|
$this->assertIsArray($settings->principles);
|
||||||
|
$this->assertGreaterThanOrEqual(1, count($settings->principles));
|
||||||
|
$this->assertNotEmpty($settings->hero_note);
|
||||||
|
$this->assertNotEmpty($settings->hero_secondary_cta_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_can_save_editorial_site_settings_fields(): void
|
||||||
|
{
|
||||||
|
$admin = User::factory()->admin()->create();
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test(ManageSiteSettings::class)
|
||||||
|
->set('data.hero_secondary_cta_label', 'Conheça nosso olhar')
|
||||||
|
->set('data.hero_note', 'Planejamento cuidadoso do primeiro encontro ao último detalhe.')
|
||||||
|
->set('data.manifesto_title', 'Sofisticação que também se traduz em organização.')
|
||||||
|
->set('data.manifesto_lead', 'Um evento memorável não nasce apenas de uma boa estética.')
|
||||||
|
->set('data.manifesto_body', 'A Amare combina sensibilidade e precisão.')
|
||||||
|
->set('data.method_intro', 'Clareza em cada etapa.')
|
||||||
|
->set('data.method_steps', [
|
||||||
|
['title' => 'Escuta', 'body' => 'Entendimento do contexto.'],
|
||||||
|
['title' => 'Direção', 'body' => 'Definição de escopo.'],
|
||||||
|
['title' => 'Produção', 'body' => 'Coordenação de cronograma.'],
|
||||||
|
['title' => 'Execução', 'body' => 'Presença atenta no evento.'],
|
||||||
|
])
|
||||||
|
->set('data.principles', [
|
||||||
|
'Personalização sem complicação desnecessária',
|
||||||
|
'Comunicação clara e decisões bem orientadas',
|
||||||
|
'Atenção à experiência de clientes e convidados',
|
||||||
|
'Execução responsável do início ao fim',
|
||||||
|
])
|
||||||
|
->set('data.city', 'São Paulo - SP')
|
||||||
|
->call('save')
|
||||||
|
->assertHasNoFormErrors();
|
||||||
|
|
||||||
|
$settings = SiteSetting::instance()->refresh();
|
||||||
|
|
||||||
|
$this->assertSame('Conheça nosso olhar', $settings->hero_secondary_cta_label);
|
||||||
|
$this->assertSame('São Paulo - SP', $settings->city);
|
||||||
|
$this->assertCount(4, $settings->method_steps);
|
||||||
|
$this->assertSame('Escuta', $settings->method_steps[0]['title']);
|
||||||
|
$this->assertCount(4, $settings->principles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_logo_upload_requires_alt_text(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
$admin = User::factory()->admin()->create();
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test(ManageSiteSettings::class)
|
||||||
|
->set('data.logo_path', [
|
||||||
|
UploadedFile::fake()->image('logo.png', 400, 400),
|
||||||
|
])
|
||||||
|
->set('data.logo_alt', null)
|
||||||
|
->call('save')
|
||||||
|
->assertHasFormErrors(['logo_alt' => 'required']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_admin_can_upload_logo_with_alt_text(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
$admin = User::factory()->admin()->create();
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$this->actingAs($admin);
|
||||||
|
|
||||||
|
Livewire::test(ManageSiteSettings::class)
|
||||||
|
->set('data.logo_path', [
|
||||||
|
UploadedFile::fake()->image('logo.png', 400, 400),
|
||||||
|
])
|
||||||
|
->set('data.logo_alt', 'Amare Assessoria')
|
||||||
|
->call('save')
|
||||||
|
->assertHasNoFormErrors();
|
||||||
|
|
||||||
|
$settings = SiteSetting::instance()->refresh();
|
||||||
|
|
||||||
|
$this->assertSame('Amare Assessoria', $settings->logo_alt);
|
||||||
|
$this->assertNotNull($settings->logo_path);
|
||||||
|
Storage::disk('public')->assertExists($settings->logo_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Feature\Marketing;
|
namespace Tests\Feature\Marketing;
|
||||||
|
|
||||||
use App\Filament\Resources\Testimonials\Pages\ListTestimonials;
|
use App\Filament\Resources\Testimonials\Pages\ListTestimonials;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Database\Seeders\ContentSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -44,4 +47,54 @@ class TestimonialsTest extends TestCase
|
|||||||
Livewire::test(ListTestimonials::class)
|
Livewire::test(ListTestimonials::class)
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
|
||||||
|
$this->seed(ContentSeeder::class);
|
||||||
|
|
||||||
|
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
||||||
|
|
||||||
|
$this->assertSame([
|
||||||
|
'Jeniffer e Maick',
|
||||||
|
'Quesia e Jhonata',
|
||||||
|
'Milena e Weslley',
|
||||||
|
'Raquel e Pedro',
|
||||||
|
'Victoria e Pedro',
|
||||||
|
], $authors);
|
||||||
|
|
||||||
|
$this->assertNull(
|
||||||
|
Testimonial::query()->where('author_name', 'Ana Souza')->first(),
|
||||||
|
'Fictional demo authors must not remain after seeding real testimonials.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
||||||
|
|
||||||
|
$this->assertNotNull($jeniffer);
|
||||||
|
$this->assertStringContainsString('Mi, quero agradecer', (string) $jeniffer->quote);
|
||||||
|
$this->assertStringContainsString("\n\n", (string) $jeniffer->quote);
|
||||||
|
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
Testimonial::factory()->published()->featured()->create([
|
||||||
|
'quote' => "Primeiro parágrafo do depoimento.\n\nSegundo parágrafo com continuidade.",
|
||||||
|
'author_name' => 'Casal Exemplo',
|
||||||
|
'context' => 'Casamento · 01/01/2026',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('Primeiro parágrafo do depoimento.', false);
|
||||||
|
$response->assertSee('Segundo parágrafo com continuidade.', false);
|
||||||
|
$response->assertDontSee(
|
||||||
|
'Primeiro parágrafo do depoimento. Segundo parágrafo com continuidade.',
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,27 @@ class AccessibilityStructureTest extends TestCase
|
|||||||
$this->assertMatchesRegularExpression('/<main\b/i', $html);
|
$this->assertMatchesRegularExpression('/<main\b/i', $html);
|
||||||
$this->assertMatchesRegularExpression('/<footer\b/i', $html);
|
$this->assertMatchesRegularExpression('/<footer\b/i', $html);
|
||||||
$this->assertStringContainsString('href="#conteudo"', $html);
|
$this->assertStringContainsString('href="#conteudo"', $html);
|
||||||
|
$this->assertStringContainsString('Ir para o conteúdo', $html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_header_exposes_brand_mark_and_mobile_menu_toggle(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'brand_name' => 'Amare Assessoria',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = $this->get(route('home'))->assertOk()->getContent();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('class="brand-logo', $html);
|
||||||
|
$this->assertMatchesRegularExpression('/alt="[^"]*Amare[^"]*"/i', $html);
|
||||||
|
$this->assertStringContainsString('id="main-nav"', $html);
|
||||||
|
$this->assertStringContainsString('aria-controls="main-nav"', $html);
|
||||||
|
$this->assertStringContainsString('aria-expanded="false"', $html);
|
||||||
|
$this->assertStringContainsString('data-menu-button', $html);
|
||||||
|
$this->assertStringContainsString('Abrir menu', $html);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_content_images_expose_alt_text(): void
|
public function test_content_images_expose_alt_text(): void
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
Storage::fake('public');
|
||||||
|
|||||||
75
tests/Feature/PublicSite/HeritageEditorialTokensTest.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class HeritageEditorialTokensTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_tokens_define_heritage_editorial_palette_and_layout_contracts(): void
|
||||||
|
{
|
||||||
|
$tokens = (string) file_get_contents(resource_path('css/tokens.css'));
|
||||||
|
|
||||||
|
$this->assertStringContainsString('--amare-color-bg: #FBF9F4', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-bg-deep: #F0EEE9', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-bg-archive: #E4E2DD', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-accent: #556B2F', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-accent-deep: #3E5219', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-sage: #8B9D77', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-text: #1B1C19', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-muted: #5D6155', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-color-border: #C5C8B8', $tokens);
|
||||||
|
|
||||||
|
$this->assertStringContainsString('EB Garamond', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-radius-sm: 0', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-radius-md: 0', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-radius-lg: 0', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-radius-xl: 0', $tokens);
|
||||||
|
$this->assertStringContainsString('--amare-container-max: 1120px', $tokens);
|
||||||
|
|
||||||
|
$this->assertStringNotContainsString('--amare-shadow-sm:', $tokens);
|
||||||
|
$this->assertStringNotContainsString('--amare-shadow-md:', $tokens);
|
||||||
|
$this->assertStringNotContainsString('--amare-shadow-lg:', $tokens);
|
||||||
|
$this->assertStringNotContainsString('Instrument Sans', $tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_theme_mapping_exposes_heritage_tokens_without_card_shadows(): void
|
||||||
|
{
|
||||||
|
$appCss = (string) file_get_contents(resource_path('css/app.css'));
|
||||||
|
|
||||||
|
$this->assertStringContainsString('--color-amare-bg-deep: var(--amare-color-bg-deep)', $appCss);
|
||||||
|
$this->assertStringContainsString('--color-amare-sage: var(--amare-color-sage)', $appCss);
|
||||||
|
$this->assertStringContainsString('--color-amare-accent-deep: var(--amare-color-accent-deep)', $appCss);
|
||||||
|
$this->assertStringContainsString('--color-amare-muted: var(--amare-color-muted)', $appCss);
|
||||||
|
$this->assertStringNotContainsString('--shadow-amare-sm:', $appCss);
|
||||||
|
$this->assertStringNotContainsString('--shadow-amare-md:', $appCss);
|
||||||
|
$this->assertStringNotContainsString('--shadow-amare-lg:', $appCss);
|
||||||
|
$this->assertStringContainsString('font-family: var(--amare-font-serif)', $appCss);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_vite_self_hosts_eb_garamond(): void
|
||||||
|
{
|
||||||
|
$vite = (string) file_get_contents(base_path('vite.config.js'));
|
||||||
|
|
||||||
|
$this->assertStringContainsString("bunny('EB Garamond'", $vite);
|
||||||
|
$this->assertStringNotContainsString('Instrument Sans', $vite);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_brand_logo_assets_exist_for_light_and_dark_fields(): void
|
||||||
|
{
|
||||||
|
foreach ([
|
||||||
|
'public/brand/lockup-on-light.webp',
|
||||||
|
'public/brand/lockup-on-dark.webp',
|
||||||
|
'public/brand/mark-on-light.webp',
|
||||||
|
'public/brand/mark-on-dark.webp',
|
||||||
|
'public/brand/lockup-on-light.png',
|
||||||
|
'public/brand/lockup-on-dark.png',
|
||||||
|
] as $relativePath) {
|
||||||
|
$this->assertFileExists(base_path($relativePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertFileExists(resource_path('views/components/brand/logo.blade.php'));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ class HomePageContentTest extends TestCase
|
|||||||
$settings->update([
|
$settings->update([
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar orçamento',
|
||||||
|
'manifesto_title' => 'Manifesto Amare',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Service::factory()->create([
|
Service::factory()->create([
|
||||||
@@ -73,12 +74,23 @@ class HomePageContentTest extends TestCase
|
|||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Celebrações com propósito')
|
->assertSee('Celebrações com propósito')
|
||||||
|
->assertSeeInOrder([
|
||||||
|
'id="hero-heading"',
|
||||||
|
'id="manifesto-heading"',
|
||||||
|
'id="services-heading"',
|
||||||
|
'id="portfolio-heading"',
|
||||||
|
'id="method-heading"',
|
||||||
|
'id="testimonials-heading"',
|
||||||
|
'id="positioning-heading"',
|
||||||
|
'id="final-cta-heading"',
|
||||||
|
], false)
|
||||||
->assertSeeInOrder(['Serviço A', 'Serviço B'])
|
->assertSeeInOrder(['Serviço A', 'Serviço B'])
|
||||||
->assertSeeInOrder(['Caso A', 'Caso B'])
|
->assertSeeInOrder(['Caso A', 'Caso B'])
|
||||||
->assertSeeInOrder(['Autor A', 'Autor B'])
|
->assertSeeInOrder(['Autor A', 'Autor B'])
|
||||||
->assertDontSee('Serviço Rascunho')
|
->assertDontSee('Serviço Rascunho')
|
||||||
->assertDontSee('Caso Rascunho')
|
->assertDontSee('Caso Rascunho')
|
||||||
->assertDontSee('Autor Rascunho')
|
->assertDontSee('Autor Rascunho')
|
||||||
|
->assertSee('data-testid="home-primary-cta"', false)
|
||||||
->assertSee('href="'.route('contact').'"', false);
|
->assertSee('href="'.route('contact').'"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,10 +103,11 @@ class HomePageContentTest extends TestCase
|
|||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('id="services-heading"', false)
|
->assertDontSee('id="services-heading"', false)
|
||||||
->assertDontSee('id="proof-heading"', false)
|
->assertDontSee('id="portfolio-heading"', false)
|
||||||
->assertDontSee('id="cases-heading"', false)
|
|
||||||
->assertDontSee('id="testimonials-heading"', false)
|
->assertDontSee('id="testimonials-heading"', false)
|
||||||
|
->assertSee('id="manifesto-heading"', false)
|
||||||
->assertSee('id="method-heading"', false)
|
->assertSee('id="method-heading"', false)
|
||||||
|
->assertSee('id="positioning-heading"', false)
|
||||||
->assertSee('id="final-cta-heading"', false);
|
->assertSee('id="final-cta-heading"', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class PublicLayoutSeoTest extends TestCase
|
|||||||
$settings->update([
|
$settings->update([
|
||||||
'brand_name' => 'Amare Brand',
|
'brand_name' => 'Amare Brand',
|
||||||
'email' => 'hello@amare.test',
|
'email' => 'hello@amare.test',
|
||||||
'phone' => '(85) 91111-1111',
|
'phone' => '(11) 91111-1111',
|
||||||
'city' => 'Fortaleza, CE',
|
'city' => 'São Paulo - SP',
|
||||||
'default_meta_title' => 'Titulo SEO Amare',
|
'default_meta_title' => 'Titulo SEO Amare',
|
||||||
'default_meta_description' => 'Descricao SEO Amare',
|
'default_meta_description' => 'Descricao SEO Amare',
|
||||||
'default_og_image_path' => 'og/default.jpg',
|
'default_og_image_path' => 'og/default.jpg',
|
||||||
@@ -53,7 +53,7 @@ class PublicLayoutSeoTest extends TestCase
|
|||||||
->assertSee('<footer', false)
|
->assertSee('<footer', false)
|
||||||
->assertSee('Amare Brand')
|
->assertSee('Amare Brand')
|
||||||
->assertSee('hello@amare.test')
|
->assertSee('hello@amare.test')
|
||||||
->assertSee('(85) 91111-1111')
|
->assertSee('(11) 91111-1111')
|
||||||
->assertSee('Política de privacidade')
|
->assertSee('Política de privacidade')
|
||||||
->assertSee('https://instagram.com/amare', false);
|
->assertSee('https://instagram.com/amare', false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,25 +108,43 @@ class PublicPagesTest extends TestCase
|
|||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
$settings->update([
|
$settings->update([
|
||||||
'about_summary' => 'Sobre a Amare boutique',
|
'about_summary' => 'Sobre a Amare boutique',
|
||||||
'email' => 'contato@amare.test',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(85) 90000-0000',
|
'phone' => '(11) 90000-0000',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => ['instagram' => 'https://instagram.com/amare'],
|
'social_links' => ['instagram' => 'https://instagram.com/amare'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$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')
|
||||||
|
->assertDontSee('Fortaleza')
|
||||||
|
->assertSee('min-h-[calc(100dvh-14rem)]', false);
|
||||||
|
|
||||||
$this->get(route('privacy'))
|
$this->get(route('privacy'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Política de privacidade')
|
->assertSee('Política de privacidade')
|
||||||
->assertSee('contato@amare.test');
|
->assertSee('amareassessoriaeventos@gmail.com');
|
||||||
|
|
||||||
$this->get(route('contact'))
|
$this->get(route('contact'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('contato@amare.test')
|
->assertSee('amareassessoriaeventos@gmail.com')
|
||||||
->assertSee('(85) 90000-0000')
|
->assertSee('(11) 90000-0000')
|
||||||
->assertSee('https://instagram.com/amare', false);
|
->assertSee('São Paulo - SP')
|
||||||
|
->assertDontSee('Fortaleza')
|
||||||
|
->assertSee('https://instagram.com/amare', false)
|
||||||
|
->assertSee('min-h-[calc(100dvh-14rem)]', false)
|
||||||
|
->assertDontSee('<form', false)
|
||||||
|
->assertDontSee('</form>', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_site_setting_defaults_use_sao_paulo_and_official_email(): void
|
||||||
|
{
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
|
$this->assertSame('São Paulo - SP', $settings->city);
|
||||||
|
$this->assertSame('amareassessoriaeventos@gmail.com', $settings->email);
|
||||||
|
$this->assertStringNotContainsStringIgnoringCase('Fortaleza', (string) $settings->city);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_branded_404_and_500_without_stack_trace_when_debug_disabled(): void
|
public function test_branded_404_and_500_without_stack_trace_when_debug_disabled(): void
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class StructuredDataTest extends TestCase
|
|||||||
SiteSetting::instance()->update([
|
SiteSetting::instance()->update([
|
||||||
'brand_name' => 'Amare Brand',
|
'brand_name' => 'Amare Brand',
|
||||||
'email' => 'hello@amare.test',
|
'email' => 'hello@amare.test',
|
||||||
'phone' => '(85) 91111-1111',
|
'phone' => '(11) 91111-1111',
|
||||||
'city' => 'Fortaleza, CE',
|
'city' => 'São Paulo - SP',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$html = $this->get(route('home'))->assertOk()->getContent();
|
$html = $this->get(route('home'))->assertOk()->getContent();
|
||||||
@@ -29,8 +29,8 @@ class StructuredDataTest extends TestCase
|
|||||||
$this->assertSame('Organization', $jsonLd['@type']);
|
$this->assertSame('Organization', $jsonLd['@type']);
|
||||||
$this->assertSame('Amare Brand', $jsonLd['name']);
|
$this->assertSame('Amare Brand', $jsonLd['name']);
|
||||||
$this->assertSame('hello@amare.test', $jsonLd['email']);
|
$this->assertSame('hello@amare.test', $jsonLd['email']);
|
||||||
$this->assertSame('(85) 91111-1111', $jsonLd['telephone']);
|
$this->assertSame('(11) 91111-1111', $jsonLd['telephone']);
|
||||||
$this->assertSame('Fortaleza, CE', $jsonLd['address']['addressLocality']);
|
$this->assertSame('São Paulo - SP', $jsonLd['address']['addressLocality']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_portfolio_show_emits_parseable_article_json_ld(): void
|
public function test_portfolio_show_emits_parseable_article_json_ld(): void
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
|||||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||||
refresh: true,
|
refresh: true,
|
||||||
fonts: [
|
fonts: [
|
||||||
bunny('Instrument Sans', {
|
bunny('EB Garamond', {
|
||||||
weights: [400, 500, 600],
|
weights: [400, 500, 600],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||