Compare commits
7 Commits
feat/gitea
...
research/m
| Author | SHA1 | Date | |
|---|---|---|---|
| 50c5650228 | |||
| 93b680c172 | |||
| ba388942c0 | |||
| f4c2e4ed2f | |||
| e2af1f6a23 | |||
| 68460e061b | |||
| fc1c6177d6 |
19
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
19
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## WHAT
|
||||||
|
|
||||||
|
<!-- What changed? -->
|
||||||
|
|
||||||
|
## WHY
|
||||||
|
|
||||||
|
<!-- Why this change? -->
|
||||||
|
|
||||||
|
## HOW
|
||||||
|
|
||||||
|
<!-- How was it implemented? Include verification. -->
|
||||||
|
|
||||||
|
## Linear Issue
|
||||||
|
|
||||||
|
<!-- Required. Identifier + URL, e.g. MAN-133 https://linear.app/maneco-workspace/issue/MAN-133 -->
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
<!-- Notes for reviewers. Screenshots, follow-ups, out of scope. -->
|
||||||
19
.github/pull_request_template.md
vendored
Normal file
19
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## WHAT
|
||||||
|
|
||||||
|
<!-- What changed? -->
|
||||||
|
|
||||||
|
## WHY
|
||||||
|
|
||||||
|
<!-- Why this change? -->
|
||||||
|
|
||||||
|
## HOW
|
||||||
|
|
||||||
|
<!-- How was it implemented? Include verification. -->
|
||||||
|
|
||||||
|
## Linear Issue
|
||||||
|
|
||||||
|
<!-- Required. Identifier + URL, e.g. MAN-133 https://linear.app/maneco-workspace/issue/MAN-133 -->
|
||||||
|
|
||||||
|
## Comments
|
||||||
|
|
||||||
|
<!-- Notes for reviewers. Screenshots, follow-ups, out of scope. -->
|
||||||
18
AGENTS.md
18
AGENTS.md
@@ -18,7 +18,19 @@ Feature and browser tests require the `amare_test` PostgreSQL database configure
|
|||||||
|
|
||||||
## Worktrees
|
## 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.
|
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`. Include the Linear identifier in the branch name (e.g. `docs/man-132-…`). When the SDLC gate below is green, commit and open the PR without asking. Watch CI until green, then merge. Clean up the worktree with `git worktree remove` after merge.
|
||||||
|
|
||||||
|
## Agent SDLC (do not ask)
|
||||||
|
|
||||||
|
Never ask whether to commit or open a PR. After each complete slice of work, ship it:
|
||||||
|
|
||||||
|
1. **Work is done** in a Linear-linked worktree.
|
||||||
|
2. **Tests are written or updated** for the changed layer. Skip new tests only when the change has no runtime impact (docs, templates, static config).
|
||||||
|
3. **Pre-commit passes** (`composer pint:check` and `composer phpstan`). Do not `--no-verify`. If the hook fails, fix and rerun.
|
||||||
|
4. **Commit** with Conventional Commits. Cite the Linear identifier.
|
||||||
|
5. **Push and open the Gitea PR** (`tea pulls create`) using `.gitea/PULL_REQUEST_TEMPLATE.md` (WHAT / WHY / HOW / Linear Issue / Comments). Attach the PR URL on the Linear issue via `save_issue` `links`. GitHub is the legacy mirror only.
|
||||||
|
|
||||||
|
Do not wait for "pode commitar?" or "abre o PR?".
|
||||||
|
|
||||||
## Git Hooks (husky)
|
## Git Hooks (husky)
|
||||||
|
|
||||||
@@ -41,7 +53,7 @@ Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Tests are verif
|
|||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
History follows Conventional Commit-style subjects, for example `feat: Fase 0 — Fundação`. Use `<type>: <imperative summary>` (`feat`, `fix`, `docs`, `test`, `chore`) and keep commits focused. Pull requests should explain scope, link the relevant issue or OpenSpec requirement, list verification commands, and include screenshots for UI changes. Ensure all CI jobs pass.
|
History follows Conventional Commit-style subjects, for example `feat: Fase 0 — Fundação`. Use `<type>: <imperative summary>` (`feat`, `fix`, `docs`, `test`, `chore`) and keep commits focused. Fill the PR template (WHAT / WHY / HOW / Linear Issue / Comments). **Linear Issue is required** (identifier + URL). An OpenSpec change is extra context, not a substitute. Include verification commands and screenshots for UI changes. Ensure all CI jobs pass. After pre-commit is green, commit and open the PR — do not ask.
|
||||||
|
|
||||||
## Security & Configuration
|
## Security & Configuration
|
||||||
|
|
||||||
@@ -59,6 +71,8 @@ The design system lives in `DESIGN.md` (palette, typography, layout, do's and do
|
|||||||
|
|
||||||
Issues live in Linear, driven through the Linear MCP tools. See `docs/agents/issue-tracker.md` for workspace, team, and tool conventions. The repo ships no `.mcp.json`, so the Linear MCP has to be enabled for the session before those tools exist — if it isn't, report that instead of silently falling back to another tracker.
|
Issues live in Linear, driven through the Linear MCP tools. See `docs/agents/issue-tracker.md` for workspace, team, and tool conventions. The repo ships no `.mcp.json`, so the Linear MCP has to be enabled for the session before those tools exist — if it isn't, report that instead of silently falling back to another tracker.
|
||||||
|
|
||||||
|
**Every piece of work and every PR must be tied to a Linear issue.** Do not create a branch, worktree, or PR until an issue exists (identifier like `MAN-132`). If the user did not give one, search Linear first; if none fits, create it with `save_issue` on team `Maneco-workspace` before starting. Put the identifier in the branch name. Cite identifier + URL in the PR body. After opening the PR, attach the PR URL on the issue via `save_issue` `links`. Do not start untracked work.
|
||||||
|
|
||||||
### Triage labels
|
### Triage labels
|
||||||
|
|
||||||
Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
||||||
|
|||||||
20
app/Application/Data/AboutContent.php
Normal file
20
app/Application/Data/AboutContent.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Application\Data;
|
||||||
|
|
||||||
|
use App\Models\PortfolioCase;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
|
final readonly class AboutContent
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Collection<int, PortfolioCase> $featuredCases
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public SiteSetting $settings,
|
||||||
|
public Collection $featuredCases,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
25
app/Application/Queries/Marketing/GetAboutContent.php
Normal file
25
app/Application/Queries/Marketing/GetAboutContent.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Application\Queries\Marketing;
|
||||||
|
|
||||||
|
use App\Application\Data\AboutContent;
|
||||||
|
use App\Models\PortfolioCase;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
|
||||||
|
final class GetAboutContent
|
||||||
|
{
|
||||||
|
public function __invoke(): AboutContent
|
||||||
|
{
|
||||||
|
return new AboutContent(
|
||||||
|
settings: SiteSetting::instance(),
|
||||||
|
featuredCases: PortfolioCase::query()
|
||||||
|
->published()
|
||||||
|
->where('is_featured', true)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->limit(6)
|
||||||
|
->get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,9 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
if ($settings && filled($settings->about_image_path)) {
|
if ($settings && filled($settings->about_image_path)) {
|
||||||
$paths[] = (string) $settings->about_image_path;
|
$paths[] = (string) $settings->about_image_path;
|
||||||
}
|
}
|
||||||
|
if ($settings && filled($settings->founder_image_path)) {
|
||||||
|
$paths[] = (string) $settings->founder_image_path;
|
||||||
|
}
|
||||||
if ($settings && filled($settings->hero_image_path)) {
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
$paths[] = (string) $settings->hero_image_path;
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,8 +268,10 @@ class ManageSiteSettings extends Page
|
|||||||
->columns(2),
|
->columns(2),
|
||||||
Section::make('Página Sobre')
|
Section::make('Página Sobre')
|
||||||
->schema([
|
->schema([
|
||||||
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem da página Sobre', 'content/about'),
|
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem do hero / Sobre', 'content/about'),
|
||||||
PublicImageUploadRules::altTextField('about_image_alt', 'about_image_path'),
|
PublicImageUploadRules::altTextField('about_image_alt', 'about_image_path'),
|
||||||
|
PublicImageUploadRules::fileUpload('founder_image_path', 'Foto da Michele', 'content/about/founder'),
|
||||||
|
PublicImageUploadRules::altTextField('founder_image_alt', 'founder_image_path'),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
Section::make('SEO padrão')
|
Section::make('SEO padrão')
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ declare(strict_types=1);
|
|||||||
namespace App\Http\Controllers\PublicSite;
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
use App\Application\Data\PageMeta;
|
use App\Application\Data\PageMeta;
|
||||||
|
use App\Application\Queries\Marketing\GetAboutContent;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
|
||||||
final class PageController extends Controller
|
final class PageController extends Controller
|
||||||
{
|
{
|
||||||
public function about(): View
|
public function about(GetAboutContent $getAboutContent): View
|
||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
$content = $getAboutContent();
|
||||||
|
$settings = $content->settings;
|
||||||
|
|
||||||
return view('pages.about', [
|
return view('pages.about', [
|
||||||
|
'content' => $content,
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('about'),
|
canonical: route('about'),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
* @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_path
|
||||||
* @property string|null $about_image_alt
|
* @property string|null $about_image_alt
|
||||||
|
* @property string|null $founder_image_path
|
||||||
|
* @property string|null $founder_image_alt
|
||||||
* @property string|null $hero_image_path
|
* @property string|null $hero_image_path
|
||||||
* @property string|null $hero_image_alt
|
* @property string|null $hero_image_alt
|
||||||
* @property string|null $services_hero_image_path
|
* @property string|null $services_hero_image_path
|
||||||
@@ -47,6 +49,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'about_summary',
|
'about_summary',
|
||||||
'about_image_path',
|
'about_image_path',
|
||||||
'about_image_alt',
|
'about_image_alt',
|
||||||
|
'founder_image_path',
|
||||||
|
'founder_image_alt',
|
||||||
'manifesto_title',
|
'manifesto_title',
|
||||||
'manifesto_lead',
|
'manifesto_lead',
|
||||||
'manifesto_body',
|
'manifesto_body',
|
||||||
|
|||||||
@@ -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('founder_image_path')->nullable()->after('about_image_alt');
|
||||||
|
$table->string('founder_image_alt')->nullable()->after('founder_image_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['founder_image_path', 'founder_image_alt']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -71,6 +71,8 @@ class ContentSeeder extends Seeder
|
|||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
||||||
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
||||||
|
'founder_image_path' => $this->copyFixture('about-image.jpg', 'content/about/founder/michele.jpg'),
|
||||||
|
'founder_image_alt' => 'Michele, da Amare',
|
||||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
'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_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.',
|
'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.',
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class VisualContentSeeder extends Seeder
|
|||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
||||||
'about_image_alt' => 'Imagem editorial da página Sobre',
|
'about_image_alt' => 'Imagem editorial da página Sobre',
|
||||||
|
'founder_image_path' => $this->writeSolidJpeg('visual/about/founder-michele.jpg', 900, 1200, [210, 205, 192]),
|
||||||
|
'founder_image_alt' => 'Michele, da Amare',
|
||||||
'manifesto_title' => 'Sofisticação que também se traduz em organização.',
|
'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_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.',
|
'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.',
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ Issues and specs for this repo live in Linear. Use the Linear MCP tools for all
|
|||||||
- **Link work**: `save_issue` with `project`, `cycle`, `parentId`, `blocks` / `blockedBy` (relations are append-only).
|
- **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`.
|
- **Cross-link to a PR**: attach the PR URL via `save_issue` `links`.
|
||||||
|
|
||||||
|
## Required: issue before work and before PR
|
||||||
|
|
||||||
|
Every branch, worktree, and pull request in this repo must be tied to a Linear issue.
|
||||||
|
|
||||||
|
1. **Starting work:** require an identifier (e.g. `MAN-132`). If the user did not give one, search Linear. If none fits, create the issue with `save_issue` on team `Maneco-workspace` *before* creating a branch or worktree.
|
||||||
|
2. **Branch:** include the identifier (`docs/man-132-…`, `feat/man-126-…`).
|
||||||
|
3. **PR:** cite identifier + URL in the body. An OpenSpec change is extra context, not a substitute.
|
||||||
|
4. **After the PR exists:** attach the PR URL on the issue via `save_issue` `links`. Do this as part of the automatic SDLC in `AGENTS.md` — do not ask before opening the PR.
|
||||||
|
5. If Linear MCP is unavailable, stop and report — do not start untracked work.
|
||||||
|
|
||||||
## When a skill says "publish to the issue tracker"
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
Create a Linear issue with `save_issue` on team `Maneco-workspace`.
|
Create a Linear issue with `save_issue` on team `Maneco-workspace`.
|
||||||
|
|||||||
92
docs/research/man-136-orphan-gitea-merges.md
Normal file
92
docs/research/man-136-orphan-gitea-merges.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# MAN-136 — Inventário de merges órfãos no Gitea
|
||||||
|
|
||||||
|
**Issue:** [MAN-136](https://linear.app/maneco-workspace/issue/MAN-136/research-inventariar-merges-orfaos-no-gitea)
|
||||||
|
**Branch:** `research/man-136-orphan-gitea-merges`
|
||||||
|
**HEAD inventariado:** `origin/main` @ `93b680c`
|
||||||
|
**Data:** 2026-08-13
|
||||||
|
**Escopo:** inventário only — **não** criar issues Linear retroativas.
|
||||||
|
|
||||||
|
## Critério de janela
|
||||||
|
|
||||||
|
**Início inclusivo:** commit `84f6d7c` (`ci: migrate GitHub Actions para Gitea Actions`, 2026-08-12).
|
||||||
|
**Fim:** `HEAD` de `origin/main` (`93b680c`).
|
||||||
|
|
||||||
|
Justificativa: primeiro commit que troca CI/registry/docs para Gitea (`git.hellomanoel.com`). Commits anteriores (GitHub PR #5–#60 era) ficam fora. A fronteira é o SHA da migração de Actions, não a data de criação do repo Gitea.
|
||||||
|
|
||||||
|
## Critério de “issue correspondente”
|
||||||
|
|
||||||
|
Um merge/commit **tem** issue Linear correspondente no projeto **Amare — Lançamento MVP** se **qualquer** for verdade:
|
||||||
|
|
||||||
|
1. Body/assunto do PR ou commit cita identifier `MAN-xxx` desse projeto; ou
|
||||||
|
2. Existe issue no projeto cujo título + critérios de aceite descrevem de forma única a mudança shipped (match funcional 1:1).
|
||||||
|
|
||||||
|
**Não conta como correspondente:**
|
||||||
|
|
||||||
|
- Epic/issue já **Done** antes do merge que “poderia” cobrir trabalho novo (ex.: MAN-102 Done em 2026-08-08 vs remodel `/sobre` em 2026-08-12).
|
||||||
|
- Issue ampla de copy/arquitetura sem AC específico para o ship (ex.: MAN-97).
|
||||||
|
- Mencionar “Gitea” só em issues de template (MAN-133) não cobre a migração de CI.
|
||||||
|
|
||||||
|
Classificação pedida: **órfão shipped** = chegou em `main` sem issue correspondente. Sem criar tickets.
|
||||||
|
|
||||||
|
## Fontes
|
||||||
|
|
||||||
|
| Fonte | Uso |
|
||||||
|
| --- | --- |
|
||||||
|
| `git log origin/main` (`84f6d7c^..HEAD`) | commits da janela |
|
||||||
|
| `tea pr list --state all` + API Gitea `/pulls/{1..4}` | PRs Gitea merged |
|
||||||
|
| Linear `list_issues` project `Amare — Lançamento MVP` (incl. archived) | universo de issues |
|
||||||
|
|
||||||
|
## Matriz PRs Gitea #1–#4
|
||||||
|
|
||||||
|
| PR | Título | Merge SHA | Issue Linear? | Classificação |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| [#1](https://git.hellomanoel.com/manoel-freitas/amare/pulls/1) | feat: remodelar `/sobre` conforme mock editorial | `fc1c617` | Nenhuma (sem citação; MAN-102 já Done; sem issue dedicada) | **órfão shipped** |
|
||||||
|
| [#2](https://git.hellomanoel.com/manoel-freitas/amare/pulls/2) | fix: CTAs de modalidades em `/servicos` via WhatsApp | `e2af1f6` | [MAN-127](https://linear.app/maneco-workspace/issue/MAN-127/ajustar-ctas-consultivos-no-whatsapp-e-briefing-comercial) (Fluxo 1 — CTAs WhatsApp nos cards de modalidade; PR não cita, match por AC) | matched (não órfão) |
|
||||||
|
| [#3](https://git.hellomanoel.com/manoel-freitas/amare/pulls/3) | fix(about): alinhar layout `/sobre` ao ritmo editorial | `ba38894` | Nenhuma (follow-up de #1; sem citação) | **órfão shipped** |
|
||||||
|
| [#4](https://git.hellomanoel.com/manoel-freitas/amare/pulls/4) | docs: require Linear issues and auto-ship PRs | `93b680c` | [MAN-132](https://linear.app/maneco-workspace/issue/MAN-132/exigir-issue-linear-em-todo-trabalho-e-pr) + [MAN-133](https://linear.app/maneco-workspace/issue/MAN-133/adicionar-pr-template-no-gitea-e-no-github) (citados no body) | matched (não órfão) |
|
||||||
|
|
||||||
|
## Órfãos shipped (tabela canônica)
|
||||||
|
|
||||||
|
| Nome descritivo | Evidência | Link |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Remodelar `/sobre` conforme mock editorial | Gitea PR #1 merged | https://git.hellomanoel.com/manoel-freitas/amare/pulls/1 |
|
||||||
|
| Alinhar layout `/sobre` ao ritmo editorial | Gitea PR #3 merged | https://git.hellomanoel.com/manoel-freitas/amare/pulls/3 |
|
||||||
|
| Migração CI GitHub → Gitea Actions (+ follow-ups de runner/registry/staging) | 10 commits direct-push em `main`, **sem** PR Gitea | sem PR — ver série abaixo |
|
||||||
|
|
||||||
|
### Série vizinha sem PR (direct push)
|
||||||
|
|
||||||
|
Todos sem citação `MAN-*` e sem issue de migração Gitea no projeto Amare MVP:
|
||||||
|
|
||||||
|
| SHA | Assunto |
|
||||||
|
| --- | --- |
|
||||||
|
| `84f6d7c` | ci: migrate GitHub Actions para Gitea Actions |
|
||||||
|
| `716196d` | fix: owner path manoel-freitas/amare no registry Gitea |
|
||||||
|
| `1e215ac` | docs: alinhar remotes e registry para Gitea |
|
||||||
|
| `f730e76` | fix: secrets de registry sem prefixo GITEA_ |
|
||||||
|
| `1468a23` | fix: remover actions/cache no Gitea Actions |
|
||||||
|
| `87a0191` | fix: CI Postgres via hostname, sem bind :5432 no host |
|
||||||
|
| `dd82ca2` | fix: paralelizar CI no act_runner |
|
||||||
|
| `91c0d41` | fix: nomes únicos para containers CI no docker.sock compartilhado |
|
||||||
|
| `ac08b5f` | fix: staging deploy via needs no CI, não workflow_run |
|
||||||
|
| `26a68e1` | fix: restaurar deploy-staging via workflow_run (Gitea ≥1.25) |
|
||||||
|
|
||||||
|
Tratados como **um** órfão shipped agregado (mesmo tema ops/CI), não dez tickets potenciais.
|
||||||
|
|
||||||
|
## Não-órfãos na janela (referência)
|
||||||
|
|
||||||
|
- PR #2 → MAN-127 (parcial; issue ainda Todo no Linear — status Linear ≠ presença de issue).
|
||||||
|
- PR #4 → MAN-132 + MAN-133.
|
||||||
|
|
||||||
|
## Ambiguidades registradas
|
||||||
|
|
||||||
|
1. **PR #2 sem citação explícita:** match por AC de MAN-127 (Fluxo 1). Se o inventário MAN-135 exigir citação literal, reclassificar #2 como órfão — este doc **não** faz isso.
|
||||||
|
2. **`/sobre` vs MAN-97 / MAN-98 / MAN-102:** rejeitado como correspondente (Done prévio ou escopo sem AC de remodel layout).
|
||||||
|
3. **Merge commits vs squash:** PR #1 landa como `fc1c617` (sem merge commit separado); PR #3 tem merge `ba38894`. Ambos contam como merges shipped.
|
||||||
|
|
||||||
|
## Resumo executivo
|
||||||
|
|
||||||
|
**3 órfãos shipped** na era pós-migração Gitea:
|
||||||
|
|
||||||
|
1. Remodelar `/sobre` — [PR #1](https://git.hellomanoel.com/manoel-freitas/amare/pulls/1)
|
||||||
|
2. Fix layout `/sobre` — [PR #3](https://git.hellomanoel.com/manoel-freitas/amare/pulls/3)
|
||||||
|
3. Migração CI/Gitea Actions (+ follow-ups) — direct push, sem PR
|
||||||
2
openspec/changes/remodel-about-page/.openspec.yaml
Normal file
2
openspec/changes/remodel-about-page/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-12
|
||||||
27
openspec/changes/remodel-about-page/design.md
Normal file
27
openspec/changes/remodel-about-page/design.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Design: remodel About page
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Mock `amare-sobre(1).html` define composição e copy. O site já tem Heritage Editorial (`tokens.css`, `DESIGN.md`), `about_image_*` no CMS, e `PortfolioCase` featured na home.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **Hero** reusa `about_image_path` com layout split próprio (`x-about.hero`), não o full-viewport `x-public.photo-hero`. Fallback tonal quando sem imagem.
|
||||||
|
2. **Michele** usa `founder_image_*` novos; sem path → coluna tonal, sem request de imagem inventada.
|
||||||
|
3. **Portfólio** lê até 6 casos `published` + `is_featured` via `GetAboutContent` (mesma regra da home).
|
||||||
|
4. **Copy** de pilares/Michele/processo/CTA fixa do mock; `about_summary` alimenta lead/SEO.
|
||||||
|
5. **Visual**: estrutura do mock; radius 0, EB Garamond, botões `btn` existentes — desvios tipográficos/pill do HTML são descartados.
|
||||||
|
|
||||||
|
## Spine
|
||||||
|
|
||||||
|
```
|
||||||
|
PageController::about
|
||||||
|
→ GetAboutContent
|
||||||
|
→ AboutContent
|
||||||
|
→ pages/about.blade.php + x-about.*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Testes que acoplam `/sobre` a `data-photo-hero` full-vh precisam atualizar o contrato.
|
||||||
|
- Home positioning continua usando `about_image_path` — não reaproveitar esse campo como foto da Michele.
|
||||||
26
openspec/changes/remodel-about-page/proposal.md
Normal file
26
openspec/changes/remodel-about-page/proposal.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Remodel página Sobre conforme mock aprovado
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
A rota `/sobre` ainda usa abertura `photo-hero` full-viewport + lista de princípios. O cliente validou o mock `amare-sobre(1).html` com jornada editorial própria: hero split, pilares, bloco Michele, processo, portfólio em destaque e CTA. Sem essa remodelação, a página institucional fica desalinhada da home e do material aprovado.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- `/sobre` recomposta em seções `x-about.*` espelhando a ordem do mock (hero, pilares, Michele, processo, portfólio masonry, CTA).
|
||||||
|
- Novo campo CMS `founder_image_path` / `founder_image_alt` no singleton `site_settings` para a foto da Michele; hero continua em `about_image_path`.
|
||||||
|
- Query `GetAboutContent` + DTO `AboutContent` (featured cases ≤6) no spine Application.
|
||||||
|
- Tokens Heritage Editorial do repo (sem Inter/Cormorant, sem pills do HTML estático).
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- CMS para copy dos pilares/Michele/processo.
|
||||||
|
- Replicar header/footer do HTML estático.
|
||||||
|
- Alterar tokens globais assertados por `HeritageEditorialTokensTest`.
|
||||||
|
- Gerar fotografia da Michele (apenas upload).
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: `/sobre` com composição do mock; motion contract preservado.
|
||||||
|
- `site-settings`: campos `founder_image_path` e `founder_image_alt` administráveis no Filament.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: About page follows approved editorial composition
|
||||||
|
|
||||||
|
The `/sobre` route SHALL render the Heritage Editorial public layout with this section order: split hero (eyebrow “Sobre nós”, title “Sobre a Amare”, lead from `about_summary` or editorial default, CTA to `#michele`), three pillars, founder block `#michele` (Michele), process (“Como trabalhamos”), featured portfolio masonry (up to six published featured cases), and a final proposal CTA. The page MUST use shared motion markers (`data-motion="page-open"`, `data-reveal*`) without route transitions. Principles list MUST NOT appear on `/sobre` (home may still show them). Contact, privacy and error surfaces remain unchanged by this requirement.
|
||||||
|
|
||||||
|
#### Scenario: Visitor sees the mock section order
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/sobre`
|
||||||
|
- **THEN** the response MUST include the headings for Sobre a Amare, the three pillars, Conheça Michele, Como trabalhamos, Portfólio em destaque, and the proposal CTA copy
|
||||||
|
- **AND** MUST NOT render the numbered principles list formerly used on About
|
||||||
|
|
||||||
|
#### Scenario: Founder photo comes from CMS when configured
|
||||||
|
|
||||||
|
- **GIVEN** `site_settings.founder_image_path` is set with alt text
|
||||||
|
- **WHEN** a visitor loads `/sobre`
|
||||||
|
- **THEN** the Michele section MUST render that image with the configured alt
|
||||||
|
- **AND** MUST use eager loading only for the about hero image, not invent a founder asset path when unset
|
||||||
|
|
||||||
|
#### Scenario: About hero uses about_image with tonal fallback
|
||||||
|
|
||||||
|
- **WHEN** `about_image_path` is configured
|
||||||
|
- **THEN** `/sobre` MUST render a split editorial hero with that media (`loading="eager"` and `fetchpriority="high"`)
|
||||||
|
- **WHEN** `about_image_path` is empty
|
||||||
|
- **THEN** `/sobre` MUST render a tonal hero fallback without an empty image request
|
||||||
|
|
||||||
|
#### Scenario: Featured portfolio tiles use real cases
|
||||||
|
|
||||||
|
- **GIVEN** published featured portfolio cases exist
|
||||||
|
- **WHEN** a visitor loads `/sobre`
|
||||||
|
- **THEN** the masonry MUST link to those cases (or the portfolio index)
|
||||||
|
- **AND** MUST NOT invent decorative photography when no cases exist (tonal slots allowed)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
## 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, optional about hero image path and alt text, optional founder image path and alt text (Michele portrait for `/sobre`), 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: Founder image upload requires alt text
|
||||||
|
|
||||||
|
- **WHEN** an admin uploads a founder image without alt text
|
||||||
|
- **THEN** validation MUST fail with a pt-BR error message
|
||||||
|
- **AND** alt text MUST remain optional when no founder image is present
|
||||||
|
|
||||||
|
#### 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
|
||||||
8
openspec/changes/remodel-about-page/tasks.md
Normal file
8
openspec/changes/remodel-about-page/tasks.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Tasks: remodel-about-page
|
||||||
|
|
||||||
|
- [x] 1. Migration + SiteSetting fillable/PHPDoc para `founder_image_path` / `founder_image_alt`
|
||||||
|
- [x] 2. Filament ManageSiteSettings: labels hero Sobre + upload Michele; media variants + seeders
|
||||||
|
- [x] 3. AboutContent DTO + GetAboutContent; PageController injeta query
|
||||||
|
- [x] 4. Componentes `x-about.*` + reescrever `pages/about.blade.php`
|
||||||
|
- [x] 5. Feature tests About + atualizar ImmersivePhotoHero / Media / Motion / PublicPages / Filament alt
|
||||||
|
- [ ] 6. PR verde
|
||||||
16
resources/views/components/about/cta.blade.php
Normal file
16
resources/views/components/about/cta.blade.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
<section aria-labelledby="about-cta-heading" class="border-b border-amare-border bg-amare-bg-deep py-20 md:py-24" data-about-cta data-reveal-group>
|
||||||
|
<div class="container-amare max-w-4xl space-y-6" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||||
|
<h2 id="about-cta-heading" class="text-headline font-medium text-amare-text">Vamos criar algo inesquecível juntos?</h2>
|
||||||
|
<p class="max-w-2xl text-amare-muted">Conte com a Amare para transformar seu evento em uma experiência organizada, cuidadosa e memorável.</p>
|
||||||
|
<div>
|
||||||
|
<a href="{{ route('briefing') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
40
resources/views/components/about/founder.blade.php
Normal file
40
resources/views/components/about/founder.blade.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$hasImage = filled($settings->founder_image_path);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section id="michele" aria-labelledby="founder-heading" class="border-b border-amare-border bg-amare-bg" data-about-founder>
|
||||||
|
<div class="grid min-h-[500px] lg:grid-cols-12" data-reveal-group>
|
||||||
|
@if ($hasImage)
|
||||||
|
<div class="min-h-[420px] overflow-hidden bg-amare-bg-deep max-lg:aspect-[4/5] lg:col-span-6 lg:min-h-full" data-reveal-media>
|
||||||
|
<x-media.image
|
||||||
|
:path="$settings->founder_image_path"
|
||||||
|
:alt="$settings->founder_image_alt ?: 'Michele, da Amare'"
|
||||||
|
sizes="(max-width: 1023px) 100vw, 50vw"
|
||||||
|
class="img-editorial h-full min-h-[420px] w-full object-cover object-[center_35%] lg:min-h-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="flex min-h-[420px] items-center justify-center bg-amare-bg-deep lg:col-span-6 lg:min-h-full" data-founder-tonal>
|
||||||
|
<p class="px-4 text-center text-xs font-semibold uppercase tracking-[0.16em] text-amare-muted">Retrato da Michele</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex flex-col justify-center px-6 py-16 lg:col-span-6 lg:px-[clamp(3rem,6vw,7rem)]" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A pessoa por trás da experiência</p>
|
||||||
|
<h2 id="founder-heading" class="mt-3 text-headline font-medium text-amare-text">Conheça Michele</h2>
|
||||||
|
<p class="mt-5 max-w-[520px] text-lg leading-relaxed text-amare-text">Michele representa o olhar humano da Amare: escuta, organização e presença durante todo o processo de construção do evento.</p>
|
||||||
|
<p class="mt-3.5 max-w-[520px] text-lg leading-relaxed text-amare-text-muted">Seu papel é transformar necessidades e escolhas em um planejamento claro, cuidando dos detalhes sem perder de vista a experiência de quem contrata e de quem participa.</p>
|
||||||
|
<div class="mt-8 flex max-w-[500px] flex-col items-start justify-between gap-4 border border-amare-border bg-amare-bg-deep px-5 py-4 sm:flex-row sm:items-center">
|
||||||
|
<div>
|
||||||
|
<strong class="block text-[2rem] font-normal italic text-amare-text">Michele</strong>
|
||||||
|
<small class="text-xs font-semibold uppercase tracking-[0.08em] text-amare-muted">Assessoria e produção de eventos</small>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('briefing') }}" class="btn btn-outline shrink-0 text-xs font-bold uppercase tracking-[0.11em]">Falar com a Michele</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
50
resources/views/components/about/hero.blade.php
Normal file
50
resources/views/components/about/hero.blade.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
@props([
|
||||||
|
'settings',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$lead = $settings->about_summary ?: 'Eventos que refletem propósito, conectam pessoas e criam memórias.';
|
||||||
|
$hasImage = filled($settings->about_image_path);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section aria-labelledby="about-hero-heading" class="border-b border-amare-border bg-amare-bg" data-about-hero data-motion="page-open">
|
||||||
|
@if ($hasImage)
|
||||||
|
<div class="grid min-h-[440px] lg:grid-cols-12">
|
||||||
|
<div class="flex min-w-0 items-center bg-amare-bg px-6 py-16 lg:col-span-5 lg:px-[clamp(3rem,6vw,7rem)]" data-reveal-group>
|
||||||
|
<div class="w-full max-w-[470px] space-y-6">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||||
|
<h1 id="about-hero-heading" class="max-w-[470px] whitespace-normal break-normal text-hero-spread font-medium text-amare-text" data-motion-beat="title">Sobre a Amare</h1>
|
||||||
|
<p class="max-w-[450px] text-[clamp(1.25rem,2vw,1.55rem)] italic leading-snug text-amare-text-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||||
|
<span class="block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||||
|
<p class="max-w-[430px] text-lg leading-relaxed text-amare-text">A Amare une sensibilidade, organização e atenção aos detalhes para criar experiências bem planejadas, do primeiro encontro à execução do evento.</p>
|
||||||
|
<div data-motion-beat="cta">
|
||||||
|
<a href="#michele" class="btn btn-outline text-xs font-bold uppercase tracking-[0.11em]">Conhecer mais</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-[360px] overflow-hidden bg-amare-bg-deep max-lg:aspect-[4/5] lg:col-span-7 lg:min-h-0" data-motion-beat="media" data-reveal-media>
|
||||||
|
<x-media.image
|
||||||
|
:path="$settings->about_image_path"
|
||||||
|
:alt="$settings->about_image_alt ?: 'Sobre a Amare'"
|
||||||
|
loading="eager"
|
||||||
|
fetchpriority="high"
|
||||||
|
sizes="(max-width: 1023px) 100vw, 58vw"
|
||||||
|
class="img-editorial h-full min-h-[360px] w-full object-cover lg:min-h-[440px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="container-amare py-16 md:py-24" data-tonal-hero>
|
||||||
|
<div class="max-w-[470px] space-y-5" data-motion-beat="heading">
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||||
|
<h1 id="about-hero-heading" class="text-headline font-medium tracking-tight text-amare-text" data-motion-beat="title">Sobre a Amare</h1>
|
||||||
|
<p class="text-lg italic leading-snug text-amare-text-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||||
|
<span class="block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||||
|
<p class="text-lg leading-relaxed text-amare-text">A Amare une sensibilidade, organização e atenção aos detalhes para criar experiências bem planejadas, do primeiro encontro à execução do evento.</p>
|
||||||
|
<div data-motion-beat="cta">
|
||||||
|
<a href="#michele" class="btn btn-outline text-xs font-bold uppercase tracking-[0.11em]">Conhecer mais</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
31
resources/views/components/about/pillars.blade.php
Normal file
31
resources/views/components/about/pillars.blade.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<section aria-label="Pilares da Amare" class="border-b border-amare-border bg-amare-bg" data-about-pillars>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="grid border-l border-t border-amare-border md:grid-cols-3">
|
||||||
|
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<circle cx="18" cy="16" r="6"/>
|
||||||
|
<circle cx="30" cy="16" r="6"/>
|
||||||
|
<path d="M7 37c1-8 5-12 11-12s10 4 11 12M21 37c1-8 5-12 11-12 5 0 9 4 10 12"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Atendimento próximo</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Escuta atenta para compreender prioridades, contexto e expectativas de cada evento.</p>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<rect x="10" y="7" width="28" height="34" rx="2"/>
|
||||||
|
<path d="M17 15h14M17 22h14M17 29h8M29 29h2"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Planejamento minucioso</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Organização clara das etapas para transformar decisões em uma execução consistente.</p>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col gap-4 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path d="M24 40C10 32 8 20 8 12c8 0 14 4 16 11 2-7 8-11 16-11 0 8-2 20-16 28Z"/>
|
||||||
|
<path d="M24 23v17"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.05em] text-amare-text">Execução tranquila</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Cuidado com a operação para que anfitriões e convidados possam viver o momento.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
64
resources/views/components/about/portfolio.blade.php
Normal file
64
resources/views/components/about/portfolio.blade.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@props([
|
||||||
|
'cases',
|
||||||
|
])
|
||||||
|
|
||||||
|
@php
|
||||||
|
$slots = collect([
|
||||||
|
['label' => 'Eventos sociais', 'tall' => true],
|
||||||
|
['label' => 'Celebrações', 'tall' => false],
|
||||||
|
['label' => 'Detalhes', 'tall' => false],
|
||||||
|
['label' => 'Sociais', 'tall' => false],
|
||||||
|
['label' => 'Corporativo', 'tall' => false],
|
||||||
|
['label' => 'Produção', 'tall' => false],
|
||||||
|
]);
|
||||||
|
$cases = $cases->take(6)->values();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section id="portfolio" aria-labelledby="about-portfolio-heading" class="border-b border-amare-border bg-amare-bg" data-about-portfolio>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<div class="grid gap-10 lg:grid-cols-12 lg:items-end" data-reveal data-reveal-from="up">
|
||||||
|
<div class="lg:col-span-5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Seleção de trabalhos</p>
|
||||||
|
<h2 id="about-portfolio-heading" class="mt-3 text-headline font-medium text-amare-text">Portfólio em destaque</h2>
|
||||||
|
</div>
|
||||||
|
<div class="lg:col-span-6 lg:col-start-7">
|
||||||
|
<p class="max-w-xl text-amare-text-muted">Uma composição de eventos sociais e corporativos para apresentar a versatilidade da Amare.</p>
|
||||||
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost mt-5 min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
|
<span class="border-b border-amare-accent pb-1">Ver portfólio completo</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-[14px] sm:grid-cols-2 lg:grid-cols-[1.2fr_0.8fr_0.8fr]" aria-label="Projetos em destaque" data-editorial-portfolio data-reveal-group>
|
||||||
|
@foreach ($slots as $index => $slot)
|
||||||
|
@php
|
||||||
|
$case = $cases->get($index);
|
||||||
|
@endphp
|
||||||
|
<div
|
||||||
|
@class([
|
||||||
|
'overflow-hidden bg-amare-bg-deep',
|
||||||
|
'sm:col-span-2 sm:min-h-[420px] lg:col-span-1 lg:row-span-2 lg:min-h-[634px]' => $slot['tall'],
|
||||||
|
'min-h-[220px] sm:min-h-[310px]' => ! $slot['tall'],
|
||||||
|
])
|
||||||
|
data-reveal
|
||||||
|
data-reveal-from="up"
|
||||||
|
>
|
||||||
|
@if ($case && filled($case->cover_image_path))
|
||||||
|
<a href="{{ route('portfolio.show', $case) }}" class="block h-full w-full" aria-label="{{ $case->title }}">
|
||||||
|
<x-media.image
|
||||||
|
:path="$case->cover_image_path"
|
||||||
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
|
sizes="(max-width: 1023px) 100vw, 33vw"
|
||||||
|
class="img-editorial h-full w-full object-cover transition-opacity hover:opacity-90"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
@else
|
||||||
|
<div class="flex h-full w-full items-center justify-center">
|
||||||
|
<p class="px-4 text-center text-xs font-semibold uppercase tracking-[0.16em] text-amare-muted">{{ $slot['label'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
30
resources/views/components/about/process.blade.php
Normal file
30
resources/views/components/about/process.blade.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<section aria-labelledby="process-heading" class="border-b border-amare-border bg-amare-bg" data-about-process>
|
||||||
|
<div class="container-amare py-16 md:py-24" data-reveal-group>
|
||||||
|
<h2 id="process-heading" class="mb-10 text-center text-headline font-medium text-amare-text" data-reveal data-reveal-from="up">Como trabalhamos</h2>
|
||||||
|
<div class="grid border-l border-t border-amare-border md:grid-cols-3">
|
||||||
|
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path d="M15 29c-5-4-6-12-2-17 5-7 17-7 22 0 4 6 2 14-4 18-2 1-3 4-3 7H20c0-4-2-6-5-8Z"/>
|
||||||
|
<path d="M20 41h8"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Escuta e entendimento</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Começamos pelo contexto do evento, suas prioridades, estilo e expectativas.</p>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<rect x="8" y="11" width="32" height="29" rx="2"/>
|
||||||
|
<path d="M15 7v8M33 7v8M8 20h32M15 26h4M23 26h4M31 26h3M15 33h4M23 33h4"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Planejamento e curadoria</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Organizamos etapas, fornecedores e decisões para manter cada frente alinhada.</p>
|
||||||
|
</article>
|
||||||
|
<article class="flex flex-col gap-3 border-b border-r border-amare-border p-8 text-center md:p-10" data-reveal data-reveal-from="up">
|
||||||
|
<svg class="mx-auto h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path d="M24 39S8 30 8 18c0-6 4-10 10-10 4 0 7 2 9 6 2-4 5-6 9-6 6 0 10 4 10 10 0 12-22 21-22 21Z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-base font-medium uppercase tracking-[0.04em] text-amare-text">Execução e experiência</h3>
|
||||||
|
<p class="mx-auto max-w-[280px] text-amare-text-muted">Coordenamos a operação para que o evento aconteça com fluidez e presença.</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -1,29 +1,10 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
<x-about.hero :settings="$content->settings" />
|
||||||
$principles = filled($siteSettings->principles)
|
<x-about.pillars />
|
||||||
? $siteSettings->principles
|
<x-about.founder :settings="$content->settings" />
|
||||||
: \App\Models\SiteSetting::defaultPrinciples();
|
<x-about.process />
|
||||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
<x-about.portfolio :cases="$content->featuredCases" />
|
||||||
@endphp
|
<x-about.cta :settings="$content->settings" />
|
||||||
|
|
||||||
<x-public.photo-hero :image-path="$siteSettings->about_image_path" :image-alt="$siteSettings->about_image_alt" eyebrow="A Amare" title="Humana no cuidado. Precisa na entrega." :summary="$siteSettings->about_summary">
|
|
||||||
<p class="max-w-xl text-amare-text-muted">A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo, presença no dia do evento e uma condução serena do início ao fim.</p>
|
|
||||||
</x-public.photo-hero>
|
|
||||||
|
|
||||||
<div class="border-b border-amare-border bg-amare-bg">
|
|
||||||
<div class="container-amare py-16 md:py-24">
|
|
||||||
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare" data-reveal-group>
|
|
||||||
@foreach ($principles as $index => $principle)
|
|
||||||
<li class="grid grid-cols-[3rem_minmax(0,1fr)] gap-4 border-b border-amare-border pb-4 text-amare-text" data-reveal data-reveal-from="up">
|
|
||||||
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
|
||||||
<span>{{ $principle }}</span>
|
|
||||||
</li>
|
|
||||||
@endforeach
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<x-home.final-cta :settings="$siteSettings" />
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -47,8 +47,7 @@
|
|||||||
:show-intro="false"
|
:show-intro="false"
|
||||||
kicker="tag"
|
kicker="tag"
|
||||||
:show-subtitle="true"
|
:show-subtitle="true"
|
||||||
cta-route="contact"
|
:band-cta-href="route('briefing')"
|
||||||
:band-cta-href="route('contact')"
|
|
||||||
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
|
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
|
||||||
:show-note="false"
|
:show-note="false"
|
||||||
/>
|
/>
|
||||||
|
|||||||
145
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
145
tests/Feature/PublicSite/AboutPageContentTest.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use App\Filament\Pages\ManageSiteSettings;
|
||||||
|
use App\Models\PortfolioCase;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class AboutPageContentTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_about_page_renders_mock_section_order_without_principles_list(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'about_summary' => 'Eventos que refletem propósito, conectam pessoas e criam memórias.',
|
||||||
|
'principles' => ['Princípio que não deve aparecer no sobre'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get(route('about'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('data-motion="page-open"', false)
|
||||||
|
->assertSee('Sobre nós')
|
||||||
|
->assertSee('Sobre a Amare')
|
||||||
|
->assertSee('Eventos que refletem propósito, conectam pessoas e criam memórias.')
|
||||||
|
->assertSee('Atendimento próximo')
|
||||||
|
->assertSee('Planejamento minucioso')
|
||||||
|
->assertSee('Execução tranquila')
|
||||||
|
->assertSee('id="michele"', false)
|
||||||
|
->assertSee('Conheça Michele')
|
||||||
|
->assertSee('Como trabalhamos')
|
||||||
|
->assertSee('Escuta e entendimento')
|
||||||
|
->assertSee('Portfólio em destaque')
|
||||||
|
->assertSee('Vamos criar algo inesquecível juntos?')
|
||||||
|
->assertDontSee('Princípio que não deve aparecer no sobre')
|
||||||
|
->assertDontSee('Personalização sem complicação desnecessária');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_about_layout_matches_site_editorial_rhythm(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'about_image_path' => 'content/heroes/about.jpg',
|
||||||
|
'about_image_alt' => 'Estúdio Amare',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = $this->get(route('about'))->assertOk()->getContent();
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression(
|
||||||
|
'/data-about-hero[\s\S]*?lg:grid-cols-12[\s\S]*?lg:col-span-5[\s\S]*?text-hero-spread[\s\S]*?lg:col-span-7/',
|
||||||
|
$html,
|
||||||
|
);
|
||||||
|
$this->assertStringNotContainsString('data-photo-hero', $html);
|
||||||
|
$this->assertDoesNotMatchRegularExpression(
|
||||||
|
'/data-about-hero[\s\S]*?min-h-\[calc\(100dvh-5rem\)\]/',
|
||||||
|
$html,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertMatchesRegularExpression('/data-about-pillars[\s\S]*?py-16 md:py-24/', $html);
|
||||||
|
$this->assertMatchesRegularExpression('/data-about-process[\s\S]*?py-16 md:py-24/', $html);
|
||||||
|
$this->assertMatchesRegularExpression('/data-about-portfolio[\s\S]*?data-editorial-portfolio/', $html);
|
||||||
|
$this->assertDoesNotMatchRegularExpression('/data-about-portfolio[\s\S]*?rounded-full/', $html);
|
||||||
|
$this->assertMatchesRegularExpression('/<(?:section)[^>]*(?:data-about-cta[^>]*bg-amare-bg-deep|bg-amare-bg-deep[^>]*data-about-cta)/', $html);
|
||||||
|
$this->assertStringContainsString('Próximo passo', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_about_renders_founder_image_from_cms_when_configured(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'founder_image_path' => 'content/about/founder/michele.jpg',
|
||||||
|
'founder_image_alt' => 'Michele, da Amare',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get(route('about'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('content/about/founder/michele.jpg', false)
|
||||||
|
->assertSee('Michele, da Amare')
|
||||||
|
->assertSee('data-about-founder', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_about_omits_founder_image_request_when_unset(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update([
|
||||||
|
'founder_image_path' => null,
|
||||||
|
'founder_image_alt' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$html = $this->get(route('about'))->assertOk()->getContent();
|
||||||
|
|
||||||
|
$this->assertStringContainsString('data-about-founder', $html);
|
||||||
|
$this->assertStringNotContainsString('content/about/founder/', $html);
|
||||||
|
$this->assertStringContainsString('data-founder-tonal', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_about_portfolio_links_featured_cases(): void
|
||||||
|
{
|
||||||
|
$case = PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Casamento Ana e Lucas',
|
||||||
|
'slug' => 'casamento-ana-lucas',
|
||||||
|
'is_featured' => true,
|
||||||
|
'cover_image_path' => 'content/cases/ana-lucas.jpg',
|
||||||
|
'cover_image_alt' => 'Cerimônia ao ar livre',
|
||||||
|
'sort_order' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
PortfolioCase::factory()->published()->create([
|
||||||
|
'title' => 'Caso não destacado',
|
||||||
|
'slug' => 'nao-destacado',
|
||||||
|
'is_featured' => false,
|
||||||
|
'cover_image_path' => 'content/cases/other.jpg',
|
||||||
|
'sort_order' => 2,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->get(route('about'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee(route('portfolio.show', $case), false)
|
||||||
|
->assertSee('Cerimônia ao ar livre')
|
||||||
|
->assertDontSee(route('portfolio.show', 'nao-destacado'), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_founder_image_upload_requires_alt_text(): void
|
||||||
|
{
|
||||||
|
Storage::fake('public');
|
||||||
|
$this->actingAs(User::factory()->admin()->create());
|
||||||
|
|
||||||
|
Livewire::test(ManageSiteSettings::class)
|
||||||
|
->set('data.founder_image_path', [UploadedFile::fake()->create('michele.jpg', 100, 'image/jpeg')])
|
||||||
|
->set('data.founder_image_alt', null)
|
||||||
|
->call('save')
|
||||||
|
->assertHasFormErrors(['founder_image_alt' => 'required']);
|
||||||
|
|
||||||
|
Livewire::test(ManageSiteSettings::class)
|
||||||
|
->set('data.founder_image_path', null)
|
||||||
|
->set('data.founder_image_alt', null)
|
||||||
|
->call('save')
|
||||||
|
->assertHasNoFormErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,14 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
->assertDontSee('data-split-hero', false)
|
->assertDontSee('data-split-hero', false)
|
||||||
->assertDontSee('content/heroes/home.jpg', false);
|
->assertDontSee('content/heroes/home.jpg', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($route === route('about')) {
|
||||||
|
$response
|
||||||
|
->assertSee('data-motion="page-open"', false)
|
||||||
|
->assertSee('data-about-hero', false)
|
||||||
|
->assertDontSee('data-photo-hero', false)
|
||||||
|
->assertDontSee('min-h-[calc(100dvh-5rem)]', false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
'cover_image_alt' => 'Cerimônia ao ar livre',
|
'cover_image_alt' => 'Cerimônia ao ar livre',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
foreach ([route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug), route('about')] as $route) {
|
foreach ([route('services.index'), route('portfolio.index'), route('portfolio.show', $case->slug)] as $route) {
|
||||||
$this->get($route)
|
$this->get($route)
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('data-photo-hero', false)
|
->assertSee('data-photo-hero', false)
|
||||||
@@ -107,6 +115,15 @@ class ImmersivePhotoHeroTest extends TestCase
|
|||||||
->assertSee('fetchpriority="high"', false)
|
->assertSee('fetchpriority="high"', false)
|
||||||
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false);
|
->assertSee('sizes="(max-width: 1023px) 100vw, 58vw"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->get(route('about'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('data-about-hero', false)
|
||||||
|
->assertSee('content/heroes/about.jpg', false)
|
||||||
|
->assertSee('loading="eager"', false)
|
||||||
|
->assertSee('fetchpriority="high"', false)
|
||||||
|
->assertDontSee('data-photo-hero', false)
|
||||||
|
->assertDontSee('data-tonal-hero', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
public function test_hero_upload_requires_alt_text_only_when_an_image_is_uploaded(): void
|
||||||
|
|||||||
@@ -196,9 +196,11 @@ class PublicPagesTest extends TestCase
|
|||||||
$this->get(route('about'))
|
$this->get(route('about'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Sobre a Amare boutique')
|
->assertSee('Sobre a Amare boutique')
|
||||||
->assertSee('São Paulo - SP')
|
->assertSee('Sobre a Amare')
|
||||||
|
->assertSee('Conheça Michele')
|
||||||
->assertDontSee('Fortaleza')
|
->assertDontSee('Fortaleza')
|
||||||
->assertSee('data-tonal-hero', false);
|
->assertSee('data-tonal-hero', false)
|
||||||
|
->assertSee('data-about-hero', false);
|
||||||
|
|
||||||
$this->get(route('privacy'))
|
$this->get(route('privacy'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
|
|||||||
53
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
53
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ServicesPageCtaTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_package_cta_uses_whatsapp_when_a_number_is_configured(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
|
||||||
|
|
||||||
|
WeddingPackage::factory()->published()->create([
|
||||||
|
'name' => 'Grand Jour',
|
||||||
|
'cta_label' => 'Quero conhecer a Grand Jour',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('services.index'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee(
|
||||||
|
'https://wa.me/5511988887777?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade Grand Jour para meu casamento.'),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
->assertSee('target="_blank"', false)
|
||||||
|
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_package_cta_falls_back_to_briefing_without_whatsapp_number(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update(['whatsapp_number' => null]);
|
||||||
|
|
||||||
|
WeddingPackage::factory()->published()->create([
|
||||||
|
'name' => 'Essenza',
|
||||||
|
'cta_label' => 'Quero conhecer a Essenza',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('services.index'));
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
|
||||||
|
->assertDontSee('wa.me/', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user