Compare commits
1 Commits
main
...
research/m
| Author | SHA1 | Date | |
|---|---|---|---|
| 17060c054c |
@@ -79,4 +79,4 @@ Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-
|
|||||||
|
|
||||||
### Domain docs
|
### Domain docs
|
||||||
|
|
||||||
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`.
|
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.
|
||||||
|
|||||||
@@ -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.
|
- **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`.
|
- **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/`.
|
- **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`, `/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.
|
- **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`).
|
||||||
- **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`.
|
- **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
|
## Navigating the normative docs
|
||||||
|
|||||||
42
SPEC.md
42
SPEC.md
@@ -283,15 +283,11 @@ Uma funcionalidade fora do MVP só poderá entrar quando:
|
|||||||
| Método | Rota | Nome sugerido | Finalidade |
|
| Método | Rota | Nome sugerido | Finalidade |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| GET | `/` | `home` | Home editorial |
|
| GET | `/` | `home` | Home editorial |
|
||||||
| GET | `/servicos` | `services.index` | Lista de serviços / modalidades publicadas |
|
| GET | `/servicos` | `services.index` | Lista de serviços publicados |
|
||||||
| GET | `/pacotes/{slug}` | `packages.show` | Detalhe de uma modalidade de casamento |
|
|
||||||
| GET | `/portfolio` | `portfolio.index` | Lista de casos publicados |
|
| GET | `/portfolio` | `portfolio.index` | Lista de casos publicados |
|
||||||
| GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso |
|
| GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso |
|
||||||
| GET | `/sobre` | `about` | História, método e credenciais |
|
| GET | `/sobre` | `about` | História, método e credenciais |
|
||||||
| GET | `/briefing` | `briefing` | Briefing comercial (pedido de proposta) |
|
| GET | `/contato` | `contact` | Briefing de contato |
|
||||||
| 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 | `/privacidade` | `privacy` | Política de privacidade |
|
||||||
| GET | `/sitemap.xml` | `sitemap` | Sitemap público |
|
| GET | `/sitemap.xml` | `sitemap` | Sitemap público |
|
||||||
| GET | `/robots.txt` | `robots` | Política de crawling |
|
| GET | `/robots.txt` | `robots` | Política de crawling |
|
||||||
@@ -328,8 +324,7 @@ Administração
|
|||||||
|
|
||||||
- Dashboard: `Filament Page` customizada com widgets orientados a exceção.
|
- Dashboard: `Filament Page` customizada com widgets orientados a exceção.
|
||||||
- Detalhe do evento: página customizada do Resource com resumo operacional.
|
- Detalhe do evento: página customizada do Resource com resumo operacional.
|
||||||
- Briefing comercial: Blade + Controller (`POST /briefing`), ver §11.2.
|
- Briefing público: Blade + Controller (`POST /contato`), ver §11.2.
|
||||||
- Contato de parceiro: Blade + Controller (`POST /contato`) — canal separado do briefing.
|
|
||||||
- Home: Blade com componentes de design reutilizáveis.
|
- Home: Blade com componentes de design reutilizáveis.
|
||||||
|
|
||||||
### 5.4 Decisões de UX YAGNI
|
### 5.4 Decisões de UX YAGNI
|
||||||
@@ -1248,35 +1243,6 @@ Auditoria não precisa ser um event sourcing. Registrar apenas operações crít
|
|||||||
|
|
||||||
## 8. Modelo de domínio e banco de dados
|
## 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
|
### 8.1 Módulos
|
||||||
|
|
||||||
| Módulo | Responsabilidade |
|
| Módulo | Responsabilidade |
|
||||||
@@ -1829,7 +1795,7 @@ Não transformar seções estáticas em componentes Livewire. Usar Blade quando
|
|||||||
|
|
||||||
### 11.2 Formulário de briefing
|
### 11.2 Formulário de briefing
|
||||||
|
|
||||||
> **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.
|
> **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.
|
||||||
|
|
||||||
O formulário deve:
|
O formulário deve:
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Application\Queries\Marketing;
|
namespace App\Application\Queries\Marketing;
|
||||||
|
|
||||||
use App\Domain\Marketing\PortfolioVertical;
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
final class GetPublishedPortfolioCases
|
final class GetPublishedPortfolioCases
|
||||||
@@ -15,37 +12,12 @@ final class GetPublishedPortfolioCases
|
|||||||
/**
|
/**
|
||||||
* @return Collection<int, PortfolioCase>
|
* @return Collection<int, PortfolioCase>
|
||||||
*/
|
*/
|
||||||
public function __invoke(?PortfolioVertical $vertical = null): Collection
|
public function __invoke(): Collection
|
||||||
{
|
{
|
||||||
return $this->baseQuery($vertical)->get();
|
return PortfolioCase::query()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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()
|
->published()
|
||||||
->with(['images'])
|
->with(['images'])
|
||||||
->orderBy('sort_order');
|
->orderBy('sort_order')
|
||||||
|
->get();
|
||||||
if ($vertical !== null) {
|
|
||||||
$query->where(function (Builder $builder) use ($vertical): void {
|
|
||||||
foreach ($vertical->eventTypePatterns() as $pattern) {
|
|
||||||
$builder->orWhere('event_type', 'ilike', $pattern);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
<?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,35 +6,33 @@ namespace App\Http\Controllers\PublicSite;
|
|||||||
|
|
||||||
use App\Application\Data\PageMeta;
|
use App\Application\Data\PageMeta;
|
||||||
use App\Application\Queries\Marketing\FindPublishedPortfolioCaseBySlug;
|
use App\Application\Queries\Marketing\FindPublishedPortfolioCaseBySlug;
|
||||||
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
|
||||||
use App\Domain\Marketing\PortfolioVertical;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
final class PortfolioController extends Controller
|
final class PortfolioController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Request $request, GetPublishedPortfolioCases $getPublishedPortfolioCases): View
|
public function index(): View
|
||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
$vertical = PortfolioVertical::tryFromQuery($request->query('vertente'));
|
|
||||||
$cases = $getPublishedPortfolioCases->paginate($vertical);
|
/** @var LengthAwarePaginator<int, PortfolioCase> $cases */
|
||||||
|
$cases = PortfolioCase::query()
|
||||||
|
->published()
|
||||||
|
->with(['images'])
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->paginate(9);
|
||||||
|
|
||||||
return view('pages.portfolio.index', [
|
return view('pages.portfolio.index', [
|
||||||
'cases' => $cases,
|
'cases' => $cases,
|
||||||
'vertical' => $vertical,
|
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('portfolio.index', array_filter([
|
canonical: route('portfolio.index'),
|
||||||
'vertente' => $vertical?->value,
|
|
||||||
])),
|
|
||||||
settings: $settings,
|
settings: $settings,
|
||||||
title: PageMeta::withBrandSuffix(
|
title: PageMeta::withBrandSuffix('Portfólio', $settings),
|
||||||
$vertical === null ? 'Portfólio' : 'Portfólio — '.$vertical->label(),
|
|
||||||
$settings,
|
|
||||||
),
|
|
||||||
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -8,45 +8,20 @@ use App\Models\SiteSetting;
|
|||||||
|
|
||||||
final class PackageContactLink
|
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.
|
* Resolve the contextual contact link for a wedding package modality.
|
||||||
*
|
*
|
||||||
* @return array{href: string, isWhatsapp: bool}
|
* @return array{href: string, isWhatsapp: bool}
|
||||||
*/
|
*/
|
||||||
public static function for(SiteSetting $settings, string $packageName): array
|
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);
|
$digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
|
||||||
$isWhatsapp = strlen($digits) >= 10;
|
$isWhatsapp = strlen($digits) >= 10;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'href' => $isWhatsapp
|
'href' => $isWhatsapp
|
||||||
? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappMessage)
|
? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.')
|
||||||
: route('briefing', $briefingQuery),
|
: route('briefing', ['servico_interesse' => $packageName]),
|
||||||
'isWhatsapp' => $isWhatsapp,
|
'isWhatsapp' => $isWhatsapp,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,51 @@
|
|||||||
# Domain Docs
|
# Domain Docs
|
||||||
|
|
||||||
How engineering skills should consume this repo's domain documentation.
|
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
|
||||||
|
|
||||||
## Single source of truth
|
## Before exploring, read these
|
||||||
|
|
||||||
**`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.
|
- **`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.
|
||||||
|
|
||||||
Also useful:
|
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.
|
||||||
|
|
||||||
- **`PRODUCT.md`** — positioning and audience (not the glossary)
|
## File structure
|
||||||
- **`openspec/specs/`** — current capability contracts
|
|
||||||
- **`docs/adr/README.md`** — index only; ADR-001–010 live in `SPEC.md` §21
|
|
||||||
|
|
||||||
## Before exploring
|
Single-context repo (most repos):
|
||||||
|
|
||||||
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.
|
/
|
||||||
|
├── CONTEXT.md
|
||||||
|
├── docs/adr/
|
||||||
|
│ ├── 0001-event-sourced-orders.md
|
||||||
|
│ └── 0002-postgres-for-write-model.md
|
||||||
|
└── src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── CONTEXT-MAP.md
|
||||||
|
├── docs/adr/ ← system-wide decisions
|
||||||
|
└── src/
|
||||||
|
├── ordering/
|
||||||
|
│ ├── CONTEXT.md
|
||||||
|
│ └── docs/adr/ ← context-specific decisions
|
||||||
|
└── billing/
|
||||||
|
├── CONTEXT.md
|
||||||
|
└── docs/adr/
|
||||||
|
```
|
||||||
|
|
||||||
## Use the glossary's vocabulary
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
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_`).
|
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
If the concept 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`.
|
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
## Flag ADR conflicts
|
## Flag ADR conflicts
|
||||||
|
|
||||||
If output contradicts an ADR in `SPEC.md` §21, surface it explicitly rather than silently overriding.
|
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
|
||||||
|
|||||||
118
docs/research/man-135-classify-amare-mvp.md
Normal file
118
docs/research/man-135-classify-amare-mvp.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# MAN-135 — Classificar issues Amare MVP vs Gitea `main`
|
||||||
|
|
||||||
|
**Issue:** [Research: classificar issues Amare MVP vs Gitea main](https://linear.app/maneco-workspace/issue/MAN-135/research-classificar-issues-amare-mvp-vs-gitea-main)
|
||||||
|
**Mapa:** [MAN-134](https://linear.app/maneco-workspace/issue/MAN-134/mapa-classificar-amare-mvp-linear-gitea)
|
||||||
|
**Branch:** `research/man-135-classify-amare-mvp`
|
||||||
|
**HEAD canônico inventariado:** `origin/main` @ `93b680c` (`docs: require Linear issues and auto-ship PRs (#4)`)
|
||||||
|
**Data:** 2026-08-13
|
||||||
|
|
||||||
|
## Regras (de MAN-134)
|
||||||
|
|
||||||
|
| Regra | Aplicação |
|
||||||
|
| --- | --- |
|
||||||
|
| Code truth | Gitea `origin/main` (`manoel-freitas/amare`) |
|
||||||
|
| Done = shipped | commit presente em `origin/main` |
|
||||||
|
| GitHub | só pista (`amore-site`); não canônico |
|
||||||
|
| Pós-MVP | fora de escopo: MAN-111, 112, 113, 114, 115, 131 |
|
||||||
|
| Status Linear | **não** alterados neste research (exceto fechar MAN-135) |
|
||||||
|
|
||||||
|
## Fontes
|
||||||
|
|
||||||
|
| Fonte | Uso |
|
||||||
|
| --- | --- |
|
||||||
|
| Linear MCP `list_issues` / `get_issue` project **Amare — Lançamento MVP** | universo, status, labels, ACs |
|
||||||
|
| `git fetch origin` + `git log origin/main` | evidência shipped |
|
||||||
|
| `tea pr list --state all` (0 open; #1–#4 merged) | PRs Gitea |
|
||||||
|
| `gh pr list` / `gh pr view` (GitHub mirror) | pistas #63, #64 |
|
||||||
|
| Branches `origin/*` | trabalho em voo |
|
||||||
|
| Cross-check | [MAN-136 orphans](https://linear.app/maneco-workspace/issue/MAN-136/research-inventariar-merges-orfaos-no-gitea), [MAN-137 MAN-126 state](https://linear.app/maneco-workspace/issue/MAN-137/research-estado-real-de-casamentos-corporate-man-126) (branches locais; **não** em `main` ainda) |
|
||||||
|
|
||||||
|
## Contagens (36 issues in-scope)
|
||||||
|
|
||||||
|
| Bucket | Count |
|
||||||
|
| --- | ---: |
|
||||||
|
| **shipped** | 14 |
|
||||||
|
| **em voo** | 5 |
|
||||||
|
| **não começou** | 12 |
|
||||||
|
| · cliente | 5 |
|
||||||
|
| · engenharia | 5 |
|
||||||
|
| · ops-lançamento | 2 |
|
||||||
|
| **Linear podre** | 5 |
|
||||||
|
| **Total classificado** | **36** |
|
||||||
|
| Excluídos Pós-MVP | 6 |
|
||||||
|
| Issues no projeto | 42 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tabela canônica
|
||||||
|
|
||||||
|
| Issue | Linear status | Bucket | Evidência / nota |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| [Implementar Home e páginas institucionais do MVP](https://linear.app/maneco-workspace/issue/MAN-102/implementar-home-e-paginas-institucionais-do-mvp) | Done | **shipped** | Site público + home editorial em `main` (ex. `#53`, heros, `/sobre`, `/servicos`) |
|
||||||
|
| [Implementar portfólio e depoimentos](https://linear.app/maneco-workspace/issue/MAN-103/implementar-portfolio-e-depoimentos) | Done | **shipped** | Portfólio/depoimentos + seed fix `49fbc0f` (MAN-103) |
|
||||||
|
| [Implementar formulário de solicitação de proposta](https://linear.app/maneco-workspace/issue/MAN-104/implementar-formulario-de-solicitacao-de-proposta) | Done | **shipped** | Briefing/contato em `main` (ex. `eed8240`) |
|
||||||
|
| [Implementar tracking simples da origem dos leads](https://linear.app/maneco-workspace/issue/MAN-105/implementar-tracking-simples-da-origem-dos-leads) | Done | **shipped** | `37296f7` (MAN-105) / GH #37 |
|
||||||
|
| [Definir e implementar atualização simples de conteúdo](https://linear.app/maneco-workspace/issue/MAN-106/definir-e-implementar-atualizacao-simples-de-conteudo) | Done | **shipped** | `4001cd0` (MAN-106 AC4) / GH #33 |
|
||||||
|
| [Finalizar SEO técnico e metadados sociais](https://linear.app/maneco-workspace/issue/MAN-107/finalizar-seo-tecnico-e-metadados-sociais) | Done | **shipped** | SEO desde ship público (`3dc1f44` et al.) |
|
||||||
|
| [Rodar auditoria Lighthouse e corrigir performance do site público](https://linear.app/maneco-workspace/issue/MAN-109/rodar-auditoria-lighthouse-e-corrigir-performance-do-site-publico) | Done | **shipped** | `0ecae4c` (MAN-109) / GH #43 |
|
||||||
|
| [Auditar se Vue + Livewire faz sentido na arquitetura atual](https://linear.app/maneco-workspace/issue/MAN-117/auditar-se-vue-livewire-faz-sentido-na-arquitetura-atual) | Done | **shipped** | `2e28603` reconcilia stack declarada |
|
||||||
|
| [Portar identidade visual do site público para o admin em Filament](https://linear.app/maneco-workspace/issue/MAN-118/portar-identidade-visual-do-site-publico-para-o-admin-em-filament) | Done | **shipped** | `63c0269` (MAN-118) / GH #40 |
|
||||||
|
| [Estabilizar baselines de regressão visual no CI (paridade local×CI)](https://linear.app/maneco-workspace/issue/MAN-119/estabilizar-baselines-de-regressao-visual-no-ci-paridade-localci) | Done | **shipped** | Trabalho de baselines chegou em `main`; remoção posterior `#44` é resolução intencional, não “Done sem merge” |
|
||||||
|
| [Fechar portões de qualidade e deploy da Fase 0 antes do lançamento](https://linear.app/maneco-workspace/issue/MAN-120/fechar-portoes-de-qualidade-e-deploy-da-fase-0-antes-do-lancamento) | Done | **shipped** | `7e68c0e` (MAN-120) / GH #39 |
|
||||||
|
| [Reencodar imagens de marca e regerar baselines visuais](https://linear.app/maneco-workspace/issue/MAN-122/reencodar-imagens-de-marca-e-regerar-baselines-visuais) | Done | **shipped** | Era de baselines/imagem em `main` (série #24/#7 + follow-ups) |
|
||||||
|
| [Deploy de staging falha no Dokploy desde 08/08 — causa server-side, sem log acessível](https://linear.app/maneco-workspace/issue/MAN-123/deploy-de-staging-falha-no-dokploy-desde-0808-causa-server-side-sem) | Done | **shipped** | Pipeline/fixes Dokploy+staging em `main` (ex. `#6`–`#9`, `ac08b5f`, `26a68e1`) |
|
||||||
|
| [Reconciliar documentação e changes OpenSpec após os PRs #41–#49](https://linear.app/maneco-workspace/issue/MAN-124/reconciliar-documentacao-e-changes-openspec-apos-os-prs-41-49) | Done | **shipped** | `213d684` (MAN-124) / GH #50 |
|
||||||
|
| [Reestruturar jornada pública em Amare Casamentos e Amare Corporate](https://linear.app/maneco-workspace/issue/MAN-126/reestruturar-jornada-publica-em-amare-casamentos-e-amare-corporate) | In Progress | **em voo** | `origin/feat/man-126-casamentos-corporate` (1 commit ahead, não em `main`); GH PR [#63](https://github.com/manoel-freitas/amore-site/pull/63) open; **0** PR Gitea. Ver MAN-137 |
|
||||||
|
| [Executar QA de responsividade e acessibilidade](https://linear.app/maneco-workspace/issue/MAN-108/executar-qa-de-responsividade-e-acessibilidade) | In Progress | **em voo** | Status coerente com QA aberto; PRs de suporte (#44–#49) já em `main`, ACs manuais não fechados; sem PR Gitea aberto só desta issue |
|
||||||
|
| [Configurar domínio, ambiente de produção e checklist de lançamento](https://linear.app/maneco-workspace/issue/MAN-110/configurar-dominio-ambiente-de-producao-e-checklist-de-lancamento) | In Progress | **em voo** | Ops de lançamento em curso (staging/deploy em `main`; produção/DNS/checklist incompletos) |
|
||||||
|
| [Ajustar CTAs consultivos no WhatsApp e briefing comercial](https://linear.app/maneco-workspace/issue/MAN-127/ajustar-ctas-consultivos-no-whatsapp-e-briefing-comercial) | Todo | **em voo** | Fluxo 1 parcial em `main` via Gitea [#2](https://git.hellomanoel.com/manoel-freitas/amare/pulls/2) (`e2af1f6`); Fluxo 2 (indecisos) + fechamento de ACs ainda abertos; Linear Todo atrasa vs código parcial |
|
||||||
|
| [Research: classificar issues Amare MVP vs Gitea main](https://linear.app/maneco-workspace/issue/MAN-135/research-classificar-issues-amare-mvp-vs-gitea-main) | Backlog→Done* | **em voo** | Este arquivo / branch `research/man-135-classify-amare-mvp` — **ainda não** em `origin/main` (*Done só no Linear após resolução; ver podre se Done sem merge) |
|
||||||
|
| [Exigir issue Linear em todo trabalho e PR](https://linear.app/maneco-workspace/issue/MAN-132/exigir-issue-linear-em-todo-trabalho-e-pr) | In Progress | **Linear podre** | Conteúdo em `main` via Gitea [#4](https://git.hellomanoel.com/manoel-freitas/amare/pulls/4) (`93b680c`); Linear ainda In Progress. GH [#64](https://github.com/manoel-freitas/amore-site/pull/64) open = mirror stale |
|
||||||
|
| [Adicionar PR template no Gitea e no GitHub](https://linear.app/maneco-workspace/issue/MAN-133/adicionar-pr-template-no-gitea-e-no-github) | In Progress | **Linear podre** | `.gitea/PULL_REQUEST_TEMPLATE.md` + `.github/pull_request_template.md` em `origin/main` (mesmo PR #4); Linear In Progress |
|
||||||
|
| [Fechar arquitetura e copy final do MVP](https://linear.app/maneco-workspace/issue/MAN-97/fechar-arquitetura-e-copy-final-do-mvp) | In Progress | **Linear podre** | In Progress sem branch/PR abertos; arquitetura/home editorial já em `main` (#46–#53, #52…). Residual de copy/conteúdo vive em tickets cliente (MAN-98…) e MAN-126 |
|
||||||
|
| [Research: inventariar merges órfãos no Gitea](https://linear.app/maneco-workspace/issue/MAN-136/research-inventariar-merges-orfaos-no-gitea) | Done | **Linear podre** | Done no Linear; deliverable em `origin/research/man-136-orphan-gitea-merges` — **não** merged em `main` |
|
||||||
|
| [Research: estado real de Casamentos × Corporate (MAN-126)](https://linear.app/maneco-workspace/issue/MAN-137/research-estado-real-de-casamentos-corporate-man-126) | Done | **Linear podre** | Done no Linear; `docs/research/man-137-man-126-state.md` em branch local ahead — **não** em `origin/main` |
|
||||||
|
| [Receber e consolidar conteúdo institucional e serviços da Michelle](https://linear.app/maneco-workspace/issue/MAN-98/receber-e-consolidar-conteudo-institucional-e-servicos-da-michelle) | Todo | **não começou · cliente** | Labels Conteúdo/Cliente; sem branch/PR |
|
||||||
|
| [Fechar portfólio e banco de imagens do lançamento](https://linear.app/maneco-workspace/issue/MAN-99/fechar-portfolio-e-banco-de-imagens-do-lancamento) | Todo | **não começou · cliente** | Labels Conteúdo/Cliente; sem branch/PR |
|
||||||
|
| [Consolidar depoimentos reais para o MVP](https://linear.app/maneco-workspace/issue/MAN-100/consolidar-depoimentos-reais-para-o-mvp) | Todo | **não começou · cliente** | Labels Conteúdo/Cliente; sem branch/PR |
|
||||||
|
| [Confirmar WhatsApp Business e canais de contato](https://linear.app/maneco-workspace/issue/MAN-101/confirmar-whatsapp-business-e-canais-de-contato) | Todo | **não começou · cliente** | Labels Conteúdo/Cliente; sem branch/PR |
|
||||||
|
| [Aplicar identidade visual oficial e rodada de revisão da cliente](https://linear.app/maneco-workspace/issue/MAN-128/aplicar-identidade-visual-oficial-e-rodada-de-revisao-da-cliente) | Todo | **não começou · cliente** | Labels Cliente/Design; sem branch/PR |
|
||||||
|
| [Papel Assistant não tem nenhuma permissão funcional](https://linear.app/maneco-workspace/issue/MAN-121/papel-assistant-nao-tem-nenhuma-permissao-funcional) | Backlog | **não começou · engenharia** | Enum `Assistant` existe; permissões funcionais não entregues; sem branch |
|
||||||
|
| [Provar falha dos gates e rollback antes de arquivar complete-foundation-parity](https://linear.app/maneco-workspace/issue/MAN-125/provar-falha-dos-gates-e-rollback-antes-de-arquivar-complete) | Todo | **não começou · engenharia** | ACs de prova controlada abertos; experiência **não** deve mergear em `main` |
|
||||||
|
| [Revisar privacidade, LGPD e consentimentos do MVP](https://linear.app/maneco-workspace/issue/MAN-116/revisar-privacidade-lgpd-e-consentimentos-do-mvp) | Todo | **não começou · engenharia** | Página privacidade existe; revisão/consentimentos ACs abertos; sem branch |
|
||||||
|
| [Mapa: classificar Amare MVP (Linear × Gitea)](https://linear.app/maneco-workspace/issue/MAN-134/mapa-classificar-amare-mvp-linear-gitea) | Todo | **não começou · engenharia** | Wayfinder map — inventário ainda em research filhos |
|
||||||
|
| [Grilling: lista de ações de sync após inventário](https://linear.app/maneco-workspace/issue/MAN-138/grilling-lista-de-acoes-de-sync-apos-inventario) | Backlog | **não começou · engenharia** | Depende do inventário (este + MAN-136/137) |
|
||||||
|
| [Configurar Search Console, Meta Pixel e mensuração de lançamento](https://linear.app/maneco-workspace/issue/MAN-129/configurar-search-console-meta-pixel-e-mensuracao-de-lancamento) | Todo | **não começou · ops-lançamento** | Mensuração de go-live; sem branch |
|
||||||
|
| [Preparar acesso e treinamento do painel administrativo](https://linear.app/maneco-workspace/issue/MAN-130/preparar-acesso-e-treinamento-do-painel-administrativo) | Todo | **não começou · ops-lançamento** | Acesso/treino go-live (label Cliente+Engenharia); sem branch |
|
||||||
|
|
||||||
|
### Fora de escopo (Pós-MVP)
|
||||||
|
|
||||||
|
| Issue | Nota |
|
||||||
|
| --- | --- |
|
||||||
|
| [MAN-111](https://linear.app/maneco-workspace/issue/MAN-111/pos-mvp-produzir-pdf-comercial-estatico-de-servicos) … [MAN-115](https://linear.app/maneco-workspace/issue/MAN-115/pos-mvp-mapear-convivencia-com-o-assessoria-vip), [MAN-131](https://linear.app/maneco-workspace/issue/MAN-131/pos-mvp-formulario-para-fornecedores-e-parcerias) | Não classificados (mapa Out of scope) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hints verificados
|
||||||
|
|
||||||
|
| Hint | Resultado |
|
||||||
|
| --- | --- |
|
||||||
|
| MAN-132 + MAN-133 via Gitea PR #4, Linear In Progress | **Confirmado → Linear podre** (código shipped; status podre). Também GH #64 ainda open |
|
||||||
|
| MAN-126: GH #63 + branch, sem PR Gitea | **Confirmado → em voo** |
|
||||||
|
| Gitea 0 open PRs; #1–#4 merged | **Confirmado** (`tea pr list`) |
|
||||||
|
|
||||||
|
## Ações de sync sugeridas (não executadas aqui)
|
||||||
|
|
||||||
|
1. Marcar **Done** no Linear: MAN-132, MAN-133 (e fechar GH #64 stale).
|
||||||
|
2. Reavaliar MAN-97 → Done ou decompor residual em tickets cliente/MAN-126.
|
||||||
|
3. Abrir **PR Gitea** para `feat/man-126-casamentos-corporate`; fechar ou ignorar GH #63 após mirror.
|
||||||
|
4. MAN-127: mover para In Progress; fechar Fluxo 2 / ACs restantes ou fatiar ticket.
|
||||||
|
5. Mergear (ou aceitar exceção) branches de research MAN-136/137/135 se “Done” exigir `main`.
|
||||||
|
6. Lista priorizada fica para [MAN-138](https://linear.app/maneco-workspace/issue/MAN-138/grilling-lista-de-acoes-de-sync-apos-inventario).
|
||||||
|
|
||||||
|
## Ambiguidades
|
||||||
|
|
||||||
|
- **Wayfinder research Done:** mapa diz Done = `main`; prática atual marca Done após comentário Linear com arquivo só na branch → classificado **Linear podre** para MAN-136/137.
|
||||||
|
- **MAN-119/122:** remoção de regressão visual (`#44`) após estabilizar — tratados como shipped (resolução chegou em `main`).
|
||||||
|
- **MAN-127:** bucket **em voo** (parcial shipped + resto aberto); alternativa seria “não começou” só do residual — preferimos um bucket por issue.
|
||||||
|
- **MAN-108/110:** sem branch dedicada; “em voo” = processo In Progress, não PR aberta.
|
||||||
@@ -134,8 +134,7 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
[id$='-heading'],
|
[id$='-heading'] {
|
||||||
.home-chapter[id] {
|
|
||||||
scroll-margin-top: 5.5rem;
|
scroll-margin-top: 5.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$orientationLink = \App\Support\PackageContactLink::forOrientation($settings);
|
$bandHref = $bandCtaHref ?? route('briefing');
|
||||||
$bandHref = $bandCtaHref ?? $orientationLink['href'];
|
|
||||||
$bandIsWhatsapp = $bandCtaHref === null && $orientationLink['isWhatsapp'];
|
|
||||||
@endphp
|
@endphp
|
||||||
<section
|
<section
|
||||||
aria-labelledby="packages-heading"
|
aria-labelledby="packages-heading"
|
||||||
@@ -79,11 +77,7 @@
|
|||||||
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
|
<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>
|
<p class="text-amare-accent-text/80">{{ $bandBody }}</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<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">
|
||||||
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
|
Conversar com a Amare
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<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">
|
<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>
|
<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="{{ 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="{{ 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="{{ 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('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="{{ 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('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('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>
|
<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,6 +73,7 @@
|
|||||||
<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">
|
<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
|
Conte seu evento
|
||||||
</a>
|
</a>
|
||||||
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -110,39 +111,29 @@
|
|||||||
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="container-amare flex flex-col gap-10 py-14 md:py-[58px]">
|
<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="flex flex-col gap-8 md:flex-row md:items-end md:justify-between">
|
<div class="space-y-3">
|
||||||
<div class="space-y-3">
|
<x-brand.logo variant="on-light" class="h-10 w-auto" />
|
||||||
<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>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<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">
|
<p class="text-sm text-amare-muted md:text-right">
|
||||||
<a href="{{ url('/#casamentos') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Casamentos</a>
|
@if ($footerSocials->isEmpty())
|
||||||
<a href="{{ url('/#corporate') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Corporate</a>
|
Instagram · WhatsApp · E-mail · LinkedIn (quando confirmado)
|
||||||
<a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Serviços</a>
|
@else
|
||||||
<a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Portfólio</a>
|
@foreach ($footerSocials as $network => $url)
|
||||||
<a href="{{ route('about') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Sobre</a>
|
<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
|
||||||
<a href="{{ route('briefing') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Briefing</a>
|
@endforeach
|
||||||
<a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Parcerias</a>
|
@endif
|
||||||
<a href="{{ route('privacy') }}" class="inline-flex min-h-11 items-center transition-colors hover:text-amare-accent">Privacidade</a>
|
</p>
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-amare-border">
|
<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">
|
<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>© {{ 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>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -5,38 +5,9 @@
|
|||||||
|
|
||||||
<div class="border-b border-amare-border bg-amare-bg-deep">
|
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
<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())
|
@if ($cases->isEmpty())
|
||||||
@if ($vertical === \App\Domain\Marketing\PortfolioVertical::Corporate)
|
<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>
|
||||||
<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
|
@else
|
||||||
@php
|
@php
|
||||||
$hasPrioritizedImage = false;
|
$hasPrioritizedImage = false;
|
||||||
@@ -82,9 +53,6 @@
|
|||||||
@endphp
|
@endphp
|
||||||
@endif
|
@endif
|
||||||
<div class="space-y-2 border-t border-amare-border pt-4">
|
<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">
|
<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>
|
<a href="{{ route('portfolio.show', $case->slug) }}" class="transition-colors hover:text-amare-accent">{{ $case->title }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
:show-intro="false"
|
:show-intro="false"
|
||||||
kicker="tag"
|
kicker="tag"
|
||||||
:show-subtitle="true"
|
:show-subtitle="true"
|
||||||
|
:band-cta-href="route('briefing')"
|
||||||
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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
|||||||
namespace Tests\Feature\Application\Queries\Marketing;
|
namespace Tests\Feature\Application\Queries\Marketing;
|
||||||
|
|
||||||
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
use App\Application\Queries\Marketing\GetPublishedPortfolioCases;
|
||||||
use App\Domain\Marketing\PortfolioVertical;
|
|
||||||
use App\Models\PortfolioCase;
|
use App\Models\PortfolioCase;
|
||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@@ -59,26 +58,4 @@ class GetPublishedPortfolioCasesTest extends TestCase
|
|||||||
$this->assertTrue($cases->get(0)?->relationLoaded('images'));
|
$this->assertTrue($cases->get(0)?->relationLoaded('images'));
|
||||||
$this->assertCount(2, $cases->get(0)?->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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,26 +213,7 @@ class HomePageContentTest extends TestCase
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
->assertSee('target="_blank"', 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
|
public function test_corporate_steps_and_placeholder_render_from_settings(): void
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
<?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,8 +54,7 @@ class PublicLayoutSeoTest extends TestCase
|
|||||||
->assertSee('Amare Brand')
|
->assertSee('Amare Brand')
|
||||||
->assertSee('hello@amare.test')
|
->assertSee('hello@amare.test')
|
||||||
->assertSee('(11) 91111-1111')
|
->assertSee('(11) 91111-1111')
|
||||||
->assertSee('Privacidade')
|
->assertSee('Política de privacidade')
|
||||||
->assertSee(route('privacy'), false)
|
|
||||||
->assertSee('https://instagram.com/amare', false);
|
->assertSee('https://instagram.com/amare', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<?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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,18 +50,4 @@ class ServicesPageCtaTest extends TestCase
|
|||||||
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
|
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
|
||||||
->assertDontSee('wa.me/', 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