Files
amare/app/Filament/Pages/ManageSiteSettings.php
Manoel Freitas 484ac77ce7 feat: Home editorial conforme preview (header, footer, 8 seções) (#53)
* feat: modalidades de casamento e etapas corporativas

* feat: home editorial e header/footer conforme preview

* docs: proposta openspec home editorial

* test: CTA hero agora ancora #sobre conforme preview

* test: speed browser suite via visit reuse

Collapse route×viewport cartesian products, merge home motion
asserts, skip existing VisualContentSeeder JPEGs, ignore Screenshots.
2026-08-11 18:14:24 -03:00

341 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Models\SiteSetting;
use App\Support\PublicImageUploadRules;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
use Filament\Forms\Components\KeyValue;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Concerns\CanUseDatabaseTransactions;
use Filament\Pages\Page;
use Filament\Schemas\Components\Actions;
use Filament\Schemas\Components\Component;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Components\Form;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Support\Exceptions\Halt;
use Filament\Support\Facades\FilamentView;
use Filament\Support\Icons\Heroicon;
use Livewire\Attributes\Locked;
use Throwable;
use UnitEnum;
/**
* @property-read Schema $form
*/
class ManageSiteSettings extends Page
{
use CanUseDatabaseTransactions;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedCog6Tooth;
protected static ?string $navigationLabel = 'Configurações';
protected static ?string $title = 'Configurações do site';
protected static string|UnitEnum|null $navigationGroup = 'Conteúdo do site';
protected static ?int $navigationSort = 1;
/**
* @var array<string, mixed>|null
*/
public ?array $data = [];
#[Locked]
public SiteSetting $record;
public static function canAccess(): bool
{
$user = auth()->user();
return $user !== null && $user->isAdmin();
}
public function mount(): void
{
$this->record = SiteSetting::instance();
abort_unless(auth()->user()?->can('update', $this->record), 403);
$this->fillForm();
}
public function hydrate(): void
{
abort_unless(auth()->user()?->can('update', $this->record), 403);
}
protected function fillForm(): void
{
$this->form->fill($this->record->attributesToArray());
}
public function save(): void
{
try {
$this->beginDatabaseTransaction();
$data = $this->form->getState();
$this->record->update($data);
} catch (Halt $exception) {
$exception->shouldRollbackDatabaseTransaction() ?
$this->rollBackDatabaseTransaction() :
$this->commitDatabaseTransaction();
return;
} catch (Throwable $exception) {
$this->rollBackDatabaseTransaction();
throw $exception;
}
$this->commitDatabaseTransaction();
Notification::make()
->success()
->title('Configurações salvas com sucesso.')
->send();
if ($redirectUrl = $this->getRedirectUrl()) {
$this->redirect($redirectUrl, navigate: FilamentView::hasSpaMode($redirectUrl));
}
}
protected function getRedirectUrl(): ?string
{
return null;
}
public function defaultForm(Schema $schema): Schema
{
return $schema
->operation('edit')
->model($this->record)
->statePath('data');
}
public function form(Schema $schema): Schema
{
return $schema
->components([
Section::make('Marca e hero')
->schema([
TextInput::make('brand_name')
->label('Nome da marca')
->required()
->maxLength(255),
PublicImageUploadRules::fileUpload('logo_path', 'Logo da marca', 'content/logo'),
PublicImageUploadRules::altTextField('logo_alt', 'logo_path', 'Texto alternativo do logo'),
TextInput::make('hero_eyebrow')
->label('Eyebrow do hero')
->maxLength(255),
TextInput::make('hero_title')
->label('Título do hero')
->required()
->maxLength(255),
Textarea::make('hero_subtitle')
->label('Subtítulo do hero')
->required()
->rows(3),
TextInput::make('hero_cta_label')
->label('Texto do CTA principal')
->required()
->maxLength(255),
TextInput::make('hero_secondary_cta_label')
->label('Texto do CTA secundário')
->maxLength(255),
Textarea::make('hero_note')
->label('Nota do hero')
->rows(2),
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem do hero', 'content/home'),
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
Textarea::make('about_summary')
->label('Resumo institucional')
->rows(3),
])
->columns(2),
Section::make('Imagens das aberturas')
->description('Fotos reais e autorizadas para as primeiras dobras. Sem imagem, cada rota mantém a abertura tonal.')
->schema([
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem da home', 'content/heroes'),
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
PublicImageUploadRules::fileUpload('services_hero_image_path', 'Imagem de Serviços', 'content/heroes'),
PublicImageUploadRules::altTextField('services_hero_image_alt', 'services_hero_image_path'),
PublicImageUploadRules::fileUpload('portfolio_hero_image_path', 'Imagem de Portfólio', 'content/heroes'),
PublicImageUploadRules::altTextField('portfolio_hero_image_alt', 'portfolio_hero_image_path'),
])
->columns(2),
Section::make('Manifesto editorial')
->schema([
TextInput::make('manifesto_title')
->label('Título do manifesto')
->maxLength(255),
Textarea::make('manifesto_lead')
->label('Lead do manifesto')
->rows(3),
Textarea::make('manifesto_body')
->label('Corpo do manifesto')
->rows(4),
]),
Section::make('Método')
->schema([
Textarea::make('method_intro')
->label('Introdução do método')
->rows(2),
Repeater::make('method_steps')
->label('Passos do método')
->schema([
TextInput::make('title')
->label('Título')
->required()
->maxLength(255),
Textarea::make('body')
->label('Descrição')
->required()
->rows(2),
])
->defaultItems(0)
->maxItems(4)
->reorderable()
->columnSpanFull(),
]),
Section::make('Corporate')
->description('Etapas apresentadas na frente Amare Corporate da home.')
->schema([
Repeater::make('corporate_steps')
->label('Etapas do serviço corporativo')
->schema([
TextInput::make('title')
->label('Título')
->required()
->maxLength(255),
Textarea::make('body')
->label('Descrição')
->required()
->rows(2),
])
->defaultItems(3)
->maxItems(6)
->reorderable()
->columnSpanFull(),
]),
Section::make('Princípios')
->schema([
TagsInput::make('principles')
->label('Princípios')
->placeholder('Adicionar princípio')
->helperText('Até quatro princípios editoriais.')
->columnSpanFull(),
]),
Section::make('Contato')
->schema([
TextInput::make('email')
->label('E-mail')
->email()
->required()
->maxLength(255),
TextInput::make('phone')
->label('Telefone')
->required()
->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')
->label('Cidade')
->maxLength(255),
KeyValue::make('social_links')
->label('Redes sociais')
->keyLabel('Rede')
->valueLabel('URL')
->addActionLabel('Adicionar rede'),
])
->columns(2),
Section::make('Página Sobre')
->schema([
PublicImageUploadRules::fileUpload('about_image_path', 'Imagem da página Sobre', 'content/about'),
PublicImageUploadRules::altTextField('about_image_alt', 'about_image_path'),
])
->columns(2),
Section::make('SEO padrão')
->schema([
TextInput::make('default_meta_title')
->label('Meta title padrão')
->required()
->maxLength(255),
Textarea::make('default_meta_description')
->label('Meta description padrão')
->required()
->rows(3),
PublicImageUploadRules::fileUpload('default_og_image_path', 'Imagem Open Graph padrão', 'content/og'),
PublicImageUploadRules::altTextField('default_og_image_alt', 'default_og_image_path'),
]),
Section::make('Analytics')
->schema([
Toggle::make('analytics_enabled')
->label('Analytics habilitado')
->default(false),
Textarea::make('analytics_script')
->label('Script de analytics')
->rows(4)
->visible(fn (callable $get): bool => (bool) $get('analytics_enabled')),
]),
]);
}
/**
* @return array<Action|ActionGroup>
*/
protected function getFormActions(): array
{
return [
Action::make('save')
->label('Salvar')
->submit('save')
->keyBindings(['mod+s']),
];
}
public function content(Schema $schema): Schema
{
return $schema
->components([
$this->getFormContentComponent(),
]);
}
public function getFormContentComponent(): Component
{
return Form::make([EmbeddedSchema::make('form')])
->id('form')
->livewireSubmitHandler('save')
->footer([
Actions::make($this->getFormActions())
->alignment($this->getFormActionsAlignment())
->fullWidth($this->hasFullWidthFormActions())
->sticky($this->areFormActionsSticky())
->key('form-actions'),
]);
}
protected function hasFullWidthFormActions(): bool
{
return false;
}
}