Compare commits

...

7 Commits

Author SHA1 Message Date
444153bf3d fix: assert font provider via import, not bunny( substring (MAN-139)
All checks were successful
CI / static (pull_request) Successful in 2m37s
CI / unit (pull_request) Successful in 3m55s
CI / feature (pull_request) Successful in 2m59s
CI / container (pull_request) Successful in 1m12s
CI / browser (pull_request) Successful in 5m40s
Comment in vite.config.js still said bunny(), so assertStringNotContainsString failed in unit coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:47:52 -03:00
290585d713 fix: use single quotes in fontsource Vite assertion (MAN-139)
Some checks failed
CI / static (pull_request) Successful in 2m37s
CI / unit (pull_request) Failing after 3m50s
CI / container (pull_request) Has been cancelled
CI / browser (pull_request) Has been cancelled
CI / feature (pull_request) Has been cancelled
Pint failed the static CI job on bunny( with double quotes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:34:22 -03:00
8965a7c2fd fix: build EB Garamond from fontsource instead of Bunny (MAN-139)
Some checks failed
CI / static (pull_request) Failing after 1m59s
CI / browser (pull_request) Has been cancelled
CI / container (pull_request) Has been cancelled
CI / feature (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
Vite was fetching fonts.bunny.net at build time; the feature CI job timed out on that host.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:27:38 -03:00
8decba693a feat: polir delight editorial no site público (MAN-139)
Some checks failed
CI / static (pull_request) Successful in 3m52s
CI / unit (pull_request) Successful in 4m54s
CI / feature (pull_request) Failing after 2m11s
CI / container (pull_request) Successful in 5m17s
CI / browser (pull_request) Successful in 7m25s
Confirmação de briefing/parceria, micro-feedback de campos e nav, e copy mais humana nos erros — sem quebrar reduced-motion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:13:09 -03:00
dfd9763029 feat: jornada pública Casamentos × Corporate (MAN-126) (#5)
All checks were successful
CI / static (push) Successful in 2m40s
CI / unit (push) Successful in 3m41s
CI / feature (push) Successful in 2m56s
CI / container (push) Successful in 17s
CI / browser (push) Successful in 3m20s
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
2026-08-13 18:48:58 +00:00
93b680c172 docs: require Linear issues and auto-ship PRs (#4)
All checks were successful
CI / static (push) Successful in 2m25s
CI / unit (push) Successful in 3m34s
CI / feature (push) Successful in 3m17s
CI / container (push) Successful in 20s
CI / browser (push) Successful in 3m43s
## WHAT

- `AGENTS.md` e `docs/agents/issue-tracker.md`: todo trabalho e todo PR precisam de issue Linear.
- SDLC automático: trabalho feito → testes feitos ou atualizados → pre-commit verde → commit → PR, sem perguntar.
- Templates de PR idênticos no Gitea (`.gitea/PULL_REQUEST_TEMPLATE.md`) e no GitHub (`.github/pull_request_template.md`) com WHAT / WHY / HOW / Linear Issue / Comments.

## WHY

Agentes estavam soltos no tracker e paravam pra perguntar se podiam commitar. O contrato precisa ser o ciclo completo, com issue Linear e template de PR.

## HOW

Docs e templates only. Sem mudança de runtime. Pre-commit (`composer pint:check` + `composer phpstan`) passou.

## Linear Issue

- [MAN-132](https://linear.app/maneco-workspace/issue/MAN-132/exigir-issue-linear-em-todo-trabalho-e-pr)
- [MAN-133](https://linear.app/maneco-workspace/issue/MAN-133/adicionar-pr-template-no-gitea-e-no-github)

## Comments

GitHub é o mirror legado; o PR canônico é este no Gitea.

Reviewed-on: #4
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
2026-08-13 11:46:36 +00:00
ba388942c0 Merge pull request 'fix(about): alinhar layout /sobre ao ritmo editorial' (#3) from fix/about-layout-parity into main
All checks were successful
CI / static (push) Successful in 2m20s
CI / unit (push) Successful in 2m45s
CI / feature (push) Successful in 1m52s
CI / container (push) Successful in 15s
CI / browser (push) Successful in 2m42s
2026-08-12 21:23:28 +00:00
37 changed files with 560 additions and 88 deletions

View 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
View 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. -->

View File

@@ -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,6 +71,8 @@ The design system lives in `DESIGN.md` (palette, typography, layout, do's and do
Issues live in Linear, driven through the Linear MCP tools. See `docs/agents/issue-tracker.md` for workspace, team, and tool conventions. The repo ships no `.mcp.json`, so the Linear MCP has to be enabled for the session before those tools exist — if it isn't, report that instead of silently falling back to another tracker.
**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`.

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -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.'.',
),
]);

View File

@@ -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`.

11
package-lock.json generated
View File

@@ -5,6 +5,7 @@
"packages": {
"": {
"devDependencies": {
"@fontsource/eb-garamond": "^5.2.7",
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1",
"daisyui": "^5.7.16",
@@ -49,6 +50,16 @@
"tslib": "^2.4.0"
}
},
"node_modules/@fontsource/eb-garamond": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/eb-garamond/-/eb-garamond-5.3.0.tgz",
"integrity": "sha512-VplwHpB8FLuzl+4G5LDci1b3Z4SghiuXH8XEgaxjnOb5aue5mEt1njmDKeO6iyLeGa73LffU7PC7hzncLRx0ZA==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",

View File

@@ -8,6 +8,7 @@
"prepare": "husky"
},
"devDependencies": {
"@fontsource/eb-garamond": "^5.2.7",
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1",
"daisyui": "^5.7.16",

View File

@@ -134,7 +134,8 @@
overflow-wrap: break-word;
}
[id$='-heading'] {
[id$='-heading'],
.home-chapter[id] {
scroll-margin-top: 5.5rem;
}
@@ -188,6 +189,18 @@
overflow-x: clip;
}
.link-editorial {
background-image: linear-gradient(currentColor, currentColor);
background-position: 0 100%;
background-repeat: no-repeat;
background-size: 100% 1px;
padding-bottom: 1px;
}
.nav-link-editorial {
position: relative;
}
@media (prefers-reduced-motion: no-preference) {
:where(a, button, input, textarea, select) {
transition-duration: var(--amare-motion-feedback);
@@ -199,6 +212,45 @@
transform: translateY(1px);
}
:where(button[aria-busy='true'], button:disabled):active {
transform: none;
}
.field-editorial {
transition:
border-color var(--amare-motion-feedback) var(--amare-ease-arrival),
background-color var(--amare-motion-feedback) var(--amare-ease-arrival);
}
.field-editorial:focus,
.field-editorial:focus-visible {
background-color: var(--amare-color-bg-deep);
}
.link-editorial {
transition: color var(--amare-motion-feedback) var(--amare-ease-arrival);
}
@media (min-width: 768px) {
.nav-link-editorial::after {
background-color: var(--amare-color-accent);
bottom: 0.55rem;
content: '';
height: 1px;
left: 0.75rem;
position: absolute;
right: 0.75rem;
transform: scaleX(0);
transform-origin: left center;
transition: transform var(--amare-motion-feedback) var(--amare-ease-arrival);
}
.nav-link-editorial:hover::after,
.nav-link-editorial:focus-visible::after {
transform: scaleX(1);
}
}
/* Transform/clip only — never fade text opacity (axe + WCAG mid-transition). */
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) [data-motion-beat] {
transform: translateY(var(--amare-motion-distance-y));

View File

@@ -24,7 +24,7 @@
<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>
<span class="link-editorial">Ver portfólio completo</span>
</a>
</div>
</div>

View File

@@ -50,7 +50,7 @@
autocomplete="name"
maxlength="120"
placeholder="Seu nome"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
@error('nome') aria-invalid="true" aria-describedby="briefing-nome-error" @enderror
>
@error('nome')
@@ -69,7 +69,7 @@
autocomplete="email"
maxlength="254"
placeholder="voce@email.com"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
@error('email') aria-invalid="true" aria-describedby="briefing-email-error" @enderror
>
@error('email')
@@ -88,7 +88,7 @@
autocomplete="tel"
maxlength="40"
placeholder="(11) 90000-0000"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
@error('telefone') aria-invalid="true" aria-describedby="briefing-telefone-error" @enderror
>
@error('telefone')
@@ -102,7 +102,7 @@
id="briefing-tipo_evento"
name="tipo_evento"
required
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
class="select field-editorial w-full text-amare-text @error('tipo_evento') select-error @enderror"
@error('tipo_evento') aria-invalid="true" aria-describedby="briefing-tipo_evento-error" @enderror
>
<option value="" selected disabled>Selecione...</option>
@@ -125,7 +125,7 @@
value="{{ old('data_periodo') }}"
maxlength="80"
placeholder="Ex.: novembro de 2027"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
@error('data_periodo') aria-invalid="true" aria-describedby="briefing-data_periodo-error" @enderror
>
@error('data_periodo')
@@ -143,7 +143,7 @@
required
maxlength="80"
placeholder="Ex.: São Paulo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
@error('cidade') aria-invalid="true" aria-describedby="briefing-cidade-error" @enderror
>
@error('cidade')
@@ -163,7 +163,7 @@
inputmode="numeric"
autocomplete="off"
placeholder="Ex.: 120"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
@error('convidados') aria-invalid="true" aria-describedby="briefing-convidados-error" @enderror
>
@error('convidados')
@@ -180,7 +180,7 @@
value="{{ old('servico_interesse') }}"
maxlength="120"
placeholder="Ex.: planejamento completo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
@error('servico_interesse') aria-invalid="true" aria-describedby="briefing-servico_interesse-error" @enderror
>
@error('servico_interesse')
@@ -198,7 +198,7 @@
rows="5"
maxlength="3000"
placeholder="O que já está definido? Em que ponto você precisa de ajuda?"
class="textarea w-full min-h-[110px] text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
class="textarea field-editorial w-full min-h-[110px] text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
@error('mensagem') aria-invalid="true" aria-describedby="briefing-mensagem-error" @enderror
>{{ old('mensagem') }}</textarea>
@error('mensagem')

View File

@@ -19,7 +19,7 @@
@if (filled($settings->hero_secondary_cta_label))
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost 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">{{ $settings->hero_secondary_cta_label }}</span>
<span class="link-editorial">{{ $settings->hero_secondary_cta_label }}</span>
</a>
@endif
</div>

View File

@@ -64,7 +64,7 @@
href="{{ route('packages.show', $package->slug) }}"
class="btn btn-ghost min-h-0 px-0 text-xs font-semibold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"
>
<span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
<span class="link-editorial">Conhecer esta modalidade</span>
</a>
</div>
</article>

View File

@@ -35,7 +35,7 @@
</ul>
<p>
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
<span class="link-editorial">Conhecer a Amare</span>
</a>
</p>
</div>

View File

@@ -27,7 +27,7 @@
<p>
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
<span class="link-editorial">Ver todos os serviços</span>
</a>
</p>
</div>

View File

@@ -9,7 +9,7 @@
<ol class="grid gap-8 border-t border-amare-border pt-6 md:grid-cols-3">
@foreach ($packages as $package)
@php($contactLink = \App\Support\PackageContactLink::for($settings, $package->name))
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><div class="space-y-2"><a href="{{ $contactLink['href'] }}" @if ($contactLink['isWhatsapp']) target="_blank" rel="noopener" @endif class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">{{ $package->cta_label }}</span></a><a href="{{ route('packages.show', $package->slug) }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span></a></div></li>
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><div class="space-y-2"><a href="{{ $contactLink['href'] }}" @if ($contactLink['isWhatsapp']) target="_blank" rel="noopener" @endif class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"><span class="link-editorial">{{ $package->cta_label }}</span></a><a href="{{ route('packages.show', $package->slug) }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"><span class="link-editorial">Conhecer esta modalidade</span></a></div></li>
@endforeach
</ol>
@endif

View File

@@ -0,0 +1,44 @@
@props([
'seal' => 'Recebido',
'title',
'body',
'homeLabel' => 'Voltar para a home',
'whatsappLabel' => 'Falar no WhatsApp',
'whatsappText' => 'Olá, enviei um briefing e gostaria de acompanhar.',
])
@php
$settings = $siteSettings ?? \App\Models\SiteSetting::instance();
$digits = preg_replace('/\D/', '', (string) ($settings->whatsapp_number ?? ''));
$whatsappHref = strlen($digits) >= 10
? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappText)
: null;
@endphp
<div
role="status"
data-form-success
class="mt-10 max-w-2xl space-y-6 border border-amare-border bg-amare-bg-deep px-6 py-10 md:px-10"
data-motion="page-open"
{{ $attributes }}
>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">{{ $seal }}</p>
<h3 class="text-3xl font-medium tracking-tight text-amare-text md:text-4xl" data-motion-beat="title">{{ $title }}</h3>
<p class="max-w-xl text-lg leading-relaxed text-amare-muted" data-motion-beat="heading">{{ $body }}</p>
<div class="flex flex-col items-start gap-4 pt-2 sm:flex-row sm:items-center sm:gap-8" data-motion-beat="cta">
<a href="{{ route('home') }}" class="btn btn-primary min-h-11 px-6 text-xs font-bold uppercase tracking-[0.09em]">
{{ $homeLabel }}
</a>
@if ($whatsappHref)
<a
href="{{ $whatsappHref }}"
class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"
rel="noopener noreferrer"
target="_blank"
>
<span class="link-editorial">{{ $whatsappLabel }}</span>
</a>
@endif
</div>
</div>

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Página não encontrada - Amare Assessoria',
description: 'A página que você procura não existe ou foi movida.',
description: 'Não encontramos este endereço. Volte à home e continue por ali.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 404</p>
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Página não encontrada</h1>
<p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não encontramos este endereço talvez tenha mudado de lugar. A home continua sendo o melhor ponto de partida.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Sessão expirada - Amare Assessoria',
description: 'Sua sessão expirou. Volte e tente enviar novamente.',
description: 'Sua sessão ficou quieta demais. Volte e envie de novo com calma.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 419</p>
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Sessão expirada</h1>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão ficou quieta demais e precisamos recomeçar. Volte à página anterior e envie de novo os dados ainda estão seguros.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Muitas solicitações - Amare Assessoria',
description: 'Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.',
description: 'Recebemos muitos pedidos em pouco tempo. Aguarde um instante e tente de novo.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 429</p>
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Muitas solicitações</h1>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Recebemos vários pedidos em pouco tempo. Espere um instante assim que pudermos, tentamos de novo com calma.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Algo deu errado - Amare Assessoria',
description: 'Não foi possível concluir o pedido. Tente novamente em instantes.',
description: 'Algo saiu do roteiro do nosso lado. Tente novamente em instantes.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 500</p>
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Algo deu errado</h1>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Algo saiu do roteiro do nosso lado. estamos cuidando disso tente novamente em instantes.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Em manutenção - Amare Assessoria',
description: 'Estamos realizando uma breve manutenção. Tente novamente em instantes.',
description: 'Estamos em uma pausa breve de manutenção. Voltamos em instantes.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 503</p>
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Em manutenção</h1>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos em uma pausa breve para deixar tudo no lugar. Voltamos em instantes obrigado pela paciência.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -58,10 +58,10 @@
<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="{{ 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="{{ url('/#casamentos') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Casamentos</a>
<a href="{{ url('/#corporate') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Corporate</a>
<a href="{{ route('portfolio.index') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Portfólio</a>
<a href="{{ route('about') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent 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>
</nav>
@@ -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 &amp; produção de eventos &bull; 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 &amp; produção &bull; São Paulo</p>
</div>
<p class="text-sm text-amare-muted md:text-right">
@if ($footerSocials->isEmpty())
Instagram &middot; WhatsApp &middot; E-mail &middot; 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) &nbsp;&middot;&nbsp; @endunless
@endforeach
@endif
</p>
</div>
<p class="text-sm text-amare-muted md:text-right">
@if ($footerSocials->isEmpty())
Instagram &middot; WhatsApp &middot; E-mail &middot; 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) &nbsp;&middot;&nbsp; @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>&copy; {{ 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>

View File

@@ -42,10 +42,11 @@
</p>
@if (session('status') === 'briefing-sent')
<div role="status" class="alert mt-8 border border-amare-border bg-amare-bg-deep">
<p class="font-semibold text-amare-text">Mensagem enviada.</p>
<p class="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
</div>
<x-public.form-success
seal="Briefing recebido"
title="Recebemos sua ficha."
body="Vamos ler com atenção e retornar em breve pelo canal que você informou. Enquanto isso, pode seguir explorando o portfólio."
/>
@else
<form
method="POST"
@@ -79,7 +80,7 @@
autocomplete="name"
maxlength="120"
placeholder="Seu nome"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
@error('nome') aria-invalid="true" aria-describedby="nome-error" @enderror
>
@error('nome')
@@ -98,7 +99,7 @@
autocomplete="email"
maxlength="254"
placeholder="voce@email.com"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
@error('email') aria-invalid="true" aria-describedby="email-error" @enderror
>
@error('email')
@@ -117,7 +118,7 @@
autocomplete="tel"
maxlength="40"
placeholder="(11) 90000-0000"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
@error('telefone') aria-invalid="true" aria-describedby="telefone-error" @enderror
>
@error('telefone')
@@ -131,7 +132,7 @@
id="tipo_evento"
name="tipo_evento"
required
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
class="select field-editorial w-full text-amare-text @error('tipo_evento') select-error @enderror"
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
>
<option value="" selected disabled>Selecione...</option>
@@ -154,7 +155,7 @@
value="{{ old('data_periodo') }}"
maxlength="80"
placeholder="Ex.: novembro de 2027"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
@error('data_periodo') aria-invalid="true" aria-describedby="data_periodo-error" @enderror
>
@error('data_periodo')
@@ -172,7 +173,7 @@
required
maxlength="80"
placeholder="Ex.: São Paulo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
@error('cidade') aria-invalid="true" aria-describedby="cidade-error" @enderror
>
@error('cidade')
@@ -192,7 +193,7 @@
inputmode="numeric"
autocomplete="off"
placeholder="Ex.: 120"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
@error('convidados') aria-invalid="true" aria-describedby="convidados-error" @enderror
>
@error('convidados')
@@ -209,7 +210,7 @@
value="{{ old('servico_interesse', request('servico_interesse')) }}"
maxlength="120"
placeholder="Ex.: planejamento completo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
@error('servico_interesse') aria-invalid="true" aria-describedby="servico_interesse-error" @enderror
>
@error('servico_interesse')
@@ -227,7 +228,7 @@
rows="6"
maxlength="3000"
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
class="textarea w-full text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
class="textarea field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
@error('mensagem') aria-invalid="true" aria-describedby="mensagem-error" @enderror
>{{ old('mensagem') }}</textarea>
@error('mensagem')

View File

@@ -20,7 +20,12 @@
<div class="container-amare py-16 md:py-24" data-reveal-group>
<h2 id="partner-form-heading" class="text-3xl font-medium text-amare-text" data-reveal data-reveal-from="up">Apresente seu trabalho</h2>
@if (session('status') === 'partner-inquiry-sent')
<div role="status" class="mt-8 border border-amare-border bg-amare-bg px-6 py-5"><p class="font-semibold">Mensagem enviada.</p><p class="mt-1 text-amare-muted">Recebemos sua apresentação e entraremos em contato se houver aderência.</p></div>
<x-public.form-success
seal="Apresentação recebida"
title="Recebemos sua apresentação."
body="Vamos avaliar a aderência com o cuidado que cada parceria merece. Se fizer sentido, entraremos em contato."
whatsapp-text="Olá, enviei uma apresentação de parceria e gostaria de acompanhar."
/>
@else
<form method="POST" action="{{ route('contact.store') }}" class="mt-10 max-w-3xl space-y-8" data-contact-form data-reveal data-reveal-from="up">
@csrf
@@ -28,10 +33,10 @@
@if ($errors->any())<div role="alert" class="border border-amare-error/40 px-6 py-5">Revise os campos destacados abaixo.</div>@endif
<div class="grid gap-8 sm:grid-cols-2">
@foreach (['nome' => 'Nome *', 'empresa' => 'Nome profissional ou empresa', 'email' => 'E-mail *', 'atuacao' => 'Atuação ou serviço *', 'area_atendimento' => 'Área de atendimento *', 'portfolio_redes' => 'Portfólio ou redes', 'telefone' => 'Telefone'] as $field => $label)
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="{{ $field }}">{{ $label }}</label><input class="w-full border-0 border-b border-amare-border bg-transparent py-3 focus:border-amare-accent focus:outline-none @error($field) border-amare-error @enderror" id="{{ $field }}" name="{{ $field }}" value="{{ old($field) }}" maxlength="{{ $field === 'portfolio_redes' ? 500 : ($field === 'email' ? 254 : 160) }}" @required(in_array($field, ['nome','email','atuacao','area_atendimento'], true)) @error($field) aria-invalid="true" @enderror>@error($field)<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="{{ $field }}">{{ $label }}</label><input class="field-editorial w-full border-0 border-b border-amare-border bg-transparent py-3 focus:border-amare-accent focus:outline-none @error($field) border-amare-error @enderror" id="{{ $field }}" name="{{ $field }}" value="{{ old($field) }}" maxlength="{{ $field === 'portfolio_redes' ? 500 : ($field === 'email' ? 254 : 160) }}" @required(in_array($field, ['nome','email','atuacao','area_atendimento'], true)) @error($field) aria-invalid="true" @enderror>@error($field)<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
@endforeach
</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="field-editorial w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<p class="max-w-2xl text-sm text-amare-muted">Ao enviar, você reconhece o uso destes dados exclusivamente para analisar uma possível parceria, conforme a <a class="text-amare-accent underline" href="{{ route('privacy') }}">política de privacidade</a>.</p>
<button type="submit" data-submit-button class="btn btn-primary text-sm font-semibold uppercase tracking-[.12em]">Enviar apresentação</button>
</form>

View File

@@ -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>

View File

@@ -13,7 +13,7 @@
Conhecer os serviços
</a>
<a href="{{ route('contact') }}" class="btn btn-ghost 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">Tenho um evento em mente</span>
<span class="link-editorial">Tenho um evento em mente</span>
</a>
</div>
</x-public.photo-hero>
@@ -65,7 +65,7 @@
<p class="text-amare-text-muted">{{ $package->compare_summary }}</p>
</div>
<a href="{{ route('packages.show', $package->slug) }}" class="btn btn-ghost min-h-0 self-start px-0 text-xs font-semibold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
<span class="link-editorial">Conhecer esta modalidade</span>
</a>
</article>
@endif

View File

@@ -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);
}
}

View File

@@ -89,6 +89,28 @@ class ContactBriefingTest extends TestCase
});
}
public function test_successful_submission_shows_editorial_confirmation(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
Mail::fake();
$response = $this
->from(route('briefing'))
->followingRedirects()
->post(route('briefing.store'), $this->validPayload());
$response
->assertOk()
->assertSee('data-form-success', false)
->assertSee('Briefing recebido')
->assertSee('Recebemos sua ficha')
->assertSee('role="status"', false)
->assertSee('data-motion-beat="seal"', false)
->assertSee(route('home'), false)
->assertSee('https://wa.me/5511988887777', false)
->assertDontSee('<form', false);
}
public function test_submission_normalizes_email_and_phone_before_reaching_mailables(): void
{
$settings = SiteSetting::instance();

View File

@@ -72,7 +72,19 @@ class HeritageEditorialTokensTest extends TestCase
{
$vite = (string) file_get_contents(base_path('vite.config.js'));
$this->assertStringContainsString("bunny('EB Garamond'", $vite);
$this->assertMatchesRegularExpression(
"/^import \\{ fontsource \\} from 'laravel-vite-plugin\\/fonts';/m",
$vite,
);
$this->assertDoesNotMatchRegularExpression(
'/^import \\{[^}]*\\bbunny\\b/m',
$vite,
);
$this->assertDoesNotMatchRegularExpression(
'/^import \\{[^}]*\\bgoogle\\b/m',
$vite,
);
$this->assertStringContainsString("fontsource('EB Garamond'", $vite);
$this->assertStringNotContainsString('Instrument Sans', $vite);
}

View File

@@ -96,6 +96,9 @@ class MotionMarkupTest extends TestCase
$this->assertStringContainsString('[data-reveal]', $appCss);
$this->assertStringNotContainsString('[data-chapter-index]', $appCss);
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
$this->assertStringContainsString('.field-editorial', $appCss);
$this->assertStringContainsString('.nav-link-editorial', $appCss);
$this->assertStringContainsString('.link-editorial', $appCss);
// Entrance motion must not fade text opacity — mid-fade fails WCAG contrast (axe).
$this->assertDoesNotMatchRegularExpression(

View File

@@ -66,6 +66,22 @@ class PartnerInquiryTest extends TestCase
Mail::assertQueued(PartnerInquiry::class, 1);
}
public function test_successful_partner_inquiry_shows_editorial_confirmation(): void
{
SiteSetting::instance();
Mail::fake();
$this
->from(route('contact'))
->followingRedirects()
->post(route('contact.store'), $this->validPayload())
->assertOk()
->assertSee('data-form-success', false)
->assertSee('Apresentação recebida')
->assertSee('Recebemos sua apresentação')
->assertDontSee('name="atuacao"', false);
}
public function test_partner_honeypot_reports_success_without_sending_mail(): void
{
SiteSetting::instance();

View 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');
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import { fontsource } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
/**
@@ -76,7 +76,7 @@ export default defineConfig({
],
refresh: true,
fonts: [
bunny('EB Garamond', {
fontsource('EB Garamond', {
weights: [400, 500, 600],
}),
],