feat(settings): implement application settings management with contact, general, social, and SEO forms

This commit is contained in:
Yoga Pangestu 2025-10-18 19:37:49 +07:00
parent cc64a09250
commit 8c7f869c76
17 changed files with 722 additions and 0 deletions

View File

@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Forms\Studio\Setting;
use App\Rules\PhoneNumber;
use App\Settings\ContactSettings;
use Illuminate\Validation\Rule;
use Livewire\Form;
class ContactForm extends Form
{
public string $phone_number = '';
public ?string $email = null;
public string $address = '';
public ?string $map_embed = null;
public function rules(): array
{
return [
'phone_number' => ['required', 'string', new PhoneNumber],
'email' => ['nullable', 'email', 'max:100', Rule::unique('users', 'email')],
'address' => ['required', 'string', 'max:255'],
'map_embed' => ['nullable', 'string'],
];
}
public function validationAttributes(): array
{
return [
'phone_number' => 'nomor telepon',
'address' => 'alamat',
'map_embed' => 'map embed',
];
}
public function setSettings(ContactSettings $settings): void
{
$this->address = $settings->address;
$this->phone_number = $settings->phone_number;
$this->email = $settings->email;
$this->map_embed = $settings->map_embed;
}
public function update(ContactSettings $settings): void
{
$this->validate();
$settings->address = $this->address;
$settings->phone_number = $this->phone_number;
$settings->email = $this->email;
$settings->map_embed = $this->map_embed;
$settings->save();
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Livewire\Forms\Studio\Setting;
use App\Settings\GeneralSettings;
use Livewire\Form;
class GeneralForm extends Form
{
public string $site_name = '';
public string $site_tagline = '';
public string $site_description = '';
public bool $maintenance_mode = false;
public function rules(): array
{
return [
'site_name' => ['required', 'string', 'max:100'],
'site_tagline' => ['required', 'string', 'max:150'],
'site_description' => ['required', 'string'],
'maintenance_mode' => ['required', 'boolean'],
];
}
public function setSettings(GeneralSettings $settings): void
{
$this->site_name = $settings->site_name;
$this->site_tagline = $settings->site_tagline;
$this->site_description = $settings->site_description;
$this->maintenance_mode = $settings->maintenance_mode;
}
public function update(GeneralSettings $settings): void
{
$this->validate();
$settings->site_name = $this->site_name;
$settings->site_tagline = $this->site_tagline;
$settings->site_description = $this->site_description;
$settings->maintenance_mode = $this->maintenance_mode;
$settings->save();
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Livewire\Forms\Studio\Setting;
use App\Settings\SeoSettings;
use Livewire\Form;
class SeoForm extends Form
{
public string $meta_description = '';
public string $meta_keywords = '';
public string $meta_author = '';
public ?string $og_image = null;
public ?string $og_title = null;
public ?string $og_description = null;
public ?string $og_type = null;
public ?string $twitter_card = null;
public ?string $canonical_url = null;
public function rules(): array
{
return [
'meta_description' => ['required', 'string'],
'meta_keywords' => ['required', 'string'],
'meta_author' => ['required', 'string', 'max:150'],
'og_image' => ['nullable', 'string', 'max:255'],
'og_title' => ['nullable', 'string', 'max:255'],
'og_description' => ['nullable', 'string'],
'og_type' => ['nullable', 'string', 'max:50'],
'twitter_card' => ['nullable', 'string', 'max:50'],
'canonical_url' => ['nullable', 'string', 'max:255'],
];
}
public function setSettings(SeoSettings $settings): void
{
$this->meta_description = $settings->meta_description;
$this->meta_keywords = $settings->meta_keywords;
$this->meta_author = $settings->meta_author;
$this->og_image = $settings->og_image;
$this->og_title = $settings->og_title;
$this->og_description = $settings->og_description;
$this->og_type = $settings->og_type;
$this->twitter_card = $settings->twitter_card;
$this->canonical_url = $settings->canonical_url;
}
public function update(SeoSettings $settings): void
{
$this->validate();
$settings->meta_description = $this->meta_description;
$settings->meta_keywords = $this->meta_keywords;
$settings->meta_author = $this->meta_author;
$settings->og_image = $this->og_image;
$settings->og_title = $this->og_title;
$settings->og_description = $this->og_description;
$settings->og_type = $this->og_type;
$settings->twitter_card = $this->twitter_card;
$settings->canonical_url = $this->canonical_url;
$settings->save();
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace App\Livewire\Forms\Studio\Setting;
use App\Settings\SocialSettings;
use Livewire\Form;
class SocialForm extends Form
{
public ?string $facebook = null;
public ?string $instagram = null;
public ?string $youtube = null;
public ?string $tiktok = null;
public ?string $whatsapp = null;
public ?string $telegram = null;
protected function rules(): array
{
return [
'facebook' => ['nullable', 'url'],
'instagram' => ['nullable', 'url'],
'youtube' => ['nullable', 'url'],
'tiktok' => ['nullable', 'url'],
'whatsapp' => ['required', 'url'],
'telegram' => ['nullable', 'url'],
];
}
public function setSettings(SocialSettings $settings): void
{
$this->facebook = $settings->facebook;
$this->instagram = $settings->instagram;
$this->youtube = $settings->youtube;
$this->tiktok = $settings->tiktok;
$this->whatsapp = $settings->whatsapp;
$this->telegram = $settings->telegram;
}
public function update(SocialSettings $settings): void
{
$this->validate();
$settings->facebook = $this->facebook;
$settings->instagram = $this->instagram;
$settings->youtube = $this->youtube;
$settings->tiktok = $this->tiktok;
$settings->whatsapp = $this->whatsapp;
$settings->telegram = $this->telegram;
$settings->save();
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Livewire\Studio\Setting;
use App\Livewire\Forms\Studio\Setting\ContactForm;
use App\Livewire\Forms\Studio\Setting\GeneralForm;
use App\Livewire\Forms\Studio\Setting\SeoForm;
use App\Livewire\Forms\Studio\Setting\SocialForm;
use App\Settings\ContactSettings;
use App\Settings\GeneralSettings;
use App\Settings\SeoSettings;
use App\Settings\SocialSettings;
use App\Traits\WithAuthorization;
use App\Traits\WithToast;
use App\Traits\WithUpdatedData;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Aplikasi')]
class Application extends Component
{
use WithAuthorization, WithToast, WithUpdatedData;
public GeneralForm $generalForm;
public ContactForm $contactForm;
public SocialForm $socialForm;
public SeoForm $seoForm;
public function mount(
GeneralSettings $generalSettings,
ContactSettings $contactSettings,
SocialSettings $socialSettings,
SeoSettings $seoSettings,
) {
$this->generalForm->setSettings($generalSettings);
$this->contactForm->setSettings($contactSettings);
$this->socialForm->setSettings($socialSettings);
$this->seoForm->setSettings($seoSettings);
}
public function updateGeneral(GeneralSettings $generalSettings)
{
$this->canOrAbort('update general settings');
$this->generalForm->update($generalSettings);
$this->toast('Pengaturan umum berhasil diperbarui.');
}
public function updateContact(ContactSettings $contactSettings)
{
$this->canOrAbort('update contact settings');
$this->contactForm->update($contactSettings);
$this->toast('Pengaturan kontak berhasil diperbarui.');
}
public function updateSocial(SocialSettings $socialSettings)
{
$this->canOrAbort('update social settings');
$this->socialForm->update($socialSettings);
$this->toast('Pengaturan media sosial berhasil diperbarui.');
}
public function updateSeo(SeoSettings $seoSettings)
{
$this->canOrAbort('update seo settings');
$this->seoForm->update($seoSettings);
$this->toast('Pengaturan SEO berhasil diperbarui.');
}
public function render()
{
return view('livewire.studio.setting.application', [
'pageTitle' => 'Aplikasi',
]);
}
}

View File

@ -153,6 +153,18 @@ public function boot(): void
],
],
],
[
'heading' => 'Pengaturan',
'items' => [
[
'label' => 'Aplikasi',
'icon' => 'cog-6-tooth',
'route' => 'studio.setting.application',
'match' => 'studio.setting.application',
'can' => 'view application settings',
],
],
],
];
$view->with('sidebar', $sidebar);

View File

@ -0,0 +1,21 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class ContactSettings extends Settings
{
public string $phone_number;
public ?string $email = null;
public string $address;
public ?string $map_embed = null;
public static function group(): string
{
return 'contact';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class GeneralSettings extends Settings
{
public string $site_name;
public string $site_tagline;
public string $site_description;
public bool $maintenance_mode;
public static function group(): string
{
return 'general';
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class SeoSettings extends Settings
{
public string $meta_description;
public string $meta_keywords;
public string $meta_author;
public ?string $og_image = null;
public ?string $og_title = null;
public ?string $og_description = null;
public ?string $og_type = null;
public ?string $twitter_card = null;
public ?string $canonical_url = null;
public static function group(): string
{
return 'seo';
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class SocialSettings extends Settings
{
public ?string $facebook = null;
public ?string $instagram = null;
public ?string $youtube = null;
public ?string $tiktok = null;
public ?string $whatsapp = null;
public ?string $telegram = null;
public static function group(): string
{
return 'social';
}
}

View File

@ -100,6 +100,20 @@ public function run(): void
'view purchase',
'create purchase',
'delete purchase',
'view application settings',
'view general settings',
'update general settings',
'view contact settings',
'update contact settings',
'view seo settings',
'update seo settings',
'view social settings',
'update social settings',
];
foreach ($permissions as $permission) {

View File

@ -0,0 +1,21 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('general.site_name', 'Yadi Parfum');
$this->migrator->add('general.site_tagline', 'Seungit Pisan');
$this->migrator->add('general.site_description', 'Yadi Parfum menyediakan parfum refill dan produk pewangi dengan kualitas terbaik.');
$this->migrator->add('general.logo', null);
$this->migrator->add('general.favicon', null);
$this->migrator->add('general.maintenance_mode', false);
}
public function down(): void
{
$this->migrator->deleteIfExists('general');
}
};

View File

@ -0,0 +1,19 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('contact.phone_number', '083816931711');
$this->migrator->add('contact.email', 'yadiparfum@gmail.com');
$this->migrator->add('contact.address', 'Jl. Taman Pahlawan No.41, Nagri Kaler, Kec. Purwakarta, Kabupaten Purwakarta, Jawa Barat 41119');
$this->migrator->add('contact.map_embed', '<iframe src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d8867.063325637613!2d107.43739457349945!3d-6.539860399999985!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2e690f57aa3eaf5b%3A0x2bbc334edbb98b17!2sYadi%20Parfum!5e1!3m2!1sen!2sid!4v1760775408540!5m2!1sen!2sid" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>');
}
public function down(): void
{
$this->migrator->deleteIfExists('contact');
}
};

View File

@ -0,0 +1,24 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('seo.meta_description', 'Nikmati pengalaman wangi berkualitas tinggi dengan parfum refill dan produk unggulan dari Yadi Parfum.');
$this->migrator->add('seo.meta_keywords', 'parfum, refill, wangi, aroma, purwakarta, yadi parfum');
$this->migrator->add('seo.meta_author', 'Yadi Parfum');
$this->migrator->add('seo.og_image', '');
$this->migrator->add('seo.og_title', 'Yadi Parfum');
$this->migrator->add('seo.og_description', 'Parfum refill berkualitas premium untuk Anda.');
$this->migrator->add('seo.og_type', 'website');
$this->migrator->add('seo.twitter_card', 'summary_large_image');
$this->migrator->add('seo.canonical_url', 'https://yadiparfum.com');
}
public function down(): void
{
$this->migrator->deleteIfExists('seo');
}
};

View File

@ -0,0 +1,22 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('social.facebook', 'https://web.facebook.com/yadie.aza.188');
$this->migrator->add('social.instagram', null);
$this->migrator->add('social.twitter', null);
$this->migrator->add('social.youtube', null);
$this->migrator->add('social.tiktok', 'https://www.tiktok.com/@yadiparfum_');
$this->migrator->add('social.whatsapp', 'https://wa.me/6283816931711');
$this->migrator->add('social.telegram', null);
}
public function down(): void
{
$this->migrator->deleteIfExists('social');
}
};

View File

@ -0,0 +1,186 @@
<flux:main>
<flux:heading size="xl">Pengaturan Aplikasi</flux:heading>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="xl">Umum</flux:heading>
<flux:subheading>Kelola informasi dasar seperti nama situs, tagline, dan detail utama lainnya yang digunakan
di seluruh sistem.</flux:subheading>
</div>
<div class="flex-1 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>Nama Situs <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Contoh: Yadi Parfum" wire:model.live.debounce.500ms="generalForm.site_name"
autocomplete="off" />
<flux:error name="generalForm.site_name" />
</flux:field>
<flux:field>
<flux:label>Tagline <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Contoh: Premium Refill & Fragrance Store"
wire:model.live.debounce.500ms="generalForm.site_tagline" />
</flux:field>
</div>
<flux:field>
<flux:label>Deskripsi <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea rows="4" placeholder="Tuliskan deskripsi singkat tentang bisnis Anda"
wire:model.live.debounce.500ms="generalForm.site_description" />
</flux:field>
<flux:field>
<flux:label>Mode Pemeliharaan</flux:label>
<flux:switch wire:model="generalForm.maintenance_mode" />
</flux:field>
@can('update general settings')
<flux:button variant="primary" wire:click="updateGeneral">
Simpan
</flux:button>
@endcan
</div>
</div>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="xl">Kontak</flux:heading>
<flux:subheading>
Atur informasi kontak seperti alamat, nomor telepon, email, dan lokasi peta agar pelanggan
mudah menghubungi Anda.
</flux:subheading>
</div>
<div class="flex-1 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>Nomor Telepon <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input placeholder="Contoh: 0821 xxxx xxxx" mask="9999 9999 99999"
wire:model.live.debounce.500ms="contactForm.phone_number" autocomplete="off" />
<flux:error name="contactForm.phone_number" />
</flux:field>
<flux:input label="Email" placeholder="Contoh: yadiparfum@.com"
wire:model.live.debounce.500ms="contactForm.email" autocomplete="off" />
</div>
<flux:field>
<flux:label>Alamat <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea rows="3" wire:model.live.debounce.500ms="contactForm.address"
placeholder="Contoh: Jl. Taman Pahlawan No.41, Nagri Kaler, Kec. Purwakarta, Kabupaten Purwakarta, Jawa Barat 41119" />
<flux:error name="contactForm.address" />
</flux:field>
<flux:textarea label="Embed Map" rows="3" wire:model.live.debounce.500ms="contactForm.map_embed"
placeholder="Tempelkan kode embed Google Maps di sini" />
@can('update contact settings')
<flux:button variant="primary" wire:click="updateContact">
Simpan
</flux:button>
@endcan
</div>
</div>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="xl">Media Sosial</flux:heading>
<flux:subheading>
Hubungkan situs Anda dengan berbagai platform media sosial untuk memperkuat kehadiran dan interaksi
online.
</flux:subheading>
</div>
<div class="flex-1 space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:input label="Facebook" wire:model.live.debounce.500ms="socialForm.facebook"
placeholder="https://facebook.com/yadiparfum" />
<flux:input label="Instagram" wire:model.live.debounce.500ms="socialForm.instagram"
placeholder="https://instagram.com/yadiparfum" />
<flux:input label="YouTube" wire:model.live.debounce.500ms="socialForm.youtube"
placeholder="https://youtube.com/@yadiparfum" />
<flux:input label="TikTok" wire:model.live.debounce.500ms="socialForm.tiktok"
placeholder="https://tiktok.com/@yadiparfum" />
<flux:input label="WhatsApp" wire:model.live.debounce.500ms="socialForm.whatsapp"
placeholder="https://wa.me/6281234567890" />
<flux:input label="Telegram" wire:model.live.debounce.500ms="socialForm.telegram"
placeholder="https://t.me/yadiparfum" />
</div>
@can('update social settings')
<flux:button variant="primary" wire:click="updateSocial">
Simpan
</flux:button>
@endcan
</div>
</div>
<flux:separator variant="subtle" class="my-8" />
<div class="flex flex-col lg:flex-row gap-4 lg:gap-6">
<div class="w-80">
<flux:heading size="xl">SEO</flux:heading>
<flux:subheading>
Optimalkan visibilitas situs di mesin pencari dan media sosial dengan mengatur meta tag, Open Graph, dan
informasi berbagi.
</flux:subheading>
</div>
<div class="flex-1 space-y-8">
<div class="space-y-6">
<flux:field>
<flux:label>Meta Description <span class="text-red-500 ms-1">*</span></flux:label>
<flux:textarea rows="3" wire:model.live.debounce.500ms="seoForm.meta_description"
placeholder="Tulis deskripsi singkat situs Anda yang akan muncul di hasil pencarian Google." />
<flux:error name="seoForm.meta_description" />
</flux:field>
<flux:field>
<flux:label>Meta Keywords <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input wire:model.live.debounce.500ms="seoForm.meta_keywords"
placeholder="parfum refill, parfum original, toko parfum Purwakarta" />
<flux:error name="seoForm.meta_keywords" />
</flux:field>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<flux:field>
<flux:label>Meta Author <span class="text-red-500 ms-1">*</span></flux:label>
<flux:input wire:model.live.debounce.500ms="seoForm.meta_author"
placeholder="Yadi Parfum Official" />
<flux:error name="seoForm.meta_author" />
</flux:field>
<flux:input label="OG Title" wire:model.live.debounce.500ms="seoForm.og_title"
placeholder="Yadi Parfum Wangi yang Mewakili Gaya Anda" />
</div>
<flux:textarea label="OG Description" rows="3"
wire:model.live.debounce.500ms="seoForm.og_description"
placeholder="Temukan koleksi parfum terbaik dari Yadi Parfum untuk setiap momen istimewa." />
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<flux:input label="OG Type" wire:model.live.debounce.500ms="seoForm.og_type"
placeholder="website, article, product, dll." />
<flux:input label="Twitter Card" wire:model.live.debounce.500ms="seoForm.twitter_card"
placeholder="summary_large_image" />
<flux:input label="Canonical URL" wire:model.live.debounce.500ms="seoForm.canonical_url"
placeholder="https://yadiparfum.com" />
</div>
</div>
@can('update seo settings')
<flux:button variant="primary" wire:click="updateSeo">
Simpan
</flux:button>
@endcan
</div>
</div>
</flux:main>

View File

@ -32,6 +32,7 @@
use App\Livewire\Studio\Master\User\Edit as UserEdit;
use App\Livewire\Studio\Master\User\Index as UserIndex;
use App\Livewire\Studio\Setting\Account as AccountComponent;
use App\Livewire\Studio\Setting\Application as ApplicationComponent;
use App\Livewire\Studio\Setting\Password as PasswordComponent;
use App\Livewire\Studio\Setting\Profile as ProfileComponent;
use Illuminate\Support\Facades\Route;
@ -148,4 +149,6 @@
Route::delete('/{purchase}/delete', PurchaseCreate::class)->name('delete')->middleware('can:delete purchase');
});
});
Route::get('/settings/applications', ApplicationComponent::class)->name('studio.setting.application')->middleware('can:view application settings');
});