Compare commits
4 Commits
feat/dossi
...
feat/git-h
| Author | SHA1 | Date | |
|---|---|---|---|
| 246410803d | |||
| af8484c74e | |||
| 35bc5a59d9 | |||
| 27711ad0c2 |
3
.husky/pre-commit
Executable file
3
.husky/pre-commit
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
composer pint:check && composer phpstan
|
||||||
33
.husky/pre-push
Executable file
33
.husky/pre-push
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
# Abort push when the test database is unreachable, so broken code never
|
||||||
|
# reaches CI. Test DB settings come from phpunit.xml.
|
||||||
|
php -r '
|
||||||
|
$xml = @simplexml_load_file("phpunit.xml");
|
||||||
|
if ($xml === false) {
|
||||||
|
fwrite(STDERR, "phpunit.xml not found — aborting pre-push.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$defaults = ["DB_HOST" => "127.0.0.1", "DB_PORT" => "5432", "DB_DATABASE" => "amare_test", "DB_USERNAME" => "amare", "DB_PASSWORD" => "secret"];
|
||||||
|
$env = [];
|
||||||
|
foreach ($xml->php->env as $node) {
|
||||||
|
$name = (string) $node["name"];
|
||||||
|
if (isset($defaults[$name])) {
|
||||||
|
$env[$name] = (string) $node["value"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$config = array_merge($defaults, $env);
|
||||||
|
|
||||||
|
$dsn = sprintf("pgsql:host=%s;port=%s;dbname=%s", $config["DB_HOST"], $config["DB_PORT"], $config["DB_DATABASE"]);
|
||||||
|
try {
|
||||||
|
new PDO($dsn, $config["DB_USERNAME"], $config["DB_PASSWORD"], [PDO::ATTR_TIMEOUT => 3]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
fwrite(STDERR, "\033[31mPostgreSQL is unreachable on {$config["DB_HOST"]}:{$config["DB_PORT"]} (db: {$config["DB_DATABASE"]}).\033[0m\n");
|
||||||
|
fwrite(STDERR, "Start it with: docker compose up -d\n");
|
||||||
|
fwrite(STDERR, "Then retry the push.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
' || exit 1
|
||||||
|
|
||||||
|
composer test:unit && composer test:feature
|
||||||
@@ -15,6 +15,13 @@ This is a Laravel 13 application for an event-planning consultancy. Application
|
|||||||
|
|
||||||
Feature and browser tests require the `amare_test` PostgreSQL database configured in `phpunit.xml`.
|
Feature and browser tests require the `amare_test` PostgreSQL database configured in `phpunit.xml`.
|
||||||
|
|
||||||
|
## Git Hooks (husky)
|
||||||
|
|
||||||
|
Hooks live in `.husky/` and auto-install on any plain `npm install` via the `prepare` script. Note `composer setup` runs `npm install --ignore-scripts`, which skips hook installation — after setup, run `npm install` once (or `npx husky`) to activate hooks.
|
||||||
|
|
||||||
|
- `pre-commit`: runs `composer pint:check` and `composer phpstan`.
|
||||||
|
- `pre-push`: gates on the `amare_test` database (settings parsed from `phpunit.xml`), blocks the push with a `docker compose up -d` hint when Postgres is unreachable, then runs `composer test:unit` and `composer test:feature`. Browser tests are CI-only (FrankenPHP container).
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -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\Testimonial;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
@@ -23,7 +22,7 @@ class ContentSeeder extends Seeder
|
|||||||
$this->seedSiteSettings();
|
$this->seedSiteSettings();
|
||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->seedTestimonials();
|
$this->call(TestimonialsSeeder::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedSiteSettings(): void
|
private function seedSiteSettings(): void
|
||||||
@@ -170,67 +169,6 @@ class ContentSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedTestimonials(): void
|
|
||||||
{
|
|
||||||
// Real couples from depoimentos.md. Production publication still requires
|
|
||||||
// explicit couple authorization before setting published_at outside local/demo seeds.
|
|
||||||
$testimonials = [
|
|
||||||
[
|
|
||||||
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
|
||||||
'author_name' => 'Jeniffer e Maick',
|
|
||||||
'context' => 'Casamento · 06/12/2025',
|
|
||||||
'sort_order' => 1,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
|
||||||
'author_name' => 'Quesia e Jhonata',
|
|
||||||
'context' => 'Casamento · 21/12/2025',
|
|
||||||
'sort_order' => 2,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
|
||||||
'author_name' => 'Milena e Weslley',
|
|
||||||
'context' => 'Casamento · 13/02/2026',
|
|
||||||
'sort_order' => 3,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
|
||||||
'author_name' => 'Raquel e Pedro',
|
|
||||||
'context' => 'Casamento · 09/05/2026',
|
|
||||||
'sort_order' => 4,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
|
||||||
'author_name' => 'Victoria e Pedro',
|
|
||||||
'context' => 'Casamento · 24/06/2026',
|
|
||||||
'sort_order' => 5,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$keepAuthors = array_column($testimonials, 'author_name');
|
|
||||||
|
|
||||||
Testimonial::query()
|
|
||||||
->whereNotIn('author_name', $keepAuthors)
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
foreach ($testimonials as $testimonial) {
|
|
||||||
Testimonial::query()->updateOrCreate(
|
|
||||||
['author_name' => $testimonial['author_name']],
|
|
||||||
[
|
|
||||||
...$testimonial,
|
|
||||||
'photo_path' => null,
|
|
||||||
'photo_alt' => null,
|
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function copyFixture(string $fixtureName, string $destination): string
|
private function copyFixture(string $fixtureName, string $destination): string
|
||||||
{
|
{
|
||||||
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
$source = base_path('tests/fixtures/images/'.$fixtureName);
|
||||||
|
|||||||
70
database/seeders/TestimonialsSeeder.php
Normal file
70
database/seeders/TestimonialsSeeder.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Testimonial;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class TestimonialsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
private const PUBLISHED_AT = '2026-08-05 00:00:00';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$testimonials = [
|
||||||
|
[
|
||||||
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Casamento · 06/12/2025',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_featured' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_featured' => true,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||||
|
'author_name' => 'Milena e Weslley',
|
||||||
|
'context' => 'Casamento · 13/02/2026',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||||
|
'author_name' => 'Raquel e Pedro',
|
||||||
|
'context' => 'Casamento · 09/05/2026',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||||
|
'author_name' => 'Victoria e Pedro',
|
||||||
|
'context' => 'Casamento · 24/06/2026',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_featured' => false,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
DB::transaction(function () use ($testimonials): void {
|
||||||
|
foreach ($testimonials as $testimonial) {
|
||||||
|
Testimonial::query()->updateOrCreate(
|
||||||
|
['author_name' => $testimonial['author_name']],
|
||||||
|
[
|
||||||
|
'quote' => $testimonial['quote'],
|
||||||
|
'context' => $testimonial['context'],
|
||||||
|
'sort_order' => $testimonial['sort_order'],
|
||||||
|
'is_featured' => $testimonial['is_featured'],
|
||||||
|
'published_at' => self::PUBLISHED_AT,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -159,7 +159,7 @@ HTTP 404 from `compose.deploy` usually means the compose id is wrong (Applicatio
|
|||||||
|
|
||||||
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
|
||||||
|
|
||||||
## First admin (no `db:seed` in production)
|
## First admin and authorized production seeding
|
||||||
|
|
||||||
Seed credentials are local-only. For staging/production:
|
Seed credentials are local-only. For staging/production:
|
||||||
|
|
||||||
@@ -192,6 +192,35 @@ XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::que
|
|||||||
|
|
||||||
Never reuse `admin@amare.local` / `password`.
|
Never reuse `admin@amare.local` / `password`.
|
||||||
|
|
||||||
|
### Load authorized testimonials after migrations
|
||||||
|
|
||||||
|
After migrations, manually load the five authorized testimonials in **staging**, then repeat in **production**. From Dokploy, open a terminal on the environment's `web` service (or run an equivalent one-off process):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan db:seed --class='Database\Seeders\TestimonialsSeeder' --force --no-interaction
|
||||||
|
```
|
||||||
|
|
||||||
|
This seeder is safe to rerun: it overwrites canonical source-owned fields, preserves curated photo fields, and leaves unrelated testimonials unchanged. The five records are published with the approved deterministic timestamp. Upsert keys on `author_name` (no unique DB constraint); keep one row per couple before/after running.
|
||||||
|
|
||||||
|
Optional verification:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||||
|
\$expected = collect(['Jeniffer e Maick', 'Quesia e Jhonata', 'Milena e Weslley', 'Raquel e Pedro', 'Victoria e Pedro']);
|
||||||
|
\$rows = \\App\\Models\\Testimonial::query()->whereIn('author_name', \$expected)->get(['author_name', 'published_at'])->groupBy('author_name');
|
||||||
|
\$valid = \$expected->every(function (string \$author) use (\$rows): bool {
|
||||||
|
\$matches = \$rows->get(\$author, collect());
|
||||||
|
return \$matches->count() === 1
|
||||||
|
&& \$matches->first()->published_at?->format('Y-m-d H:i:s') === '2026-08-05 00:00:00';
|
||||||
|
});
|
||||||
|
echo (\$valid ? 'ok' : 'invalid').PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output: `ok`.
|
||||||
|
|
||||||
|
Do not run `DatabaseSeeder` or `ContentSeeder` in staging or production: they include local credentials and/or broad demo-content effects. Deployment workflows intentionally remain migrate-only; loading these testimonials is a deliberate manual operation in each environment.
|
||||||
|
|
||||||
## Backup and restore
|
## Backup and restore
|
||||||
|
|
||||||
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -7,6 +7,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
@@ -890,6 +891,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/husky": {
|
||||||
|
"version": "9.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
|
||||||
|
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"husky": "bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/typicode"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev": "vite"
|
"dev": "vite",
|
||||||
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
|
|||||||
@@ -179,9 +179,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
/* Transform/clip only — never fade text opacity (axe + WCAG mid-transition). */
|
||||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="seal"],
|
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="seal"],
|
||||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="cta"] {
|
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="cta"] {
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0.5rem);
|
transform: translateY(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,11 +194,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat] {
|
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat] {
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
clip-path: inset(0 0 0 0);
|
clip-path: inset(0 0 0 0);
|
||||||
transition:
|
transition:
|
||||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
|
||||||
transform var(--amare-duration-slow) var(--amare-ease-arrival),
|
transform var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||||
clip-path var(--amare-duration-focal) var(--amare-ease-arrival);
|
clip-path var(--amare-duration-focal) var(--amare-ease-arrival);
|
||||||
}
|
}
|
||||||
@@ -220,29 +218,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html[data-motion="enhance"] [data-reveal]:not(.is-revealed) {
|
html[data-motion="enhance"] [data-reveal]:not(.is-revealed) {
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0.5rem);
|
transform: translateY(0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-motion="enhance"] [data-reveal].is-revealed {
|
html[data-motion="enhance"] [data-reveal].is-revealed {
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
transition:
|
transition: transform var(--amare-duration-slow) var(--amare-ease-arrival);
|
||||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
|
||||||
transform var(--amare-duration-slow) var(--amare-ease-arrival);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) {
|
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) {
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(0.4rem);
|
transform: translateY(0.4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-motion="enhance"] [data-motion="page-open"].is-active {
|
html[data-motion="enhance"] [data-motion="page-open"].is-active {
|
||||||
opacity: 1;
|
|
||||||
transform: none;
|
transform: none;
|
||||||
transition:
|
transition: transform var(--amare-duration-normal) var(--amare-ease-arrival);
|
||||||
opacity var(--amare-duration-normal) var(--amare-ease-arrival),
|
|
||||||
transform var(--amare-duration-normal) var(--amare-ease-arrival);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
tasks.md
Normal file
8
tasks.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Tasks: Git hooks (pre-commit + pre-push)
|
||||||
|
|
||||||
|
- [x] Install husky + add prepare script
|
||||||
|
- [x] Create .husky/pre-commit (pint + phpstan)
|
||||||
|
- [x] Create .husky/pre-push (DB gate + tests)
|
||||||
|
- [x] Install hooks into repo, chmod +x
|
||||||
|
- [x] Verify hooks (DB up/down, pre-commit)
|
||||||
|
- [x] Document hooks in AGENTS.md
|
||||||
@@ -9,6 +9,7 @@ use App\Models\SiteSetting;
|
|||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Database\Seeders\ContentSeeder;
|
use Database\Seeders\ContentSeeder;
|
||||||
|
use Database\Seeders\TestimonialsSeeder;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
@@ -18,6 +19,59 @@ class TestimonialsTest extends TestCase
|
|||||||
{
|
{
|
||||||
use RefreshDatabase;
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var list<array{
|
||||||
|
* quote: string,
|
||||||
|
* author_name: string,
|
||||||
|
* context: string,
|
||||||
|
* sort_order: int,
|
||||||
|
* is_featured: bool,
|
||||||
|
* published_at: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private const EXPECTED_TESTIMONIALS = [
|
||||||
|
[
|
||||||
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Casamento · 06/12/2025',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'is_featured' => true,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.\n\nObrigada por tudo !",
|
||||||
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'is_featured' => true,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => 'Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!!',
|
||||||
|
'author_name' => 'Milena e Weslley',
|
||||||
|
'context' => 'Casamento · 13/02/2026',
|
||||||
|
'sort_order' => 3,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.\n\nDesde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.\n\nTambém adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.\n\nObrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.",
|
||||||
|
'author_name' => 'Raquel e Pedro',
|
||||||
|
'context' => 'Casamento · 09/05/2026',
|
||||||
|
'sort_order' => 4,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'quote' => "Miiii, meu amor… você e sua equipe foram impecáveis.\n\nSuperou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.\n\nSempre muito atenciosa e paciente.\n\nAdoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia.",
|
||||||
|
'author_name' => 'Victoria e Pedro',
|
||||||
|
'context' => 'Casamento · 24/06/2026',
|
||||||
|
'sort_order' => 5,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => '2026-08-05 00:00:00',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
public function test_published_scope_excludes_unpublished_testimonials(): void
|
public function test_published_scope_excludes_unpublished_testimonials(): void
|
||||||
{
|
{
|
||||||
Testimonial::factory()->create(['published_at' => null]);
|
Testimonial::factory()->create(['published_at' => null]);
|
||||||
@@ -48,11 +102,27 @@ class TestimonialsTest extends TestCase
|
|||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_maps_all_five_real_couples_from_depoimentos(): void
|
||||||
|
{
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$testimonials = Testimonial::query()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $this->sourceOwnedState($testimonial))
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertSame(self::EXPECTED_TESTIMONIALS, $testimonials);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
public function test_content_seeder_loads_five_real_couples_from_depoimentos(): void
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
Storage::fake('public');
|
||||||
|
|
||||||
$this->seed(ContentSeeder::class);
|
$this->artisan('db:seed', [
|
||||||
|
'--class' => ContentSeeder::class,
|
||||||
|
'--no-interaction' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
|
||||||
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
$authors = Testimonial::query()->orderBy('sort_order')->pluck('author_name')->all();
|
||||||
|
|
||||||
@@ -64,11 +134,6 @@ class TestimonialsTest extends TestCase
|
|||||||
'Victoria e Pedro',
|
'Victoria e Pedro',
|
||||||
], $authors);
|
], $authors);
|
||||||
|
|
||||||
$this->assertNull(
|
|
||||||
Testimonial::query()->where('author_name', 'Ana Souza')->first(),
|
|
||||||
'Fictional demo authors must not remain after seeding real testimonials.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
$jeniffer = Testimonial::query()->where('author_name', 'Jeniffer e Maick')->first();
|
||||||
|
|
||||||
$this->assertNotNull($jeniffer);
|
$this->assertNotNull($jeniffer);
|
||||||
@@ -77,6 +142,87 @@ class TestimonialsTest extends TestCase
|
|||||||
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
$this->assertSame('Casamento · 06/12/2025', $jeniffer->context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_authoritatively_overwrites_source_owned_fields(): void
|
||||||
|
{
|
||||||
|
$testimonial = Testimonial::factory()->create([
|
||||||
|
'quote' => 'Depoimento desatualizado.',
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Contexto desatualizado',
|
||||||
|
'sort_order' => 99,
|
||||||
|
'is_featured' => false,
|
||||||
|
'published_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
self::EXPECTED_TESTIMONIALS[0],
|
||||||
|
$this->sourceOwnedState($testimonial->refresh()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_preserves_curated_photo_fields_for_known_author(): void
|
||||||
|
{
|
||||||
|
$testimonial = Testimonial::factory()->create([
|
||||||
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'photo_path' => 'testimonials/jeniffer-e-maick.jpg',
|
||||||
|
'photo_alt' => 'Jeniffer e Maick durante o casamento',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$testimonial->refresh();
|
||||||
|
|
||||||
|
$this->assertSame('testimonials/jeniffer-e-maick.jpg', $testimonial->photo_path);
|
||||||
|
$this->assertSame('Jeniffer e Maick durante o casamento', $testimonial->photo_alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_leaves_unrelated_testimonials_unchanged(): void
|
||||||
|
{
|
||||||
|
$unrelated = Testimonial::factory()->published()->featured()->create([
|
||||||
|
'quote' => 'Depoimento cadastrado pela equipe.',
|
||||||
|
'author_name' => 'Casal não presente na fonte',
|
||||||
|
'context' => 'Bodas · 02/08/2026',
|
||||||
|
'photo_path' => 'testimonials/casal-equipe.jpg',
|
||||||
|
'photo_alt' => 'Casal cadastrado pela equipe',
|
||||||
|
'sort_order' => 42,
|
||||||
|
]);
|
||||||
|
$originalState = $unrelated->getAttributes();
|
||||||
|
ksort($originalState);
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$persisted = $unrelated->fresh();
|
||||||
|
|
||||||
|
$this->assertNotNull($persisted);
|
||||||
|
$persistedState = $persisted->getAttributes();
|
||||||
|
ksort($persistedState);
|
||||||
|
|
||||||
|
$this->assertSame($originalState, $persistedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_testimonials_seeder_is_idempotent_without_duplicate_authors(): void
|
||||||
|
{
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
$firstRunState = Testimonial::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->runTestimonialsSeeder();
|
||||||
|
|
||||||
|
$secondRunState = Testimonial::query()
|
||||||
|
->orderBy('id')
|
||||||
|
->get()
|
||||||
|
->map(fn (Testimonial $testimonial): array => $testimonial->getAttributes())
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$this->assertCount(5, $secondRunState);
|
||||||
|
$this->assertSame($firstRunState, $secondRunState);
|
||||||
|
$this->assertSame(5, Testimonial::query()->distinct()->count('author_name'));
|
||||||
|
}
|
||||||
|
|
||||||
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
public function test_home_renders_multi_paragraph_testimonial_quotes(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
@@ -97,4 +243,34 @@ class TestimonialsTest extends TestCase
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function runTestimonialsSeeder(): void
|
||||||
|
{
|
||||||
|
$this->artisan('db:seed', [
|
||||||
|
'--class' => TestimonialsSeeder::class,
|
||||||
|
'--no-interaction' => true,
|
||||||
|
])->assertExitCode(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* quote: string,
|
||||||
|
* author_name: string,
|
||||||
|
* context: string,
|
||||||
|
* sort_order: int,
|
||||||
|
* is_featured: bool,
|
||||||
|
* published_at: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
private function sourceOwnedState(Testimonial $testimonial): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'quote' => (string) $testimonial->quote,
|
||||||
|
'author_name' => (string) $testimonial->author_name,
|
||||||
|
'context' => (string) $testimonial->context,
|
||||||
|
'sort_order' => (int) $testimonial->sort_order,
|
||||||
|
'is_featured' => (bool) $testimonial->is_featured,
|
||||||
|
'published_at' => $testimonial->published_at?->format('Y-m-d H:i:s') ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,20 @@ class MotionMarkupTest extends TestCase
|
|||||||
$this->assertStringContainsString('[data-chapter-index]', $appCss);
|
$this->assertStringContainsString('[data-chapter-index]', $appCss);
|
||||||
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
|
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
|
||||||
|
|
||||||
|
// Entrance motion must not fade text opacity — mid-fade fails WCAG contrast (axe).
|
||||||
|
$this->assertDoesNotMatchRegularExpression(
|
||||||
|
'/\[data-motion-beat="seal"\][^{]*\{[^}]*opacity:\s*0/s',
|
||||||
|
$appCss,
|
||||||
|
);
|
||||||
|
$this->assertDoesNotMatchRegularExpression(
|
||||||
|
'/\[data-motion="page-open"\]:not\(\.is-active\)[^{]*\{[^}]*opacity:\s*0/s',
|
||||||
|
$appCss,
|
||||||
|
);
|
||||||
|
$this->assertDoesNotMatchRegularExpression(
|
||||||
|
'/\[data-reveal\]:not\(\.is-revealed\)[^{]*\{[^}]*opacity:\s*0/s',
|
||||||
|
$appCss,
|
||||||
|
);
|
||||||
|
|
||||||
$this->assertFileExists(resource_path('js/motion.js'));
|
$this->assertFileExists(resource_path('js/motion.js'));
|
||||||
$this->assertStringContainsString("import './motion.js'", $appJs);
|
$this->assertStringContainsString("import './motion.js'", $appJs);
|
||||||
$this->assertStringContainsString('prefers-reduced-motion', (string) file_get_contents(resource_path('js/motion.js')));
|
$this->assertStringContainsString('prefers-reduced-motion', (string) file_get_contents(resource_path('js/motion.js')));
|
||||||
|
|||||||
Reference in New Issue
Block a user