Compare commits
14 Commits
feat/man-1
...
feat/servi
| Author | SHA1 | Date | |
|---|---|---|---|
| 623e45cbea | |||
| 455bc4b7d8 | |||
| 3f55180b73 | |||
| 1425d7aaa1 | |||
| 62a19147ad | |||
| 90632ada88 | |||
| ce90cedb18 | |||
| a7d72f3756 | |||
| 0e0724c1cc | |||
| eca261ceba | |||
| ad29ce54d8 | |||
| 0d1ca40c11 | |||
| 484ac77ce7 | |||
| d0e508b237 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -26,3 +26,6 @@ Homestead.json
|
|||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
.worktrees/
|
.worktrees/
|
||||||
|
|
||||||
|
# Pest Browser visual diffs (local/CI artifacts)
|
||||||
|
/tests/Browser/Screenshots/
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ Hooks live in `.husky/` and auto-install on any plain `npm install` via the `pre
|
|||||||
|
|
||||||
Follow PSR-4 and Laravel conventions: PascalCase classes, camelCase methods, and snake_case database columns. Use four spaces (two in YAML, except four in Compose files), LF endings, and UTF-8 as defined by `.editorconfig`. Every project-owned PHP file must place `declare(strict_types=1);` immediately after `<?php`. Keep domain code independent of Filament and Livewire. Run `composer pint` to format and `composer phpstan` before review.
|
Follow PSR-4 and Laravel conventions: PascalCase classes, camelCase methods, and snake_case database columns. Use four spaces (two in YAML, except four in Compose files), LF endings, and UTF-8 as defined by `.editorconfig`. Every project-owned PHP file must place `declare(strict_types=1);` immediately after `<?php`. Keep domain code independent of Filament and Livewire. Run `composer pint` to format and `composer phpstan` before review.
|
||||||
|
|
||||||
|
## Design Principles: DRY & YAGNI
|
||||||
|
|
||||||
|
Write for the problem at hand, not an imagined future. **DRY**: extract and reuse a piece of logic as soon as it is genuinely duplicated in more than one place — but not before. **YAGNI**: do not add abstraction, configurability, or layers speculatively; add them only when a concrete requirement demands it. Prefer the simplest thing that solves the current requirement. Duplication that appears once is not yet a reason to abstract — wait for a second real occurrence before generalizing. This repo already encodes YAGNI in `openspec/config.yaml` (no generic repositories / `BaseService`); keep that spirit in new code. Avoid over-engineering and avoid premature extraction.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Name files by behavior, ending in `Test.php`, and add tests in the suite matching the changed layer. Feature tests use `RefreshDatabase`. Add architecture coverage for dependency-boundary changes. No numeric coverage threshold is enforced, but changed behavior must have regression coverage.
|
Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Tests are verification, not a design driver — write them to cover behavior you've already implemented, matching the layer you changed. Name files by behavior, ending in `Test.php`, and add tests in the suite matching the changed layer. Feature tests use `RefreshDatabase`. Add architecture coverage for dependency-boundary changes. No numeric coverage threshold is enforced; add regression tests where a bug was fixed or behavior is non-obvious, without making tests a front-loaded design ceremony.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ If the import above did not load, read `AGENTS.md` at the repo root now — it i
|
|||||||
- Every project-owned PHP file starts with `declare(strict_types=1);` immediately after `<?php`.
|
- Every project-owned PHP file starts with `declare(strict_types=1);` immediately after `<?php`.
|
||||||
- Browser tests are CI-only (they run against a FrankenPHP container built by `docker build`, not `artisan serve`).
|
- Browser tests are CI-only (they run against a FrankenPHP container built by `docker build`, not `artisan serve`).
|
||||||
|
|
||||||
|
## Design principles: DRY & YAGNI (not TDD)
|
||||||
|
|
||||||
|
Write for the problem at hand, not an imagined future. **DRY**: extract and reuse only once logic is genuinely duplicated in more than one place. **YAGNI**: no speculative abstraction, configurability, or layers — add them only when a concrete requirement demands it. Prefer the simplest thing that solves the current requirement; avoid over-engineering and premature extraction. Tests are verification, not a design driver: write them to cover behavior already implemented, not as a front-loaded TDD ceremony.
|
||||||
|
|
||||||
## Environment note
|
## Environment note
|
||||||
|
|
||||||
PHP and Composer are **not on PATH** in this environment, and `vendor/` and `node_modules/` are absent. Every `composer …` / `php artisan …` command in `AGENTS.md` and `README.md` assumes a PHP 8.4+ runtime with Composer 2 installed. Verify the toolchain before promising a command ran.
|
PHP and Composer are **not on PATH** in this environment, and `vendor/` and `node_modules/` are absent. Every `composer …` / `php artisan …` command in `AGENTS.md` and `README.md` assumes a PHP 8.4+ runtime with Composer 2 installed. Verify the toolchain before promising a command ran.
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ final readonly class HomeContent
|
|||||||
* @param Collection<int, Service> $featuredServices
|
* @param Collection<int, Service> $featuredServices
|
||||||
* @param Collection<int, PortfolioCase> $featuredCases
|
* @param Collection<int, PortfolioCase> $featuredCases
|
||||||
* @param Collection<int, Testimonial> $testimonials
|
* @param Collection<int, Testimonial> $testimonials
|
||||||
* @param Collection<int, WeddingPackage> $weddingPackages
|
* @param Collection<int, WeddingPackage> $packages
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public SiteSetting $settings,
|
public SiteSetting $settings,
|
||||||
public Collection $featuredServices,
|
public Collection $featuredServices,
|
||||||
public Collection $featuredCases,
|
public Collection $featuredCases,
|
||||||
public Collection $testimonials,
|
public Collection $testimonials,
|
||||||
public Collection $weddingPackages,
|
public Collection $packages,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class GetHomeContent
|
|||||||
->published()
|
->published()
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->get(),
|
->get(),
|
||||||
weddingPackages: WeddingPackage::query()
|
packages: WeddingPackage::query()
|
||||||
->published()
|
->published()
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->get(),
|
->get(),
|
||||||
|
|||||||
@@ -212,6 +212,26 @@ class ManageSiteSettings extends Page
|
|||||||
->reorderable()
|
->reorderable()
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
]),
|
]),
|
||||||
|
Section::make('Corporate')
|
||||||
|
->description('Etapas apresentadas na frente Amare Corporate da home.')
|
||||||
|
->schema([
|
||||||
|
Repeater::make('corporate_steps')
|
||||||
|
->label('Etapas do serviço corporativo')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('title')
|
||||||
|
->label('Título')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('body')
|
||||||
|
->label('Descrição')
|
||||||
|
->required()
|
||||||
|
->rows(2),
|
||||||
|
])
|
||||||
|
->defaultItems(3)
|
||||||
|
->maxItems(6)
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
Section::make('Princípios')
|
Section::make('Princípios')
|
||||||
->schema([
|
->schema([
|
||||||
TagsInput::make('principles')
|
TagsInput::make('principles')
|
||||||
|
|||||||
@@ -5,15 +5,9 @@ declare(strict_types=1);
|
|||||||
namespace App\Filament\Resources\WeddingPackages\Pages;
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
use Filament\Actions\DeleteAction;
|
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
class EditWeddingPackage extends EditRecord
|
class EditWeddingPackage extends EditRecord
|
||||||
{
|
{
|
||||||
protected static string $resource = WeddingPackageResource::class;
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
|
||||||
{
|
|
||||||
return [DeleteAction::make()];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class ListWeddingPackages extends ListRecords
|
|||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [CreateAction::make()];
|
return [
|
||||||
|
CreateAction::make(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
class WeddingPackageForm
|
||||||
|
{
|
||||||
|
public static function configure(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema
|
||||||
|
->components([
|
||||||
|
TextInput::make('name')
|
||||||
|
->label('Nome da modalidade')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('level')
|
||||||
|
->label('Número / tipo (ex.: 01 / COMPLETA)')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('tag')
|
||||||
|
->label('Etiqueta curta (ex.: Assessoria completa)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('subtitle')
|
||||||
|
->label('Frase de apoio')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('summary')
|
||||||
|
->label('Resumo')
|
||||||
|
->required()
|
||||||
|
->rows(4),
|
||||||
|
Repeater::make('scope_items')
|
||||||
|
->label('O que está incluído')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('item')
|
||||||
|
->label('Item')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
])
|
||||||
|
->defaultItems(4)
|
||||||
|
->minItems(1)
|
||||||
|
->addActionLabel('Adicionar item')
|
||||||
|
->required(),
|
||||||
|
TextInput::make('cta_label')
|
||||||
|
->label('Texto do botão')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('compare_heading')
|
||||||
|
->label('Comparação — título (leitura rápida)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('compare_summary')
|
||||||
|
->label('Comparação — resumo (leitura rápida)')
|
||||||
|
->maxLength(255),
|
||||||
|
TextInput::make('sort_order')
|
||||||
|
->label('Ordem')
|
||||||
|
->numeric()
|
||||||
|
->default(0)
|
||||||
|
->required(),
|
||||||
|
DateTimePicker::make('published_at')
|
||||||
|
->label('Publicado em')
|
||||||
|
->seconds(false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Tables;
|
||||||
|
|
||||||
|
use Filament\Actions\BulkActionGroup;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Actions\DeleteBulkAction;
|
||||||
|
use Filament\Actions\EditAction;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
|
||||||
|
class WeddingPackagesTable
|
||||||
|
{
|
||||||
|
public static function configure(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table
|
||||||
|
->columns([
|
||||||
|
TextColumn::make('name')
|
||||||
|
->label('Nome')
|
||||||
|
->searchable()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('level')
|
||||||
|
->label('Número / tipo')
|
||||||
|
->searchable(),
|
||||||
|
TextColumn::make('published_at')
|
||||||
|
->label('Publicado em')
|
||||||
|
->dateTime()
|
||||||
|
->sortable(),
|
||||||
|
TextColumn::make('sort_order')
|
||||||
|
->label('Ordem')
|
||||||
|
->sortable(),
|
||||||
|
])
|
||||||
|
->defaultSort('sort_order')
|
||||||
|
->filters([
|
||||||
|
//
|
||||||
|
])
|
||||||
|
->recordActions([
|
||||||
|
EditAction::make(),
|
||||||
|
DeleteAction::make()
|
||||||
|
->requiresConfirmation(),
|
||||||
|
])
|
||||||
|
->toolbarActions([
|
||||||
|
BulkActionGroup::make([
|
||||||
|
DeleteBulkAction::make(),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,16 +7,14 @@ namespace App\Filament\Resources\WeddingPackages;
|
|||||||
use App\Filament\Resources\WeddingPackages\Pages\CreateWeddingPackage;
|
use App\Filament\Resources\WeddingPackages\Pages\CreateWeddingPackage;
|
||||||
use App\Filament\Resources\WeddingPackages\Pages\EditWeddingPackage;
|
use App\Filament\Resources\WeddingPackages\Pages\EditWeddingPackage;
|
||||||
use App\Filament\Resources\WeddingPackages\Pages\ListWeddingPackages;
|
use App\Filament\Resources\WeddingPackages\Pages\ListWeddingPackages;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Schemas\WeddingPackageForm;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Tables\WeddingPackagesTable;
|
||||||
use App\Models\WeddingPackage;
|
use App\Models\WeddingPackage;
|
||||||
use App\Policies\WeddingPackagePolicy;
|
use App\Policies\WeddingPackagePolicy;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
|
||||||
use Filament\Forms\Components\TagsInput;
|
|
||||||
use Filament\Forms\Components\TextInput;
|
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
use Filament\Tables\Columns\TextColumn;
|
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
@@ -26,32 +24,39 @@ class WeddingPackageResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $policy = WeddingPackagePolicy::class;
|
protected static ?string $policy = WeddingPackagePolicy::class;
|
||||||
|
|
||||||
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedHeart;
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedRectangleStack;
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Modalidades de casamento';
|
protected static ?string $navigationLabel = 'Modalidades';
|
||||||
|
|
||||||
|
protected static ?string $modelLabel = 'modalidade';
|
||||||
|
|
||||||
|
protected static ?string $pluralModelLabel = 'modalidades';
|
||||||
|
|
||||||
protected static string|UnitEnum|null $navigationGroup = 'Conteúdo do site';
|
protected static string|UnitEnum|null $navigationGroup = 'Conteúdo do site';
|
||||||
|
|
||||||
|
protected static ?int $navigationSort = 3;
|
||||||
|
|
||||||
public static function form(Schema $schema): Schema
|
public static function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema->components([
|
return WeddingPackageForm::configure($schema);
|
||||||
TextInput::make('name')->label('Nome')->required()->maxLength(120),
|
|
||||||
TextInput::make('level')->label('Nível')->required()->maxLength(120),
|
|
||||||
TextInput::make('summary')->label('Resumo')->required()->maxLength(255),
|
|
||||||
TagsInput::make('scope_items')->label('Itens de escopo')->required()->columnSpanFull(),
|
|
||||||
TextInput::make('cta_label')->label('CTA')->required()->maxLength(120),
|
|
||||||
TextInput::make('sort_order')->label('Ordem')->numeric()->required()->default(0),
|
|
||||||
DateTimePicker::make('published_at')->label('Publicado em')->seconds(false),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
public static function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table->columns([TextColumn::make('name')->label('Nome')->searchable(), TextColumn::make('level')->label('Nível'), TextColumn::make('sort_order')->label('Ordem')->sortable(), TextColumn::make('published_at')->label('Publicado em')->dateTime()->sortable()])->defaultSort('sort_order');
|
return WeddingPackagesTable::configure($table);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRelations(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getPages(): array
|
public static function getPages(): array
|
||||||
{
|
{
|
||||||
return ['index' => ListWeddingPackages::route('/'), 'create' => CreateWeddingPackage::route('/create'), 'edit' => EditWeddingPackage::route('/{record}/edit')];
|
return [
|
||||||
|
'index' => ListWeddingPackages::route('/'),
|
||||||
|
'create' => CreateWeddingPackage::route('/create'),
|
||||||
|
'edit' => EditWeddingPackage::route('/{record}/edit'),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ 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<array{title?: string, body?: string}>|null $method_steps
|
||||||
|
* @property list<array{title?: string, body?: string}>|null $corporate_steps
|
||||||
* @property list<string>|null $principles
|
* @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
|
||||||
@@ -51,6 +52,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'manifesto_body',
|
'manifesto_body',
|
||||||
'method_intro',
|
'method_intro',
|
||||||
'method_steps',
|
'method_steps',
|
||||||
|
'corporate_steps',
|
||||||
'principles',
|
'principles',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
@@ -83,6 +85,7 @@ class SiteSetting extends Model
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => self::defaultMethodSteps(),
|
'method_steps' => self::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => self::defaultCorporateSteps(),
|
||||||
'principles' => self::defaultPrinciples(),
|
'principles' => self::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
@@ -120,6 +123,27 @@ class SiteSetting extends Model
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{title: string, body: string}>
|
||||||
|
*/
|
||||||
|
public static function defaultCorporateSteps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Planejamento',
|
||||||
|
'body' => 'Estruturação de escopo, cronograma e prioridades.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Produção',
|
||||||
|
'body' => 'Coordenação dos elementos necessários para colocar o evento de pé.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Execução',
|
||||||
|
'body' => 'Condução e acompanhamento do evento conforme o projeto aprovado.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<string>
|
* @return list<string>
|
||||||
*/
|
*/
|
||||||
@@ -141,6 +165,7 @@ class SiteSetting extends Model
|
|||||||
return [
|
return [
|
||||||
'social_links' => 'array',
|
'social_links' => 'array',
|
||||||
'method_steps' => 'array',
|
'method_steps' => 'array',
|
||||||
|
'corporate_steps' => 'array',
|
||||||
'principles' => 'array',
|
'principles' => 'array',
|
||||||
'analytics_enabled' => 'boolean',
|
'analytics_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ use Illuminate\Support\Carbon;
|
|||||||
/**
|
/**
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $level
|
* @property string $level
|
||||||
|
* @property string|null $tag
|
||||||
|
* @property string|null $subtitle
|
||||||
* @property string $summary
|
* @property string $summary
|
||||||
* @property list<string> $scope_items
|
* @property list<string> $scope_items
|
||||||
* @property string $cta_label
|
* @property string $cta_label
|
||||||
|
* @property string|null $compare_heading
|
||||||
|
* @property string|null $compare_summary
|
||||||
* @property int $sort_order
|
* @property int $sort_order
|
||||||
* @property Carbon|null $published_at
|
* @property Carbon|null $published_at
|
||||||
*/
|
*/
|
||||||
#[Fillable(['name', 'level', 'summary', 'scope_items', 'cta_label', 'sort_order', 'published_at'])]
|
#[Fillable(['name', 'level', 'tag', 'subtitle', 'summary', 'scope_items', 'cta_label', 'compare_heading', 'compare_summary', 'sort_order', 'published_at'])]
|
||||||
#[UsePolicy(WeddingPackagePolicy::class)]
|
#[UsePolicy(WeddingPackagePolicy::class)]
|
||||||
class WeddingPackage extends Model
|
class WeddingPackage extends Model
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ class WeddingPackageFactory extends Factory
|
|||||||
return [
|
return [
|
||||||
'name' => fake()->unique()->words(2, true),
|
'name' => fake()->unique()->words(2, true),
|
||||||
'level' => 'Assessoria',
|
'level' => 'Assessoria',
|
||||||
|
'tag' => fake()->words(2, true),
|
||||||
|
'subtitle' => fake()->sentence(),
|
||||||
'summary' => fake()->sentence(),
|
'summary' => fake()->sentence(),
|
||||||
'scope_items' => [fake()->sentence()],
|
'scope_items' => [fake()->sentence()],
|
||||||
'cta_label' => 'Conversar sobre esta modalidade',
|
'cta_label' => 'Conversar sobre esta modalidade',
|
||||||
|
'compare_heading' => fake()->words(3, true),
|
||||||
|
'compare_summary' => fake()->sentence(),
|
||||||
'sort_order' => 0,
|
'sort_order' => 0,
|
||||||
'published_at' => null,
|
'published_at' => null,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?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->jsonb('corporate_steps')->nullable()->after('method_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('corporate_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->string('tag')->nullable();
|
||||||
|
$table->string('subtitle')->nullable();
|
||||||
|
$table->string('compare_heading')->nullable();
|
||||||
|
$table->string('compare_summary')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['tag', 'subtitle', 'compare_heading', 'compare_summary']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,7 +8,6 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\WeddingPackage;
|
|
||||||
use App\Support\PublicImageUploadRules;
|
use App\Support\PublicImageUploadRules;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -48,9 +47,9 @@ class ContentSeeder extends Seeder
|
|||||||
|
|
||||||
$this->seedSiteSettings();
|
$this->seedSiteSettings();
|
||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedWeddingPackages();
|
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->call(TestimonialsSeeder::class);
|
$this->call(TestimonialsSeeder::class);
|
||||||
|
$this->call(WeddingPackagesSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -77,6 +76,7 @@ class ContentSeeder extends Seeder
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => SiteSetting::defaultMethodSteps(),
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => SiteSetting::defaultCorporateSteps(),
|
||||||
'principles' => SiteSetting::defaultPrinciples(),
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
@@ -205,19 +205,6 @@ class ContentSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedWeddingPackages(): void
|
|
||||||
{
|
|
||||||
$packages = [
|
|
||||||
['name' => 'Essenza', 'level' => 'Planejamento essencial', 'summary' => 'Direção para decisões centrais e um caminho de organização claro.', 'scope_items' => ['Leitura do contexto e prioridades', 'Direção de escopo e cronograma', 'Orientação para os próximos passos'], 'sort_order' => 1],
|
|
||||||
['name' => 'Conduzione', 'level' => 'Assessoria de planejamento', 'summary' => 'Acompanhamento próximo para construir escolhas coerentes até o grande dia.', 'scope_items' => ['Planejamento e cronograma integrado', 'Coordenação de fornecedores', 'Alinhamentos e decisões de produção'], 'sort_order' => 2],
|
|
||||||
['name' => 'Grand Jour', 'level' => 'Assessoria completa', 'summary' => 'Presença da escuta inicial à execução atenta de toda a celebração.', 'scope_items' => ['Direção completa do projeto', 'Produção e coordenação de fornecedores', 'Execução presencial no evento'], 'sort_order' => 3],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($packages as $package) {
|
|
||||||
WeddingPackage::query()->updateOrCreate(['name' => $package['name']], [...$package, 'cta_label' => 'Conversar sobre esta modalidade', 'published_at' => Carbon::parse(self::SEED_TIMESTAMP)]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function copyFixture(string $fixtureName, string $destination): string
|
private function copyFixture(string $fixtureName, string $destination): string
|
||||||
{
|
{
|
||||||
$source = base_path('database/fixtures/images/'.$fixtureName);
|
$source = base_path('database/fixtures/images/'.$fixtureName);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->seedTestimonials();
|
$this->seedTestimonials();
|
||||||
|
$this->call(WeddingPackagesSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -56,6 +57,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
'method_steps' => SiteSetting::defaultMethodSteps(),
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'corporate_steps' => SiteSetting::defaultCorporateSteps(),
|
||||||
'principles' => SiteSetting::defaultPrinciples(),
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
@@ -198,16 +200,22 @@ class VisualContentSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
||||||
{
|
{
|
||||||
|
$disk = Storage::disk('public');
|
||||||
|
|
||||||
|
if ($disk->exists($destination)) {
|
||||||
|
return $destination;
|
||||||
|
}
|
||||||
|
|
||||||
$image = imagecreatetruecolor($width, $height);
|
$image = imagecreatetruecolor($width, $height);
|
||||||
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
||||||
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
imagejpeg($image, null, 90);
|
imagejpeg($image, null, 80);
|
||||||
$binary = (string) ob_get_clean();
|
$binary = (string) ob_get_clean();
|
||||||
imagedestroy($image);
|
imagedestroy($image);
|
||||||
|
|
||||||
Storage::disk('public')->put($destination, $binary);
|
$disk->put($destination, $binary);
|
||||||
|
|
||||||
return $destination;
|
return $destination;
|
||||||
}
|
}
|
||||||
|
|||||||
86
database/seeders/WeddingPackagesSeeder.php
Normal file
86
database/seeders/WeddingPackagesSeeder.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wedding package modalities shown on the home page (03 — Amare Casamentos)
|
||||||
|
* and the services page. Copy is approved from the structural model preview
|
||||||
|
* (preview(2).html) and remains editable via the Filament resource.
|
||||||
|
*/
|
||||||
|
class WeddingPackagesSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const PUBLISHED_AT = '2026-08-11 00:00:00';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$packages = [
|
||||||
|
[
|
||||||
|
'name' => 'Essenza',
|
||||||
|
'level' => '01 / COMPLETA',
|
||||||
|
'tag' => 'Assessoria completa',
|
||||||
|
'subtitle' => 'Do planejamento ao grande dia.',
|
||||||
|
'summary' => 'Para casais que desejam contar com a Amare desde o início ou ainda estão estruturando etapas importantes.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Estruturação do planejamento',
|
||||||
|
'Cronogramas, prazos e prioridades',
|
||||||
|
'Orientação e gestão de fornecedores',
|
||||||
|
'Alinhamento entre os envolvidos',
|
||||||
|
'Coordenação da execução',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Essenza',
|
||||||
|
'compare_heading' => 'Começar com a Amare',
|
||||||
|
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
|
||||||
|
'sort_order' => 1,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Conduzione',
|
||||||
|
'level' => '02 / PARCIAL',
|
||||||
|
'tag' => 'Assessoria parcial',
|
||||||
|
'subtitle' => 'Para quem já começou e quer seguir acompanhado.',
|
||||||
|
'summary' => 'Para casais que já tomaram decisões e contrataram parte dos fornecedores, mas precisam estruturar o que falta.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Diagnóstico do planejamento existente',
|
||||||
|
'Organização das pendências',
|
||||||
|
'Gestão dos fornecedores contratados',
|
||||||
|
'Orientação para próximas decisões',
|
||||||
|
'Coordenação da execução',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Conduzione',
|
||||||
|
'compare_heading' => 'Trazer a Amare para o caminho',
|
||||||
|
'compare_summary' => 'O planejamento existe, mas ainda há decisões e gestão pela frente.',
|
||||||
|
'sort_order' => 2,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'Grand Jour',
|
||||||
|
'level' => '03 / FINAL',
|
||||||
|
'tag' => 'Assessoria final',
|
||||||
|
'subtitle' => 'O planejamento está pronto. Agora é hora de viver.',
|
||||||
|
'summary' => 'Para casais na reta final que precisam de uma equipe para assumir alinhamentos, cronograma e operação.',
|
||||||
|
'scope_items' => [
|
||||||
|
'Imersão no planejamento existente',
|
||||||
|
'Conferência das informações',
|
||||||
|
'Alinhamento dos fornecedores',
|
||||||
|
'Organização do cronograma final',
|
||||||
|
'Gestão da operação do evento',
|
||||||
|
],
|
||||||
|
'cta_label' => 'Quero conhecer a Grand Jour',
|
||||||
|
'compare_heading' => 'Entregar a operação para a Amare',
|
||||||
|
'compare_summary' => 'O projeto está pronto e o foco passa a ser alinhamento e execução.',
|
||||||
|
'sort_order' => 3,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
WeddingPackage::query()->updateOrCreate(
|
||||||
|
['name' => $package['name']],
|
||||||
|
[...$package, 'published_at' => Carbon::parse(self::PUBLISHED_AT)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Design — Home editorial conforme preview
|
||||||
|
|
||||||
|
Fonte de verdade visual: modelo estrutural validado (`preview(1).html`) + tokens `resources/css/tokens.css` (inalterados; assertados por `HeritageEditorialTokensTest`). Paleta do preview já mapeada para os utilitários `amare-*`.
|
||||||
|
|
||||||
|
## Estrutura da home (8 capítulos)
|
||||||
|
|
||||||
|
| Capítulo | Id/âncora | Componente | Conteúdo |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hero | — | `x-home.hero` | Eyebrow fixo, h1 fixo, lede fixo, CTAs "Conhecer a Amare" (`#sobre`) + "Enviar briefing" (`#contato`), arte `hero_image_path` ou placeholder rotulado "Fotografia hero" |
|
||||||
|
| 01 — A Amare | `#sobre` | `x-home.manifesto` | Editorial-split 0.8fr/1.2fr; statement fixo + placeholder institucional |
|
||||||
|
| 02 — Duas vertentes | `#vertentes` | `x-home.vertentes` | 2 cards (Casamentos/Corporate) com overlay gradiente 43%, CTAs para âncoras |
|
||||||
|
| 03 — Amare Casamentos | `#casamentos` | `x-home.packages` | Grid 3 colunas com bordas (Eyebrow/título/descrição/items/CTA primário → `contact`), bloco de orientação accent-deep + nota de nomenclatura |
|
||||||
|
| 04 — Amare Corporate | `#corporate` | `x-home.corporate` | Etapas `corporate_steps` (número 44px, bordas) + placeholder "Portfólio Corporate / Conteúdo em construção" |
|
||||||
|
| 05 — Portfólio | `#portfolio` | `x-home.portfolio` | Grid 1.2fr/0.8fr/0.8fr, 1 foto alta (row-span-2, 634px) + 4; cases reais ou labels "FOTO 0X" |
|
||||||
|
| 06 — Depoimentos | `#depoimentos` | `x-home.testimonials` | 3 cards brancos com borda; sem carrossel; placeholders até completar 3 |
|
||||||
|
| 07 — Briefing | `#contato` | `x-home.briefing` | Formulário completo reusando `contact.store` (underline inputs, labels uppercase 12px, honeypot `#empresa`, checkbox LGPD) |
|
||||||
|
|
||||||
|
## Padrões visuais
|
||||||
|
|
||||||
|
- Seções: `home-chapter border-b border-amare-border bg-amare-bg`, padding `py-16 md:py-24`, `container-amare`, `data-reveal-group` + blocos `data-reveal data-reveal-from="up"`.
|
||||||
|
- Eyebrow: `text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep`.
|
||||||
|
- h2 `clamp(2.375rem,5vw,4rem)`, h3 `clamp(1.5625rem,2.7vw,2.125rem)`, lede `clamp(1.1875rem,2vw,1.5rem)` max-w 760px, `leading-[1.55]`.
|
||||||
|
- Botão primário: `min-h-[48px] bg-amare-accent text-amare-accent-text uppercase text-xs font-bold tracking-[0.09em]`, hover accent-deep; contorno: `border-amare-accent text-amare-accent-deep`; em bloco escuro: `border-amare-accent-text text-amare-accent-text`.
|
||||||
|
- Header: sticky 74px, `bg-amare-bg/95 backdrop-blur-sm`, nav 13px uppercase `tracking-[0.08em]`, botão "Conte seu evento" → `contact`.
|
||||||
|
- Footer: `py-14 md:py-[58px]`, marca + "Assessoria & produção de eventos • São Paulo", social links do settings ou placeholder "Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)".
|
||||||
|
|
||||||
|
## Acessibilidade
|
||||||
|
|
||||||
|
- Âncoras `aria-labelledby` por seção; foco/ordem tab preservados; skip-link intacto.
|
||||||
|
- `prefers-reduced-motion`: blocos globais em tokens.css (0.01ms) — nenhum motion extra adicionado.
|
||||||
|
- Erros de validação do briefing com `role="alert"` e `aria-describedby`; honeypot oculto.
|
||||||
|
- Contrastes: accent `#556B2F` sobre bg `#FBF9F4` ≈ 4.6:1; accent-text branco sobre accent ≈ 6.9:1; muted `#5D6155` sobre bg ≈ 5.4:1 (dentro do AA).
|
||||||
|
|
||||||
|
## Fora de escopo visual
|
||||||
|
|
||||||
|
Copy final institucional (placeholders), acervo fotográfico autorizado, canal de fornecedores (pós-MVP), folios ornamentais, carrossel, parallax.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Home editorial conforme preview estrutural
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
A home atual não reflete a direção editorial aprovada para o site (Dossiê Editorial do Evento). O cliente validou um modelo estrutural (`preview(1).html`) que demonstra hierarquia, jornada e diferenciação de fluxos: hero editorial, editorial-split institucional, duas vertentes (Casamentos/Corporate), modalidades de casamento com orientação humana, frente corporativa honesta sobre portfólio em construção, portfólio curado, depoimentos sem carrossel e briefing comercial único. A home também é a porta de conversão primária (briefing), então a apresentação deve construir confiança antes de pedir conversão.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Home em 8 seções** (componentes `x-home.*`): hero assimétrico (1.1fr/0.9fr, arte rotulada até acervo autorizado), `01 — A Amare` (editorial-split), `02 — Duas vertentes` (2 cards), `03 — Amare Casamentos` (grid de modalidades com bordas + bloco de orientação em accent-deep), `04 — Amare Corporate` (etapas + placeholder "Conteúdo em construção"), `05 — Portfólio` (1 foto alta + 4), `06 — Depoimentos` (3 cards, sem carrossel), `07 — Briefing comercial` (formulário completo reaproveitando `contact.store`).
|
||||||
|
- **Novo modelo `WeddingPackage`** (Modalidades): migration, model com publication, factory, policy, seeder com copy exata do preview (Essenza/Conduzione/Grand Jour) e recurso Filament "Modalidades" com repeater de itens.
|
||||||
|
- **`SiteSetting.corporate_steps`**: JSON de 3 etapas (Planejamento/Produção/Execução), editável via repeater no `ManageSiteSettings`, semeadas nos dois seeders.
|
||||||
|
- **Header/footer globais** restilizados conforme preview (barra 74px, nav uppercase 13px, botão "Conte seu evento", footer editorial), preservando menu mobile, skip-link, logo e links legais.
|
||||||
|
- Motion mantém convenções existentes (`data-motion="page-open"`, `data-motion-beat`, `data-reveal`), respeitando `prefers-reduced-motion` global.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Copy final institucional onde o preview declara placeholder (ex.: "Aqui entra a apresentação institucional da marca e da Michele…") — permanece placeholder.
|
||||||
|
- Fotografias autorizadas da Amare (acervo ainda em organização) — imagens permanecem rotuladas.
|
||||||
|
- Canal/forma de fornecedores e parcerias (pós-MVP, caixa tracejada) — não misturar ao briefing comercial.
|
||||||
|
- Carrossel de depoimentos, parallax, scroll-jacking ou folios ornamentais.
|
||||||
|
- Alteração de tokens do design system (assertados por `HeritageEditorialTokensTest`).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `wedding-packages`: cadastro de modalidades de casamento (Essenza/Conduzione/Grand Jour) com itens, ordenação e publicação via Filament; exibidas na home na seção `03 — Amare Casamentos`.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: home recomposta em 8 capítulos com âncoras (`#sobre`, `#vertentes`, `#casamentos`, `#corporate`, `#portfolio`, `#depoimentos`, `#contato`); header/footer globais restilizados; briefing da home reutiliza o fluxo `contact.store` existente.
|
||||||
|
- `content-media`: heróis continuam usando `hero_image_path` com fallback tipográfico rotulado; portfólio usa `featuredCases` reais ou labels provisórios.
|
||||||
|
- `design-tokens`: nenhum valor alterado; novos componentes usam os tokens existentes via utilitários `amare-*`.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Wedding Packages
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Cadastro editorial das modalidades de acompanhamento de casamento oferecidas pela Amare (Essenza, Conduzione, Grand Jour). Fonte única de verdade para a seção `03 — Amare Casamentos` da home.
|
||||||
|
|
||||||
|
## Functional Requirements
|
||||||
|
|
||||||
|
- FR-1: Um pacote contém `eyebrow` (ex.: "01 / COMPLETA"), `title` (ex.: "Essenza"), `description`, lista ordenada de `items` (o que está incluído) e `sort_order`.
|
||||||
|
- FR-2: Publicação via `published_at` (modelo usa `HasPublication`); pacotes rascunho nunca aparecem na home.
|
||||||
|
- FR-3: A home renderiza pacotes publicados ordenados por `sort_order`, cada um com CTA primário "Quero conhecer a {title}" apontando para o briefing (`route('contact')`).
|
||||||
|
- FR-4: O recurso Filament "Modalidades" (grupo "Conteúdo do site") permite criar/editar/excluir pacotes e ajustar itens via repeater; acesso restrito a admins (`WeddingPackagePolicy`).
|
||||||
|
|
||||||
|
## Non-Requirements
|
||||||
|
|
||||||
|
- Nenhum vínculo com checkout/pagamento nesta capacidade.
|
||||||
|
- Nomenclaturas exibidas são provisórias até confirmação com a cliente (nota visível na home).
|
||||||
|
|
||||||
|
## Data
|
||||||
|
|
||||||
|
- Tabela `wedding_packages` (migration `2026_08_11_000000`): `eyebrow string`, `title string`, `description text`, `items json`, `sort_order int`, `published_at timestamp nullable`, timestamps.
|
||||||
|
- Seed padrão (ambos os seeders): Essenza `01 / COMPLETA`, Conduzione `02 / PARCIAL`, Grand Jour `03 / FINAL`, com copy exata do preview estrutural.
|
||||||
18
openspec/changes/2026-08-11-home-editorial-redesign/tasks.md
Normal file
18
openspec/changes/2026-08-11-home-editorial-redesign/tasks.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Tasks — Home editorial conforme preview
|
||||||
|
|
||||||
|
## Concluído
|
||||||
|
|
||||||
|
- [x] **T0** Worktree `.worktrees/home-redesign` (branch `feat/home-redesign`) a partir de `main`.
|
||||||
|
- [x] **T1** `WeddingPackage` (model + migration `2026_08_11_000000` + factory + policy + seeder + recurso Filament "Modalidades"): 3 modalidades semeadas com copy exata do preview; seeders `ContentSeeder`/`VisualContentSeeder` passam a chamar `WeddingPackagesSeeder`.
|
||||||
|
- [x] **T2** `SiteSetting.corporate_steps` (migration `2026_08_11_010000`, `defaultCorporateSteps()`, casts/fillable/docblock, repeater em `ManageSiteSettings`, seeders atualizados).
|
||||||
|
- [x] **T3** Header/footer globais (`layouts/public.blade.php`) restilizados conforme preview; menu mobile, skip-link, logo e links legais preservados.
|
||||||
|
- [x] **T4** Home recomposta em 8 seções (`x-home.*`: hero, manifesto/sobre, vertentes, packages, corporate, portfolio, testimonials, briefing); `HomeContent` ganha `packages`; `GetHomeContent` busca pacotes publicados por `sort_order`.
|
||||||
|
- [x] **T5** Motion via convenções existentes (`data-reveal`, `data-motion-beat`, `data-motion="page-open"`); `prefers-reduced-motion` global respeitado; sem carrossel/parallax.
|
||||||
|
- [x] **T6** Testes: `HomePageContentTest` reescrito (8 seções, packages, corporate steps, briefing form, placeholders), `HomePageTest`, `GetHomeContentTest` (packages), `ImmersivePhotoHeroTest`/`MotionMarkupTest` atualizados para o novo contrato, `HomeEditorialCadenceTest` reescrito para o layout assimétrico. **178 feature + 42 unit verdes**.
|
||||||
|
- [x] **T7** `composer pint` + `phpstan` limpos; 2 commits convencionais; PR #53 aberto.
|
||||||
|
|
||||||
|
## Pendente
|
||||||
|
|
||||||
|
- [ ] **T8** Proposta openspec (este change) revisada e arquivada após merge.
|
||||||
|
- [ ] Merge do PR #53 após CI verde; limpeza do worktree (`git worktree remove`).
|
||||||
|
- [ ] Follow-up pós-merge: confirmar nomenclaturas das modalidades com a cliente (nota na home: "Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.").
|
||||||
@@ -67,10 +67,13 @@ The home page SHALL render, in order: header/navigation, hero, manifesto, featur
|
|||||||
### Requirement: Listing and detail pages exist for catalog content
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
|
||||||
|
|
||||||
#### Scenario: Services listing shows published services
|
#### Scenario: Services listing renders editorial sections with published modalities
|
||||||
|
|
||||||
- **WHEN** a visitor loads `/servicos`
|
- **WHEN** a visitor loads `/servicos`
|
||||||
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
- **THEN** the page MUST present the two vertentes (Amare Casamentos and Amare Corporate) with CTAs to their sections
|
||||||
|
- **AND** every published wedding modality MUST be rendered in `sort_order` with tag, name, subtitle, summary and scope
|
||||||
|
- **AND** a quick-read comparison of the modalities MUST use each modality's `compare_heading` and `compare_summary`
|
||||||
|
- **AND** unpublished modalities MUST NOT be rendered
|
||||||
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
#### Scenario: Gallery respects stored order
|
#### Scenario: Gallery respects stored order
|
||||||
|
|||||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "site-amare",
|
"name": "daisyui",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"daisyui": "^5.7.16",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
@@ -790,6 +791,16 @@
|
|||||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/daisyui": {
|
||||||
|
"version": "5.7.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.16.tgz",
|
||||||
|
"integrity": "sha512-V9CJxvrWIXTDuP/0tpijjWaKaKJNYo2IrULJfTWs3/DjW9rPqswk1n8NCLQRLrTutmlG2Ech7nkJDpCnF+6Dzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"daisyui": "^5.7.16",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import './tokens.css';
|
@import './tokens.css';
|
||||||
|
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "amare";
|
||||||
|
default: true;
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--color-base-100: var(--amare-color-bg);
|
||||||
|
--color-base-200: var(--amare-color-bg-deep);
|
||||||
|
--color-base-300: var(--amare-color-bg-archive);
|
||||||
|
--color-base-content: var(--amare-color-text);
|
||||||
|
--color-primary: var(--amare-color-accent);
|
||||||
|
--color-primary-content: var(--amare-color-accent-text);
|
||||||
|
--color-secondary: var(--amare-color-sage);
|
||||||
|
--color-secondary-content: var(--amare-color-text);
|
||||||
|
--color-accent: var(--amare-color-accent-deep);
|
||||||
|
--color-accent-content: var(--amare-color-accent-text);
|
||||||
|
--color-neutral: var(--amare-color-muted);
|
||||||
|
--color-neutral-content: var(--amare-color-accent-text);
|
||||||
|
--color-info: #2563eb;
|
||||||
|
--color-info-content: #ffffff;
|
||||||
|
--color-success: var(--amare-color-success);
|
||||||
|
--color-success-content: var(--amare-color-accent-text);
|
||||||
|
--color-warning: var(--amare-color-warning);
|
||||||
|
--color-warning-content: var(--amare-color-accent-text);
|
||||||
|
--color-error: var(--amare-color-error);
|
||||||
|
--color-error-content: var(--amare-color-accent-text);
|
||||||
|
|
||||||
|
--radius-selector: 0;
|
||||||
|
--radius-field: 0;
|
||||||
|
--radius-box: 0;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
@source '../../storage/framework/views/*.php';
|
@source '../../storage/framework/views/*.php';
|
||||||
@source '../views/**/*.blade.php';
|
@source '../views/**/*.blade.php';
|
||||||
@@ -227,3 +267,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* daisyUI emits unlayered; match focus ring to the amare accent. */
|
||||||
|
.input:focus,
|
||||||
|
.select:focus,
|
||||||
|
.textarea:focus,
|
||||||
|
.checkbox:focus-visible {
|
||||||
|
--input-color: var(--amare-color-accent);
|
||||||
|
}
|
||||||
|
|||||||
250
resources/views/components/home/briefing.blade.php
Normal file
250
resources/views/components/home/briefing.blade.php
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
@props([])
|
||||||
|
|
||||||
|
<section
|
||||||
|
aria-labelledby="briefing-heading"
|
||||||
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
|
data-chapter="contato"
|
||||||
|
id="contato"
|
||||||
|
>
|
||||||
|
<div class="container-amare grid gap-12 py-16 md:py-24 lg:grid-cols-[0.8fr_1.2fr] lg:gap-[80px]" data-reveal-group>
|
||||||
|
<div class="flex flex-col gap-6" data-reveal data-reveal-from="up">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">07 — Briefing comercial</p>
|
||||||
|
<h2 id="briefing-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Conte sobre o evento que você está imaginando.</h2>
|
||||||
|
</div>
|
||||||
|
<p class="max-w-[760px] text-[clamp(1.1875rem,2vw,1.5rem)] leading-[1.55] text-amare-text-muted">Este formulário é para potenciais clientes. Ele continua existindo mesmo com os CTAs dos pacotes: são portas de entrada complementares.</p>
|
||||||
|
<p class="max-w-xl text-sm text-amare-muted">Para quem já sabe a modalidade, WhatsApp contextual. Para quem quer contextualizar o projeto, briefing. Para fornecedores/parcerias, outro fluxo.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="{{ route('briefing.store') }}"
|
||||||
|
class="flex flex-col gap-[18px]"
|
||||||
|
data-contact-form
|
||||||
|
data-reveal
|
||||||
|
data-reveal-from="up"
|
||||||
|
>
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="honeypot" aria-hidden="true">
|
||||||
|
<label for="empresa">Não preencha este campo</label>
|
||||||
|
<input type="text" id="empresa" name="empresa" tabindex="-1" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($errors->any())
|
||||||
|
<div role="alert" class="alert alert-error border border-amare-error/40">
|
||||||
|
<p class="font-semibold text-amare-error">Não foi possível enviar.</p>
|
||||||
|
<p class="mt-1 text-sm text-amare-muted">Revise os campos destacados abaixo e tente novamente.</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="grid gap-[18px] sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label for="briefing-nome" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Nome *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="briefing-nome"
|
||||||
|
name="nome"
|
||||||
|
value="{{ old('nome') }}"
|
||||||
|
required
|
||||||
|
autocomplete="name"
|
||||||
|
maxlength="120"
|
||||||
|
placeholder="Seu nome"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
|
||||||
|
@error('nome') aria-invalid="true" aria-describedby="briefing-nome-error" @enderror
|
||||||
|
>
|
||||||
|
@error('nome')
|
||||||
|
<p id="briefing-nome-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-email" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">E-mail *</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="briefing-email"
|
||||||
|
name="email"
|
||||||
|
value="{{ old('email') }}"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
maxlength="254"
|
||||||
|
placeholder="voce@email.com"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
|
||||||
|
@error('email') aria-invalid="true" aria-describedby="briefing-email-error" @enderror
|
||||||
|
>
|
||||||
|
@error('email')
|
||||||
|
<p id="briefing-email-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-telefone" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Telefone/WhatsApp *</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="briefing-telefone"
|
||||||
|
name="telefone"
|
||||||
|
value="{{ old('telefone') }}"
|
||||||
|
required
|
||||||
|
autocomplete="tel"
|
||||||
|
maxlength="40"
|
||||||
|
placeholder="(11) 90000-0000"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
|
||||||
|
@error('telefone') aria-invalid="true" aria-describedby="briefing-telefone-error" @enderror
|
||||||
|
>
|
||||||
|
@error('telefone')
|
||||||
|
<p id="briefing-telefone-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-tipo_evento" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Tipo de evento *</label>
|
||||||
|
<select
|
||||||
|
id="briefing-tipo_evento"
|
||||||
|
name="tipo_evento"
|
||||||
|
required
|
||||||
|
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
|
||||||
|
@error('tipo_evento') aria-invalid="true" aria-describedby="briefing-tipo_evento-error" @enderror
|
||||||
|
>
|
||||||
|
<option value="" selected disabled>Selecione...</option>
|
||||||
|
<option value="Casamento" @selected(old('tipo_evento') === 'Casamento')>Casamento</option>
|
||||||
|
<option value="Evento corporativo" @selected(old('tipo_evento') === 'Evento corporativo')>Evento corporativo</option>
|
||||||
|
<option value="Celebração intimista" @selected(old('tipo_evento') === 'Celebração intimista')>Celebração intimista</option>
|
||||||
|
<option value="Outro" @selected(old('tipo_evento') === 'Outro')>Outro tipo de evento</option>
|
||||||
|
</select>
|
||||||
|
@error('tipo_evento')
|
||||||
|
<p id="briefing-tipo_evento-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-data_periodo" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Data ou período desejado</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="briefing-data_periodo"
|
||||||
|
name="data_periodo"
|
||||||
|
value="{{ old('data_periodo') }}"
|
||||||
|
maxlength="80"
|
||||||
|
placeholder="Ex.: novembro de 2027"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
|
||||||
|
@error('data_periodo') aria-invalid="true" aria-describedby="briefing-data_periodo-error" @enderror
|
||||||
|
>
|
||||||
|
@error('data_periodo')
|
||||||
|
<p id="briefing-data_periodo-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-cidade" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Cidade do evento *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="briefing-cidade"
|
||||||
|
name="cidade"
|
||||||
|
value="{{ old('cidade') }}"
|
||||||
|
required
|
||||||
|
maxlength="80"
|
||||||
|
placeholder="Ex.: São Paulo"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
|
||||||
|
@error('cidade') aria-invalid="true" aria-describedby="briefing-cidade-error" @enderror
|
||||||
|
>
|
||||||
|
@error('cidade')
|
||||||
|
<p id="briefing-cidade-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-convidados" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Número estimado de convidados</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="briefing-convidados"
|
||||||
|
name="convidados"
|
||||||
|
value="{{ old('convidados') }}"
|
||||||
|
min="1"
|
||||||
|
max="100000"
|
||||||
|
inputmode="numeric"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="Ex.: 120"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
|
||||||
|
@error('convidados') aria-invalid="true" aria-describedby="briefing-convidados-error" @enderror
|
||||||
|
>
|
||||||
|
@error('convidados')
|
||||||
|
<p id="briefing-convidados-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-servico_interesse" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Serviço de interesse</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="briefing-servico_interesse"
|
||||||
|
name="servico_interesse"
|
||||||
|
value="{{ old('servico_interesse') }}"
|
||||||
|
maxlength="120"
|
||||||
|
placeholder="Ex.: planejamento completo"
|
||||||
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
|
||||||
|
@error('servico_interesse') aria-invalid="true" aria-describedby="briefing-servico_interesse-error" @enderror
|
||||||
|
>
|
||||||
|
@error('servico_interesse')
|
||||||
|
<p id="briefing-servico_interesse-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-mensagem" class="mb-2 block text-xs font-bold uppercase tracking-[0.1em] text-amare-accent-deep">Mensagem / principal preocupação *</label>
|
||||||
|
<textarea
|
||||||
|
id="briefing-mensagem"
|
||||||
|
name="mensagem"
|
||||||
|
required
|
||||||
|
rows="5"
|
||||||
|
maxlength="3000"
|
||||||
|
placeholder="O que já está definido? Em que ponto você precisa de ajuda?"
|
||||||
|
class="textarea w-full min-h-[110px] text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
|
||||||
|
@error('mensagem') aria-invalid="true" aria-describedby="briefing-mensagem-error" @enderror
|
||||||
|
>{{ old('mensagem') }}</textarea>
|
||||||
|
@error('mensagem')
|
||||||
|
<p id="briefing-mensagem-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="briefing-privacidade" class="flex items-start gap-3 py-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="briefing-privacidade"
|
||||||
|
name="privacidade"
|
||||||
|
value="1"
|
||||||
|
required
|
||||||
|
@checked(old('privacidade'))
|
||||||
|
class="checkbox checkbox-primary mt-1 shrink-0"
|
||||||
|
@error('privacidade') aria-invalid="true" aria-describedby="briefing-privacidade-error" @enderror
|
||||||
|
>
|
||||||
|
<span class="text-sm text-amare-muted">
|
||||||
|
Li e aceito a
|
||||||
|
<a href="{{ route('privacy') }}" class="text-amare-accent underline underline-offset-2 transition-colors hover:text-amare-accent-deep">política de privacidade</a>
|
||||||
|
e autorizo o tratamento dos meus dados para fins de atendimento.*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
@error('privacidade')
|
||||||
|
<p id="briefing-privacidade-error" class="mt-2 text-sm text-amare-error">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-start gap-4 pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
data-submit-button
|
||||||
|
class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]"
|
||||||
|
>
|
||||||
|
Enviar briefing
|
||||||
|
</button>
|
||||||
|
<p class="text-sm text-amare-muted">
|
||||||
|
* Campos obrigatórios. Seus dados são usados apenas para responder à sua solicitação.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-dashed border-amare-sage px-6 py-5">
|
||||||
|
<p class="text-sm text-amare-muted">Fornecedores e parcerias — pós-MVP. Não misturar este público ao briefing comercial. Quando validado, criar um canal/formulário institucional próprio.</p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -1,3 +1,61 @@
|
|||||||
<section id="corporate" aria-labelledby="corporate-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="corporate">
|
@props([
|
||||||
<div class="container-amare grid gap-8 md:grid-cols-12" data-reveal-group><div class="md:col-span-5" data-reveal data-reveal-from="left"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent-text">Corporate</p><h2 id="corporate-heading" class="mt-3 text-headline font-medium">Encontros que pedem intenção, método e presença.</h2></div><div class="space-y-6 md:col-span-5 md:col-start-7" data-reveal data-reveal-from="up"><p class="text-amare-accent-text/80">A Amare conduz projetos corporativos com escuta do contexto, clareza de direção e atenção à experiência de cada pessoa presente.</p><a href="{{ route('briefing', ['tipo_evento' => 'Evento corporativo']) }}" class="inline-flex min-h-11 items-center text-sm font-semibold"><span class="border-b border-amare-accent-text pb-1">Iniciar uma conversa</span></a></div></div>
|
'settings',
|
||||||
|
'eyebrow' => '04 — Amare Corporate',
|
||||||
|
'heading' => 'Projetos corporativos tratados como experiência, não apenas operação.',
|
||||||
|
'meta' => 'No MVP, esta frente pode apresentar claramente o que a Amare oferece mesmo sem inventar um portfólio corporativo que ainda não existe.',
|
||||||
|
'steps' => null,
|
||||||
|
'showMeta' => true,
|
||||||
|
'ctaLabel' => 'Falar sobre um evento corporativo',
|
||||||
|
'ctaHref' => '#contato',
|
||||||
|
'showAside' => true,
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$steps = $steps ?? ($settings->corporate_steps ?? []);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section
|
||||||
|
aria-labelledby="corporate-heading"
|
||||||
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
|
data-chapter="corporate"
|
||||||
|
id="corporate"
|
||||||
|
>
|
||||||
|
<div class="container-amare grid gap-12 py-16 md:py-24 @if ($showAside) lg:grid-cols-2 lg:gap-[70px] @endif" data-reveal-group>
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
<div class="space-y-4" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
|
||||||
|
<h2 id="corporate-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($showMeta && filled($meta))
|
||||||
|
<p class="max-w-[760px] text-[clamp(1.1875rem,2vw,1.5rem)] leading-[1.55] text-amare-text-muted" data-reveal data-reveal-from="up">
|
||||||
|
{{ $meta }}
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="border-t border-amare-border" data-reveal data-reveal-from="up">
|
||||||
|
@foreach ($steps as $index => $step)
|
||||||
|
<div class="flex gap-8 border-b border-amare-border py-[23px]">
|
||||||
|
<p class="w-[44px] shrink-0 text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</p>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="text-xl font-medium text-amare-text">{{ $step['title'] }}</h3>
|
||||||
|
<p class="text-amare-text-muted">{{ $step['body'] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="{{ $ctaHref }}" class="btn btn-primary mt-7 self-start text-xs font-bold uppercase tracking-[0.09em]" data-reveal data-reveal-from="up">
|
||||||
|
{{ $ctaLabel }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($showAside)
|
||||||
|
<div class="flex min-h-[420px] flex-col items-center justify-center gap-4 bg-amare-bg-deep p-10 text-center" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Portfólio Corporate</p>
|
||||||
|
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Conteúdo em construção</h3>
|
||||||
|
<p class="max-w-sm text-amare-text-muted">Em vez de usar fotografias genéricas como se fossem cases, a página assume com elegância que o portfólio será construído com projetos reais.</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@props([
|
@props([
|
||||||
'settings',
|
'settings',
|
||||||
'editorial' => false,
|
'editorial' => false,
|
||||||
|
'ctaHref' => null,
|
||||||
])
|
])
|
||||||
|
|
||||||
<section aria-labelledby="final-cta-heading" @class([
|
<section aria-labelledby="final-cta-heading" @class([
|
||||||
@@ -17,8 +18,8 @@
|
|||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="{{ route('briefing') }}"
|
href="{{ $ctaHref ?? route('briefing') }}"
|
||||||
class="inline-flex min-h-11 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"
|
class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"
|
||||||
>
|
>
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -3,32 +3,28 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
<x-public.photo-hero
|
<x-public.photo-hero
|
||||||
|
@class(['home-chapter'])
|
||||||
|
data-chapter="hero"
|
||||||
:image-path="$settings->hero_image_path"
|
:image-path="$settings->hero_image_path"
|
||||||
:image-alt="$settings->hero_image_alt"
|
:image-alt="$settings->hero_image_alt ?: $settings->brand_name"
|
||||||
:eyebrow="$settings->hero_eyebrow"
|
:eyebrow="$settings->hero_eyebrow"
|
||||||
:title="$settings->hero_title ?: 'Celebrações com propósito'"
|
:title="$settings->hero_title ?: 'Celebrações com propósito'"
|
||||||
:summary="$settings->hero_subtitle"
|
:summary="$settings->hero_subtitle"
|
||||||
heading-id="hero-heading"
|
|
||||||
brand-mark
|
brand-mark
|
||||||
full-height-fallback
|
|
||||||
class="home-chapter"
|
|
||||||
data-chapter="hero"
|
|
||||||
>
|
>
|
||||||
<div class="space-y-5">
|
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<a href="{{ route('briefing') }}" data-testid="home-primary-cta" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover">
|
<a href="{{ route('contact') }}" data-testid="home-primary-cta" class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]">
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@if (filled($settings->hero_secondary_cta_label))
|
@if (filled($settings->hero_secondary_cta_label))
|
||||||
<a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
|
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filled($settings->hero_note))
|
@if (filled($settings->hero_note))
|
||||||
<p class="max-w-[470px] text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
|
<p class="mt-4 max-w-xl text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
</x-public.photo-hero>
|
</x-public.photo-hero>
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
@props([
|
@props([])
|
||||||
'settings',
|
|
||||||
])
|
|
||||||
|
|
||||||
@php
|
<section
|
||||||
$title = $settings->manifesto_title ?: 'Sofisticação que também se traduz em organização.';
|
aria-labelledby="manifesto-heading"
|
||||||
$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.';
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
$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.';
|
data-chapter="sobre"
|
||||||
@endphp
|
id="sobre"
|
||||||
|
>
|
||||||
<section id="amare" aria-labelledby="manifesto-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-24 md:py-32" data-chapter="manifesto">
|
<div class="container-amare grid gap-10 py-16 md:grid-cols-[0.8fr_1.2fr] md:gap-[90px] md:py-24" data-reveal-group>
|
||||||
<div class="container-amare grid gap-10 md:grid-cols-12" data-reveal-group>
|
<div data-reveal data-reveal-from="up">
|
||||||
<div class="space-y-5 md:col-span-4 md:pt-20" data-reveal data-reveal-from="left">
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">01 — A Amare</p>
|
||||||
<p class="max-w-52 text-lg leading-snug text-amare-text">Humana no cuidado. Precisa na entrega.</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-6 md:col-span-7 md:col-start-6" data-reveal data-reveal-from="up">
|
|
||||||
<h2 id="manifesto-heading" class="max-w-3xl text-headline font-medium text-amare-text">{{ $title }}</h2>
|
<div class="flex flex-col gap-8" data-reveal data-reveal-from="up">
|
||||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $lead }}</p>
|
<h2 id="manifesto-heading" class="text-[clamp(1.875rem,4vw,3.25rem)] font-medium leading-[1.15] tracking-[-0.02em] text-amare-text">
|
||||||
<p class="max-w-2xl text-amare-text-muted">{{ $body }}</p>
|
Mais do que organizar um evento, a Amare conduz pessoas, decisões e detalhes para que cada experiência aconteça com clareza e tranquilidade.
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="max-w-[760px] text-[clamp(1.1875rem,2vw,1.5rem)] leading-[1.55] text-amare-text-muted">
|
||||||
|
Aqui entra a apresentação institucional da marca e da Michele, com texto real aprovado pela cliente. A home deve construir confiança antes de pedir uma conversão.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
91
resources/views/components/home/packages.blade.php
Normal file
91
resources/views/components/home/packages.blade.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
@props([
|
||||||
|
'packages',
|
||||||
|
'settings',
|
||||||
|
'eyebrow' => '03 — Amare Casamentos',
|
||||||
|
'heading' => 'Um acompanhamento para cada momento.',
|
||||||
|
'intro' => 'As modalidades são pontos de partida. O site apresenta o conceito e ajuda o casal a reconhecer seu momento; detalhes operacionais e comerciais ficam para a conversa e proposta.',
|
||||||
|
'kicker' => 'level',
|
||||||
|
'showSubtitle' => false,
|
||||||
|
'showIntro' => true,
|
||||||
|
'showNote' => true,
|
||||||
|
'ctaRoute' => null,
|
||||||
|
'bandCtaHref' => null,
|
||||||
|
'bandBody' => 'Conte um pouco sobre o seu casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático.',
|
||||||
|
'note' => 'Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$whatsappDigits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
|
||||||
|
$hasWhatsapp = strlen($whatsappDigits) >= 10;
|
||||||
|
$bandHref = $bandCtaHref ?? route('briefing');
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section
|
||||||
|
aria-labelledby="packages-heading"
|
||||||
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
|
data-chapter="casamentos"
|
||||||
|
id="casamentos"
|
||||||
|
>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="flex flex-col gap-6 md:flex-row md:items-end md:justify-between" data-reveal data-reveal-from="up">
|
||||||
|
<div class="max-w-2xl space-y-4">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
|
||||||
|
<h2 id="packages-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
|
||||||
|
</div>
|
||||||
|
@if ($showIntro && filled($intro))
|
||||||
|
<p class="max-w-[600px] text-amare-text-muted">{{ $intro }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($packages->isNotEmpty())
|
||||||
|
<div class="mt-12 grid border-l border-t border-amare-border md:mt-16 md:grid-cols-3">
|
||||||
|
@foreach ($packages as $package)
|
||||||
|
<article class="flex min-h-[480px] flex-col justify-between gap-8 border-b border-r border-amare-border p-8 md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ $kicker === 'tag' ? $package->tag : $package->level }}</p>
|
||||||
|
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">{{ $package->name }}</h3>
|
||||||
|
@if ($showSubtitle && filled($package->subtitle))
|
||||||
|
<p class="text-[clamp(1.0625rem,1.7vw,1.25rem)] font-medium leading-snug text-amare-text">{{ $package->subtitle }}</p>
|
||||||
|
@endif
|
||||||
|
<p class="text-amare-text-muted">{{ $package->summary }}</p>
|
||||||
|
<ul class="space-y-3 text-amare-text-muted">
|
||||||
|
@foreach ($package->scope_items as $item)
|
||||||
|
<li class="flex gap-3"><span aria-hidden="true" class="text-amare-accent">•</span><span>{{ $item }}</span></li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@php
|
||||||
|
$waLink = $hasWhatsapp
|
||||||
|
? 'https://wa.me/'.$whatsappDigits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.')
|
||||||
|
: route('briefing', ['servico_interesse' => $package->name]);
|
||||||
|
$href = $ctaRoute
|
||||||
|
? route($ctaRoute)
|
||||||
|
: $waLink;
|
||||||
|
@endphp
|
||||||
|
<a
|
||||||
|
href="{{ $href }}"
|
||||||
|
@if ($ctaRoute === null && $hasWhatsapp) target="_blank" rel="noopener noreferrer" @endif
|
||||||
|
class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]"
|
||||||
|
>
|
||||||
|
{{ $package->cta_label }}
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="mt-6 flex flex-col justify-between gap-8 bg-amare-accent-deep p-8 text-amare-accent-text md:flex-row md:items-center md:p-12" data-reveal data-reveal-from="up">
|
||||||
|
<div class="max-w-xl space-y-3">
|
||||||
|
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
|
||||||
|
<p class="text-amare-accent-text/80">{{ $bandBody }}</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ $bandHref }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto">
|
||||||
|
Conversar com a Amare
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($showNote && filled($note))
|
||||||
|
<p class="mt-6 text-sm text-amare-muted">{{ $note }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -2,67 +2,55 @@
|
|||||||
'cases',
|
'cases',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section id="portfolio" aria-labelledby="portfolio-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="portfolio">
|
<section
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
aria-labelledby="portfolio-heading"
|
||||||
<div class="grid gap-4 md:grid-cols-12" data-reveal data-reveal-from="up">
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
<div class="md:col-span-7 md:col-start-5 space-y-3">
|
data-chapter="portfolio"
|
||||||
<h2 id="portfolio-heading" class="text-headline font-medium">Celebrações que ganham forma com intenção.</h2>
|
id="portfolio"
|
||||||
<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 class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="flex flex-col gap-6 md:flex-row md:items-end md:justify-between" data-reveal data-reveal-from="up">
|
||||||
|
<h2 id="portfolio-heading" class="max-w-2xl text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Trabalhos que carregam a assinatura Amare.</h2>
|
||||||
|
<p class="max-w-[600px] text-amare-text-muted">Priorizar fotografias reais e uma curadoria pequena. O portfólio não precisa explicar tudo: deve provar cuidado, execução e consistência visual.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($cases->isNotEmpty())
|
|
||||||
<div class="grid gap-8 md:grid-cols-12 md:gap-x-8 md:gap-y-14" data-editorial-portfolio>
|
|
||||||
@foreach ($cases as $case)
|
|
||||||
@php
|
@php
|
||||||
$isFeature = $loop->first;
|
$slots = collect([
|
||||||
$isOffset = ! $isFeature && $loop->even;
|
['label' => 'FOTO DE CASAMENTO 01', 'tall' => true],
|
||||||
|
['label' => 'FOTO 02', 'tall' => false],
|
||||||
|
['label' => 'FOTO 03', 'tall' => false],
|
||||||
|
['label' => 'FOTO 04', 'tall' => false],
|
||||||
|
['label' => 'FOTO 05', 'tall' => false],
|
||||||
|
]);
|
||||||
|
$cases = $cases->take(5)->values();
|
||||||
@endphp
|
@endphp
|
||||||
<article
|
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-[14px] sm:grid-cols-2 lg:grid-cols-[1.2fr_0.8fr_0.8fr]" data-editorial-portfolio data-reveal-group>
|
||||||
|
@foreach ($slots as $index => $slot)
|
||||||
|
@php
|
||||||
|
$case = $cases->get($index);
|
||||||
|
@endphp
|
||||||
|
<div
|
||||||
@class([
|
@class([
|
||||||
'space-y-4 border-t border-amare-accent-text/30 pt-4',
|
'overflow-hidden bg-amare-bg-deep',
|
||||||
'md:col-span-7' => $isFeature,
|
'sm:col-span-2 sm:min-h-[420px] lg:col-span-1 lg:row-span-2 lg:min-h-[634px]' => $slot['tall'],
|
||||||
'md:col-span-5 md:col-start-8 md:pt-12' => $isOffset,
|
'min-h-[220px] sm:min-h-[310px]' => ! $slot['tall'],
|
||||||
'md:col-span-5' => ! $isFeature && ! $isOffset,
|
|
||||||
])
|
])
|
||||||
data-editorial-portfolio-item="{{ $isFeature ? 'feature' : ($isOffset ? 'offset' : 'standard') }}"
|
@if ($slot['tall']) data-editorial-portfolio-item="feature" @endif
|
||||||
data-reveal
|
data-reveal
|
||||||
data-reveal-from="up"
|
data-reveal-from="up"
|
||||||
>
|
>
|
||||||
@if (filled($case->cover_image_path))
|
@if ($case && filled($case->cover_image_path))
|
||||||
<x-media.image
|
<a href="{{ route('portfolio.show', $case) }}" class="block h-full w-full" aria-label="{{ $case->title }}">
|
||||||
:path="$case->cover_image_path"
|
<x-media.image :path="$case->cover_image_path" :alt="$case->cover_image_alt ?: $case->title" sizes="(max-width: 1023px) 100vw, 33vw" class="img-editorial h-full w-full object-cover transition-opacity hover:opacity-90" />
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
|
||||||
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
|
|
||||||
@class([
|
|
||||||
'img-editorial w-full object-cover',
|
|
||||||
'aspect-[5/4]' => $isFeature,
|
|
||||||
'aspect-[4/5]' => $isOffset,
|
|
||||||
'aspect-[4/3]' => ! $isFeature && ! $isOffset,
|
|
||||||
])
|
|
||||||
/>
|
|
||||||
@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 min-h-11 items-center text-sm font-semibold transition-colors hover:text-amare-accent-text">
|
|
||||||
<span class="border-b border-amare-accent-text pb-1">Ver caso</span>
|
|
||||||
</a>
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="flex h-full w-full items-center justify-center">
|
||||||
|
<p class="px-4 text-center text-xs font-semibold uppercase tracking-[0.16em] text-amare-muted">{{ $slot['label'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</article>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
@else
|
|
||||||
<p class="max-w-2xl border-t border-amare-accent-text/30 pt-5 text-amare-accent-text/80">Nosso acervo de eventos reais e autorizados está em preparação. Em breve, esta página reunirá recortes que contam melhor o trabalho da Amare.</p>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<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="inline-flex min-h-11 items-center text-sm font-semibold transition-colors hover:text-amare-accent-text">
|
|
||||||
<span class="border-b border-amare-accent-text pb-1">Conhecer o portfólio</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('about') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
<span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
|
<span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
<span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
|
<span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,46 +3,52 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$testimonials = $testimonials
|
$quotes = collect($testimonials)
|
||||||
->filter(fn ($testimonial): bool => filled(trim((string) $testimonial->quote)))
|
->filter(fn ($testimonial) => filled($testimonial->quote) && filled(trim((string) $testimonial->quote)))
|
||||||
|
->take(3)
|
||||||
->values();
|
->values();
|
||||||
|
$cards = $quotes->map(fn ($testimonial) => [
|
||||||
|
'quote' => $testimonial->quote,
|
||||||
|
'author' => $testimonial->author_name,
|
||||||
|
'context' => $testimonial->context,
|
||||||
|
])->all();
|
||||||
|
while (count($cards) < 3) {
|
||||||
|
$cards[] = [
|
||||||
|
'quote' => 'Depoimento real aprovado pela cliente entra aqui.',
|
||||||
|
'author' => 'Nome e data reais — aguardando autorização',
|
||||||
|
'context' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($testimonials->isNotEmpty())
|
<section
|
||||||
<section id="depoimentos" aria-labelledby="testimonials-heading" class="home-chapter border-b border-amare-border bg-amare-bg py-24" data-chapter="testimonials">
|
aria-labelledby="testimonials-heading"
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
<div class="max-w-2xl space-y-3 md:ml-[16.666667%]" data-reveal data-reveal-from="up">
|
data-chapter="depoimentos"
|
||||||
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
id="depoimentos"
|
||||||
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="space-y-4" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">06 — Depoimentos</p>
|
||||||
|
<h2 id="testimonials-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">Confiança construída na experiência.</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-12">
|
<div class="mt-12 grid gap-6 md:mt-16 md:grid-cols-3">
|
||||||
@foreach ($testimonials as $testimonial)
|
@foreach ($cards as $card)
|
||||||
@php
|
<figure class="flex flex-col justify-between gap-6 border border-amare-border bg-white p-8" data-reveal data-reveal-from="up">
|
||||||
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
<blockquote class="text-lg leading-[1.55] text-amare-text">
|
||||||
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
<p>{{ $card['quote'] }}</p>
|
||||||
@endphp
|
|
||||||
<blockquote @class([
|
|
||||||
'space-y-4 border-t border-amare-border pt-4 md:col-span-5' => $loop->odd,
|
|
||||||
'space-y-4 border-t border-amare-border pt-4 md:col-span-5 md:col-start-7 md:mt-16' => $loop->even,
|
|
||||||
]) data-reveal data-reveal-from="{{ $loop->odd ? 'left' : 'right' }}">
|
|
||||||
<div class="grid grid-cols-[1.5rem_minmax(0,1fr)] gap-2">
|
|
||||||
<span class="text-3xl leading-none text-amare-sage" aria-hidden="true">“</span>
|
|
||||||
<div class="space-y-3 text-lg text-amare-text">
|
|
||||||
@foreach ($paragraphs as $paragraph)
|
|
||||||
<p>{{ $paragraph }}</p>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<footer class="pl-8 text-sm text-amare-text-muted">
|
|
||||||
<cite class="not-italic font-semibold text-amare-text">{{ $testimonial->author_name }}</cite>
|
|
||||||
@if (filled($testimonial->context))
|
|
||||||
<span> — {{ $testimonial->context }}</span>
|
|
||||||
@endif
|
|
||||||
</footer>
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<figcaption class="text-sm text-amare-muted">
|
||||||
|
@if (filled($card['context']))
|
||||||
|
<p class="font-semibold text-amare-text">{{ $card['author'] }}</p>
|
||||||
|
<p>{{ $card['context'] }}</p>
|
||||||
|
@else
|
||||||
|
<p>{{ $card['author'] }}</p>
|
||||||
|
@endif
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@endif
|
|
||||||
|
|||||||
50
resources/views/components/home/vertentes.blade.php
Normal file
50
resources/views/components/home/vertentes.blade.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
@props([
|
||||||
|
'eyebrow' => '02 — Duas vertentes, uma mesma forma de cuidar',
|
||||||
|
'heading' => 'Como podemos acompanhar você?',
|
||||||
|
'cards' => [
|
||||||
|
[
|
||||||
|
'label' => 'Amare Casamentos',
|
||||||
|
'title' => 'Planejamento e condução para viver o processo com mais leveza.',
|
||||||
|
'body' => 'Do início da organização à reta final, modalidades pensadas para diferentes momentos do casal.',
|
||||||
|
'href' => '#casamentos',
|
||||||
|
'cta' => 'Conhecer casamentos',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Amare Corporate',
|
||||||
|
'title' => 'Eventos empresariais com organização, intenção e execução segura.',
|
||||||
|
'body' => 'Uma frente própria para projetos corporativos, com linguagem e necessidades diferentes das celebrações sociais.',
|
||||||
|
'href' => '#corporate',
|
||||||
|
'cta' => 'Conhecer Corporate',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
|
||||||
|
<section
|
||||||
|
aria-labelledby="vertentes-heading"
|
||||||
|
class="home-chapter border-b border-amare-border bg-amare-bg"
|
||||||
|
data-chapter="vertentes"
|
||||||
|
id="vertentes"
|
||||||
|
>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="max-w-3xl space-y-4" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $eyebrow }}</p>
|
||||||
|
<h2 id="vertentes-heading" class="text-[clamp(2.375rem,5vw,4rem)] font-medium leading-[1.05] tracking-[-0.02em] text-amare-text">{{ $heading }}</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 grid gap-6 md:mt-16 md:grid-cols-2 md:gap-[24px]">
|
||||||
|
@foreach ($cards as $card)
|
||||||
|
<article class="relative flex min-h-[420px] flex-col justify-end bg-amare-bg-deep p-8 md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<div aria-hidden="true" class="pointer-events-none absolute inset-x-0 top-0 h-[43%] bg-gradient-to-b from-amare-bg to-transparent"></div>
|
||||||
|
<div class="relative flex flex-col gap-5">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $card['label'] }}</p>
|
||||||
|
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">{{ $card['title'] }}</h3>
|
||||||
|
<p class="text-amare-text-muted">{{ $card['body'] }}</p>
|
||||||
|
<a href="{{ $card['href'] }}" class="btn btn-outline mt-2 self-start text-xs font-bold uppercase tracking-[0.09em]">
|
||||||
|
{{ $card['cta'] }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
@foreach ($packages as $package)
|
@foreach ($packages as $package)
|
||||||
@php($digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number))
|
@php($digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number))
|
||||||
@php($href = strlen((string) $digits) >= 10 ? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.') : route('briefing', ['servico_interesse' => $package->name]))
|
@php($href = strlen((string) $digits) >= 10 ? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.') : route('briefing', ['servico_interesse' => $package->name]))
|
||||||
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><a href="{{ $href }}" @if (str_starts_with($href, 'https://wa.me/')) target="_blank" rel="noopener" @endif class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">{{ $package->cta_label }}</span></a></li>
|
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><a href="{{ $href }}" @if (str_starts_with($href, 'https://wa.me/')) target="_blank" rel="noopener" @endif class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">{{ $package->cta_label }}</span></a></li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ol>
|
</ol>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Página não encontrada</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Página não encontrada</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center 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">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Sessão expirada</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Sessão expirada</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center 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">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Muitas solicitações</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Muitas solicitações</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center 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">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Algo deu errado</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Algo deu errado</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center 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">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Em manutenção</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Em manutenção</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center 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">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="amare">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@@ -55,28 +55,29 @@
|
|||||||
Ir para o conteúdo
|
Ir para o conteúdo
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 backdrop-blur-sm">
|
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 backdrop-blur-sm">
|
||||||
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr]">
|
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr] md:h-[74px] md:py-0">
|
||||||
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||||
<a href="{{ route('home') }}#amare" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Amare</a>
|
<a href="{{ route('home') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Início</a>
|
||||||
<a href="{{ route('home') }}#casamentos" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Casamentos</a>
|
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Serviços</a>
|
||||||
<a href="{{ route('home') }}#corporate" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Corporate</a>
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Portfólio</a>
|
||||||
<a href="{{ route('home') }}#portfolio" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Portfólio</a>
|
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Amare</a>
|
||||||
<a href="{{ route('briefing') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
<a href="{{ route('briefing') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
||||||
</nav>
|
</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">
|
<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" />
|
<x-brand.logo variant="on-light" class="h-10 w-auto md:h-11" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
||||||
<a href="{{ route('briefing') }}" class="hidden items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent transition-colors hover:text-amare-accent-deep md:inline-flex">
|
<a href="{{ route('briefing') }}" class="btn btn-outline hidden min-h-0 px-6 text-xs font-bold uppercase tracking-[0.09em] md:inline-flex">
|
||||||
Solicitar proposta
|
Conte seu evento
|
||||||
|
</a>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-button inline-flex h-11 w-11 items-center justify-center border border-amare-border text-amare-text md:hidden"
|
class="menu-button btn btn-square btn-ghost border border-amare-border text-amare-text md:hidden"
|
||||||
aria-label="Abrir menu"
|
aria-label="Abrir menu"
|
||||||
aria-controls="main-nav"
|
aria-controls="main-nav"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
@@ -97,50 +98,34 @@
|
|||||||
@yield('content')
|
@yield('content')
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="border-t border-amare-border bg-amare-bg-deep">
|
<footer class="border-t border-amare-border bg-amare-bg">
|
||||||
<div class="container-amare grid gap-10 py-12 md:grid-cols-[minmax(0,1.4fr)_repeat(2,minmax(0,1fr))]">
|
@php
|
||||||
<div class="space-y-4">
|
$footerSocialLabel = static function (string $network): string {
|
||||||
<x-brand.logo variant="on-light" class="h-12 w-auto" />
|
return match ($network) {
|
||||||
<p class="max-w-md text-amare-muted">
|
'instagram' => 'Instagram',
|
||||||
{{ $siteSettings->about_summary ?: 'Assessoria boutique em São Paulo - SP.' }}
|
'whatsapp' => 'WhatsApp',
|
||||||
</p>
|
'linkedin' => 'LinkedIn',
|
||||||
|
default => ucfirst($network),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="container-amare flex flex-col gap-8 py-14 md:flex-row md:items-end md:justify-between md:py-[58px]">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
||||||
|
<p class="text-sm text-amare-muted">Assessoria & produção de eventos • São Paulo</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-sm">
|
<p class="text-sm text-amare-muted md:text-right">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Navegação</h2>
|
@if ($footerSocials->isEmpty())
|
||||||
<ul class="mt-3 space-y-3">
|
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
||||||
<li><a href="{{ route('home') }}#casamentos" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Casamentos</a></li>
|
@else
|
||||||
<li><a href="{{ route('home') }}#corporate" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Corporate</a></li>
|
@foreach ($footerSocials as $network => $url)
|
||||||
<li><a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Portfólio</a></li>
|
<a href="{{ $url }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ $footerSocialLabel((string) $network) }}</a>@unless ($loop->last) · @endunless
|
||||||
<li><a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Contato</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm">
|
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Contato</h2>
|
|
||||||
<address class="mt-3 space-y-3 not-italic">
|
|
||||||
@if ($siteSettings->city)
|
|
||||||
<p class="text-amare-muted">{{ $siteSettings->city }}</p>
|
|
||||||
@endif
|
|
||||||
@if ($siteSettings->email)
|
|
||||||
<p>
|
|
||||||
<a href="mailto:{{ $siteSettings->email }}" class="inline-flex max-w-full min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent"><span class="min-w-0 break-words">{{ $siteSettings->email }}</span></a>
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
@if ($siteSettings->phone)
|
|
||||||
<p>
|
|
||||||
<a href="tel:{{ preg_replace('/\D/', '', (string) $siteSettings->phone) }}" class="inline-flex max-w-full min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent"><span class="min-w-0 break-words">{{ $siteSettings->phone }}</span></a>
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
@foreach ($siteSettings->social_links ?? [] as $network => $url)
|
|
||||||
@if (filled($url))
|
|
||||||
<p>
|
|
||||||
<a href="{{ $url }}" class="inline-flex max-w-full min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank"><span class="min-w-0 break-words">{{ ucfirst((string) $network) }}</span></a>
|
|
||||||
</p>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</address>
|
@endif
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-amare-border">
|
<div class="border-t border-amare-border">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (session('status') === 'briefing-sent')
|
@if (session('status') === 'briefing-sent')
|
||||||
<div role="status" class="mt-8 border border-amare-border bg-amare-bg-deep px-6 py-5">
|
<div role="status" class="alert mt-8 border border-amare-border bg-amare-bg-deep">
|
||||||
<p class="font-semibold text-amare-text">Mensagem enviada.</p>
|
<p class="font-semibold text-amare-text">Mensagem enviada.</p>
|
||||||
<p class="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
|
<p class="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
<div role="alert" class="border border-amare-error/40 bg-amare-error/5 px-6 py-5">
|
<div role="alert" class="alert alert-error border border-amare-error/40">
|
||||||
<p class="font-semibold text-amare-error">Não foi possível enviar.</p>
|
<p class="font-semibold text-amare-error">Não foi possível enviar.</p>
|
||||||
<p class="mt-1 text-sm text-amare-muted">Revise os campos destacados abaixo e tente novamente.</p>
|
<p class="mt-1 text-sm text-amare-muted">Revise os campos destacados abaixo e tente novamente.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Seu nome"
|
placeholder="Seu nome"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('nome') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
|
||||||
@error('nome') aria-invalid="true" aria-describedby="nome-error" @enderror
|
@error('nome') aria-invalid="true" aria-describedby="nome-error" @enderror
|
||||||
>
|
>
|
||||||
@error('nome')
|
@error('nome')
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
maxlength="254"
|
maxlength="254"
|
||||||
placeholder="voce@email.com"
|
placeholder="voce@email.com"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('email') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
|
||||||
@error('email') aria-invalid="true" aria-describedby="email-error" @enderror
|
@error('email') aria-invalid="true" aria-describedby="email-error" @enderror
|
||||||
>
|
>
|
||||||
@error('email')
|
@error('email')
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
maxlength="40"
|
maxlength="40"
|
||||||
placeholder="(11) 90000-0000"
|
placeholder="(11) 90000-0000"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('telefone') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
|
||||||
@error('telefone') aria-invalid="true" aria-describedby="telefone-error" @enderror
|
@error('telefone') aria-invalid="true" aria-describedby="telefone-error" @enderror
|
||||||
>
|
>
|
||||||
@error('telefone')
|
@error('telefone')
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
id="tipo_evento"
|
id="tipo_evento"
|
||||||
name="tipo_evento"
|
name="tipo_evento"
|
||||||
required
|
required
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors focus:border-amare-accent focus:outline-none @error('tipo_evento') border-amare-error @enderror"
|
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
|
||||||
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
||||||
>
|
>
|
||||||
<option value="" selected disabled>Selecione...</option>
|
<option value="" selected disabled>Selecione...</option>
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
value="{{ old('data_periodo') }}"
|
value="{{ old('data_periodo') }}"
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
placeholder="Ex.: novembro de 2027"
|
placeholder="Ex.: novembro de 2027"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('data_periodo') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
|
||||||
@error('data_periodo') aria-invalid="true" aria-describedby="data_periodo-error" @enderror
|
@error('data_periodo') aria-invalid="true" aria-describedby="data_periodo-error" @enderror
|
||||||
>
|
>
|
||||||
@error('data_periodo')
|
@error('data_periodo')
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
required
|
required
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
placeholder="Ex.: São Paulo"
|
placeholder="Ex.: São Paulo"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('cidade') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
|
||||||
@error('cidade') aria-invalid="true" aria-describedby="cidade-error" @enderror
|
@error('cidade') aria-invalid="true" aria-describedby="cidade-error" @enderror
|
||||||
>
|
>
|
||||||
@error('cidade')
|
@error('cidade')
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="Ex.: 120"
|
placeholder="Ex.: 120"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('convidados') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
|
||||||
@error('convidados') aria-invalid="true" aria-describedby="convidados-error" @enderror
|
@error('convidados') aria-invalid="true" aria-describedby="convidados-error" @enderror
|
||||||
>
|
>
|
||||||
@error('convidados')
|
@error('convidados')
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
value="{{ old('servico_interesse', request('servico_interesse')) }}"
|
value="{{ old('servico_interesse', request('servico_interesse')) }}"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Ex.: planejamento completo"
|
placeholder="Ex.: planejamento completo"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('servico_interesse') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
|
||||||
@error('servico_interesse') aria-invalid="true" aria-describedby="servico_interesse-error" @enderror
|
@error('servico_interesse') aria-invalid="true" aria-describedby="servico_interesse-error" @enderror
|
||||||
>
|
>
|
||||||
@error('servico_interesse')
|
@error('servico_interesse')
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
rows="6"
|
rows="6"
|
||||||
maxlength="3000"
|
maxlength="3000"
|
||||||
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
|
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('mensagem') border-amare-error @enderror"
|
class="textarea w-full text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
|
||||||
@error('mensagem') aria-invalid="true" aria-describedby="mensagem-error" @enderror
|
@error('mensagem') aria-invalid="true" aria-describedby="mensagem-error" @enderror
|
||||||
>{{ old('mensagem') }}</textarea>
|
>{{ old('mensagem') }}</textarea>
|
||||||
@error('mensagem')
|
@error('mensagem')
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
value="1"
|
value="1"
|
||||||
required
|
required
|
||||||
@checked(old('privacidade'))
|
@checked(old('privacidade'))
|
||||||
class="mt-1 h-5 w-5 shrink-0 accent-amare-accent"
|
class="checkbox checkbox-primary mt-1 shrink-0"
|
||||||
@error('privacidade') aria-invalid="true" aria-describedby="privacidade-error" @enderror
|
@error('privacidade') aria-invalid="true" aria-describedby="privacidade-error" @enderror
|
||||||
>
|
>
|
||||||
<span class="text-sm text-amare-muted">
|
<span class="text-sm text-amare-muted">
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
data-submit-button
|
data-submit-button
|
||||||
class="inline-flex min-h-[44px] items-center justify-center bg-amare-accent px-8 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep"
|
class="btn btn-primary min-h-11 px-8 text-sm font-semibold uppercase tracking-[0.12em]"
|
||||||
>
|
>
|
||||||
Enviar briefing
|
Enviar briefing
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,14 +3,12 @@
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="home-chapters">
|
<div class="home-chapters">
|
||||||
<x-home.hero :settings="$content->settings" />
|
<x-home.hero :settings="$content->settings" />
|
||||||
<x-home.manifesto :settings="$content->settings" />
|
<x-home.manifesto />
|
||||||
<x-home.services :services="$content->featuredServices" />
|
<x-home.vertentes />
|
||||||
<x-home.wedding-packages :packages="$content->weddingPackages" :settings="$content->settings" />
|
<x-home.packages :packages="$content->packages" :settings="$content->settings" />
|
||||||
<x-home.corporate />
|
<x-home.corporate :settings="$content->settings" />
|
||||||
<x-home.portfolio :cases="$content->featuredCases" />
|
<x-home.portfolio :cases="$content->featuredCases" />
|
||||||
<x-home.method :settings="$content->settings" />
|
|
||||||
<x-home.testimonials :testimonials="$content->testimonials" />
|
<x-home.testimonials :testimonials="$content->testimonials" />
|
||||||
<x-home.positioning :settings="$content->settings" />
|
<x-home.briefing />
|
||||||
<x-home.final-cta :settings="$content->settings" editorial />
|
|
||||||
</div>
|
</div>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
|
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
|
||||||
<p class="max-w-2xl text-sm text-amare-muted">Ao enviar, você reconhece o uso destes dados exclusivamente para analisar uma possível parceria, conforme a <a class="text-amare-accent underline" href="{{ route('privacy') }}">política de privacidade</a>.</p>
|
<p class="max-w-2xl text-sm text-amare-muted">Ao enviar, você reconhece o uso destes dados exclusivamente para analisar uma possível parceria, conforme a <a class="text-amare-accent underline" href="{{ route('privacy') }}">política de privacidade</a>.</p>
|
||||||
<button type="submit" data-submit-button class="inline-flex min-h-11 bg-amare-accent px-6 py-3 text-sm font-semibold uppercase tracking-[.12em] text-amare-accent-text hover:bg-amare-accent-deep">Enviar apresentação</button>
|
<button type="submit" data-submit-button class="btn btn-primary text-sm font-semibold uppercase tracking-[.12em]">Enviar apresentação</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,55 +1,92 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<x-public.photo-hero :image-path="$siteSettings->services_hero_image_path" :image-alt="$siteSettings->services_hero_image_alt" eyebrow="Serviços" title="Uma mesma excelência, diferentes ocasiões." summary="O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento." />
|
<x-public.photo-hero
|
||||||
|
:image-path="$siteSettings->services_hero_image_path"
|
||||||
<div class="border-b border-amare-border bg-amare-bg">
|
:image-alt="$siteSettings->services_hero_image_alt"
|
||||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
eyebrow="Serviços"
|
||||||
|
title="O cuidado certo para cada tipo de evento."
|
||||||
@if ($services->isEmpty())
|
summary="A Amare acompanha projetos sociais e corporativos com planejamento, organização e condução profissional. O formato muda conforme o momento, o evento e a necessidade de cada cliente."
|
||||||
<p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p>
|
>
|
||||||
@else
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
@php($hasPrioritizedImage = false)
|
<a href="#vertentes" class="btn btn-primary text-xs font-bold uppercase tracking-[0.09em]">
|
||||||
<div class="divide-y divide-amare-border border-y border-amare-border" data-editorial-service-list data-reveal-group>
|
Conhecer os serviços
|
||||||
@foreach ($services as $index => $service)
|
</a>
|
||||||
<article @class([
|
<a href="{{ route('contact') }}" class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
'grid gap-4 py-8 md:grid-cols-[5rem_minmax(0,1fr)_minmax(0,1.2fr)] md:items-start',
|
<span class="border-b border-amare-accent pb-1">Tenho um evento em mente</span>
|
||||||
'md:translate-x-8' => $loop->even,
|
</a>
|
||||||
]) data-reveal data-reveal-from="up">
|
|
||||||
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
|
||||||
<div class="space-y-3">
|
|
||||||
<h2 class="text-2xl font-medium text-amare-text md:text-3xl">{{ $service->title }}</h2>
|
|
||||||
<p class="text-amare-muted">{{ $service->summary }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
</x-public.photo-hero>
|
||||||
@if (filled($service->cover_image_path))
|
|
||||||
@php($prioritizeImage = ! $hasPrioritizedImage)
|
<x-home.vertentes
|
||||||
<x-media.image
|
eyebrow="Duas vertentes"
|
||||||
:path="$service->cover_image_path"
|
heading="Escolha por contexto, não por uma lista de pacotes."
|
||||||
:alt="$service->cover_image_alt ?: $service->title"
|
:cards="[
|
||||||
:loading="$prioritizeImage ? 'eager' : 'lazy'"
|
[
|
||||||
:fetchpriority="$prioritizeImage ? 'high' : null"
|
'label' => 'Amare Casamentos',
|
||||||
sizes="(max-width: 768px) calc(100vw - 3rem), 40vw"
|
'title' => 'Acompanhamento para diferentes momentos do planejamento.',
|
||||||
class="img-editorial aspect-[16/10] w-full object-cover"
|
'body' => 'Três modalidades para casais que estão começando, já avançaram ou precisam de apoio na reta final.',
|
||||||
|
'href' => '#casamentos',
|
||||||
|
'cta' => 'Ver modalidades',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Amare Corporate',
|
||||||
|
'title' => 'Planejamento e execução para eventos empresariais.',
|
||||||
|
'body' => 'Uma frente própria, com linguagem profissional e estrutura adequada a projetos corporativos.',
|
||||||
|
'href' => '#corporate',
|
||||||
|
'cta' => 'Ver Corporate',
|
||||||
|
],
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-home.packages
|
||||||
|
:packages="$weddingPackages"
|
||||||
|
:settings="$siteSettings"
|
||||||
|
eyebrow="Amare Casamentos"
|
||||||
|
heading="Três formas de ter a Amare ao lado."
|
||||||
|
:show-intro="false"
|
||||||
|
kicker="tag"
|
||||||
|
:show-subtitle="true"
|
||||||
|
cta-route="contact"
|
||||||
|
:band-cta-href="route('contact')"
|
||||||
|
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
|
||||||
|
:show-note="false"
|
||||||
/>
|
/>
|
||||||
@php($hasPrioritizedImage = true)
|
|
||||||
@endif
|
|
||||||
@if (filled($service->description))
|
|
||||||
<div class="text-amare-muted">
|
|
||||||
{!! nl2br(e($service->description)) !!}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($weddingPackages->isNotEmpty())
|
@if ($weddingPackages->isNotEmpty())
|
||||||
<section class="border-b border-amare-border bg-amare-bg-deep py-16 md:py-24" aria-labelledby="wedding-packages-heading"><div class="container-amare"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">Casamentos</p><h2 id="wedding-packages-heading" class="mt-3 text-3xl font-medium">Modalidades de assessoria</h2><div class="mt-8 grid gap-6 md:grid-cols-3">@foreach ($weddingPackages as $package)<article class="border-t border-amare-border pt-4"><p class="text-sm text-amare-accent">{{ $package->level }}</p><h3 class="mt-2 text-2xl font-medium">{{ $package->name }}</h3><p class="mt-2 text-amare-muted">{{ $package->summary }}</p><ul class="mt-4 space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li>{{ $item }}</li>@endforeach</ul></article>@endforeach</div></div></section>
|
<section aria-label="Comparação rápida entre as modalidades" class="border-b border-amare-border bg-amare-bg">
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="grid border-l border-t border-amare-border md:grid-cols-3">
|
||||||
|
@foreach ($weddingPackages as $package)
|
||||||
|
@if (filled($package->compare_heading))
|
||||||
|
<article class="flex min-h-[300px] flex-col justify-between gap-6 border-b border-r border-amare-border p-8 md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent">{{ $package->name }}</p>
|
||||||
|
<h3 class="text-[clamp(1.25rem,2vw,1.5rem)] font-medium leading-tight text-amare-text">{{ $package->compare_heading }}</h3>
|
||||||
|
<p class="text-amare-text-muted">{{ $package->compare_summary }}</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<x-home.final-cta :settings="$siteSettings" />
|
<x-home.corporate
|
||||||
|
:settings="$siteSettings"
|
||||||
|
eyebrow="Amare Corporate"
|
||||||
|
heading="Organização, produção e condução com linguagem empresarial."
|
||||||
|
:show-meta="false"
|
||||||
|
:steps="[
|
||||||
|
['title' => 'Planejamento', 'body' => 'Estruturação do evento, escopo, prioridades e cronograma.'],
|
||||||
|
['title' => 'Produção', 'body' => 'Coordenação dos elementos e fornecedores necessários ao projeto.'],
|
||||||
|
['title' => 'Execução', 'body' => 'Acompanhamento e condução do evento conforme o planejamento aprovado.'],
|
||||||
|
]"
|
||||||
|
cta-label="Falar sobre um evento Corporate"
|
||||||
|
:cta-href="route('contact')"
|
||||||
|
:show-aside="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<x-home.final-cta :settings="$siteSettings" :cta-href="route('contact')" />
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ uses(RefreshDatabase::class);
|
|||||||
|
|
||||||
beforeEach(function (): void {
|
beforeEach(function (): void {
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
});
|
|
||||||
|
|
||||||
it('has no critical or serious accessibility issues on covered public routes', function (): void {
|
$this->portfolioCase = PortfolioCase::factory()->published()->create([
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
'slug' => 'casamento-jardim',
|
||||||
'title' => 'Casamento Jardim',
|
'title' => 'Casamento Jardim',
|
||||||
]);
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has no critical accessibility or console issues on covered public routes', function (): void {
|
||||||
$routes = [
|
$routes = [
|
||||||
'/',
|
'/',
|
||||||
'/servicos',
|
'/servicos',
|
||||||
'/portfolio',
|
'/portfolio',
|
||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$this->portfolioCase->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
'/briefing',
|
'/briefing',
|
||||||
@@ -30,9 +30,16 @@ it('has no critical or serious accessibility issues on covered public routes', f
|
|||||||
'/__missing-accessibility-page',
|
'/__missing-accessibility-page',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
$page = $this->visit($routes[0]);
|
||||||
$this->visit($route)
|
|
||||||
->assertNoAccessibilityIssues(1);
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
$page
|
||||||
|
->assertNoAccessibilityIssues(1)
|
||||||
|
->assertNoJavaScriptErrors();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,52 +91,32 @@ it('reaches the primary CTA by keyboard and activates it', function (): void {
|
|||||||
$outlineStyle = $page->script('() => getComputedStyle(document.activeElement).outlineStyle');
|
$outlineStyle = $page->script('() => getComputedStyle(document.activeElement).outlineStyle');
|
||||||
expect($outlineStyle)->not->toBe('none');
|
expect($outlineStyle)->not->toBe('none');
|
||||||
|
|
||||||
$page->keys('[data-testid="home-primary-cta"]', 'Enter')
|
$page->keys('[data-testid="home-primary-cta"]', 'Enter');
|
||||||
->assertPathIs('/briefing');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('loads covered public routes without console errors', function (): void {
|
$page->assertPathIs('/contato');
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ([
|
|
||||||
'/',
|
|
||||||
'/servicos',
|
|
||||||
'/portfolio',
|
|
||||||
'/portfolio/'.$case->slug,
|
|
||||||
'/sobre',
|
|
||||||
'/contato',
|
|
||||||
'/briefing',
|
|
||||||
'/privacidade',
|
|
||||||
'/__missing-console-page',
|
|
||||||
] as $route) {
|
|
||||||
$this->visit($route)
|
|
||||||
->assertNoJavaScriptErrors();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
|
||||||
'slug' => 'casamento-jardim',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$routes = [
|
$routes = [
|
||||||
'/',
|
'/',
|
||||||
'/servicos',
|
'/servicos',
|
||||||
'/portfolio',
|
'/portfolio',
|
||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$this->portfolioCase->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
'/briefing',
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($routes as $route) {
|
$page = $this->visit($routes[0], [
|
||||||
$page = $this->visit($route, [
|
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
$duration = $page->script(<<<'JS'
|
$duration = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
const probe = document.createElement('div');
|
const probe = document.createElement('div');
|
||||||
|
|||||||
@@ -12,22 +12,22 @@ beforeEach(function (): void {
|
|||||||
Artisan::call('db:seed', ['--class' => VisualContentSeeder::class, '--force' => true]);
|
Artisan::call('db:seed', ['--class' => VisualContentSeeder::class, '--force' => true]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses a five-seven desktop spread with a bounded reading column and full-bleed media', function (): void {
|
it('keeps the hero asymmetric on desktop and stacked through md and mobile', function (): void {
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
])->resize(1440, 1000);
|
]);
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$waitFrames = '() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))';
|
||||||
$page->script('() => document.fonts.ready');
|
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
$page->resize(1440, 1000);
|
||||||
|
$page->script($waitFrames);
|
||||||
|
|
||||||
|
$desktop = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
const header = document.querySelector('.site-header');
|
const header = document.querySelector('.site-header');
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
const content = hero?.querySelector('[data-hero-content]');
|
const content = hero?.querySelector('[data-hero-content]');
|
||||||
const media = hero?.querySelector('[data-split-hero]');
|
const media = hero?.querySelector('[data-split-hero]');
|
||||||
const heading = hero?.querySelector('h1');
|
|
||||||
const image = media?.querySelector('img');
|
|
||||||
const headerRect = header?.getBoundingClientRect();
|
const headerRect = header?.getBoundingClientRect();
|
||||||
const heroRect = hero?.getBoundingClientRect();
|
const heroRect = hero?.getBoundingClientRect();
|
||||||
const contentRect = content?.getBoundingClientRect();
|
const contentRect = content?.getBoundingClientRect();
|
||||||
@@ -36,51 +36,27 @@ it('uses a five-seven desktop spread with a bounded reading column and full-blee
|
|||||||
return {
|
return {
|
||||||
hasContentColumn: Boolean(content),
|
hasContentColumn: Boolean(content),
|
||||||
startsBelowHeader: Math.abs((heroRect?.top ?? -1) - (headerRect?.bottom ?? -2)) <= 1,
|
startsBelowHeader: Math.abs((heroRect?.top ?? -1) - (headerRect?.bottom ?? -2)) <= 1,
|
||||||
fillsRemainingViewport: (heroRect?.bottom ?? 0) >= window.innerHeight - 1,
|
mediaBesideContent: (mediaRect?.left ?? 0) >= (contentRect?.right ?? Number.POSITIVE_INFINITY) - 1,
|
||||||
mediaTouchesHeroTop: Math.abs((mediaRect?.top ?? -1) - (heroRect?.top ?? -2)) <= 1,
|
mediaMinHeight: Math.round(mediaRect?.height ?? 0) >= 519,
|
||||||
mediaTouchesHeroBottom: Math.abs((mediaRect?.bottom ?? -1) - (heroRect?.bottom ?? -2)) <= 1,
|
mediaTouchesViewportRight: (mediaRect?.right ?? 0) >= window.innerWidth - 4,
|
||||||
mediaTouchesViewportRight: Math.abs((mediaRect?.right ?? -1) - window.innerWidth) <= 1,
|
|
||||||
splitImageObjectFit: image ? getComputedStyle(image).objectFit : null,
|
|
||||||
columnRatio: Number(((contentRect?.width ?? 0) / (mediaRect?.width ?? 1)).toFixed(3)),
|
|
||||||
readingColumnWithinLimit: (heading?.getBoundingClientRect().width ?? Number.POSITIVE_INFINITY) <= 470,
|
|
||||||
titleClipsHorizontally: (() => {
|
|
||||||
if (!heading) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(heading);
|
|
||||||
|
|
||||||
return Array.from(range.getClientRects()).some((line) => line.right > heading.getBoundingClientRect().right + 1);
|
|
||||||
})(),
|
|
||||||
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($desktop)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'startsBelowHeader' => true,
|
'startsBelowHeader' => true,
|
||||||
'fillsRemainingViewport' => true,
|
'mediaBesideContent' => true,
|
||||||
'mediaTouchesHeroTop' => true,
|
'mediaMinHeight' => true,
|
||||||
'mediaTouchesHeroBottom' => true,
|
|
||||||
'mediaTouchesViewportRight' => true,
|
'mediaTouchesViewportRight' => true,
|
||||||
'splitImageObjectFit' => 'cover',
|
|
||||||
'columnRatio' => 0.714,
|
|
||||||
'readingColumnWithinLimit' => true,
|
|
||||||
'titleClipsHorizontally' => false,
|
|
||||||
'horizontalOverflow' => false,
|
'horizontalOverflow' => false,
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
it('keeps the home hero linear through tablet with a full-width four-by-five crop', function (): void {
|
$page->resize(767, 1000);
|
||||||
$page = $this->visit('/', [
|
$page->script($waitFrames);
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
])->resize(1023, 1000);
|
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$md = $page->script(<<<'JS'
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
const content = hero?.querySelector('[data-hero-content]');
|
const content = hero?.querySelector('[data-hero-content]');
|
||||||
@@ -91,30 +67,25 @@ it('keeps the home hero linear through tablet with a full-width four-by-five cro
|
|||||||
return {
|
return {
|
||||||
hasContentColumn: Boolean(content),
|
hasContentColumn: Boolean(content),
|
||||||
mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1,
|
mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1,
|
||||||
mediaFillsViewportWidth: Math.abs((mediaRect?.width ?? 0) - window.innerWidth) <= 1,
|
mediaFillsContainerWidth: (mediaRect?.width ?? 0) > window.innerWidth - 100,
|
||||||
mediaAspectRatio: Number(((mediaRect?.width ?? 0) / (mediaRect?.height ?? 1)).toFixed(3)),
|
mediaMinHeight: Math.round(mediaRect?.height ?? 0) >= 319,
|
||||||
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($md)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'mediaFollowsContent' => true,
|
'mediaFollowsContent' => true,
|
||||||
'mediaFillsViewportWidth' => true,
|
'mediaFillsContainerWidth' => true,
|
||||||
'mediaAspectRatio' => 0.8,
|
'mediaMinHeight' => true,
|
||||||
'horizontalOverflow' => false,
|
'horizontalOverflow' => false,
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
|
|
||||||
it('stacks the home hero media after its content as a full-width four-by-five crop on mobile', function (): void {
|
$page->resize(390, 844);
|
||||||
$page = $this->visit('/', [
|
$page->script($waitFrames);
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
])->resize(390, 844);
|
|
||||||
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
$mobile = $page->script(<<<'JS'
|
||||||
|
|
||||||
$layout = $page->script(<<<'JS'
|
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-chapter="hero"]');
|
const hero = document.querySelector('[data-chapter="hero"]');
|
||||||
const content = hero?.querySelector('[data-hero-content]');
|
const content = hero?.querySelector('[data-hero-content]');
|
||||||
@@ -125,27 +96,27 @@ it('stacks the home hero media after its content as a full-width four-by-five cr
|
|||||||
return {
|
return {
|
||||||
hasContentColumn: Boolean(content),
|
hasContentColumn: Boolean(content),
|
||||||
mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1,
|
mediaFollowsContent: (mediaRect?.top ?? 0) >= (contentRect?.bottom ?? Number.POSITIVE_INFINITY) - 1,
|
||||||
mediaFillsViewportWidth: Math.abs((mediaRect?.width ?? 0) - window.innerWidth) <= 1,
|
mediaFillsContainerWidth: (mediaRect?.width ?? 0) > window.innerWidth - 100,
|
||||||
mediaAspectRatio: Number(((mediaRect?.width ?? 0) / (mediaRect?.height ?? 1)).toFixed(3)),
|
|
||||||
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($layout)->toBe([
|
expect($mobile)->toBe([
|
||||||
'hasContentColumn' => true,
|
'hasContentColumn' => true,
|
||||||
'mediaFollowsContent' => true,
|
'mediaFollowsContent' => true,
|
||||||
'mediaFillsViewportWidth' => true,
|
'mediaFillsContainerWidth' => true,
|
||||||
'mediaAspectRatio' => 0.8,
|
|
||||||
'horizontalOverflow' => false,
|
'horizontalOverflow' => false,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the full home cadence accessible and contained at both viewports', function (): void {
|
it('keeps the full home cadence accessible and contained at both viewports', function (): void {
|
||||||
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
])->resize($width, $height);
|
]);
|
||||||
|
|
||||||
|
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
||||||
|
$page->resize($width, $height);
|
||||||
|
|
||||||
$page->assertNoAccessibilityIssues(1);
|
$page->assertNoAccessibilityIssues(1);
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,14 @@ beforeEach(function (): void {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps every home motion target in final visible state when reduced motion is preferred', function (): void {
|
it('keeps reduced-motion home final and free of ornamental chapter counters', function (): void {
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'is_featured' => true,
|
||||||
|
]);
|
||||||
|
Testimonial::factory()->published()->create([
|
||||||
|
'quote' => 'Relato publicado.',
|
||||||
|
]);
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
]);
|
]);
|
||||||
@@ -37,6 +44,9 @@ it('keeps every home motion target in final visible state when reduced motion is
|
|||||||
const titleStyle = title ? getComputedStyle(title) : null;
|
const titleStyle = title ? getComputedStyle(title) : null;
|
||||||
const finalReveals = [...document.querySelectorAll('[data-reveal]')]
|
const finalReveals = [...document.querySelectorAll('[data-reveal]')]
|
||||||
.every((node) => node.classList.contains('is-revealed'));
|
.every((node) => node.classList.contains('is-revealed'));
|
||||||
|
const flow = document.querySelector('.home-chapters');
|
||||||
|
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
|
||||||
|
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
heroActive: hero?.classList.contains('is-active') ?? false,
|
heroActive: hero?.classList.contains('is-active') ?? false,
|
||||||
@@ -45,6 +55,9 @@ it('keeps every home motion target in final visible state when reduced motion is
|
|||||||
hasActiveChapter: Boolean(active),
|
hasActiveChapter: Boolean(active),
|
||||||
finalReveals,
|
finalReveals,
|
||||||
enhancementEnabled: document.documentElement.hasAttribute('data-motion'),
|
enhancementEnabled: document.documentElement.hasAttribute('data-motion'),
|
||||||
|
reset: flow ? getComputedStyle(flow).counterReset : '',
|
||||||
|
increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement),
|
||||||
|
folioCount: folios.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
@@ -55,9 +68,12 @@ it('keeps every home motion target in final visible state when reduced motion is
|
|||||||
expect($state['hasActiveChapter'])->toBeFalse();
|
expect($state['hasActiveChapter'])->toBeFalse();
|
||||||
expect($state['finalReveals'])->toBeTrue();
|
expect($state['finalReveals'])->toBeTrue();
|
||||||
expect($state['enhancementEnabled'])->toBeFalse();
|
expect($state['enhancementEnabled'])->toBeFalse();
|
||||||
|
expect($state['reset'])->toBe('none');
|
||||||
|
expect($state['increments'])->each->toBe('none');
|
||||||
|
expect($state['folioCount'])->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('activates the home opening with 500ms beats and capped 90ms stagger', function (): void {
|
it('opens home motion, reveals once, and allows CTA during open', function (): void {
|
||||||
$page = $this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'reducedMotion' => 'no-preference',
|
'reducedMotion' => 'no-preference',
|
||||||
]);
|
]);
|
||||||
@@ -77,45 +93,39 @@ it('activates the home opening with 500ms beats and capped 90ms stagger', functi
|
|||||||
})
|
})
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$state = $page->script(<<<'JS'
|
$opening = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
const hero = document.querySelector('[data-motion="page-open"]');
|
const hero = document.querySelector('[data-motion="page-open"]');
|
||||||
const beatDurations = [...hero.querySelectorAll('[data-motion-beat]')]
|
const beatDurations = [...hero.querySelectorAll('[data-motion-beat]')]
|
||||||
.map((beat) => Number.parseFloat(getComputedStyle(beat).transitionDuration));
|
.map((beat) => Number.parseFloat(getComputedStyle(beat).transitionDuration));
|
||||||
const methodItems = [...document.querySelectorAll('[data-chapter="method"] [data-reveal]')];
|
const vertentesItems = [...document.querySelectorAll('[data-chapter="vertentes"] [data-reveal]')];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active: hero?.classList.contains('is-active') ?? false,
|
active: hero?.classList.contains('is-active') ?? false,
|
||||||
beatDurations,
|
beatDurations,
|
||||||
methodIndexes: methodItems.map((item) => item.style.getPropertyValue('--motion-index')),
|
vertentesIndexes: vertentesItems.map((item) => item.style.getPropertyValue('--motion-index')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($state['active'])->toBeTrue();
|
expect($opening['active'])->toBeTrue();
|
||||||
expect($state['beatDurations'])->each->toBe(0.5);
|
expect($opening['beatDurations'])->each->toBe(0.5);
|
||||||
expect($state['methodIndexes'])->toBe(['0', '1', '2', '3', '3']);
|
expect($opening['vertentesIndexes'])->toBe(['0', '1', '2']);
|
||||||
});
|
|
||||||
|
|
||||||
it('reveals a section once and keeps it final after returning in the scroll', function (): void {
|
$beforeReveal = $page->script(<<<'JS'
|
||||||
$page = $this->visit('/', [
|
() => document.querySelector('#briefing-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$before = $page->script(<<<'JS'
|
|
||||||
() => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
$page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
document.querySelector('#final-cta-heading')?.scrollIntoView({ block: 'center' });
|
document.querySelector('#briefing-heading')?.scrollIntoView({ block: 'center' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
$page->script(<<<'JS'
|
||||||
() => new Promise((resolve) => {
|
() => new Promise((resolve) => {
|
||||||
const target = document.querySelector('#final-cta-heading')?.closest('[data-reveal]');
|
const target = document.querySelector('#briefing-heading')?.closest('[data-reveal]');
|
||||||
const wait = () => {
|
const wait = () => {
|
||||||
if (target?.classList.contains('is-revealed')) {
|
if (target?.classList.contains('is-revealed')) {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
@@ -124,67 +134,44 @@ it('reveals a section once and keeps it final after returning in the scroll', fu
|
|||||||
requestAnimationFrame(wait);
|
requestAnimationFrame(wait);
|
||||||
};
|
};
|
||||||
wait();
|
wait();
|
||||||
|
setTimeout(() => resolve(false), 2000);
|
||||||
})
|
})
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->script('() => window.scrollTo({ top: 0, behavior: "instant" })');
|
$page->script('() => window.scrollTo({ top: 0, behavior: "instant" })');
|
||||||
|
|
||||||
$after = $page->script(<<<'JS'
|
$afterReveal = $page->script(<<<'JS'
|
||||||
() => document.querySelector('#final-cta-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
() => document.querySelector('#briefing-heading')?.closest('[data-reveal]')?.classList.contains('is-revealed') ?? false
|
||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect($before)->toBeFalse();
|
expect($beforeReveal)->toBeFalse();
|
||||||
expect($after)->toBeTrue();
|
expect($afterReveal)->toBeTrue();
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves testimonial directions with reduced lateral distance on mobile', function (): void {
|
$page->script(<<<'JS'
|
||||||
Testimonial::factory()->published()->create([
|
() => {
|
||||||
'quote' => 'Primeiro relato.',
|
const opening = document.querySelector('[data-motion="page-open"]');
|
||||||
'sort_order' => 10,
|
opening?.classList.remove('is-active');
|
||||||
]);
|
requestAnimationFrame(() => opening?.classList.add('is-active'));
|
||||||
Testimonial::factory()->published()->create([
|
return true;
|
||||||
'quote' => 'Segundo relato.',
|
}
|
||||||
'sort_order' => 20,
|
JS);
|
||||||
]);
|
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
$page->click('[data-testid="home-primary-cta"]');
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$readDistances = <<<'JS'
|
$page->assertPathIs('/contato');
|
||||||
() => [...document.querySelectorAll('[data-chapter="testimonials"] blockquote')]
|
|
||||||
.map((item) => ({
|
|
||||||
direction: item.dataset.revealFrom,
|
|
||||||
distance: getComputedStyle(item).getPropertyValue('--motion-translate-x').trim(),
|
|
||||||
}))
|
|
||||||
JS;
|
|
||||||
|
|
||||||
$page->resize(1440, 1000);
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
|
||||||
$desktop = $page->script($readDistances);
|
|
||||||
|
|
||||||
$page->resize(390, 844);
|
|
||||||
$page->script('() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)))');
|
|
||||||
$mobile = $page->script($readDistances);
|
|
||||||
|
|
||||||
expect($desktop)->toBe([
|
|
||||||
['direction' => 'left', 'distance' => 'calc(-1 * 24px)'],
|
|
||||||
['direction' => 'right', 'distance' => '24px'],
|
|
||||||
]);
|
|
||||||
expect($mobile)->toBe([
|
|
||||||
['direction' => 'left', 'distance' => 'calc(-1 * 16px)'],
|
|
||||||
['direction' => 'right', 'distance' => '16px'],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps content and navigation usable without javascript', function (): void {
|
it('keeps content and navigation usable without javascript', function (): void {
|
||||||
$this->visit('/', [
|
$page = $this->visit('/', [
|
||||||
'javaScriptEnabled' => false,
|
'javaScriptEnabled' => false,
|
||||||
])
|
]);
|
||||||
|
|
||||||
|
$page
|
||||||
->assertVisible('#hero-heading')
|
->assertVisible('#hero-heading')
|
||||||
->assertVisible('[data-testid="home-primary-cta"]')
|
->assertVisible('[data-testid="home-primary-cta"]')
|
||||||
->click('[data-testid="home-primary-cta"]')
|
->click('[data-testid="home-primary-cta"]');
|
||||||
->assertPathIs('/briefing');
|
|
||||||
|
$page->assertPathIs('/contato');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps targets final when intersection observer is unavailable', function (): void {
|
it('keeps targets final when intersection observer is unavailable', function (): void {
|
||||||
@@ -212,24 +199,6 @@ it('keeps targets final when intersection observer is unavailable', function ():
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows cta activation while its opening is in progress', function (): void {
|
|
||||||
$page = $this->visit('/', [
|
|
||||||
'reducedMotion' => 'no-preference',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$page->script(<<<'JS'
|
|
||||||
() => {
|
|
||||||
const opening = document.querySelector('[data-motion="page-open"]');
|
|
||||||
opening?.classList.remove('is-active');
|
|
||||||
requestAnimationFrame(() => opening?.classList.add('is-active'));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
JS);
|
|
||||||
|
|
||||||
$page->click('[data-testid="home-primary-cta"]')
|
|
||||||
->assertPathIs('/briefing');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
||||||
$case = PortfolioCase::factory()->published()->create([
|
$case = PortfolioCase::factory()->published()->create([
|
||||||
'slug' => 'casamento-jardim',
|
'slug' => 'casamento-jardim',
|
||||||
@@ -247,11 +216,19 @@ it('avoids horizontal overflow on visual public routes at desktop and mobile', f
|
|||||||
'/__missing-public-motion',
|
'/__missing-public-motion',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
|
$viewports = [[1440, 1000], [390, 844]];
|
||||||
foreach ($routes as $route) {
|
|
||||||
$page = $this->visit($route, [
|
$page = $this->visit($routes[0], [
|
||||||
'reducedMotion' => 'no-preference',
|
'reducedMotion' => 'no-preference',
|
||||||
])->resize($width, $height);
|
]);
|
||||||
|
|
||||||
|
foreach ($routes as $index => $route) {
|
||||||
|
if ($index > 0) {
|
||||||
|
$page->navigate($route);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($viewports as [$width, $height]) {
|
||||||
|
$page->resize($width, $height);
|
||||||
|
|
||||||
$overflow = $page->script('() => document.documentElement.scrollWidth - document.documentElement.clientWidth');
|
$overflow = $page->script('() => document.documentElement.scrollWidth - document.documentElement.clientWidth');
|
||||||
|
|
||||||
@@ -259,34 +236,3 @@ it('avoids horizontal overflow on visual public routes at desktop and mobile', f
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps rendered home chapters free of ornamental folios and counters', function (): void {
|
|
||||||
PortfolioCase::factory()->published()->create([
|
|
||||||
'is_featured' => true,
|
|
||||||
]);
|
|
||||||
Testimonial::factory()->published()->create([
|
|
||||||
'quote' => 'Relato publicado.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$page = $this->visit('/', [
|
|
||||||
'reducedMotion' => 'reduce',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$editorial = $page->script(<<<'JS'
|
|
||||||
() => {
|
|
||||||
const flow = document.querySelector('.home-chapters');
|
|
||||||
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
|
|
||||||
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
reset: flow ? getComputedStyle(flow).counterReset : '',
|
|
||||||
increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement),
|
|
||||||
folioCount: folios.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
JS);
|
|
||||||
|
|
||||||
expect($editorial['reset'])->toBe('none');
|
|
||||||
expect($editorial['increments'])->each->toBe('none');
|
|
||||||
expect($editorial['folioCount'])->toBe(0);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -33,6 +34,10 @@ class GetHomeContentTest extends TestCase
|
|||||||
$testimonialB = Testimonial::factory()->published()->create(['author_name' => 'Author B', 'sort_order' => 20]);
|
$testimonialB = Testimonial::factory()->published()->create(['author_name' => 'Author B', 'sort_order' => 20]);
|
||||||
$testimonialA = Testimonial::factory()->published()->create(['author_name' => 'Author A', 'sort_order' => 10]);
|
$testimonialA = Testimonial::factory()->published()->create(['author_name' => 'Author A', 'sort_order' => 10]);
|
||||||
|
|
||||||
|
WeddingPackage::factory()->create(['name' => 'Draft Package', 'published_at' => null]);
|
||||||
|
$packageB = WeddingPackage::factory()->published()->create(['name' => 'Package B', 'sort_order' => 20]);
|
||||||
|
$packageA = WeddingPackage::factory()->published()->create(['name' => 'Package A', 'sort_order' => 10]);
|
||||||
|
|
||||||
$content = (new GetHomeContent)();
|
$content = (new GetHomeContent)();
|
||||||
|
|
||||||
$this->assertTrue($content->settings->is($settings));
|
$this->assertTrue($content->settings->is($settings));
|
||||||
@@ -46,5 +51,8 @@ class GetHomeContentTest extends TestCase
|
|||||||
$this->assertCount(2, $content->testimonials);
|
$this->assertCount(2, $content->testimonials);
|
||||||
$this->assertTrue($content->testimonials->get(0)?->is($testimonialA));
|
$this->assertTrue($content->testimonials->get(0)?->is($testimonialA));
|
||||||
$this->assertTrue($content->testimonials->get(1)?->is($testimonialB));
|
$this->assertTrue($content->testimonials->get(1)?->is($testimonialB));
|
||||||
|
$this->assertCount(2, $content->packages);
|
||||||
|
$this->assertTrue($content->packages->get(0)?->is($packageA));
|
||||||
|
$this->assertTrue($content->packages->get(1)?->is($packageB));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class HomePageTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($settings->hero_title)
|
->assertSee('Celebrações com propósito')
|
||||||
->assertSee($settings->brand_name);
|
->assertSee($settings->brand_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Feature\PublicSite;
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\Service;
|
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
use App\Models\WeddingPackage;
|
use App\Models\WeddingPackage;
|
||||||
@@ -18,24 +17,18 @@ class HomePageContentTest extends TestCase
|
|||||||
|
|
||||||
public function test_published_featured_content_appears_in_order_and_drafts_are_hidden(): void
|
public function test_published_featured_content_appears_in_order_and_drafts_are_hidden(): void
|
||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
$settings->update([
|
|
||||||
'hero_title' => 'Celebrações com propósito',
|
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
|
||||||
'manifesto_title' => 'Manifesto Amare',
|
|
||||||
]);
|
|
||||||
|
|
||||||
Service::factory()->create([
|
WeddingPackage::factory()->create([
|
||||||
'title' => 'Serviço Rascunho',
|
'name' => 'Modalidade Rascunho',
|
||||||
'is_featured' => true,
|
|
||||||
'published_at' => null,
|
'published_at' => null,
|
||||||
]);
|
]);
|
||||||
Service::factory()->published()->featured()->create([
|
WeddingPackage::factory()->published()->create([
|
||||||
'title' => 'Serviço B',
|
'name' => 'Modalidade B',
|
||||||
'sort_order' => 20,
|
'sort_order' => 20,
|
||||||
]);
|
]);
|
||||||
Service::factory()->published()->featured()->create([
|
WeddingPackage::factory()->published()->create([
|
||||||
'title' => 'Serviço A',
|
'name' => 'Modalidade A',
|
||||||
'sort_order' => 10,
|
'sort_order' => 10,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -74,89 +67,65 @@ class HomePageContentTest extends TestCase
|
|||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Celebrações com propósito')
|
|
||||||
->assertSeeInOrder([
|
->assertSeeInOrder([
|
||||||
'id="hero-heading"',
|
'id="hero-heading"',
|
||||||
'id="manifesto-heading"',
|
'id="manifesto-heading"',
|
||||||
'id="services-heading"',
|
'id="vertentes-heading"',
|
||||||
|
'id="packages-heading"',
|
||||||
|
'id="corporate-heading"',
|
||||||
'id="portfolio-heading"',
|
'id="portfolio-heading"',
|
||||||
'id="method-heading"',
|
|
||||||
'id="testimonials-heading"',
|
'id="testimonials-heading"',
|
||||||
'id="positioning-heading"',
|
'id="briefing-heading"',
|
||||||
'id="final-cta-heading"',
|
|
||||||
], false)
|
], false)
|
||||||
->assertSeeInOrder(['Serviço A', 'Serviço B'])
|
->assertSeeInOrder(['Modalidade A', 'Modalidade 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('Modalidade Rascunho')
|
||||||
->assertDontSee('Caso Rascunho')
|
->assertDontSee('Caso Rascunho')
|
||||||
->assertDontSee('Autor Rascunho')
|
->assertDontSee('Autor Rascunho')
|
||||||
->assertSee('data-testid="home-primary-cta"', false)
|
->assertSee('data-testid="home-primary-cta"', false)
|
||||||
->assertSee('href="'.route('briefing').'"', false);
|
->assertSee('href="'.route('briefing').'"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_empty_sections_are_omitted_when_no_published_content(): void
|
public function test_all_sections_render_with_placeholders_when_no_published_content(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance()->update(['hero_image_path' => null]);
|
||||||
|
|
||||||
$response = $this->get(route('home'));
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('id="services-heading"', false)
|
->assertSeeInOrder([
|
||||||
->assertSee('id="portfolio-heading"', false)
|
'id="hero-heading"',
|
||||||
->assertSee('Nosso acervo de eventos reais e autorizados está em preparação.')
|
'id="manifesto-heading"',
|
||||||
->assertDontSee('id="testimonials-heading"', false)
|
'id="vertentes-heading"',
|
||||||
->assertSee('id="manifesto-heading"', false)
|
'id="packages-heading"',
|
||||||
->assertSee('id="method-heading"', false)
|
'id="corporate-heading"',
|
||||||
->assertSee('id="positioning-heading"', false)
|
'id="portfolio-heading"',
|
||||||
->assertSee('id="final-cta-heading"', false);
|
'id="testimonials-heading"',
|
||||||
}
|
'id="briefing-heading"',
|
||||||
|
], false)
|
||||||
public function test_home_exposes_anchored_wedding_and_corporate_chapters_with_cta_fallback(): void
|
->assertSee('FOTO DE CASAMENTO 01')
|
||||||
{
|
->assertSee('Nome e data reais — aguardando autorização')
|
||||||
$settings = SiteSetting::instance();
|
->assertSee('Conteúdo em construção')
|
||||||
$settings->update(['whatsapp_number' => null]);
|
->assertSee('Fornecedores e parcerias — pós-MVP');
|
||||||
WeddingPackage::factory()->published()->create(['name' => 'Essenza', 'sort_order' => 1]);
|
|
||||||
|
|
||||||
$this->get(route('home'))
|
|
||||||
->assertOk()
|
|
||||||
->assertSee('id="amare"', false)
|
|
||||||
->assertSee('id="casamentos"', false)
|
|
||||||
->assertSee('id="corporate"', false)
|
|
||||||
->assertSee('id="portfolio"', false)
|
|
||||||
->assertSee('Essenza')
|
|
||||||
->assertSee(route('briefing', ['servico_interesse' => 'Essenza']), false)
|
|
||||||
->assertSee(route('briefing', ['tipo_evento' => 'Evento corporativo']), false)
|
|
||||||
->assertDontSee('https://wa.me/', false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_wedding_cta_uses_contextual_whatsapp_link_when_official_number_is_valid(): void
|
|
||||||
{
|
|
||||||
SiteSetting::instance()->update(['whatsapp_number' => '+55 (11) 98888-7777']);
|
|
||||||
WeddingPackage::factory()->published()->create(['name' => 'Grand Jour']);
|
|
||||||
|
|
||||||
$this->get(route('home'))
|
|
||||||
->assertOk()
|
|
||||||
->assertSee('https://wa.me/5511988887777?text=Ol%C3%A1%2C%20gostaria%20de%20conversar%20sobre%20a%20modalidade%20Grand%20Jour%20para%20meu%20casamento.', false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_hero_falls_back_to_default_copy_when_fields_are_empty(): void
|
public function test_hero_falls_back_to_default_copy_when_fields_are_empty(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance()->update([
|
SiteSetting::instance()->update([
|
||||||
'hero_title' => '',
|
'hero_title' => '',
|
||||||
'hero_cta_label' => '',
|
|
||||||
'hero_secondary_cta_label' => '',
|
|
||||||
'hero_subtitle' => '',
|
'hero_subtitle' => '',
|
||||||
'hero_note' => '',
|
'hero_cta_label' => '',
|
||||||
|
'hero_image_path' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$response = $this->get(route('home'));
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSeeInOrder(['id="hero-heading"', 'Celebrações com propósito'])
|
->assertSeeInOrder(['id="hero-heading"', 'Celebrações com propósito'], false)
|
||||||
->assertSeeInOrder(['data-testid="home-primary-cta"', 'Solicitar proposta']);
|
->assertSeeInOrder(['data-testid="home-primary-cta"', 'Solicitar proposta'], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_home_hero_uses_its_own_cms_image_instead_of_the_social_og_image(): void
|
public function test_home_hero_uses_its_own_cms_image_instead_of_the_social_og_image(): void
|
||||||
@@ -188,13 +157,114 @@ class HomePageContentTest extends TestCase
|
|||||||
|
|
||||||
$this->get(route('home'))
|
$this->get(route('home'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('min-h-[100dvh]', false)
|
->assertSee('data-tonal-hero', false)
|
||||||
->assertSee('id="hero-heading"', false)
|
->assertSee('id="hero-heading"', false)
|
||||||
->assertSee('data-testid="home-primary-cta"', false)
|
->assertSee('data-testid="home-primary-cta"', false)
|
||||||
->assertDontSee('data-motion-beat="media"', false);
|
->assertDontSee('data-photo-hero', false)
|
||||||
|
->assertDontSee('data-motion-beat="media"', false)
|
||||||
|
->assertDontSee('content/home/hero.jpg', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_blank_quote_testimonials_are_skipped(): void
|
public function test_wedding_packages_render_from_database_with_guidance_block(): void
|
||||||
|
{
|
||||||
|
WeddingPackage::factory()->published()->create([
|
||||||
|
'level' => '01 / COMPLETA',
|
||||||
|
'name' => 'Essenza',
|
||||||
|
'summary' => 'Para casais que desejam contar com a Amare desde o planejamento até a realização do casamento.',
|
||||||
|
'scope_items' => ['Planejamento e organização', 'Gestão de etapas e prioridades'],
|
||||||
|
'cta_label' => 'Quero conhecer a Essenza',
|
||||||
|
'sort_order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Um acompanhamento para cada momento.')
|
||||||
|
->assertSee('01 / COMPLETA')
|
||||||
|
->assertSee('Essenza')
|
||||||
|
->assertSee('Para casais que desejam contar com a Amare desde o planejamento até a realização do casamento.')
|
||||||
|
->assertSee('Planejamento e organização')
|
||||||
|
->assertSee('Gestão de etapas e prioridades')
|
||||||
|
->assertSee('Quero conhecer a Essenza')
|
||||||
|
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
|
||||||
|
->assertSee('Ainda não sabe qual modalidade é ideal?')
|
||||||
|
->assertSee('Conversar com a Amare')
|
||||||
|
->assertSee('Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_package_cta_uses_whatsapp_when_a_number_is_configured(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
|
||||||
|
|
||||||
|
WeddingPackage::factory()->published()->create([
|
||||||
|
'name' => 'Grand Jour',
|
||||||
|
'cta_label' => 'Quero conhecer a Grand Jour',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee(
|
||||||
|
'https://wa.me/5511988887777?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade Grand Jour para meu casamento.'),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
->assertSee('target="_blank"', false)
|
||||||
|
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_corporate_steps_and_placeholder_render_from_settings(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'corporate_steps' => [
|
||||||
|
['title' => 'Planejamento', 'body' => 'Estruturação de escopo, cronograma e prioridades.'],
|
||||||
|
['title' => 'Produção', 'body' => 'Coordenação dos elementos necessários para colocar o evento de pé.'],
|
||||||
|
['title' => 'Execução', 'body' => 'Condução e acompanhamento do evento conforme o projeto aprovado.'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Projetos corporativos tratados como experiência, não apenas operação.')
|
||||||
|
->assertSeeInOrder(['Planejamento', 'Estruturação de escopo, cronograma e prioridades.'])
|
||||||
|
->assertSeeInOrder(['Produção', 'Coordenação dos elementos necessários para colocar o evento de pé.'])
|
||||||
|
->assertSeeInOrder(['Execução', 'Condução e acompanhamento do evento conforme o projeto aprovado.'])
|
||||||
|
->assertSee('Falar sobre um evento corporativo')
|
||||||
|
->assertSee('Portfólio Corporate')
|
||||||
|
->assertSee('Conteúdo em construção')
|
||||||
|
->assertSee('Em vez de usar fotografias genéricas como se fossem cases');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_briefing_form_renders_all_fields_and_posts_to_briefing_store(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Conte sobre o evento que você está imaginando.')
|
||||||
|
->assertSee('method="POST"', false)
|
||||||
|
->assertSee('action="'.route('briefing.store').'"', false)
|
||||||
|
->assertSee('name="empresa"', false)
|
||||||
|
->assertSee('id="briefing-nome"', false)
|
||||||
|
->assertSee('id="briefing-email"', false)
|
||||||
|
->assertSee('id="briefing-telefone"', false)
|
||||||
|
->assertSee('id="briefing-tipo_evento"', false)
|
||||||
|
->assertSee('id="briefing-data_periodo"', false)
|
||||||
|
->assertSee('id="briefing-cidade"', false)
|
||||||
|
->assertSee('id="briefing-convidados"', false)
|
||||||
|
->assertSee('id="briefing-servico_interesse"', false)
|
||||||
|
->assertSee('id="briefing-mensagem"', false)
|
||||||
|
->assertSee('id="briefing-privacidade"', false)
|
||||||
|
->assertSee('Enviar briefing')
|
||||||
|
->assertSee('href="'.route('privacy').'"', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_blank_quote_testimonials_are_skipped_and_padded_with_placeholders(): void
|
||||||
{
|
{
|
||||||
Testimonial::factory()->published()->create([
|
Testimonial::factory()->published()->create([
|
||||||
'author_name' => 'Autor Mantido',
|
'author_name' => 'Autor Mantido',
|
||||||
@@ -212,44 +282,34 @@ class HomePageContentTest extends TestCase
|
|||||||
->assertSee('Autor Mantido')
|
->assertSee('Autor Mantido')
|
||||||
->assertSee('Experiência impecável do início ao fim.')
|
->assertSee('Experiência impecável do início ao fim.')
|
||||||
->assertDontSee('Autor Oculto')
|
->assertDontSee('Autor Oculto')
|
||||||
->assertSee('data-reveal-from="left"', false);
|
->assertSee('Nome e data reais — aguardando autorização');
|
||||||
|
|
||||||
|
$this->assertSame(3, preg_match_all('/<figure\b/i', $response->getContent()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_blank_testimonials_are_filtered_before_directional_sequence(): void
|
public function test_testimonials_render_up_to_three_cards_without_a_carousel(): void
|
||||||
{
|
{
|
||||||
|
foreach ([10, 20, 30, 40] as $index => $sortOrder) {
|
||||||
Testimonial::factory()->published()->create([
|
Testimonial::factory()->published()->create([
|
||||||
'author_name' => 'Primeira autora',
|
'author_name' => 'Autora '.($index + 1),
|
||||||
'quote' => 'Primeiro relato.',
|
'quote' => 'Relato '.($index + 1).'.',
|
||||||
'sort_order' => 10,
|
'sort_order' => $sortOrder,
|
||||||
]);
|
|
||||||
Testimonial::factory()->published()->create([
|
|
||||||
'author_name' => 'Relato vazio',
|
|
||||||
'quote' => " \n ",
|
|
||||||
'sort_order' => 20,
|
|
||||||
]);
|
|
||||||
Testimonial::factory()->published()->create([
|
|
||||||
'author_name' => 'Segunda autora',
|
|
||||||
'quote' => 'Segundo relato.',
|
|
||||||
'sort_order' => 30,
|
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$response = $this->get(route('home'));
|
$response = $this->get(route('home'));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('Relato vazio')
|
->assertSeeInOrder(['Relato 1.', 'Relato 2.', 'Relato 3.'])
|
||||||
->assertSeeInOrder([
|
->assertDontSee('Relato 4.');
|
||||||
'data-reveal-from="left"',
|
|
||||||
'Primeira autora',
|
|
||||||
'data-reveal-from="right"',
|
|
||||||
'Segunda autora',
|
|
||||||
], false);
|
|
||||||
|
|
||||||
$this->assertSame(1, preg_match_all('/<blockquote\b[^>]*data-reveal-from="left"/i', $response->getContent()));
|
$this->assertSame(3, preg_match_all('/<figure\b/i', $response->getContent()));
|
||||||
$this->assertSame(1, preg_match_all('/<blockquote\b[^>]*data-reveal-from="right"/i', $response->getContent()));
|
$this->assertStringNotContainsString('data-carousel', $response->getContent());
|
||||||
|
$this->assertStringNotContainsString('aria-roledescription="carousel"', $response->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_testimonials_section_is_omitted_when_every_published_quote_is_blank(): void
|
public function test_testimonials_section_renders_placeholders_when_every_published_quote_is_blank(): void
|
||||||
{
|
{
|
||||||
Testimonial::factory()->published()->create([
|
Testimonial::factory()->published()->create([
|
||||||
'author_name' => 'Relato vazio',
|
'author_name' => 'Relato vazio',
|
||||||
@@ -258,8 +318,8 @@ class HomePageContentTest extends TestCase
|
|||||||
|
|
||||||
$this->get(route('home'))
|
$this->get(route('home'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('id="testimonials-heading"', false)
|
->assertSee('id="testimonials-heading"', false)
|
||||||
->assertDontSee('href="#testimonials-heading"', false)
|
->assertSee('Depoimento real aprovado pela cliente entra aqui.')
|
||||||
->assertDontSee('Relato vazio');
|
->assertDontSee('Relato vazio');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +327,7 @@ class HomePageContentTest extends TestCase
|
|||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
|
|
||||||
Service::factory()->published()->featured()->create();
|
WeddingPackage::factory()->published()->create();
|
||||||
PortfolioCase::factory()->published()->create(['is_featured' => true]);
|
PortfolioCase::factory()->published()->create(['is_featured' => true]);
|
||||||
Testimonial::factory()->published()->create(['quote' => 'Relato publicado.']);
|
Testimonial::factory()->published()->create(['quote' => 'Relato publicado.']);
|
||||||
|
|
||||||
|
|||||||
@@ -38,19 +38,16 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
->assertSee('data-chapter="hero"', false)
|
->assertSee('data-chapter="hero"', false)
|
||||||
->assertSee('data-motion="page-open"', false)
|
->assertSee('data-motion="page-open"', false)
|
||||||
->assertSee('data-photo-hero', false)
|
->assertSee('data-photo-hero', false)
|
||||||
|
->assertSee('min-h-[calc(100dvh-5rem)]', false)
|
||||||
->assertSee('data-hero-content', false)
|
->assertSee('data-hero-content', false)
|
||||||
->assertSee('data-split-hero', false)
|
->assertSee('data-split-hero', false)
|
||||||
->assertSee('data-motion-beat="media"', false)
|
->assertSee('data-motion-beat="media"', false)
|
||||||
->assertSee('lg:grid-cols-12', false)
|
->assertSee('lg:grid-cols-12', false)
|
||||||
->assertSee('lg:col-span-5', false)
|
|
||||||
->assertSee('lg:col-span-7', false)
|
|
||||||
->assertSee('max-w-[470px]', false)
|
|
||||||
->assertSee('break-normal', false)
|
|
||||||
->assertSee('max-lg:aspect-[4/5]', false)
|
|
||||||
->assertSee('content/heroes/home.jpg', false)
|
->assertSee('content/heroes/home.jpg', false)
|
||||||
->assertSee('loading="eager"', false)
|
->assertSee('loading="eager"', false)
|
||||||
->assertSee('fetchpriority="high"', false)
|
->assertSee('fetchpriority="high"', false)
|
||||||
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false)
|
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false)
|
||||||
|
->assertDontSee('data-tonal-hero', false)
|
||||||
->assertSee('content="http://localhost/storage/content/og/social.jpg"', false);
|
->assertSee('content="http://localhost/storage/content/og/social.jpg"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +76,7 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
->assertSee('data-motion="page-open"', false)
|
->assertSee('data-motion="page-open"', false)
|
||||||
->assertSee('id="hero-heading"', false)
|
->assertSee('id="hero-heading"', false)
|
||||||
->assertSee('data-testid="home-primary-cta"', false)
|
->assertSee('data-testid="home-primary-cta"', false)
|
||||||
|
->assertDontSee('data-photo-hero', false)
|
||||||
->assertDontSee('data-split-hero', false)
|
->assertDontSee('data-split-hero', false)
|
||||||
->assertDontSee('content/heroes/home.jpg', false);
|
->assertDontSee('content/heroes/home.jpg', false);
|
||||||
}
|
}
|
||||||
@@ -105,25 +103,12 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
$this->get($route)
|
$this->get($route)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('data-photo-hero', false)
|
->assertSee('data-photo-hero', false)
|
||||||
->assertSee('lg:grid-cols-12', false)
|
|
||||||
->assertSee('lg:col-span-5', false)
|
|
||||||
->assertSee('lg:col-span-7', false)
|
|
||||||
->assertSee('loading="eager"', false)
|
->assertSee('loading="eager"', false)
|
||||||
->assertSee('fetchpriority="high"', false)
|
->assertSee('fetchpriority="high"', false)
|
||||||
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false);
|
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_contact_and_privacy_remain_sober_tonal_surfaces(): void
|
|
||||||
{
|
|
||||||
foreach ([route('contact'), route('privacy')] as $route) {
|
|
||||||
$this->get($route)
|
|
||||||
->assertOk()
|
|
||||||
->assertDontSee('data-photo-hero', false)
|
|
||||||
->assertDontSee('data-split-hero', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
Storage::fake('public');
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ class MediaPerformanceTest extends TestCase
|
|||||||
$about = $this->get(route('about'))->assertOk()->getContent();
|
$about = $this->get(route('about'))->assertOk()->getContent();
|
||||||
$detail = $this->get(route('portfolio.show', $firstCase->slug))->assertOk()->getContent();
|
$detail = $this->get(route('portfolio.show', $firstCase->slug))->assertOk()->getContent();
|
||||||
|
|
||||||
$this->assertMatchesRegularExpression('/service-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $services);
|
$this->assertStringNotContainsString('service-first.jpg', $services);
|
||||||
$this->assertMatchesRegularExpression('/service-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $services);
|
$this->assertStringNotContainsString('service-later.jpg', $services);
|
||||||
$this->assertMatchesRegularExpression('/case-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
|
$this->assertMatchesRegularExpression('/case-first\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
|
||||||
$this->assertMatchesRegularExpression('/case-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $portfolio);
|
$this->assertMatchesRegularExpression('/case-later\.jpg"[^>]*loading="lazy"(?![^>]*fetchpriority)/', $portfolio);
|
||||||
$this->assertMatchesRegularExpression('/about\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $about);
|
$this->assertMatchesRegularExpression('/about\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $about);
|
||||||
@@ -89,7 +89,7 @@ class MediaPerformanceTest extends TestCase
|
|||||||
$services = $this->get(route('services.index'))->assertOk()->getContent();
|
$services = $this->get(route('services.index'))->assertOk()->getContent();
|
||||||
$portfolio = $this->get(route('portfolio.index'))->assertOk()->getContent();
|
$portfolio = $this->get(route('portfolio.index'))->assertOk()->getContent();
|
||||||
|
|
||||||
$this->assertMatchesRegularExpression('/visible-service\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $services);
|
$this->assertStringNotContainsString('visible-service.jpg', $services);
|
||||||
$this->assertMatchesRegularExpression('/visible-case\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
|
$this->assertMatchesRegularExpression('/visible-case\.jpg"[^>]*loading="eager"[^>]*fetchpriority="high"/', $portfolio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ class MotionMarkupTest extends TestCase
|
|||||||
->assertDontSee('data-chapter-index', false)
|
->assertDontSee('data-chapter-index', false)
|
||||||
->assertDontSee('data-chapter-progress', false)
|
->assertDontSee('data-chapter-progress', false)
|
||||||
->assertSee('data-chapter="hero"', false)
|
->assertSee('data-chapter="hero"', false)
|
||||||
->assertSee('data-chapter="manifesto"', false)
|
->assertSee('data-chapter="sobre"', false)
|
||||||
->assertSee('data-reveal', false)
|
->assertSee('data-reveal', false)
|
||||||
->assertDontSee('data-home-folio', false);
|
->assertDontSee('data-home-folio', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_home_has_no_chapter_navigation_when_cms_sections_are_empty(): void
|
public function test_home_has_no_chapter_navigation_and_keeps_all_sections_when_cms_sections_are_empty(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
|
|
||||||
@@ -60,9 +60,16 @@ class MotionMarkupTest extends TestCase
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('data-chapter-index', false)
|
->assertDontSee('data-chapter-index', false)
|
||||||
->assertDontSee('data-chapter-progress', false)
|
->assertDontSee('data-chapter-progress', false)
|
||||||
->assertDontSee('id="services-heading"', false)
|
->assertSeeInOrder([
|
||||||
->assertSee('id="portfolio-heading"', false)
|
'id="hero-heading"',
|
||||||
->assertDontSee('id="testimonials-heading"', false);
|
'id="manifesto-heading"',
|
||||||
|
'id="vertentes-heading"',
|
||||||
|
'id="packages-heading"',
|
||||||
|
'id="corporate-heading"',
|
||||||
|
'id="portfolio-heading"',
|
||||||
|
'id="testimonials-heading"',
|
||||||
|
'id="briefing-heading"',
|
||||||
|
], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_motion_tokens_and_runtime_exist_with_reduced_motion_guards(): void
|
public function test_motion_tokens_and_runtime_exist_with_reduced_motion_guards(): void
|
||||||
@@ -131,7 +138,6 @@ class MotionMarkupTest extends TestCase
|
|||||||
$this->get(route('portfolio.show', $case->slug)),
|
$this->get(route('portfolio.show', $case->slug)),
|
||||||
$this->get(route('about')),
|
$this->get(route('about')),
|
||||||
$this->get(route('contact')),
|
$this->get(route('contact')),
|
||||||
$this->get(route('briefing')),
|
|
||||||
$this->get(route('privacy')),
|
$this->get(route('privacy')),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ namespace Tests\Feature\PublicSite;
|
|||||||
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use App\Models\Service;
|
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@@ -18,17 +18,45 @@ class PublicPagesTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
public function test_services_index_lists_published_services_in_sort_order(): void
|
public function test_services_index_renders_editorial_sections_with_published_modalities_only(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
|
|
||||||
Service::factory()->create(['title' => 'Rascunho', 'published_at' => null]);
|
WeddingPackage::factory()->create(['name' => 'Rascunho', 'published_at' => null]);
|
||||||
Service::factory()->published()->create(['title' => 'Serviço B', 'sort_order' => 20]);
|
WeddingPackage::factory()->published()->create([
|
||||||
Service::factory()->published()->create(['title' => 'Serviço A', 'sort_order' => 10]);
|
'name' => 'Essenza',
|
||||||
|
'level' => '01 / COMPLETA',
|
||||||
|
'tag' => 'Assessoria completa',
|
||||||
|
'subtitle' => 'Do planejamento ao grande dia.',
|
||||||
|
'summary' => 'Para casais que desejam contar com a Amare desde o início.',
|
||||||
|
'scope_items' => ['Estruturação do planejamento', 'Coordenação da execução'],
|
||||||
|
'cta_label' => 'Quero conhecer a Essenza',
|
||||||
|
'compare_heading' => 'Começar com a Amare',
|
||||||
|
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
|
||||||
|
'sort_order' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
$this->get(route('services.index'))
|
$this->get(route('services.index'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSeeInOrder(['Serviço A', 'Serviço B'])
|
->assertSee('O cuidado certo para cada tipo de evento.')
|
||||||
|
->assertSee('Conhecer os serviços')
|
||||||
|
->assertSee('Tenho um evento em mente')
|
||||||
|
->assertSee('Duas vertentes')
|
||||||
|
->assertSee('Escolha por contexto, não por uma lista de pacotes.')
|
||||||
|
->assertSee('Amare Casamentos')
|
||||||
|
->assertSee('Acompanhamento para diferentes momentos do planejamento.')
|
||||||
|
->assertSee('Amare Corporate')
|
||||||
|
->assertSee('Planejamento e execução para eventos empresariais.')
|
||||||
|
->assertSee('Três formas de ter a Amare ao lado.')
|
||||||
|
->assertSee('Assessoria completa')
|
||||||
|
->assertSee('Essenza')
|
||||||
|
->assertSee('Do planejamento ao grande dia.')
|
||||||
|
->assertSee('Quero conhecer a Essenza')
|
||||||
|
->assertSee('Começar com a Amare')
|
||||||
|
->assertSee('Organização, produção e condução com linguagem empresarial.')
|
||||||
|
->assertSee('Estruturação do evento, escopo, prioridades e cronograma.')
|
||||||
|
->assertSee('Falar sobre um evento Corporate')
|
||||||
|
->assertSee('Do casamento ao evento corporativo, tudo começa com uma boa conversa.')
|
||||||
->assertDontSee('Rascunho');
|
->assertDontSee('Rascunho');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +208,6 @@ class PublicPagesTest extends TestCase
|
|||||||
$this->get(route('contact'))
|
$this->get(route('contact'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('amareassessoriaeventos@gmail.com')
|
->assertSee('amareassessoriaeventos@gmail.com')
|
||||||
->assertSee('(11) 90000-0000')
|
|
||||||
->assertSee('São Paulo - SP')
|
->assertSee('São Paulo - SP')
|
||||||
->assertDontSee('Fortaleza')
|
->assertDontSee('Fortaleza')
|
||||||
->assertSee('https://instagram.com/amare', false)
|
->assertSee('https://instagram.com/amare', false)
|
||||||
|
|||||||
Reference in New Issue
Block a user