Compare commits

..

1 Commits

Author SHA1 Message Date
50c5650228 docs: inventariar merges órfãos Gitea (MAN-136)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 13:37:14 -03:00
10 changed files with 129 additions and 280 deletions

View File

@@ -4,10 +4,7 @@ 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
@@ -15,37 +12,12 @@ final class GetPublishedPortfolioCases
/**
* @return Collection<int, PortfolioCase>
*/
public function __invoke(?PortfolioVertical $vertical = null): Collection
public function __invoke(): Collection
{
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()
return PortfolioCase::query()
->published()
->with(['images'])
->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;
->orderBy('sort_order')
->get();
}
}

View File

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

View File

@@ -6,35 +6,33 @@ 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(Request $request, GetPublishedPortfolioCases $getPublishedPortfolioCases): View
public function index(): View
{
$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', [
'cases' => $cases,
'vertical' => $vertical,
'siteSettings' => $settings,
'pageMeta' => PageMeta::forPage(
canonical: route('portfolio.index', array_filter([
'vertente' => $vertical?->value,
])),
canonical: route('portfolio.index'),
settings: $settings,
title: PageMeta::withBrandSuffix(
$vertical === null ? 'Portfólio' : 'Portfólio — '.$vertical->label(),
$settings,
),
title: PageMeta::withBrandSuffix('Portfólio', $settings),
description: 'Casos reais de eventos conduzidos pela '.$settings->brand_name.'.',
),
]);

View File

@@ -0,0 +1,92 @@
# MAN-136 — Inventário de merges órfãos no Gitea
**Issue:** [MAN-136](https://linear.app/maneco-workspace/issue/MAN-136/research-inventariar-merges-orfaos-no-gitea)
**Branch:** `research/man-136-orphan-gitea-merges`
**HEAD inventariado:** `origin/main` @ `93b680c`
**Data:** 2026-08-13
**Escopo:** inventário only — **não** criar issues Linear retroativas.
## Critério de janela
**Início inclusivo:** commit `84f6d7c` (`ci: migrate GitHub Actions para Gitea Actions`, 2026-08-12).
**Fim:** `HEAD` de `origin/main` (`93b680c`).
Justificativa: primeiro commit que troca CI/registry/docs para Gitea (`git.hellomanoel.com`). Commits anteriores (GitHub PR #5#60 era) ficam fora. A fronteira é o SHA da migração de Actions, não a data de criação do repo Gitea.
## Critério de “issue correspondente”
Um merge/commit **tem** issue Linear correspondente no projeto **Amare — Lançamento MVP** se **qualquer** for verdade:
1. Body/assunto do PR ou commit cita identifier `MAN-xxx` desse projeto; ou
2. Existe issue no projeto cujo título + critérios de aceite descrevem de forma única a mudança shipped (match funcional 1:1).
**Não conta como correspondente:**
- Epic/issue já **Done** antes do merge que “poderia” cobrir trabalho novo (ex.: MAN-102 Done em 2026-08-08 vs remodel `/sobre` em 2026-08-12).
- Issue ampla de copy/arquitetura sem AC específico para o ship (ex.: MAN-97).
- Mencionar “Gitea” só em issues de template (MAN-133) não cobre a migração de CI.
Classificação pedida: **órfão shipped** = chegou em `main` sem issue correspondente. Sem criar tickets.
## Fontes
| Fonte | Uso |
| --- | --- |
| `git log origin/main` (`84f6d7c^..HEAD`) | commits da janela |
| `tea pr list --state all` + API Gitea `/pulls/{1..4}` | PRs Gitea merged |
| Linear `list_issues` project `Amare — Lançamento MVP` (incl. archived) | universo de issues |
## Matriz PRs Gitea #1#4
| PR | Título | Merge SHA | Issue Linear? | Classificação |
| --- | --- | --- | --- | --- |
| [#1](https://git.hellomanoel.com/manoel-freitas/amare/pulls/1) | feat: remodelar `/sobre` conforme mock editorial | `fc1c617` | Nenhuma (sem citação; MAN-102 já Done; sem issue dedicada) | **órfão shipped** |
| [#2](https://git.hellomanoel.com/manoel-freitas/amare/pulls/2) | fix: CTAs de modalidades em `/servicos` via WhatsApp | `e2af1f6` | [MAN-127](https://linear.app/maneco-workspace/issue/MAN-127/ajustar-ctas-consultivos-no-whatsapp-e-briefing-comercial) (Fluxo 1 — CTAs WhatsApp nos cards de modalidade; PR não cita, match por AC) | matched (não órfão) |
| [#3](https://git.hellomanoel.com/manoel-freitas/amare/pulls/3) | fix(about): alinhar layout `/sobre` ao ritmo editorial | `ba38894` | Nenhuma (follow-up de #1; sem citação) | **órfão shipped** |
| [#4](https://git.hellomanoel.com/manoel-freitas/amare/pulls/4) | docs: require Linear issues and auto-ship PRs | `93b680c` | [MAN-132](https://linear.app/maneco-workspace/issue/MAN-132/exigir-issue-linear-em-todo-trabalho-e-pr) + [MAN-133](https://linear.app/maneco-workspace/issue/MAN-133/adicionar-pr-template-no-gitea-e-no-github) (citados no body) | matched (não órfão) |
## Órfãos shipped (tabela canônica)
| Nome descritivo | Evidência | Link |
| --- | --- | --- |
| Remodelar `/sobre` conforme mock editorial | Gitea PR #1 merged | https://git.hellomanoel.com/manoel-freitas/amare/pulls/1 |
| Alinhar layout `/sobre` ao ritmo editorial | Gitea PR #3 merged | https://git.hellomanoel.com/manoel-freitas/amare/pulls/3 |
| Migração CI GitHub → Gitea Actions (+ follow-ups de runner/registry/staging) | 10 commits direct-push em `main`, **sem** PR Gitea | sem PR — ver série abaixo |
### Série vizinha sem PR (direct push)
Todos sem citação `MAN-*` e sem issue de migração Gitea no projeto Amare MVP:
| SHA | Assunto |
| --- | --- |
| `84f6d7c` | ci: migrate GitHub Actions para Gitea Actions |
| `716196d` | fix: owner path manoel-freitas/amare no registry Gitea |
| `1e215ac` | docs: alinhar remotes e registry para Gitea |
| `f730e76` | fix: secrets de registry sem prefixo GITEA_ |
| `1468a23` | fix: remover actions/cache no Gitea Actions |
| `87a0191` | fix: CI Postgres via hostname, sem bind :5432 no host |
| `dd82ca2` | fix: paralelizar CI no act_runner |
| `91c0d41` | fix: nomes únicos para containers CI no docker.sock compartilhado |
| `ac08b5f` | fix: staging deploy via needs no CI, não workflow_run |
| `26a68e1` | fix: restaurar deploy-staging via workflow_run (Gitea ≥1.25) |
Tratados como **um** órfão shipped agregado (mesmo tema ops/CI), não dez tickets potenciais.
## Não-órfãos na janela (referência)
- PR #2 → MAN-127 (parcial; issue ainda Todo no Linear — status Linear ≠ presença de issue).
- PR #4 → MAN-132 + MAN-133.
## Ambiguidades registradas
1. **PR #2 sem citação explícita:** match por AC de MAN-127 (Fluxo 1). Se o inventário MAN-135 exigir citação literal, reclassificar #2 como órfão — este doc **não** faz isso.
2. **`/sobre` vs MAN-97 / MAN-98 / MAN-102:** rejeitado como correspondente (Done prévio ou escopo sem AC de remodel layout).
3. **Merge commits vs squash:** PR #1 landa como `fc1c617` (sem merge commit separado); PR #3 tem merge `ba38894`. Ambos contam como merges shipped.
## Resumo executivo
**3 órfãos shipped** na era pós-migração Gitea:
1. Remodelar `/sobre` — [PR #1](https://git.hellomanoel.com/manoel-freitas/amare/pulls/1)
2. Fix layout `/sobre` — [PR #3](https://git.hellomanoel.com/manoel-freitas/amare/pulls/3)
3. Migração CI/Gitea Actions (+ follow-ups) — direct push, sem PR

View File

@@ -134,8 +134,7 @@
overflow-wrap: break-word;
}
[id$='-heading'],
.home-chapter[id] {
[id$='-heading'] {
scroll-margin-top: 5.5rem;
}

View File

@@ -58,8 +58,8 @@
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 backdrop-blur-sm">
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr] md:h-[74px] md:py-0">
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
<a href="{{ url('/#casamentos') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Casamentos</a>
<a href="{{ url('/#corporate') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Corporate</a>
<a href="{{ route('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="{{ 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">
Conte seu evento
</a>
</a>
<button
type="button"
@@ -110,11 +111,10 @@
$footerSocials = collect($siteSettings->social_links ?? [])->filter(static fn ($url) => filled($url));
@endphp
<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="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">Boutique de assessoria &amp; produção &bull; São Paulo</p>
<p class="text-sm text-amare-muted">Assessoria &amp; produção de eventos &bull; São Paulo</p>
</div>
<p class="text-sm text-amare-muted md:text-right">
@@ -128,21 +128,12 @@
</p>
</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">
<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

@@ -5,38 +5,9 @@
<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())
@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;
@@ -82,9 +53,6 @@
@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

@@ -5,7 +5,6 @@ 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;
@@ -59,26 +58,4 @@ 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

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

View File

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