Enhance DatabaseSeeder and Home view functionality
- Updated DatabaseSeeder to check for existing admin user before creation. - Modified Home view to include max-height for collection items and added filter functionality for product categories. - Introduced JavaScript for dynamic filtering of product cards based on selected categories.
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\Categories\Schemas;
|
namespace App\Filament\Resources\Categories\Schemas;
|
||||||
|
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,14 @@ class DatabaseSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
// User::factory(10)->create();
|
||||||
|
|
||||||
User::factory()->create([
|
if (User::where('email', 'admin@example.com')->doesntExist()) {
|
||||||
'name' => 'Admin',
|
User::factory()->create([
|
||||||
'email' => 'admin@example.com',
|
'name' => 'Admin',
|
||||||
'password' => Hash::make('password'),
|
'email' => 'admin@example.com',
|
||||||
]);
|
'password' => Hash::make('password'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->call(DemoSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
database/seeders/DemoSeeder.php
Normal file
128
database/seeders/DemoSeeder.php
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\CarouselSlide;
|
||||||
|
use App\Models\Category;
|
||||||
|
use App\Models\Collection;
|
||||||
|
use App\Models\CollectionItem;
|
||||||
|
use App\Models\Product;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class DemoSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Ensure storage directories exist
|
||||||
|
Storage::disk('public')->makeDirectory('slides');
|
||||||
|
Storage::disk('public')->makeDirectory('products');
|
||||||
|
Storage::disk('public')->makeDirectory('collections');
|
||||||
|
|
||||||
|
$this->command->info('Seeding Carousel Slides...');
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$imagePath = 'slides/slide-'.$i.'.jpg';
|
||||||
|
$this->generateDummyImage($imagePath, 1200, 600, 'Slide '.$i);
|
||||||
|
|
||||||
|
CarouselSlide::create([
|
||||||
|
'title' => 'Täze harytlar '.$i,
|
||||||
|
'subtitle' => 'Iň oňat bahalar bilen öýüňizi bezäň',
|
||||||
|
'order' => $i,
|
||||||
|
'is_active' => true,
|
||||||
|
'image' => $imagePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('Seeding Categories & Products...');
|
||||||
|
$categories = ['Diwanlar', 'Krowatlar', 'Stollar', 'Oturgyçlar', 'Şkaflar'];
|
||||||
|
|
||||||
|
foreach ($categories as $index => $categoryName) {
|
||||||
|
$category = Category::create([
|
||||||
|
'name' => $categoryName,
|
||||||
|
'icon' => 'heroicon-o-archive-box', // dummy icon
|
||||||
|
'order' => $index + 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for ($p = 1; $p <= 4; $p++) {
|
||||||
|
$imagePath = 'products/product-'.$category->id.'-'.$p.'.jpg';
|
||||||
|
$this->generateDummyImage($imagePath, 800, 800, $categoryName.' '.$p);
|
||||||
|
|
||||||
|
Product::create([
|
||||||
|
'category_id' => $category->id,
|
||||||
|
'name' => 'Owadan '.Str::lower($categoryName).' modeli '.$p,
|
||||||
|
'description' => 'Bu '.Str::lower($categoryName).' örän ýokary hilli we berk materialdan öndürilen. Öýüňize bezeg gatar.',
|
||||||
|
'price' => rand(1000, 15000),
|
||||||
|
'is_active' => true,
|
||||||
|
'image' => $imagePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('Seeding Collections & Items...');
|
||||||
|
$collections = ['Tomus Kolleksiýasy', 'Täze Ýyl', 'Otag Bezegi'];
|
||||||
|
|
||||||
|
foreach ($collections as $index => $collectionName) {
|
||||||
|
$collection = Collection::create([
|
||||||
|
'title' => $collectionName,
|
||||||
|
'subtitle' => 'Aýratyn dizaýnlar',
|
||||||
|
'order' => $index + 1,
|
||||||
|
'is_active' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$imagePath = 'collections/item-'.$collection->id.'-'.$i.'.jpg';
|
||||||
|
$this->generateDummyImage($imagePath, 600, 800, 'Item '.$i);
|
||||||
|
|
||||||
|
CollectionItem::create([
|
||||||
|
'collection_id' => $collection->id,
|
||||||
|
'title' => $collectionName.' elementi '.$i,
|
||||||
|
'subtitle' => 'Gözel dizaýn '.$i,
|
||||||
|
'order' => $i,
|
||||||
|
'is_active' => true,
|
||||||
|
'image' => $imagePath,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateDummyImage(string $path, int $width, int $height, string $text): void
|
||||||
|
{
|
||||||
|
// Try to download a random image from picsum, if it fails, generate a basic image using GD
|
||||||
|
try {
|
||||||
|
// Using UI Avatars for reliable and fast text-based placeholders
|
||||||
|
$url = 'https://ui-avatars.com/api/?name='.urlencode($text).'&size=512&background=random&color=fff&font-size=0.33';
|
||||||
|
$contents = file_get_contents($url);
|
||||||
|
if ($contents) {
|
||||||
|
Storage::disk('public')->put($path, $contents);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// GD fallback if network fails
|
||||||
|
if (extension_loaded('gd')) {
|
||||||
|
$image = imagecreatetruecolor($width, $height);
|
||||||
|
$bg = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
|
||||||
|
$textColor = imagecolorallocate($image, 255, 255, 255);
|
||||||
|
imagefill($image, 0, 0, $bg);
|
||||||
|
imagestring($image, 5, $width / 2 - strlen($text) * 4, $height / 2 - 8, $text, $textColor);
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
imagejpeg($image);
|
||||||
|
$contents = ob_get_clean();
|
||||||
|
imagedestroy($image);
|
||||||
|
|
||||||
|
Storage::disk('public')->put($path, $contents);
|
||||||
|
} else {
|
||||||
|
// If GD is not available and download failed, just put an empty file or dummy svg
|
||||||
|
$svg = '<svg xmlns="http://www.w3.org/2000/svg" width="'.$width.'" height="'.$height.'"><rect width="100%" height="100%" fill="#ccc"/><text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="sans-serif" font-size="20px" fill="#333">'.$text.'</text></svg>';
|
||||||
|
Storage::disk('public')->put($path, $svg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex overflow-x-auto hide-scrollbar gap-6 pb-8 snap-x snap-mandatory" id="carousel-{{ $collection->id }}">
|
<div class="flex overflow-x-auto hide-scrollbar gap-6 pb-8 snap-x snap-mandatory" id="carousel-{{ $collection->id }}">
|
||||||
@foreach($collection->collectionItems as $item)
|
@foreach($collection->collectionItems as $item)
|
||||||
<div class="min-w-[280px] md:min-w-[320px] aspect-[3/4] relative snap-start group cursor-pointer border border-antique-gold/40 bg-surface-container-low p-4 flex flex-col justify-end overflow-hidden">
|
<div style="max-height: 420px;" class="min-w-[280px] md:min-w-[320px] aspect-[3/4] relative snap-start group cursor-pointer border border-antique-gold/40 bg-surface-container-low p-4 flex flex-col justify-end overflow-hidden">
|
||||||
<div class="absolute inset-4 border border-outline/30 z-10 pointer-events-none transition-transform duration-500 group-hover:scale-95"></div>
|
<div class="absolute inset-4 border border-outline/30 z-10 pointer-events-none transition-transform duration-500 group-hover:scale-95"></div>
|
||||||
<div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105 opacity-80" data-alt="{{ $item->subtitle }}" style="background-image: url('{{ $item->image ? Storage::url($item->image) : asset('assets/images/collection-chocolate-cake.jpg') }}');"></div>
|
<div class="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-105 opacity-80" data-alt="{{ $item->subtitle }}" style="background-image: url('{{ $item->image ? Storage::url($item->image) : asset('assets/images/collection-chocolate-cake.jpg') }}');"></div>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-primary-container/90 via-primary-container/30 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-primary-container/90 via-primary-container/30 to-transparent"></div>
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
|
|
||||||
<section class="py-16 px-margin-mobile md:px-margin-desktop max-w-container-max mx-auto bg-surface-container-low border-y border-outline/10" id="menu">
|
<section class="py-16 px-margin-mobile md:px-margin-desktop max-w-container-max mx-auto bg-surface-container-low border-y border-outline/10" id="menu">
|
||||||
<div class="flex overflow-x-auto hide-scrollbar gap-4 pb-8 mb-8 justify-center animate-on-scroll">
|
<div class="flex overflow-x-auto hide-scrollbar gap-4 pb-8 mb-8 justify-center animate-on-scroll">
|
||||||
<button class="px-6 py-2 rounded-full bg-ink-brown text-surface-container-lowest font-label-sm text-label-sm uppercase tracking-wide flex items-center gap-2 whitespace-nowrap">
|
<button class="filter-btn px-6 py-2 rounded-full bg-ink-brown text-surface-container-lowest font-label-sm text-label-sm uppercase tracking-wide flex items-center gap-2 whitespace-nowrap" data-category="all">
|
||||||
<span class="material-symbols-outlined text-[16px]">cake</span> Hemmesi
|
<span class="material-symbols-outlined text-[16px]">cake</span> Hemmesi
|
||||||
</button>
|
</button>
|
||||||
@foreach($categories as $category)
|
@foreach($categories as $category)
|
||||||
<button class="px-6 py-2 rounded-full border border-outline/30 text-on-surface-variant hover:border-antique-gold hover:text-ink-brown transition-colors font-label-sm text-label-sm uppercase tracking-wide flex items-center gap-2 whitespace-nowrap bg-surface">
|
<button class="filter-btn px-6 py-2 rounded-full border border-outline/30 text-on-surface-variant hover:border-antique-gold hover:text-ink-brown transition-colors font-label-sm text-label-sm uppercase tracking-wide flex items-center gap-2 whitespace-nowrap bg-surface" data-category="{{ $category->id }}">
|
||||||
@if($category->icon)
|
@if($category->icon)
|
||||||
<span class="material-symbols-outlined text-[16px]">{{ $category->icon }}</span>
|
<span class="material-symbols-outlined text-[16px]">{{ $category->icon }}</span>
|
||||||
@endif
|
@endif
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-12">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-x-8 gap-y-12">
|
||||||
@foreach($products as $index => $product)
|
@foreach($products as $index => $product)
|
||||||
<div class="group animate-on-scroll" style="transition-delay: {{ ($index % 4) * 100 }}ms;">
|
<div class="product-card group animate-on-scroll" data-category="{{ $product->category_id }}" style="transition-delay: {{ ($index % 4) * 100 }}ms;">
|
||||||
<div class="aspect-square mb-4 overflow-hidden relative bg-surface-variant">
|
<div class="aspect-square mb-4 overflow-hidden relative bg-surface-variant">
|
||||||
<img alt="{{ $product->name }}" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" data-alt="{{ $product->name }}" src="{{ $product->image ? Storage::url($product->image) : asset('assets/images/product-macaron.jpg') }}"/>
|
<img alt="{{ $product->name }}" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" data-alt="{{ $product->name }}" src="{{ $product->image ? Storage::url($product->image) : asset('assets/images/product-macaron.jpg') }}"/>
|
||||||
<div class="absolute inset-0 border border-outline/10 pointer-events-none m-2"></div>
|
<div class="absolute inset-0 border border-outline/10 pointer-events-none m-2"></div>
|
||||||
@@ -128,4 +128,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const filterBtns = document.querySelectorAll('.filter-btn');
|
||||||
|
const productCards = document.querySelectorAll('.product-card');
|
||||||
|
|
||||||
|
const activeClasses = ['bg-ink-brown', 'text-surface-container-lowest'];
|
||||||
|
const inactiveClasses = ['border', 'border-outline/30', 'text-on-surface-variant', 'bg-surface'];
|
||||||
|
|
||||||
|
filterBtns.forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const category = btn.getAttribute('data-category');
|
||||||
|
|
||||||
|
// 1. Update button styles
|
||||||
|
filterBtns.forEach(b => {
|
||||||
|
b.classList.remove(...activeClasses);
|
||||||
|
b.classList.add(...inactiveClasses);
|
||||||
|
});
|
||||||
|
btn.classList.remove(...inactiveClasses);
|
||||||
|
btn.classList.add(...activeClasses);
|
||||||
|
|
||||||
|
// 2. Filter products
|
||||||
|
productCards.forEach(card => {
|
||||||
|
if (category === 'all' || card.getAttribute('data-category') === category) {
|
||||||
|
card.style.display = 'block';
|
||||||
|
// Small timeout to allow display:block to apply before re-animating
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.add('is-visible');
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
card.classList.remove('is-visible');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</x-layouts.app>
|
</x-layouts.app>
|
||||||
|
|||||||
Reference in New Issue
Block a user