Compare commits
1 Commits
feat/whats
...
feat/man-1
| Author | SHA1 | Date | |
|---|---|---|---|
| 68632c32df |
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
|
|
||||||
final readonly class HomeContent
|
final readonly class HomeContent
|
||||||
@@ -16,11 +17,13 @@ final readonly class HomeContent
|
|||||||
* @param Collection<int, Service> $featuredServices
|
* @param Collection<int, Service> $featuredServices
|
||||||
* @param Collection<int, PortfolioCase> $featuredCases
|
* @param Collection<int, PortfolioCase> $featuredCases
|
||||||
* @param Collection<int, Testimonial> $testimonials
|
* @param Collection<int, Testimonial> $testimonials
|
||||||
|
* @param Collection<int, WeddingPackage> $weddingPackages
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public SiteSetting $settings,
|
public SiteSetting $settings,
|
||||||
public Collection $featuredServices,
|
public Collection $featuredServices,
|
||||||
public Collection $featuredCases,
|
public Collection $featuredCases,
|
||||||
public Collection $testimonials,
|
public Collection $testimonials,
|
||||||
|
public Collection $weddingPackages,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
|
||||||
final class GetHomeContent
|
final class GetHomeContent
|
||||||
{
|
{
|
||||||
@@ -31,6 +32,10 @@ final class GetHomeContent
|
|||||||
->published()
|
->published()
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->get(),
|
->get(),
|
||||||
|
weddingPackages: WeddingPackage::query()
|
||||||
|
->published()
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,6 +231,11 @@ class ManageSiteSettings extends Page
|
|||||||
->label('Telefone')
|
->label('Telefone')
|
||||||
->required()
|
->required()
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
TextInput::make('whatsapp_number')
|
||||||
|
->label('WhatsApp oficial para as modalidades')
|
||||||
|
->tel()
|
||||||
|
->maxLength(40)
|
||||||
|
->helperText('Use o número com DDD. Sem um número válido, as CTAs levam ao briefing.'),
|
||||||
TextInput::make('city')
|
TextInput::make('city')
|
||||||
->label('Cidade')
|
->label('Cidade')
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Resources\Pages\CreateRecord;
|
||||||
|
|
||||||
|
class CreateWeddingPackage extends CreateRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Actions\DeleteAction;
|
||||||
|
use Filament\Resources\Pages\EditRecord;
|
||||||
|
|
||||||
|
class EditWeddingPackage extends EditRecord
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [DeleteAction::make()];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages\Pages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\WeddingPackageResource;
|
||||||
|
use Filament\Actions\CreateAction;
|
||||||
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
|
class ListWeddingPackages extends ListRecords
|
||||||
|
{
|
||||||
|
protected static string $resource = WeddingPackageResource::class;
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
return [CreateAction::make()];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Filament\Resources\WeddingPackages;
|
||||||
|
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\CreateWeddingPackage;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\EditWeddingPackage;
|
||||||
|
use App\Filament\Resources\WeddingPackages\Pages\ListWeddingPackages;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use App\Policies\WeddingPackagePolicy;
|
||||||
|
use BackedEnum;
|
||||||
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
|
use Filament\Forms\Components\TagsInput;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Resources\Resource;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Filament\Tables\Columns\TextColumn;
|
||||||
|
use Filament\Tables\Table;
|
||||||
|
use UnitEnum;
|
||||||
|
|
||||||
|
class WeddingPackageResource extends Resource
|
||||||
|
{
|
||||||
|
protected static ?string $model = WeddingPackage::class;
|
||||||
|
|
||||||
|
protected static ?string $policy = WeddingPackagePolicy::class;
|
||||||
|
|
||||||
|
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedHeart;
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Modalidades de casamento';
|
||||||
|
|
||||||
|
protected static string|UnitEnum|null $navigationGroup = 'Conteúdo do site';
|
||||||
|
|
||||||
|
public static function form(Schema $schema): Schema
|
||||||
|
{
|
||||||
|
return $schema->components([
|
||||||
|
TextInput::make('name')->label('Nome')->required()->maxLength(120),
|
||||||
|
TextInput::make('level')->label('Nível')->required()->maxLength(120),
|
||||||
|
TextInput::make('summary')->label('Resumo')->required()->maxLength(255),
|
||||||
|
TagsInput::make('scope_items')->label('Itens de escopo')->required()->columnSpanFull(),
|
||||||
|
TextInput::make('cta_label')->label('CTA')->required()->maxLength(120),
|
||||||
|
TextInput::make('sort_order')->label('Ordem')->numeric()->required()->default(0),
|
||||||
|
DateTimePicker::make('published_at')->label('Publicado em')->seconds(false),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function table(Table $table): Table
|
||||||
|
{
|
||||||
|
return $table->columns([TextColumn::make('name')->label('Nome')->searchable(), TextColumn::make('level')->label('Nível'), TextColumn::make('sort_order')->label('Ordem')->sortable(), TextColumn::make('published_at')->label('Publicado em')->dateTime()->sortable()])->defaultSort('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPages(): array
|
||||||
|
{
|
||||||
|
return ['index' => ListWeddingPackages::route('/'), 'create' => CreateWeddingPackage::route('/create'), 'edit' => EditWeddingPackage::route('/{record}/edit')];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,6 @@ final class ContactController extends Controller
|
|||||||
|
|
||||||
private function success(): RedirectResponse
|
private function success(): RedirectResponse
|
||||||
{
|
{
|
||||||
return redirect()->route('contact')->with('status', 'briefing-sent');
|
return redirect()->route('briefing')->with('status', 'briefing-sent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class PageController extends Controller
|
|||||||
{
|
{
|
||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
return view('pages.contact', [
|
return view('pages.partners', [
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('contact'),
|
canonical: route('contact'),
|
||||||
@@ -55,4 +55,14 @@ final class PageController extends Controller
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function briefing(): View
|
||||||
|
{
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
|
return view('pages.contact', [
|
||||||
|
'siteSettings' => $settings,
|
||||||
|
'pageMeta' => PageMeta::forPage(canonical: route('briefing'), settings: $settings, title: PageMeta::withBrandSuffix('Briefing', $settings), description: 'Solicite uma proposta à '.$settings->brand_name.'.'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
app/Http/Controllers/PublicSite/PartnerInquiryController.php
Normal file
66
app/Http/Controllers/PublicSite/PartnerInquiryController.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\PublicSite;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\PublicSite\PartnerInquiryRequest;
|
||||||
|
use App\Mail\PartnerInquiry;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
final class PartnerInquiryController extends Controller
|
||||||
|
{
|
||||||
|
private const string DUPLICATE_SESSION_KEY = 'partner_inquiry_hash';
|
||||||
|
|
||||||
|
public function store(PartnerInquiryRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
if (filled($request->input('website'))) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
if (session()->get(self::DUPLICATE_SESSION_KEY) === $this->hash($validated)) {
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->put(self::DUPLICATE_SESSION_KEY, $this->hash($validated));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
|
||||||
|
if (filled($settings->email)) {
|
||||||
|
Mail::to($settings->email)->send(new PartnerInquiry([
|
||||||
|
'Nome' => $validated['nome'],
|
||||||
|
'Nome profissional ou empresa' => $validated['empresa'] ?? null,
|
||||||
|
'E-mail' => $validated['email'],
|
||||||
|
'Atuação ou serviço' => $validated['atuacao'],
|
||||||
|
'Área de atendimento' => $validated['area_atendimento'],
|
||||||
|
'Mensagem' => $validated['mensagem'],
|
||||||
|
'Portfólio ou redes' => $validated['portfolio_redes'] ?? null,
|
||||||
|
'Telefone' => $validated['telefone'] ?? null,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
Log::error('Falha ao enviar consulta de parceiro', ['exception' => $exception::class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $validated */
|
||||||
|
private function hash(array $validated): string
|
||||||
|
{
|
||||||
|
return hash('sha256', serialize($validated));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function success(): RedirectResponse
|
||||||
|
{
|
||||||
|
return redirect()->route('contact')->with('status', 'partner-inquiry-sent');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use App\Application\Data\PageMeta;
|
|||||||
use App\Application\Queries\Marketing\GetPublishedServices;
|
use App\Application\Queries\Marketing\GetPublishedServices;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Contracts\View\View;
|
use Illuminate\Contracts\View\View;
|
||||||
|
|
||||||
final class ServiceController extends Controller
|
final class ServiceController extends Controller
|
||||||
@@ -19,6 +20,7 @@ final class ServiceController extends Controller
|
|||||||
|
|
||||||
return view('pages.services.index', [
|
return view('pages.services.index', [
|
||||||
'services' => $services,
|
'services' => $services,
|
||||||
|
'weddingPackages' => WeddingPackage::query()->published()->orderBy('sort_order')->get(),
|
||||||
'siteSettings' => $settings,
|
'siteSettings' => $settings,
|
||||||
'pageMeta' => PageMeta::forPage(
|
'pageMeta' => PageMeta::forPage(
|
||||||
canonical: route('services.index'),
|
canonical: route('services.index'),
|
||||||
|
|||||||
32
app/Http/Requests/PublicSite/PartnerInquiryRequest.php
Normal file
32
app/Http/Requests/PublicSite/PartnerInquiryRequest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\PublicSite;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class PartnerInquiryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
protected function prepareForValidation(): void
|
||||||
|
{
|
||||||
|
$email = $this->input('email');
|
||||||
|
|
||||||
|
$this->merge(['email' => is_string($email) ? mb_strtolower(trim($email)) : $email]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<int, string>> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nome' => ['required', 'string', 'max:120'],
|
||||||
|
'empresa' => ['nullable', 'string', 'max:160'],
|
||||||
|
'email' => ['required', 'email', 'max:254'],
|
||||||
|
'atuacao' => ['required', 'string', 'max:120'],
|
||||||
|
'area_atendimento' => ['required', 'string', 'max:160'],
|
||||||
|
'mensagem' => ['required', 'string', 'max:3000'],
|
||||||
|
'portfolio_redes' => ['nullable', 'string', 'max:500'],
|
||||||
|
'telefone' => ['nullable', 'string', 'max:40'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Mail/PartnerInquiry.php
Normal file
31
app/Mail/PartnerInquiry.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Mail;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Mail\Mailable;
|
||||||
|
use Illuminate\Mail\Mailables\Content;
|
||||||
|
use Illuminate\Mail\Mailables\Envelope;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
final class PartnerInquiry extends Mailable implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $fields */
|
||||||
|
public function __construct(public readonly array $fields) {}
|
||||||
|
|
||||||
|
public function envelope(): Envelope
|
||||||
|
{
|
||||||
|
return new Envelope(subject: 'Nova consulta de fornecedor ou parceria — Amare Assessoria');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function content(): Content
|
||||||
|
{
|
||||||
|
return new Content(html: 'emails.partner-inquiry', text: 'emails.partner-inquiry-text');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'principles',
|
'principles',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
'phone',
|
||||||
|
'whatsapp_number',
|
||||||
'city',
|
'city',
|
||||||
'social_links',
|
'social_links',
|
||||||
'default_meta_title',
|
'default_meta_title',
|
||||||
@@ -85,6 +86,7 @@ class SiteSetting extends Model
|
|||||||
'principles' => self::defaultPrinciples(),
|
'principles' => self::defaultPrinciples(),
|
||||||
'email' => 'amareassessoriaeventos@gmail.com',
|
'email' => 'amareassessoriaeventos@gmail.com',
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
|
'whatsapp_number' => null,
|
||||||
'city' => 'São Paulo - SP',
|
'city' => 'São Paulo - SP',
|
||||||
'social_links' => [],
|
'social_links' => [],
|
||||||
'default_meta_title' => 'Amare Assessoria de Eventos',
|
'default_meta_title' => 'Amare Assessoria de Eventos',
|
||||||
|
|||||||
39
app/Models/WeddingPackage.php
Normal file
39
app/Models/WeddingPackage.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Models\Concerns\HasPublication;
|
||||||
|
use App\Policies\WeddingPackagePolicy;
|
||||||
|
use Database\Factories\WeddingPackageFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $name
|
||||||
|
* @property string $level
|
||||||
|
* @property string $summary
|
||||||
|
* @property list<string> $scope_items
|
||||||
|
* @property string $cta_label
|
||||||
|
* @property int $sort_order
|
||||||
|
* @property Carbon|null $published_at
|
||||||
|
*/
|
||||||
|
#[Fillable(['name', 'level', 'summary', 'scope_items', 'cta_label', 'sort_order', 'published_at'])]
|
||||||
|
#[UsePolicy(WeddingPackagePolicy::class)]
|
||||||
|
class WeddingPackage extends Model
|
||||||
|
{
|
||||||
|
/** @use HasFactory<WeddingPackageFactory> */
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
use HasPublication;
|
||||||
|
|
||||||
|
/** @return array<string, string|class-string> */
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return ['scope_items' => 'array', 'sort_order' => 'integer', 'published_at' => 'datetime'];
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/Policies/WeddingPackagePolicy.php
Normal file
36
app/Policies/WeddingPackagePolicy.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
|
||||||
|
class WeddingPackagePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, WeddingPackage $package): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
RateLimiter::for('contact-briefing', function (Request $request): Limit {
|
RateLimiter::for('contact-briefing', function (Request $request): Limit {
|
||||||
return Limit::perMinute(5)->by($request->ip().'|contact-briefing');
|
return Limit::perMinute(5)->by($request->ip().'|contact-briefing');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
RateLimiter::for('partner-inquiry', function (Request $request): Limit {
|
||||||
|
return Limit::perMinute(5)->by($request->ip().'|partner-inquiry');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function freezeClockWhenConfigured(): void
|
private function freezeClockWhenConfigured(): void
|
||||||
|
|||||||
33
database/factories/WeddingPackageFactory.php
Normal file
33
database/factories/WeddingPackageFactory.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
/** @extends Factory<WeddingPackage> */
|
||||||
|
class WeddingPackageFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = WeddingPackage::class;
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => fake()->unique()->words(2, true),
|
||||||
|
'level' => 'Assessoria',
|
||||||
|
'summary' => fake()->sentence(),
|
||||||
|
'scope_items' => [fake()->sentence()],
|
||||||
|
'cta_label' => 'Conversar sobre esta modalidade',
|
||||||
|
'sort_order' => 0,
|
||||||
|
'published_at' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function published(): static
|
||||||
|
{
|
||||||
|
return $this->state(fn (): array => ['published_at' => now()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->string('whatsapp_number')->nullable()->after('phone');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn('whatsapp_number');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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::create('wedding_packages', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('level');
|
||||||
|
$table->string('summary');
|
||||||
|
$table->jsonb('scope_items');
|
||||||
|
$table->string('cta_label');
|
||||||
|
$table->integer('sort_order')->default(0)->index();
|
||||||
|
$table->timestamp('published_at')->nullable()->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('wedding_packages');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\PortfolioImage;
|
use App\Models\PortfolioImage;
|
||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use App\Support\PublicImageUploadRules;
|
use App\Support\PublicImageUploadRules;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
@@ -47,6 +48,7 @@ class ContentSeeder extends Seeder
|
|||||||
|
|
||||||
$this->seedSiteSettings();
|
$this->seedSiteSettings();
|
||||||
$this->seedServices();
|
$this->seedServices();
|
||||||
|
$this->seedWeddingPackages();
|
||||||
$this->seedPortfolioCases();
|
$this->seedPortfolioCases();
|
||||||
$this->call(TestimonialsSeeder::class);
|
$this->call(TestimonialsSeeder::class);
|
||||||
}
|
}
|
||||||
@@ -203,6 +205,19 @@ class ContentSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function seedWeddingPackages(): void
|
||||||
|
{
|
||||||
|
$packages = [
|
||||||
|
['name' => 'Essenza', 'level' => 'Planejamento essencial', 'summary' => 'Direção para decisões centrais e um caminho de organização claro.', 'scope_items' => ['Leitura do contexto e prioridades', 'Direção de escopo e cronograma', 'Orientação para os próximos passos'], 'sort_order' => 1],
|
||||||
|
['name' => 'Conduzione', 'level' => 'Assessoria de planejamento', 'summary' => 'Acompanhamento próximo para construir escolhas coerentes até o grande dia.', 'scope_items' => ['Planejamento e cronograma integrado', 'Coordenação de fornecedores', 'Alinhamentos e decisões de produção'], 'sort_order' => 2],
|
||||||
|
['name' => 'Grand Jour', 'level' => 'Assessoria completa', 'summary' => 'Presença da escuta inicial à execução atenta de toda a celebração.', 'scope_items' => ['Direção completa do projeto', 'Produção e coordenação de fornecedores', 'Execução presencial no evento'], 'sort_order' => 3],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($packages as $package) {
|
||||||
|
WeddingPackage::query()->updateOrCreate(['name' => $package['name']], [...$package, 'cta_label' => 'Conversar sobre esta modalidade', 'published_at' => Carbon::parse(self::SEED_TIMESTAMP)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function copyFixture(string $fixtureName, string $destination): string
|
private function copyFixture(string $fixtureName, string $destination): string
|
||||||
{
|
{
|
||||||
$source = base_path('database/fixtures/images/'.$fixtureName);
|
$source = base_path('database/fixtures/images/'.$fixtureName);
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
The public site currently sends every commercial CTA to `/contato`, where the detailed event briefing lives. Its home chapters reflect the original editorial sequence, while the CMS only has generic `Service` records. Production seed gating already distinguishes environmental content but the public portfolio needs an explicit empty-acervo state so no demonstrative case can be mistaken for proof.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Keep the public surface server-rendered Blade with progressive vanilla JS and a single Heritage Editorial visual language.
|
||||||
|
- Make wedding modalities typed, publishable, ordered CMS content; derive their WhatsApp CTA server-side with an explicit briefing fallback.
|
||||||
|
- Split prospect and partner submissions at route, request, controller, mail and success-state boundaries.
|
||||||
|
- Keep public data minimised: partner submissions are mailed, never persisted.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- No WhatsApp API/integration, CRM Lead creation, partner directory, Corporate service inventory, Corporate case, invented evidence or page builder.
|
||||||
|
- No production migration of fixture cases into real proof; the authorized client content remains an operational prerequisite.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Typed `WeddingPackage` content instead of a JSON setting
|
||||||
|
|
||||||
|
`wedding_packages` has a dedicated model, migration, factory, policy, Filament resource and public query. This matches the existing publication/order model for Services and keeps content validation, ordering and authorization visible. A JSON array in `site_settings` would be faster initially but would weaken publication control and future editing.
|
||||||
|
|
||||||
|
### WhatsApp is a deeplink, not an integration
|
||||||
|
|
||||||
|
The setting stores digits-normalized `whatsapp_number`; a small value object builds `https://wa.me/<digits>?text=<encoded contextual message>`. If the setting is absent or invalid, the same CTA points to `/briefing?servico_interesse=<package>` so conversion still works. This offers the authorized official channel without violating SPEC §4.2's prohibition on an official integration.
|
||||||
|
|
||||||
|
### Separate partner boundary, not a mode on briefing
|
||||||
|
|
||||||
|
`PartnerInquiryRequest`, `PartnerInquiryController` and `PartnerInquiry` mailable have their own payload, honeypot key, throttle and session duplicate hash. It prevents event fields, marketing origin and confirmations from crossing into the partnership flow. A single controller with a discriminated input was rejected because it increases the chance of accidental routing or data disclosure.
|
||||||
|
|
||||||
|
### Honest public availability states
|
||||||
|
|
||||||
|
The home consumes `GetHomeContent`, which returns only actual published cases in production. The portfolio chapter itself remains present and shows an acervo-em-preparação note on empty data. Non-production visual fixtures continue to seed only for development/staging. Corporate remains copy plus a briefing CTA, deliberately without data-driven claims, media or a service list.
|
||||||
|
|
||||||
|
### Navigation links use canonical home fragments
|
||||||
|
|
||||||
|
Header/footer link to `/#amare`, `/#casamentos`, `/#corporate`, `/#portfolio` and `/#depoimentos` from other routes, and native fragments on the home. This preserves deep links and no-JS behavior; CSS scroll margin compensates for the sticky header.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Official WhatsApp number is not configured at deploy] → every package CTA has a deterministic briefing fallback and settings label calls out the operational requirement.
|
||||||
|
- [Client proof is still unavailable] → production renders no fixture cases, a visible acervo note and no Corporate cases.
|
||||||
|
- [New contact route changes incoming links] → `/contato` remains available for partners and header/footer continue to expose it; event CTA links move deliberately to `/briefing`.
|
||||||
|
- [Detailed forms duplicate markup] → shared field styles and narrow controllers are preferred over generic, condition-heavy form components.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Deploy the additive table/column migrations and code together.
|
||||||
|
2. Run the content seeder to install official wedding modalities; set the official WhatsApp number in Filament before promoting CTAs.
|
||||||
|
3. In production, leave portfolio cases unpublished until real authorized material is loaded; verify the empty-acervo state.
|
||||||
|
4. Rollback is code-safe because routes remain additive and the legacy setting data is untouched; if necessary, unset the new WhatsApp column and retain the package rows without public publication.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- Corporate institutional copy and authorized proof (MAN-98/MAN-99) remain client dependencies; no placeholder claims will be added.
|
||||||
|
- The official WhatsApp number must be supplied before the campaign links are promoted.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
A home atual apresenta os conteúdos do CMS, mas não conduz com clareza as duas frentes da Amare nem separa o pedido de proposta da relação com fornecedores. A estrutura de lançamento precisa tornar Casamentos e Corporate compreensíveis sem inventar prova, preservar o acervo real como requisito de publicação e abrir um canal mínimo, privado e específico para parcerias.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Reorganizar a home em capítulos editoriais ancoráveis: apresentação, Amare, vertentes, Casamentos, Corporate, acervo, depoimentos e fechamento.
|
||||||
|
- Adicionar modalidades de casamento publicáveis e ordenáveis no CMS, com conteúdo inicial oficial para Essenza, Conduzione e Grand Jour.
|
||||||
|
- Adicionar número oficial de WhatsApp às configurações e tornar as CTAs das modalidades contextuais, com fallback para o briefing.
|
||||||
|
- Mover o formulário de pedido de proposta para `/briefing`; transformar `/contato` em uma jornada independente para fornecedores e parcerias.
|
||||||
|
- Enviar a consulta de parceiro por e-mail próprio, sem criar Lead ou persistir dados; manter CSRF, honeypot, rate limit, validação e aviso de privacidade proporcional à finalidade.
|
||||||
|
- Manter Corporate como apresentação institucional e CTA até haver copy, fotos e cases autorizados; em produção, não publicar fixtures de portfólio e informar que o acervo está em preparação.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `wedding-packages`: modalidades de casamento gerenciadas no CMS e CTAs contextuais de WhatsApp/briefing.
|
||||||
|
- `partner-inquiries`: formulário público mínimo, privado e não persistente para fornecedores e parcerias.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: nova estrutura editorial da home, âncoras e separação entre briefing e contato de parceiros.
|
||||||
|
- `service-catalog`: aprofundamento de serviços passa a apresentar as modalidades de casamento publicadas.
|
||||||
|
- `site-settings`: configurações tipadas passam a incluir o número oficial de WhatsApp.
|
||||||
|
- `transactional-email`: e-mail operacional separado para consultas de parceiros.
|
||||||
|
- `content-media`: fixtures de casos ficam limitadas a desenvolvimento e staging; produção só mostra acervo real publicado.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta rotas, controllers, requests, mailables, modelos/migrations/seeders, Filament, consultas de marketing, componentes Blade, navegação, estilos e testes feature/browser. A mudança atende WEB-01, WEB-02, WEB-03, WEB-05, WEB-06 e WEB-07, sem introduzir integração oficial com WhatsApp, CRM, persistência de leads, marketplace ou outros itens fora do MVP (SPEC.md §4.2).
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Production never promotes visual fixture cases as Amare proof
|
||||||
|
The content seeder SHALL restrict visual fixture portfolio cases to non-production environments. Production public output SHALL only render cases that were explicitly loaded and published as authorized content.
|
||||||
|
|
||||||
|
#### Scenario: Production seed does not create fixture cases
|
||||||
|
- **WHEN** the content seeder runs with the production environment
|
||||||
|
- **THEN** no fixture portfolio case or fixture portfolio image is published
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Partners have a separate, non-persistent contact journey
|
||||||
|
The system SHALL expose `/contato` as a supplier and partnership form. It MUST collect name, optional professional/company name, e-mail, service/activity, coverage area, message, optional portfolio/social link and optional telephone. It SHALL send a dedicated internal e-mail and MUST NOT persist a Lead or partner record.
|
||||||
|
|
||||||
|
#### Scenario: Valid partner inquiry is delivered separately
|
||||||
|
- **WHEN** a visitor submits valid partner data on `/contato`
|
||||||
|
- **THEN** the configured internal recipient receives the dedicated partner inquiry e-mail
|
||||||
|
- **AND** no briefing confirmation or Lead is created
|
||||||
|
|
||||||
|
### Requirement: Partner inquiry is protected and transparent
|
||||||
|
The partner form SHALL enforce CSRF, server-side limits, honeypot, rate limiting and duplicate-click protection. It MUST show a purpose-specific privacy notice linking to the privacy policy.
|
||||||
|
|
||||||
|
#### Scenario: Honeypot blocks automated partner form submissions
|
||||||
|
- **WHEN** the partner honeypot is filled
|
||||||
|
- **THEN** the system reports generic success without sending an e-mail
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home presents the weddings and corporate editorial journey
|
||||||
|
The home SHALL expose anchored chapters for presentation, Amare, service directions, weddings, Corporate, portfolio/acervo, testimonials and final contact paths. It MUST retain the photographic hero, tonal fields, fine rules, EB Garamond and progressive/reduced-motion behavior. Corporate SHALL present only institutional copy and a briefing CTA until authorized copy and proof exist.
|
||||||
|
|
||||||
|
#### Scenario: Canonical anchor journey is reachable
|
||||||
|
- **WHEN** a visitor loads the home or follows a `/#chapter` link from another public page
|
||||||
|
- **THEN** the relevant chapter exists with a stable id and is reachable without JavaScript
|
||||||
|
|
||||||
|
#### Scenario: Corporate does not imply unauthorized proof
|
||||||
|
- **WHEN** a visitor loads the Corporate chapter before client proof is published
|
||||||
|
- **THEN** no Corporate service list, image, metric or case is rendered
|
||||||
|
|
||||||
|
### Requirement: Production portfolio remains truthful when no cases are published
|
||||||
|
The public portfolio chapter SHALL render a visible acervo-em-preparação note when no real published cases are available. Production seeders MUST NOT publish development fixture cases.
|
||||||
|
|
||||||
|
#### Scenario: Empty production acervo is explicit
|
||||||
|
- **GIVEN** production has no published portfolio cases
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the portfolio chapter displays the acervo preparation note and no case card
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Contact page presents contact data as briefing placeholder
|
||||||
|
|
||||||
|
The `contact` route SHALL render the partner and supplier inquiry page using `site_settings` contact data. The detailed event proposal form SHALL be served by the `briefing` route at `/briefing`; public proposal CTAs MUST target that route. Neither public form MUST create a Lead in the launch scope.
|
||||||
|
|
||||||
|
#### Scenario: Contact page shows partner journey
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/contato`
|
||||||
|
- **THEN** partner-specific copy, fields and privacy notice MUST be displayed
|
||||||
|
- **AND** the detailed event briefing MUST NOT be rendered
|
||||||
|
|
||||||
|
#### Scenario: Briefing route remains reachable for prospects
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/briefing`
|
||||||
|
- **THEN** the detailed event proposal form MUST be displayed
|
||||||
|
- **AND** no Lead record MUST be created
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Services explain published wedding modalities
|
||||||
|
The public services experience SHALL present published wedding modalities as the available Casamentos scope, including level, summary and ordered scope items. It MUST not treat unpublished modalities as public services.
|
||||||
|
|
||||||
|
#### Scenario: Draft modality remains private
|
||||||
|
- **GIVEN** a wedding modality with `published_at` null
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** that modality MUST NOT be rendered
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Official WhatsApp number is a typed site setting
|
||||||
|
The typed site settings singleton SHALL include an optional `whatsapp_number`. The admin form MUST validate and label it as the official number used by public modality CTAs; it MUST NOT create an external WhatsApp integration.
|
||||||
|
|
||||||
|
#### Scenario: Admin stores official WhatsApp number
|
||||||
|
- **WHEN** an admin saves a valid number in site settings
|
||||||
|
- **THEN** public wedding modality CTAs can derive the contextual deeplink
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Partner inquiries use a dedicated transactional message
|
||||||
|
The system SHALL send supplier and partnership submissions in a dedicated `PartnerInquiry` mailable to the configured internal site recipient. It MUST NOT send the visitor briefing confirmation for this form.
|
||||||
|
|
||||||
|
#### Scenario: Partner email does not cross into briefing mail
|
||||||
|
- **WHEN** a valid partner inquiry is submitted
|
||||||
|
- **THEN** the recipient receives the partner field set
|
||||||
|
- **AND** the visitor does not receive `ContactBriefingConfirmation`
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Wedding modalities are managed as publishable ordered content
|
||||||
|
The system SHALL let an admin manage `WeddingPackage` records with name, level, summary, ordered scope items, CTA label, sort order and `published_at`. Only published records SHALL appear in public wedding sections, in ascending `sort_order`.
|
||||||
|
|
||||||
|
#### Scenario: Published modalities follow CMS order
|
||||||
|
- **GIVEN** published and draft wedding modalities with different sort orders
|
||||||
|
- **WHEN** a visitor loads the home or services page
|
||||||
|
- **THEN** only published modalities appear in ascending `sort_order`
|
||||||
|
|
||||||
|
#### Scenario: Official initial modalities are available
|
||||||
|
- **WHEN** the content seeder runs
|
||||||
|
- **THEN** Essenza, Conduzione and Grand Jour are created as the official initial modalities
|
||||||
|
|
||||||
|
### Requirement: Wedding modality CTA has a contextual channel and conversion fallback
|
||||||
|
The system SHALL build a WhatsApp deeplink containing the selected modality when `whatsapp_number` is valid. When it is missing or invalid, the CTA SHALL point to `/briefing` with the modality prefilled as `servico_interesse`.
|
||||||
|
|
||||||
|
#### Scenario: Valid WhatsApp setting creates contextual link
|
||||||
|
- **GIVEN** a valid official WhatsApp number
|
||||||
|
- **WHEN** a visitor activates a wedding modality CTA
|
||||||
|
- **THEN** the link targets `wa.me` with a URL-encoded message naming that modality
|
||||||
|
|
||||||
|
#### Scenario: Missing WhatsApp setting preserves briefing conversion
|
||||||
|
- **GIVEN** no valid official WhatsApp number
|
||||||
|
- **WHEN** a visitor activates a wedding modality CTA
|
||||||
|
- **THEN** the visitor reaches `/briefing` with `servico_interesse` prefilled
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## 1. CMS and public content model
|
||||||
|
|
||||||
|
- [x] 1.1 Add the typed official WhatsApp setting with migration, model/form validation and regression coverage.
|
||||||
|
- [x] 1.2 Add the publishable, ordered WeddingPackage model, migration, factory, policy and Filament resource with failing-first coverage.
|
||||||
|
- [x] 1.3 Add official Essenza, Conduzione and Grand Jour seed content and production-safe visual fixture gating.
|
||||||
|
- [x] 1.4 Extend the public marketing query/data object to load wedding modalities and truthful acervo state.
|
||||||
|
|
||||||
|
## 2. Proposal and partner boundaries
|
||||||
|
|
||||||
|
- [x] 2.1 Move the existing event proposal request/form/mail route to `/briefing`, including prefilled modality interest.
|
||||||
|
- [x] 2.2 Add the isolated partner inquiry request, controller, rate limit, honeypot, duplicate guard and dedicated mailable.
|
||||||
|
- [x] 2.3 Cover each public form's validation, delivery, privacy copy, anti-spam path and absence of cross-flow mail/persistence.
|
||||||
|
|
||||||
|
## 3. Editorial public journey
|
||||||
|
|
||||||
|
- [x] 3.1 Recompose home chapters and stable anchors for Amare, directions, weddings, Corporate, acervo and testimonials.
|
||||||
|
- [x] 3.2 Render published wedding modalities with contextual WhatsApp/fallback CTAs and keep Corporate proof-free.
|
||||||
|
- [x] 3.3 Update navigation, footer and deep-route `/#chapter` links; preserve the services, portfolio, about and privacy deep dives.
|
||||||
|
- [x] 3.4 Update `/servicos`, `/briefing` and `/contato` with the approved factual copy and Heritage Editorial form/layout treatment.
|
||||||
|
|
||||||
|
## 4. Verification and handoff
|
||||||
|
|
||||||
|
- [x] 4.1 Run the focused feature suite and static/formatting gates; fix all scoped regressions.
|
||||||
|
- [x] 4.2 Run browser coverage at desktop and mobile, keyboard/axe and reduced-motion checks without visual snapshots.
|
||||||
|
- [x] 4.3 Validate the OpenSpec change and update every completed task only with recorded verification evidence.
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
|
<env name="APP_ENV" value="testing"/>
|
||||||
<env name="APP_KEY" value="base64:NXm/6jIyFcDGHoMKGc5QZuSaq0dRZFYPg1Isuy1fNvE="/>
|
<env name="APP_KEY" value="base64:NXm/6jIyFcDGHoMKGc5QZuSaq0dRZFYPg1Isuy1fNvE="/>
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
|
|||||||
3
resources/views/components/home/corporate.blade.php
Normal file
3
resources/views/components/home/corporate.blade.php
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<section id="corporate" aria-labelledby="corporate-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="corporate">
|
||||||
|
<div class="container-amare grid gap-8 md:grid-cols-12" data-reveal-group><div class="md:col-span-5" data-reveal data-reveal-from="left"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent-text">Corporate</p><h2 id="corporate-heading" class="mt-3 text-headline font-medium">Encontros que pedem intenção, método e presença.</h2></div><div class="space-y-6 md:col-span-5 md:col-start-7" data-reveal data-reveal-from="up"><p class="text-amare-accent-text/80">A Amare conduz projetos corporativos com escuta do contexto, clareza de direção e atenção à experiência de cada pessoa presente.</p><a href="{{ route('briefing', ['tipo_evento' => 'Evento corporativo']) }}" class="inline-flex min-h-11 items-center text-sm font-semibold"><span class="border-b border-amare-accent-text pb-1">Iniciar uma conversa</span></a></div></div>
|
||||||
|
</section>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="{{ route('contact') }}"
|
href="{{ route('briefing') }}"
|
||||||
class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-deep"
|
class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-deep"
|
||||||
>
|
>
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
>
|
>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
<a href="{{ route('contact') }}" data-testid="home-primary-cta" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover">
|
<a href="{{ route('briefing') }}" data-testid="home-primary-cta" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover">
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="manifesto-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-24 md:py-32" data-chapter="manifesto">
|
<section id="amare" aria-labelledby="manifesto-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-24 md:py-32" data-chapter="manifesto">
|
||||||
<div class="container-amare grid gap-10 md:grid-cols-12" data-reveal-group>
|
<div class="container-amare grid gap-10 md:grid-cols-12" data-reveal-group>
|
||||||
<div class="space-y-5 md:col-span-4 md:pt-20" data-reveal data-reveal-from="left">
|
<div class="space-y-5 md:col-span-4 md:pt-20" data-reveal data-reveal-from="left">
|
||||||
<p class="max-w-52 text-lg leading-snug text-amare-text">Humana no cuidado. Precisa na entrega.</p>
|
<p class="max-w-52 text-lg leading-snug text-amare-text">Humana no cuidado. Precisa na entrega.</p>
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
'cases',
|
'cases',
|
||||||
])
|
])
|
||||||
|
|
||||||
@if ($cases->isNotEmpty())
|
<section id="portfolio" aria-labelledby="portfolio-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="portfolio">
|
||||||
<section aria-labelledby="portfolio-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="portfolio">
|
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="grid gap-4 md:grid-cols-12" data-reveal data-reveal-from="up">
|
<div class="grid gap-4 md:grid-cols-12" data-reveal data-reveal-from="up">
|
||||||
<div class="md:col-span-7 md:col-start-5 space-y-3">
|
<div class="md:col-span-7 md:col-start-5 space-y-3">
|
||||||
@@ -12,6 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($cases->isNotEmpty())
|
||||||
<div class="grid gap-8 md:grid-cols-12 md:gap-x-8 md:gap-y-14" data-editorial-portfolio>
|
<div class="grid gap-8 md:grid-cols-12 md:gap-x-8 md:gap-y-14" data-editorial-portfolio>
|
||||||
@foreach ($cases as $case)
|
@foreach ($cases as $case)
|
||||||
@php
|
@php
|
||||||
@@ -52,6 +52,9 @@
|
|||||||
</article>
|
</article>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<p class="max-w-2xl border-t border-amare-accent-text/30 pt-5 text-amare-accent-text/80">Nosso acervo de eventos reais e autorizados está em preparação. Em breve, esta página reunirá recortes que contam melhor o trabalho da Amare.</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 border-t border-amare-accent-text/30 pt-5 md:flex-row md:items-end md:justify-between">
|
<div class="flex flex-col gap-4 border-t border-amare-accent-text/30 pt-5 md:flex-row md:items-end md:justify-between">
|
||||||
<p class="max-w-2xl text-sm text-amare-accent-text/75">
|
<p class="max-w-2xl text-sm text-amare-accent-text/75">
|
||||||
@@ -63,4 +66,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@endif
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($services->isNotEmpty())
|
@if ($services->isNotEmpty())
|
||||||
<section aria-labelledby="services-heading" class="home-chapter border-b border-amare-border py-24" data-chapter="services">
|
<section id="vertentes" aria-labelledby="services-heading" class="home-chapter border-b border-amare-border py-24" data-chapter="services">
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="grid gap-6 md:grid-cols-12 md:items-end" data-reveal data-reveal-from="up">
|
<div class="grid gap-6 md:grid-cols-12 md:items-end" data-reveal data-reveal-from="up">
|
||||||
<div class="space-y-3 md:col-span-4">
|
<div class="space-y-3 md:col-span-4">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($testimonials->isNotEmpty())
|
@if ($testimonials->isNotEmpty())
|
||||||
<section aria-labelledby="testimonials-heading" class="home-chapter border-b border-amare-border bg-amare-bg py-24" data-chapter="testimonials">
|
<section id="depoimentos" aria-labelledby="testimonials-heading" class="home-chapter border-b border-amare-border bg-amare-bg py-24" data-chapter="testimonials">
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="max-w-2xl space-y-3 md:ml-[16.666667%]" data-reveal data-reveal-from="up">
|
<div class="max-w-2xl space-y-3 md:ml-[16.666667%]" data-reveal data-reveal-from="up">
|
||||||
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
||||||
|
|||||||
18
resources/views/components/home/wedding-packages.blade.php
Normal file
18
resources/views/components/home/wedding-packages.blade.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@props(['packages', 'settings'])
|
||||||
|
|
||||||
|
<section id="casamentos" aria-labelledby="weddings-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-24" data-chapter="weddings">
|
||||||
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
|
<div class="grid gap-6 md:grid-cols-12" data-reveal data-reveal-from="up"><div class="md:col-span-5"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">Casamentos</p><h2 id="weddings-heading" class="mt-3 text-headline font-medium">Uma condução à altura do que importa.</h2></div><p class="max-w-xl text-amare-muted md:col-span-5 md:col-start-7">Modalidades para diferentes momentos de planejamento, sempre construídas a partir do contexto de cada celebração.</p></div>
|
||||||
|
@if ($packages->isEmpty())
|
||||||
|
<p class="border-t border-amare-border pt-5 text-amare-muted">As modalidades de casamento estão em atualização. Conte-nos sobre o seu momento no briefing.</p>
|
||||||
|
@else
|
||||||
|
<ol class="grid gap-8 border-t border-amare-border pt-6 md:grid-cols-3">
|
||||||
|
@foreach ($packages as $package)
|
||||||
|
@php($digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number))
|
||||||
|
@php($href = strlen((string) $digits) >= 10 ? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$package->name.' para meu casamento.') : route('briefing', ['servico_interesse' => $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><a href="{{ $href }}" @if (str_starts_with($href, 'https://wa.me/')) target="_blank" rel="noopener" @endif class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent hover:text-amare-accent-deep"><span class="border-b border-amare-accent pb-1">{{ $package->cta_label }}</span></a></li>
|
||||||
|
@endforeach
|
||||||
|
</ol>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
5
resources/views/emails/partner-inquiry-text.blade.php
Normal file
5
resources/views/emails/partner-inquiry-text.blade.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Nova consulta de fornecedor ou parceria
|
||||||
|
|
||||||
|
@foreach ($fields as $label => $value)
|
||||||
|
{{ $label }}: {{ $value ?: '—' }}
|
||||||
|
@endforeach
|
||||||
10
resources/views/emails/partner-inquiry.blade.php
Normal file
10
resources/views/emails/partner-inquiry.blade.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR"><head><meta charset="utf-8"><title>Nova consulta de parceria</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Nova consulta de fornecedor ou parceria</h1>
|
||||||
|
<table>
|
||||||
|
@foreach ($fields as $label => $value)
|
||||||
|
<tr><th scope="row">{{ $label }}</th><td>{{ $value ?: '—' }}</td></tr>
|
||||||
|
@endforeach
|
||||||
|
</table>
|
||||||
|
</body></html>
|
||||||
@@ -58,11 +58,11 @@
|
|||||||
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 backdrop-blur-sm">
|
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 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]">
|
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr]">
|
||||||
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||||
<a href="{{ route('home') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Início</a>
|
<a href="{{ route('home') }}#amare" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Amare</a>
|
||||||
<a href="{{ route('services.index') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Serviços</a>
|
<a href="{{ route('home') }}#casamentos" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Casamentos</a>
|
||||||
<a href="{{ route('portfolio.index') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Portfólio</a>
|
<a href="{{ route('home') }}#corporate" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Corporate</a>
|
||||||
<a href="{{ route('about') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Amare</a>
|
<a href="{{ route('home') }}#portfolio" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Portfólio</a>
|
||||||
<a href="{{ route('contact') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
<a href="{{ route('briefing') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<a href="{{ route('home') }}" class="order-1 justify-self-start md:order-2 md:justify-self-center" aria-label="{{ $siteSettings->brand_name }} — página inicial">
|
<a href="{{ route('home') }}" class="order-1 justify-self-start md:order-2 md:justify-self-center" aria-label="{{ $siteSettings->brand_name }} — página inicial">
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
||||||
<a href="{{ route('contact') }}" class="hidden items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent transition-colors hover:text-amare-accent-deep md:inline-flex">
|
<a href="{{ route('briefing') }}" class="hidden items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent transition-colors hover:text-amare-accent-deep md:inline-flex">
|
||||||
Solicitar proposta
|
Solicitar proposta
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -109,9 +109,9 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Navegação</h2>
|
<h2 class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-text">Navegação</h2>
|
||||||
<ul class="mt-3 space-y-3">
|
<ul class="mt-3 space-y-3">
|
||||||
<li><a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Serviços</a></li>
|
<li><a href="{{ route('home') }}#casamentos" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Casamentos</a></li>
|
||||||
|
<li><a href="{{ route('home') }}#corporate" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Corporate</a></li>
|
||||||
<li><a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Portfólio</a></li>
|
<li><a href="{{ route('portfolio.index') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Portfólio</a></li>
|
||||||
<li><a href="{{ route('about') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">A Amare</a></li>
|
|
||||||
<li><a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Contato</a></li>
|
<li><a href="{{ route('contact') }}" class="inline-flex min-h-11 items-center text-amare-muted transition-colors hover:text-amare-accent">Contato</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
@else
|
@else
|
||||||
<form
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="{{ route('contact.store') }}"
|
action="{{ route('briefing.store') }}"
|
||||||
class="mt-10 max-w-3xl space-y-10"
|
class="mt-10 max-w-3xl space-y-10"
|
||||||
data-contact-form
|
data-contact-form
|
||||||
>
|
>
|
||||||
@@ -135,8 +135,8 @@
|
|||||||
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
||||||
>
|
>
|
||||||
<option value="" selected disabled>Selecione...</option>
|
<option value="" selected disabled>Selecione...</option>
|
||||||
<option value="Casamento" @selected(old('tipo_evento') === 'Casamento')>Casamento</option>
|
<option value="Casamento" @selected(old('tipo_evento', request('tipo_evento')) === 'Casamento')>Casamento</option>
|
||||||
<option value="Evento corporativo" @selected(old('tipo_evento') === 'Evento corporativo')>Evento corporativo</option>
|
<option value="Evento corporativo" @selected(old('tipo_evento', request('tipo_evento')) === 'Evento corporativo')>Evento corporativo</option>
|
||||||
<option value="Celebração intimista" @selected(old('tipo_evento') === 'Celebração intimista')>Celebração intimista</option>
|
<option value="Celebração intimista" @selected(old('tipo_evento') === 'Celebração intimista')>Celebração intimista</option>
|
||||||
<option value="Outro" @selected(old('tipo_evento') === 'Outro')>Outro tipo de evento</option>
|
<option value="Outro" @selected(old('tipo_evento') === 'Outro')>Outro tipo de evento</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
id="servico_interesse"
|
id="servico_interesse"
|
||||||
name="servico_interesse"
|
name="servico_interesse"
|
||||||
value="{{ old('servico_interesse') }}"
|
value="{{ old('servico_interesse', request('servico_interesse')) }}"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Ex.: planejamento completo"
|
placeholder="Ex.: planejamento completo"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('servico_interesse') border-amare-error @enderror"
|
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('servico_interesse') border-amare-error @enderror"
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
<x-home.hero :settings="$content->settings" />
|
<x-home.hero :settings="$content->settings" />
|
||||||
<x-home.manifesto :settings="$content->settings" />
|
<x-home.manifesto :settings="$content->settings" />
|
||||||
<x-home.services :services="$content->featuredServices" />
|
<x-home.services :services="$content->featuredServices" />
|
||||||
|
<x-home.wedding-packages :packages="$content->weddingPackages" :settings="$content->settings" />
|
||||||
|
<x-home.corporate />
|
||||||
<x-home.portfolio :cases="$content->featuredCases" />
|
<x-home.portfolio :cases="$content->featuredCases" />
|
||||||
<x-home.method :settings="$content->settings" />
|
<x-home.method :settings="$content->settings" />
|
||||||
<x-home.testimonials :testimonials="$content->testimonials" />
|
<x-home.testimonials :testimonials="$content->testimonials" />
|
||||||
|
|||||||
41
resources/views/pages/partners.blade.php
Normal file
41
resources/views/pages/partners.blade.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@extends('layouts.public')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<section class="border-b border-amare-border bg-amare-bg" data-motion="page-open">
|
||||||
|
<div class="container-amare grid gap-12 py-16 md:grid-cols-[1.15fr_.85fr] md:py-24" data-reveal-group>
|
||||||
|
<div class="space-y-6" data-motion-beat="heading" data-reveal data-reveal-from="up">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Contato profissional</p>
|
||||||
|
<h1 class="text-headline font-medium tracking-tight text-amare-text">Fornecedores e parcerias</h1>
|
||||||
|
<p class="max-w-2xl text-lg text-amare-muted">Se o seu trabalho conversa com a forma de atuar da Amare, apresente-se. Usaremos suas informações somente para avaliar uma possível relação profissional.</p>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-amare-border pt-6 text-amare-muted md:border-l md:border-t-0 md:pl-10 md:pt-0" data-reveal data-reveal-from="up">
|
||||||
|
<p>{{ $siteSettings->city ?: 'São Paulo - SP' }}</p>
|
||||||
|
@if ($siteSettings->email)<a class="mt-3 inline-flex min-h-11 items-center text-amare-accent underline hover:text-amare-accent-deep" href="mailto:{{ $siteSettings->email }}">{{ $siteSettings->email }}</a>@endif
|
||||||
|
<p class="mt-5 text-sm">Para solicitar uma proposta para um evento, use o <a class="text-amare-accent underline" href="{{ route('briefing') }}">briefing</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="border-b border-amare-border bg-amare-bg-deep" aria-labelledby="partner-form-heading">
|
||||||
|
<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>
|
||||||
|
@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
|
||||||
|
<div class="honeypot" aria-hidden="true"><label for="website">Não preencha este campo</label><input id="website" name="website" tabindex="-1" autocomplete="off"></div>
|
||||||
|
@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>
|
||||||
|
@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>
|
||||||
|
<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="inline-flex min-h-11 bg-amare-accent px-6 py-3 text-sm font-semibold uppercase tracking-[.12em] text-amare-accent-text hover:bg-amare-accent-deep">Enviar apresentação</button>
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@endsection
|
||||||
@@ -47,5 +47,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($weddingPackages->isNotEmpty())
|
||||||
|
<section class="border-b border-amare-border bg-amare-bg-deep py-16 md:py-24" aria-labelledby="wedding-packages-heading"><div class="container-amare"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">Casamentos</p><h2 id="wedding-packages-heading" class="mt-3 text-3xl font-medium">Modalidades de assessoria</h2><div class="mt-8 grid gap-6 md:grid-cols-3">@foreach ($weddingPackages as $package)<article class="border-t border-amare-border pt-4"><p class="text-sm text-amare-accent">{{ $package->level }}</p><h3 class="mt-2 text-2xl font-medium">{{ $package->name }}</h3><p class="mt-2 text-amare-muted">{{ $package->summary }}</p><ul class="mt-4 space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li>{{ $item }}</li>@endforeach</ul></article>@endforeach</div></div></section>
|
||||||
|
@endif
|
||||||
|
|
||||||
<x-home.final-cta :settings="$siteSettings" />
|
<x-home.final-cta :settings="$siteSettings" />
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
use App\Http\Controllers\PublicSite\ContactController;
|
use App\Http\Controllers\PublicSite\ContactController;
|
||||||
use App\Http\Controllers\PublicSite\HomeController;
|
use App\Http\Controllers\PublicSite\HomeController;
|
||||||
use App\Http\Controllers\PublicSite\PageController;
|
use App\Http\Controllers\PublicSite\PageController;
|
||||||
|
use App\Http\Controllers\PublicSite\PartnerInquiryController;
|
||||||
use App\Http\Controllers\PublicSite\PortfolioController;
|
use App\Http\Controllers\PublicSite\PortfolioController;
|
||||||
use App\Http\Controllers\PublicSite\RobotsController;
|
use App\Http\Controllers\PublicSite\RobotsController;
|
||||||
use App\Http\Controllers\PublicSite\ServiceController;
|
use App\Http\Controllers\PublicSite\ServiceController;
|
||||||
@@ -19,8 +20,12 @@ Route::get('/portfolio/{slug}', [PortfolioController::class, 'show'])->name('por
|
|||||||
Route::get('/sobre', [PageController::class, 'about'])->name('about');
|
Route::get('/sobre', [PageController::class, 'about'])->name('about');
|
||||||
Route::get('/privacidade', [PageController::class, 'privacy'])->name('privacy');
|
Route::get('/privacidade', [PageController::class, 'privacy'])->name('privacy');
|
||||||
Route::get('/contato', [PageController::class, 'contact'])->name('contact');
|
Route::get('/contato', [PageController::class, 'contact'])->name('contact');
|
||||||
Route::post('/contato', [ContactController::class, 'store'])
|
Route::post('/contato', [PartnerInquiryController::class, 'store'])
|
||||||
->middleware('throttle:contact-briefing')
|
->middleware('throttle:partner-inquiry')
|
||||||
->name('contact.store');
|
->name('contact.store');
|
||||||
|
Route::get('/briefing', [PageController::class, 'briefing'])->name('briefing');
|
||||||
|
Route::post('/briefing', [ContactController::class, 'store'])
|
||||||
|
->middleware('throttle:contact-briefing')
|
||||||
|
->name('briefing.store');
|
||||||
Route::get('/sitemap.xml', SitemapController::class)->name('sitemap');
|
Route::get('/sitemap.xml', SitemapController::class)->name('sitemap');
|
||||||
Route::get('/robots.txt', RobotsController::class)->name('robots');
|
Route::get('/robots.txt', RobotsController::class)->name('robots');
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ it('has no critical or serious accessibility issues on covered public routes', f
|
|||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$case->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
'/__missing-accessibility-page',
|
'/__missing-accessibility-page',
|
||||||
];
|
];
|
||||||
@@ -84,7 +85,7 @@ it('reaches the primary CTA by keyboard and activates it', function (): void {
|
|||||||
expect($outlineStyle)->not->toBe('none');
|
expect($outlineStyle)->not->toBe('none');
|
||||||
|
|
||||||
$page->keys('[data-testid="home-primary-cta"]', 'Enter')
|
$page->keys('[data-testid="home-primary-cta"]', 'Enter')
|
||||||
->assertPathIs('/contato');
|
->assertPathIs('/briefing');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads covered public routes without console errors', function (): void {
|
it('loads covered public routes without console errors', function (): void {
|
||||||
@@ -99,6 +100,7 @@ it('loads covered public routes without console errors', function (): void {
|
|||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$case->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
'/__missing-console-page',
|
'/__missing-console-page',
|
||||||
] as $route) {
|
] as $route) {
|
||||||
@@ -119,6 +121,7 @@ it('disables transitions when prefers-reduced-motion is reduce', function (): vo
|
|||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$case->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ beforeEach(function (): void {
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not overflow horizontally when contact info contains long unbroken strings', function (): void {
|
it('does not overflow horizontally when briefing contact info contains long unbroken strings', function (): void {
|
||||||
SiteSetting::instance()->update([
|
SiteSetting::instance()->update([
|
||||||
'email' => str_repeat('a', 50).'@email.'.str_repeat('b', 40),
|
'email' => str_repeat('a', 50).'@email.'.str_repeat('b', 40),
|
||||||
'phone' => '(11) 99999-9999',
|
'phone' => '(11) 99999-9999',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$page = $this->visit('/contato');
|
$page = $this->visit('/briefing');
|
||||||
|
|
||||||
$overflow = $page->script(
|
$overflow = $page->script(
|
||||||
'() => document.documentElement.scrollWidth - document.documentElement.clientWidth',
|
'() => document.documentElement.scrollWidth - document.documentElement.clientWidth',
|
||||||
@@ -27,7 +27,7 @@ it('does not overflow horizontally when contact info contains long unbroken stri
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows sending state on submit and resets on bfcache restore', function (): void {
|
it('shows sending state on submit and resets on bfcache restore', function (): void {
|
||||||
$page = $this->visit('/contato');
|
$page = $this->visit('/briefing');
|
||||||
|
|
||||||
$state = $page->script(<<<'JS'
|
$state = $page->script(<<<'JS'
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ it('keeps content and navigation usable without javascript', function (): void {
|
|||||||
->assertVisible('#hero-heading')
|
->assertVisible('#hero-heading')
|
||||||
->assertVisible('[data-testid="home-primary-cta"]')
|
->assertVisible('[data-testid="home-primary-cta"]')
|
||||||
->click('[data-testid="home-primary-cta"]')
|
->click('[data-testid="home-primary-cta"]')
|
||||||
->assertPathIs('/contato');
|
->assertPathIs('/briefing');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps targets final when intersection observer is unavailable', function (): void {
|
it('keeps targets final when intersection observer is unavailable', function (): void {
|
||||||
@@ -227,7 +227,7 @@ it('allows cta activation while its opening is in progress', function (): void {
|
|||||||
JS);
|
JS);
|
||||||
|
|
||||||
$page->click('[data-testid="home-primary-cta"]')
|
$page->click('[data-testid="home-primary-cta"]')
|
||||||
->assertPathIs('/contato');
|
->assertPathIs('/briefing');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
it('avoids horizontal overflow on visual public routes at desktop and mobile', function (): void {
|
||||||
@@ -242,6 +242,7 @@ it('avoids horizontal overflow on visual public routes at desktop and mobile', f
|
|||||||
'/portfolio/'.$case->slug,
|
'/portfolio/'.$case->slug,
|
||||||
'/sobre',
|
'/sobre',
|
||||||
'/contato',
|
'/contato',
|
||||||
|
'/briefing',
|
||||||
'/privacidade',
|
'/privacidade',
|
||||||
'/__missing-public-motion',
|
'/__missing-public-motion',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ it('renders the public home page', function (): void {
|
|||||||
|
|
||||||
it('renders the admin login page', function (): void {
|
it('renders the admin login page', function (): void {
|
||||||
$this->visit('/admin/login')
|
$this->visit('/admin/login')
|
||||||
->assertSee('E-mail');
|
->assertVisible('input[type="email"]');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ class PasswordResetRequestTest extends TestCase
|
|||||||
// `request()` resets the form (see the base page's `$this->form->fill()`).
|
// `request()` resets the form (see the base page's `$this->form->fill()`).
|
||||||
$page->set('data.email', $user->email)
|
$page->set('data.email', $user->email)
|
||||||
->call('request')
|
->call('request')
|
||||||
->assertNotified(Password::RESET_LINK_SENT);
|
->assertNotified($this->expectedSentNotification());
|
||||||
|
|
||||||
$page->set('data.email', $user->email)
|
$page->set('data.email', $user->email)
|
||||||
->call('request')
|
->call('request')
|
||||||
->assertNotified(Password::RESET_LINK_SENT);
|
->assertNotified($this->expectedSentNotification());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function expectedSentNotification(): FilamentNotification
|
private function expectedSentNotification(): FilamentNotification
|
||||||
|
|||||||
40
tests/Feature/Marketing/WeddingPackageTest.php
Normal file
40
tests/Feature/Marketing/WeddingPackageTest.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\Marketing;
|
||||||
|
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
|
use Database\Seeders\ContentSeeder;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class WeddingPackageTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_published_scope_excludes_drafts_and_keeps_cms_order(): void
|
||||||
|
{
|
||||||
|
WeddingPackage::factory()->create(['name' => 'Rascunho', 'published_at' => null]);
|
||||||
|
$second = WeddingPackage::factory()->published()->create(['name' => 'Segundo', 'sort_order' => 20]);
|
||||||
|
$first = WeddingPackage::factory()->published()->create(['name' => 'Primeiro', 'sort_order' => 10]);
|
||||||
|
|
||||||
|
$packages = WeddingPackage::query()->published()->orderBy('sort_order')->get();
|
||||||
|
|
||||||
|
$this->assertCount(2, $packages);
|
||||||
|
$this->assertTrue($packages->first()?->is($first));
|
||||||
|
$this->assertTrue($packages->last()?->is($second));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_content_seeder_creates_official_wedding_modalities(): void
|
||||||
|
{
|
||||||
|
$this->app->detectEnvironment(fn (): string => 'testing');
|
||||||
|
|
||||||
|
$this->seed(ContentSeeder::class);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
['Essenza', 'Conduzione', 'Grand Jour'],
|
||||||
|
WeddingPackage::query()->published()->orderBy('sort_order')->pluck('name')->all(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,11 +56,11 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$this->get('/contato?utm_source=instagram&utm_medium=social&utm_campaign=lancamento-2026')
|
$this->get('/briefing?utm_source=instagram&utm_medium=social&utm_campaign=lancamento-2026')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -76,10 +76,10 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
// No GET beforehand: posting cold, exactly like a direct/no-JS submission.
|
// No GET beforehand: posting cold, exactly like a direct/no-JS submission.
|
||||||
$response = $this->post(route('contact.store'), $this->validPayload());
|
$response = $this->post(route('briefing.store'), $this->validPayload());
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -97,11 +97,11 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
// identifiable e-mail/subscriber id) — only the site identity may
|
// identifiable e-mail/subscriber id) — only the site identity may
|
||||||
// be captured as marketing origin, never the query string itself.
|
// be captured as marketing origin, never the query string itself.
|
||||||
$this->withHeader('referer', 'https://mail.example.com/click?email=maria%40example.com&subscriber_id=42')
|
$this->withHeader('referer', 'https://mail.example.com/click?email=maria%40example.com&subscriber_id=42')
|
||||||
->get('/contato')
|
->get('/briefing')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -114,15 +114,15 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$this->get('/contato?utm_source=google&utm_medium=cpc')
|
$this->get('/briefing?utm_source=google&utm_medium=cpc')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
// Navigate elsewhere with no UTM parameters at all before submitting.
|
// Navigate elsewhere with no UTM parameters at all before submitting.
|
||||||
$this->get('/servicos')->assertOk();
|
$this->get('/servicos')->assertOk();
|
||||||
$this->get('/sobre')->assertOk();
|
$this->get('/sobre')->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -135,16 +135,16 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$this->get('/contato?utm_source=google&utm_medium=cpc')
|
$this->get('/briefing?utm_source=google&utm_medium=cpc')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
// A second page load with its own, different UTM parameters must
|
// A second page load with its own, different UTM parameters must
|
||||||
// not overwrite what was captured on first touch.
|
// not overwrite what was captured on first touch.
|
||||||
$this->get('/contato?utm_source=facebook&utm_medium=social')
|
$this->get('/briefing?utm_source=facebook&utm_medium=social')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -157,11 +157,11 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$this->get('/contato?utm_source=instagram&utm_campaign=segredo-interno')
|
$this->get('/briefing?utm_source=instagram&utm_campaign=segredo-interno')
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefingConfirmation::class, function (ContactBriefingConfirmation $mail): bool {
|
Mail::assertQueued(ContactBriefingConfirmation::class, function (ContactBriefingConfirmation $mail): bool {
|
||||||
@@ -187,10 +187,10 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
// the array shape the controller expects. The acceptance
|
// the array shape the controller expects. The acceptance
|
||||||
// criterion is that this never blocks the submission.
|
// criterion is that this never blocks the submission.
|
||||||
$response = $this->withSession([MarketingOrigin::SESSION_KEY => 'lixo'])
|
$response = $this->withSession([MarketingOrigin::SESSION_KEY => 'lixo'])
|
||||||
->post(route('contact.store'), $this->validPayload());
|
->post(route('briefing.store'), $this->validPayload());
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail): bool {
|
||||||
@@ -206,13 +206,13 @@ class ContactBriefingOriginTest extends TestCase
|
|||||||
|
|
||||||
$overlongCampaign = str_repeat('a', 300);
|
$overlongCampaign = str_repeat('a', 300);
|
||||||
|
|
||||||
$this->get('/contato?'.http_build_query([
|
$this->get('/briefing?'.http_build_query([
|
||||||
'utm_source' => '<script>alert(1)</script>',
|
'utm_source' => '<script>alert(1)</script>',
|
||||||
'utm_campaign' => $overlongCampaign,
|
'utm_campaign' => $overlongCampaign,
|
||||||
]))->assertOk();
|
]))->assertOk();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $this->validPayload())
|
$this->post(route('briefing.store'), $this->validPayload())
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
$capturedOrigin = null;
|
$capturedOrigin = null;
|
||||||
|
|||||||
@@ -45,16 +45,16 @@ class ContactBriefingTest extends TestCase
|
|||||||
], $overrides);
|
], $overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_contact_page_renders_briefing_form_with_all_fields(): void
|
public function test_briefing_page_renders_event_form_with_all_fields(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
|
|
||||||
$response = $this->get(route('contact'));
|
$response = $this->get(route('briefing'));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('<form', false)
|
->assertSee('<form', false)
|
||||||
->assertSee('action="'.route('contact.store').'"', false)
|
->assertSee('action="'.route('briefing.store').'"', false)
|
||||||
->assertSee('name="nome"', false)
|
->assertSee('name="nome"', false)
|
||||||
->assertSee('name="email"', false)
|
->assertSee('name="email"', false)
|
||||||
->assertSee('name="telefone"', false)
|
->assertSee('name="telefone"', false)
|
||||||
@@ -74,10 +74,10 @@ class ContactBriefingTest extends TestCase
|
|||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$response = $this->post(route('contact.store'), $this->validPayload());
|
$response = $this->post(route('briefing.store'), $this->validPayload());
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail) use ($settings): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail) use ($settings): bool {
|
||||||
@@ -94,13 +94,13 @@ class ContactBriefingTest extends TestCase
|
|||||||
$settings = SiteSetting::instance();
|
$settings = SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$response = $this->post(route('contact.store'), $this->validPayload([
|
$response = $this->post(route('briefing.store'), $this->validPayload([
|
||||||
'email' => ' Maria.Silva@EXAMPLE.com ',
|
'email' => ' Maria.Silva@EXAMPLE.com ',
|
||||||
'telefone' => '+55 (11) 98888-7777',
|
'telefone' => '+55 (11) 98888-7777',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail) use ($settings): bool {
|
Mail::assertQueued(ContactBriefing::class, function (ContactBriefing $mail) use ($settings): bool {
|
||||||
@@ -119,12 +119,12 @@ class ContactBriefingTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$response = $this->post(route('contact.store'), $this->validPayload([
|
$response = $this->post(route('briefing.store'), $this->validPayload([
|
||||||
'empresa' => 'http://spam.example',
|
'empresa' => 'http://spam.example',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
|
|
||||||
Mail::assertNothingSent();
|
Mail::assertNothingSent();
|
||||||
@@ -135,11 +135,11 @@ class ContactBriefingTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$response = $this->from(route('contact'))->post(route('contact.store'), $this->validPayload([
|
$response = $this->from(route('briefing'))->post(route('briefing.store'), $this->validPayload([
|
||||||
'privacidade' => '',
|
'privacidade' => '',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$response->assertRedirect(route('contact'));
|
$response->assertRedirect(route('briefing'));
|
||||||
$response->assertSessionHasErrors('privacidade');
|
$response->assertSessionHasErrors('privacidade');
|
||||||
|
|
||||||
Mail::assertNothingSent();
|
Mail::assertNothingSent();
|
||||||
@@ -150,9 +150,9 @@ class ContactBriefingTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
Mail::fake();
|
Mail::fake();
|
||||||
|
|
||||||
$response = $this->from(route('contact'))->post(route('contact.store'), []);
|
$response = $this->from(route('briefing'))->post(route('briefing.store'), []);
|
||||||
|
|
||||||
$response->assertRedirect(route('contact'));
|
$response->assertRedirect(route('briefing'));
|
||||||
|
|
||||||
foreach (['nome', 'email', 'telefone', 'tipo_evento', 'cidade', 'mensagem', 'privacidade'] as $field) {
|
foreach (['nome', 'email', 'telefone', 'tipo_evento', 'cidade', 'mensagem', 'privacidade'] as $field) {
|
||||||
$response->assertSessionHasErrors($field);
|
$response->assertSessionHasErrors($field);
|
||||||
@@ -168,8 +168,8 @@ class ContactBriefingTest extends TestCase
|
|||||||
|
|
||||||
$payload = $this->validPayload();
|
$payload = $this->validPayload();
|
||||||
|
|
||||||
$this->post(route('contact.store'), $payload)->assertRedirect(route('contact'));
|
$this->post(route('briefing.store'), $payload)->assertRedirect(route('briefing'));
|
||||||
$this->post(route('contact.store'), $payload)->assertRedirect(route('contact'));
|
$this->post(route('briefing.store'), $payload)->assertRedirect(route('briefing'));
|
||||||
|
|
||||||
Mail::assertQueued(ContactBriefing::class, 1);
|
Mail::assertQueued(ContactBriefing::class, 1);
|
||||||
Mail::assertQueued(ContactBriefingConfirmation::class, 1);
|
Mail::assertQueued(ContactBriefingConfirmation::class, 1);
|
||||||
@@ -180,10 +180,10 @@ class ContactBriefingTest extends TestCase
|
|||||||
SiteSetting::instance();
|
SiteSetting::instance();
|
||||||
config(['mail.default' => 'mailer-inexistente']);
|
config(['mail.default' => 'mailer-inexistente']);
|
||||||
|
|
||||||
$response = $this->post(route('contact.store'), $this->validPayload());
|
$response = $this->post(route('briefing.store'), $this->validPayload());
|
||||||
|
|
||||||
$response
|
$response
|
||||||
->assertRedirect(route('contact'))
|
->assertRedirect(route('briefing'))
|
||||||
->assertSessionHas('status', 'briefing-sent');
|
->assertSessionHas('status', 'briefing-sent');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
|
|||||||
use App\Models\Service;
|
use App\Models\Service;
|
||||||
use App\Models\SiteSetting;
|
use App\Models\SiteSetting;
|
||||||
use App\Models\Testimonial;
|
use App\Models\Testimonial;
|
||||||
|
use App\Models\WeddingPackage;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ class HomePageContentTest extends TestCase
|
|||||||
->assertDontSee('Caso Rascunho')
|
->assertDontSee('Caso Rascunho')
|
||||||
->assertDontSee('Autor Rascunho')
|
->assertDontSee('Autor Rascunho')
|
||||||
->assertSee('data-testid="home-primary-cta"', false)
|
->assertSee('data-testid="home-primary-cta"', false)
|
||||||
->assertSee('href="'.route('contact').'"', false);
|
->assertSee('href="'.route('briefing').'"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_empty_sections_are_omitted_when_no_published_content(): void
|
public function test_empty_sections_are_omitted_when_no_published_content(): void
|
||||||
@@ -103,7 +104,8 @@ class HomePageContentTest extends TestCase
|
|||||||
$response
|
$response
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertDontSee('id="services-heading"', false)
|
->assertDontSee('id="services-heading"', false)
|
||||||
->assertDontSee('id="portfolio-heading"', false)
|
->assertSee('id="portfolio-heading"', false)
|
||||||
|
->assertSee('Nosso acervo de eventos reais e autorizados está em preparação.')
|
||||||
->assertDontSee('id="testimonials-heading"', false)
|
->assertDontSee('id="testimonials-heading"', false)
|
||||||
->assertSee('id="manifesto-heading"', false)
|
->assertSee('id="manifesto-heading"', false)
|
||||||
->assertSee('id="method-heading"', false)
|
->assertSee('id="method-heading"', false)
|
||||||
@@ -111,6 +113,34 @@ class HomePageContentTest extends TestCase
|
|||||||
->assertSee('id="final-cta-heading"', false);
|
->assertSee('id="final-cta-heading"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_home_exposes_anchored_wedding_and_corporate_chapters_with_cta_fallback(): void
|
||||||
|
{
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
$settings->update(['whatsapp_number' => null]);
|
||||||
|
WeddingPackage::factory()->published()->create(['name' => 'Essenza', 'sort_order' => 1]);
|
||||||
|
|
||||||
|
$this->get(route('home'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('id="amare"', false)
|
||||||
|
->assertSee('id="casamentos"', false)
|
||||||
|
->assertSee('id="corporate"', false)
|
||||||
|
->assertSee('id="portfolio"', false)
|
||||||
|
->assertSee('Essenza')
|
||||||
|
->assertSee(route('briefing', ['servico_interesse' => 'Essenza']), false)
|
||||||
|
->assertSee(route('briefing', ['tipo_evento' => 'Evento corporativo']), false)
|
||||||
|
->assertDontSee('https://wa.me/', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_wedding_cta_uses_contextual_whatsapp_link_when_official_number_is_valid(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance()->update(['whatsapp_number' => '+55 (11) 98888-7777']);
|
||||||
|
WeddingPackage::factory()->published()->create(['name' => 'Grand Jour']);
|
||||||
|
|
||||||
|
$this->get(route('home'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('https://wa.me/5511988887777?text=Ol%C3%A1%2C%20gostaria%20de%20conversar%20sobre%20a%20modalidade%20Grand%20Jour%20para%20meu%20casamento.', false);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_hero_falls_back_to_default_copy_when_fields_are_empty(): void
|
public function test_hero_falls_back_to_default_copy_when_fields_are_empty(): void
|
||||||
{
|
{
|
||||||
SiteSetting::instance()->update([
|
SiteSetting::instance()->update([
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class MotionMarkupTest extends TestCase
|
|||||||
->assertDontSee('data-chapter-index', false)
|
->assertDontSee('data-chapter-index', false)
|
||||||
->assertDontSee('data-chapter-progress', false)
|
->assertDontSee('data-chapter-progress', false)
|
||||||
->assertDontSee('id="services-heading"', false)
|
->assertDontSee('id="services-heading"', false)
|
||||||
->assertDontSee('id="portfolio-heading"', false)
|
->assertSee('id="portfolio-heading"', false)
|
||||||
->assertDontSee('id="testimonials-heading"', false);
|
->assertDontSee('id="testimonials-heading"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +131,7 @@ class MotionMarkupTest extends TestCase
|
|||||||
$this->get(route('portfolio.show', $case->slug)),
|
$this->get(route('portfolio.show', $case->slug)),
|
||||||
$this->get(route('about')),
|
$this->get(route('about')),
|
||||||
$this->get(route('contact')),
|
$this->get(route('contact')),
|
||||||
|
$this->get(route('briefing')),
|
||||||
$this->get(route('privacy')),
|
$this->get(route('privacy')),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
80
tests/Feature/PublicSite/PartnerInquiryTest.php
Normal file
80
tests/Feature/PublicSite/PartnerInquiryTest.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Feature\PublicSite;
|
||||||
|
|
||||||
|
use App\Mail\PartnerInquiry;
|
||||||
|
use App\Models\SiteSetting;
|
||||||
|
use Illuminate\Foundation\Http\Middleware\PreventRequestForgery;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||||
|
use Illuminate\Support\Facades\Mail;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class PartnerInquiryTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->withoutMiddleware([ThrottleRequests::class, PreventRequestForgery::class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
private function validPayload(array $overrides = []): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'nome' => 'Ana Costa',
|
||||||
|
'empresa' => 'Flores da Ana',
|
||||||
|
'email' => 'ana@example.com',
|
||||||
|
'atuacao' => 'Florista',
|
||||||
|
'area_atendimento' => 'São Paulo capital',
|
||||||
|
'mensagem' => 'Gostaria de apresentar nosso portfólio para futuras parcerias.',
|
||||||
|
'portfolio_redes' => 'https://instagram.com/floresdaana',
|
||||||
|
'telefone' => '(11) 98888-7777',
|
||||||
|
'website' => '',
|
||||||
|
], $overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_contact_page_is_a_partner_journey_not_the_event_briefing(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
|
||||||
|
$this->get(route('contact'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Fornecedores e parcerias')
|
||||||
|
->assertSee('name="atuacao"', false)
|
||||||
|
->assertSee('name="area_atendimento"', false)
|
||||||
|
->assertSee('name="website"', false)
|
||||||
|
->assertDontSee('name="tipo_evento"', false)
|
||||||
|
->assertSee(route('privacy'), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_valid_partner_inquiry_sends_only_the_dedicated_internal_email(): void
|
||||||
|
{
|
||||||
|
$settings = SiteSetting::instance();
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$this->post(route('contact.store'), $this->validPayload())
|
||||||
|
->assertRedirect(route('contact'))
|
||||||
|
->assertSessionHas('status', 'partner-inquiry-sent');
|
||||||
|
|
||||||
|
Mail::assertQueued(PartnerInquiry::class, fn (PartnerInquiry $mail): bool => $mail->hasTo($settings->email));
|
||||||
|
Mail::assertQueued(PartnerInquiry::class, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_partner_honeypot_reports_success_without_sending_mail(): void
|
||||||
|
{
|
||||||
|
SiteSetting::instance();
|
||||||
|
Mail::fake();
|
||||||
|
|
||||||
|
$this->post(route('contact.store'), $this->validPayload(['website' => 'https://spam.invalid']))
|
||||||
|
->assertRedirect(route('contact'))
|
||||||
|
->assertSessionHas('status', 'partner-inquiry-sent');
|
||||||
|
|
||||||
|
Mail::assertNothingSent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -184,11 +184,18 @@ class PublicPagesTest extends TestCase
|
|||||||
->assertSee('São Paulo - SP')
|
->assertSee('São Paulo - SP')
|
||||||
->assertDontSee('Fortaleza')
|
->assertDontSee('Fortaleza')
|
||||||
->assertSee('https://instagram.com/amare', false)
|
->assertSee('https://instagram.com/amare', false)
|
||||||
->assertSee('min-h-[calc(100dvh-14rem)]', false)
|
->assertSee('Fornecedores e parcerias')
|
||||||
->assertSee('<form', false)
|
->assertSee('<form', false)
|
||||||
->assertSee('name="nome"', false)
|
->assertSee('name="nome"', false)
|
||||||
->assertSee('name="privacidade"', false)
|
->assertSee('name="atuacao"', false)
|
||||||
|
->assertDontSee('name="tipo_evento"', false)
|
||||||
->assertSee(route('privacy'), false);
|
->assertSee(route('privacy'), false);
|
||||||
|
|
||||||
|
$this->get(route('briefing'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('min-h-[calc(100dvh-14rem)]', false)
|
||||||
|
->assertSee('name="tipo_evento"', false)
|
||||||
|
->assertSee('name="privacidade"', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_site_setting_defaults_use_sao_paulo_and_official_email(): void
|
public function test_site_setting_defaults_use_sao_paulo_and_official_email(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user