Compare commits
8 Commits
feat/remod
...
feat/man-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 70d19a7c55 | |||
| dfd9763029 | |||
| 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. -->
|
||||
20
AGENTS.md
20
AGENTS.md
@@ -18,7 +18,19 @@ Feature and browser tests require the `amare_test` PostgreSQL database configure
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -41,7 +53,7 @@ Tests use Pest 4; browser coverage uses Pest Browser/Playwright. Tests are verif
|
||||
|
||||
## 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
|
||||
|
||||
@@ -59,10 +71,12 @@ 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.
|
||||
|
||||
**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
|
||||
|
||||
Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
|
||||
|
||||
### Domain docs
|
||||
|
||||
Single-context repo. There is no `CONTEXT.md` — the domain is documented in `SPEC.md` (§8 is the domain model and database schema) and `PRODUCT.md`, with current capabilities described per-capability under `openspec/specs/`. `docs/adr/README.md` is an index only: ADR-001 through ADR-010 are decided in `SPEC.md` §21, and there are no standalone ADR files. `docs/agents/domain.md` describes the generic `CONTEXT.md`/`CONTEXT-MAP.md` layout that the engineering skills look for and instructs them to proceed silently when it's absent, which is the case here.
|
||||
Single-context repo. There is no `CONTEXT.md` on purpose — **`SPEC.md` §8.0 is the ubiquitous-language SSOT** (domain model/schema continue in §8.1+). `PRODUCT.md` covers positioning; capabilities live under `openspec/specs/`. `docs/adr/README.md` is an index only: ADR-001 through ADR-010 are decided in `SPEC.md` §21. `docs/agents/domain.md` tells skills to use `SPEC.md` §8 instead of inventing a parallel `CONTEXT.md`.
|
||||
|
||||
@@ -67,7 +67,7 @@ Site-wide content is a singleton row reached via `SiteSetting::instance()`. Publ
|
||||
- **Livewire/Filament temp uploads are pinned to the `local` disk** when `FILESYSTEM_DISK=r2`, because the S3 driver would make the browser PUT straight to R2 and hit CORS. Final media still lands on `r2` via `App\Support\PublicImageUploadRules`. Set `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK` explicitly to override.
|
||||
- **Contact form is rate limited**: named limiter `contact-briefing`, 5/min per IP, registered in `AppServiceProvider` and applied in `routes/web.php`.
|
||||
- **Filament 5 nested resource layout**: resources are split into `app/Filament/Resources/<Resource>/{Pages,Schemas,Tables,RelationManagers}` rather than a flat resource class. Follow the existing shape in `Resources/PortfolioCases/`.
|
||||
- **Everything user-facing is pt-BR**: routes are `/servicos`, `/portfolio`, `/portfolio/{slug}`, `/sobre`, `/privacidade`, `/contato`. `APP_LOCALE=pt_BR`, `APP_TIMEZONE=America/Sao_Paulo` (`config/app.php:68`).
|
||||
- **Everything user-facing is pt-BR**: routes are `/servicos`, `/pacotes/{slug}`, `/portfolio`, `/portfolio/{slug}`, `/sobre`, `/briefing`, `/contato`, `/privacidade`. `APP_LOCALE=pt_BR`, `APP_TIMEZONE=America/Sao_Paulo` (`config/app.php:68`). Glossário canônico: `SPEC.md` §8.0.
|
||||
- **Design tokens** live in `resources/css/tokens.css` (Heritage Editorial; see `DESIGN.md`). `tests/Feature/PublicSite/HeritageEditorialTokensTest.php` reads that file and asserts the exact hex values, `EB Garamond`, zero border radii, `--amare-container-max: 1120px`, and the *absence* of shadow tokens — so any token edit is a deliberate test change too. Motion lives in `resources/js/motion.js` and is asserted by `tests/Feature/PublicSite/MotionMarkupTest.php` + `tests/Browser/MotionTest.php`.
|
||||
|
||||
## Navigating the normative docs
|
||||
|
||||
42
SPEC.md
42
SPEC.md
@@ -283,11 +283,15 @@ Uma funcionalidade fora do MVP só poderá entrar quando:
|
||||
| Método | Rota | Nome sugerido | Finalidade |
|
||||
|---|---|---|---|
|
||||
| GET | `/` | `home` | Home editorial |
|
||||
| GET | `/servicos` | `services.index` | Lista de serviços publicados |
|
||||
| GET | `/servicos` | `services.index` | Lista de serviços / modalidades publicadas |
|
||||
| GET | `/pacotes/{slug}` | `packages.show` | Detalhe de uma modalidade de casamento |
|
||||
| GET | `/portfolio` | `portfolio.index` | Lista de casos publicados |
|
||||
| GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso |
|
||||
| GET | `/sobre` | `about` | História, método e credenciais |
|
||||
| GET | `/contato` | `contact` | Briefing de contato |
|
||||
| GET | `/briefing` | `briefing` | Briefing comercial (pedido de proposta) |
|
||||
| POST | `/briefing` | `briefing.store` | Envio do briefing comercial |
|
||||
| GET | `/contato` | `contact` | Contato de parceiro / fornecedor (não é briefing) |
|
||||
| POST | `/contato` | `contact.store` | Envio da consulta de parceiro |
|
||||
| GET | `/privacidade` | `privacy` | Política de privacidade |
|
||||
| GET | `/sitemap.xml` | `sitemap` | Sitemap público |
|
||||
| GET | `/robots.txt` | `robots` | Política de crawling |
|
||||
@@ -324,7 +328,8 @@ Administração
|
||||
|
||||
- Dashboard: `Filament Page` customizada com widgets orientados a exceção.
|
||||
- Detalhe do evento: página customizada do Resource com resumo operacional.
|
||||
- Briefing público: Blade + Controller (`POST /contato`), ver §11.2.
|
||||
- Briefing comercial: Blade + Controller (`POST /briefing`), ver §11.2.
|
||||
- Contato de parceiro: Blade + Controller (`POST /contato`) — canal separado do briefing.
|
||||
- Home: Blade com componentes de design reutilizáveis.
|
||||
|
||||
### 5.4 Decisões de UX YAGNI
|
||||
@@ -1243,6 +1248,35 @@ Auditoria não precisa ser um event sourcing. Registrar apenas operações crít
|
||||
|
||||
## 8. Modelo de domínio e banco de dados
|
||||
|
||||
Esta seção é a **fonte única de verdade** do vocabulário de domínio do produto. Issues Linear, OpenSpec, código e copy devem usar estes termos. Não criar glossário paralelo em `CONTEXT.md`.
|
||||
|
||||
### 8.0 Linguagem ubíqua (glossário)
|
||||
|
||||
**Vertente**:
|
||||
Frente de marca da Amare — **Casamentos** ou **Corporate**.
|
||||
_Avoid_: linha de negócio, vertical de produto (em copy pública), “área”
|
||||
|
||||
**Modalidade**:
|
||||
Pacote/formato de acompanhamento de casamento gerenciado no CMS (`WeddingPackage`: Essenza, Conduzione, Grand Jour).
|
||||
_Avoid_: pacote (em copy pública preferir “modalidade”), serviço genérico, plano
|
||||
|
||||
**Briefing**:
|
||||
Pedido comercial de um potencial cliente descrevendo o evento e solicitando atendimento/proposta. Rota canônica: `/briefing`.
|
||||
_Avoid_: contato (quando o sentido é pedido comercial), formulário de parceiro
|
||||
|
||||
**Contato parceiro**:
|
||||
Consulta de fornecedor ou proposta de parceria. Rota canônica: `/contato`. Não mistura com Briefing.
|
||||
_Avoid_: briefing, lead comercial
|
||||
|
||||
**CTA contextual**:
|
||||
Link de conversão de uma Modalidade escolhida — WhatsApp com mensagem que nomeia a modalidade, ou fallback para `/briefing?servico_interesse=` quando não há número oficial válido.
|
||||
|
||||
**CTA orientação**:
|
||||
Link de conversão para visitante que ainda não escolheu Modalidade — WhatsApp com mensagem de orientação humana (sem quiz), ou fallback para `/briefing` quando não há número oficial válido.
|
||||
|
||||
**Boutique**:
|
||||
Conceito de posicionamento (cuidado, especialização, múltiplos serviços sob a mesma marca) — não é entidade de domínio nem rota.
|
||||
|
||||
### 8.1 Módulos
|
||||
|
||||
| Módulo | Responsabilidade |
|
||||
@@ -1795,7 +1829,7 @@ Não transformar seções estáticas em componentes Livewire. Usar Blade quando
|
||||
|
||||
### 11.2 Formulário de briefing
|
||||
|
||||
> **Estado atual:** o formulário é implementado em Blade + Controller (`POST /contato`, `ContactBriefingRequest`), conforme WEB-05, e essa é a abordagem aceita — não um estágio provisório. Os requisitos abaixo valem independentemente da tecnologia; a criação de Lead segue para a Fase 2.
|
||||
> **Estado atual:** o formulário de **Briefing** é Blade + Controller (`GET/POST /briefing`, `ContactBriefingRequest`), conforme WEB-05. A rota `/contato` é o canal de **Contato parceiro** (consulta não persistente), separado do briefing. A criação de Lead comercial persistente segue para a Fase 2.
|
||||
|
||||
O formulário deve:
|
||||
|
||||
|
||||
@@ -4,7 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Application\Queries\Marketing;
|
||||
|
||||
use App\Domain\Marketing\PortfolioVertical;
|
||||
use App\Models\PortfolioCase;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
final class GetPublishedPortfolioCases
|
||||
@@ -12,12 +15,37 @@ final class GetPublishedPortfolioCases
|
||||
/**
|
||||
* @return Collection<int, PortfolioCase>
|
||||
*/
|
||||
public function __invoke(): Collection
|
||||
public function __invoke(?PortfolioVertical $vertical = null): Collection
|
||||
{
|
||||
return PortfolioCase::query()
|
||||
return $this->baseQuery($vertical)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LengthAwarePaginator<int, PortfolioCase>
|
||||
*/
|
||||
public function paginate(?PortfolioVertical $vertical = null, int $perPage = 9): LengthAwarePaginator
|
||||
{
|
||||
return $this->baseQuery($vertical)->paginate($perPage)->withQueryString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<PortfolioCase>
|
||||
*/
|
||||
private function baseQuery(?PortfolioVertical $vertical): Builder
|
||||
{
|
||||
$query = PortfolioCase::query()
|
||||
->published()
|
||||
->with(['images'])
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
->orderBy('sort_order');
|
||||
|
||||
if ($vertical !== null) {
|
||||
$query->where(function (Builder $builder) use ($vertical): void {
|
||||
foreach ($vertical->eventTypePatterns() as $pattern) {
|
||||
$builder->orWhere('event_type', 'ilike', $pattern);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
45
app/Domain/Marketing/PortfolioVertical.php
Normal file
45
app/Domain/Marketing/PortfolioVertical.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Domain\Marketing;
|
||||
|
||||
/**
|
||||
* Public portfolio verticals used to separate Casamentos and Corporate proof.
|
||||
*
|
||||
* Matching is intentionally tolerant of free-text `event_type` values already
|
||||
* stored in the CMS (e.g. "Casamento", "Mini wedding", "Corporativo").
|
||||
*/
|
||||
enum PortfolioVertical: string
|
||||
{
|
||||
case Casamentos = 'casamentos';
|
||||
case Corporate = 'corporate';
|
||||
|
||||
public function label(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::Casamentos => 'Casamentos',
|
||||
self::Corporate => 'Corporate',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function eventTypePatterns(): array
|
||||
{
|
||||
return match ($this) {
|
||||
self::Casamentos => ['%casamento%', '%wedding%', '%social%'],
|
||||
self::Corporate => ['%corporat%', '%empresa%', '%business%'],
|
||||
};
|
||||
}
|
||||
|
||||
public static function tryFromQuery(?string $value): ?self
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::tryFrom($value);
|
||||
}
|
||||
}
|
||||
@@ -6,33 +6,35 @@ namespace App\Http\Controllers\PublicSite;
|
||||
|
||||
use App\Application\Data\PageMeta;
|
||||
use App\Application\Queries\Marketing\FindPublishedPortfolioCaseBySlug;
|
||||
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
||||
use App\Domain\Marketing\PortfolioVertical;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
final class PortfolioController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
public function index(Request $request, GetPublishedPortfolioCases $getPublishedPortfolioCases): View
|
||||
{
|
||||
$settings = SiteSetting::instance();
|
||||
|
||||
/** @var LengthAwarePaginator<int, PortfolioCase> $cases */
|
||||
$cases = PortfolioCase::query()
|
||||
->published()
|
||||
->with(['images'])
|
||||
->orderBy('sort_order')
|
||||
->paginate(9);
|
||||
$vertical = PortfolioVertical::tryFromQuery($request->query('vertente'));
|
||||
$cases = $getPublishedPortfolioCases->paginate($vertical);
|
||||
|
||||
return view('pages.portfolio.index', [
|
||||
'cases' => $cases,
|
||||
'vertical' => $vertical,
|
||||
'siteSettings' => $settings,
|
||||
'pageMeta' => PageMeta::forPage(
|
||||
canonical: route('portfolio.index'),
|
||||
canonical: route('portfolio.index', array_filter([
|
||||
'vertente' => $vertical?->value,
|
||||
])),
|
||||
settings: $settings,
|
||||
title: PageMeta::withBrandSuffix('Portfólio', $settings),
|
||||
title: PageMeta::withBrandSuffix(
|
||||
$vertical === null ? 'Portfólio' : 'Portfólio — '.$vertical->label(),
|
||||
$settings,
|
||||
),
|
||||
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -8,20 +8,45 @@ use App\Models\SiteSetting;
|
||||
|
||||
final class PackageContactLink
|
||||
{
|
||||
private const ORIENTATION_MESSAGE = 'Olá, ainda não sei qual modalidade de acompanhamento combina com o meu casamento e gostaria de orientação da Amare.';
|
||||
|
||||
/**
|
||||
* Resolve the contextual contact link for a wedding package modality.
|
||||
*
|
||||
* @return array{href: string, isWhatsapp: bool}
|
||||
*/
|
||||
public static function for(SiteSetting $settings, string $packageName): array
|
||||
{
|
||||
return self::resolve(
|
||||
$settings,
|
||||
'Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.',
|
||||
['servico_interesse' => $packageName],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the orientation CTA for visitors who have not chosen a modality yet.
|
||||
*
|
||||
* @return array{href: string, isWhatsapp: bool}
|
||||
*/
|
||||
public static function forOrientation(SiteSetting $settings): array
|
||||
{
|
||||
return self::resolve($settings, self::ORIENTATION_MESSAGE, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $briefingQuery
|
||||
* @return array{href: string, isWhatsapp: bool}
|
||||
*/
|
||||
private static function resolve(SiteSetting $settings, string $whatsappMessage, array $briefingQuery): array
|
||||
{
|
||||
$digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
|
||||
$isWhatsapp = strlen($digits) >= 10;
|
||||
|
||||
return [
|
||||
'href' => $isWhatsapp
|
||||
? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.')
|
||||
: route('briefing', ['servico_interesse' => $packageName]),
|
||||
? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappMessage)
|
||||
: route('briefing', $briefingQuery),
|
||||
'isWhatsapp' => $isWhatsapp,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,51 +1,28 @@
|
||||
# Domain Docs
|
||||
|
||||
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||
How engineering skills should consume this repo's domain documentation.
|
||||
|
||||
## Before exploring, read these
|
||||
## Single source of truth
|
||||
|
||||
- **`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.
|
||||
**`SPEC.md` §8** (especially **§8.0 Linguagem ubíqua**) is the glossary and domain model for Amare. There is no root `CONTEXT.md` on purpose — do not create one. Prefer `SPEC.md` over Linear copy or OpenSpec prose when terms conflict.
|
||||
|
||||
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.
|
||||
Also useful:
|
||||
|
||||
## File structure
|
||||
- **`PRODUCT.md`** — positioning and audience (not the glossary)
|
||||
- **`openspec/specs/`** — current capability contracts
|
||||
- **`docs/adr/README.md`** — index only; ADR-001–010 live in `SPEC.md` §21
|
||||
|
||||
Single-context repo (most repos):
|
||||
## Before exploring
|
||||
|
||||
```
|
||||
/
|
||||
├── 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/
|
||||
```
|
||||
1. Read `SPEC.md` §8.0 for vocabulary; skim §5.1 for public routes and §8.2+ for persistence shape when relevant.
|
||||
2. If a skill expects `CONTEXT.md` / `CONTEXT-MAP.md` and finds neither, **proceed using `SPEC.md` §8** — do not invent a parallel glossary file.
|
||||
|
||||
## 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.
|
||||
When output names a domain concept (issue title, refactor, hypothesis, test name), use the term as defined in `SPEC.md` §8.0. Don't drift to synonyms the glossary explicitly avoids (`_Avoid_`).
|
||||
|
||||
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`).
|
||||
If the concept isn't in §8.0 yet, either you're inventing language the project doesn't use (reconsider) or there's a real gap — resolve it by updating `SPEC.md` §8.0 via `/grill-with-docs` / domain-modeling, not by adding `CONTEXT.md`.
|
||||
|
||||
## 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…_
|
||||
If output contradicts an ADR in `SPEC.md` §21, surface it explicitly rather than silently overriding.
|
||||
|
||||
@@ -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).
|
||||
- **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"
|
||||
|
||||
Create a Linear issue with `save_issue` on team `Maneco-workspace`.
|
||||
|
||||
@@ -134,7 +134,8 @@
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
[id$='-heading'] {
|
||||
[id$='-heading'],
|
||||
.home-chapter[id] {
|
||||
scroll-margin-top: 5.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
'settings',
|
||||
])
|
||||
|
||||
<section aria-labelledby="about-cta-heading" class="border-b border-amare-border bg-amare-bg py-6 md:py-7" data-about-cta>
|
||||
<div class="container-amare grid items-center gap-7 md:grid-cols-[auto_minmax(0,1fr)_auto]" data-reveal-group>
|
||||
<svg class="mx-auto h-11 w-11 text-amare-accent md:mx-0" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true" data-reveal data-reveal-from="up">
|
||||
<rect x="7" y="12" width="34" height="26" rx="2"/>
|
||||
<path d="m8 15 16 13 16-13"/>
|
||||
<path d="M18 12v-2a6 6 0 0 1 12 0v2"/>
|
||||
</svg>
|
||||
<div class="text-center md:text-left" data-reveal data-reveal-from="up">
|
||||
<h2 id="about-cta-heading" class="text-[clamp(1.75rem,3vw,2rem)] font-medium text-amare-accent-deep">Vamos criar algo inesquecível juntos?</h2>
|
||||
<p class="mt-1 text-base leading-relaxed text-amare-text">Conte com a Amare para transformar seu evento em uma experiência organizada, cuidadosa e memorável.</p>
|
||||
</div>
|
||||
<div class="justify-self-center md:justify-self-end" data-reveal data-reveal-from="up">
|
||||
<a href="{{ route('briefing') }}" class="btn btn-primary text-xs font-bold uppercase tracking-[0.11em]">
|
||||
<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>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
@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-2" data-reveal-group>
|
||||
<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 lg:min-h-full" data-reveal-media>
|
||||
<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'"
|
||||
@@ -18,20 +18,20 @@
|
||||
/>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex min-h-[420px] items-center justify-center bg-amare-bg-deep lg:min-h-full" data-founder-tonal>
|
||||
<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-12 md:px-12 md:py-16 lg:px-[70px]" data-reveal data-reveal-from="up">
|
||||
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent">A pessoa por trás da experiência</p>
|
||||
<h2 id="founder-heading" class="mt-3 text-[clamp(2rem,3vw,2.55rem)] font-medium text-amare-accent-deep">Conheça Michele</h2>
|
||||
<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">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-5 flex max-w-[500px] flex-col items-start justify-between gap-4 bg-amare-bg-deep px-4 py-3.5 sm:flex-row sm:items-center">
|
||||
<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-accent-deep">Michele</strong>
|
||||
<small class="text-[0.65rem] uppercase tracking-[0.08em] text-amare-muted">Assessoria e produção de eventos</small>
|
||||
<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>
|
||||
|
||||
@@ -9,18 +9,20 @@
|
||||
|
||||
<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="container-amare grid min-h-[440px] lg:grid-cols-[minmax(0,0.85fr)_minmax(0,1.45fr)]">
|
||||
<div class="flex flex-col items-start justify-center py-12 md:py-14 lg:py-16 lg:pr-10" data-reveal-group>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent" data-motion-beat="seal">Sobre nós</p>
|
||||
<h1 id="about-hero-heading" class="mt-3 max-w-[470px] text-[clamp(3rem,5vw,5rem)] font-medium leading-[0.96] tracking-tight text-amare-accent-deep" data-motion-beat="title">Sobre a Amare</h1>
|
||||
<p class="mt-4 max-w-[450px] font-serif text-[clamp(1.25rem,2vw,1.55rem)] italic leading-snug text-amare-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||
<span class="my-6 block h-9 w-px bg-amare-accent" aria-hidden="true"></span>
|
||||
<p class="mb-7 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 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 lg:min-h-full" data-motion-beat="media" data-reveal-media>
|
||||
<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'"
|
||||
@@ -34,9 +36,9 @@
|
||||
@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.16em] 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-accent-deep" data-motion-beat="title">Sobre a Amare</h1>
|
||||
<p class="text-lg italic leading-snug text-amare-muted" data-motion-beat="lede">{{ $lead }}</p>
|
||||
<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">
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<section aria-label="Pilares da Amare" class="border-b border-amare-border bg-amare-bg" data-about-pillars>
|
||||
<div class="container-amare" data-reveal-group>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<article class="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto mb-2.5 h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<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-accent-deep">Atendimento próximo</h3>
|
||||
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Escuta atenta para compreender prioridades, contexto e expectativas de cada evento.</p>
|
||||
<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="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-[30px] md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto mb-2.5 h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<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-accent-deep">Planejamento minucioso</h3>
|
||||
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Organização clara das etapas para transformar decisões em uma execução consistente.</p>
|
||||
<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="relative border-t border-amare-border px-6 py-8 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-[30px] md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||
<svg class="mx-auto mb-2.5 h-[34px] w-[34px] text-amare-muted" viewBox="0 0 48 48" fill="none" stroke="currentColor" aria-hidden="true">
|
||||
<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-accent-deep">Execução tranquila</h3>
|
||||
<p class="mx-auto mt-2 max-w-[280px] text-[0.95rem] leading-relaxed text-amare-text">Cuidado com a operação para que anfitriões e convidados possam viver o momento.</p>
|
||||
<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>
|
||||
|
||||
@@ -3,53 +3,58 @@
|
||||
])
|
||||
|
||||
@php
|
||||
$slots = [
|
||||
'Eventos sociais',
|
||||
'Celebrações',
|
||||
'Detalhes',
|
||||
'Sociais',
|
||||
'Corporativo',
|
||||
'Produção',
|
||||
];
|
||||
$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 grid lg:grid-cols-[315px_minmax(0,1fr)]" data-reveal-group>
|
||||
<div class="flex flex-col justify-center px-6 py-11 md:px-10" data-reveal data-reveal-from="up">
|
||||
<p class="text-sm font-medium uppercase tracking-[0.16em] text-amare-accent">Seleção de trabalhos</p>
|
||||
<h2 id="about-portfolio-heading" class="mt-3 text-[clamp(1.75rem,3vw,2.05rem)] font-medium leading-tight text-amare-accent-deep">Portfólio em destaque</h2>
|
||||
<p class="mt-4 max-w-sm text-base leading-relaxed text-amare-text">Uma composição de eventos sociais e corporativos para apresentar a versatilidade da Amare.</p>
|
||||
<a href="{{ route('portfolio.index') }}" class="btn btn-outline mt-5 self-start text-xs font-bold uppercase tracking-[0.11em]">Ver portfólio completo</a>
|
||||
<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="grid auto-rows-[150px] grid-cols-2 gap-1 py-1 lg:auto-rows-[118px] lg:grid-cols-12" aria-label="Projetos em destaque" data-reveal-group>
|
||||
@foreach ($slots as $index => $label)
|
||||
<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);
|
||||
$span = match ($index) {
|
||||
0 => 'lg:col-span-5',
|
||||
1 => 'lg:col-span-4',
|
||||
2 => 'lg:col-span-3 lg:row-span-2 min-h-[190px] lg:min-h-0',
|
||||
3 => 'lg:col-span-4',
|
||||
4 => 'lg:col-span-3',
|
||||
default => 'lg:col-span-2',
|
||||
};
|
||||
@endphp
|
||||
<div @class(['relative min-h-[150px] overflow-hidden bg-amare-bg-deep', $span]) data-reveal data-reveal-from="up">
|
||||
<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) 50vw, 25vw"
|
||||
class="img-editorial h-full w-full object-cover transition-transform duration-[450ms] ease-out hover:scale-[1.03]"
|
||||
sizes="(max-width: 1023px) 100vw, 33vw"
|
||||
class="img-editorial h-full w-full object-cover transition-opacity hover:opacity-90"
|
||||
/>
|
||||
<span class="absolute bottom-3 left-3 bg-amare-accent-deep/90 px-3 py-1.5 text-[0.57rem] font-semibold uppercase tracking-[0.06em] text-amare-accent-text">{{ $label }}</span>
|
||||
</a>
|
||||
@else
|
||||
<div class="flex h-full w-full items-end justify-start p-3">
|
||||
<span class="bg-amare-accent-deep/90 px-3 py-1.5 text-[0.57rem] font-semibold uppercase tracking-[0.06em] text-amare-accent-text">{{ $label }}</span>
|
||||
<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>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
<section aria-labelledby="process-heading" class="border-b border-amare-border bg-amare-bg py-5 md:py-6" data-about-process>
|
||||
<div class="container-amare" data-reveal-group>
|
||||
<h2 id="process-heading" class="mb-3 text-center text-[clamp(1.75rem,3vw,2rem)] font-medium text-amare-accent-deep" data-reveal data-reveal-from="up">Como trabalhamos</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<article class="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-10" data-reveal data-reveal-from="up">
|
||||
<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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Escuta e entendimento</h3>
|
||||
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Começamos pelo contexto do evento, suas prioridades, estilo e expectativas.</p>
|
||||
<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="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-3 md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||
<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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Planejamento e curadoria</h3>
|
||||
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Organizamos etapas, fornecedores e decisões para manter cada frente alinhada.</p>
|
||||
<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="relative border-t border-amare-border px-6 py-4 text-center md:border-t-0 md:px-10 md:before:absolute md:before:inset-y-3 md:before:left-0 md:before:w-px md:before:bg-amare-border" data-reveal data-reveal-from="up">
|
||||
<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="mt-2 text-base font-medium uppercase tracking-[0.04em] text-amare-accent-deep">Execução e experiência</h3>
|
||||
<p class="mx-auto mt-1.5 max-w-[280px] text-[0.94rem] leading-relaxed text-amare-text">Coordenamos a operação para que o evento aconteça com fluidez e presença.</p>
|
||||
<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>
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
])
|
||||
|
||||
@php
|
||||
$bandHref = $bandCtaHref ?? route('briefing');
|
||||
$orientationLink = \App\Support\PackageContactLink::forOrientation($settings);
|
||||
$bandHref = $bandCtaHref ?? $orientationLink['href'];
|
||||
$bandIsWhatsapp = $bandCtaHref === null && $orientationLink['isWhatsapp'];
|
||||
@endphp
|
||||
<section
|
||||
aria-labelledby="packages-heading"
|
||||
@@ -77,7 +79,11 @@
|
||||
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
|
||||
<p class="text-amare-accent-text/80">{{ $bandBody }}</p>
|
||||
</div>
|
||||
<a href="{{ $bandHref }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto">
|
||||
<a
|
||||
href="{{ $bandHref }}"
|
||||
@if ($bandIsWhatsapp) target="_blank" rel="noopener noreferrer" @endif
|
||||
class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto"
|
||||
>
|
||||
Conversar com a Amare
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 backdrop-blur-sm">
|
||||
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr] md:h-[74px] md:py-0">
|
||||
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||
<a href="{{ route('home') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Início</a>
|
||||
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Serviços</a>
|
||||
<a href="{{ url('/#casamentos') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Casamentos</a>
|
||||
<a href="{{ url('/#corporate') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Corporate</a>
|
||||
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Portfólio</a>
|
||||
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Amare</a>
|
||||
<a href="{{ route('briefing') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
||||
@@ -73,7 +73,6 @@
|
||||
<a href="{{ route('briefing') }}" class="btn btn-outline hidden min-h-0 px-6 text-xs font-bold uppercase tracking-[0.09em] md:inline-flex">
|
||||
Conte seu evento
|
||||
</a>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -111,29 +110,39 @@
|
||||
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
||||
@endphp
|
||||
|
||||
<div class="container-amare flex flex-col gap-8 py-14 md:flex-row md:items-end md:justify-between md:py-[58px]">
|
||||
<div class="space-y-3">
|
||||
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
||||
<p class="text-sm text-amare-muted">Assessoria & produção de eventos • São Paulo</p>
|
||||
<div class="container-amare flex flex-col gap-10 py-14 md:py-[58px]">
|
||||
<div class="flex flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
||||
<div class="space-y-3">
|
||||
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
||||
<p class="text-sm text-amare-muted">Boutique de assessoria & produção • São Paulo</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-amare-muted md:text-right">
|
||||
@if ($footerSocials->isEmpty())
|
||||
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
||||
@else
|
||||
@foreach ($footerSocials as $network => $url)
|
||||
<a href="{{ $url }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ $footerSocialLabel((string) $network) }}</a>@unless ($loop->last) · @endunless
|
||||
@endforeach
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-amare-muted md:text-right">
|
||||
@if ($footerSocials->isEmpty())
|
||||
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
||||
@else
|
||||
@foreach ($footerSocials as $network => $url)
|
||||
<a href="{{ $url }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent" rel="noopener noreferrer" target="_blank">{{ $footerSocialLabel((string) $network) }}</a>@unless ($loop->last) · @endunless
|
||||
@endforeach
|
||||
@endif
|
||||
</p>
|
||||
<nav aria-label="Rodapé" class="flex flex-col gap-4 border-t border-amare-border pt-8 text-sm text-amare-muted md:flex-row md:flex-wrap md:items-center md:gap-x-6 md:gap-y-2">
|
||||
<a href="{{ url('/#casamentos') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Casamentos</a>
|
||||
<a href="{{ url('/#corporate') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Corporate</a>
|
||||
<a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Serviços</a>
|
||||
<a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Portfólio</a>
|
||||
<a href="{{ route('about') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Sobre</a>
|
||||
<a href="{{ route('briefing') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Briefing</a>
|
||||
<a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Parcerias</a>
|
||||
<a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Privacidade</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-amare-border">
|
||||
<div class="container-amare flex flex-col gap-2 py-6 text-sm text-amare-muted md:flex-row md:items-center md:justify-between">
|
||||
<p>© {{ now()->year }} {{ $siteSettings->brand_name }}. Todos os direitos reservados.</p>
|
||||
<p>
|
||||
<a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Política de privacidade</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -5,9 +5,38 @@
|
||||
|
||||
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||
<nav aria-label="Vertentes do portfólio" class="flex flex-wrap gap-x-6 gap-y-2 border-b border-amare-border pb-6 text-sm">
|
||||
<a
|
||||
href="{{ route('portfolio.index') }}"
|
||||
@class([
|
||||
'inline-flex min-h-11 items-center uppercase tracking-[0.08em] transition-colors',
|
||||
'font-semibold text-amare-accent' => $vertical === null,
|
||||
'text-amare-muted hover:text-amare-accent' => $vertical !== null,
|
||||
])
|
||||
>Todos</a>
|
||||
@foreach (\App\Domain\Marketing\PortfolioVertical::cases() as $option)
|
||||
<a
|
||||
href="{{ route('portfolio.index', ['vertente' => $option->value]) }}"
|
||||
@class([
|
||||
'inline-flex min-h-11 items-center uppercase tracking-[0.08em] transition-colors',
|
||||
'font-semibold text-amare-accent' => $vertical === $option,
|
||||
'text-amare-muted hover:text-amare-accent' => $vertical !== $option,
|
||||
])
|
||||
>{{ $option->label() }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@if ($cases->isEmpty())
|
||||
<p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p>
|
||||
@if ($vertical === \App\Domain\Marketing\PortfolioVertical::Corporate)
|
||||
<div class="flex min-h-[280px] flex-col items-center justify-center gap-4 bg-amare-bg p-10 text-center">
|
||||
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">Portfólio Corporate</p>
|
||||
<h2 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight text-amare-text">Conteúdo em construção</h2>
|
||||
<p class="max-w-lg text-amare-text-muted">Ainda não publicamos cases corporativos autorizados. Em vez de inventar prova, mantemos este espaço pronto para projetos reais.</p>
|
||||
<a href="{{ route('briefing') }}" class="btn btn-outline mt-2 text-xs font-bold uppercase tracking-[0.09em]">Falar sobre um evento corporativo</a>
|
||||
</div>
|
||||
@else
|
||||
<p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p>
|
||||
@endif
|
||||
@else
|
||||
@php
|
||||
$hasPrioritizedImage = false;
|
||||
@@ -53,6 +82,9 @@
|
||||
@endphp
|
||||
@endif
|
||||
<div class="space-y-2 border-t border-amare-border pt-4">
|
||||
@if (filled($case->event_type))
|
||||
<p class="text-xs font-bold uppercase tracking-[0.16em] text-amare-accent-deep">{{ $case->event_type }}</p>
|
||||
@endif
|
||||
<h2 class="text-2xl font-medium text-amare-text">
|
||||
<a href="{{ route('portfolio.show', $case->slug) }}" class="transition-colors hover:text-amare-accent">{{ $case->title }}</a>
|
||||
</h2>
|
||||
|
||||
@@ -47,8 +47,6 @@
|
||||
:show-intro="false"
|
||||
kicker="tag"
|
||||
:show-subtitle="true"
|
||||
cta-route="contact"
|
||||
:band-cta-href="route('contact')"
|
||||
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
|
||||
:show-note="false"
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tests\Feature\Application\Queries\Marketing;
|
||||
|
||||
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
||||
use App\Domain\Marketing\PortfolioVertical;
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\PortfolioImage;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -58,4 +59,26 @@ class GetPublishedPortfolioCasesTest extends TestCase
|
||||
$this->assertTrue($cases->get(0)?->relationLoaded('images'));
|
||||
$this->assertCount(2, $cases->get(0)?->images ?? []);
|
||||
}
|
||||
|
||||
public function test_filters_published_cases_by_portfolio_vertical(): void
|
||||
{
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Wedding Case',
|
||||
'event_type' => 'Mini wedding',
|
||||
'sort_order' => 10,
|
||||
]);
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Corporate Case',
|
||||
'event_type' => 'Evento corporativo',
|
||||
'sort_order' => 20,
|
||||
]);
|
||||
|
||||
$weddings = (new GetPublishedPortfolioCases)(PortfolioVertical::Casamentos);
|
||||
$corporate = (new GetPublishedPortfolioCases)(PortfolioVertical::Corporate);
|
||||
|
||||
$this->assertCount(1, $weddings);
|
||||
$this->assertSame('Wedding Case', $weddings->first()?->title);
|
||||
$this->assertCount(1, $corporate);
|
||||
$this->assertSame('Corporate Case', $corporate->first()?->title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,33 @@ class AboutPageContentTest extends TestCase
|
||||
->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([
|
||||
|
||||
@@ -213,7 +213,26 @@ class HomePageContentTest extends TestCase
|
||||
false,
|
||||
)
|
||||
->assertSee('target="_blank"', false)
|
||||
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
|
||||
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false)
|
||||
->assertSee(
|
||||
'https://wa.me/5511988887777?text='.rawurlencode('Olá, ainda não sei qual modalidade de acompanhamento combina com o meu casamento e gostaria de orientação da Amare.'),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_orientation_cta_falls_back_to_briefing_without_whatsapp_number(): void
|
||||
{
|
||||
SiteSetting::instance()->update(['whatsapp_number' => null]);
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('href="'.route('briefing').'"', false)
|
||||
->assertDontSee(
|
||||
'Olá, ainda não sei qual modalidade de acompanhamento combina com o meu casamento e gostaria de orientação da Amare.',
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
public function test_corporate_steps_and_placeholder_render_from_settings(): void
|
||||
|
||||
60
tests/Feature/PublicSite/PortfolioVerticalFilterTest.php
Normal file
60
tests/Feature/PublicSite/PortfolioVerticalFilterTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\PublicSite;
|
||||
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PortfolioVerticalFilterTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_portfolio_index_filters_by_vertical_and_shows_corporate_empty_state(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Casamento Jardim',
|
||||
'slug' => 'casamento-jardim',
|
||||
'event_type' => 'Casamento',
|
||||
'sort_order' => 10,
|
||||
]);
|
||||
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Convenção Anual',
|
||||
'slug' => 'convencao-anual',
|
||||
'event_type' => 'Corporativo',
|
||||
'sort_order' => 20,
|
||||
]);
|
||||
|
||||
$this->get(route('portfolio.index'))
|
||||
->assertOk()
|
||||
->assertSee('Casamento Jardim')
|
||||
->assertSee('Convenção Anual')
|
||||
->assertSee('Vertentes do portfólio')
|
||||
->assertSee('Casamentos')
|
||||
->assertSee('Corporate');
|
||||
|
||||
$this->get(route('portfolio.index', ['vertente' => 'casamentos']))
|
||||
->assertOk()
|
||||
->assertSee('Casamento Jardim')
|
||||
->assertDontSee('Convenção Anual');
|
||||
|
||||
$this->get(route('portfolio.index', ['vertente' => 'corporate']))
|
||||
->assertOk()
|
||||
->assertSee('Convenção Anual')
|
||||
->assertDontSee('Casamento Jardim');
|
||||
|
||||
PortfolioCase::query()->where('slug', 'convencao-anual')->update(['published_at' => null]);
|
||||
|
||||
$this->get(route('portfolio.index', ['vertente' => 'corporate']))
|
||||
->assertOk()
|
||||
->assertSee('Portfólio Corporate')
|
||||
->assertSee('Conteúdo em construção')
|
||||
->assertDontSee('Convenção Anual');
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,8 @@ class PublicLayoutSeoTest extends TestCase
|
||||
->assertSee('Amare Brand')
|
||||
->assertSee('hello@amare.test')
|
||||
->assertSee('(11) 91111-1111')
|
||||
->assertSee('Política de privacidade')
|
||||
->assertSee('Privacidade')
|
||||
->assertSee(route('privacy'), false)
|
||||
->assertSee('https://instagram.com/amare', false);
|
||||
}
|
||||
}
|
||||
|
||||
43
tests/Feature/PublicSite/PublicNavigationVerticalsTest.php
Normal file
43
tests/Feature/PublicSite/PublicNavigationVerticalsTest.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\PublicSite;
|
||||
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PublicNavigationVerticalsTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_header_and_footer_expose_casamentos_and_corporate_without_implying_weddings_only(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
|
||||
$response = $this->get(route('about'))->assertOk();
|
||||
|
||||
$homeCasamentos = url('/#casamentos');
|
||||
$homeCorporate = url('/#corporate');
|
||||
|
||||
$response
|
||||
->assertSee('id="main-nav"', false)
|
||||
->assertSee('href="'.$homeCasamentos.'"', false)
|
||||
->assertSee('>Casamentos</a>', false)
|
||||
->assertSee('href="'.$homeCorporate.'"', false)
|
||||
->assertSee('>Corporate</a>', false)
|
||||
->assertSee('href="'.route('portfolio.index').'"', false)
|
||||
->assertSee('href="'.route('about').'"', false)
|
||||
->assertSee('href="'.route('services.index').'"', false)
|
||||
->assertSee('href="'.route('privacy').'"', false)
|
||||
->assertSee('aria-label="Rodapé"', false)
|
||||
->assertSeeInOrder([
|
||||
'aria-label="Rodapé"',
|
||||
'href="'.$homeCasamentos.'"',
|
||||
'Casamentos',
|
||||
'href="'.$homeCorporate.'"',
|
||||
'Corporate',
|
||||
], false);
|
||||
}
|
||||
}
|
||||
67
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
67
tests/Feature/PublicSite/ServicesPageCtaTest.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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);
|
||||
}
|
||||
|
||||
public function test_orientation_cta_uses_distinct_whatsapp_message_when_a_number_is_configured(): void
|
||||
{
|
||||
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
|
||||
|
||||
$response = $this->get(route('services.index'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee(
|
||||
'https://wa.me/5511988887777?text='.rawurlencode('Olá, ainda não sei qual modalidade de acompanhamento combina com o meu casamento e gostaria de orientação da Amare.'),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user