Compare commits
35 Commits
feature/re
...
feat/setup
| Author | SHA1 | Date | |
|---|---|---|---|
| 76b7673a51 | |||
| 457845758c | |||
| 0aee15e848 | |||
| b372eaea4f | |||
| 7380998e8e | |||
| 4b440b7e5b | |||
| 5949fad9b1 | |||
| d054d04ab9 | |||
| db4453b165 | |||
| 946939ba94 | |||
| 10a1f0dc7c | |||
| c04790ee1f | |||
| edf1c48050 | |||
| 3c711a74f3 | |||
| 88d63f349d | |||
| e0390ff333 | |||
| 321f0cee11 | |||
| 1d23e16625 | |||
| 33788af172 | |||
| 119711d6f0 | |||
| f09ea83071 | |||
| 2fe262ab59 | |||
| 018b43e24f | |||
| 9fac784f6a | |||
| 8a12b7d82c | |||
| af8484c74e | |||
| 35bc5a59d9 | |||
| 27711ad0c2 | |||
| af31738021 | |||
| 286522d8a2 | |||
| 3e68192cf3 | |||
| 1a31dd1cf6 | |||
| 9060036024 | |||
| 7f4ea01f4b | |||
| cf1589c916 |
@@ -2,6 +2,7 @@ APP_NAME=Amare
|
|||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
# Staging/production: set APP_URL to the public HTTPS origin (e.g. https://staging.example.com).
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
APP_LOCALE=pt_BR
|
APP_LOCALE=pt_BR
|
||||||
@@ -38,6 +39,10 @@ SESSION_LIFETIME=120
|
|||||||
SESSION_ENCRYPT=false
|
SESSION_ENCRYPT=false
|
||||||
SESSION_PATH=/
|
SESSION_PATH=/
|
||||||
SESSION_DOMAIN=null
|
SESSION_DOMAIN=null
|
||||||
|
# Staging/production behind HTTPS (Dokploy Traefik): SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_SECURE_COOKIE=false
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
|
||||||
BROADCAST_CONNECTION=log
|
BROADCAST_CONNECTION=log
|
||||||
FILESYSTEM_DISK=local
|
FILESYSTEM_DISK=local
|
||||||
|
|||||||
26
.github/workflows/ci.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
|||||||
name: static
|
name: static
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -39,7 +40,7 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
@@ -55,7 +56,8 @@ jobs:
|
|||||||
name: unit
|
name: unit
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +65,7 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
@@ -92,7 +94,8 @@ jobs:
|
|||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -100,13 +103,13 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
restore-keys: composer-${{ runner.os }}-
|
restore-keys: composer-${{ runner.os }}-
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -136,7 +139,8 @@ jobs:
|
|||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 10
|
--health-retries 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
- run: cp .env.example .env
|
||||||
|
|
||||||
- uses: shivammathur/setup-php@v2
|
- uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
@@ -144,13 +148,13 @@ jobs:
|
|||||||
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_pgsql, bcmath, intl, sodium, gd
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.composer/cache/files
|
path: ~/.composer/cache/files
|
||||||
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
|
||||||
restore-keys: composer-${{ runner.os }}-
|
restore-keys: composer-${{ runner.os }}-
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
|
||||||
@@ -224,7 +228,7 @@ jobs:
|
|||||||
name: container
|
name: container
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Build production image
|
- name: Build production image
|
||||||
run: docker build -t amare-app:ci .
|
run: docker build -t amare-app:ci .
|
||||||
|
|||||||
82
.github/workflows/deploy-staging.yml
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
name: Deploy staging
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [CI]
|
||||||
|
types: [completed]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-staging
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: publish-and-deploy-staging
|
||||||
|
if: >-
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.event == 'push' &&
|
||||||
|
github.event.workflow_run.head_branch == 'main'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout deployed SHA
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|
||||||
|
- name: Set image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHA="${{ github.event.workflow_run.head_sha }}"
|
||||||
|
SHORT_SHA="${SHA:0:7}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push SHA + staging tags
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}
|
||||||
|
${{ steps.meta.outputs.image }}:staging
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Deploy staging on Dokploy
|
||||||
|
env:
|
||||||
|
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||||
|
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||||
|
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_STAGING_COMPOSE_ID }}
|
||||||
|
DEPLOY_TITLE: "staging ${{ steps.meta.outputs.short_sha }}"
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
- name: Smoke staging
|
||||||
|
env:
|
||||||
|
SMOKE_BASE_URL: ${{ secrets.STAGING_URL }}
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/smoke.sh
|
||||||
|
./scripts/deploy/smoke.sh
|
||||||
84
.github/workflows/promote-production.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Promote production
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
sha:
|
||||||
|
description: Full git SHA already published to GHCR (same digest used by staging)
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
confirm:
|
||||||
|
description: Type PRODUCTION to confirm promotion of the given SHA
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-production
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
promote:
|
||||||
|
name: promote-production
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Guard confirmation
|
||||||
|
run: |
|
||||||
|
if [ "${{ inputs.confirm }}" != "PRODUCTION" ]; then
|
||||||
|
echo "Confirmation must be exactly PRODUCTION" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Checkout repository scripts
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set image metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
SHA="${{ inputs.sha }}"
|
||||||
|
SHORT_SHA="${SHA:0:7}"
|
||||||
|
IMAGE="${REGISTRY}/${IMAGE_NAME}"
|
||||||
|
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Point :production at existing SHA digest (no rebuild)
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "${{ steps.meta.outputs.image }}:production" \
|
||||||
|
"${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.sha }}"
|
||||||
|
|
||||||
|
- name: Deploy production on Dokploy
|
||||||
|
env:
|
||||||
|
DOKPLOY_URL: ${{ secrets.DOKPLOY_URL }}
|
||||||
|
DOKPLOY_API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||||
|
DOKPLOY_COMPOSE_ID: ${{ secrets.DOKPLOY_PRODUCTION_COMPOSE_ID }}
|
||||||
|
DEPLOY_TITLE: "production ${{ steps.meta.outputs.short_sha }}"
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/dokploy-deploy.sh
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
- name: Smoke production
|
||||||
|
env:
|
||||||
|
SMOKE_BASE_URL: ${{ secrets.PRODUCTION_URL }}
|
||||||
|
run: |
|
||||||
|
chmod +x scripts/deploy/smoke.sh
|
||||||
|
./scripts/deploy/smoke.sh
|
||||||
1
.gitignore
vendored
@@ -18,6 +18,7 @@
|
|||||||
/public/fonts-manifest.dev.json
|
/public/fonts-manifest.dev.json
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
|
/tests/Browser/Screenshots
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
|
|||||||
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
@@ -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
|
||||||
@@ -12,5 +12,6 @@ related_targets: ["resources/views/pages/services/index.blade.php","resources/vi
|
|||||||
- **Action, proof, and constraints:** ação principal é enviar briefing inicial. Prova disponível: cinco depoimentos reais de casamentos, com nomes e datas; publicação depende de autorização final. Não inventar cases corporativos, credenciais ou resultados. Fotografias permanecem ilustrativas e marcadas até chegada de acervo autorizado.
|
- **Action, proof, and constraints:** ação principal é enviar briefing inicial. Prova disponível: cinco depoimentos reais de casamentos, com nomes e datas; publicação depende de autorização final. Não inventar cases corporativos, credenciais ou resultados. Fotografias permanecem ilustrativas e marcadas até chegada de acervo autorizado.
|
||||||
- **Chosen direction:** “Dossiê Editorial do Evento”. Home funciona como capa e índice; serviços viram capítulos, portfólio vira cadernos de caso, sobre vira perfil editorial e contato vira ficha de briefing. Sistema visual global segue Heritage Editorial em DESIGN.md.
|
- **Chosen direction:** “Dossiê Editorial do Evento”. Home funciona como capa e índice; serviços viram capítulos, portfólio vira cadernos de caso, sobre vira perfil editorial e contato vira ficha de briefing. Sistema visual global segue Heritage Editorial em DESIGN.md.
|
||||||
- **Memorable moment:** coração facetado atua como selo editorial enquanto índice discreto acompanha capítulos e deixa serviço, método e próximo passo visíveis sem transformar página em dashboard.
|
- **Memorable moment:** coração facetado atua como selo editorial enquanto índice discreto acompanha capítulos e deixa serviço, método e próximo passo visíveis sem transformar página em dashboard.
|
||||||
|
- **Motion thesis (Dossiê vivo):** Home recebe abertura autoral (selo → título → recorte de imagem → CTAs, ≤800ms). Índice lateral no desktop e linha de progresso no mobile acompanham capítulos. Serviços, portfólio e sobre usam abertura curta e revelações discretas; contato, privacidade e erros permanecem quase estáticos. Sem parallax, loops, bounce ou scroll-jacking. `prefers-reduced-motion` entrega estado final imediato.
|
||||||
- **Responsive and interaction:** spreads assimétricos no desktop; sequência linear no mobile. CTAs recorrentes, navegação clara, formulário com loading, erro e sucesso, foco visível e suporte a movimento reduzido.
|
- **Responsive and interaction:** spreads assimétricos no desktop; sequência linear no mobile. CTAs recorrentes, navegação clara, formulário com loading, erro e sucesso, foco visível e suporte a movimento reduzido.
|
||||||
- **Unresolved:** logo transparente ou vetorial; fotografias autorizadas; WhatsApp, e-mail e Instagram oficiais; textos jurídicos; autorização dos depoimentos; provas reais de eventos corporativos.
|
- **Unresolved:** logo transparente ou vetorial; fotografias autorizadas; WhatsApp, e-mail e Instagram oficiais; textos jurídicos; autorização dos depoimentos; provas reais de eventos corporativos.
|
||||||
|
|||||||
25
AGENTS.md
@@ -15,6 +15,17 @@ 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`.
|
||||||
|
|
||||||
|
## Worktrees
|
||||||
|
|
||||||
|
Always work in a git worktree created from the `main` ref — never modify `main` directly and never commit from the primary working tree. Create a dedicated worktree per feature/branch with `git worktree add -b <branch> <path> main`. On finishing work, create a PR, watch CI until green, then merge it. Clean up the worktree with `git worktree remove` after merge.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -30,3 +41,17 @@ History follows Conventional Commit-style subjects, for example `feat: Fase 0
|
|||||||
## Security & Configuration
|
## Security & Configuration
|
||||||
|
|
||||||
Copy `.env.example`; never commit secrets or production credentials. Development seed credentials are local-only. Validate uploads and authorization through Laravel policies, and run `composer security-audit` after dependency changes.
|
Copy `.env.example`; never commit secrets or production credentials. Development seed credentials are local-only. Validate uploads and authorization through Laravel policies, and run `composer security-audit` after dependency changes.
|
||||||
|
|
||||||
|
## Agent skills
|
||||||
|
|
||||||
|
### Issue tracker
|
||||||
|
|
||||||
|
Issues live in Linear, driven through the Linear MCP tools. See `docs/agents/issue-tracker.md`.
|
||||||
|
|
||||||
|
### Triage labels
|
||||||
|
|
||||||
|
Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
||||||
|
|
||||||
|
### Domain docs
|
||||||
|
|
||||||
|
Single-context: `CONTEXT.md` at root + `docs/adr/`. See `docs/agents/domain.md`.
|
||||||
|
|||||||
@@ -125,3 +125,10 @@ Após `php artisan db:seed`:
|
|||||||
- [SPEC.md](SPEC.md) — especificação do produto
|
- [SPEC.md](SPEC.md) — especificação do produto
|
||||||
- [docs/adr/](docs/adr/) — ADRs aceitas
|
- [docs/adr/](docs/adr/) — ADRs aceitas
|
||||||
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
|
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
|
||||||
|
- [docs/deployment/dokploy.md](docs/deployment/dokploy.md) — deploy staging/produção no Dokploy + GHCR
|
||||||
|
|
||||||
|
## Deploy (Dokploy)
|
||||||
|
|
||||||
|
Staging publica automaticamente após CI verde em `main` (imagem GHCR por SHA + alias `:staging`). Produção promove a **mesma digest** com workflow manual `Promote production` (sem rebuild).
|
||||||
|
|
||||||
|
Ver runbook completo: [docs/deployment/dokploy.md](docs/deployment/dokploy.md).
|
||||||
|
|||||||
104
SPEC.md
@@ -14,7 +14,8 @@
|
|||||||
| Estágio | MVP |
|
| Estágio | MVP |
|
||||||
| Status da especificação | Aprovada para implementação |
|
| Status da especificação | Aprovada para implementação |
|
||||||
| Idioma da interface | Português do Brasil (`pt-BR`) |
|
| Idioma da interface | Português do Brasil (`pt-BR`) |
|
||||||
| Timezone padrão | `America/Fortaleza` |
|
| Timezone padrão | `America/Sao_Paulo` |
|
||||||
|
| Cidade de atuação | São Paulo (capital) |
|
||||||
| Moeda | BRL, sem conversão entre moedas |
|
| Moeda | BRL, sem conversão entre moedas |
|
||||||
| Princípio principal | YAGNI — implementar somente o necessário para validar o produto |
|
| Princípio principal | YAGNI — implementar somente o necessário para validar o produto |
|
||||||
| Arquitetura | Monólito modular Laravel |
|
| Arquitetura | Monólito modular Laravel |
|
||||||
@@ -44,6 +45,8 @@ Em caso de conflito, seguir esta ordem:
|
|||||||
|
|
||||||
O agente **NÃO DEVE** alterar silenciosamente uma decisão deste documento. Uma alteração de escopo ou arquitetura deve atualizar esta especificação ou criar uma ADR.
|
O agente **NÃO DEVE** alterar silenciosamente uma decisão deste documento. Uma alteração de escopo ou arquitetura deve atualizar esta especificação ou criar uma ADR.
|
||||||
|
|
||||||
|
Mudanças incrementais são planejadas em `openspec/changes/` e, após arquivadas, este documento DEVE ser revalidado para incorporar decisões ratificadas (back-sync). Este arquivo permanece a fonte de verdade do produto.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Contrato de operação para agentes
|
## 1. Contrato de operação para agentes
|
||||||
@@ -349,13 +352,14 @@ A home DEVE conter, nesta ordem aproximada:
|
|||||||
|
|
||||||
1. header e navegação;
|
1. header e navegação;
|
||||||
2. hero com proposta de valor e CTA;
|
2. hero com proposta de valor e CTA;
|
||||||
3. prova visual por eventos em destaque;
|
3. manifesto da marca;
|
||||||
4. resumo dos serviços;
|
4. resumo dos serviços em destaque;
|
||||||
5. método de trabalho;
|
5. casos selecionados do portfólio;
|
||||||
6. casos selecionados;
|
6. método de trabalho (4 passos);
|
||||||
7. depoimentos;
|
7. depoimentos;
|
||||||
8. CTA final para briefing;
|
8. posicionamento/perfil;
|
||||||
9. footer com contato, redes e links legais.
|
9. CTA final para briefing;
|
||||||
|
10. footer com contato, redes e links legais.
|
||||||
|
|
||||||
A ordem pode variar apenas se a revisão de UX justificar a mudança.
|
A ordem pode variar apenas se a revisão de UX justificar a mudança.
|
||||||
|
|
||||||
@@ -374,6 +378,15 @@ Centralizar tokens de:
|
|||||||
|
|
||||||
Não espalhar valores visuais arbitrários por componentes.
|
Não espalhar valores visuais arbitrários por componentes.
|
||||||
|
|
||||||
|
O MVP adota o sistema de design **Heritage Editorial** (ver DESIGN.md e ADR-013):
|
||||||
|
|
||||||
|
- tipografia serifada auto-hospedada (EB Garamond) com escala de display a label;
|
||||||
|
- paleta papel/oliva/sálvia/tinta com superfícies tonais; hierarquia sem sombras de card;
|
||||||
|
- raio de borda zero para superfícies interativas e de conteúdo;
|
||||||
|
- largura de container 1120px e ritmo de espaçamento de 8px;
|
||||||
|
- `prefers-reduced-motion` respeitado;
|
||||||
|
- contraste WCAG AA (tinta sobre papel e oliva sobre papel).
|
||||||
|
|
||||||
### 6.4 Requisitos de mídia
|
### 6.4 Requisitos de mídia
|
||||||
|
|
||||||
- Imagens públicas DEVERÃO possuir texto alternativo.
|
- Imagens públicas DEVERÃO possuir texto alternativo.
|
||||||
@@ -383,6 +396,7 @@ Não espalhar valores visuais arbitrários por componentes.
|
|||||||
- Dimensões DEVERÃO ser reservadas para evitar layout shift.
|
- Dimensões DEVERÃO ser reservadas para evitar layout shift.
|
||||||
- Não armazenar blobs de imagem no PostgreSQL.
|
- Não armazenar blobs de imagem no PostgreSQL.
|
||||||
- Não depender do disco efêmero do contêiner em produção.
|
- Não depender do disco efêmero do contêiner em produção.
|
||||||
|
- O logotipo da marca DEVE possuir texto alternativo e variantes claro/escuro; `site_settings.logo_path` sobrescreve o asset padrão quando preenchido.
|
||||||
|
|
||||||
### 6.5 Acessibilidade
|
### 6.5 Acessibilidade
|
||||||
|
|
||||||
@@ -534,12 +548,10 @@ Campos públicos:
|
|||||||
- validar no servidor;
|
- validar no servidor;
|
||||||
- usar honeypot e rate limiting;
|
- usar honeypot e rate limiting;
|
||||||
- impedir duplo envio acidental;
|
- impedir duplo envio acidental;
|
||||||
- criar Lead com status `new`;
|
|
||||||
- registrar origem `website`;
|
|
||||||
- notificar administradores;
|
|
||||||
- exibir sucesso sem revelar dados internos;
|
|
||||||
- enviar e-mail de confirmação quando o serviço de e-mail estiver configurado;
|
- enviar e-mail de confirmação quando o serviço de e-mail estiver configurado;
|
||||||
- falha no e-mail não pode apagar o lead já criado.
|
- exibir sucesso sem revelar dados internos.
|
||||||
|
|
||||||
|
> **Estado atual (Fases 0–1):** o formulário usa Blade + Controller e envia apenas e-mails informativos (para a assessoria e confirmação ao visitante), sem criar Lead. A criação de Lead (status `new`, origem `website`), a notificação aos administradores e o registro de aceite de privacidade entram na **Fase 2**, quando o formulário passa a criar o Lead via `CaptureWebsiteLead`. A falha de e-mail não pode apagar dados já criados.
|
||||||
|
|
||||||
**Aceite:**
|
**Aceite:**
|
||||||
|
|
||||||
@@ -1253,10 +1265,19 @@ Singleton:
|
|||||||
|
|
||||||
- `id` bigint PK;
|
- `id` bigint PK;
|
||||||
- `brand_name`;
|
- `brand_name`;
|
||||||
|
- `logo_path` nullable;
|
||||||
|
- `logo_alt` nullable;
|
||||||
- `hero_eyebrow` nullable;
|
- `hero_eyebrow` nullable;
|
||||||
- `hero_title`;
|
- `hero_title`;
|
||||||
- `hero_subtitle`;
|
- `hero_subtitle`;
|
||||||
- `hero_cta_label`;
|
- `hero_cta_label`;
|
||||||
|
- `hero_cta_secondary_label` nullable;
|
||||||
|
- `hero_note` nullable;
|
||||||
|
- `manifesto_title`;
|
||||||
|
- `manifesto_lead`;
|
||||||
|
- `manifesto_body`;
|
||||||
|
- `method_steps` jsonb (4 passos tipados);
|
||||||
|
- `principles` jsonb (lista tipada);
|
||||||
- `about_summary` nullable;
|
- `about_summary` nullable;
|
||||||
- `email`;
|
- `email`;
|
||||||
- `phone`;
|
- `phone`;
|
||||||
@@ -1265,7 +1286,8 @@ Singleton:
|
|||||||
- `default_meta_title`;
|
- `default_meta_title`;
|
||||||
- `default_meta_description`;
|
- `default_meta_description`;
|
||||||
- `default_og_image_path` nullable;
|
- `default_og_image_path` nullable;
|
||||||
- analytics fields nullable;
|
- `default_og_image_alt` nullable;
|
||||||
|
- `analytics_enabled` boolean default false;
|
||||||
- timestamps.
|
- timestamps.
|
||||||
|
|
||||||
#### `services`
|
#### `services`
|
||||||
@@ -1536,7 +1558,7 @@ Constraints de banco devem proteger:
|
|||||||
| Servidor | FrankenPHP + Caddy |
|
| Servidor | FrankenPHP + Caddy |
|
||||||
| Assets | Vite |
|
| Assets | Vite |
|
||||||
| Testes | Pest 4 + Pest Browser/Playwright |
|
| Testes | Pest 4 + Pest Browser/Playwright |
|
||||||
| Arquivos | Laravel Filesystem + storage S3-compatible em produção |
|
| Arquivos | Laravel Filesystem + Cloudflare R2 (S3-compatible) em produção |
|
||||||
| Fila | Database queue |
|
| Fila | Database queue |
|
||||||
| Scheduler | Laravel Scheduler em processo separado |
|
| Scheduler | Laravel Scheduler em processo separado |
|
||||||
|
|
||||||
@@ -1764,6 +1786,8 @@ Não transformar todas as seções estáticas em componentes Livewire. Usar Blad
|
|||||||
|
|
||||||
### 11.2 Formulário de briefing
|
### 11.2 Formulário de briefing
|
||||||
|
|
||||||
|
> **Estado atual (Fases 0–1):** o formulário é implementado em Blade + Controller (`POST /contato`, `ContactBriefingRequest`), conforme WEB-05. Se a Fase 2 mantiver Blade + Controller, os requisitos abaixo valem para o formulário e seus testes independentemente da tecnologia; a criação de Lead segue para a Fase 2.
|
||||||
|
|
||||||
O componente deve:
|
O componente deve:
|
||||||
|
|
||||||
- ter estado tipado ou Form Object quando útil;
|
- ter estado tipado ou Form Object quando útil;
|
||||||
@@ -1970,7 +1994,7 @@ Determinismo obrigatório:
|
|||||||
|
|
||||||
- Chromium e imagem Linux fixos;
|
- Chromium e imagem Linux fixos;
|
||||||
- viewport fixo;
|
- viewport fixo;
|
||||||
- timezone `America/Fortaleza`;
|
- timezone `America/Sao_Paulo`;
|
||||||
- locale `pt-BR`;
|
- locale `pt-BR`;
|
||||||
- fontes instaladas na imagem;
|
- fontes instaladas na imagem;
|
||||||
- relógio congelado;
|
- relógio congelado;
|
||||||
@@ -2052,7 +2076,7 @@ quality → Pint check + PHPStan/Larastan + audits + testes
|
|||||||
|
|
||||||
| Job | Responsabilidade | Bloqueia merge |
|
| Job | Responsabilidade | Bloqueia merge |
|
||||||
|---|---|---:|
|
|---|---|---:|
|
||||||
| `static` | Pint, PHPStan/Larastan, Composer validate e audits | Sim |
|
| `static` | Pint, PHPStan/Larastan, Composer validate, Composer e npm audit | Sim |
|
||||||
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
||||||
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
||||||
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e visual | Sim |
|
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e visual | Sim |
|
||||||
@@ -2073,11 +2097,11 @@ quality → Pint check + PHPStan/Larastan + audits + testes
|
|||||||
### 14.3 Branches e ambientes
|
### 14.3 Branches e ambientes
|
||||||
|
|
||||||
- PR: testes e preview opcional;
|
- PR: testes e preview opcional;
|
||||||
- `main`: build imutável por SHA e deploy automático em staging;
|
- `main`: build imutável por SHA publicado no GHCR e deploy automático em staging via Dokploy;
|
||||||
- staging: migration, cache warmup e smoke pós-deploy;
|
- staging: Dokploy Compose executa migração, healthcheck `/up` e smoke pós-deploy (`/up`, `/`, `/admin/login`);
|
||||||
- produção: promoção da mesma imagem aprovada, sem rebuild;
|
- produção: promoção da mesma imagem aprovada, sem rebuild (retag do digest em `:production`);
|
||||||
- produção requer aprovação humana explícita no MVP;
|
- produção requer aprovação humana explícita no MVP (`workflow_dispatch` com confirmação);
|
||||||
- rollback usa imagem anterior;
|
- rollback usa imagem anterior (SHA anterior, sem rebuild);
|
||||||
- migrations devem ser backward-compatible quando possível.
|
- migrations devem ser backward-compatible quando possível.
|
||||||
|
|
||||||
### 14.4 Definition of Done
|
### 14.4 Definition of Done
|
||||||
@@ -2159,7 +2183,7 @@ APP_DEBUG=false
|
|||||||
APP_URL
|
APP_URL
|
||||||
APP_LOCALE=pt_BR
|
APP_LOCALE=pt_BR
|
||||||
APP_FALLBACK_LOCALE=pt_BR
|
APP_FALLBACK_LOCALE=pt_BR
|
||||||
APP_TIMEZONE=America/Fortaleza
|
APP_TIMEZONE=America/Sao_Paulo
|
||||||
|
|
||||||
DB_CONNECTION=pgsql
|
DB_CONNECTION=pgsql
|
||||||
DB_HOST
|
DB_HOST
|
||||||
@@ -2172,19 +2196,16 @@ CACHE_STORE=database ou file conforme ambiente
|
|||||||
QUEUE_CONNECTION=database
|
QUEUE_CONNECTION=database
|
||||||
SESSION_DRIVER=database ou cookie conforme decisão
|
SESSION_DRIVER=database ou cookie conforme decisão
|
||||||
|
|
||||||
FILESYSTEM_DISK=s3 em produção
|
FILESYSTEM_DISK=r2 em produção
|
||||||
AWS_ACCESS_KEY_ID
|
R2_ACCESS_KEY_ID
|
||||||
AWS_SECRET_ACCESS_KEY
|
R2_SECRET_ACCESS_KEY
|
||||||
AWS_DEFAULT_REGION
|
R2_BUCKET
|
||||||
AWS_BUCKET
|
R2_ENDPOINT
|
||||||
AWS_ENDPOINT opcional
|
R2_URL (domínio próprio opcional)
|
||||||
AWS_USE_PATH_STYLE_ENDPOINT opcional
|
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_ENDPOINT, AWS_USE_PATH_STYLE_ENDPOINT — opcionais, apenas se o disk s3 for usado
|
||||||
|
|
||||||
MAIL_MAILER
|
MAIL_MAILER=resend em produção (mailer nativo Laravel)
|
||||||
MAIL_HOST
|
RESEND_API_KEY
|
||||||
MAIL_PORT
|
|
||||||
MAIL_USERNAME
|
|
||||||
MAIL_PASSWORD
|
|
||||||
MAIL_FROM_ADDRESS
|
MAIL_FROM_ADDRESS
|
||||||
MAIL_FROM_NAME
|
MAIL_FROM_NAME
|
||||||
```
|
```
|
||||||
@@ -2279,7 +2300,7 @@ O seed deve criar:
|
|||||||
- configurações do site;
|
- configurações do site;
|
||||||
- 3 serviços;
|
- 3 serviços;
|
||||||
- 3 casos de portfólio;
|
- 3 casos de portfólio;
|
||||||
- 3 depoimentos;
|
- depoimentos reais: os 5 casais de `depoimentos.md` (Jeniffer e Maick, Quesia e Jhonata, Milena e Weslley, Raquel e Pedro, Victoria e Pedro), preservando texto e datas; autores fictícios de demonstração removidos; em produção permanecem não publicados até autorização explícita de publicação;
|
||||||
- leads em estados variados;
|
- leads em estados variados;
|
||||||
- 2 eventos futuros e 1 concluído;
|
- 2 eventos futuros e 1 concluído;
|
||||||
- tarefas vencidas e futuras;
|
- tarefas vencidas e futuras;
|
||||||
@@ -2314,7 +2335,7 @@ O agente deve implementar na sequência, salvo instrução explícita.
|
|||||||
- [x] Filament instalado e autenticado;
|
- [x] Filament instalado e autenticado;
|
||||||
- [x] Livewire configurado;
|
- [x] Livewire configurado;
|
||||||
- [x] Tailwind/Vite;
|
- [x] Tailwind/Vite;
|
||||||
- [x] FrankenPHP e Docker Compose local;
|
- [~] FrankenPHP e Docker Compose local (imagem pronta; serviço de aplicação local pendente);
|
||||||
- [x] papéis admin/assistant;
|
- [x] papéis admin/assistant;
|
||||||
- [x] Pint;
|
- [x] Pint;
|
||||||
- [x] PHPStan/Larastan;
|
- [x] PHPStan/Larastan;
|
||||||
@@ -2325,6 +2346,13 @@ O agente deve implementar na sequência, salvo instrução explícita.
|
|||||||
- [x] design tokens mínimos;
|
- [x] design tokens mínimos;
|
||||||
- [x] healthcheck;
|
- [x] healthcheck;
|
||||||
- [x] seed de admin local.
|
- [x] seed de admin local.
|
||||||
|
- [ ] verificação de e-mail e reset seguro (MustVerifyEmail);
|
||||||
|
- [ ] npm audit no `composer quality` e no job `static`;
|
||||||
|
- [ ] gate de cobertura `Domain`/`Application` ≥ 80%;
|
||||||
|
- [ ] serviço de aplicação FrankenPHP no Compose local;
|
||||||
|
- [ ] hello-world implantado em staging (critério de saída).
|
||||||
|
|
||||||
|
> Os itens pendentes acima são tratados pela mudança OpenSpec `complete-foundation-parity`; o critério de saída da fase só é atingido com staging implantado.
|
||||||
|
|
||||||
**Critério de saída:** pipeline verde e hello-world implantado em staging.
|
**Critério de saída:** pipeline verde e hello-world implantado em staging.
|
||||||
|
|
||||||
@@ -2481,6 +2509,10 @@ Toda operação financeira deve:
|
|||||||
| ADR-008 | Database queue; Redis adiado | Aceita |
|
| ADR-008 | Database queue; Redis adiado | Aceita |
|
||||||
| ADR-009 | Dinheiro em BRL armazenado como centavos inteiros | Aceita |
|
| ADR-009 | Dinheiro em BRL armazenado como centavos inteiros | Aceita |
|
||||||
| ADR-010 | Home com estrutura fixa e CMS tipado, sem page builder | Aceita |
|
| ADR-010 | Home com estrutura fixa e CMS tipado, sem page builder | Aceita |
|
||||||
|
| ADR-011 | Cloudflare R2 (S3-compatible) como storage de objetos em produção | Aceita |
|
||||||
|
| ADR-012 | E-mail transacional via Resend (mailer nativo Laravel) | Aceita |
|
||||||
|
| ADR-013 | Design system Heritage Editorial para o site público | Aceita |
|
||||||
|
| ADR-014 | Deploy via Dokploy Compose com imagem imutável por SHA no GHCR | Aceita |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ final readonly class PageMeta
|
|||||||
public ?string $ogImageUrl = null,
|
public ?string $ogImageUrl = null,
|
||||||
public ?string $ogImageAlt = null,
|
public ?string $ogImageAlt = null,
|
||||||
public ?array $jsonLd = null,
|
public ?array $jsonLd = null,
|
||||||
|
public string $siteName = '',
|
||||||
|
public string $robots = 'index, follow',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,6 +46,7 @@ final readonly class PageMeta
|
|||||||
ogImageUrl: $ogImageUrl ?? self::defaultOgImageUrl($settings),
|
ogImageUrl: $ogImageUrl ?? self::defaultOgImageUrl($settings),
|
||||||
ogImageAlt: $ogImageAlt ?? $settings->default_og_image_alt,
|
ogImageAlt: $ogImageAlt ?? $settings->default_og_image_alt,
|
||||||
jsonLd: $jsonLd,
|
jsonLd: $jsonLd,
|
||||||
|
siteName: (string) $settings->brand_name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +80,53 @@ final readonly class PageMeta
|
|||||||
ogImageUrl: $ogImageUrl,
|
ogImageUrl: $ogImageUrl,
|
||||||
ogImageAlt: $ogImageAlt,
|
ogImageAlt: $ogImageAlt,
|
||||||
jsonLd: $jsonLd,
|
jsonLd: $jsonLd,
|
||||||
|
siteName: (string) $settings->brand_name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the metadata for branded error pages.
|
||||||
|
*
|
||||||
|
* Error pages carry no canonical, are excluded from search indexes and use
|
||||||
|
* the page name suffixed with the brand name as their title.
|
||||||
|
*/
|
||||||
|
public static function forErrorPage(
|
||||||
|
SiteSetting $settings,
|
||||||
|
int $status = 404,
|
||||||
|
): self {
|
||||||
|
[$title, $description] = match ($status) {
|
||||||
|
500 => ['Algo deu errado', 'Não foi possível concluir o pedido. Tente novamente em instantes.'],
|
||||||
|
419 => ['Sessão expirada', 'Sua sessão expirou. Volte e tente enviar novamente.'],
|
||||||
|
429 => ['Muitas solicitações', 'Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.'],
|
||||||
|
503 => ['Em manutenção', 'Estamos realizando uma breve manutenção. Tente novamente em instantes.'],
|
||||||
|
default => ['Página não encontrada', 'A página que você procura não existe ou foi movida.'],
|
||||||
|
};
|
||||||
|
|
||||||
|
return new self(
|
||||||
|
title: trim($title).' - '.$settings->brand_name,
|
||||||
|
description: $description,
|
||||||
|
canonical: '',
|
||||||
|
robots: 'noindex, nofollow',
|
||||||
|
siteName: (string) $settings->brand_name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append the brand name to a page title when it is not already present.
|
||||||
|
*/
|
||||||
|
public static function withBrandSuffix(string $title, SiteSetting $settings): string
|
||||||
|
{
|
||||||
|
$brand = filled($settings->default_meta_title)
|
||||||
|
? (string) $settings->default_meta_title
|
||||||
|
: (string) $settings->brand_name;
|
||||||
|
|
||||||
|
if (str_contains($title, $brand)) {
|
||||||
|
return $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($title).' - '.$brand;
|
||||||
|
}
|
||||||
|
|
||||||
private static function defaultTitle(SiteSetting $settings): string
|
private static function defaultTitle(SiteSetting $settings): string
|
||||||
{
|
{
|
||||||
return filled($settings->default_meta_title)
|
return filled($settings->default_meta_title)
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
if ($settings && filled($settings->default_og_image_path)) {
|
if ($settings && filled($settings->default_og_image_path)) {
|
||||||
$paths[] = (string) $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) {
|
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
||||||
$paths[] = (string) $path;
|
$paths[] = (string) $path;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ use BackedEnum;
|
|||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
use Filament\Forms\Components\KeyValue;
|
use Filament\Forms\Components\KeyValue;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
@@ -135,6 +137,8 @@ class ManageSiteSettings extends Page
|
|||||||
->label('Nome da marca')
|
->label('Nome da marca')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
PublicImageUploadRules::fileUpload('logo_path', 'Logo da marca', 'content/logo'),
|
||||||
|
PublicImageUploadRules::altTextField('logo_alt', 'logo_path', 'Texto alternativo do logo'),
|
||||||
TextInput::make('hero_eyebrow')
|
TextInput::make('hero_eyebrow')
|
||||||
->label('Eyebrow do hero')
|
->label('Eyebrow do hero')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
@@ -147,14 +151,62 @@ class ManageSiteSettings extends Page
|
|||||||
->required()
|
->required()
|
||||||
->rows(3),
|
->rows(3),
|
||||||
TextInput::make('hero_cta_label')
|
TextInput::make('hero_cta_label')
|
||||||
->label('Texto do CTA')
|
->label('Texto do CTA principal')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('hero_secondary_cta_label')
|
||||||
|
->label('Texto do CTA secundário')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('hero_note')
|
||||||
|
->label('Nota do hero')
|
||||||
|
->rows(2),
|
||||||
Textarea::make('about_summary')
|
Textarea::make('about_summary')
|
||||||
->label('Resumo institucional')
|
->label('Resumo institucional')
|
||||||
->rows(3),
|
->rows(3),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
|
Section::make('Manifesto editorial')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('manifesto_title')
|
||||||
|
->label('Título do manifesto')
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('manifesto_lead')
|
||||||
|
->label('Lead do manifesto')
|
||||||
|
->rows(3),
|
||||||
|
Textarea::make('manifesto_body')
|
||||||
|
->label('Corpo do manifesto')
|
||||||
|
->rows(4),
|
||||||
|
]),
|
||||||
|
Section::make('Método')
|
||||||
|
->schema([
|
||||||
|
Textarea::make('method_intro')
|
||||||
|
->label('Introdução do método')
|
||||||
|
->rows(2),
|
||||||
|
Repeater::make('method_steps')
|
||||||
|
->label('Passos do método')
|
||||||
|
->schema([
|
||||||
|
TextInput::make('title')
|
||||||
|
->label('Título')
|
||||||
|
->required()
|
||||||
|
->maxLength(255),
|
||||||
|
Textarea::make('body')
|
||||||
|
->label('Descrição')
|
||||||
|
->required()
|
||||||
|
->rows(2),
|
||||||
|
])
|
||||||
|
->defaultItems(0)
|
||||||
|
->maxItems(4)
|
||||||
|
->reorderable()
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
|
Section::make('Princípios')
|
||||||
|
->schema([
|
||||||
|
TagsInput::make('principles')
|
||||||
|
->label('Princípios')
|
||||||
|
->placeholder('Adicionar princípio')
|
||||||
|
->helperText('Até quatro princípios editoriais.')
|
||||||
|
->columnSpanFull(),
|
||||||
|
]),
|
||||||
Section::make('Contato')
|
Section::make('Contato')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('email')
|
TextInput::make('email')
|
||||||
@@ -176,6 +228,12 @@ class ManageSiteSettings extends Page
|
|||||||
->addActionLabel('Adicionar rede'),
|
->addActionLabel('Adicionar rede'),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->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')
|
Section::make('SEO padrão')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('default_meta_title')
|
TextInput::make('default_meta_title')
|
||||||
|
|||||||
116
app/Http/Controllers/PublicSite/ContactController.php
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\PublicSite\ContactBriefingRequest;
|
||||||
|
use App\Mail\ContactBriefing;
|
||||||
|
use App\Mail\ContactBriefingConfirmation;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class ContactController extends Controller
|
||||||
|
{
|
||||||
|
private const string DUPLICATE_SESSION_KEY = 'contact_briefing_hash';
|
||||||
|
|
||||||
|
public function store(ContactBriefingRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (filled($request->input('empresa'))) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
unset($validated['privacidade']);
|
||||||
|
|
||||||
|
if ($this->isDuplicate($validated)) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->rememberSubmission($validated);
|
||||||
|
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
$fields = $this->buildFields($validated);
|
||||||
|
|
||||||
|
$this->dispatchEmails($settings, $validated['nome'], $validated['email'], $fields);
|
||||||
|
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
private function buildFields(array $validated): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Nome' => (string) $validated['nome'],
|
||||||
|
'E-mail' => (string) $validated['email'],
|
||||||
|
'Telefone/WhatsApp' => (string) $validated['telefone'],
|
||||||
|
'Tipo de evento' => (string) $validated['tipo_evento'],
|
||||||
|
'Data ou período desejado' => isset($validated['data_periodo']) ? (string) $validated['data_periodo'] : null,
|
||||||
|
'Cidade' => (string) $validated['cidade'],
|
||||||
|
'Número estimado de convidados' => isset($validated['convidados']) ? (string) $validated['convidados'] : null,
|
||||||
|
'Serviço de interesse' => isset($validated['servico_interesse']) ? (string) $validated['servico_interesse'] : null,
|
||||||
|
'Mensagem' => (string) $validated['mensagem'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
private function isDuplicate(array $validated): bool
|
||||||
|
{
|
||||||
|
$hash = $this->hash($validated);
|
||||||
|
|
||||||
|
return session()->get(self::DUPLICATE_SESSION_KEY) === $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
private function rememberSubmission(array $validated): void
|
||||||
|
{
|
||||||
|
session()->put(self::DUPLICATE_SESSION_KEY, $this->hash($validated));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $validated
|
||||||
|
*/
|
||||||
|
private function hash(array $validated): string
|
||||||
|
{
|
||||||
|
return hash('sha256', serialize($validated));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $fields
|
||||||
|
*/
|
||||||
|
private function dispatchEmails(SiteSetting $settings, string $name, string $email, array $fields): void
|
||||||
|
{
|
||||||
|
$attempt = function () use ($settings, $name, $email, $fields): void {
|
||||||
|
if (filled($settings->email)) {
|
||||||
|
Mail::to($settings->email)->send(new ContactBriefing($fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filled(config('mail.default'))) {
|
||||||
|
Mail::to($email)->send(new ContactBriefingConfirmation($name, (string) $settings->brand_name));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
$attempt();
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Log::error('Falha ao enviar briefing de contato', [
|
||||||
|
'exception' => $exception,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function success(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('contact')->with('status', 'briefing-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ final class PageController extends Controller
|
|||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('about'),
|
canonical: route('about'),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: 'Sobre',
|
title: PageMeta::withBrandSuffix('Sobre', $settings),
|
||||||
description: $settings->about_summary ?: ('Conheça a '.$settings->brand_name.'.'),
|
description: $settings->about_summary ?: ('Conheça a '.$settings->brand_name.'.'),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -35,7 +35,7 @@ final class PageController extends Controller
|
|||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('privacy'),
|
canonical: route('privacy'),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: 'Política de privacidade',
|
title: PageMeta::withBrandSuffix('Política de privacidade', $settings),
|
||||||
description: 'Política de privacidade da '.$settings->brand_name.'.',
|
description: 'Política de privacidade da '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
@@ -50,7 +50,7 @@ final class PageController extends Controller
|
|||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('contact'),
|
canonical: route('contact'),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: 'Contato',
|
title: PageMeta::withBrandSuffix('Contato', $settings),
|
||||||
description: 'Fale com a '.$settings->brand_name.'.',
|
description: 'Fale com a '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ final class PortfolioController extends Controller
|
|||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('portfolio.index'),
|
canonical: route('portfolio.index'),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: 'Portfólio',
|
title: PageMeta::withBrandSuffix('Portfólio', $settings),
|
||||||
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ final class ServiceController extends Controller
|
|||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('services.index'),
|
canonical: route('services.index'),
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: 'Serviços',
|
title: PageMeta::withBrandSuffix('Serviços', $settings),
|
||||||
description: 'Conheça os serviços de assessoria de eventos da '.$settings->brand_name.'.',
|
description: 'Conheça os serviços de assessoria de eventos da '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
57
app/Http/Requests/PublicSite/ContactBriefingRequest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\PublicSite;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class ContactBriefingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nome' => ['required', 'string', 'max:120'],
|
||||||
|
'email' => ['required', 'email', 'max:254'],
|
||||||
|
'telefone' => ['required', 'string', 'max:40'],
|
||||||
|
'tipo_evento' => ['required', 'string', 'max:80'],
|
||||||
|
'data_periodo' => ['nullable', 'string', 'max:80'],
|
||||||
|
'cidade' => ['required', 'string', 'max:80'],
|
||||||
|
'convidados' => ['nullable', 'integer', 'min:1', 'max:100000'],
|
||||||
|
'servico_interesse' => ['nullable', 'string', 'max:120'],
|
||||||
|
'mensagem' => ['required', 'string', 'max:3000'],
|
||||||
|
'privacidade' => ['accepted'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nome.required' => 'Informe seu nome completo.',
|
||||||
|
'nome.max' => 'O nome deve ter no máximo :max caracteres.',
|
||||||
|
'email.required' => 'Informe seu e-mail.',
|
||||||
|
'email.email' => 'Informe um e-mail válido.',
|
||||||
|
'email.max' => 'O e-mail deve ter no máximo :max caracteres.',
|
||||||
|
'telefone.required' => 'Informe um telefone ou WhatsApp.',
|
||||||
|
'telefone.max' => 'O telefone deve ter no máximo :max caracteres.',
|
||||||
|
'tipo_evento.required' => 'Selecione o tipo de evento.',
|
||||||
|
'tipo_evento.max' => 'O tipo de evento deve ter no máximo :max caracteres.',
|
||||||
|
'data_periodo.max' => 'A data ou período deve ter no máximo :max caracteres.',
|
||||||
|
'cidade.required' => 'Informe a cidade do evento.',
|
||||||
|
'cidade.max' => 'A cidade deve ter no máximo :max caracteres.',
|
||||||
|
'convidados.integer' => 'Informe um número de convidados válido.',
|
||||||
|
'convidados.min' => 'O número de convidados deve ser maior que zero.',
|
||||||
|
'convidados.max' => 'O número de convidados informado é inválido.',
|
||||||
|
'servico_interesse.max' => 'O serviço deve ter no máximo :max caracteres.',
|
||||||
|
'mensagem.required' => 'Conte brevemente o que você precisa.',
|
||||||
|
'mensagem.max' => 'A mensagem deve ter no máximo :max caracteres.',
|
||||||
|
'privacidade.accepted' => 'Você precisa aceitar a política de privacidade.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Mail/ContactBriefing.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
final class ContactBriefing extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $fields
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly array $fields,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Novo briefing de contato — Amare Assessoria',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
html: 'emails.contact-briefing',
|
||||||
|
text: 'emails.contact-briefing-text',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Mail/ContactBriefingConfirmation.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
final class ContactBriefingConfirmation extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $name,
|
||||||
|
public readonly string $brandName,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(
|
||||||
|
subject: 'Recebemos sua mensagem — '.$this->brandName,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(
|
||||||
|
html: 'emails.contact-briefing-confirmation',
|
||||||
|
text: 'emails.contact-briefing-confirmation-text',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,17 +11,35 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @property array<string, string|null> $social_links
|
* @property array<string, string|null> $social_links
|
||||||
|
* @property list<array{title?: string, body?: string}>|null $method_steps
|
||||||
|
* @property list<string>|null $principles
|
||||||
* @property bool $analytics_enabled
|
* @property bool $analytics_enabled
|
||||||
* @property string|null $default_og_image_path
|
* @property string|null $default_og_image_path
|
||||||
* @property string|null $default_og_image_alt
|
* @property string|null $default_og_image_alt
|
||||||
|
* @property string|null $about_image_path
|
||||||
|
* @property string|null $about_image_alt
|
||||||
|
* @property string|null $logo_path
|
||||||
|
* @property string|null $logo_alt
|
||||||
*/
|
*/
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'brand_name',
|
'brand_name',
|
||||||
|
'logo_path',
|
||||||
|
'logo_alt',
|
||||||
'hero_eyebrow',
|
'hero_eyebrow',
|
||||||
'hero_title',
|
'hero_title',
|
||||||
'hero_subtitle',
|
'hero_subtitle',
|
||||||
'hero_cta_label',
|
'hero_cta_label',
|
||||||
|
'hero_secondary_cta_label',
|
||||||
|
'hero_note',
|
||||||
'about_summary',
|
'about_summary',
|
||||||
|
'about_image_path',
|
||||||
|
'about_image_alt',
|
||||||
|
'manifesto_title',
|
||||||
|
'manifesto_lead',
|
||||||
|
'manifesto_body',
|
||||||
|
'method_intro',
|
||||||
|
'method_steps',
|
||||||
|
'principles',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
'city',
|
'city',
|
||||||
@@ -40,21 +58,67 @@ class SiteSetting extends Model
|
|||||||
{
|
{
|
||||||
return static::query()->firstOrCreate([], [
|
return static::query()->firstOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique em Fortaleza.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique em São Paulo - SP.',
|
||||||
'city' => 'Fortaleza, CE',
|
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
||||||
|
'manifesto_lead' => 'Um evento memorável não nasce apenas de uma boa estética. Ele depende de decisões bem conduzidas, fornecedores alinhados e atenção constante ao que realmente importa.',
|
||||||
|
'manifesto_body' => 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => self::defaultMethodSteps(),
|
||||||
|
'principles' => self::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [],
|
'social_links' => [],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{title: string, body: string}>
|
||||||
|
*/
|
||||||
|
public static function defaultMethodSteps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Escuta',
|
||||||
|
'body' => 'Entendimento do contexto, das prioridades, do público e do que o evento precisa comunicar.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Direção',
|
||||||
|
'body' => 'Definição de escopo, próximos passos, responsabilidades e critérios para orientar decisões.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Produção',
|
||||||
|
'body' => 'Coordenação de cronograma, fornecedores, detalhes, alinhamentos e contingências.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Execução',
|
||||||
|
'body' => 'Presença atenta no evento para que o planejado aconteça com ritmo, cuidado e segurança.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
public static function defaultPrinciples(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'Personalização sem complicação desnecessária',
|
||||||
|
'Comunicação clara e decisões bem orientadas',
|
||||||
|
'Atenção à experiência de clientes e convidados',
|
||||||
|
'Execução responsável do início ao fim',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, string|class-string>
|
* @return array<string, string|class-string>
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +126,8 @@ class SiteSetting extends Model
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'social_links' => 'array',
|
'social_links' => 'array',
|
||||||
|
'method_steps' => 'array',
|
||||||
|
'principles' => 'array',
|
||||||
'analytics_enabled' => 'boolean',
|
'analytics_enabled' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ namespace App\Providers;
|
|||||||
use App\Application\Data\PageMeta;
|
use App\Application\Data\PageMeta;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Carbon\CarbonImmutable;
|
use Carbon\CarbonImmutable;
|
||||||
|
use Illuminate\Cache\RateLimiting\Limit;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\View\View as ViewInstance;
|
use Illuminate\View\View as ViewInstance;
|
||||||
@@ -26,7 +29,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
$this->configureLivewireTemporaryUploads();
|
||||||
$this->freezeClockWhenConfigured();
|
$this->freezeClockWhenConfigured();
|
||||||
|
$this->configureRateLimiters();
|
||||||
|
|
||||||
View::composer('layouts.public', function (ViewInstance $view): void {
|
View::composer('layouts.public', function (ViewInstance $view): void {
|
||||||
$settings = $view->offsetExists('siteSettings')
|
$settings = $view->offsetExists('siteSettings')
|
||||||
@@ -46,6 +51,29 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep Livewire/Filament temp uploads on the local disk.
|
||||||
|
*
|
||||||
|
* When FILESYSTEM_DISK=r2, Livewire would otherwise use the S3 driver and
|
||||||
|
* browser-PUT straight to R2 (CORS). Final media still uses the r2 disk via
|
||||||
|
* PublicImageUploadRules. Explicit LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK wins.
|
||||||
|
*/
|
||||||
|
private function configureLivewireTemporaryUploads(): void
|
||||||
|
{
|
||||||
|
if (filled(config('livewire.temporary_file_upload.disk'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config(['livewire.temporary_file_upload.disk' => 'local']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function configureRateLimiters(): void
|
||||||
|
{
|
||||||
|
RateLimiter::for('contact-briefing', function (Request $request): Limit {
|
||||||
|
return Limit::perMinute(5)->by($request->ip().'|contact-briefing');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private function freezeClockWhenConfigured(): void
|
private function freezeClockWhenConfigured(): void
|
||||||
{
|
{
|
||||||
if ($this->app->environment('production')) {
|
if ($this->app->environment('production')) {
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
//
|
// Trust Traefik/Dokploy (and local reverse proxies) for X-Forwarded-* headers.
|
||||||
|
$middleware->trustProxies(at: '*');
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions): void {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|||||||
12
composer.lock
generated
@@ -2802,16 +2802,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "league/commonmark",
|
"name": "league/commonmark",
|
||||||
"version": "2.8.3",
|
"version": "2.9.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/thephpleague/commonmark.git",
|
"url": "https://github.com/thephpleague/commonmark.git",
|
||||||
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7"
|
"reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7",
|
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
|
||||||
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7",
|
"reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2848,7 +2848,7 @@
|
|||||||
"type": "library",
|
"type": "library",
|
||||||
"extra": {
|
"extra": {
|
||||||
"branch-alias": {
|
"branch-alias": {
|
||||||
"dev-main": "2.9-dev"
|
"dev-main": "2.10-dev"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@@ -2905,7 +2905,7 @@
|
|||||||
"type": "tidelift"
|
"type": "tidelift"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2026-07-12T15:29:16+00:00"
|
"time": "2026-08-03T13:42:31+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "league/config",
|
"name": "league/config",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class PortfolioCaseFactory extends Factory
|
|||||||
'slug' => str($title)->slug()->toString(),
|
'slug' => str($title)->slug()->toString(),
|
||||||
'summary' => fake()->sentence(),
|
'summary' => fake()->sentence(),
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => fake()->company(),
|
'venue' => fake()->company(),
|
||||||
'event_date' => fake()->date(),
|
'event_date' => fake()->date(),
|
||||||
'challenge' => fake()->paragraph(),
|
'challenge' => fake()->paragraph(),
|
||||||
|
|||||||
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,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->string('logo_path')->nullable()->after('brand_name');
|
||||||
|
$table->string('logo_alt')->nullable()->after('logo_path');
|
||||||
|
$table->string('hero_secondary_cta_label')->nullable()->after('hero_cta_label');
|
||||||
|
$table->text('hero_note')->nullable()->after('hero_secondary_cta_label');
|
||||||
|
$table->string('manifesto_title')->nullable()->after('about_summary');
|
||||||
|
$table->text('manifesto_lead')->nullable()->after('manifesto_title');
|
||||||
|
$table->text('manifesto_body')->nullable()->after('manifesto_lead');
|
||||||
|
$table->text('method_intro')->nullable()->after('manifesto_body');
|
||||||
|
$table->jsonb('method_steps')->nullable()->after('method_intro');
|
||||||
|
$table->jsonb('principles')->nullable()->after('method_steps');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn([
|
||||||
|
'logo_path',
|
||||||
|
'logo_alt',
|
||||||
|
'hero_secondary_cta_label',
|
||||||
|
'hero_note',
|
||||||
|
'manifesto_title',
|
||||||
|
'manifesto_lead',
|
||||||
|
'manifesto_body',
|
||||||
|
'method_intro',
|
||||||
|
'method_steps',
|
||||||
|
'principles',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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,7 +8,7 @@ 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 App\Support\PublicImageUploadRules;
|
||||||
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,26 +23,36 @@ 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
|
||||||
{
|
{
|
||||||
SiteSetting::query()->updateOrCreate([], [
|
SiteSetting::query()->updateOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em Fortaleza.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em São Paulo.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'city' => 'Fortaleza, CE',
|
'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.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [
|
'social_links' => [
|
||||||
'instagram' => 'https://instagram.com/amare',
|
'instagram' => 'https://instagram.com/amare',
|
||||||
],
|
],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'content/og/og-default.jpg'),
|
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'content/og/og-default.jpg'),
|
||||||
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
@@ -84,7 +94,7 @@ class ContentSeeder extends Seeder
|
|||||||
['slug' => $service['slug']],
|
['slug' => $service['slug']],
|
||||||
[
|
[
|
||||||
...$service,
|
...$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'],
|
'cover_image_alt' => 'Capa do serviço '.$service['title'],
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
],
|
],
|
||||||
@@ -98,9 +108,9 @@ class ContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Casamento Ana e Lucas',
|
'title' => 'Casamento Ana e Lucas',
|
||||||
'slug' => 'casamento-ana-lucas',
|
'slug' => 'casamento-ana-lucas',
|
||||||
'summary' => 'Cerimônia ao ar livre em Fortaleza.',
|
'summary' => 'Cerimônia ao ar livre em São Paulo.',
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Espaço Jardim Atlântico',
|
'venue' => 'Espaço Jardim Atlântico',
|
||||||
'event_date' => '2025-11-20',
|
'event_date' => '2025-11-20',
|
||||||
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
||||||
@@ -113,7 +123,7 @@ class ContentSeeder extends Seeder
|
|||||||
'slug' => 'lancamento-verano',
|
'slug' => 'lancamento-verano',
|
||||||
'summary' => 'Evento corporativo de lançamento de coleção.',
|
'summary' => 'Evento corporativo de lançamento de coleção.',
|
||||||
'event_type' => 'Corporativo',
|
'event_type' => 'Corporativo',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Centro de Convenções',
|
'venue' => 'Centro de Convenções',
|
||||||
'event_date' => '2025-09-10',
|
'event_date' => '2025-09-10',
|
||||||
'challenge' => 'Ativar marca em ambiente multiestação.',
|
'challenge' => 'Ativar marca em ambiente multiestação.',
|
||||||
@@ -124,10 +134,10 @@ class ContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Mini wedding Marina',
|
'title' => 'Mini wedding Marina',
|
||||||
'slug' => 'mini-wedding-marina',
|
'slug' => 'mini-wedding-marina',
|
||||||
'summary' => 'Celebração intimista à beira-mar.',
|
'summary' => 'Celebração intimista em São Paulo.',
|
||||||
'event_type' => 'Mini wedding',
|
'event_type' => 'Mini wedding',
|
||||||
'city' => 'Caucaia',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Pousada da Praia',
|
'venue' => 'Espaço intimista',
|
||||||
'event_date' => '2025-06-02',
|
'event_date' => '2025-06-02',
|
||||||
'challenge' => 'Clima e logística em área externa.',
|
'challenge' => 'Clima e logística em área externa.',
|
||||||
'solution' => 'Plano B estruturado e fornecedores locais alinhados.',
|
'solution' => 'Plano B estruturado e fornecedores locais alinhados.',
|
||||||
@@ -141,7 +151,7 @@ class ContentSeeder extends Seeder
|
|||||||
['slug' => $caseData['slug']],
|
['slug' => $caseData['slug']],
|
||||||
[
|
[
|
||||||
...$caseData,
|
...$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'],
|
'cover_image_alt' => 'Capa do caso '.$caseData['title'],
|
||||||
'is_featured' => true,
|
'is_featured' => true,
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
@@ -153,7 +163,7 @@ class ContentSeeder extends Seeder
|
|||||||
foreach ([1, 2] as $index) {
|
foreach ([1, 2] as $index) {
|
||||||
PortfolioImage::query()->create([
|
PortfolioImage::query()->create([
|
||||||
'portfolio_case_id' => $case->id,
|
'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,
|
'alt_text' => 'Galeria '.$caseData['title'].' '.$index,
|
||||||
'caption' => $index === 1 ? 'Detalhe da decoração' : null,
|
'caption' => $index === 1 ? 'Detalhe da decoração' : null,
|
||||||
'sort_order' => $index,
|
'sort_order' => $index,
|
||||||
@@ -162,52 +172,12 @@ class ContentSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function seedTestimonials(): void
|
|
||||||
{
|
|
||||||
$testimonials = [
|
|
||||||
[
|
|
||||||
'quote' => 'A Amare transformou nosso casamento em uma experiência inesquecível.',
|
|
||||||
'author_name' => 'Ana Souza',
|
|
||||||
'context' => 'Noiva',
|
|
||||||
'sort_order' => 1,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => 'Profissionalismo do início ao fim no lançamento da nossa coleção.',
|
|
||||||
'author_name' => 'Marcos Lima',
|
|
||||||
'context' => 'Diretor de marketing',
|
|
||||||
'sort_order' => 2,
|
|
||||||
'is_featured' => true,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'quote' => 'Cuidaram de cada detalhe com sensibilidade e precisão.',
|
|
||||||
'author_name' => 'Marina Costa',
|
|
||||||
'context' => 'Anfitriã',
|
|
||||||
'sort_order' => 3,
|
|
||||||
'is_featured' => false,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($testimonials as $testimonial) {
|
|
||||||
Testimonial::query()->updateOrCreate(
|
|
||||||
[
|
|
||||||
'author_name' => $testimonial['author_name'],
|
|
||||||
'quote' => $testimonial['quote'],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
...$testimonial,
|
|
||||||
'photo_path' => $this->copyFixture('testimonial.jpg', 'content/testimonials/'.str($testimonial['author_name'])->slug().'.jpg'),
|
|
||||||
'photo_alt' => 'Foto de '.$testimonial['author_name'],
|
|
||||||
'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('database/fixtures/images/'.$fixtureName);
|
||||||
Storage::disk('public')->put($destination, File::get($source));
|
$disk = PublicImageUploadRules::disk();
|
||||||
|
|
||||||
|
Storage::disk($disk)->put($destination, File::get($source), 'public');
|
||||||
|
|
||||||
return $destination;
|
return $destination;
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ use App\Models\SiteSetting;
|
|||||||
use App\Models\Testimonial;
|
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\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,20 +36,30 @@ class VisualContentSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
SiteSetting::query()->updateOrCreate([], [
|
SiteSetting::query()->updateOrCreate([], [
|
||||||
'brand_name' => 'Amare Assessoria',
|
'brand_name' => 'Amare Assessoria',
|
||||||
'hero_eyebrow' => 'Assessoria de eventos',
|
'hero_eyebrow' => 'Assessoria e produção de eventos · São Paulo',
|
||||||
'hero_title' => 'Celebrações com propósito',
|
'hero_title' => 'Celebrações com propósito',
|
||||||
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em Fortaleza.',
|
'hero_subtitle' => 'Planejamento completo para casamentos e eventos corporativos em São Paulo.',
|
||||||
'hero_cta_label' => 'Solicitar orçamento',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis.',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'email' => 'contato@amare.local',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
'phone' => '(85) 99999-9999',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'city' => 'Fortaleza, CE',
|
'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.',
|
||||||
|
'method_intro' => 'Clareza em cada etapa. Tranquilidade durante todo o processo.',
|
||||||
|
'method_steps' => SiteSetting::defaultMethodSteps(),
|
||||||
|
'principles' => SiteSetting::defaultPrinciples(),
|
||||||
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
|
'phone' => '(11) 99999-9999',
|
||||||
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [
|
'social_links' => [
|
||||||
'instagram' => 'https://instagram.com/amare',
|
'instagram' => 'https://instagram.com/amare',
|
||||||
],
|
],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos.',
|
'default_meta_description' => 'Assessoria premium para casamentos e eventos corporativos em São Paulo.',
|
||||||
'default_og_image_path' => $this->copyFixture('og-default.jpg', 'visual/og/og-default.jpg'),
|
'default_og_image_path' => $this->writeSolidJpeg('visual/og/og-default.jpg', 1600, 1067, [85, 107, 47]),
|
||||||
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
'default_og_image_alt' => 'Identidade visual da Amare Assessoria de Eventos',
|
||||||
'analytics_enabled' => false,
|
'analytics_enabled' => false,
|
||||||
'analytics_script' => null,
|
'analytics_script' => null,
|
||||||
@@ -81,7 +90,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
['slug' => $service['slug']],
|
['slug' => $service['slug']],
|
||||||
[
|
[
|
||||||
...$service,
|
...$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'],
|
'cover_image_alt' => 'Capa do serviço '.$service['title'],
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
],
|
],
|
||||||
@@ -95,9 +104,9 @@ class VisualContentSeeder extends Seeder
|
|||||||
[
|
[
|
||||||
'title' => 'Casamento Ana e Lucas',
|
'title' => 'Casamento Ana e Lucas',
|
||||||
'slug' => 'casamento-ana-lucas',
|
'slug' => 'casamento-ana-lucas',
|
||||||
'summary' => 'Cerimônia ao ar livre em Fortaleza.',
|
'summary' => 'Cerimônia ao ar livre em São Paulo.',
|
||||||
'event_type' => 'Casamento',
|
'event_type' => 'Casamento',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Espaço Jardim Atlântico',
|
'venue' => 'Espaço Jardim Atlântico',
|
||||||
'event_date' => '2025-11-20',
|
'event_date' => '2025-11-20',
|
||||||
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
'challenge' => 'Integrar cerimônia e recepção em áreas distintas.',
|
||||||
@@ -110,7 +119,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
'slug' => 'lancamento-verano',
|
'slug' => 'lancamento-verano',
|
||||||
'summary' => 'Evento corporativo de lançamento de coleção.',
|
'summary' => 'Evento corporativo de lançamento de coleção.',
|
||||||
'event_type' => 'Corporativo',
|
'event_type' => 'Corporativo',
|
||||||
'city' => 'Fortaleza',
|
'city' => 'São Paulo',
|
||||||
'venue' => 'Centro de Convenções',
|
'venue' => 'Centro de Convenções',
|
||||||
'event_date' => '2025-09-10',
|
'event_date' => '2025-09-10',
|
||||||
'challenge' => 'Ativar marca em ambiente multiestação.',
|
'challenge' => 'Ativar marca em ambiente multiestação.',
|
||||||
@@ -125,7 +134,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
['slug' => $caseData['slug']],
|
['slug' => $caseData['slug']],
|
||||||
[
|
[
|
||||||
...$caseData,
|
...$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'],
|
'cover_image_alt' => 'Capa do caso '.$caseData['title'],
|
||||||
'is_featured' => true,
|
'is_featured' => true,
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
@@ -136,7 +145,7 @@ class VisualContentSeeder extends Seeder
|
|||||||
|
|
||||||
PortfolioImage::query()->create([
|
PortfolioImage::query()->create([
|
||||||
'portfolio_case_id' => $case->id,
|
'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',
|
'alt_text' => 'Galeria '.$caseData['title'].' 1',
|
||||||
'caption' => 'Detalhe da decoração',
|
'caption' => 'Detalhe da decoração',
|
||||||
'sort_order' => 1,
|
'sort_order' => 1,
|
||||||
@@ -146,26 +155,53 @@ class VisualContentSeeder extends Seeder
|
|||||||
|
|
||||||
private function seedTestimonials(): void
|
private function seedTestimonials(): void
|
||||||
{
|
{
|
||||||
Testimonial::query()->updateOrCreate(
|
Testimonial::query()->whereNotIn('author_name', [
|
||||||
|
'Jeniffer e Maick',
|
||||||
|
'Quesia e Jhonata',
|
||||||
|
])->delete();
|
||||||
|
|
||||||
|
foreach ([
|
||||||
[
|
[
|
||||||
'author_name' => 'Ana Souza',
|
'quote' => "Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.\n\nEra nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.\n\nQue você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!!\n\nMuito obrigada!",
|
||||||
'quote' => 'A Amare transformou nosso casamento em uma experiência inesquecível.',
|
'author_name' => 'Jeniffer e Maick',
|
||||||
|
'context' => 'Casamento · 06/12/2025',
|
||||||
|
'sort_order' => 1,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'context' => 'Noiva',
|
'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 !",
|
||||||
'sort_order' => 1,
|
'author_name' => 'Quesia e Jhonata',
|
||||||
|
'context' => 'Casamento · 21/12/2025',
|
||||||
|
'sort_order' => 2,
|
||||||
|
],
|
||||||
|
] as $testimonial) {
|
||||||
|
Testimonial::query()->updateOrCreate(
|
||||||
|
['author_name' => $testimonial['author_name']],
|
||||||
|
[
|
||||||
|
...$testimonial,
|
||||||
'is_featured' => true,
|
'is_featured' => true,
|
||||||
'photo_path' => $this->copyFixture('testimonial.jpg', 'visual/testimonials/ana-souza.jpg'),
|
'photo_path' => null,
|
||||||
'photo_alt' => 'Foto de Ana Souza',
|
'photo_alt' => null,
|
||||||
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
'published_at' => Carbon::parse(self::SEED_TIMESTAMP),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
$image = imagecreatetruecolor($width, $height);
|
||||||
Storage::disk('public')->put($destination, File::get($source));
|
$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;
|
return $destination;
|
||||||
}
|
}
|
||||||
|
|||||||
30
depoimentos.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
Mi, quero agradecer você e a sua equipe por todo empenho, atenção, vocês são abençoadas.
|
||||||
|
Era nítida sua preocupação em garantir que todos os detalhes planejados desta comemoração, fossem atendidos.
|
||||||
|
Que você possa transformar o grande dia das noivinhas sempre com essa sua leveza!!! ❤️
|
||||||
|
Muito obrigada!
|
||||||
|
Jeniffer e Maick
|
||||||
|
Casamento - 06/12/2025
|
||||||
|
----
|
||||||
|
Mi, eu não tenho palavras pra agradecer você e tudo que você fez por mim e por nós na realização desse sonho. Eu tô ainda extasiada com tudo que aconteceu hoje; mas tenho certeza que sem a sua ajuda, muita coisa não aconteceria.
|
||||||
|
Obrigada por tudo !
|
||||||
|
Quesia e Jhonata
|
||||||
|
casamento - 21/12/2025
|
||||||
|
----
|
||||||
|
@_amareassessoria
|
||||||
|
Que equipe!! Que equipe maravilhosa!! Obrigado pelo empenho de fazer tudo como eu queria!! Obrigado por se esforçar tanto e vir de tão longe pra realizar meu sonho!! Incríveis!! 😍😍
|
||||||
|
Milena e Weslley
|
||||||
|
casamento - 13/02/2026
|
||||||
|
---
|
||||||
|
Gostaríamos de agradecer por todo o acompanhamento e dedicação durante a realização do nosso casamento. Foi um dia muito especial e inesquecível para nós.
|
||||||
|
Desde o início, conseguimos conduzir tudo aquilo que estávamos planejando, dentro dos horários que estipulamos, o que foi ótimo, e no grande dia sua equipe nos recebeu e tratou com muito carinho, atenção e cuidado, o que fez toda a diferença para vivermos esse momento com mais tranquilidade.
|
||||||
|
Também adoramos as sugestões e ideias para as fotos, que deixaram os registros ainda mais bonitos e espontâneos, porque não iríamos lembrar de quais poses fazer na hora.
|
||||||
|
Obrigada por fazer parte de um momento tão importante das nossas vidas. Desejamos muito sucesso e que muitos outros casais possam viver dias especiais através do trabalho da AMARE.
|
||||||
|
Raquel e Pedro
|
||||||
|
casamento - 09/05/2026
|
||||||
|
----
|
||||||
|
Miiii, meu amor… você e sua equipe foram impecáveis.
|
||||||
|
Superou todas as nossas expectativas. Somos eternamente gratos por fazer nosso dia acontecer muito melhor do que imaginávamos.
|
||||||
|
Sempre muito atenciosa e paciente.
|
||||||
|
Adoramos te conhecer e estamos muito felizes em termos escolhido você para assessorar nosso dia. ♥️🙏🏻
|
||||||
|
Victoria e Pedro
|
||||||
|
casamento -24/06/2026
|
||||||
73
docker-compose.deploy.yml
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Shared Compose for Dokploy staging and production.
|
||||||
|
# Both stacks use the same file with different env:
|
||||||
|
# APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||||
|
# IMAGE_TAG=staging|production|<git-sha>
|
||||||
|
# PostgreSQL is a separate Dokploy database service (not defined here).
|
||||||
|
# Traefik/Dokploy domains should target service `web` port 8000.
|
||||||
|
# App services join dokploy-network so they can resolve Dokploy-managed
|
||||||
|
# Postgres internal hosts (e.g. amare-stg-pez43e).
|
||||||
|
|
||||||
|
services:
|
||||||
|
migrate:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: "no"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
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
|
||||||
|
|
||||||
|
web:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/up"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
start_period: 40s
|
||||||
|
retries: 3
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
queue:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command: ["php", "artisan", "queue:work", "--sleep=2", "--tries=3", "--max-time=3600"]
|
||||||
|
stop_grace_period: 60s
|
||||||
|
stop_signal: SIGTERM
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
image: ${APP_IMAGE}:${IMAGE_TAG}
|
||||||
|
pull_policy: always
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
migrate:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command: ["php", "artisan", "schedule:work"]
|
||||||
|
stop_grace_period: 30s
|
||||||
|
stop_signal: SIGTERM
|
||||||
|
networks:
|
||||||
|
- dokploy-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dokploy-network:
|
||||||
|
external: true
|
||||||
@@ -7,5 +7,8 @@ Mesma imagem, comandos distintos:
|
|||||||
| web | `frankenphp run --config /etc/caddy/Caddyfile` |
|
| web | `frankenphp run --config /etc/caddy/Caddyfile` |
|
||||||
| queue | `php artisan queue:work --sleep=2 --tries=3` |
|
| queue | `php artisan queue:work --sleep=2 --tries=3` |
|
||||||
| scheduler | `php artisan schedule:work` |
|
| scheduler | `php artisan schedule:work` |
|
||||||
|
| migrate | `php artisan migrate --force` (one-shot no Compose de deploy) |
|
||||||
|
|
||||||
FrankenPHP em **modo regular** (ADR-006). Worker mode proibido no MVP.
|
FrankenPHP em **modo regular** (ADR-006). Worker mode proibido no MVP.
|
||||||
|
|
||||||
|
Deploy Dokploy (staging/produção): ver [`docker-compose.deploy.yml`](../docker-compose.deploy.yml) e [docs/deployment/dokploy.md](../docs/deployment/dokploy.md).
|
||||||
|
|||||||
51
docs/agents/domain.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Domain Docs
|
||||||
|
|
||||||
|
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||||
|
|
||||||
|
## Before exploring, read these
|
||||||
|
|
||||||
|
- **`CONTEXT.md`** at the repo root, or
|
||||||
|
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
|
||||||
|
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
|
||||||
|
|
||||||
|
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
Single-context repo (most repos):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── docs/adr/
|
||||||
|
│ ├── 0001-event-sourced-orders.md
|
||||||
|
│ └── 0002-postgres-for-write-model.md
|
||||||
|
└── src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT-MAP.md
|
||||||
|
├── docs/adr/ ← system-wide decisions
|
||||||
|
└── src/
|
||||||
|
├── ordering/
|
||||||
|
│ ├── CONTEXT.md
|
||||||
|
│ └── docs/adr/ ← context-specific decisions
|
||||||
|
└── billing/
|
||||||
|
├── CONTEXT.md
|
||||||
|
└── docs/adr/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
|
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
|
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
|
## Flag ADR conflicts
|
||||||
|
|
||||||
|
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||||
26
docs/agents/issue-tracker.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Issue tracker: Linear
|
||||||
|
|
||||||
|
Issues and specs for this repo live in Linear. Use the Linear MCP tools for all operations.
|
||||||
|
|
||||||
|
- **Workspace**: maneco-workspace (https://linear.app/maneco-workspace)
|
||||||
|
- **Team**: Maneco-workspace
|
||||||
|
- **Issue statuses**: Backlog, Todo, In Progress, Done, Canceled, Duplicate
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Create an issue**: `save_issue` with `title`, `team` (`Maneco-workspace`), and markdown `description`. Assign via `assignee` ("me" for the current user).
|
||||||
|
- **Read an issue**: `get_issue` by identifier (e.g. `LIN-123`).
|
||||||
|
- **List issues**: `list_issues` filtered by `team`, `assignee`, `state`, or `project`.
|
||||||
|
- **Comment on an issue**: `save_comment` with `issueId` and markdown `body`.
|
||||||
|
- **Apply / remove labels**: `save_issue` with `labels` (replaces the full set).
|
||||||
|
- **Move workflow state**: `save_issue` with `state` (e.g. `Todo`, `In Progress`, `Done`, `Canceled`).
|
||||||
|
- **Link work**: `save_issue` with `project`, `cycle`, `parentId`, `blocks` / `blockedBy` (relations are append-only).
|
||||||
|
- **Cross-link to a PR**: attach the PR URL via `save_issue` `links`.
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a Linear issue with `save_issue` on team `Maneco-workspace`.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
Run `get_issue` on the identifier and read the description, state, labels, and assignee.
|
||||||
15
docs/agents/triage-labels.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Triage Labels
|
||||||
|
|
||||||
|
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
|
||||||
|
|
||||||
|
| Label in mattpocock/skills | Label in our tracker | Meaning |
|
||||||
|
| -------------------------- | -------------------- | ---------------------------------------- |
|
||||||
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
|
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||||
|
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
|
| `wontfix` | `wontfix` | Will not be actioned |
|
||||||
|
|
||||||
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
|
||||||
|
|
||||||
|
Edit the right-hand column to match whatever vocabulary you actually use.
|
||||||
300
docs/deployment/dokploy.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# Deploy Dokploy (staging → production)
|
||||||
|
|
||||||
|
Runbook for operating Amare on a VPS with Dokploy connected to GitHub, publishing immutable images to GHCR.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
CI (main) → build FrankenPHP image → GHCR :<sha> + :staging
|
||||||
|
→ Dokploy staging compose.deploy
|
||||||
|
→ smoke /up / /admin/login
|
||||||
|
|
||||||
|
Promote (manual) → retag same digest as :production (no rebuild)
|
||||||
|
→ Dokploy production compose.deploy
|
||||||
|
→ smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
| Piece | Detail |
|
||||||
|
|---|---|
|
||||||
|
| Compose file | [`docker-compose.deploy.yml`](../../docker-compose.deploy.yml) |
|
||||||
|
| Processes | `migrate` (one-shot) → `web` / `queue` / `scheduler` |
|
||||||
|
| Image | `ghcr.io/<owner>/<repo>:<sha>` (+ aliases `:staging`, `:production`) |
|
||||||
|
| Database | Dokploy PostgreSQL **per environment** (not in the app image) |
|
||||||
|
| Media | Cloudflare R2 (`FILESYSTEM_DISK=r2`), separate buckets per environment |
|
||||||
|
| Mail | Resend (`MAIL_MAILER=resend`) |
|
||||||
|
| Proxy | Dokploy Traefik → service `web` port `8000` |
|
||||||
|
|
||||||
|
## Prerequisites (manual)
|
||||||
|
|
||||||
|
1. Dokploy installed on the VPS; GitHub provider connected.
|
||||||
|
2. GHCR registry in Dokploy (`ghcr.io`) with a PAT that can **read** packages (`read:packages`). Prefer a dedicated bot/token; do not store write tokens on the VPS.
|
||||||
|
3. Two PostgreSQL services in Dokploy (staging + production), private (no public port).
|
||||||
|
4. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
|
||||||
|
5. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
|
||||||
|
|
||||||
|
## Create Compose stacks
|
||||||
|
|
||||||
|
Create **two** Dokploy Compose services (same repo, same compose path):
|
||||||
|
|
||||||
|
| Stack | Compose path | `IMAGE_TAG` | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| staging | `docker-compose.deploy.yml` | `staging` | Auto-deployed after CI on `main` |
|
||||||
|
| production | `docker-compose.deploy.yml` | `production` | Manual promotion only |
|
||||||
|
|
||||||
|
Dokploy Environment for each stack must set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_IMAGE=ghcr.io/<owner>/<repo>
|
||||||
|
IMAGE_TAG=staging # or production
|
||||||
|
```
|
||||||
|
|
||||||
|
Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish PostgreSQL or host ports for app processes.
|
||||||
|
|
||||||
|
Compose services must join the external Docker network `dokploy-network` (declared in `docker-compose.deploy.yml`) so they can resolve the Dokploy-managed Postgres internal host (e.g. `amare-stg-pez43e`). Set `DB_HOST` to that **Internal Host** from the Dokploy database UI — not a public hostname.
|
||||||
|
|
||||||
|
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
|
||||||
|
|
||||||
|
## Required Laravel env (Dokploy only)
|
||||||
|
|
||||||
|
Set these in Dokploy Environment UI (written to `.env` next to the compose file). **Never** put them in GitHub Actions secrets or image layers.
|
||||||
|
|
||||||
|
```env
|
||||||
|
APP_NAME=Amare
|
||||||
|
APP_ENV=staging # or production
|
||||||
|
APP_KEY=base64:... # unique per environment — generate with php artisan key:generate --show
|
||||||
|
APP_DEBUG=false
|
||||||
|
APP_URL=https://staging.example.com
|
||||||
|
|
||||||
|
APP_LOCALE=pt_BR
|
||||||
|
APP_FALLBACK_LOCALE=pt_BR
|
||||||
|
APP_TIMEZONE=America/Fortaleza
|
||||||
|
|
||||||
|
DB_CONNECTION=pgsql
|
||||||
|
DB_HOST=<dokploy-postgres-internal-host> # Internal Host from Dokploy UI (requires dokploy-network)
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=amare_staging
|
||||||
|
DB_USERNAME=...
|
||||||
|
DB_PASSWORD=...
|
||||||
|
|
||||||
|
SESSION_DRIVER=database
|
||||||
|
SESSION_SECURE_COOKIE=true
|
||||||
|
SESSION_HTTP_ONLY=true
|
||||||
|
SESSION_SAME_SITE=lax
|
||||||
|
CACHE_STORE=database
|
||||||
|
QUEUE_CONNECTION=database
|
||||||
|
|
||||||
|
FILESYSTEM_DISK=r2
|
||||||
|
R2_ACCESS_KEY_ID=...
|
||||||
|
R2_SECRET_ACCESS_KEY=...
|
||||||
|
R2_BUCKET=...
|
||||||
|
R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
|
||||||
|
R2_URL=https://media-staging.example.com
|
||||||
|
|
||||||
|
MAIL_MAILER=resend
|
||||||
|
RESEND_API_KEY=...
|
||||||
|
MAIL_FROM_ADDRESS=noreply@example.com
|
||||||
|
MAIL_FROM_NAME=Amare
|
||||||
|
|
||||||
|
LOG_LEVEL=warning
|
||||||
|
```
|
||||||
|
|
||||||
|
Trusted proxies are configured in `bootstrap/app.php` so Traefik `X-Forwarded-*` headers work for HTTPS cookies and URLs.
|
||||||
|
|
||||||
|
Livewire temporary uploads default to the **local** disk in `AppServiceProvider` (even when `FILESYSTEM_DISK=r2`), so Filament does not browser-PUT to R2. Final media still lands on R2 via `PublicImageUploadRules`. Optional override: `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK` in Dokploy `.env` (`env_file` accepts any key — does not need a compose `environment:` entry).
|
||||||
|
|
||||||
|
### R2 CORS (public/media reads from JS)
|
||||||
|
|
||||||
|
Upload path does not need R2 CORS with the local temp-disk default. Still useful if the browser fetches R2 URLs cross-origin from JS. Origin must match `APP_URL` exactly; include `AllowedHeaders`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"AllowedOrigins": ["https://hellomanoel.com"],
|
||||||
|
"AllowedMethods": ["GET", "PUT", "POST", "HEAD"],
|
||||||
|
"AllowedHeaders": ["*"],
|
||||||
|
"ExposeHeaders": ["ETag", "Content-Type"],
|
||||||
|
"MaxAgeSeconds": 3600
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Also enable public access / custom domain for `R2_URL` so `<img>` URLs work after save.
|
||||||
|
|
||||||
|
## GitHub Actions secrets
|
||||||
|
|
||||||
|
Repository secrets used by workflows:
|
||||||
|
|
||||||
|
| Secret | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `DOKPLOY_URL` | Panel origin **without** `/api` (e.g. `https://panel.example.com`). Do not use the OpenAPI base URL that ends in `/api` — that yields `/api/api/...` and 404s. |
|
||||||
|
| `DOKPLOY_API_KEY` | API key from Dokploy profile → API/CLI |
|
||||||
|
| `DOKPLOY_STAGING_COMPOSE_ID` | Staging **Compose** service id (not an Application id) |
|
||||||
|
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production **Compose** service id (not an Application id) |
|
||||||
|
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
|
||||||
|
| `PRODUCTION_URL` | Public origin for production smoke |
|
||||||
|
|
||||||
|
HTTP 404 from `compose.deploy` usually means the compose id is wrong (Application id instead of Compose) or `DOKPLOY_URL` still includes `/api`.
|
||||||
|
|
||||||
|
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Staging (automatic)
|
||||||
|
|
||||||
|
[`.github/workflows/deploy-staging.yml`](../../.github/workflows/deploy-staging.yml)
|
||||||
|
|
||||||
|
1. Waits for workflow `CI` success on push to `main`.
|
||||||
|
2. Builds once; pushes `:<full-sha>` and `:staging`.
|
||||||
|
3. Calls Dokploy `compose.deploy` and polls until done.
|
||||||
|
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
|
||||||
|
|
||||||
|
### Production (manual)
|
||||||
|
|
||||||
|
[`.github/workflows/promote-production.yml`](../../.github/workflows/promote-production.yml)
|
||||||
|
|
||||||
|
1. Operator runs **Actions → Promote production**.
|
||||||
|
2. Inputs: full `sha` already on GHCR; `confirm` must be exactly `PRODUCTION`.
|
||||||
|
3. Retags the **same digest** as `:production` (no rebuild).
|
||||||
|
4. Deploys production compose + smoke.
|
||||||
|
|
||||||
|
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 and authorized production seeding
|
||||||
|
|
||||||
|
Seed credentials are local-only. For staging/production:
|
||||||
|
|
||||||
|
FrankenPHP sets `XDG_CONFIG_HOME=/config` (Caddy). PsySH/tinker then tries `/config/psysh`, which `appuser` cannot write — you get `Writing to directory /config/psysh is not allowed.` Override that env for the one-shot command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From Dokploy → staging/production → Open terminal on `web` (or one-off run)
|
||||||
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="
|
||||||
|
\$user = \\App\\Models\\User::query()->updateOrCreate(
|
||||||
|
['email' => 'admin@example.com'],
|
||||||
|
[
|
||||||
|
'name' => 'Admin',
|
||||||
|
'password' => 'use-a-strong-password',
|
||||||
|
'role' => 'admin',
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
\$user->forceFill(['email_verified_at' => now()])->save();
|
||||||
|
echo \$user->email.PHP_EOL;
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Plain password is enough: `User` casts `password` to `hashed` (and skips re-hash when value already hashed). `email_verified_at` is not mass-assignable — use `forceFill` as above.
|
||||||
|
|
||||||
|
Confirm:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
XDG_CONFIG_HOME=/tmp php artisan tinker --execute="echo \\App\\Models\\User::query()->where('email', 'admin@example.com')->exists() ? 'ok' : 'missing';"
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Policy (SPEC §16.3): daily PostgreSQL backup, retention ≥ 14 days, RPO ≤ 24h, RTO ≤ 4h.
|
||||||
|
|
||||||
|
### Configure (Dokploy)
|
||||||
|
|
||||||
|
1. Settings → Destinations: add S3-compatible destination (AWS S3, R2, etc.).
|
||||||
|
2. Open each PostgreSQL service → Backup:
|
||||||
|
- Destination: the S3 destination
|
||||||
|
- Schedule: cron e.g. `0 3 * * *`
|
||||||
|
- Prefix: `amare/staging` or `amare/production`
|
||||||
|
- Enabled: on
|
||||||
|
3. Click **Test** and verify the object appears in the bucket.
|
||||||
|
4. Prefer Dokploy alerts/webhooks for backup failure if configured.
|
||||||
|
|
||||||
|
### Restore (staging rehearsal before first production promote)
|
||||||
|
|
||||||
|
1. Create a scratch database or restore into a disposable Postgres service.
|
||||||
|
2. Database → Backup → **Restore**: pick destination + backup file + target database name.
|
||||||
|
3. Point a temporary compose env at the restored DB and confirm `/up` + `/admin/login`.
|
||||||
|
4. Document the timestamp of the successful rehearsal.
|
||||||
|
|
||||||
|
Do **not** promote to production until staging restore has been proven once.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
No rebuild. Move the environment alias to a previous SHA digest and redeploy.
|
||||||
|
|
||||||
|
### Staging
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Locally or in a one-off Actions shell with GHCR login
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag ghcr.io/<owner>/<repo>:staging \
|
||||||
|
ghcr.io/<owner>/<repo>:<previous-sha>
|
||||||
|
|
||||||
|
# Then trigger Dokploy deploy (UI Deploy, or):
|
||||||
|
DOKPLOY_URL=... DOKPLOY_API_KEY=... DOKPLOY_COMPOSE_ID=... \
|
||||||
|
./scripts/deploy/dokploy-deploy.sh
|
||||||
|
|
||||||
|
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production
|
||||||
|
|
||||||
|
Same pattern with `:production` tag and production compose id / URL. Prefer re-running **Promote production** with the previous SHA and confirmation `PRODUCTION`.
|
||||||
|
|
||||||
|
If a migration is not backward-compatible, fix forward with a new SHA; keep migrations reversible when possible.
|
||||||
|
|
||||||
|
## Smoke checks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SMOKE_BASE_URL=https://staging.example.com ./scripts/deploy/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expects HTTP 200 for `/up`, `/`, and `/admin/login`.
|
||||||
|
|
||||||
|
## Domain checklist before production promote
|
||||||
|
|
||||||
|
- [ ] Final hostname DNS → VPS
|
||||||
|
- [ ] Dokploy TLS certificate issued
|
||||||
|
- [ ] `APP_URL` matches public HTTPS origin
|
||||||
|
- [ ] `SESSION_SECURE_COOKIE=true`
|
||||||
|
- [ ] Staging smoke green on the SHA to promote
|
||||||
|
- [ ] Staging backup + restore rehearsed
|
||||||
|
- [ ] Production Postgres backup schedule enabled
|
||||||
|
- [ ] Production R2 bucket + Resend domain ready
|
||||||
|
- [ ] First admin created without seed
|
||||||
|
|
||||||
|
## Local validation of Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_IMAGE=ghcr.io/<owner>/<repo> IMAGE_TAG=staging \
|
||||||
|
docker compose -f docker-compose.deploy.yml config
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires a `.env` file present (Dokploy creates it from Environment UI). For local config checks, an empty `.env` is enough.
|
||||||
BIN
docs/evidence/public-site-motion/about-desktop.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
docs/evidence/public-site-motion/contact-mobile.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
262
frontend-audit.md
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
Audite e melhore integralmente este projeto do site da **Amare**, incluindo todas as rotas públicas, componentes compartilhados, formulários, navegação, conteúdo, metadados e comportamento responsivo.
|
||||||
|
|
||||||
|
Você está autorizado a executar o projeto, inspecionar o repositório e modificar diretamente o código. Não entregue somente recomendações: implemente as correções necessárias e valide o resultado.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Deixar o site tecnicamente sólido, visualmente refinado e pronto para apresentação à cliente e posterior lançamento, corrigindo problemas de:
|
||||||
|
|
||||||
|
* UI e UX;
|
||||||
|
* responsividade;
|
||||||
|
* posicionamento e conteúdo;
|
||||||
|
* acessibilidade;
|
||||||
|
* conversão;
|
||||||
|
* SEO;
|
||||||
|
* performance;
|
||||||
|
* robustez;
|
||||||
|
* qualidade e manutenção do código.
|
||||||
|
|
||||||
|
## Contexto do produto
|
||||||
|
|
||||||
|
A Amare é uma empresa de assessoria, produção e organização de eventos em São Paulo.
|
||||||
|
|
||||||
|
Ela atende:
|
||||||
|
|
||||||
|
* casamentos;
|
||||||
|
* eventos sociais e celebrações particulares;
|
||||||
|
* eventos corporativos.
|
||||||
|
|
||||||
|
O site não deve transmitir que a empresa trabalha exclusivamente com casamentos.
|
||||||
|
|
||||||
|
A experiência precisa equilibrar:
|
||||||
|
|
||||||
|
* emoção, proximidade, sensibilidade e sofisticação para eventos sociais;
|
||||||
|
* organização, segurança, método, clareza e credibilidade para eventos corporativos.
|
||||||
|
|
||||||
|
O resultado deve ser editorial, contemporâneo, humano, elegante e profissional, sem parecer excessivamente romântico nem corporativo demais.
|
||||||
|
|
||||||
|
Preview de referência:
|
||||||
|
|
||||||
|
`https://amare.preview.hellomanoel.com/`
|
||||||
|
|
||||||
|
Considere os documentos existentes no repositório, especialmente `AGENTS.md`, `README.md`, `SPEC.md`, `DESIGN.md` e equivalentes, como contexto autoritativo do projeto.
|
||||||
|
|
||||||
|
## Direção visual
|
||||||
|
|
||||||
|
Preserve a identidade visual existente quando ela funcionar corretamente:
|
||||||
|
|
||||||
|
* fundos off-white ou bege claro;
|
||||||
|
* verde oliva;
|
||||||
|
* composição editorial;
|
||||||
|
* fotografias em destaque;
|
||||||
|
* espaços em branco generosos;
|
||||||
|
* EB Garamond em títulos e destaques;
|
||||||
|
* elementos minimalistas;
|
||||||
|
* animações discretas;
|
||||||
|
* aparência refinada e atemporal.
|
||||||
|
|
||||||
|
Não preserve decisões que prejudiquem usabilidade, contraste, legibilidade, acessibilidade, responsividade ou performance.
|
||||||
|
|
||||||
|
Avalie especialmente:
|
||||||
|
|
||||||
|
* uso excessivo de Garamond em textos pequenos, menus, botões e formulários;
|
||||||
|
* verdes claros com contraste insuficiente;
|
||||||
|
* CTAs discretos demais;
|
||||||
|
* espaços vazios excessivos no celular;
|
||||||
|
* imagens que reforcem somente o posicionamento de casamento;
|
||||||
|
* falta de equilíbrio entre conteúdo social e corporativo;
|
||||||
|
* inconsistência tipográfica ou de espaçamento;
|
||||||
|
* aparência de template genérico de casamento.
|
||||||
|
|
||||||
|
## Escopo da auditoria
|
||||||
|
|
||||||
|
Analise todas as rotas encontradas no código e corrija problemas relacionados a:
|
||||||
|
|
||||||
|
### Produto e conteúdo
|
||||||
|
|
||||||
|
* clareza da proposta de valor;
|
||||||
|
* equilíbrio entre eventos sociais e corporativos;
|
||||||
|
* hierarquia das informações;
|
||||||
|
* conteúdo genérico, redundante ou sem função;
|
||||||
|
* coerência entre títulos, textos, imagens e CTAs;
|
||||||
|
* clareza dos serviços;
|
||||||
|
* confiança e credibilidade;
|
||||||
|
* caminho até contato ou solicitação de proposta.
|
||||||
|
|
||||||
|
Não invente história, números, clientes, prêmios, depoimentos, equipe, serviços, telefone, e-mail ou qualquer informação não confirmada.
|
||||||
|
|
||||||
|
### UI e experiência
|
||||||
|
|
||||||
|
* navegação;
|
||||||
|
* header e footer;
|
||||||
|
* menu mobile;
|
||||||
|
* hierarquia visual;
|
||||||
|
* tipografia;
|
||||||
|
* espaçamento;
|
||||||
|
* grids;
|
||||||
|
* CTAs;
|
||||||
|
* formulários;
|
||||||
|
* estados interativos;
|
||||||
|
* consistência entre páginas;
|
||||||
|
* experiência mobile-first;
|
||||||
|
* ausência de overflow, cortes, sobreposições ou distorções;
|
||||||
|
* adaptação real do layout ao celular, e não apenas redução da versão desktop.
|
||||||
|
|
||||||
|
### Acessibilidade
|
||||||
|
|
||||||
|
Use WCAG 2.2 AA como referência prática.
|
||||||
|
|
||||||
|
Corrija problemas de:
|
||||||
|
|
||||||
|
* HTML semântico;
|
||||||
|
* hierarquia de headings;
|
||||||
|
* navegação por teclado;
|
||||||
|
* foco visível;
|
||||||
|
* contraste;
|
||||||
|
* nomes acessíveis;
|
||||||
|
* labels;
|
||||||
|
* mensagens de erro;
|
||||||
|
* áreas de toque;
|
||||||
|
* textos alternativos;
|
||||||
|
* menu mobile;
|
||||||
|
* overlays;
|
||||||
|
* preferência por redução de movimento;
|
||||||
|
* uso correto de links e botões.
|
||||||
|
|
||||||
|
Prefira HTML semântico a ARIA desnecessária.
|
||||||
|
|
||||||
|
### Formulários e conversão
|
||||||
|
|
||||||
|
Garanta, quando aplicável:
|
||||||
|
|
||||||
|
* campos e obrigatoriedade claros;
|
||||||
|
* validação adequada;
|
||||||
|
* mensagens de erro específicas;
|
||||||
|
* estado de envio;
|
||||||
|
* prevenção de envio duplicado;
|
||||||
|
* estados de sucesso e falha;
|
||||||
|
* tratamento de erro de rede;
|
||||||
|
* preservação dos dados após erros recuperáveis;
|
||||||
|
* proteção antispam simples;
|
||||||
|
* privacidade e LGPD;
|
||||||
|
* links e mensagens de WhatsApp corretos;
|
||||||
|
* ausência de segredos expostos no cliente.
|
||||||
|
|
||||||
|
Quando algum dado real não estiver disponível, use configuração ou variável de ambiente e documente a pendência.
|
||||||
|
|
||||||
|
### SEO
|
||||||
|
|
||||||
|
Corrija, quando aplicável:
|
||||||
|
|
||||||
|
* títulos exclusivos por rota;
|
||||||
|
* meta descriptions;
|
||||||
|
* canonical;
|
||||||
|
* Open Graph;
|
||||||
|
* Twitter cards;
|
||||||
|
* sitemap;
|
||||||
|
* robots;
|
||||||
|
* favicon;
|
||||||
|
* idioma;
|
||||||
|
* headings;
|
||||||
|
* URLs;
|
||||||
|
* links internos;
|
||||||
|
* página 404;
|
||||||
|
* metadados sociais;
|
||||||
|
* indexação distinta entre preview e produção.
|
||||||
|
|
||||||
|
Não crie dados estruturados com informações não confirmadas.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
Corrija problemas relevantes relacionados a:
|
||||||
|
|
||||||
|
* imagens;
|
||||||
|
* tamanhos responsivos;
|
||||||
|
* formatos modernos;
|
||||||
|
* lazy loading;
|
||||||
|
* LCP;
|
||||||
|
* CLS;
|
||||||
|
* INP;
|
||||||
|
* fontes;
|
||||||
|
* scripts desnecessários;
|
||||||
|
* hidratação excessiva;
|
||||||
|
* JavaScript evitável;
|
||||||
|
* componentes client-side sem necessidade;
|
||||||
|
* animações custosas;
|
||||||
|
* dependências pesadas;
|
||||||
|
* carregamento de conteúdo abaixo da dobra.
|
||||||
|
|
||||||
|
Preserve a qualidade visual das fotografias.
|
||||||
|
|
||||||
|
### Qualidade técnica
|
||||||
|
|
||||||
|
Corrija:
|
||||||
|
|
||||||
|
* erros de TypeScript;
|
||||||
|
* erros de lint;
|
||||||
|
* erros de build;
|
||||||
|
* warnings de hidratação;
|
||||||
|
* erros de console;
|
||||||
|
* requisições quebradas;
|
||||||
|
* links inválidos;
|
||||||
|
* rotas órfãs;
|
||||||
|
* componentes duplicados quando a consolidação simplificar o projeto;
|
||||||
|
* tratamento de erros ausente;
|
||||||
|
* tipagem insegura;
|
||||||
|
* complexidade desnecessária diretamente relacionada ao escopo.
|
||||||
|
|
||||||
|
## Prioridades
|
||||||
|
|
||||||
|
Considere:
|
||||||
|
|
||||||
|
* **P0:** bloqueia funcionamento, segurança, uso ou lançamento;
|
||||||
|
* **P1:** prejudica significativamente experiência, conversão, acessibilidade, SEO, performance ou credibilidade;
|
||||||
|
* **P2:** refinamento não essencial para o lançamento.
|
||||||
|
|
||||||
|
Implemente todos os itens P0 e P1 que possam ser resolvidos com as informações existentes.
|
||||||
|
|
||||||
|
Itens dependentes de dados reais da cliente devem permanecer como pendências explícitas, sem conteúdo fictício.
|
||||||
|
|
||||||
|
## Restrições
|
||||||
|
|
||||||
|
* Preserve a stack e a arquitetura existentes quando forem adequadas.
|
||||||
|
* Prefira mudanças simples, localizadas e fáceis de manter.
|
||||||
|
* Não reescreva o projeto sem necessidade concreta.
|
||||||
|
* Não adicione bibliotecas quando a solução atual for suficiente.
|
||||||
|
* Não implemente pagamentos, CRM, área do cliente, chat, contratos avançados ou funcionalidades fora do MVP.
|
||||||
|
* Não altere arquivos não relacionados sem justificativa.
|
||||||
|
* Preserve alterações locais preexistentes.
|
||||||
|
* Não silencie problemas com `any`, `eslint-disable`, casts inseguros ou desativação de validações.
|
||||||
|
* Não reduza acessibilidade para preservar estética.
|
||||||
|
* Não deixe mocks ou soluções temporárias como implementação final.
|
||||||
|
|
||||||
|
## Critérios de conclusão
|
||||||
|
|
||||||
|
A tarefa estará concluída quando:
|
||||||
|
|
||||||
|
* todas as rotas públicas existentes tiverem sido auditadas;
|
||||||
|
* todos os problemas P0 e P1 solucionáveis tiverem sido corrigidos;
|
||||||
|
* o site funcionar corretamente em mobile e desktop;
|
||||||
|
* não houver overflow horizontal, sobreposição ou componentes quebrados;
|
||||||
|
* menu, navegação, links, CTAs e formulários funcionarem;
|
||||||
|
* o posicionamento social e corporativo estiver claro;
|
||||||
|
* a acessibilidade essencial estiver atendida;
|
||||||
|
* os metadados e fundamentos de SEO estiverem corretos;
|
||||||
|
* os principais problemas de performance tiverem sido tratados;
|
||||||
|
* erros causados ou revelados pelas alterações tiverem sido resolvidos;
|
||||||
|
* lint, TypeScript, testes e build disponíveis tiverem sido executados com sucesso.
|
||||||
|
|
||||||
|
Não declare uma validação como aprovada sem executá-la.
|
||||||
|
|
||||||
|
## Entrega final
|
||||||
|
|
||||||
|
Ao concluir, apresente:
|
||||||
|
|
||||||
|
1. rotas auditadas;
|
||||||
|
2. problemas principais encontrados;
|
||||||
|
3. alterações implementadas;
|
||||||
|
4. arquivos modificados;
|
||||||
|
5. comandos executados e seus resultados;
|
||||||
|
6. decisões e trade-offs relevantes;
|
||||||
|
7. informações que dependem da cliente;
|
||||||
|
8. itens P2 mantidos fora do escopo.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-02
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Notes — recreate-public-frontend
|
||||||
|
|
||||||
|
## Remaining unresolved (do not invent)
|
||||||
|
|
||||||
|
- **Official contacts:** production e-mail is `amareassessoriaeventos@gmail.com`; WhatsApp and Instagram handles still placeholder until the owner confirms.
|
||||||
|
- **Testimonial authorization:** five real couples from `depoimentos.md` are seeded for local/demo/visual; production publish still requires explicit couple authorization before `published_at` goes live.
|
||||||
|
- **Authorized photography:** public images remain fixture/demo with editorial disclosure notes until Amare supplies an authorized portfolio archive.
|
||||||
|
- **WEB-05 briefing form:** `/contato` stays presentation-only (channels + CTA); lead capture is a future change.
|
||||||
|
|
||||||
|
## Adaptation note
|
||||||
|
|
||||||
|
Mockup `amare-home-editorial.html` was single-page with anchors. Implementation keeps Laravel multipage routes; home carries the editorial narrative, internal pages are chapters with the same Heritage Editorial grammar.
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
O site público da Fase 1 já entrega rotas, CMS, SEO, mídia responsiva, regressão visual e acessibilidade. A identidade visual, porém, ainda é o placeholder da fundação: Instrument Sans, acento ouro, raios arredondados e cartões com sombra. `DESIGN.md` (“Heritage Editorial”), o mockup `amare-home-editorial.html`, o logo fornecido (coração facetado) e `depoimentos.md` (cinco casais reais) definem a marca a materializar.
|
||||||
|
|
||||||
|
Restrições que condicionam o desenho:
|
||||||
|
|
||||||
|
- Arquitetura multipágina Laravel/Blade/CMS já aprovada; o mockup HTML é single-page com âncoras — adaptar, não portar literalmente.
|
||||||
|
- Contato permanece placeholder (WEB-05 fora de escopo); CTAs levam a `/contato` sem criar leads.
|
||||||
|
- Fontes self-hosted via Vite (determinismo visual); sem Google Fonts CDN.
|
||||||
|
- Imagens públicas via disco configurado + variantes; Unsplash do mockup não entra no app.
|
||||||
|
- `PRODUCT.md`: São Paulo capital; seeders atuais ainda usam Fortaleza e depoimentos fictícios.
|
||||||
|
- Change paralela `complete-foundation-parity` não bloqueia nem é bloqueada por esta.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Materializar Heritage Editorial em todas as rotas públicas (home, serviços, portfólio, caso, sobre, contato, privacidade, 404, 500).
|
||||||
|
- Home como capa do “Dossiê Editorial do Evento”: hero, manifesto, serviços, portfólio, método (4 passos), depoimentos reais, perfil Amare, CTA final.
|
||||||
|
- Tokens centralizados alinhados a `DESIGN.md`; EB Garamond única família; radius 0; elevação por campos tonais.
|
||||||
|
- Logo oficial otimizado (selo + lockup) em fundos claros/escuros, geometria preservada.
|
||||||
|
- Cinco depoimentos de `depoimentos.md` no CMS/seed, multipárrafo, com nota de autorização.
|
||||||
|
- Manter publicação dinâmica, paginação, eager loading, SEO, axe, teclado, contraste AA e baselines determinísticas.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Briefing funcional / lead (WEB-05), WhatsApp automatizado, inventar provas corporativas.
|
||||||
|
- Redesign do Filament, page builder, single-page navigation como modelo primário.
|
||||||
|
- Fotografia proprietária real (permanece ilustrativa e marcada até acervo autorizado).
|
||||||
|
- Staging/deploy/auth parity (`complete-foundation-parity`).
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1 — Multipágina editorial, não single-page literal
|
||||||
|
|
||||||
|
Preservar rotas do SPEC §5.1. Home concentra a narrativa do mockup; páginas internas herdam a mesma gramática (eyebrow, títulos, linhas 1px, spreads assimétricos no desktop, sequência linear no mobile). Header usa links de rota (não `#âncoras` como navegação primária), com CTA “Solicitar proposta” → `contact`.
|
||||||
|
|
||||||
|
*Alternativas:* home quase idêntica com âncoras (rejeitada: conflita com SEO/CMS/rotas já testadas); portar HTML estático (rejeitada: perde CMS e determinismo).
|
||||||
|
|
||||||
|
### D2 — Tokens Heritage Editorial como única fonte visual
|
||||||
|
|
||||||
|
Reescrever `resources/css/tokens.css` e o mapeamento `@theme` em `app.css`:
|
||||||
|
|
||||||
|
| Papel | Token | Valor |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| Fundo | `--amare-color-bg` | `#FBF9F4` (Papel Marfim) |
|
||||||
|
| Fundo profundo | `--amare-color-bg-deep` | `#F0EEE9` |
|
||||||
|
| Arquivo | `--amare-color-bg-archive` | `#E4E2DD` |
|
||||||
|
| Oliva | `--amare-color-accent` | `#556B2F` |
|
||||||
|
| Oliva profunda | `--amare-color-accent-deep` | `#3E5219` |
|
||||||
|
| Sálvia | `--amare-color-sage` | `#8B9D77` |
|
||||||
|
| Tinta | `--amare-color-text` | `#1B1C19` |
|
||||||
|
| Tinta suave | `--amare-color-muted` | `#5D6155` |
|
||||||
|
| Linha | `--amare-color-border` | `#C5C8B8` |
|
||||||
|
| Radius | `--amare-radius-*` | `0` |
|
||||||
|
| Container | `--amare-container-max` | `1120px` |
|
||||||
|
| Sombra | removida / não usada em conteúdo | — |
|
||||||
|
|
||||||
|
Tipografia: EB Garamond (400/500/600) self-hosted via Vite; escala display/headline/title/body/label conforme `DESIGN.md`. Componentes públicos deixam de usar `rounded-*` e shadows de cartão.
|
||||||
|
|
||||||
|
*Alternativa:* CSS inline do mockup (rejeitada: foge de `design-tokens` e quebra Tailwind/`@theme`).
|
||||||
|
|
||||||
|
### D3 — Composição da home e omissão de seções vazias
|
||||||
|
|
||||||
|
Ordem canônica:
|
||||||
|
|
||||||
|
1. Hero (settings + imagem OG/hero se houver)
|
||||||
|
2. Manifesto (copy de settings)
|
||||||
|
3. Serviços em destaque (lista editorial, não grid de cartões)
|
||||||
|
4. Portfólio em destaque (bloco escuro oliva; funde proof+cases atuais)
|
||||||
|
5. Método (4 passos: Escuta, Direção, Produção, Execução)
|
||||||
|
6. Depoimentos publicados
|
||||||
|
7. Perfil / posicionamento Amare (about + princípios)
|
||||||
|
8. CTA final → `/contato`
|
||||||
|
|
||||||
|
Seções alimentadas por collections (serviços, casos, depoimentos) **omitidas** quando vazias. Manifesto, método, perfil e CTA final permanecem (copy de settings / defaults editoriais). Um único `h1` no hero; demais seções usam `h2`/`h3`.
|
||||||
|
|
||||||
|
### D4 — Contato continua presentation-only
|
||||||
|
|
||||||
|
`/contato` e o CTA da home mostram canais de `site_settings` (e-mail, telefone, cidade, sociais). Nenhum `<form>` funcional, nenhum lead. O mockup de formulário serve só como referência visual futura para WEB-05; nesta change o bloco de contato da home é CTA editorial + link para a página de contato, não formulário embutido.
|
||||||
|
|
||||||
|
### D5 — Extensão mínima tipada de `site_settings`
|
||||||
|
|
||||||
|
Novos campos tipados (não key/value genérico):
|
||||||
|
|
||||||
|
- `logo_path` / `logo_alt` (opcional; fallback para lockup estático em `public/`)
|
||||||
|
- `hero_secondary_cta_label` (opcional)
|
||||||
|
- `hero_note` (texto curto sob CTAs)
|
||||||
|
- `manifesto_title`, `manifesto_lead`, `manifesto_body`
|
||||||
|
- `method_intro` (opcional; passos estruturados em JSON tipado ou colunas `method_step_{1..4}_{title,body}` — preferir JSONB `method_steps` validado no Filament)
|
||||||
|
- `principles` (JSONB lista de até 4 strings) **ou** quatro colunas `principle_1..4`
|
||||||
|
- Manter `about_summary`, hero atual, contato, SEO, analytics
|
||||||
|
|
||||||
|
Filament `ManageSiteSettings` ganha seções editoriais em pt-BR. Defaults no seeder alinhados ao mockup + São Paulo.
|
||||||
|
|
||||||
|
*Alternativa:* hardcode de manifesto/método nas Blade (rejeitada parcialmente: método/princípios podem ter default no view, mas copy institucional deve ser editável como o hero).
|
||||||
|
|
||||||
|
### D6 — Logo: ativo estático + campo CMS opcional
|
||||||
|
|
||||||
|
1. Converter/otimizar o PNG fornecido para WebP/SVG derivados em `public/brand/` (selo coração + lockup completo), com versões para fundo claro (oliva) e fundo escuro (papel/branco).
|
||||||
|
2. Componente `<x-brand.logo>` escolhe variante por contexto (`on-dark` / `on-light`) e expõe `alt` acessível.
|
||||||
|
3. Se `logo_path` em settings estiver preenchido, usa o upload; senão, o estático versionado.
|
||||||
|
|
||||||
|
Não redesenhar o coração facetado; não inventar polígonos decorativos genéricos.
|
||||||
|
|
||||||
|
### D7 — Depoimentos reais multipárrafo
|
||||||
|
|
||||||
|
- Seedar os 5 casais de `depoimentos.md` em `quote` (texto completo com quebras `\n\n`), `author_name`, `context` (ex.: `Casamento · 06/12/2025`), `sort_order`, `is_featured`, `published_at` conforme ambiente.
|
||||||
|
- Blade renderiza parágrafos a partir de quebras de linha; tipografia editorial (aspas, offset).
|
||||||
|
- Nota discreta no markup/admin: autorização final dos casais antes de publicação em produção.
|
||||||
|
- Remover depoimentos fictícios dos seeders de demo/visual ou substituí-los pelos reais (visual seeder usa subset determinístico, tipicamente 2 featured).
|
||||||
|
|
||||||
|
### D8 — Navegação responsiva com JS mínimo
|
||||||
|
|
||||||
|
`resources/js/app.js` ganha toggle de menu mobile (aria-expanded, `menu-open`, fechar ao navegar), espelhando o mockup. Sem framework novo. `prefers-reduced-motion` continua a anular transições não essenciais. Hover de imagem (scale leve) só quando motion permitido.
|
||||||
|
|
||||||
|
### D9 — Páginas internas como capítulos
|
||||||
|
|
||||||
|
| Rota | Tratamento |
|
||||||
|
|------|------------|
|
||||||
|
| `/servicos` | Lista editorial (número + nome + resumo), não cartões |
|
||||||
|
| `/portfolio` | Grade assimétrica / stack com captions; fundo pode usar papel profundo |
|
||||||
|
| `/portfolio/{slug}` | Caderno de caso: metadados, desafio/solução/resultado, galeria |
|
||||||
|
| `/sobre` | Perfil editorial + princípios |
|
||||||
|
| `/contato` | Canais + CTA textual (sem form) |
|
||||||
|
| `/privacidade` | Tipografia editorial sobre papel |
|
||||||
|
| 404/500 | Mesma linguagem; 500 sem internals |
|
||||||
|
|
||||||
|
### D10 — Testes e baselines
|
||||||
|
|
||||||
|
- Atualizar feature tests de home (ordem de seções, omissão, CTA → contact, `data-testid="home-primary-cta"`).
|
||||||
|
- Browser: axe nas rotas cobertas; Tab até CTA; console limpo.
|
||||||
|
- `composer visual:update` após aprovação visual local; timezone permanece `America/Fortaleza` (SPEC); copy de cidade pública passa a São Paulo.
|
||||||
|
- Fotos fixture locais continuam; filtro CSS de saturação contida via classe utilitária, não via URL externa.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Regeneração de 8+ snapshots** → risco de ruído no PR; mitigar com seeder visual estável e revisão humana do diff.
|
||||||
|
- **EB Garamond em forms/UI** → legibilidade de labels uppercase; mitigar com letter-spacing e peso 600 conforme DESIGN.md; validar contraste AA.
|
||||||
|
- **Depoimentos longos** → layout quebra em mobile; mitigar com tipografia responsiva e subset featured na home.
|
||||||
|
- **Autorização de depoimentos** → risco legal/reputacional; mitigar com nota explícita e `published_at` null até autorização.
|
||||||
|
- **Campos novos em site_settings** → migração + Filament; mitigar com defaults e nullable.
|
||||||
|
- **Paridade com mockup single-page** → expectativa visual vs rotas; documentar adaptação multipágina na proposta e no surface brief.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Migrar tokens/fontes/logo estático (sem breaking de rotas).
|
||||||
|
2. Migrar `site_settings` (colunas novas nullable + backfill de defaults).
|
||||||
|
3. Atualizar seeders (SP + depoimentos reais).
|
||||||
|
4. Trocar layout e páginas; manter contratos de testes passando incrementalmente.
|
||||||
|
5. Regenerar baselines com `composer visual:update`.
|
||||||
|
6. Rollback: reverter deploy/commit; migração down remove colunas novas; assets estáticos são aditivos.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Formato exato de `method_steps` / `principles` (JSONB vs colunas) — default recomendado: JSONB validado no Filament.
|
||||||
|
- WhatsApp/e-mail/Instagram oficiais ainda ausentes — settings continuam placeholder até o dono informar.
|
||||||
|
- Subconjunto de depoimentos na home (2 vs 5) — default: featured first, até 2 na home estilo mockup; listagem completa só se houver página dedicada (não há); home mostra todos published featured ou os N primeiros por `sort_order` (cap 2–3 para ritmo editorial).
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
O site público já existe com rotas, CMS e gates de qualidade, mas a identidade visual ainda é o placeholder da Fase 0 (Instrument Sans, ouro, cantos arredondados, cartões genéricos). `DESIGN.md` e o mockup editorial já definem o sistema Heritage Editorial; `depoimentos.md` e o logo fornecido finalmente permitem prova e marca reais. Sem recriar o frontend agora, o critério de saída da Fase 1 (“site público aprovado visualmente”) permanece ligado a uma UI que não representa a marca, e a Fase 2 (WEB-05) herdaria um shell genérico.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Substituir tokens, tipografia e composição do site público pelo sistema **Heritage Editorial** de `DESIGN.md` (EB Garamond, oliva/papel, cantos retos, campos tonais, sem sombras de cartão SaaS).
|
||||||
|
- Adaptar o mockup `amare-home-editorial.html` à arquitetura **multipágina** Laravel/CMS existente: home como capa editorial; serviços, portfólio, caso, sobre, contato, privacidade e erros como capítulos coerentes.
|
||||||
|
- Incorporar o logo fornecido (coração facetado + lockup) como ativo otimizado com variantes para fundos claros/escuros, sem redesenhar a geometria.
|
||||||
|
- Seedar e renderizar os **cinco depoimentos reais** de `depoimentos.md` (texto, casal, data/contexto), com autorização final como requisito de publicação.
|
||||||
|
- Reestruturar a home na narrativa editorial: hero → manifesto → serviços → portfólio → método → depoimentos → perfil Amare → CTA final (WEB-01).
|
||||||
|
- Atualizar layout público (header/footer, navegação responsiva, selo), componentes Blade e páginas internas para a mesma linguagem visual.
|
||||||
|
- Estender `site_settings` o mínimo necessário para copy editorial (manifesto, nota do hero, CTA secundário, passos do método, princípios) mantendo singleton tipado (WEB-06).
|
||||||
|
- Regenerar baselines visuais desktop/mobile e manter axe, teclado, landmarks, SEO e publicação dinâmica (SPEC §6.5, §13.5, §13.8).
|
||||||
|
- Atualizar seeders/conteúdo demonstrativo para São Paulo e marcar fotografias ilustrativas até existir acervo autorizado.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
Conforme [SPEC.md §4.2](../../SPEC.md) e decisões desta proposta:
|
||||||
|
|
||||||
|
- Formulário funcional de briefing / criação de lead (WEB-05) — permanece placeholder em `/contato`; change futura `build-lead-capture`.
|
||||||
|
- Integração WhatsApp, portal do cliente, page builder, i18n, PWA.
|
||||||
|
- Inventar cases corporativos, credenciais, números, imprensa ou provas não autorizadas.
|
||||||
|
- Redesign do painel Filament / área interna.
|
||||||
|
- Deploy staging / paridade de fundação — change paralela `complete-foundation-parity`, independente desta.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
<!-- Nenhuma capability nova: a recriação altera requisitos de capabilities já existentes. -->
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `design-tokens`: substituir tokens placeholder (Instrument Sans, ouro, raios, sombras) pelos tokens Heritage Editorial (EB Garamond, oliva/papel, radius 0, elevação tonal).
|
||||||
|
- `public-site-pages`: reestruturar home editorial e páginas públicas para a composição “Dossiê Editorial do Evento”, preservando rotas e publicação.
|
||||||
|
- `site-settings`: campos tipados adicionais para copy editorial da home (manifesto, CTAs, método, princípios, logo).
|
||||||
|
- `testimonials`: suporte a depoimentos multipárrafo reais com contexto/data e regra de autorização antes da publicação.
|
||||||
|
- `content-media`: logo da marca e tratamento editorial de imagens demonstrativas (marcação, filtros contidos) sem inventar acervo.
|
||||||
|
- `visual-regression`: baselines regeneradas sob a nova identidade; determinismo e cobertura de telas mantidos.
|
||||||
|
- `web-accessibility`: preservação/reforço de landmarks, foco, teclado, contraste AA e movimento reduzido após a troca tipográfica/cromática.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Altera**: `resources/css/tokens.css`, `resources/css/app.css`, `vite.config.js`, `resources/views/layouts/public.blade.php`, `resources/views/pages/**`, `resources/views/components/home/**`, `resources/js/app.js`, seeders (`ContentSeeder`, `VisualContentSeeder`), possivelmente migração + model/Filament de `site_settings`, testes Feature/Browser e snapshots em `tests/.pest/snapshots/`.
|
||||||
|
- **Cria**: ativo de logo em `public/` (ou storage CMS), componentes Blade de marca/manifesto/positioning, campos tipados novos em `site_settings` se necessário.
|
||||||
|
- **Depende de**: specs atuais `design-tokens`, `public-site-pages`, `site-settings`, `testimonials`, `content-media`, `visual-regression`, `web-accessibility`, `public-seo`, `service-catalog`, `portfolio-cases`.
|
||||||
|
- **Independente de**: `complete-foundation-parity` (auth/staging/coverage).
|
||||||
|
- **Risco**: regeneração ampla de snapshots; mitigado por seed determinístico, fontes self-hosted e `composer visual:update` com revisão humana. Depoimentos reais exigem confirmação de autorização antes de publicar em produção.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Brand logo assets are available to the public layout
|
||||||
|
|
||||||
|
The system SHALL provide optimized Amare brand logo assets derived from the official faceted-heart lockup for use in the public header, footer and institutional pages. Assets MUST preserve the original geometry, include an accessible text alternative, and provide variants suitable for light and dark tonal fields. When `site_settings.logo_path` is present, the uploaded logo MUST be used; otherwise the versioned static brand asset MUST be used.
|
||||||
|
|
||||||
|
#### Scenario: Header renders brand mark with alt text
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** the brand mark image or equivalent MUST expose accessible alternative text identifying Amare Assessoria
|
||||||
|
|
||||||
|
#### Scenario: Dark portfolio field uses a legible logo variant
|
||||||
|
|
||||||
|
- **WHEN** the brand mark is rendered on an olive-deep or otherwise dark public surface
|
||||||
|
- **THEN** the chosen logo variant MUST remain legible against that background
|
||||||
|
|
||||||
|
#### Scenario: Uploaded logo overrides static fallback
|
||||||
|
|
||||||
|
- **GIVEN** an admin has saved `logo_path` and `logo_alt` in site settings
|
||||||
|
- **WHEN** the public layout renders the brand mark
|
||||||
|
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Decorative saturation/contrast treatment for editorial mood MUST be applied via CSS on self-hosted images and MUST NOT introduce external image CDN dependencies that break deterministic visual tests.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Design tokens are centralized for the public site
|
||||||
|
|
||||||
|
The system SHALL define design tokens in a single source consumed by the public site layout and components. Tokens MUST implement the Heritage Editorial system from `DESIGN.md`: typography family EB Garamond (self-hosted), font scale (display/headline/title/body/label), spacing on an 8px rhythm, border radius `0` for interactive and content surfaces, container max width `1120px`, paper/olive/sage/ink color roles, transition duration/easing, and MUST NOT rely on card shadows as a hierarchy mechanism for regular content.
|
||||||
|
|
||||||
|
#### Scenario: Public layout uses shared Heritage Editorial tokens
|
||||||
|
|
||||||
|
- **WHEN** a public page is rendered
|
||||||
|
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
||||||
|
- **AND** the primary typeface MUST be EB Garamond (or the declared serif fallback stack)
|
||||||
|
- **AND** public content surfaces MUST use `0` border radius from tokens
|
||||||
|
|
||||||
|
#### Scenario: Palette commits paper and olive regions
|
||||||
|
|
||||||
|
- **WHEN** the public site is rendered
|
||||||
|
- **THEN** background regions MUST use paper ivory / paper deep / olive deep tokens rather than pure white card stacks on a white page
|
||||||
|
- **AND** primary interactive emphasis MUST use olive heritage (`#556B2F`) / olive deep (`#3E5219`) tokens
|
||||||
|
|
||||||
|
### Requirement: Public site respects reduced motion preference
|
||||||
|
|
||||||
|
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site, including image hover scales and menu transitions.
|
||||||
|
|
||||||
|
#### Scenario: User prefers reduced motion
|
||||||
|
|
||||||
|
- **WHEN** a visitor has `prefers-reduced-motion: reduce` enabled
|
||||||
|
- **THEN** the public site MUST NOT play non-essential motion effects
|
||||||
|
|
||||||
|
### Requirement: Public site meets baseline accessibility contrast
|
||||||
|
|
||||||
|
The system SHALL use Heritage Editorial color combinations that meet WCAG AA contrast requirements for text and interactive elements. Long-form text MUST use ink on paper; sage MUST NOT replace reading color when contrast would fall below AA.
|
||||||
|
|
||||||
|
#### Scenario: Primary text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary body text is rendered on its background color
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
#### Scenario: Olive on paper interactive text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary buttons or links use olive tokens on paper backgrounds (or paper text on olive)
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Elevation comes from tonal fields not card shadows
|
||||||
|
|
||||||
|
The public site SHALL express hierarchy through tonal paper fields, 1px botanical rules, and editorial overlap. Regular content components MUST NOT use short grey SaaS card shadows.
|
||||||
|
|
||||||
|
#### Scenario: Content cards omit drop shadows
|
||||||
|
|
||||||
|
- **WHEN** home services, testimonials, or portfolio items are rendered
|
||||||
|
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
||||||
|
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
|
||||||
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition (asymmetric spreads on desktop, linear sequence on mobile) rather than rounded card grids.
|
||||||
|
|
||||||
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
|
- **GIVEN** published services, cases and testimonials exist
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
|
||||||
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
|
- **GIVEN** no published services, cases or testimonials
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the response MUST be 200
|
||||||
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
|
- **WHEN** the home is loaded in a real browser at desktop and mobile viewports
|
||||||
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
|
|
||||||
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
|
||||||
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
||||||
|
|
||||||
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
|
- **GIVEN** a published case with multiple gallery images
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the images MUST be rendered ordered by `sort_order`
|
||||||
|
|
||||||
|
#### Scenario: Listings paginate open-ended growth
|
||||||
|
|
||||||
|
- **WHEN** the number of published cases exceeds the page size
|
||||||
|
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
||||||
|
|
||||||
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
|
||||||
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
|
- **WHEN** a visitor requests a non-existent public URL
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
- **AND** the page MUST use the public layout and offer navigation back to the home
|
||||||
|
|
||||||
|
#### Scenario: Server error hides internals in production
|
||||||
|
|
||||||
|
- **GIVEN** `APP_DEBUG` is false
|
||||||
|
- **WHEN** an unhandled exception occurs on a public route
|
||||||
|
- **THEN** the response MUST be a generic branded error page
|
||||||
|
- **AND** MUST NOT contain a stack trace, file path, or environment variable
|
||||||
|
|
||||||
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
|
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads and MUST NOT submit a functional briefing form in this change.
|
||||||
|
|
||||||
|
#### Scenario: Contact page shows configured contact data
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/contato`
|
||||||
|
- **THEN** the e-mail and phone stored in `site_settings` MUST be displayed
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public header exposes brand mark and responsive navigation
|
||||||
|
|
||||||
|
The public layout SHALL render the Amare brand mark (faceted-heart logo lockup or configured logo), primary route navigation, and a contact CTA. On narrow viewports the navigation MUST be operable via a disclosure control with accessible name and `aria-expanded` state.
|
||||||
|
|
||||||
|
#### Scenario: Desktop header shows navigation and CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads any public page at a desktop viewport
|
||||||
|
- **THEN** the header MUST include brand mark, links to home/services/portfolio/about/contact, and a contact CTA
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu toggles accessibly
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the menu button on a narrow viewport
|
||||||
|
- **THEN** the primary navigation MUST become available
|
||||||
|
- **AND** the control MUST expose an updated `aria-expanded` value
|
||||||
|
- **AND** activating a navigation link MUST close the menu
|
||||||
|
|
||||||
|
### Requirement: Demonstrative photography is labeled until authorized assets exist
|
||||||
|
|
||||||
|
When public pages render illustrative/demo photography that is not an authorized Amare asset, the system SHALL mark that imagery as demonstrative in visible copy or accessible labeling so visitors are not misled.
|
||||||
|
|
||||||
|
#### Scenario: Portfolio demo imagery is disclosed
|
||||||
|
|
||||||
|
- **WHEN** the home or portfolio renders placeholder photography
|
||||||
|
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Site settings singleton is manageable by admin only
|
||||||
|
|
||||||
|
The system SHALL persist site-wide settings in a `site_settings` table as a typed singleton (SPEC WEB-06, §8.2). Fields MUST include brand name, optional logo path and logo alt text, hero copy (eyebrow, title, subtitle, primary CTA label, optional secondary CTA label, optional hero note), manifesto copy (title, lead, body), method steps (structured typed data for four editorial steps), principles (structured typed list), about summary, contact email/phone/city, social links (jsonb), default meta title/description, default OG image path and alt text, and optional analytics fields disabled by default.
|
||||||
|
|
||||||
|
#### Scenario: Admin updates site settings
|
||||||
|
|
||||||
|
- **WHEN** an admin saves the site settings form in Filament
|
||||||
|
- **THEN** the singleton record is updated
|
||||||
|
- **AND** labels and validation messages are in pt-BR
|
||||||
|
|
||||||
|
#### Scenario: Assistant cannot access site settings
|
||||||
|
|
||||||
|
- **WHEN** an assistant navigates to site settings in Filament
|
||||||
|
- **THEN** access MUST be denied with HTTP 403
|
||||||
|
|
||||||
|
#### Scenario: Default OG image requires alt text
|
||||||
|
|
||||||
|
- **WHEN** an admin uploads a default OG image without alt text
|
||||||
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
|
- **AND** alt text MUST remain optional when no default OG image is present
|
||||||
|
|
||||||
|
#### Scenario: Logo upload requires alt text
|
||||||
|
|
||||||
|
- **WHEN** an admin uploads a brand logo without alt text
|
||||||
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
|
- **AND** alt text MUST remain optional when no logo is uploaded
|
||||||
|
|
||||||
|
#### Scenario: Singleton avoids generic key-value store
|
||||||
|
|
||||||
|
- **WHEN** site settings are stored
|
||||||
|
- **THEN** the system MUST use typed columns on `site_settings`
|
||||||
|
- **AND** MUST NOT introduce a generic key/value configuration table
|
||||||
|
|
||||||
|
#### Scenario: Editorial defaults remain available when optional fields are empty
|
||||||
|
|
||||||
|
- **GIVEN** manifesto, method steps or principles fields are empty
|
||||||
|
- **WHEN** the home is rendered
|
||||||
|
- **THEN** the page MUST still render those sections using safe editorial defaults
|
||||||
|
- **AND** MUST NOT error
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public geography defaults to São Paulo
|
||||||
|
|
||||||
|
Demo and visual seed content for site settings SHALL present the Amare operating city as São Paulo (capital), matching `PRODUCT.md`, instead of unrelated cities.
|
||||||
|
|
||||||
|
#### Scenario: Seeded settings use São Paulo
|
||||||
|
|
||||||
|
- **WHEN** content seeders populate `site_settings`
|
||||||
|
- **THEN** the city field MUST be São Paulo (or equivalent capital wording)
|
||||||
|
- **AND** MUST NOT present Fortaleza as the operating city
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
|
- **WHEN** a testimonial has `published_at` null
|
||||||
|
- **THEN** the `published()` scope MUST exclude it
|
||||||
|
|
||||||
|
#### Scenario: Published testimonial is queryable
|
||||||
|
|
||||||
|
- **WHEN** an admin sets `published_at` with required quote and author name
|
||||||
|
- **THEN** the testimonial MUST be included in the `published()` scope
|
||||||
|
|
||||||
|
#### Scenario: Assistant cannot manage testimonials
|
||||||
|
|
||||||
|
- **WHEN** an assistant attempts to access the testimonials Resource
|
||||||
|
- **THEN** access MUST be denied with HTTP 403
|
||||||
|
|
||||||
|
#### Scenario: Featured testimonials are filterable
|
||||||
|
|
||||||
|
- **WHEN** content is queried with featured filter
|
||||||
|
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
||||||
|
|
||||||
|
#### Scenario: Multi-paragraph quotes render as paragraphs
|
||||||
|
|
||||||
|
- **GIVEN** a published testimonial whose quote contains blank-line separated paragraphs
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
||||||
|
|
||||||
|
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
||||||
|
|
||||||
|
#### Scenario: Seed loads the five real couples
|
||||||
|
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** testimonials for Jeniffer e Maick, Quesia e Jhonata, Milena e Weslley, Raquel e Pedro, and Victoria e Pedro MUST exist with their source quotes and marriage context/dates
|
||||||
|
|
||||||
|
#### Scenario: Fictional demo quotes are removed
|
||||||
|
|
||||||
|
- **WHEN** the content seeder completes
|
||||||
|
- **THEN** previously invented placeholder testimonial authors MUST NOT remain as the published demo set
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
|
||||||
|
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
||||||
|
|
||||||
|
#### Scenario: Unintended visual change fails the suite
|
||||||
|
|
||||||
|
- **GIVEN** approved baselines exist
|
||||||
|
- **WHEN** a code change alters the rendering of a covered screen
|
||||||
|
- **THEN** the visual assertion MUST fail and report the diff
|
||||||
|
|
||||||
|
#### Scenario: Both viewports are covered
|
||||||
|
|
||||||
|
- **WHEN** the visual suite runs
|
||||||
|
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
||||||
|
|
||||||
|
#### Scenario: Heritage Editorial identity is captured
|
||||||
|
|
||||||
|
- **WHEN** approved baselines for the home are reviewed after this change
|
||||||
|
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
||||||
|
|
||||||
|
### Requirement: Visual runs are deterministic
|
||||||
|
|
||||||
|
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, self-hosted fonts installed/bundled for the suite, frozen clock, deterministic seed (including real testimonial subset and São Paulo settings), animations and transitions disabled, and no dependency on external network.
|
||||||
|
|
||||||
|
#### Scenario: Repeated run without code change produces no diff
|
||||||
|
|
||||||
|
- **WHEN** the visual suite runs twice against the same commit and seed
|
||||||
|
- **THEN** both runs MUST pass with no pixel diff
|
||||||
|
|
||||||
|
#### Scenario: Time-dependent content does not cause drift
|
||||||
|
|
||||||
|
- **GIVEN** the clock is frozen and the seed is deterministic
|
||||||
|
- **WHEN** the suite runs on a different calendar day
|
||||||
|
- **THEN** rendered dates MUST remain identical to the baseline
|
||||||
|
|
||||||
|
#### Scenario: Motion is disabled during capture
|
||||||
|
|
||||||
|
- **WHEN** a screenshot is captured
|
||||||
|
- **THEN** CSS animations and transitions MUST be disabled
|
||||||
|
|
||||||
|
### Requirement: Baseline updates are explicit and reviewed
|
||||||
|
|
||||||
|
Baselines SHALL only be updated through the explicit `composer visual:update` command, and the resulting diff MUST be reviewed by a human before merge. Baselines MUST NOT be regenerated automatically to make CI pass.
|
||||||
|
|
||||||
|
#### Scenario: CI does not regenerate baselines
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** it MUST run in assertion mode
|
||||||
|
- **AND** MUST NOT write new baselines
|
||||||
|
|
||||||
|
#### Scenario: Developer updates baselines intentionally
|
||||||
|
|
||||||
|
- **WHEN** a developer runs `composer visual:update`
|
||||||
|
- **THEN** the updated baseline files MUST be written to the versioned baseline directory for review
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial redesign. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
|
- **WHEN** the automated accessibility check reports a critical or serious issue on a covered route
|
||||||
|
- **THEN** the browser suite MUST fail and report the offending rule and selector
|
||||||
|
|
||||||
|
#### Scenario: Covered routes are checked
|
||||||
|
|
||||||
|
- **WHEN** the accessibility suite runs
|
||||||
|
- **THEN** the home, services listing, portfolio listing and case detail MUST each be checked
|
||||||
|
|
||||||
|
### Requirement: Public pages use accessible semantic structure
|
||||||
|
|
||||||
|
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image and brand mark, and visible focus on interactive elements (SPEC §6.5).
|
||||||
|
|
||||||
|
#### Scenario: Single h1 per page
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** exactly one `h1` element MUST be present
|
||||||
|
|
||||||
|
#### Scenario: Landmarks are present
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** `header`, `main`, `nav` and `footer` landmarks MUST be present
|
||||||
|
|
||||||
|
#### Scenario: Content images expose alt text
|
||||||
|
|
||||||
|
- **WHEN** a page renders a cover or gallery image
|
||||||
|
- **THEN** the `alt` attribute MUST contain the stored alt text
|
||||||
|
|
||||||
|
#### Scenario: Brand mark exposes accessible name
|
||||||
|
|
||||||
|
- **WHEN** the public header brand mark is rendered
|
||||||
|
- **THEN** it MUST expose an accessible name identifying Amare Assessoria
|
||||||
|
|
||||||
|
### Requirement: Public pages are fully keyboard operable
|
||||||
|
|
||||||
|
Visitors SHALL be able to reach and activate every interactive element with the keyboard, including the mobile navigation disclosure when visible, with a visible focus indicator and a skip link to the main content.
|
||||||
|
|
||||||
|
#### Scenario: Keyboard reaches the primary CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor navigates the home with the Tab key
|
||||||
|
- **THEN** the primary CTA MUST receive focus with a visible indicator
|
||||||
|
- **AND** activating it with the keyboard MUST navigate to the contact route
|
||||||
|
|
||||||
|
#### Scenario: Skip link bypasses navigation
|
||||||
|
|
||||||
|
- **WHEN** a visitor focuses the first element of a public page
|
||||||
|
- **THEN** a skip link to the main content MUST be available
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu is keyboard operable
|
||||||
|
|
||||||
|
- **WHEN** the mobile menu button is focused and activated with the keyboard
|
||||||
|
- **THEN** the navigation links MUST become reachable by subsequent Tab stops
|
||||||
|
- **AND** the button MUST expose the correct `aria-expanded` state
|
||||||
|
|
||||||
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including editorial hover scales and menu transitions introduced by the redesign.
|
||||||
|
|
||||||
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
|
- **GIVEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
|
- **WHEN** a public page is loaded
|
||||||
|
- **THEN** decorative transitions and animations MUST NOT run
|
||||||
|
|
||||||
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including pages that load the mobile navigation script.
|
||||||
|
|
||||||
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
|
- **WHEN** a covered public route is loaded in the browser suite
|
||||||
|
- **THEN** the console MUST contain no error-level messages
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
## 1. Tokens, fonts and brand assets
|
||||||
|
|
||||||
|
- [x] 1.1 Write failing feature/CSS contract tests (or extend existing token/layout assertions) for Heritage Editorial palette, EB Garamond family, radius 0 and container `1120px`
|
||||||
|
- [x] 1.2 Rewrite `resources/css/tokens.css` + `@theme` mapping in `app.css` to Heritage Editorial; remove card-shadow hierarchy for public content
|
||||||
|
- [x] 1.3 Swap Vite font from Instrument Sans to self-hosted EB Garamond (400/500/600) and update `components/fonts.blade.php` consumption
|
||||||
|
- [x] 1.4 Add optimized logo assets under `public/brand/` (light/dark variants) from the provided lockup without redrawing geometry; add `<x-brand.logo>` with accessible alt
|
||||||
|
- [x] 1.5 Run focused feature/static checks covering tokens/fonts and `npm run build`
|
||||||
|
|
||||||
|
## 2. Site settings and CMS editorial fields
|
||||||
|
|
||||||
|
- [x] 2.1 Add failing feature tests for new typed `site_settings` fields (logo, hero secondary CTA/note, manifesto, method steps, principles) and São Paulo seed city
|
||||||
|
- [x] 2.2 Create migration + model fillable/casts for the new typed columns (JSONB for method steps/principles preferred)
|
||||||
|
- [x] 2.3 Update Filament `ManageSiteSettings` with pt-BR editorial sections and logo alt validation
|
||||||
|
- [x] 2.4 Update `ContentSeeder` / factory defaults with editorial copy and São Paulo; keep optional fields nullable with safe view defaults
|
||||||
|
- [x] 2.5 Run `composer test:feature` for site-settings coverage
|
||||||
|
|
||||||
|
## 3. Real testimonials
|
||||||
|
|
||||||
|
- [x] 3.1 Add failing tests for multi-paragraph quote rendering and seeder expectations for the five couples from `depoimentos.md`
|
||||||
|
- [x] 3.2 Replace fictional testimonials in content/visual seeders with the real quotes, author names and context/dates
|
||||||
|
- [x] 3.3 Update testimonials Blade to render paragraph breaks; keep unpublished-by-default path documented for production authorization
|
||||||
|
- [x] 3.4 Run testimonials feature tests
|
||||||
|
|
||||||
|
## 4. Public layout and navigation
|
||||||
|
|
||||||
|
- [x] 4.1 Add/extend failing tests for brand mark in header, landmarks, skip link, and mobile menu `aria-expanded` behavior
|
||||||
|
- [x] 4.2 Redesign `layouts/public.blade.php` header/footer for Heritage Editorial (logo, route nav, contact CTA, footer groups)
|
||||||
|
- [x] 4.3 Implement minimal mobile menu toggle in `resources/js/app.js` with reduced-motion safety
|
||||||
|
- [x] 4.4 Run layout/accessibility structure feature tests
|
||||||
|
|
||||||
|
## 5. Home editorial reconstruction
|
||||||
|
|
||||||
|
- [x] 5.1 Update failing `HomePageContentTest` (and related) for new section order: hero → manifesto → services → portfolio → method → testimonials → positioning → final CTA; empty catalog sections omitted; CTA → contact; preserve `data-testid="home-primary-cta"`
|
||||||
|
- [x] 5.2 Rebuild home components/pages to match editorial composition (merge proof+cases into one portfolio block; add manifesto + positioning; four method steps)
|
||||||
|
- [x] 5.3 Wire settings-driven copy and demo-image disclosure note; keep single `h1`
|
||||||
|
- [x] 5.4 Run home feature tests
|
||||||
|
|
||||||
|
## 6. Internal public pages as chapters
|
||||||
|
|
||||||
|
- [x] 6.1 Extend/adjust public page feature tests for services, portfolio index/show, about, contact (presentation-only), privacy and branded errors under the new layout
|
||||||
|
- [x] 6.2 Restyle `pages/services`, `pages/portfolio/*`, `pages/about`, `pages/contact`, `pages/privacy`, `errors/404`, `errors/500` to Heritage Editorial without changing route contracts or inventing proof
|
||||||
|
- [x] 6.3 Confirm contact page shows settings channels and creates no leads
|
||||||
|
- [x] 6.4 Run `PublicPagesTest` and related SEO/N+1 tests
|
||||||
|
|
||||||
|
## 7. Accessibility, browser and visual gates
|
||||||
|
|
||||||
|
- [x] 7.1 Update browser accessibility tests for brand mark name, keyboard path to primary CTA, mobile menu operability, axe on covered routes, reduced motion and clean console
|
||||||
|
- [x] 7.2 Update `VisualContentSeeder` for deterministic Heritage Editorial content (SP + real testimonial subset + fixtures)
|
||||||
|
- [x] 7.3 Run browser a11y/smoke suites; fix regressions
|
||||||
|
- [x] 7.4 Run `composer visual:update` and review desktop `1440×1000` / mobile `390×844` diffs for home, services, portfolio, portfolio detail before committing baselines
|
||||||
|
|
||||||
|
## 8. Quality closeout
|
||||||
|
|
||||||
|
- [x] 8.1 Run `composer pint` and `composer phpstan` on touched PHP
|
||||||
|
- [x] 8.2 Run `composer test:feature` and `composer test:browser`
|
||||||
|
- [x] 8.3 Run `composer quality` (or equivalent full gate) and fix remaining failures
|
||||||
|
- [x] 8.4 Update surface brief / note remaining unresolved items (official contacts, final testimonial authorization, authorized photography) without inventing facts
|
||||||
@@ -15,15 +15,17 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
|||||||
|
|
||||||
- Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
|
- Staging em Dokploy com a mesma imagem FrankenPHP por SHA para web, queue e scheduler.
|
||||||
- Deploy automático em `main` após CI: build → GHCR → Dokploy API → migrate → health → smoke.
|
- Deploy automático em `main` após CI: build → GHCR → Dokploy API → migrate → health → smoke.
|
||||||
- Compose local com app FrankenPHP + Postgres.
|
- Produção via promoção manual da mesma digest SHA (alias `:production`), sem rebuild.
|
||||||
- PHP 8.4 canônico em docs/Docker/CI.
|
- Backup PostgreSQL diário (retenção ≥14 dias), restore documentado e rollback por SHA.
|
||||||
- Gates com `npm audit` e cobertura Domain/Application ≥ 80%.
|
- Compose local com app FrankenPHP + Postgres (pendente fora da fatia deploy).
|
||||||
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1).
|
- PHP 8.4 canônico em docs/Docker/CI (pendente).
|
||||||
- Strict types em PHP próprio faltante.
|
- Gates com `npm audit` e cobertura Domain/Application ≥ 80% (pendente).
|
||||||
|
- E-mail verificado + reset de senha seguros no painel (ADM-01 / SPEC §12.1) (pendente).
|
||||||
|
- Strict types em PHP próprio faltante (pendente).
|
||||||
|
|
||||||
**Non-Goals:**
|
**Non-Goals:**
|
||||||
|
|
||||||
- Produção, promoção humana, provisionamento de VPS para clientes.
|
- Deploy automático em produção; provisionamento de VPS para clientes.
|
||||||
- Fase 2 (briefing/CRM/E2E-01/02).
|
- Fase 2 (briefing/CRM/E2E-01/02).
|
||||||
- Redis, worker mode, CDN automation, signed private media.
|
- Redis, worker mode, CDN automation, signed private media.
|
||||||
|
|
||||||
@@ -31,11 +33,13 @@ Fases 0–1 e provedores de produção (Resend + R2) estão em `main` com CI ver
|
|||||||
|
|
||||||
### D1 — Dokploy Compose na VPS, imagem imutável no GHCR
|
### D1 — Dokploy Compose na VPS, imagem imutável no GHCR
|
||||||
|
|
||||||
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` (ou `compose.update` + `compose.deploy`) no Dokploy com `x-api-key`.
|
GitHub Actions (após CI verde em `main`) constrói **uma** imagem `ghcr.io/<owner>/<repo>:<git-sha>` (+ alias `:staging`), faz push privado e chama `POST /api/compose.deploy` no Dokploy com `x-api-key`.
|
||||||
|
|
||||||
Compose de staging referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). PostgreSQL é serviço Dokploy separado (não na imagem da app).
|
Compose compartilhado (`docker-compose.deploy.yml`) referencia `${APP_IMAGE}` / `IMAGE_TAG` para `web`, `queue`, `scheduler` e job `migrate` one-shot (`php artisan migrate --force`). Dokploy mantém **duas** stacks Compose (staging e production) com `IMAGE_TAG` distinto. PostgreSQL é serviço Dokploy separado por ambiente (não na imagem da app).
|
||||||
|
|
||||||
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil).
|
Produção: `workflow_dispatch` com SHA + confirmação `PRODUCTION` move alias `:production` para o **mesmo digest** já publicado e dispara `compose.deploy` na stack de produção. Repo privado no GitHub Free não tem required reviewers de Environment; aprovação humana = disparo manual explícito (GitHub Pro opcional depois).
|
||||||
|
|
||||||
|
*Alternativas rejeitadas:* Railway (Pro para GHCR privado + desvio de plataforma); rebuild por serviço no Dokploy (quebra “mesma imagem” SPEC §14.3/§15.2); tag só `:latest` (rollback frágil); deploy automático direto em produção.
|
||||||
|
|
||||||
### D2 — Processos e health
|
### D2 — Processos e health
|
||||||
|
|
||||||
@@ -50,7 +54,11 @@ Healthcheck Docker/Dokploy e smoke pós-deploy usam `GET /up` (sem auth, sem sec
|
|||||||
|
|
||||||
### D3 — Secrets e providers
|
### D3 — Secrets e providers
|
||||||
|
|
||||||
Secrets em GitHub Actions + env Dokploy: `APP_KEY`, DB, `MAIL_MAILER=resend` / `RESEND_API_KEY`, `FILESYSTEM_DISK=r2` / `R2_*`, tokens Dokploy/GHCR. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
GitHub Actions guarda só orquestração: `DOKPLOY_URL`, API key, compose IDs, URLs públicas de smoke. Env Laravel (`APP_KEY`, DB, Resend, R2) vive **somente** no Dokploy, isolado por ambiente. Nunca embeds em layer. Local continua `MAIL_MAILER=log` e disco `public`.
|
||||||
|
|
||||||
|
### D3b — Backup e restore
|
||||||
|
|
||||||
|
PostgreSQL staging/produção: backup diário via Dokploy → destino S3-compatible, retenção mínima 14 dias (SPEC §16.3). Restore documentado e testado em staging antes da primeira promoção a produção.
|
||||||
|
|
||||||
### D4 — Compose local com app
|
### D4 — Compose local com app
|
||||||
|
|
||||||
@@ -74,26 +82,32 @@ Alinhar README, CI (`setup-php` 8.4) e `Dockerfile` `ARG PHP_VERSION=8.4`. Mante
|
|||||||
|
|
||||||
### D8 — Rollback
|
### D8 — Rollback
|
||||||
|
|
||||||
Rollback = apontar Compose staging para tag SHA anterior no GHCR e `compose.deploy`/`redeploy`. Falha de healthcheck impede promoção. Sem rebuild.
|
Rollback = mover alias do ambiente (`:staging` ou `:production`) para tag SHA anterior no GHCR e `compose.deploy`. Falha de healthcheck/smoke impede promoção. Sem rebuild.
|
||||||
|
|
||||||
|
### D9 — Trusted proxies atrás do Traefik
|
||||||
|
|
||||||
|
`bootstrap/app.php` confia em proxies (`trustProxies(at: '*')`) para honrar `X-Forwarded-*` do Traefik/Dokploy. Staging/produção usam `SESSION_SECURE_COOKIE=true` com HTTPS.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- **[GHCR privado + pull na VPS]** → configurar registry no Dokploy com PAT `read:packages`; documentar checklist.
|
- **[GHCR privado + pull na VPS]** → configurar registry no Dokploy com PAT `read:packages`; documentar checklist.
|
||||||
- **[Migrate one-shot falha]** → deploy não promove web; manter migrations backward-compatible.
|
- **[Migrate one-shot falha]** → web/queue/scheduler dependem de migrate exit 0; manter migrations backward-compatible.
|
||||||
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain.
|
- **[GitHub Free sem required reviewers]** → promoção humana via `workflow_dispatch` + input `PRODUCTION`; Pro opcional.
|
||||||
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário; sem silenciar sem justificativa.
|
- **[Cobertura 80% com Domain quase vazio]** → medir só namespaces existentes; baseline sobe conforme Fase 2 adiciona Domain (pendente).
|
||||||
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado.
|
- **[npm audit ruido]** → `--omit=dev` + allowlist documentada se necessário (pendente).
|
||||||
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP.
|
- **[E-mail verification em staging]** → seed/users de staging com `email_verified_at`; Resend para reset real quando configurado (pendente).
|
||||||
|
- **[Compose local rebuild lento]** → documentar serve opcional; CI permanece fonte FrankenPHP (pendente).
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
1. Implementar auth/coverage/npm/strict_types/docs/Compose local; CI verde.
|
1. Fatia deploy: Compose deploy, workflows, smoke, trusted proxies, docs Dokploy/backup/rollback; CI verde.
|
||||||
2. Criar projeto Dokploy + Postgres + Compose app; registrar GHCR.
|
2. Criar projeto Dokploy + Postgres (staging + produção) + Compose apps; registrar GHCR.
|
||||||
3. Adicionar workflow deploy; primeiro push de imagem SHA; smoke `/up` + home + login.
|
3. Primeiro push de imagem SHA → staging; smoke `/up` + home + login; testar rollback e backup/restore.
|
||||||
4. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
4. Promoção manual para produção após domínio/TLS/`APP_URL` confirmados.
|
||||||
5. Rollback: redeploy tag SHA anterior.
|
5. Fatias restantes da change (auth/coverage/npm/Compose local/PHP docs) em PRs seguintes.
|
||||||
|
6. Atualizar SPEC §18 Fase 0 apenas com itens comprovados; evidência no PR.
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
- Domínio público exato do staging (DNS) — preencher na implementação com valor do operador.
|
- Domínio público exato do staging/produção (DNS) — preencher na implementação com valor do operador.
|
||||||
- Se Dokploy Compose API exigir `compose.saveEnvironment` para `IMAGE_TAG` a cada deploy: confirmar payload na primeira fatia de integração.
|
- Se Dokploy Compose API exigir `compose.update` env para `IMAGE_TAG` a cada deploy: preferir aliases `:staging`/`:production` estáveis no Compose Dokploy para evitar rewrite de env.
|
||||||
|
|||||||
@@ -6,17 +6,20 @@ Fases 0 e 1 estão implementadas e mescladas, mas o critério de saída da Fase
|
|||||||
|
|
||||||
- Implantar staging na VPS própria via Dokploy (Docker Compose): imagem única por SHA no GHCR, serviços `web`/`queue`/`scheduler`/migrate, PostgreSQL gerenciado, healthcheck `/up`, smoke pós-deploy e rollback por tag SHA anterior.
|
- Implantar staging na VPS própria via Dokploy (Docker Compose): imagem única por SHA no GHCR, serviços `web`/`queue`/`scheduler`/migrate, PostgreSQL gerenciado, healthcheck `/up`, smoke pós-deploy e rollback por tag SHA anterior.
|
||||||
- Adicionar workflow GitHub Actions de deploy em `main` após CI verde (build → push GHCR → acionar API Dokploy).
|
- Adicionar workflow GitHub Actions de deploy em `main` após CI verde (build → push GHCR → acionar API Dokploy).
|
||||||
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL).
|
- Adicionar promoção manual de produção: mesma digest SHA já publicada, alias `:production`, sem rebuild (`workflow_dispatch` + confirmação explícita).
|
||||||
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação.
|
- Documentar backup PostgreSQL diário (retenção ≥14d), restore e runbook operacional Dokploy/GHCR.
|
||||||
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9).
|
- Estender `docker-compose.yml` local com serviço de aplicação FrankenPHP (além do PostgreSQL) — **ainda pendente** nesta fatia de deploy.
|
||||||
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01).
|
- Fixar PHP **8.4** como versão canônica em Docker, CI e documentação — **ainda pendente**.
|
||||||
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões.
|
- Incluir `npm audit` e cobertura mínima de 80% para `Domain` e `Application` nos gates de qualidade (SPEC §12.6, §13.7, §13.9) — **ainda pendente**.
|
||||||
|
- Exigir e-mail verificado no painel Filament e entregar reset de senha seguro (SPEC §12.1; ADM-01) — **ainda pendente**.
|
||||||
|
- Corrigir `declare(strict_types=1);` em PHP próprio que ainda falte e cobrir regressões — **ainda pendente**.
|
||||||
|
|
||||||
## Non-Goals
|
## Non-Goals
|
||||||
|
|
||||||
Conforme [SPEC.md §4.2](../../SPEC.md):
|
Conforme [SPEC.md §4.2](../../SPEC.md):
|
||||||
|
|
||||||
- Produção com promoção humana, portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
- Deploy automático direto em produção (produção exige promoção humana da mesma imagem).
|
||||||
|
- Portal do cliente, multi-tenancy, Redis, FrankenPHP worker mode.
|
||||||
- Fase 2 (WEB-05 briefing, CRM, E2E-01/E2E-02) — change futura `build-leads-crm` após esta fechar.
|
- Fase 2 (WEB-05 briefing, CRM, E2E-01/E2E-02) — change futura `build-leads-crm` após esta fechar.
|
||||||
- Provisionamento genérico de VPS/Dokploy para clientes finais.
|
- Provisionamento genérico de VPS/Dokploy para clientes finais.
|
||||||
- Templates de e-mail de lead, auditoria completa (ADM-02), documentos privados.
|
- Templates de e-mail de lead, auditoria completa (ADM-02), documentos privados.
|
||||||
@@ -25,19 +28,20 @@ Conforme [SPEC.md §4.2](../../SPEC.md):
|
|||||||
|
|
||||||
### New Capabilities
|
### New Capabilities
|
||||||
|
|
||||||
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback (SPEC §14.3, §15.2, §18 Fase 0).
|
- `staging-deployment`: deploy automático de staging na VPS via Dokploy com imagem imutável por SHA, processos web/queue/scheduler, migração, healthcheck, smoke e rollback; promoção manual da mesma digest para produção (SPEC §14.3, §15.2, §16.3, §18 Fase 0).
|
||||||
|
|
||||||
### Modified Capabilities
|
### Modified Capabilities
|
||||||
|
|
||||||
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging.
|
- `container-runtime`: Compose local com app FrankenPHP; PHP 8.4 canônico; alinhamento da mesma imagem a processos de staging/produção.
|
||||||
- `quality-gates`: `npm audit` no gate; cobertura mínima 80% para Domain/Application; job de deploy staging após CI.
|
- `quality-gates`: `npm audit` no gate; cobertura mínima 80% para Domain/Application; job de deploy staging após CI.
|
||||||
- `health-check`: healthcheck e smoke pós-deploy de staging usam `/up` sem autenticação.
|
- `health-check`: healthcheck e smoke pós-deploy de staging usam `/up` sem autenticação.
|
||||||
- `internal-authentication`: e-mail verificado obrigatório para acesso ao painel; reset de senha seguro disponível (SPEC §12.1, ADM-01).
|
- `internal-authentication`: e-mail verificado obrigatório para acesso ao painel; reset de senha seguro disponível (SPEC §12.1, ADM-01).
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- **Cria**: `docker-compose` de staging (ou extensão), workflow `.github/workflows/deploy-staging.yml`, docs operacionais de Dokploy/GHCR, testes de auth verification/reset e cobertura.
|
- **Cria (fatia deploy)**: `docker-compose.deploy.yml`, workflows `deploy-staging.yml` / `promote-production.yml`, scripts smoke/Dokploy, docs operacionais Dokploy/GHCR/backup/rollback.
|
||||||
- **Altera**: `docker-compose.yml`, `Dockerfile`/docs PHP, `composer.json`/`package.json` scripts, `.github/workflows/ci.yml`, `User`/`AdminPanelProvider`, README, `.env.example`.
|
- **Altera (fatia deploy)**: `bootstrap/app.php` (trusted proxies), `.env.example`, README.
|
||||||
- **Infra (manual)**: projeto Dokploy na VPS, registry GHCR, secrets (`DOKPLOY_*`, `GHCR_*`, DB, `APP_KEY`, Resend/R2).
|
- **Ainda pendente nesta change**: Compose local FrankenPHP, PHP 8.4 docs, npm audit/coverage, auth verification/reset, strict_types.
|
||||||
|
- **Infra (manual)**: projeto Dokploy na VPS (duas stacks), registry GHCR, secrets (`DOKPLOY_*`, DB, `APP_KEY`, Resend/R2), backup S3.
|
||||||
- **Depende de**: specs já arquivadas (`container-runtime`, `quality-gates`, `health-check`, `internal-authentication`, `transactional-email`, `object-storage`).
|
- **Depende de**: specs já arquivadas (`container-runtime`, `quality-gates`, `health-check`, `internal-authentication`, `transactional-email`, `object-storage`).
|
||||||
- **Risco**: secrets e registry privados; mitigações: tokens com escopo mínimo, imagem por SHA, healthcheck antes de promover, rollback por tag anterior.
|
- **Risco**: secrets e registry privados; mitigações: tokens com escopo mínimo, imagem por SHA, healthcheck antes de promover, rollback por tag anterior.
|
||||||
|
|||||||
@@ -55,3 +55,23 @@ Rollback SHALL redeploy a previously published SHA-tagged image without rebuildi
|
|||||||
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
||||||
- **THEN** web, queue, and scheduler MUST run that previous image
|
- **THEN** web, queue, and scheduler MUST run that previous image
|
||||||
- **AND** no source rebuild MUST be required
|
- **AND** no source rebuild MUST be required
|
||||||
|
|
||||||
|
### Requirement: Production promotion reuses the same immutable digest
|
||||||
|
|
||||||
|
Production SHALL be promoted from an already-published SHA-tagged image without rebuilding from source. Promotion MUST require explicit human action (SPEC §14.3).
|
||||||
|
|
||||||
|
#### Scenario: Operator promotes a staging-approved SHA to production
|
||||||
|
|
||||||
|
- **WHEN** the operator confirms promotion of commit SHA `abc123`
|
||||||
|
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `ghcr.io/<owner>/<repo>:abc123`
|
||||||
|
- **AND** MUST NOT rebuild from source for that promotion
|
||||||
|
|
||||||
|
### Requirement: Database backups exist before production cutover
|
||||||
|
|
||||||
|
Staging and production PostgreSQL services SHALL have automated daily backups with retention of at least 14 days, and a documented restore procedure MUST be verified on staging before the first production promotion (SPEC §16.3).
|
||||||
|
|
||||||
|
#### Scenario: Staging restore is proven before production promotion
|
||||||
|
|
||||||
|
- **WHEN** the operator prepares the first production promotion
|
||||||
|
- **THEN** a restore from a staging backup MUST have been documented and successfully tested
|
||||||
|
- **AND** production MUST have daily backup configured with retention of at least 14 days
|
||||||
|
|||||||
@@ -19,23 +19,25 @@
|
|||||||
- [ ] 3.3 Add/adjust unit tests if current Domain/Application coverage is below threshold
|
- [ ] 3.3 Add/adjust unit tests if current Domain/Application coverage is below threshold
|
||||||
- [ ] 3.4 Verify CI `static` and `unit` fail appropriately on intentional audit/coverage breakage in a branch experiment or equivalent proof
|
- [ ] 3.4 Verify CI `static` and `unit` fail appropriately on intentional audit/coverage breakage in a branch experiment or equivalent proof
|
||||||
|
|
||||||
## 4. Staging Compose and Dokploy prep
|
## 4. Staging/production Compose and Dokploy prep
|
||||||
|
|
||||||
- [ ] 4.1 Add versioned staging Compose template (web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG`
|
- [x] 4.1 Add versioned Compose template (`docker-compose.deploy.yml`: web, queue, scheduler, migrate one-shot) parameterized by `APP_IMAGE`/`IMAGE_TAG` for staging and production stacks
|
||||||
- [ ] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres service, Compose import, required env vars (APP_KEY, DB, Resend, R2)
|
- [x] 4.2 Document Dokploy project setup: GHCR registry credentials, Postgres per environment, Compose import, required env vars (APP_KEY, DB, Resend, R2), trusted proxies/session cookies
|
||||||
- [ ] 4.3 Document rollback procedure: redeploy previous SHA tag without rebuild
|
- [x] 4.3 Document rollback procedure: move environment alias to previous SHA and redeploy without rebuild
|
||||||
|
- [x] 4.4 Document PostgreSQL daily backup (≥14d retention), restore procedure, and test restore on staging before first production promotion
|
||||||
|
|
||||||
## 5. Deploy workflow and smoke
|
## 5. Deploy workflow and smoke
|
||||||
|
|
||||||
- [ ] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
- [x] 5.1 Create `.github/workflows/deploy-staging.yml` gated on successful CI on `main`: build image, push `ghcr.io/...:<sha>` + `:staging`, trigger Dokploy `compose.deploy`
|
||||||
- [ ] 5.2 Wire migrate-before-serve (Compose migrate service or Dokploy deploy command) and healthcheck on `/up`
|
- [x] 5.2 Create `.github/workflows/promote-production.yml` (`workflow_dispatch` + confirmation): retag same digest as `:production`, deploy production stack, smoke
|
||||||
- [ ] 5.3 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
- [x] 5.3 Wire migrate-before-serve (Compose migrate service) and healthcheck on `/up`
|
||||||
- [ ] 5.4 Store secrets only in GitHub/Dokploy; ensure no secrets in image layers (reuse container CI check)
|
- [x] 5.4 Add post-deploy smoke script/job for `/up`, `/`, `/admin/login` returning 200
|
||||||
|
- [x] 5.5 Store orchestration secrets only in GitHub; Laravel/DB/R2/Resend only in Dokploy; ensure no secrets in image layers
|
||||||
|
|
||||||
## 6. Phase 0 exit evidence
|
## 6. Phase 0 exit evidence
|
||||||
|
|
||||||
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
||||||
- [ ] 6.2 Verify rollback to previous SHA works once
|
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
||||||
- [ ] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any
|
- [ ] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any
|
||||||
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
||||||
- [ ] 6.5 Report in SPEC §24 format and mark this change ready to archive after merge
|
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks (1–3) also complete
|
||||||
|
|||||||
2
openspec/changes/enhance-public-motion/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-06
|
||||||
51
openspec/changes/enhance-public-motion/design.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
O site público usa Blade/Tailwind e já contém um runtime pequeno em `resources/js/motion.js`: abertura focal da home, `page-open`, reveals com `IntersectionObserver` e progresso do índice. A cobertura e a marcação são parciais, os reveals não têm grupos/direções explícitas e a matriz browser se concentra nas quatro páginas visuais originais. A mudança cruza templates, CSS, JavaScript e testes, mas não envolve estado de aplicação, dados ou dependências.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Tornar a coreografia Heritage Editorial consistente em toda página pública visual.
|
||||||
|
- Manter texto legível durante toda entrada, animando somente `transform` e recorte de mídia.
|
||||||
|
- Garantir enhancement progressivo: estado final imediato sem JavaScript, observer ou com movimento reduzido.
|
||||||
|
- Preservar interação, foco, validação e ausência de overflow em desktop/mobile.
|
||||||
|
- Cobrir marcação, comportamento real, acessibilidade, console e capturas determinísticas.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Não criar transições de rota, parallax, animação contínua ou repetição ao voltar no scroll.
|
||||||
|
- Não mudar copy, dados de clientes, estrutura de layout, CMS, controllers, banco, APIs ou Filament.
|
||||||
|
- Não adicionar biblioteca de animação nem itens de SPEC.md §4.2.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **Contrato declarativo em atributos `data-*`.** Templates usarão `data-motion="page-open"`, `data-motion-beat`, `data-reveal-group`, `data-reveal` e `data-reveal-from="up|left|right"`. CSS define o estado visual e JS apenas ativa classes. Alternativa rejeitada: acoplar seletores a classes de layout, por tornar a coreografia frágil a ajustes visuais.
|
||||||
|
|
||||||
|
2. **Enhancement opt-in pelo elemento raiz.** O HTML inicial permanece no estado final; o runtime adiciona `html[data-motion="enhance"]` somente quando movimento é permitido e os recursos necessários existem. Sem JS, sem `IntersectionObserver`, ou em `reduce`, o atributo não é aplicado/removido e todo conteúdo fica final. Alternativa rejeitada: estado inicial oculto no HTML/CSS, pois pode prender conteúdo fora da tela.
|
||||||
|
|
||||||
|
3. **Transformação sem fade de texto.** Entradas duram cerca de 500 ms com `cubic-bezier(0.16, 1, 0.3, 1)`, deslocamento vertical de 12 px e lateral de 24 px no desktop/16 px no mobile. Grupos recebem stagger de 90 ms limitado a 270 ms. Mídia pode combinar transform com `clip-path`; texto não usa opacidade. Alternativa rejeitada: fade geral, que reduz contraste durante axe e legibilidade percebida.
|
||||||
|
|
||||||
|
4. **Observer de execução única.** Cada reveal intersectado recebe o estado final e é removido do observer. Grupos calculam índice limitado para o delay; depoimentos definem explicitamente `left`/`right` pela coluna, preservando alternância no mobile. Alternativa rejeitada: reanimar em toda rolagem, por criar fadiga e instabilidade.
|
||||||
|
|
||||||
|
5. **Feedback separado das entradas.** CTAs, links, navegação e controles recebem transições curtas (100–200 ms) em cor/transform/borda. `:focus-visible` continua prioritário; alertas e mensagens de erro não entram no contrato de motion. Alternativa rejeitada: feedback via JS, desnecessário para estados CSS.
|
||||||
|
|
||||||
|
6. **Progresso com coalescência por frame.** Eventos de scroll apenas agendam uma atualização por `requestAnimationFrame`; o cálculo existente é preservado. Alternativa rejeitada: calcular em todo evento, que multiplica leituras/escritas durante scroll.
|
||||||
|
|
||||||
|
7. **Testes em camadas.** Feature tests comprovam contratos Blade/CSS/JS e renderização de erros; browser tests comprovam entrada, stagger, direções, reduced motion, fallback, interação, overflow e console. Axe/snapshots passam a incluir sobre, contato, privacidade e 404; demais erros ficam em renderização feature.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Conteúdo pisca entre estado final e início do enhancement] → inicializar no primeiro módulo Vite, limitar transformações a distâncias pequenas e nunca ocultar texto.
|
||||||
|
- [Transforms laterais causam overflow horizontal] → limitar distância por breakpoint, manter recorte no contêiner público e testar `scrollWidth` em ambos viewports.
|
||||||
|
- [Snapshots ficam instáveis] → continuar capturando com `reducedMotion: reduce` e transições desabilitadas.
|
||||||
|
- [Muitos observers/estilos inline] → usar um único observer, custom property de índice limitada e `unobserve` imediato.
|
||||||
|
- [Páginas de erro não carregam o runtime em todos os contextos] → o estado final é o baseline seguro; feature tests cobrem os cinco templates.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Publicar CSS, JS e templates no mesmo bundle/commit; não há migração de dados. Rollback consiste em reverter esses assets e atributos, sem compatibilidade de schema ou limpeza operacional.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma; direção, alternância mobile, execução única e limites de escopo foram aprovados no plano.
|
||||||
31
openspec/changes/enhance-public-motion/proposal.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
O site público já possui uma abertura focal na home, mas o restante da experiência ainda muda de estado de forma desigual entre páginas e seções. WEB-01, WEB-07 e os requisitos transversais de acessibilidade pedem uma coreografia editorial consistente, progressiva e dispensável, sem ocultar conteúdo nem comprometer interação, performance ou movimento reduzido.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Evoluir o contrato de motion do site público com abertura de página, sequência editorial inicial e reveals direcionais executados uma única vez.
|
||||||
|
- Aplicar a coreografia à home, serviços, portfólio, detalhe, sobre, contato, privacidade e páginas de erro 404/419/429/500/503.
|
||||||
|
- Alternar depoimentos entre esquerda e direita, filtrando citações vazias antes de renderizar a sequência.
|
||||||
|
- Adicionar feedback curto e não bloqueante a links, CTAs, navegação e controles de formulário, preservando foco e mensagens de validação.
|
||||||
|
- Garantir estado final imediato sem JavaScript, sem `IntersectionObserver` e com `prefers-reduced-motion: reduce`.
|
||||||
|
- Limitar o progresso/índice da home a uma atualização por frame e ampliar testes de marcação, browser, acessibilidade, console e regressão visual.
|
||||||
|
- Não objetivos: não criar transições entre rotas, alterar conteúdo ou layout estrutural, adicionar biblioteca de animação, modificar CMS/API/banco/Filament, nem introduzir itens fora do MVP listados em SPEC.md §4.2.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `public-site-motion`: Coreografia progressiva, direções, timings, execução única, microfeedback, performance e fallbacks do motion público.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `design-tokens`: Especificar tokens compartilhados de duração, stagger, distância e easing para motion editorial.
|
||||||
|
- `public-site-pages`: Cobrir todas as páginas públicas visuais e templates de erro com o contrato compartilhado de abertura e reveal.
|
||||||
|
- `testimonials`: Filtrar citações vazias e alternar explicitamente a direção de entrada dos depoimentos renderizados.
|
||||||
|
- `visual-regression`: Ampliar a matriz visual para sobre, contato, privacidade e 404, mantendo capturas determinísticas sem animação.
|
||||||
|
- `web-accessibility`: Ampliar axe/console e validar conteúdo imediatamente utilizável com movimento reduzido ou enhancement indisponível.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta somente templates Blade públicos, `resources/js/motion.js`, tokens/regras CSS e testes Pest/Pest Browser. Não altera APIs, banco, controllers, CMS, dependências, endpoints técnicos (`sitemap.xml`, `robots.txt`) ou Filament.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Editorial motion tokens are centralized
|
||||||
|
|
||||||
|
The system SHALL define shared public motion tokens for an approximately 500 ms entrance, short interaction feedback, exponential ease-out, 90 ms stagger capped at 270 ms, 12 px vertical distance, and 24 px desktop / 16 px mobile lateral distance. Public motion CSS MUST consume these tokens instead of duplicating arbitrary values.
|
||||||
|
|
||||||
|
#### Scenario: Motion runtime uses shared values
|
||||||
|
- **WHEN** page openings, reveals, or interaction feedback are styled
|
||||||
|
- **THEN** duration, easing, stagger, and distance MUST be derived from centralized tokens
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public pages use shared progressive motion choreography
|
||||||
|
|
||||||
|
The public site SHALL expose a declarative motion contract using `data-motion="page-open"`, `data-motion-beat`, `data-reveal-group`, `data-reveal`, and `data-reveal-from="up|left|right"`. Home, services, portfolio index/detail, about, contact, privacy, and branded 404/419/429/500/503 pages MUST use the contract without introducing route transitions.
|
||||||
|
|
||||||
|
#### Scenario: Visual public route exposes motion hooks
|
||||||
|
- **WHEN** a visitor loads any visual public route or branded error page
|
||||||
|
- **THEN** the rendered page MUST expose a page opening hook and appropriate reveal hooks
|
||||||
|
|
||||||
|
#### Scenario: Technical endpoints remain outside motion
|
||||||
|
- **WHEN** a visitor requests `sitemap.xml` or `robots.txt`
|
||||||
|
- **THEN** the response MUST NOT depend on the public motion runtime
|
||||||
|
|
||||||
|
### Requirement: Editorial entrance timing and direction are consistent
|
||||||
|
|
||||||
|
Entrances SHALL use shared tokens for an approximately 500 ms duration, 90 ms stagger capped at 270 ms, 12 px vertical distance, and 24 px desktop / 16 px mobile lateral distance. Text MUST remain fully visible; motion MUST use transforms and MAY use media clipping without opacity fades for text.
|
||||||
|
|
||||||
|
#### Scenario: Reveal group computes capped stagger
|
||||||
|
- **WHEN** more than four reveal items are rendered in one group
|
||||||
|
- **THEN** the applied delay MUST NOT exceed 270 ms
|
||||||
|
|
||||||
|
#### Scenario: Text enters without opacity fade
|
||||||
|
- **WHEN** page opening or scroll reveal motion runs
|
||||||
|
- **THEN** text MUST remain visible throughout the transition
|
||||||
|
|
||||||
|
### Requirement: Reveals execute once without blocking interaction
|
||||||
|
|
||||||
|
Each scroll reveal SHALL activate once, reach its final state, and be removed from observation. Visitors MUST be able to activate links and form controls while entrance motion is running.
|
||||||
|
|
||||||
|
#### Scenario: Revealed content leaves observer
|
||||||
|
- **WHEN** a reveal target intersects the configured viewport threshold
|
||||||
|
- **THEN** it MUST receive the final state and be unobserved
|
||||||
|
- **AND** scrolling away and back MUST NOT replay the reveal
|
||||||
|
|
||||||
|
#### Scenario: CTA remains interactive during entrance
|
||||||
|
- **WHEN** a visitor activates a CTA while its entrance is in progress
|
||||||
|
- **THEN** navigation MUST proceed without waiting for animation completion
|
||||||
|
|
||||||
|
### Requirement: Motion enhancement has immediate safe fallbacks
|
||||||
|
|
||||||
|
Without JavaScript, without `IntersectionObserver`, or when `prefers-reduced-motion: reduce` is active, the site SHALL render every motion target immediately in its final usable state. Non-essential animation and transition MUST NOT run under reduced motion.
|
||||||
|
|
||||||
|
#### Scenario: JavaScript is unavailable
|
||||||
|
- **WHEN** a public page renders without executing JavaScript
|
||||||
|
- **THEN** all content MUST be visible and usable in its final state
|
||||||
|
|
||||||
|
#### Scenario: IntersectionObserver is unavailable
|
||||||
|
- **WHEN** JavaScript runs but `IntersectionObserver` is not supported
|
||||||
|
- **THEN** all reveal targets MUST remain in their final state
|
||||||
|
|
||||||
|
#### Scenario: Reduced motion is preferred
|
||||||
|
- **WHEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
|
- **THEN** page openings, reveals, media clips, and non-essential feedback transitions MUST NOT run
|
||||||
|
|
||||||
|
### Requirement: Public interaction feedback is short and accessible
|
||||||
|
|
||||||
|
CTAs, links, navigation, and form controls SHALL provide short feedback using shared tokens while preserving visible `:focus-visible` indication. Alerts and validation messages MUST NOT be animated by this contract.
|
||||||
|
|
||||||
|
#### Scenario: Keyboard focus remains visible
|
||||||
|
- **WHEN** a visitor focuses an interactive element with the keyboard
|
||||||
|
- **THEN** its focus indicator MUST remain visible and MUST NOT be displaced by motion feedback
|
||||||
|
|
||||||
|
#### Scenario: Validation message appears without motion
|
||||||
|
- **WHEN** server validation renders an error message
|
||||||
|
- **THEN** the message MUST appear in its final position without entrance animation
|
||||||
|
|
||||||
|
### Requirement: Home progress updates at most once per animation frame
|
||||||
|
|
||||||
|
Scroll-driven chapter index and progress updates on the home SHALL be coalesced through `requestAnimationFrame`, with at most one pending update per frame.
|
||||||
|
|
||||||
|
#### Scenario: Multiple scroll events occur in one frame
|
||||||
|
- **WHEN** multiple scroll events fire before the next animation frame
|
||||||
|
- **THEN** the home progress calculation MUST execute only once for that frame
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Visual public and error pages expose the shared motion contract
|
||||||
|
|
||||||
|
The system SHALL apply the shared progressive opening and reveal contract to home, services, portfolio index/detail, about, contact, privacy, and branded 404/419/429/500/503 pages. The contract MUST NOT change page copy, content ordering, route behavior, or structural layout.
|
||||||
|
|
||||||
|
#### Scenario: Institutional page uses shared opening
|
||||||
|
- **WHEN** a visitor loads `/sobre`, `/contato`, or `/privacidade`
|
||||||
|
- **THEN** the page MUST expose the shared page opening hook and section reveal hooks
|
||||||
|
|
||||||
|
#### Scenario: Error page remains branded and progressively enhanced
|
||||||
|
- **WHEN** Laravel renders a branded 404, 419, 429, 500, or 503 response
|
||||||
|
- **THEN** the page MUST expose the shared opening hook
|
||||||
|
- **AND** all error guidance and navigation MUST remain immediately usable without enhancement
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote, MUST omit blank/whitespace-only quotes before creating the rendered sequence, and MUST assign alternating `left`/`right` reveal directions according to the remaining rendered column order at desktop and mobile viewports. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
|
- **WHEN** a testimonial has `published_at` null
|
||||||
|
- **THEN** the `published()` scope MUST exclude it
|
||||||
|
|
||||||
|
#### Scenario: Published testimonial is queryable
|
||||||
|
|
||||||
|
- **WHEN** an admin sets `published_at` with required quote and author name
|
||||||
|
- **THEN** the testimonial MUST be included in the `published()` scope
|
||||||
|
|
||||||
|
#### Scenario: Assistant cannot manage testimonials
|
||||||
|
|
||||||
|
- **WHEN** an assistant attempts to access the testimonials Resource
|
||||||
|
- **THEN** access MUST be denied with HTTP 403
|
||||||
|
|
||||||
|
#### Scenario: Featured testimonials are filterable
|
||||||
|
|
||||||
|
- **WHEN** content is queried with featured filter
|
||||||
|
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
||||||
|
|
||||||
|
#### Scenario: Multi-paragraph quotes render as paragraphs
|
||||||
|
|
||||||
|
- **GIVEN** a published testimonial whose quote contains blank-line separated paragraphs
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
#### Scenario: Blank quotes do not affect alternation
|
||||||
|
|
||||||
|
- **GIVEN** the published testimonial collection contains a blank quote between two non-blank quotes
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** the blank testimonial MUST be omitted
|
||||||
|
- **AND** the two rendered testimonials MUST receive alternating `left` and `right` directions based on their rendered order
|
||||||
|
|
||||||
|
#### Scenario: Direction alternation remains on mobile
|
||||||
|
|
||||||
|
- **WHEN** testimonials are viewed at a mobile viewport
|
||||||
|
- **THEN** their explicit `left` and `right` directions MUST be preserved with the reduced mobile distance
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
|
||||||
|
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio, Detalhe do portfólio, Sobre, Contato, Privacidade and branded 404, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
||||||
|
|
||||||
|
#### Scenario: Unintended visual change fails the suite
|
||||||
|
|
||||||
|
- **GIVEN** approved baselines exist
|
||||||
|
- **WHEN** a code change alters the rendering of a covered screen
|
||||||
|
- **THEN** the visual assertion MUST fail and report the diff
|
||||||
|
|
||||||
|
#### Scenario: Both viewports are covered
|
||||||
|
|
||||||
|
- **WHEN** the visual suite runs
|
||||||
|
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
||||||
|
|
||||||
|
#### Scenario: Heritage Editorial identity is captured
|
||||||
|
|
||||||
|
- **WHEN** approved baselines for the home are reviewed after this change
|
||||||
|
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
||||||
|
|
||||||
|
#### Scenario: Motion does not destabilize new baselines
|
||||||
|
|
||||||
|
- **WHEN** Sobre, Contato, Privacidade, or 404 is captured
|
||||||
|
- **THEN** the browser MUST use the final reduced-motion state before asserting the screenshot
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial motion enhancement. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
|
- **WHEN** the automated accessibility check reports a critical or serious issue on a covered route
|
||||||
|
- **THEN** the browser suite MUST fail and report the offending rule and selector
|
||||||
|
|
||||||
|
#### Scenario: Covered routes are checked
|
||||||
|
|
||||||
|
- **WHEN** the accessibility suite runs
|
||||||
|
- **THEN** home, services listing, portfolio listing, case detail, about, contact, privacy and branded 404 MUST each be checked
|
||||||
|
|
||||||
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including page openings, editorial reveals, image hover scales, interaction feedback and menu transitions. All affected content MUST render in its final visible and interactive state.
|
||||||
|
|
||||||
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
|
- **GIVEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
|
- **WHEN** a public page is loaded
|
||||||
|
- **THEN** decorative transitions and animations MUST NOT run
|
||||||
|
- **AND** every motion target MUST be in its final visible and interactive state
|
||||||
|
|
||||||
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including home, services listing, portfolio listing, case detail, about, contact, privacy, branded 404, and pages that load the mobile navigation or motion runtime.
|
||||||
|
|
||||||
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
|
- **WHEN** a covered public route is loaded in the browser suite
|
||||||
|
- **THEN** the console MUST contain no error-level messages
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Motion enhancement failure does not hide or disable content
|
||||||
|
|
||||||
|
The public site SHALL remain readable, keyboard-operable, and interactive when JavaScript is disabled or `IntersectionObserver` is unavailable. Motion targets MUST default to their final state without relying on timeout recovery.
|
||||||
|
|
||||||
|
#### Scenario: JavaScript disabled keeps public page usable
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads a public page with JavaScript disabled
|
||||||
|
- **THEN** headings, copy, navigation, CTAs, and form controls MUST be visible and operable
|
||||||
|
|
||||||
|
#### Scenario: Observer unavailable keeps reveals final
|
||||||
|
|
||||||
|
- **WHEN** the motion runtime executes without `IntersectionObserver`
|
||||||
|
- **THEN** reveal targets MUST remain in the final state
|
||||||
24
openspec/changes/enhance-public-motion/tasks.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
## 1. Motion contract and runtime
|
||||||
|
|
||||||
|
- [x] 1.1 Extend feature tests first for motion tokens, declarative hooks, no-opacity text, safe fallbacks, one-shot observer, capped stagger and requestAnimationFrame coalescing
|
||||||
|
- [x] 1.2 Add centralized motion tokens and CSS states for page openings, grouped directional reveals, media clips and accessible interaction feedback
|
||||||
|
- [x] 1.3 Refactor `resources/js/motion.js` to enhance only when supported, initialize page beats/groups, unobserve revealed targets and coalesce progress updates per frame
|
||||||
|
|
||||||
|
## 2. Public template coverage
|
||||||
|
|
||||||
|
- [x] 2.1 Add failing markup coverage for home, services, portfolio index/detail, about, contact, privacy and branded 404/419/429/500/503 pages
|
||||||
|
- [x] 2.2 Apply page-open, beat, reveal-group and directional reveal hooks across all visual public templates without changing content or layout structure
|
||||||
|
- [x] 2.3 Add failing testimonial coverage, filter blank quotes before the rendered sequence and alternate explicit left/right directions on desktop and mobile
|
||||||
|
|
||||||
|
## 3. Browser and accessibility coverage
|
||||||
|
|
||||||
|
- [x] 3.1 Add browser tests for real opening/reveal state, 90 ms capped stagger, one-shot observation and interaction during motion
|
||||||
|
- [x] 3.2 Add desktop/mobile tests for directional distance, reduced motion final state, no-JS/observer fallback, horizontal overflow and clean console
|
||||||
|
- [x] 3.3 Extend axe and console route matrices to about, contact, privacy and branded 404
|
||||||
|
- [x] 3.4 Extend deterministic desktop/mobile visual snapshots to about, contact, privacy and branded 404 and review the generated diffs
|
||||||
|
|
||||||
|
## 4. Verification and delivery
|
||||||
|
|
||||||
|
- [x] 4.1 Run focused feature/browser tests, build, Pint, isolated-cache PHPStan and strict OpenSpec validation
|
||||||
|
- [x] 4.2 Run `composer quality` and visually review desktop/mobile with normal and reduced motion
|
||||||
|
- [ ] 4.3 Stage only scoped files, commit conventionally, open a PR with screenshots and verification evidence, watch CI to green, merge and remove the worktree
|
||||||
@@ -2,7 +2,7 @@ schema: spec-driven
|
|||||||
|
|
||||||
context: |
|
context: |
|
||||||
Fonte de verdade: SPEC.md na raiz. Precedência: instrução do dono do produto > SPEC.md > ADRs > testes > convenções.
|
Fonte de verdade: SPEC.md na raiz. Precedência: instrução do dono do produto > SPEC.md > ADRs > testes > convenções.
|
||||||
Produto: plataforma de assessoria de eventos, single-tenant, MVP. UI em pt-BR, timezone America/Fortaleza, BRL.
|
Produto: plataforma de assessoria de eventos, single-tenant, MVP. UI em pt-BR, timezone America/Sao_Paulo, atuação em São Paulo (capital), BRL.
|
||||||
Stack: Laravel 13, Filament 5 (/admin), Livewire 4 + Blade + Alpine + Tailwind (site público),
|
Stack: Laravel 13, Filament 5 (/admin), Livewire 4 + Blade + Alpine + Tailwind (site público),
|
||||||
PostgreSQL, FrankenPHP regular mode (sem worker mode), Vite, Pest 4 + Pest Browser, database queue.
|
PostgreSQL, FrankenPHP regular mode (sem worker mode), Vite, Pest 4 + Pest Browser, database queue.
|
||||||
Arquitetura: monólito modular. Interface -> Application (Actions/Queries) -> Domain (Enums/VOs) -> Infrastructure.
|
Arquitetura: monólito modular. Interface -> Application (Actions/Queries) -> Domain (Enums/VOs) -> Infrastructure.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# container-runtime Specification
|
# container-runtime Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
TBD - created by archiving change setup-foundation. Update Purpose after archive.
|
Define the production container image for the application: a reproducible multi-stage FrankenPHP build serving the public directory, run in regular mode as a non-root user, shared by web/queue/scheduler processes, with no secrets in layers and a healthcheck on `/up`.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Production image uses multi-stage FrankenPHP build
|
### Requirement: Production image uses multi-stage FrankenPHP build
|
||||||
|
|
||||||
|
|||||||
@@ -104,3 +104,33 @@ Public content image uploads (Filament FileUpload via `PublicImageUploadRules`)
|
|||||||
|
|
||||||
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
|
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
|
||||||
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`
|
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`
|
||||||
|
|
||||||
|
### Requirement: Brand logo assets are available to the public layout
|
||||||
|
|
||||||
|
The system SHALL provide optimized Amare brand logo assets derived from the official faceted-heart lockup for use in the public header, footer and institutional pages. Assets MUST preserve the original geometry, include an accessible text alternative, and provide variants suitable for light and dark tonal fields. When `site_settings.logo_path` is present, the uploaded logo MUST be used; otherwise the versioned static brand asset MUST be used.
|
||||||
|
|
||||||
|
#### Scenario: Header renders brand mark with alt text
|
||||||
|
|
||||||
|
- **WHEN** any public page is rendered
|
||||||
|
- **THEN** the brand mark image or equivalent MUST expose accessible alternative text identifying Amare Assessoria
|
||||||
|
|
||||||
|
#### Scenario: Dark portfolio field uses a legible logo variant
|
||||||
|
|
||||||
|
- **WHEN** the brand mark is rendered on an olive-deep or otherwise dark public surface
|
||||||
|
- **THEN** the chosen logo variant MUST remain legible against that background
|
||||||
|
|
||||||
|
#### Scenario: Uploaded logo overrides static fallback
|
||||||
|
|
||||||
|
- **GIVEN** an admin has saved `logo_path` and `logo_alt` in site settings
|
||||||
|
- **WHEN** the public layout renders the brand mark
|
||||||
|
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Decorative saturation/contrast treatment for editorial mood MUST be applied via CSS on self-hosted images and MUST NOT introduce external image CDN dependencies that break deterministic visual tests.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
|
|||||||
@@ -1,20 +1,28 @@
|
|||||||
# design-tokens Specification
|
# design-tokens Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
TBD - created by archiving change setup-foundation. Update Purpose after archive.
|
Centralize the public site's design tokens so the interface implements the Heritage Editorial system from DESIGN.md (EB Garamond, 8px rhythm, zero radius, 1120px container, paper/olive/sage/ink palette), honoring reduced motion and WCAG AA contrast without card-shadow hierarchy.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Design tokens are centralized for the public site
|
### Requirement: Design tokens are centralized for the public site
|
||||||
|
|
||||||
The system SHALL define minimum design tokens in a single source consumed by the public site layout and components. Tokens MUST cover typography families, font scale, spacing, border radius, container width, background/text/border/accent/state colors, shadows, and transition duration/easing.
|
The system SHALL define design tokens in a single source consumed by the public site layout and components. Tokens MUST implement the Heritage Editorial system from `DESIGN.md`: typography family EB Garamond (self-hosted), font scale (display/headline/title/body/label), spacing on an 8px rhythm, border radius `0` for interactive and content surfaces, container max width `1120px`, paper/olive/sage/ink color roles, transition duration/easing, and MUST NOT rely on card shadows as a hierarchy mechanism for regular content.
|
||||||
|
|
||||||
#### Scenario: Public layout uses shared tokens
|
#### Scenario: Public layout uses shared Heritage Editorial tokens
|
||||||
|
|
||||||
- **WHEN** a public page is rendered
|
- **WHEN** a public page is rendered
|
||||||
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
- **THEN** visual properties MUST be derived from the centralized token definitions rather than arbitrary inline values
|
||||||
|
- **AND** the primary typeface MUST be EB Garamond (or the declared serif fallback stack)
|
||||||
|
- **AND** public content surfaces MUST use `0` border radius from tokens
|
||||||
|
|
||||||
|
#### Scenario: Palette commits paper and olive regions
|
||||||
|
|
||||||
|
- **WHEN** the public site is rendered
|
||||||
|
- **THEN** background regions MUST use paper ivory / paper deep / olive deep tokens rather than pure white card stacks on a white page
|
||||||
|
- **AND** primary interactive emphasis MUST use olive heritage (`#556B2F`) / olive deep (`#3E5219`) tokens
|
||||||
|
|
||||||
### Requirement: Public site respects reduced motion preference
|
### Requirement: Public site respects reduced motion preference
|
||||||
|
|
||||||
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site.
|
The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-essential animations and transitions on the public site, including image hover scales and menu transitions.
|
||||||
|
|
||||||
#### Scenario: User prefers reduced motion
|
#### Scenario: User prefers reduced motion
|
||||||
|
|
||||||
@@ -23,10 +31,25 @@ The system SHALL honor `prefers-reduced-motion` by disabling or minimizing non-e
|
|||||||
|
|
||||||
### Requirement: Public site meets baseline accessibility contrast
|
### Requirement: Public site meets baseline accessibility contrast
|
||||||
|
|
||||||
The system SHALL use color combinations on the public site that meet WCAG AA contrast requirements for text and interactive elements defined in the token palette.
|
The system SHALL use Heritage Editorial color combinations that meet WCAG AA contrast requirements for text and interactive elements. Long-form text MUST use ink on paper; sage MUST NOT replace reading color when contrast would fall below AA.
|
||||||
|
|
||||||
#### Scenario: Primary text is readable
|
#### Scenario: Primary text is readable
|
||||||
|
|
||||||
- **WHEN** primary body text is rendered on its background color
|
- **WHEN** primary body text is rendered on its background color
|
||||||
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
#### Scenario: Olive on paper interactive text is readable
|
||||||
|
|
||||||
|
- **WHEN** primary buttons or links use olive tokens on paper backgrounds (or paper text on olive)
|
||||||
|
- **THEN** the contrast ratio MUST meet WCAG AA minimums
|
||||||
|
|
||||||
|
### Requirement: Elevation comes from tonal fields not card shadows
|
||||||
|
|
||||||
|
The public site SHALL express hierarchy through tonal paper fields, 1px botanical rules, and editorial overlap. Regular content components MUST NOT use short grey SaaS card shadows.
|
||||||
|
|
||||||
|
#### Scenario: Content cards omit drop shadows
|
||||||
|
|
||||||
|
- **WHEN** home services, testimonials, or portfolio items are rendered
|
||||||
|
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
||||||
|
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# health-check Specification
|
# health-check Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
TBD - created by archiving change setup-foundation. Update Purpose after archive.
|
Expose a public `GET /up` healthcheck that responds quickly and without authentication, exposes no secrets, and fails when the application cannot boot, so orchestrators and CI can verify availability.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public health endpoint responds without authentication
|
### Requirement: Public health endpoint responds without authentication
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# internal-authentication Specification
|
# internal-authentication Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
TBD - created by archiving change setup-foundation. Update Purpose after archive.
|
Provide session-based authentication for the internal Filament panel at `/admin`, limited to two roles (admin and assistant), with unique emails, password reset without enumeration, inactive users denied, and user management restricted to admins.
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Internal users authenticate via Filament panel
|
### Requirement: Internal users authenticate via Filament panel
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ The system SHALL expose the public routes of SPEC §5.1: `home` (`/`), `services
|
|||||||
|
|
||||||
### Requirement: Home renders the editorial structure from CMS content
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
|
||||||
The home page SHALL render, in the order defined by SPEC §6.2, header/navigation, hero, featured visual proof, services summary, working method, selected cases, testimonials, final briefing CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name and contact data MUST come from `site_settings`; services, cases and testimonials MUST come from published records.
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition (asymmetric spreads on desktop, linear sequence on mobile) rather than rounded card grids.
|
||||||
|
|
||||||
#### Scenario: Published content is displayed in configured order
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
@@ -43,18 +43,21 @@ The home page SHALL render, in the order defined by SPEC §6.2, header/navigatio
|
|||||||
- **WHEN** a visitor loads the home
|
- **WHEN** a visitor loads the home
|
||||||
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
- **AND** the hero MUST show the values stored in `site_settings`
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
|
||||||
#### Scenario: CTA leads to the briefing page
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
- **WHEN** a visitor activates the primary or final CTA on the home
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
- **THEN** the visitor MUST be taken to the `contact` route
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
#### Scenario: Empty content does not break the home
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
- **GIVEN** no published services, cases or testimonials
|
- **GIVEN** no published services, cases or testimonials
|
||||||
- **WHEN** a visitor loads the home
|
- **WHEN** a visitor loads the home
|
||||||
- **THEN** the response MUST be 200
|
- **THEN** the response MUST be 200
|
||||||
- **AND** the affected sections MUST be omitted instead of rendering empty containers
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
#### Scenario: Home has no console errors
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
@@ -63,12 +66,13 @@ The home page SHALL render, in the order defined by SPEC §6.2, header/navigatio
|
|||||||
|
|
||||||
### Requirement: Listing and detail pages exist for catalog content
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
|
||||||
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03). The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
||||||
|
|
||||||
#### Scenario: Services listing shows published services
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
- **WHEN** a visitor loads `/servicos`
|
- **WHEN** a visitor loads `/servicos`
|
||||||
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
#### Scenario: Gallery respects stored order
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
@@ -83,7 +87,7 @@ The system SHALL render a services listing (WEB-02) and a portfolio listing plus
|
|||||||
|
|
||||||
### Requirement: Institutional and error pages have brand identity
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
|
||||||
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07). The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
#### Scenario: Unknown URL renders branded 404
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
@@ -100,7 +104,7 @@ The system SHALL provide the Sobre and Política de privacidade pages and brande
|
|||||||
|
|
||||||
### Requirement: Contact page presents contact data as briefing placeholder
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads in this change.
|
The `contact` route SHALL render the contact page using `site_settings` (e-mail, phone, city, social links) so the home CTA has a valid destination before the briefing form exists. The page MUST NOT create leads and MUST NOT submit a functional briefing form in this change.
|
||||||
|
|
||||||
#### Scenario: Contact page shows configured contact data
|
#### Scenario: Contact page shows configured contact data
|
||||||
|
|
||||||
@@ -117,3 +121,28 @@ Public pages SHALL load related content with explicit eager loading through dedi
|
|||||||
- **WHEN** a case detail page with many gallery images is rendered
|
- **WHEN** a case detail page with many gallery images is rendered
|
||||||
- **THEN** the gallery MUST be loaded with eager loading
|
- **THEN** the gallery MUST be loaded with eager loading
|
||||||
- **AND** the query count MUST NOT grow with the number of images
|
- **AND** the query count MUST NOT grow with the number of images
|
||||||
|
|
||||||
|
### Requirement: Public header exposes brand mark and responsive navigation
|
||||||
|
|
||||||
|
The public layout SHALL render the Amare brand mark (faceted-heart logo lockup or configured logo), primary route navigation, and a contact CTA. On narrow viewports the navigation MUST be operable via a disclosure control with accessible name and `aria-expanded` state.
|
||||||
|
|
||||||
|
#### Scenario: Desktop header shows navigation and CTA
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads any public page at a desktop viewport
|
||||||
|
- **THEN** the header MUST include brand mark, links to home/services/portfolio/about/contact, and a contact CTA
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu toggles accessibly
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the menu button on a narrow viewport
|
||||||
|
- **THEN** the primary navigation MUST become available
|
||||||
|
- **AND** the control MUST expose an updated `aria-expanded` value
|
||||||
|
- **AND** activating a navigation link MUST close the menu
|
||||||
|
|
||||||
|
### Requirement: Demonstrative photography is labeled until authorized assets exist
|
||||||
|
|
||||||
|
When public pages render illustrative/demo photography that is not an authorized Amare asset, the system SHALL mark that imagery as demonstrative in visible copy or accessible labeling so visitors are not misled.
|
||||||
|
|
||||||
|
#### Scenario: Portfolio demo imagery is disclosed
|
||||||
|
|
||||||
|
- **WHEN** the home or portfolio renders placeholder photography
|
||||||
|
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define the typed site-wide settings singleton and its administration rules.
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Site settings singleton is manageable by admin only
|
### Requirement: Site settings singleton is manageable by admin only
|
||||||
|
|
||||||
The system SHALL persist site-wide settings in a `site_settings` table as a typed singleton (SPEC WEB-06, §8.2). Fields MUST include brand name, hero copy (eyebrow, title, subtitle, CTA label), about summary, contact email/phone/city, social links (jsonb), default meta title/description, default OG image path and alt text, and optional analytics fields disabled by default.
|
The system SHALL persist site-wide settings in a `site_settings` table as a typed singleton (SPEC WEB-06, §8.2). Fields MUST include brand name, optional logo path and logo alt text, hero copy (eyebrow, title, subtitle, primary CTA label, optional secondary CTA label, optional hero note), manifesto copy (title, lead, body), method steps (structured typed data for four editorial steps), principles (structured typed list), about summary, contact email/phone/city, social links (jsonb), default meta title/description, default OG image path and alt text, and optional analytics fields disabled by default.
|
||||||
|
|
||||||
#### Scenario: Admin updates site settings
|
#### Scenario: Admin updates site settings
|
||||||
|
|
||||||
@@ -25,8 +25,31 @@ The system SHALL persist site-wide settings in a `site_settings` table as a type
|
|||||||
- **THEN** validation MUST fail with a pt-BR error message
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
- **AND** alt text MUST remain optional when no default OG image is present
|
- **AND** alt text MUST remain optional when no default OG image is present
|
||||||
|
|
||||||
|
#### Scenario: Logo upload requires alt text
|
||||||
|
|
||||||
|
- **WHEN** an admin uploads a brand logo without alt text
|
||||||
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
|
- **AND** alt text MUST remain optional when no logo is uploaded
|
||||||
|
|
||||||
#### Scenario: Singleton avoids generic key-value store
|
#### Scenario: Singleton avoids generic key-value store
|
||||||
|
|
||||||
- **WHEN** site settings are stored
|
- **WHEN** site settings are stored
|
||||||
- **THEN** the system MUST use typed columns on `site_settings`
|
- **THEN** the system MUST use typed columns on `site_settings`
|
||||||
- **AND** MUST NOT introduce a generic key/value configuration table
|
- **AND** MUST NOT introduce a generic key/value configuration table
|
||||||
|
|
||||||
|
#### Scenario: Editorial defaults remain available when optional fields are empty
|
||||||
|
|
||||||
|
- **GIVEN** manifesto, method steps or principles fields are empty
|
||||||
|
- **WHEN** the home is rendered
|
||||||
|
- **THEN** the page MUST still render those sections using safe editorial defaults
|
||||||
|
- **AND** MUST NOT error
|
||||||
|
|
||||||
|
### Requirement: Public geography defaults to São Paulo
|
||||||
|
|
||||||
|
Demo and visual seed content for site settings SHALL present the Amare operating city as São Paulo (capital), matching `PRODUCT.md`, instead of unrelated cities.
|
||||||
|
|
||||||
|
#### Scenario: Seeded settings use São Paulo
|
||||||
|
|
||||||
|
- **WHEN** content seeders populate `site_settings`
|
||||||
|
- **THEN** the city field MUST be São Paulo (or equivalent capital wording)
|
||||||
|
- **AND** MUST NOT present Fortaleza as the operating city
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define testimonial management, publication, authorization, and featured filterin
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Testimonials are managed with publication control
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text, author name, optional context, optional photo with alt text, sort order, featured flag, and `published_at`.
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
#### Scenario: Unpublished testimonial is excluded
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
@@ -27,3 +27,23 @@ The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote te
|
|||||||
|
|
||||||
- **WHEN** content is queried with featured filter
|
- **WHEN** content is queried with featured filter
|
||||||
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
- **THEN** records with `is_featured` true MUST be retrievable independently of sort order
|
||||||
|
|
||||||
|
#### Scenario: Multi-paragraph quotes render as paragraphs
|
||||||
|
|
||||||
|
- **GIVEN** a published testimonial whose quote contains blank-line separated paragraphs
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
||||||
|
|
||||||
|
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
||||||
|
|
||||||
|
#### Scenario: Seed loads the five real couples
|
||||||
|
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** testimonials for Jeniffer e Maick, Quesia e Jhonata, Milena e Weslley, Raquel e Pedro, and Victoria e Pedro MUST exist with their source quotes and marriage context/dates
|
||||||
|
|
||||||
|
#### Scenario: Fictional demo quotes are removed
|
||||||
|
|
||||||
|
- **WHEN** the content seeder completes
|
||||||
|
- **THEN** previously invented placeholder testimonial authors MUST NOT remain as the published demo set
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define deterministic visual baselines for public screens and the explicit baseli
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public screens have desktop and mobile visual baselines
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
|
||||||
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed.
|
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
||||||
|
|
||||||
#### Scenario: Unintended visual change fails the suite
|
#### Scenario: Unintended visual change fails the suite
|
||||||
|
|
||||||
@@ -19,9 +19,14 @@ The system SHALL keep versioned screenshot baselines for the public screens avai
|
|||||||
- **WHEN** the visual suite runs
|
- **WHEN** the visual suite runs
|
||||||
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
||||||
|
|
||||||
|
#### Scenario: Heritage Editorial identity is captured
|
||||||
|
|
||||||
|
- **WHEN** approved baselines for the home are reviewed after this change
|
||||||
|
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
||||||
|
|
||||||
### Requirement: Visual runs are deterministic
|
### Requirement: Visual runs are deterministic
|
||||||
|
|
||||||
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, fonts installed in the image, frozen clock, deterministic seed, animations and transitions disabled, and no dependency on external network.
|
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Fortaleza`, locale `pt-BR`, self-hosted fonts installed/bundled for the suite, frozen clock, deterministic seed (including real testimonial subset and São Paulo settings), animations and transitions disabled, and no dependency on external network.
|
||||||
|
|
||||||
#### Scenario: Repeated run without code change produces no diff
|
#### Scenario: Repeated run without code change produces no diff
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Define automated accessibility checks, semantic structure, keyboard operability,
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public routes have no critical or serious accessibility issues
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8). A critical or serious issue MUST fail the suite.
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial redesign. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
#### Scenario: Critical issue blocks the suite
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ The system SHALL run automated accessibility checks on the public routes covered
|
|||||||
|
|
||||||
### Requirement: Public pages use accessible semantic structure
|
### Requirement: Public pages use accessible semantic structure
|
||||||
|
|
||||||
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image, and visible focus on interactive elements (SPEC §6.5).
|
Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a coherent heading order, alt text on every content image and brand mark, and visible focus on interactive elements (SPEC §6.5).
|
||||||
|
|
||||||
#### Scenario: Single h1 per page
|
#### Scenario: Single h1 per page
|
||||||
|
|
||||||
@@ -37,9 +37,14 @@ Public pages SHALL provide semantic landmarks, exactly one `h1` per page, a cohe
|
|||||||
- **WHEN** a page renders a cover or gallery image
|
- **WHEN** a page renders a cover or gallery image
|
||||||
- **THEN** the `alt` attribute MUST contain the stored alt text
|
- **THEN** the `alt` attribute MUST contain the stored alt text
|
||||||
|
|
||||||
|
#### Scenario: Brand mark exposes accessible name
|
||||||
|
|
||||||
|
- **WHEN** the public header brand mark is rendered
|
||||||
|
- **THEN** it MUST expose an accessible name identifying Amare Assessoria
|
||||||
|
|
||||||
### Requirement: Public pages are fully keyboard operable
|
### Requirement: Public pages are fully keyboard operable
|
||||||
|
|
||||||
Visitors SHALL be able to reach and activate every interactive element with the keyboard, with a visible focus indicator and a skip link to the main content.
|
Visitors SHALL be able to reach and activate every interactive element with the keyboard, including the mobile navigation disclosure when visible, with a visible focus indicator and a skip link to the main content.
|
||||||
|
|
||||||
#### Scenario: Keyboard reaches the primary CTA
|
#### Scenario: Keyboard reaches the primary CTA
|
||||||
|
|
||||||
@@ -52,9 +57,15 @@ Visitors SHALL be able to reach and activate every interactive element with the
|
|||||||
- **WHEN** a visitor focuses the first element of a public page
|
- **WHEN** a visitor focuses the first element of a public page
|
||||||
- **THEN** a skip link to the main content MUST be available
|
- **THEN** a skip link to the main content MUST be available
|
||||||
|
|
||||||
|
#### Scenario: Mobile menu is keyboard operable
|
||||||
|
|
||||||
|
- **WHEN** the mobile menu button is focused and activated with the keyboard
|
||||||
|
- **THEN** the navigation links MUST become reachable by subsequent Tab stops
|
||||||
|
- **AND** the button MUST expose the correct `aria-expanded` state
|
||||||
|
|
||||||
### Requirement: Reduced motion preference is honored
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`.
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including editorial hover scales and menu transitions introduced by the redesign.
|
||||||
|
|
||||||
#### Scenario: Reduced motion disables transitions
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
@@ -64,7 +75,7 @@ The system SHALL suppress non-essential animation and transition when the user a
|
|||||||
|
|
||||||
### Requirement: Public pages emit no console errors
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19).
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including pages that load the mobile navigation script.
|
||||||
|
|
||||||
#### Scenario: Console stays clean on covered routes
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
|
|||||||
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",
|
||||||
|
|||||||
BIN
public/brand/lockup-on-dark.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
public/brand/lockup-on-dark.webp
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/brand/lockup-on-light.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
public/brand/lockup-on-light.webp
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
public/brand/lockup-source.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
public/brand/mark-on-dark.png
Normal file
|
After Width: | Height: | Size: 24 KiB |