Compare commits
13 Commits
a2a6dc86fb
...
fix/seed-r
| Author | SHA1 | Date | |
|---|---|---|---|
| 5949fad9b1 | |||
| db4453b165 | |||
| 10a1f0dc7c | |||
| c04790ee1f | |||
| edf1c48050 | |||
| 3c711a74f3 | |||
| 88d63f349d | |||
| e0390ff333 | |||
| 321f0cee11 | |||
| 1d23e16625 | |||
| 33788af172 | |||
| 119711d6f0 | |||
| f09ea83071 |
26
.github/workflows/ci.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
name: static
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: cp .env.example .env
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -39,7 +40,7 @@ jobs:
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||
coverage: none
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -55,7 +56,8 @@ jobs:
|
||||
name: unit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: cp .env.example .env
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -63,7 +65,7 @@ jobs:
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||
coverage: none
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||
@@ -92,7 +94,8 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: cp .env.example .env
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -100,13 +103,13 @@ jobs:
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||
coverage: none
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: composer-${{ runner.os }}-
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -136,7 +139,8 @@ jobs:
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- run: cp .env.example .env
|
||||
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
@@ -144,13 +148,13 @@ jobs:
|
||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||
coverage: none
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.composer/cache/files
|
||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: composer-${{ runner.os }}-
|
||||
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||
@@ -224,7 +228,7 @@ jobs:
|
||||
name: container
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Build production image
|
||||
run: docker build -t amare-app:ci .
|
||||
|
||||
1
.gitignore
vendored
@@ -18,6 +18,7 @@
|
||||
/public/fonts-manifest.dev.json
|
||||
/public/hot
|
||||
/public/storage
|
||||
/tests/Browser/Screenshots
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
|
||||
@@ -56,6 +56,9 @@ final class MediaGenerateVariantsCommand extends Command
|
||||
if ($settings && filled($settings->default_og_image_path)) {
|
||||
$paths[] = (string) $settings->default_og_image_path;
|
||||
}
|
||||
if ($settings && filled($settings->about_image_path)) {
|
||||
$paths[] = (string) $settings->about_image_path;
|
||||
}
|
||||
|
||||
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
||||
$paths[] = (string) $path;
|
||||
|
||||
@@ -228,6 +228,12 @@ class ManageSiteSettings extends Page
|
||||
->addActionLabel('Adicionar rede'),
|
||||
])
|
||||
->columns(2),
|
||||
Section::make('Página Sobre')
|
||||
->schema([
|
||||
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem da página Sobre', 'content/about'),
|
||||
PublicImageUploadRules::altTextField('about_image_alt', 'about_image_path'),
|
||||
])
|
||||
->columns(2),
|
||||
Section::make('SEO padrão')
|
||||
->schema([
|
||||
TextInput::make('default_meta_title')
|
||||
|
||||
@@ -16,6 +16,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* @property bool $analytics_enabled
|
||||
* @property string|null $default_og_image_path
|
||||
* @property string|null $default_og_image_alt
|
||||
* @property string|null $about_image_path
|
||||
* @property string|null $about_image_alt
|
||||
* @property string|null $logo_path
|
||||
* @property string|null $logo_alt
|
||||
*/
|
||||
@@ -30,6 +32,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
'hero_secondary_cta_label',
|
||||
'hero_note',
|
||||
'about_summary',
|
||||
'about_image_path',
|
||||
'about_image_alt',
|
||||
'manifesto_title',
|
||||
'manifesto_lead',
|
||||
'manifesto_body',
|
||||
|
||||
BIN
database/fixtures/images/about-image.jpg
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
database/fixtures/images/case-casamento-ana-lucas.jpg
Normal file
|
After Width: | Height: | Size: 436 KiB |
BIN
database/fixtures/images/case-lancamento-verano.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
database/fixtures/images/case-mini-wedding-marina.jpg
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
database/fixtures/images/gallery-casamento-ana-lucas-1.jpg
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
database/fixtures/images/gallery-casamento-ana-lucas-2.jpg
Normal file
|
After Width: | Height: | Size: 499 KiB |
BIN
database/fixtures/images/gallery-lancamento-verano-1.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
database/fixtures/images/gallery-lancamento-verano-2.jpg
Normal file
|
After Width: | Height: | Size: 235 KiB |
BIN
database/fixtures/images/gallery-mini-wedding-marina-1.jpg
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
database/fixtures/images/gallery-mini-wedding-marina-2.jpg
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
database/fixtures/images/og-default.jpg
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
database/fixtures/images/service-casamentos.jpg
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
database/fixtures/images/service-celebracoes-intimistas.jpg
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
database/fixtures/images/service-eventos-corporativos.jpg
Normal file
|
After Width: | Height: | Size: 337 KiB |
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('site_settings', function (Blueprint $table): void {
|
||||
$table->string('about_image_path')->nullable()->after('about_summary');
|
||||
$table->string('about_image_alt')->nullable()->after('about_image_path');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('site_settings', function (Blueprint $table): void {
|
||||
$table->dropColumn(['about_image_path', 'about_image_alt']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
||||
use App\Models\PortfolioImage;
|
||||
use App\Models\Service;
|
||||
use App\Models\SiteSetting;
|
||||
use App\Support\PublicImageUploadRules;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
@@ -36,6 +37,8 @@ class ContentSeeder extends Seeder
|
||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
||||
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
||||
'manifesto_lead' => 'Um evento memorável não nasce apenas de uma boa estética. Ele depende de decisões bem conduzidas, fornecedores alinhados e atenção constante ao que realmente importa.',
|
||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||
@@ -91,7 +94,7 @@ class ContentSeeder extends Seeder
|
||||
['slug' => $service['slug']],
|
||||
[
|
||||
...$service,
|
||||
'cover_image_path' => $this->copyFixture('service-cover.jpg', 'content/services/'.$service['slug'].'.jpg'),
|
||||
'cover_image_path' => $this->copyFixture('service-'.$service['slug'].'.jpg', 'content/services/'.$service['slug'].'.jpg'),
|
||||
'cover_image_alt' => 'Capa do serviço '.$service['title'],
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
],
|
||||
@@ -148,7 +151,7 @@ class ContentSeeder extends Seeder
|
||||
['slug' => $caseData['slug']],
|
||||
[
|
||||
...$caseData,
|
||||
'cover_image_path' => $this->copyFixture('portfolio-cover.jpg', 'content/portfolio/'.$caseData['slug'].'-cover.jpg'),
|
||||
'cover_image_path' => $this->copyFixture('case-'.$caseData['slug'].'.jpg', 'content/portfolio/'.$caseData['slug'].'-cover.jpg'),
|
||||
'cover_image_alt' => 'Capa do caso '.$caseData['title'],
|
||||
'is_featured' => true,
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
@@ -160,7 +163,7 @@ class ContentSeeder extends Seeder
|
||||
foreach ([1, 2] as $index) {
|
||||
PortfolioImage::query()->create([
|
||||
'portfolio_case_id' => $case->id,
|
||||
'path' => $this->copyFixture('gallery.jpg', 'content/portfolio/'.$caseData['slug'].'-gallery-'.$index.'.jpg'),
|
||||
'path' => $this->copyFixture('gallery-'.$caseData['slug'].'-'.$index.'.jpg', 'content/portfolio/'.$caseData['slug'].'-gallery-'.$index.'.jpg'),
|
||||
'alt_text' => 'Galeria '.$caseData['title'].' '.$index,
|
||||
'caption' => $index === 1 ? 'Detalhe da decoração' : null,
|
||||
'sort_order' => $index,
|
||||
@@ -171,8 +174,10 @@ class ContentSeeder extends Seeder
|
||||
|
||||
private function copyFixture(string $fixtureName, string $destination): string
|
||||
{
|
||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||
Storage::disk('public')->put($destination, File::get($source));
|
||||
$source = base_path('database/fixtures/images/'.$fixtureName);
|
||||
$disk = PublicImageUploadRules::disk();
|
||||
|
||||
Storage::disk($disk)->put($destination, File::get($source), 'public');
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
@@ -44,6 +43,8 @@ class VisualContentSeeder extends Seeder
|
||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
||||
'about_image_alt' => 'Imagem editorial da página Sobre',
|
||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
||||
'manifesto_lead' => 'Um evento memorável não nasce apenas de uma boa estética. Ele depende de decisões bem conduzidas, fornecedores alinhados e atenção constante ao que realmente importa.',
|
||||
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||
@@ -58,7 +59,7 @@ class VisualContentSeeder extends Seeder
|
||||
],
|
||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'visual/og/og-default.jpg'),
|
||||
'default_og_image_path' => $this->writeSolidJpeg('visual/og/og-default.jpg', 1600, 1067, [85, 107, 47]),
|
||||
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
||||
'analytics_enabled' => false,
|
||||
'analytics_script' => null,
|
||||
@@ -89,7 +90,7 @@ class VisualContentSeeder extends Seeder
|
||||
['slug' => $service['slug']],
|
||||
[
|
||||
...$service,
|
||||
'cover_image_path' => $this->copyFixture('service-cover.jpg', 'visual/services/'.$service['slug'].'.jpg'),
|
||||
'cover_image_path' => $this->writeSolidJpeg('visual/services/'.$service['slug'].'.jpg', 1600, 1000, [196, 200, 184]),
|
||||
'cover_image_alt' => 'Capa do serviço '.$service['title'],
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
],
|
||||
@@ -133,7 +134,7 @@ class VisualContentSeeder extends Seeder
|
||||
['slug' => $caseData['slug']],
|
||||
[
|
||||
...$caseData,
|
||||
'cover_image_path' => $this->copyFixture('portfolio-cover.jpg', 'visual/portfolio/'.$caseData['slug'].'-cover.jpg'),
|
||||
'cover_image_path' => $this->writeSolidJpeg('visual/portfolio/'.$caseData['slug'].'-cover.jpg', 1600, 1200, [240, 238, 233]),
|
||||
'cover_image_alt' => 'Capa do caso '.$caseData['title'],
|
||||
'is_featured' => true,
|
||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||
@@ -144,7 +145,7 @@ class VisualContentSeeder extends Seeder
|
||||
|
||||
PortfolioImage::query()->create([
|
||||
'portfolio_case_id' => $case->id,
|
||||
'path' => $this->copyFixture('gallery.jpg', 'visual/portfolio/'.$caseData['slug'].'-gallery-1.jpg'),
|
||||
'path' => $this->writeSolidJpeg('visual/portfolio/'.$caseData['slug'].'-gallery-1.jpg', 1600, 1200, [228, 226, 221]),
|
||||
'alt_text' => 'Galeria '.$caseData['title'].' 1',
|
||||
'caption' => 'Detalhe da decoração',
|
||||
'sort_order' => 1,
|
||||
@@ -186,10 +187,21 @@ class VisualContentSeeder extends Seeder
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFixture(string $fixtureName, string $destination): string
|
||||
/**
|
||||
* @param array{0: int, 1: int, 2: int} $rgb
|
||||
*/
|
||||
private function writeSolidJpeg(string $destination, int $width, int $height, array $rgb): string
|
||||
{
|
||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||
Storage::disk('public')->put($destination, File::get($source));
|
||||
$image = imagecreatetruecolor($width, $height);
|
||||
$color = imagecolorallocate($image, $rgb[0], $rgb[1], $rgb[2]);
|
||||
imagefilledrectangle($image, 0, 0, $width, $height, $color);
|
||||
|
||||
ob_start();
|
||||
imagejpeg($image, null, 90);
|
||||
$binary = (string) ob_get_clean();
|
||||
imagedestroy($image);
|
||||
|
||||
Storage::disk('public')->put($destination, $binary);
|
||||
|
||||
return $destination;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
restart: "no"
|
||||
env_file:
|
||||
- .env
|
||||
command: ["php", "artisan", "migrate", "--force", "--no-interaction"]
|
||||
command: ["sh", "-c", "php artisan migrate --force --no-interaction && php artisan db:seed --class=ContentSeeder --force; php artisan media:generate-variants --force; true"]
|
||||
networks:
|
||||
- dokploy-network
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
--text-3xl: var(--amare-text-3xl);
|
||||
--text-4xl: var(--amare-text-4xl);
|
||||
|
||||
--text-display: clamp(3rem, 7.7vw, 5.875rem);
|
||||
--text-display--line-height: 0.94;
|
||||
--text-display--letter-spacing: -0.01em;
|
||||
|
||||
--text-headline: clamp(2.375rem, 5.2vw, 4rem);
|
||||
--text-headline--line-height: 1.02;
|
||||
|
||||
--spacing-1: var(--amare-space-1);
|
||||
--spacing-2: var(--amare-space-2);
|
||||
--spacing-3: var(--amare-space-3);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20" data-chapter="final-cta">
|
||||
<div class="container-amare space-y-6 text-center" data-reveal>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||
<h2 id="final-cta-heading" class="text-3xl font-medium text-amare-text md:text-4xl">Do casamento ao evento corporativo, tudo começa com uma boa conversa.</h2>
|
||||
<h2 id="final-cta-heading" class="text-headline font-medium text-amare-text">Do casamento ao evento corporativo, tudo começa com uma boa conversa.</h2>
|
||||
<p class="mx-auto max-w-2xl text-amare-muted">
|
||||
Compartilhe as primeiras informações do seu evento. A Amare retorna para entender o contexto e orientar os próximos passos.
|
||||
</p>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h1 id="hero-heading" data-motion-beat="title" class="max-w-3xl text-4xl font-medium leading-tight text-amare-text md:text-5xl">
|
||||
<h1 id="hero-heading" data-motion-beat="title" class="max-w-3xl text-display font-medium text-amare-text">
|
||||
{{ $settings->hero_title ?: 'Celebrações com propósito' }}
|
||||
</h1>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Manifesto</p>
|
||||
<div class="space-y-6">
|
||||
<h2 id="manifesto-heading" class="max-w-3xl text-3xl font-medium leading-tight text-amare-text md:text-4xl">{{ $title }}</h2>
|
||||
<h2 id="manifesto-heading" class="max-w-3xl text-headline font-medium text-amare-text">{{ $title }}</h2>
|
||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $lead }}</p>
|
||||
<p class="max-w-2xl text-amare-text-muted">{{ $body }}</p>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start">
|
||||
<div class="space-y-3" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Método</p>
|
||||
<h2 id="method-heading" class="text-3xl font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
||||
<h2 id="method-heading" class="text-headline font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
||||
<p class="text-amare-text-muted">{{ $intro }}</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent-text/80">Portfólio</p>
|
||||
<div class="space-y-3">
|
||||
<h2 id="portfolio-heading" class="text-3xl font-medium md:text-4xl">Celebrações que ganham forma com intenção.</h2>
|
||||
<h2 id="portfolio-heading" class="text-headline font-medium">Celebrações que ganham forma com intenção.</h2>
|
||||
<p class="max-w-2xl text-amare-accent-text/80">Recortes de eventos conduzidos com escuta, direção e presença em cada etapa.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||
<div class="space-y-3" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">A Amare</p>
|
||||
<h2 id="positioning-heading" class="text-3xl font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||
<h2 id="positioning-heading" class="text-headline font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||
</div>
|
||||
<div class="space-y-8" data-reveal>
|
||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
])
|
||||
|
||||
@if ($testimonials->isNotEmpty())
|
||||
<section aria-labelledby="testimonials-heading" class="border-b border-amare-border bg-amare-bg-muted py-16" data-chapter="testimonials">
|
||||
<section aria-labelledby="testimonials-heading" class="border-b border-amare-border bg-amare-bg py-16" data-chapter="testimonials">
|
||||
<div class="container-amare space-y-8">
|
||||
<div class="max-w-2xl space-y-3" data-reveal>
|
||||
<h2 id="testimonials-heading" class="text-3xl font-semibold text-amare-text">Depoimentos</h2>
|
||||
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
||||
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
$resolvedWidth = $width ?? $dimensions['width'] ?? null;
|
||||
$resolvedHeight = $height ?? $dimensions['height'] ?? null;
|
||||
$loadingValue = $loading === 'eager' ? 'eager' : 'lazy';
|
||||
$loadingValue = $loading;
|
||||
@endphp
|
||||
|
||||
<img
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
<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>
|
||||
<li><a href="{{ route('about') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">A Amare</a></li>
|
||||
<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>
|
||||
<li><a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Política de privacidade</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,12 +12,24 @@
|
||||
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:py-24">
|
||||
<div class="space-y-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Humana no cuidado. Precisa na entrega.</h1>
|
||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Humana no cuidado. Precisa na entrega.</h1>
|
||||
<p class="text-lg text-amare-muted">{{ $siteSettings->about_summary }}</p>
|
||||
<p class="text-amare-muted">
|
||||
A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo,
|
||||
presença no dia do evento e uma condução serena do início ao fim.
|
||||
</p>
|
||||
|
||||
@if (filled($siteSettings->about_image_path))
|
||||
<div class="overflow-hidden bg-amare-bg-deep">
|
||||
<x-media.image
|
||||
:path="$siteSettings->about_image_path"
|
||||
:alt="$siteSettings->about_image_alt ?: $siteSettings->brand_name"
|
||||
loading="lazy"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
class="img-editorial aspect-[4/3] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare">
|
||||
@@ -30,4 +42,6 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-home.final-cta :settings="$siteSettings" />
|
||||
@endsection
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.2fr)_minmax(0,0.8fr)] md:py-24">
|
||||
<div class="space-y-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Vamos conversar</p>
|
||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Todo grande encontro começa com uma boa conversa.</h1>
|
||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Todo grande encontro começa com uma boa conversa.</h1>
|
||||
<p class="max-w-2xl text-lg text-amare-muted">
|
||||
Conte sobre o seu evento no briefing abaixo. Retornaremos com uma proposta sob medida e sem compromisso.
|
||||
</p>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Atmosferas que contam histórias.</h1>
|
||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Atmosferas que contam histórias.</h1>
|
||||
<p class="text-lg text-amare-muted">Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência.</p>
|
||||
</div>
|
||||
|
||||
@@ -43,4 +43,6 @@
|
||||
<p class="text-sm text-amare-accent">Imagens demonstrativas enquanto o acervo autorizado da Amare está em organização.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-home.final-cta :settings="$siteSettings" />
|
||||
@endsection
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@if (filled($case->event_type))
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ $case->event_type }}</p>
|
||||
@endif
|
||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">{{ $case->title }}</h1>
|
||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">{{ $case->title }}</h1>
|
||||
<p class="text-lg text-amare-muted">{{ $case->summary }}</p>
|
||||
<p class="text-sm break-words text-amare-muted">
|
||||
@if ($case->city){{ $case->city }}@endif
|
||||
@@ -71,4 +71,6 @@
|
||||
</section>
|
||||
@endif
|
||||
</article>
|
||||
|
||||
<x-home.final-cta :settings="$siteSettings" />
|
||||
@endsection
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
|
||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text md:text-5xl">Uma mesma excelência, diferentes ocasiões.</h1>
|
||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Uma mesma excelência, diferentes ocasiões.</h1>
|
||||
<p class="text-lg text-amare-muted">O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento.</p>
|
||||
</div>
|
||||
|
||||
@@ -41,4 +41,6 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<x-home.final-cta :settings="$siteSettings" />
|
||||
@endsection
|
||||
|
||||
16
tasks.md
@@ -61,3 +61,19 @@
|
||||
- `PageMeta::forErrorPage` landed on `main` during implementation; rebased and extended it for 419/429/503.
|
||||
- Contact form landed on `main`; submit-state browser coverage was restored.
|
||||
- `event_type: null` violates NOT NULL column — test uses `''` instead (guard covers both).
|
||||
|
||||
# Browser CI Gate — estabilidade e diagnosticabilidade
|
||||
|
||||
## BG — Causa raiz
|
||||
- [x] Diagnosticar 3 runs: mismatch visual (8 testes), acessibilidade (1), timeout networkidle 5s + overflow real
|
||||
- [x] Reproduzir local: `waitForLoadState('networkidle')` fragil (conexões longas), default 5s
|
||||
- [x] Reproduzir warning `file_get_contents(.env)` em TODO teste (CI sem .env)
|
||||
|
||||
## BG — Fixes
|
||||
- [x] Helper `StableScreenshot` (tests/Support/): drop networkidle → readyState + fonts + settle
|
||||
- [x] Exportar PNGs standalone (diff/expected/actual) no mismatch — visíveis nos artifacts
|
||||
- [x] CI: `cp .env.example .env` nos jobs de teste; checkout/cache v5
|
||||
- [x] Gitignore tests/Browser/Screenshots
|
||||
- [x] Unit test MismatchScreenshotExporter; pint/phpstan/unit/feature verdes
|
||||
- [x] Push + PR + CI browser verde (PR #15 merge f09ea83, run 31098941038)
|
||||
- [x] Refresh baselines de CI se necessário (não necessário — browser passou com baselines atuais)
|
||||
|
||||
@@ -41,13 +41,14 @@ $viewports = [
|
||||
foreach ($screens as $screen => $path) {
|
||||
foreach ($viewports as $viewport => [$width, $height]) {
|
||||
it("matches {$screen} {$viewport} visual baseline", function () use ($path, $width, $height): void {
|
||||
$this->assertStableScreenshotMatches(
|
||||
$this->visit($path, [
|
||||
'reducedMotion' => 'reduce',
|
||||
])
|
||||
->withLocale('pt-BR')
|
||||
->withTimezone('America/Fortaleza')
|
||||
->resize($width, $height)
|
||||
->assertScreenshotMatches();
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
40
tests/Support/MismatchScreenshotExporter.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
final class MismatchScreenshotExporter
|
||||
{
|
||||
/**
|
||||
* Extracts the diff, expected, and actual images embedded in an ImageDiffView
|
||||
* HTML file and writes them as standalone PNG files so they can be inspected
|
||||
* directly in CI artifacts.
|
||||
*
|
||||
* @return list<string> paths of the written PNG files
|
||||
*/
|
||||
public static function export(string $htmlPath, string $targetDir): array
|
||||
{
|
||||
$html = (string) file_get_contents($htmlPath);
|
||||
|
||||
if (preg_match_all('/src="data:image\/png;base64,([^"]+)"/', $html, $matches) === false || ! isset($matches[1][2])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$base = pathinfo($htmlPath, PATHINFO_FILENAME);
|
||||
$labels = ['diff', 'expected', 'actual'];
|
||||
$written = [];
|
||||
|
||||
foreach (array_slice($matches[1], 0, 3) as $index => $base64) {
|
||||
$path = $targetDir.'/'.$base.'-'.($labels[$index] ?? 'image-'.$index).'.png';
|
||||
|
||||
if (file_put_contents($path, (string) base64_decode($base64, true)) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$written[] = $path;
|
||||
}
|
||||
|
||||
return $written;
|
||||
}
|
||||
}
|
||||
68
tests/Support/StableScreenshot.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Support;
|
||||
|
||||
use Pest\Browser\Api\AwaitableWebpage;
|
||||
use Pest\Browser\Exceptions\BrowserExpectationFailedException;
|
||||
use Pest\Browser\Support\Screenshot;
|
||||
use Pest\TestSuite;
|
||||
use PHPUnit\Framework\ExpectationFailedException;
|
||||
|
||||
trait StableScreenshot
|
||||
{
|
||||
/**
|
||||
* Asserts the page matches its visual baseline without relying on the
|
||||
* networkidle load state, which is inherently flaky (long-lived connections
|
||||
* like fonts, polling, or keep-alive can keep it from ever firing and cause
|
||||
* spurious timeouts at the 5s client default).
|
||||
*
|
||||
* On mismatch the vendor only writes an HTML diff view with base64-embedded
|
||||
* images, so this helper additionally exports standalone diff/expected/actual
|
||||
* PNG files that are directly viewable in CI artifacts.
|
||||
*/
|
||||
public function assertStableScreenshotMatches(AwaitableWebpage $awaitable): void
|
||||
{
|
||||
$page = $awaitable->page();
|
||||
|
||||
$page->addStyleTag('* {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
font-family: Arial, sans-serif !important;
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
-moz-osx-font-smoothing: grayscale !important;
|
||||
}
|
||||
}');
|
||||
|
||||
$page->waitForFunction(
|
||||
'document.readyState === "complete"'
|
||||
.' && document.fonts.status === "loaded"'
|
||||
);
|
||||
|
||||
usleep(300_000);
|
||||
|
||||
try {
|
||||
$page->expectScreenshot(true, false);
|
||||
} catch (ExpectationFailedException $exception) {
|
||||
$this->exportMismatchScreenshots();
|
||||
|
||||
throw BrowserExpectationFailedException::from($page, $exception);
|
||||
}
|
||||
}
|
||||
|
||||
private function exportMismatchScreenshots(): void
|
||||
{
|
||||
[$snapshotName] = TestSuite::getInstance()->snapshots->get();
|
||||
|
||||
$base = pathinfo($snapshotName, PATHINFO_FILENAME);
|
||||
$htmlPath = Screenshot::dir().'/ImageDiffView/'.$base.'.html';
|
||||
|
||||
if (! is_file($htmlPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
MismatchScreenshotExporter::export($htmlPath, Screenshot::dir());
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Tests\Support\StableScreenshot;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
use StableScreenshot;
|
||||
}
|
||||
|
||||
47
tests/Unit/Support/MismatchScreenshotExporterTest.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Tests\Support\MismatchScreenshotExporter;
|
||||
|
||||
it('exports diff, expected, and actual PNGs from an ImageDiffView html', function (): void {
|
||||
$targetDir = sys_get_temp_dir().'/mismatch-exporter-'.uniqid();
|
||||
mkdir($targetDir, 0755, true);
|
||||
|
||||
$expected = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
|
||||
$actual = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
|
||||
$diff = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgAAACAAFWoS02AAAAAElFTkSuQmCC';
|
||||
|
||||
$html = <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html><body>
|
||||
<img alt="Diff" src="data:image/png;base64,{$diff}"/>
|
||||
<img slot="image-1" alt="Expected" src="data:image/png;base64,{$expected}"/>
|
||||
<img slot="image-2" alt="Actual" src="data:image/png;base64,{$actual}"/>
|
||||
</body></html>
|
||||
HTML;
|
||||
|
||||
$htmlPath = $targetDir.'/it_matches_home_desktop.html';
|
||||
file_put_contents($htmlPath, $html);
|
||||
|
||||
$written = MismatchScreenshotExporter::export($htmlPath, $targetDir);
|
||||
|
||||
expect($written)->toHaveCount(3);
|
||||
expect($written)->toContain($targetDir.'/it_matches_home_desktop-diff.png');
|
||||
expect($written)->toContain($targetDir.'/it_matches_home_desktop-expected.png');
|
||||
expect($written)->toContain($targetDir.'/it_matches_home_desktop-actual.png');
|
||||
|
||||
expect(base64_decode($diff, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-diff.png'));
|
||||
expect(base64_decode($expected, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-expected.png'));
|
||||
expect(base64_decode($actual, true))->toBe((string) file_get_contents($targetDir.'/it_matches_home_desktop-actual.png'));
|
||||
});
|
||||
|
||||
it('returns an empty list when the html has no embedded images', function (): void {
|
||||
$targetDir = sys_get_temp_dir().'/mismatch-exporter-'.uniqid();
|
||||
mkdir($targetDir, 0755, true);
|
||||
|
||||
$htmlPath = $targetDir.'/plain.html';
|
||||
file_put_contents($htmlPath, '<html><body>no images</body></html>');
|
||||
|
||||
expect(MismatchScreenshotExporter::export($htmlPath, $targetDir))->toBe([]);
|
||||
});
|
||||
@@ -5,14 +5,23 @@ declare(strict_types=1);
|
||||
it('ships visual content fixtures as valid jpeg images', function (): void {
|
||||
$fixtures = [
|
||||
'og-default.jpg',
|
||||
'portfolio-cover.jpg',
|
||||
'service-cover.jpg',
|
||||
'gallery.jpg',
|
||||
'testimonial.jpg',
|
||||
'service-casamentos.jpg',
|
||||
'service-eventos-corporativos.jpg',
|
||||
'service-celebracoes-intimistas.jpg',
|
||||
'case-casamento-ana-lucas.jpg',
|
||||
'gallery-casamento-ana-lucas-1.jpg',
|
||||
'gallery-casamento-ana-lucas-2.jpg',
|
||||
'case-lancamento-verano.jpg',
|
||||
'gallery-lancamento-verano-1.jpg',
|
||||
'gallery-lancamento-verano-2.jpg',
|
||||
'case-mini-wedding-marina.jpg',
|
||||
'gallery-mini-wedding-marina-1.jpg',
|
||||
'gallery-mini-wedding-marina-2.jpg',
|
||||
'about-image.jpg',
|
||||
];
|
||||
|
||||
foreach ($fixtures as $fixture) {
|
||||
$path = dirname(__DIR__).'/fixtures/images/'.$fixture;
|
||||
$path = dirname(__DIR__, 2).'/database/fixtures/images/'.$fixture;
|
||||
|
||||
expect(is_file($path))->toBeTrue("Missing fixture: {$fixture}");
|
||||
|
||||
|
||||
BIN
tests/fixtures/images/gallery.jpg
vendored
|
Before Width: | Height: | Size: 310 B |
BIN
tests/fixtures/images/og-default.jpg
vendored
|
Before Width: | Height: | Size: 310 B |
BIN
tests/fixtures/images/portfolio-cover.jpg
vendored
|
Before Width: | Height: | Size: 310 B |
BIN
tests/fixtures/images/service-cover.jpg
vendored
|
Before Width: | Height: | Size: 311 B |
BIN
tests/fixtures/images/testimonial.jpg
vendored
|
Before Width: | Height: | Size: 310 B |