Compare commits

..

1 Commits

Author SHA1 Message Date
400c147a74 feat: CTA dos cards e faixa de modalidades enviam ao WhatsApp com mensagem por pacote
Campo whatsapp_message editável por modalidade (Filament) com fallback para
template padrão. Cards da home, faixa 'Conversar com a Amare' e CTA final
da página de detalhe abrem o WhatsApp diretamente quando número configurado,
mantendo briefing como fallback.
2026-08-12 10:11:06 -03:00
20 changed files with 192 additions and 78 deletions

View File

@@ -6,7 +6,7 @@ on:
pull_request:
concurrency:
group: ci-${{ gitea.workflow }}-${{ gitea.ref }}
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:

View File

@@ -15,27 +15,27 @@ concurrency:
cancel-in-progress: false
env:
REGISTRY: git.hellomanoel.com
IMAGE_NAME: ${{ gitea.repository }}
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
deploy:
name: publish-and-deploy-staging
if: >-
gitea.event.workflow_run.conclusion == 'success' &&
gitea.event.workflow_run.event == 'push' &&
gitea.event.workflow_run.head_branch == 'main'
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest
steps:
- name: Checkout deployed SHA
uses: actions/checkout@v4
with:
ref: ${{ gitea.event.workflow_run.head_sha }}
ref: ${{ github.event.workflow_run.head_sha }}
- name: Set image metadata
id: meta
run: |
SHA="${{ gitea.event.workflow_run.head_sha }}"
SHA="${{ github.event.workflow_run.head_sha }}"
SHORT_SHA="${SHA:0:7}"
IMAGE="${REGISTRY}/${IMAGE_NAME}"
IMAGE="$(echo "$IMAGE" | tr '[:upper:]' '[:lower:]')"
@@ -43,14 +43,12 @@ jobs:
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea registry
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
# Gitea's GITEA_TOKEN cannot push OCI packages (gitea#23642); a PAT
# with read:package/write:package scopes is required instead.
username: ${{ secrets.GITEA_REGISTRY_USER }}
password: ${{ secrets.GITEA_PAT }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
sha:
description: Full git SHA already published to the Gitea registry (same digest used by staging)
description: Full git SHA already published to GHCR (same digest used by staging)
required: true
type: string
confirm:
@@ -21,8 +21,8 @@ concurrency:
cancel-in-progress: false
env:
REGISTRY: git.hellomanoel.com
IMAGE_NAME: ${{ gitea.repository }}
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
promote:
@@ -50,14 +50,12 @@ jobs:
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea registry
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
# Gitea's GITEA_TOKEN cannot push OCI packages (gitea#23642); a PAT
# with read:package/write:package scopes is required instead.
username: ${{ secrets.GITEA_REGISTRY_USER }}
password: ${{ secrets.GITEA_PAT }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -22,14 +22,13 @@ Write for the problem at hand, not an imagined future. **DRY**: extract and reus
PHP and Composer are **not on PATH** in this environment, and `vendor/` and `node_modules/` are absent. Every `composer …` / `php artisan …` command in `AGENTS.md` and `README.md` assumes a PHP 8.4+ runtime with Composer 2 installed. Verify the toolchain before promising a command ran.
## Git remotes — Gitea origin, GitHub legacy
## Git remote auth — two GitHub accounts
`origin` is `git@git.hellomanoel.com:manoel-freitas/amare.git` (self-hosted Gitea). CI/CD and the container registry live there (`git.hellomanoel.com`). Verify SSH with `ssh -T git@git.hellomanoel.com` → should greet `Hi there, manoel-freitas!`.
`origin` is `git@github.com:manoel-freitas/amore-site.git`, owned by the **`manoel-freitas`** account. The machine's default SSH identity is a different account (`manoel-freitas-neto`) that cannot see this repo, so pushes fail with `ERROR: Repository not found.` — an access error that reads like a missing repo.
The remote named `github` is the legacy mirror `git@github.com:manoel-freitas/amore-site.git`. Push there only when intentionally syncing the backup. That GitHub account still needs `~/.ssh/id_github_pessoal` (or an equivalent key) when the machine's default identity is a different GitHub user (`manoel-freitas-neto`) that cannot see the repo.
- Prefer plain `git push` / `git push origin <branch>` against Gitea.
- **`gh`** talks to GitHub only. Use the Gitea web UI or API for PRs on `amare`. If you still need `gh` against the legacy remote, confirm `gh auth status` shows `manoel-freitas`.
- Correct key: `~/.ssh/id_github_pessoal`. Verify with `ssh -i ~/.ssh/id_github_pessoal -o IdentitiesOnly=yes -T git@github.com` → should greet `Hi manoel-freitas!`.
- The repo has `core.sshCommand = ssh -i ~/.ssh/id_github_pessoal -o IdentitiesOnly=yes` set locally, so plain `git push` works. If that config is lost, restore it instead of editing the remote URL.
- **`gh` authenticates separately**, by token rather than SSH key. As of 2026-08-10 it is logged in as `manoel-freitas`, so `gh pr create` / `gh repo view` work. Confirm with `gh auth status` before assuming: if it reports `manoel-freitas-neto`, that account cannot see this repo and every `gh` call fails on it. Recovering needs an interactive `gh auth login` (or `gh auth switch` with both accounts added), so ask the user to run it.
## Request spine for the public site

View File

@@ -138,10 +138,10 @@ Após `php artisan db:seed`:
- [docs/adr/](docs/adr/) — ADRs aceitas
- [docs/conventions/php-strict-types.md](docs/conventions/php-strict-types.md) — convenção de strict types
- [docs/operations/atualizacao-de-conteudo.md](docs/operations/atualizacao-de-conteudo.md) — runbook de atualização de conteúdo do site pelo painel admin
- [docs/deployment/dokploy.md](docs/deployment/dokploy.md) — deploy staging/produção no Dokploy + registry Gitea
- [docs/deployment/dokploy.md](docs/deployment/dokploy.md) — deploy staging/produção no Dokploy + GHCR
## Deploy (Dokploy)
Staging publica automaticamente após CI verde em `main` (imagem no registry Gitea por SHA + alias `:staging`). Produção promove a **mesma digest** com workflow manual `Promote production` (sem rebuild).
Staging publica automaticamente após CI verde em `main` (imagem GHCR por SHA + alias `:staging`). Produção promove a **mesma digest** com workflow manual `Promote production` (sem rebuild).
Ver runbook completo: [docs/deployment/dokploy.md](docs/deployment/dokploy.md).

View File

@@ -2073,7 +2073,7 @@ quality → Pint check + PHPStan/Larastan + audits + testes
### 14.3 Branches e ambientes
- PR: testes e preview opcional;
- `main`: build imutável por SHA publicado no registry Gitea (`git.hellomanoel.com`) e deploy automático em staging via Dokploy;
- `main`: build imutável por SHA publicado no GHCR e deploy automático em staging via Dokploy;
- staging: Dokploy Compose executa migração, healthcheck `/up` e smoke pós-deploy (`/up`, `/`, `/admin/login`);
- produção: promoção da mesma imagem aprovada, sem rebuild (retag do digest em `:production`);
- produção requer aprovação humana explícita no MVP (`workflow_dispatch` com confirmação);
@@ -2487,7 +2487,7 @@ Toda operação financeira deve:
| ADR-011 | Cloudflare R2 (S3-compatible) como storage de objetos em produção | Aceita |
| ADR-012 | E-mail transacional via Resend (mailer nativo Laravel) | Aceita |
| ADR-013 | Design system Heritage Editorial para o site público | Aceita |
| ADR-014 | Deploy via Dokploy Compose com imagem imutável por SHA no registry Gitea | Aceita |
| ADR-014 | Deploy via Dokploy Compose com imagem imutável por SHA no GHCR | Aceita |
| ADR-015 | Site público permanece Blade + JS vanilla; Livewire e Alpine ficam restritos ao Filament até o gatilho de §22. Emenda o texto da ADR-002 | Aceita |
| ADR-016 | Lançamento de 31/08/2026 entrega apenas o site institucional (Fases 01); Fases 25 seguem especificadas e adiadas, sem data | Aceita |

View File

@@ -67,6 +67,10 @@ class WeddingPackageForm
->label('Texto do botão')
->required()
->maxLength(255),
Textarea::make('whatsapp_message')
->label('Mensagem do WhatsApp (deixe vazio para usar o padrão)')
->helperText('Texto enviado ao cliente ao tocar no botão de contato. O nome da modalidade é citado automaticamente se vazio.')
->rows(3),
TextInput::make('compare_heading')
->label('Comparação — título (leitura rápida)')
->maxLength(255),

View File

@@ -23,6 +23,7 @@ use Illuminate\Support\Str;
* @property string $summary
* @property list<string> $scope_items
* @property string $cta_label
* @property string|null $whatsapp_message
* @property string|null $compare_heading
* @property string|null $compare_summary
* @property string|null $eyebrow
@@ -54,6 +55,7 @@ use Illuminate\Support\Str;
'summary',
'scope_items',
'cta_label',
'whatsapp_message',
'compare_heading',
'compare_summary',
'sort_order',

View File

@@ -11,17 +11,47 @@ final class PackageContactLink
/**
* Resolve the contextual contact link for a wedding package modality.
*
* Uses the package-specific WhatsApp message when provided; otherwise
* falls back to the default template with the modality name.
*
* @return array{href: string, isWhatsapp: bool}
*/
public static function for(SiteSetting $settings, string $packageName): array
public static function for(SiteSetting $settings, string $packageName, ?string $customMessage = null): array
{
$message = filled($customMessage)
? $customMessage
: 'Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.';
return self::build($settings, $message, $packageName);
}
/**
* Resolve a generic contact link (no specific modality), used by the
* "Conversar com a Amare" guidance band.
*
* @return array{href: string, isWhatsapp: bool}
*/
public static function generic(SiteSetting $settings): array
{
return self::build(
$settings,
'Olá, ainda estou decidindo a modalidade ideal para o meu casamento. Podemos conversar?',
null,
);
}
/**
* @return array{href: string, isWhatsapp: bool}
*/
private static function build(SiteSetting $settings, string $message, ?string $packageName): array
{
$digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
$isWhatsapp = strlen($digits) >= 10;
return [
'href' => $isWhatsapp
? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.')
: route('briefing', ['servico_interesse' => $packageName]),
? 'https://wa.me/'.$digits.'?text='.rawurlencode($message)
: route('briefing', $packageName !== null ? ['servico_interesse' => $packageName] : []),
'isWhatsapp' => $isWhatsapp,
];
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('wedding_packages', function (Blueprint $table): void {
$table->text('whatsapp_message')->nullable()->after('cta_label');
});
}
public function down(): void
{
Schema::table('wedding_packages', function (Blueprint $table): void {
$table->dropColumn('whatsapp_message');
});
}
};

View File

@@ -1,6 +1,6 @@
# Shared Compose for Dokploy staging and production.
# Both stacks use the same file with different env:
# APP_IMAGE=git.hellomanoel.com/manoel-freitas/amare
# APP_IMAGE=ghcr.io/<owner>/<repo>
# IMAGE_TAG=staging|production|<git-sha>
# PostgreSQL is a separate Dokploy database service (not defined here).
# Traefik/Dokploy domains should target service `web` port 8000.

View File

@@ -1,11 +1,11 @@
# Deploy Dokploy (staging → production)
Runbook for operating Amare on a VPS with Dokploy connected to Gitea (git.hellomanoel.com), publishing immutable images to Gitea's container registry.
Runbook for operating Amare on a VPS with Dokploy connected to GitHub, publishing immutable images to GHCR.
## Architecture
```
CI (main) → build FrankenPHP image → git.hellomanoel.com registry :<sha> + :staging
CI (main) → build FrankenPHP image → GHCR :<sha> + :staging
→ Dokploy staging compose.deploy
→ smoke /up / /admin/login
@@ -18,7 +18,7 @@ Promote (manual) → retag same digest as :production (no rebuild)
|---|---|
| Compose file | [`docker-compose.deploy.yml`](../../docker-compose.deploy.yml) |
| Processes | `migrate` (one-shot) → `web` / `queue` / `scheduler` |
| Image | `git.hellomanoel.com/manoel-freitas/amare:<sha>` (+ aliases `:staging`, `:production`) |
| Image | `ghcr.io/<owner>/<repo>:<sha>` (+ aliases `:staging`, `:production`) |
| Database | Dokploy PostgreSQL **per environment** (not in the app image) |
| Media | Cloudflare R2 (`FILESYSTEM_DISK=r2`), separate buckets per environment |
| Mail | Resend (`MAIL_MAILER=resend`) |
@@ -26,14 +26,11 @@ Promote (manual) → retag same digest as :production (no rebuild)
## Prerequisites (manual)
1. Gitea repository `manoel-freitas/amare` at `https://git.hellomanoel.com`; **Repository Actions enabled** in repo settings; a registered **Gitea Actions runner** (see [docs.gitea.com usage/actions/quickstart](https://docs.gitea.com/usage/actions/quickstart)) with an `ubuntu-latest` label.
2. Dokploy installed on the VPS.
3. Container registry in Dokploy (`git.hellomanoel.com`) with a PAT that can **read** packages (`read:package`). Prefer a dedicated bot/token; do not store write tokens on the VPS. The **write** PAT (`write:package`) lives only in Gitea repo secrets for the pipeline.
4. Two PostgreSQL services in Dokploy (staging + production), private (no public port).
5. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
6. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
Note: Gitea's `GITEA_TOKEN` cannot push OCI packages ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication in the workflows uses a PAT (`GITEA_PAT`), not the token.
1. Dokploy installed on the VPS; GitHub provider connected.
2. GHCR registry in Dokploy (`ghcr.io`) with a PAT that can **read** packages (`read:packages`). Prefer a dedicated bot/token; do not store write tokens on the VPS.
3. Two PostgreSQL services in Dokploy (staging + production), private (no public port).
4. Two R2 buckets (or prefixes) and Resend credentials for each environment as needed.
5. Domains (or temporary Dokploy/traefik.me hosts) pointing at the VPS with TLS.
## Create Compose stacks
@@ -47,7 +44,7 @@ Create **two** Dokploy Compose services (same repo, same compose path):
Dokploy Environment for each stack must set:
```bash
APP_IMAGE=git.hellomanoel.com/manoel-freitas/amare
APP_IMAGE=ghcr.io/<owner>/<repo>
IMAGE_TAG=staging # or production
```
@@ -55,11 +52,11 @@ Point Dokploy domain(s) at service **`web`**, port **`8000`**. Do not publish Po
Compose services must join the external Docker network `dokploy-network` (declared in `docker-compose.deploy.yml`) so they can resolve the Dokploy-managed Postgres internal host (e.g. `amare-stg-pez43e`). Set `DB_HOST` to that **Internal Host** from the Dokploy database UI — not a public hostname.
Source can be Gitea (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer Gitea + fixed compose path so updates stay in sync with `main`.
Source can be GitHub (so Dokploy clones the compose file) or Raw paste of `docker-compose.deploy.yml`. Prefer GitHub + fixed compose path so updates stay in sync with `main`.
## Required Laravel env (Dokploy only)
Set these in Dokploy Environment UI (written to `.env` next to the compose file). **Never** put them in Gitea Actions secrets or image layers.
Set these in Dokploy Environment UI (written to `.env` next to the compose file). **Never** put them in GitHub Actions secrets or image layers.
```env
APP_NAME=Amare
@@ -123,9 +120,9 @@ Upload path does not need R2 CORS with the local temp-disk default. Still useful
Also enable public access / custom domain for `R2_URL` so `<img>` URLs work after save.
## Gitea Actions secrets
## GitHub Actions secrets
Repository secrets (Gitea → Settings → Actions → Secrets) used by workflows:
Repository secrets used by workflows:
| Secret | Purpose |
|---|---|
@@ -135,36 +132,32 @@ Repository secrets (Gitea → Settings → Actions → Secrets) used by workflow
| `DOKPLOY_PRODUCTION_COMPOSE_ID` | Production **Compose** service id (not an Application id) |
| `STAGING_URL` | Public origin for staging smoke (e.g. `https://staging.example.com`) |
| `PRODUCTION_URL` | Public origin for production smoke |
| `GITEA_PAT` | Personal Access Token with `read:package` + `write:package` scopes — used to push images to `git.hellomanoel.com` |
| `GITEA_REGISTRY_USER` | Gitea username that owns `GITEA_PAT` (`manoel-freitas`) |
HTTP 404 from `compose.deploy` usually means the compose id is wrong (Application id instead of Compose) or `DOKPLOY_URL` still includes `/api`.
The automatic `GITEA_TOKEN` runs workflows but **cannot push OCI packages** ([gitea#23642](https://github.com/go-gitea/gitea/issues/23642)); registry authentication therefore uses `GITEA_PAT` + `GITEA_REGISTRY_USER`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in Gitea for this pipeline.
`GITHUB_TOKEN` (automatic) publishes to GHCR with `packages:write`. No Laravel/`APP_KEY`/DB/R2/Resend secrets belong in GitHub for this pipeline.
## Workflows
Workflows live in [`.gitea/workflows/`](../../.gitea/workflows/).
### Staging (automatic)
[`.gitea/workflows/deploy-staging.yml`](../../.gitea/workflows/deploy-staging.yml)
[`.github/workflows/deploy-staging.yml`](../../.github/workflows/deploy-staging.yml)
1. Waits for workflow `CI` success (`workflow_run`) on push to `main`.
2. Builds once; pushes `:<full-sha>` and `:staging` to the Gitea registry.
1. Waits for workflow `CI` success on push to `main`.
2. Builds once; pushes `:<full-sha>` and `:staging`.
3. Calls Dokploy `compose.deploy` and polls until done.
4. Runs [`scripts/deploy/smoke.sh`](../../scripts/deploy/smoke.sh) against `STAGING_URL`.
### Production (manual)
[`.gitea/workflows/promote-production.yml`](../../.gitea/workflows/promote-production.yml)
[`.github/workflows/promote-production.yml`](../../.github/workflows/promote-production.yml)
1. Operator runs **Gitea → Actions → Promote production**.
2. Inputs: full `sha` already in the Gitea registry; `confirm` must be exactly `PRODUCTION`.
1. Operator runs **Actions → Promote production**.
2. Inputs: full `sha` already on GHCR; `confirm` must be exactly `PRODUCTION`.
3. Retags the **same digest** as `:production` (no rebuild).
4. Deploys production compose + smoke.
Human approval is the explicit `workflow_dispatch` + confirmation string (Gitea does not support GitHub Environment required reviewers).
Private repos on GitHub Free do not get Environment required reviewers; human approval is the explicit `workflow_dispatch` + confirmation string. GitHub Pro Environment reviewers are optional later.
## First admin and authorized production seeding
@@ -263,10 +256,10 @@ No rebuild. Move the environment alias to a previous SHA digest and redeploy.
### Staging
```bash
# Locally or in a one-off Actions shell with Gitea registry login
# Locally or in a one-off Actions shell with GHCR login
docker buildx imagetools create \
--tag git.hellomanoel.com/manoel-freitas/amare:staging \
git.hellomanoel.com/manoel-freitas/amare:<previous-sha>
--tag ghcr.io/<owner>/<repo>:staging \
ghcr.io/<owner>/<repo>:<previous-sha>
# Then trigger Dokploy deploy (UI Deploy, or):
DOKPLOY_URL=... DOKPLOY_API_KEY=... DOKPLOY_COMPOSE_ID=... \
@@ -304,7 +297,7 @@ Expects HTTP 200 for `/up`, `/`, and `/admin/login`.
## Local validation of Compose
```bash
APP_IMAGE=git.hellomanoel.com/manoel-freitas/amare IMAGE_TAG=staging \
APP_IMAGE=ghcr.io/<owner>/<repo> IMAGE_TAG=staging \
docker compose -f docker-compose.deploy.yml config
```

View File

@@ -2,18 +2,18 @@
## Purpose
Define immutable staging and production promotion through the Gitea container registry (`git.hellomanoel.com`) and Dokploy Compose, including migration, health, smoke, rollback, and backup evidence.
Define immutable staging and production promotion through GHCR and Dokploy Compose, including migration, health, smoke, rollback, and backup evidence.
## Requirements
### Requirement: Staging deploys an immutable application image by commit SHA
The system SHALL deploy staging from a single FrankenPHP application image tagged with the Git commit SHA and published to the Gitea container registry (SPEC §14.3, §15.2). Web, queue worker, and scheduler processes MUST use that same image digest. Rebuilds per process on the staging host MUST NOT be the promotion path.
The system SHALL deploy staging from a single FrankenPHP application image tagged with the Git commit SHA and published to GHCR (SPEC §14.3, §15.2). Web, queue worker, and scheduler processes MUST use that same image digest. Rebuilds per process on the staging host MUST NOT be the promotion path.
#### Scenario: Same image serves all application processes
- **WHEN** a staging deployment is promoted for commit SHA `abc123`
- **THEN** web, queue, and scheduler MUST run from `git.hellomanoel.com/<owner>/<repo>:abc123` (or equivalent digest)
- **THEN** web, queue, and scheduler MUST run from `ghcr.io/<owner>/<repo>:abc123` (or equivalent digest)
- **AND** MUST NOT rebuild distinct images per process
#### Scenario: CI publishes the image before Dokploy deploy
@@ -69,7 +69,7 @@ Production SHALL be promoted from an already-published SHA-tagged image without
#### Scenario: Operator promotes a staging-approved SHA to production
- **WHEN** the operator confirms promotion of commit SHA `abc123`
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `git.hellomanoel.com/<owner>/<repo>:abc123`
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `ghcr.io/<owner>/<repo>:abc123`
- **AND** MUST NOT rebuild from source for that promotion
### Requirement: Database backups exist before production cutover

View File

@@ -15,7 +15,9 @@
])
@php
$bandHref = $bandCtaHref ?? route('briefing');
$bandLink = $bandCtaHref
? ['href' => $bandCtaHref, 'isWhatsapp' => false]
: \App\Support\PackageContactLink::generic($settings);
@endphp
<section
aria-labelledby="packages-heading"
@@ -52,7 +54,7 @@
</ul>
</div>
<div class="space-y-5">
@php($contactLink = $ctaRoute ? null : \App\Support\PackageContactLink::for($settings, $package->name))
@php($contactLink = $ctaRoute ? null : \App\Support\PackageContactLink::for($settings, $package->name, $package->whatsapp_message))
<a
href="{{ $ctaRoute ? route($ctaRoute) : $contactLink['href'] }}"
@if ($ctaRoute === null && $contactLink['isWhatsapp']) target="_blank" rel="noopener noreferrer" @endif
@@ -77,7 +79,11 @@
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
<p class="text-amare-accent-text/80">{{ $bandBody }}</p>
</div>
<a href="{{ $bandHref }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto">
<a
href="{{ $bandLink['href'] }}"
@if ($bandLink['isWhatsapp']) target="_blank" rel="noopener noreferrer" @endif
class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto"
>
Conversar com a Amare
</a>
</div>

View File

@@ -8,7 +8,7 @@
@else
<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))
@php($contactLink = \App\Support\PackageContactLink::for($settings, $package->name, $package->whatsapp_message))
<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>
@endforeach
</ol>

View File

@@ -3,10 +3,11 @@
'heading' => null,
'body' => null,
'packageName' => null,
'packageMessage' => null,
])
@php
$link = \App\Support\PackageContactLink::for($settings, $packageName ?? '');
$link = \App\Support\PackageContactLink::for($settings, $packageName ?? '', $packageMessage);
@endphp
<section aria-labelledby="final-cta-heading" class="border-b border-amare-border bg-amare-bg py-20 md:py-24" data-chapter="final-cta" data-reveal-group>

View File

@@ -31,5 +31,6 @@
:heading="$package->final_cta_heading"
:body="$package->final_cta_body"
:package-name="$package->name"
:package-message="$package->whatsapp_message"
/>
@endsection

View File

@@ -5,7 +5,7 @@
# DOKPLOY_API_KEY x-api-key value
# DOKPLOY_COMPOSE_ID target compose id
# Optional env:
# DEPLOY_TITLE deployment title (default: Gitea deploy)
# DEPLOY_TITLE deployment title (default: GitHub deploy)
# DEPLOY_TIMEOUT_SEC total wait seconds (default: 900)
# DEPLOY_POLL_SEC poll interval (default: 10)
@@ -20,7 +20,7 @@ DOKPLOY_URL="${DOKPLOY_URL%/}"
DOKPLOY_URL="${DOKPLOY_URL%/api}"
DOKPLOY_URL="${DOKPLOY_URL%/}"
DEPLOY_TITLE="${DEPLOY_TITLE:-Gitea deploy}"
DEPLOY_TITLE="${DEPLOY_TITLE:-GitHub deploy}"
DEPLOY_TIMEOUT_SEC="${DEPLOY_TIMEOUT_SEC:-900}"
DEPLOY_POLL_SEC="${DEPLOY_POLL_SEC:-10}"

View File

@@ -216,6 +216,44 @@ class HomePageContentTest extends TestCase
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
}
public function test_package_cta_uses_custom_whatsapp_message_when_provided(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
WeddingPackage::factory()->published()->create([
'name' => 'Grand Jour',
'cta_label' => 'Quero conhecer a Grand Jour',
'whatsapp_message' => 'Olá, quero saber mais sobre a modalidade Grand Jour para o meu casamento em julho.',
]);
$response = $this->get(route('home'));
$response
->assertOk()
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode('Olá, quero saber mais sobre a modalidade Grand Jour para o meu casamento em julho.'),
false,
)
->assertDontSee('gostaria de conversar sobre a modalidade Grand Jour para meu casamento', false);
}
public function test_guidance_band_uses_whatsapp_when_a_number_is_configured(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
WeddingPackage::factory()->published()->create();
$response = $this->get(route('home'));
$response
->assertOk()
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode('Olá, ainda estou decidindo a modalidade ideal para o meu casamento. Podemos conversar?'),
false,
)
->assertSee('Conversar com a Amare');
}
public function test_corporate_steps_and_placeholder_render_from_settings(): void
{
SiteSetting::instance()->update([

View File

@@ -100,6 +100,26 @@ class PackageDetailTest extends TestCase
->assertDontSee(route('briefing', ['servico_interesse' => 'Essenza']), false);
}
public function test_package_final_cta_uses_custom_whatsapp_message_when_provided(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
$package = WeddingPackage::factory()->published()->create([
'name' => 'Essenza',
'whatsapp_message' => 'Olá, tenho interesse na assessoria Essenza. Podem me passar mais detalhes?',
]);
$response = $this->get(route('packages.show', $package->slug));
$response
->assertOk()
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode('Olá, tenho interesse na assessoria Essenza. Podem me passar mais detalhes?'),
false,
)
->assertDontSee('gostaria de conversar sobre a modalidade Essenza para meu casamento', false);
}
public function test_package_cta_falls_back_to_briefing_without_whatsapp_number(): void
{
SiteSetting::instance();