feat: integrate homepage settings management in Admin and HomeController, enhancing homepage configuration capabilities

This commit is contained in:
Yoga Pangestu 2026-06-20 12:04:58 +07:00
parent 67ee140ba7
commit 6273d1a31d
15 changed files with 745 additions and 350 deletions

View File

@ -4,10 +4,12 @@
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\System\Setting\HomepageSettingRequest;
use App\Http\Requests\Admin\System\Setting\HrSettingRequest;
use App\Http\Requests\Admin\System\Setting\MarketplaceRequest;
use App\Http\Requests\Admin\System\Setting\SocialMediaRequest;
use App\Http\Requests\Admin\System\Setting\SystemRequest;
use App\Services\System\Setting\HomepageSettingService;
use App\Services\System\Setting\HrSettingService;
use App\Services\System\Setting\MarketplaceService;
use App\Services\System\Setting\SocialMediaService;
@ -25,6 +27,7 @@ public function __construct(
private readonly SocialMediaService $socialMediaService,
private readonly MarketplaceService $marketplaceService,
private readonly HrSettingService $hrSettingService,
private readonly HomepageSettingService $homepageSettingService,
) {}
public function index(): Response
@ -34,6 +37,7 @@ public function index(): Response
'socialMedia' => $this->socialMediaService->socialMediaData(),
'marketplace' => $this->marketplaceService->marketplaceData(),
'hr' => $this->hrSettingService->hrData(),
'homepage' => $this->homepageSettingService->homepageData(),
]);
}
@ -72,4 +76,13 @@ public function updateHr(HrSettingRequest $request): RedirectResponse
return redirect()->route('admin.system.setting.index');
}
public function updateHomepage(HomepageSettingRequest $request): RedirectResponse
{
$this->homepageSettingService->updateHomepage($request->validated());
$this->flashSuccess('Pengaturan homepage berhasil disimpan.');
return redirect()->route('admin.system.setting.index');
}
}

View File

@ -7,6 +7,7 @@
use App\Models\Product;
use App\Models\SystemConfiguration;
use App\Services\Manage\CuttingResultPriceResolver;
use App\Services\System\Setting\HomepageSettingService;
use App\Settings\SocialMediaSettings;
use App\Settings\SystemSettings;
use App\Support\Media\MediaPresenter;
@ -17,6 +18,7 @@ class HomeController extends Controller
{
public function __construct(
private readonly CuttingResultPriceResolver $cuttingResultPriceResolver,
private readonly HomepageSettingService $homepageSettingService,
) {}
public function index(): Response
@ -60,10 +62,12 @@ public function index(): Response
$settings = app(SystemSettings::class);
$socialSettings = app(SocialMediaSettings::class);
$appName = $settings->app_name ?? 'DST Collection';
return Inertia::render('Home', [
'categories' => $categories,
'products' => $products,
'appName' => $settings->app_name ?? 'DST Collection',
'appName' => $appName,
'aboutApp' => $settings->about_app ?? '',
'contactEmail' => $settings->email ?? '',
'contactPhone' => $settings->phone ?? '',
@ -72,6 +76,7 @@ public function index(): Response
'instagramUrl' => $socialSettings->instagram_url ?? null,
'facebookUrl' => $socialSettings->facebook_url ?? null,
'tiktokUrl' => $socialSettings->tiktok_url ?? null,
'homepage' => $this->homepageSettingService->homepageData(),
]);
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Http\Requests\Admin\System\Setting;
use App\Enums\Permission;
use Illuminate\Foundation\Http\FormRequest;
class HomepageSettingRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can(Permission::SETTINGS_UPDATE->value) ?? false;
}
/**
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'hero_image' => ['nullable', 'image', 'max:5120'],
'about_image' => ['nullable', 'image', 'max:5120'],
'gallery_images' => ['nullable', 'array', 'max:10'],
'gallery_images.*' => ['image', 'max:5120'],
'gallery_images_remove' => ['nullable', 'array'],
'gallery_images_remove.*' => ['integer'],
];
}
/**
* @return array<string, string>
*/
public function attributes(): array
{
return [
'hero_image' => 'Foto Hero',
'about_image' => 'Foto Tentang Kami',
'gallery_images' => 'Foto Lookbook',
'gallery_images.*' => 'Foto Lookbook',
];
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace App\Models;
use App\Models\Concerns\HasModuleMedia;
use App\Models\Concerns\InteractsWithActivityLog;
use Illuminate\Database\Eloquent\Attributes\Guarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
#[Guarded(['id'])]
class HomepageConfiguration extends Model implements HasMedia
{
use HasFactory;
use HasModuleMedia;
use InteractsWithActivityLog;
public static function mediaModuleName(): string
{
return 'homepage';
}
public static function instance(): self
{
return static::query()->firstOrCreate(['id' => 1]);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('hero_image')->singleFile();
$this->addMediaCollection('about_image')->singleFile();
$this->addMediaCollection('gallery')->singleFile();
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace App\Services\System\Setting;
use App\Models\HomepageConfiguration;
use App\Services\Media\MediaService;
use App\Support\Media\MediaPresenter;
use Illuminate\Http\UploadedFile;
class HomepageSettingService
{
public function __construct(
private readonly MediaService $mediaService,
) {}
public function homepageData(): array
{
$configuration = HomepageConfiguration::instance();
$configuration->load('media');
$heroImage = MediaPresenter::first($configuration, 'hero_image');
$aboutImage = MediaPresenter::first($configuration, 'about_image');
$galleryImages = MediaPresenter::collection($configuration, 'gallery');
return [
'hero_image_url' => $heroImage['url'] ?? null,
'about_image_url' => $aboutImage['url'] ?? null,
'gallery_images' => $galleryImages,
];
}
public function updateHomepage(array $validated): void
{
$configuration = HomepageConfiguration::instance();
if (isset($validated['hero_image']) && $validated['hero_image'] instanceof UploadedFile) {
$this->mediaService->replaceSingleFile($configuration, $validated['hero_image'], 'hero_image', 'hero-image');
}
if (isset($validated['about_image']) && $validated['about_image'] instanceof UploadedFile) {
$this->mediaService->replaceSingleFile($configuration, $validated['about_image'], 'about_image', 'about-image');
}
if (isset($validated['gallery_images'])) {
$this->mediaService->syncCollection(
$configuration,
'gallery',
$validated['gallery_images'] ?? [],
$validated['gallery_images_remove'] ?? [],
10,
'gallery',
);
}
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Settings;
use Spatie\LaravelSettings\Settings;
class HomepageSettings extends Settings
{
// Hero Section
public string $hero_badge;
public string $hero_title_line1;
public string $hero_title_line2;
public string $hero_title_highlight;
public string $hero_description;
public string $hero_cta_primary_text;
public string $hero_cta_secondary_text;
public ?string $hero_image_url;
public string $hero_bg_text_left;
public string $hero_bg_text_right;
// Scroll Indicator
public string $scroll_hashtag;
public string $scroll_tagline;
// Catalog Section
public string $catalog_badge;
public string $catalog_title;
public string $catalog_description;
public string $catalog_search_placeholder;
// Gallery Section
public string $gallery_badge;
public string $gallery_title;
public string $gallery_description;
public array $gallery_images;
// Order Guide Section
public string $order_guide_badge;
public string $order_guide_title;
public string $order_guide_description;
public array $order_steps;
// About Section
public string $about_badge;
public string $about_title;
public ?string $about_image_url;
public array $about_features;
// Contact Section
public string $contact_badge;
public string $contact_title;
public string $contact_description;
public string $contact_form_title;
// Footer
public string $footer_description;
public string $footer_copyright;
public static function group(): string
{
return 'homepage';
}
}

View File

@ -0,0 +1,21 @@
<?php
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('homepage_configurations', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('homepage_configurations');
}
};

View File

@ -0,0 +1,89 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
// Hero Section
$blueprint->add('hero_badge', 'Koleksi Segar 2026');
$blueprint->add('hero_title_line1', 'Ekspresikan');
$blueprint->add('hero_title_line2', 'Gaya');
$blueprint->add('hero_title_highlight', 'Segar Anda');
$blueprint->add('hero_description', 'Selamat datang di {app_name}. Temukan keanggunan motif batik modern dan setelan pakaian santai berkualitas premium. Dirancang khusus dengan bahan adem yang menyejukkan aktivitas harian Anda.');
$blueprint->add('hero_cta_primary_text', 'Beli Sekarang');
$blueprint->add('hero_cta_secondary_text', 'Tentang Kami');
$blueprint->add('hero_image_url', 'https://images.unsplash.com/photo-1490481651871-ab68de25d43d?w=1000&auto=format&fit=crop&q=80');
$blueprint->add('hero_bg_text_left', 'DST');
$blueprint->add('hero_bg_text_right', 'Collection');
// Scroll Indicator
$blueprint->add('scroll_hashtag', '#DSTCollection');
$blueprint->add('scroll_tagline', 'Bahan Adem & Lembut');
// Catalog Section
$blueprint->add('catalog_badge', 'Katalog Eksklusif');
$blueprint->add('catalog_title', 'Koleksi Busana Pilihan');
$blueprint->add('catalog_description', 'Gunakan kategori dan filter di bawah untuk menyesuaikan pencarian busana idaman Anda dengan mudah.');
$blueprint->add('catalog_search_placeholder', 'Cari nama pakaian...');
// Deal Section
$blueprint->add('deal_badge', 'Promo Terbatas');
$blueprint->add('deal_title', 'Penawaran Bulan Ini');
$blueprint->add('deal_description', 'Jangan lewatkan promosi batik dan daster eksklusif kami! Dapatkan potongan harga spesial up to 25% untuk varian daster pilihan dan setelan pakaian modern. Dapatkan kenyamanan ekstra dengan bahan menyejukkan sebelum masa promo habis!');
$blueprint->add('deal_cta_text', 'Jelajahi Produk Diskon');
$blueprint->add('deal_images', [
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
]);
// Order Guide Section
$blueprint->add('order_guide_badge', 'Langkah Pemesanan');
$blueprint->add('order_guide_title', 'Cara Melakukan Pemesanan');
$blueprint->add('order_guide_description', 'Sistem pemesanan kami sangat mudah dan terhubung langsung via WhatsApp untuk pelayanan cepat dan personal.');
$blueprint->add('order_steps', [
[
'title' => 'Pilih Produk',
'description' => 'Jelajahi pakaian batik dan daster favorit Anda, lalu ketuk "Lihat Detail" untuk memeriksa ukuran.',
],
[
'title' => 'Pilih Varian & Harga',
'description' => 'Tentukan varian ukuran yang diinginkan dan pilih jenis harga (Eceran, Grosir, Agen, dll.).',
],
[
'title' => 'Masukkan Keranjang',
'description' => 'Masukkan ke Keranjang Belanja untuk menampung seluruh daftar pakaian yang ingin Anda beli.',
],
[
'title' => 'Kirim ke WhatsApp',
'description' => 'Klik tombol kirim pesanan, admin kami akan merespons rincian transfer bank dan pengiriman kurir.',
],
]);
// About Section
$blueprint->add('about_badge', 'Tentang Kami');
$blueprint->add('about_title', 'DST Collection');
$blueprint->add('about_image_url', 'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80');
$blueprint->add('about_features', [
'Kain Rayon Super Tebal & Menyerap Keringat',
'Motif Eksklusif & Tidak Pasaran',
'Dukungan Penuh Layanan Admin Via WhatsApp',
]);
// Contact Section
$blueprint->add('contact_badge', 'Kontak Kami');
$blueprint->add('contact_title', 'Ada Pertanyaan? Hubungi Kami');
$blueprint->add('contact_description', 'Kami sangat senang mendengarkan pertanyaan Anda terkait spesifikasi produk, ketersediaan grosir, atau kemitraan. Hubungi tim admin kami melalui media di bawah.');
$blueprint->add('contact_form_title', 'Kirim Pesan Cepat');
// Footer
$blueprint->add('footer_description', 'Galeri resmi {app_name}. Pilihan busana lokal premium berpotongan modern dengan kenyamanan menyejukkan.');
$blueprint->add('footer_copyright', '&copy; 2026 {app_name} DST Collection. Hak Cipta Dilindungi.');
});
}
};

View File

@ -0,0 +1,32 @@
<?php
use Spatie\LaravelSettings\Migrations\SettingsBlueprint;
use Spatie\LaravelSettings\Migrations\SettingsMigration;
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->inGroup('homepage', function (SettingsBlueprint $blueprint): void {
// Delete old deal fields
$blueprint->delete('deal_badge');
$blueprint->delete('deal_title');
$blueprint->delete('deal_description');
$blueprint->delete('deal_cta_text');
$blueprint->delete('deal_images');
// Add new gallery fields
$blueprint->add('gallery_badge', 'Galeri Kami');
$blueprint->add('gallery_title', 'Koleksi Lookbook');
$blueprint->add('gallery_description', 'Intip koleksi lookbook kami untuk inspirasi gaya sehari-hari. Padu padan batik modern dan daster yang nyaman untuk berbagai suasana.');
$blueprint->add('gallery_images', [
'https://images.unsplash.com/photo-1595777457583-95e059d581b8?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1572804013309-59a88b7e92f1?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1515886657613-9f3515b0c78f?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1539109136881-3be0616acf4b?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1496747611176-843222e1e57c?w=800&auto=format&fit=crop&q=80',
'https://images.unsplash.com/photo-1509631179647-0177331693ae?w=800&auto=format&fit=crop&q=80',
]);
});
}
};

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import { router } from '@inertiajs/vue3';
import { Save } from '@lucide/vue';
import { ref } from 'vue';
import { toast } from 'vue-sonner';
import ImageUploadField from '@/components/form/image-upload-field/ImageUploadField.vue';
import MultipleImageUploadField from '@/components/form/image-upload-field/MultipleImageUploadField.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { createMediaUploadState } from '@/types/media';
import type { HomepageSettingsData } from '@/types/setting';
import type { MediaUploadState } from '@/types/media';
const props = defineProps<{
data: HomepageSettingsData;
}>();
const heroImage = ref<File | null>(null);
const aboutImage = ref<File | null>(null);
const galleryState = ref<MediaUploadState>(
createMediaUploadState(props.data.gallery_images),
);
const isSubmitting = ref(false);
function submit() {
isSubmitting.value = true;
const formData = new FormData();
if (heroImage.value) {
formData.append('hero_image', heroImage.value);
}
if (aboutImage.value) {
formData.append('about_image', aboutImage.value);
}
galleryState.value.newFiles.forEach((file) => {
formData.append('gallery_images[]', file);
});
galleryState.value.removeIds.forEach((id) => {
formData.append('gallery_images_remove[]', String(id));
});
router.put('/admin/system/setting/homepage', formData, {
preserveScroll: true,
onSuccess: () => {
toast.success('Pengaturan homepage berhasil disimpan.');
heroImage.value = null;
aboutImage.value = null;
},
onError: () => {
toast.error('Gagal menyimpan pengaturan homepage.');
},
onFinish: () => {
isSubmitting.value = false;
},
});
}
</script>
<template>
<form @submit.prevent="submit">
<div class="grid gap-4">
<!-- Hero Image -->
<Card>
<CardContent>
<ImageUploadField
id="hero_image"
label="Foto Hero"
description="Foto utama yang ditampilkan di bagian hero homepage."
:current-url="props.data.hero_image_url"
v-model="heroImage"
preview-class="aspect-[3/4] max-w-xs"
/>
</CardContent>
</Card>
<!-- Gallery / Lookbook -->
<Card>
<CardContent>
<MultipleImageUploadField
id="gallery_images"
label="Koleksi Lookbook"
description="Foto-foto yang ditampilkan di galeri lookbook."
:max-files="10"
v-model="galleryState"
/>
</CardContent>
</Card>
<!-- About Image -->
<Card>
<CardContent>
<ImageUploadField
id="about_image"
label="Foto Tentang Kami"
description="Foto yang ditampilkan di bagian tentang kami."
:current-url="props.data.about_image_url"
v-model="aboutImage"
preview-class="aspect-video max-w-md"
/>
</CardContent>
</Card>
<div class="flex justify-end">
<Button type="submit" :disabled="isSubmitting">
<Save class="size-4" />
{{ isSubmitting ? 'Menyimpan...' : 'Simpan' }}
</Button>
</div>
</div>
</form>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
import { Home, Settings2, Share2, ShoppingBag, Users } from '@lucide/vue';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { cn } from '@/lib/utils';
import type { SettingSection } from '@/types/setting';
@ -12,6 +12,7 @@ const navItems: Array<{
icon: typeof Settings2;
}> = [
{ key: 'system', label: 'Sistem', icon: Settings2 },
{ key: 'homepage', label: 'Homepage', icon: Home },
{ key: 'social', label: 'Media Sosial', icon: Share2 },
{ key: 'marketplace', label: 'Marketplace', icon: ShoppingBag },
{ key: 'hr', label: 'HR / Pegawai', icon: Users },

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,14 @@
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import { ref } from 'vue';
import HomepageSection from '@/components/admin/setting/HomepageSection.vue';
import HrSection from '@/components/admin/setting/HrSection.vue';
import MarketplaceSection from '@/components/admin/setting/MarketplaceSection.vue';
import SocialMediaSection from '@/components/admin/setting/SocialMediaSection.vue';
import SystemSection from '@/components/admin/setting/SystemSection.vue';
import SettingLayout from '@/layouts/SettingLayout.vue';
import type {
HomepageSettingsData,
HrSettingsData,
MarketplaceSettingsData,
SettingSection,
@ -19,6 +21,7 @@ defineProps<{
socialMedia: SocialMediaSettingsData;
marketplace: MarketplaceSettingsData;
hr: HrSettingsData;
homepage: HomepageSettingsData;
}>();
const activeSection = ref<SettingSection>('system');
@ -30,6 +33,7 @@ const activeSection = ref<SettingSection>('system');
<SettingLayout v-model:section="activeSection">
<SystemSection v-if="activeSection === 'system'" :data="system" />
<HomepageSection v-else-if="activeSection === 'homepage'" :data="homepage" />
<SocialMediaSection v-else-if="activeSection === 'social'" :data="socialMedia" />
<MarketplaceSection v-else-if="activeSection === 'marketplace'" :data="marketplace" />
<HrSection v-else-if="activeSection === 'hr'" :data="hr" />

View File

@ -1,4 +1,4 @@
export type SettingSection = 'system' | 'social' | 'marketplace' | 'hr';
export type SettingSection = 'system' | 'social' | 'marketplace' | 'hr' | 'homepage';
export type MarketplacePlatform = 'tiktok_shop' | 'shopee';
@ -53,3 +53,15 @@ export type HrSettingsData = {
late_penalty_amount: number;
absent_penalty_amount: number;
};
export type MediaItem = {
id: number;
url: string;
thumb_url: string;
};
export type HomepageSettingsData = {
hero_image_url: string | null;
about_image_url: string | null;
gallery_images: MediaItem[];
};

View File

@ -333,6 +333,10 @@
Route::put('hr', [SettingController::class, 'updateHr'])
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
->name('hr.update');
Route::put('homepage', [SettingController::class, 'updateHomepage'])
->middleware('permission:'.Permission::SETTINGS_UPDATE->value)
->name('homepage.update');
});
});