From 93ce5a0925ac0d7ea1b56638acb50c606314c9d8 Mon Sep 17 00:00:00 2001 From: fatihalp Date: Sat, 7 Mar 2026 17:07:32 +0300 Subject: [PATCH] Add demo mode seeders and UI tweaks --- .../Filament/Resources/ListingResource.php | 2 + .../Admin/Providers/AdminPanelProvider.php | 3 + .../seeders/ConversationDemoSeeder.php | 118 ++++++ .../database/seeders/FavoriteDemoSeeder.php | 173 ++++++++ .../Seeders/ListingPanelDemoSeeder.php | 175 ++++++++ .../Database/Seeders/ListingSeeder.php | 10 +- .../Http/Controllers/ListingController.php | 3 + Modules/Listing/Models/Listing.php | 8 +- .../views/themes/default/show.blade.php | 13 + .../views/themes/otoplus/show.blade.php | 14 + .../Filament/Resources/ListingResource.php | 2 + .../Providers/PartnerPanelProvider.php | 3 + Modules/User/App/Models/User.php | 1 + Modules/Video/Enums/VideoStatus.php | 38 ++ .../Admin/Resources/VideoResource.php | 43 ++ .../VideoResource/Pages/CreateVideo.php | 11 + .../VideoResource/Pages/EditVideo.php | 19 + .../VideoResource/Pages/ListVideos.php | 19 + .../Partner/Resources/VideoResource.php | 48 +++ .../VideoResource/Pages/CreateVideo.php | 11 + .../VideoResource/Pages/EditVideo.php | 19 + .../VideoResource/Pages/ListVideos.php | 19 + Modules/Video/Jobs/ProcessVideo.php | 47 +++ Modules/Video/Models/Video.php | 397 ++++++++++++++++++ .../Video/Providers/VideoServiceProvider.php | 19 + .../Support/Filament/VideoFormSchema.php | 206 +++++++++ .../Support/Filament/VideoTableSchema.php | 99 +++++ Modules/Video/Support/VideoTranscoder.php | 97 +++++ Modules/Video/config/video.php | 24 ++ .../2026_03_07_000000_create_video_tables.php | 41 ++ Modules/Video/module.json | 12 + .../views/filament/video-player.blade.php | 62 +++ .../filament/video-preview-field.blade.php | 50 +++ .../partials/video-upload-optimizer.blade.php | 195 +++++++++ app/Http/Controllers/PanelController.php | 9 + app/Livewire/PanelQuickListingForm.php | 61 ++- bootstrap/providers.php | 1 + database/seeders/DatabaseSeeder.php | 3 + modules_statuses.json | 5 +- resources/css/app.css | 140 ++++-- resources/views/layouts/app.blade.php | 18 +- resources/views/panel/listings.blade.php | 16 + .../views/panel/partials/sidebar.blade.php | 5 + .../partials/quick-create/form.blade.php | 70 +++ routes/web.php | 3 - 45 files changed, 2271 insertions(+), 61 deletions(-) create mode 100644 Modules/Conversation/database/seeders/ConversationDemoSeeder.php create mode 100644 Modules/Favorite/database/seeders/FavoriteDemoSeeder.php create mode 100644 Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php create mode 100644 Modules/Video/Enums/VideoStatus.php create mode 100644 Modules/Video/Filament/Admin/Resources/VideoResource.php create mode 100644 Modules/Video/Filament/Admin/Resources/VideoResource/Pages/CreateVideo.php create mode 100644 Modules/Video/Filament/Admin/Resources/VideoResource/Pages/EditVideo.php create mode 100644 Modules/Video/Filament/Admin/Resources/VideoResource/Pages/ListVideos.php create mode 100644 Modules/Video/Filament/Partner/Resources/VideoResource.php create mode 100644 Modules/Video/Filament/Partner/Resources/VideoResource/Pages/CreateVideo.php create mode 100644 Modules/Video/Filament/Partner/Resources/VideoResource/Pages/EditVideo.php create mode 100644 Modules/Video/Filament/Partner/Resources/VideoResource/Pages/ListVideos.php create mode 100644 Modules/Video/Jobs/ProcessVideo.php create mode 100644 Modules/Video/Models/Video.php create mode 100644 Modules/Video/Providers/VideoServiceProvider.php create mode 100644 Modules/Video/Support/Filament/VideoFormSchema.php create mode 100644 Modules/Video/Support/Filament/VideoTableSchema.php create mode 100644 Modules/Video/Support/VideoTranscoder.php create mode 100644 Modules/Video/config/video.php create mode 100644 Modules/Video/database/migrations/2026_03_07_000000_create_video_tables.php create mode 100644 Modules/Video/module.json create mode 100644 Modules/Video/resources/views/filament/video-player.blade.php create mode 100644 Modules/Video/resources/views/filament/video-preview-field.blade.php create mode 100644 Modules/Video/resources/views/partials/video-upload-optimizer.blade.php diff --git a/Modules/Admin/Filament/Resources/ListingResource.php b/Modules/Admin/Filament/Resources/ListingResource.php index 4f2155627..276b1dbad 100644 --- a/Modules/Admin/Filament/Resources/ListingResource.php +++ b/Modules/Admin/Filament/Resources/ListingResource.php @@ -36,6 +36,7 @@ use Modules\Listing\Support\ListingCustomFieldSchemaBuilder; use Modules\Listing\Support\ListingPanelHelper; use Modules\Location\Models\City; use Modules\Location\Models\Country; +use Modules\Video\Support\Filament\VideoFormSchema; use UnitEnum; use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput; @@ -125,6 +126,7 @@ class ListingResource extends Resource ->multiple() ->image() ->reorderable(), + VideoFormSchema::listingSection(), ]); } diff --git a/Modules/Admin/Providers/AdminPanelProvider.php b/Modules/Admin/Providers/AdminPanelProvider.php index 779cd526a..8f66bb457 100644 --- a/Modules/Admin/Providers/AdminPanelProvider.php +++ b/Modules/Admin/Providers/AdminPanelProvider.php @@ -12,6 +12,7 @@ use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use Filament\View\PanelsRenderHook; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Illuminate\Cookie\Middleware\EncryptCookies; use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken; @@ -37,8 +38,10 @@ class AdminPanelProvider extends PanelProvider ->login() ->colors(['primary' => Color::Blue]) ->discoverResources(in: module_path('Admin', 'Filament/Resources'), for: 'Modules\\Admin\\Filament\\Resources') + ->discoverResources(in: module_path('Video', 'Filament/Admin/Resources'), for: 'Modules\\Video\\Filament\\Admin\\Resources') ->discoverPages(in: module_path('Admin', 'Filament/Pages'), for: 'Modules\\Admin\\Filament\\Pages') ->discoverWidgets(in: module_path('Admin', 'Filament/Widgets'), for: 'Modules\\Admin\\Filament\\Widgets') + ->renderHook(PanelsRenderHook::BODY_END, fn () => view('video::partials.video-upload-optimizer')) ->userMenuItems([ 'view-site' => MenuItem::make() ->label('View Site') diff --git a/Modules/Conversation/database/seeders/ConversationDemoSeeder.php b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php new file mode 100644 index 000000000..e9e6f7cbb --- /dev/null +++ b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php @@ -0,0 +1,118 @@ +conversationTablesExist()) { + return; + } + + $admin = User::query()->where('email', 'a@a.com')->first(); + $partner = User::query()->where('email', 'b@b.com')->first(); + + if (! $admin || ! $partner) { + return; + } + + $listings = Listing::query() + ->where('user_id', $admin->getKey()) + ->where('status', 'active') + ->orderBy('id') + ->take(2) + ->get(); + + if ($listings->count() < 2) { + return; + } + + $this->seedConversationThread( + $listings->get(0), + $admin, + $partner, + [ + ['sender' => 'partner', 'body' => 'Hi, is this still available?', 'hours_ago' => 30, 'read_after_minutes' => 4], + ['sender' => 'admin', 'body' => 'Yes, it is available. I can share more photos.', 'hours_ago' => 29, 'read_after_minutes' => 7], + ['sender' => 'partner', 'body' => 'Perfect. Can we meet tomorrow afternoon?', 'hours_ago' => 4, 'read_after_minutes' => null], + ] + ); + + $this->seedConversationThread( + $listings->get(1), + $admin, + $partner, + [ + ['sender' => 'partner', 'body' => 'Can you confirm the final price?', 'hours_ago' => 20, 'read_after_minutes' => 8], + ['sender' => 'admin', 'body' => 'I can do a small discount if you pick it up today.', 'hours_ago' => 18, 'read_after_minutes' => null], + ] + ); + } + + private function conversationTablesExist(): bool + { + return Schema::hasTable('conversations') && Schema::hasTable('conversation_messages'); + } + + private function seedConversationThread( + ?Listing $listing, + User $admin, + User $partner, + array $messages + ): void { + if (! $listing) { + return; + } + + $conversation = Conversation::updateOrCreate( + [ + 'listing_id' => $listing->getKey(), + 'buyer_id' => $partner->getKey(), + ], + [ + 'seller_id' => $admin->getKey(), + 'last_message_at' => now(), + ] + ); + + ConversationMessage::query() + ->where('conversation_id', $conversation->getKey()) + ->delete(); + + $lastMessageAt = null; + + foreach ($messages as $payload) { + $createdAt = now()->subHours((int) $payload['hours_ago']); + $sender = ($payload['sender'] ?? 'partner') === 'admin' ? $admin : $partner; + $readAfterMinutes = $payload['read_after_minutes']; + $readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null; + + $message = new ConversationMessage(); + $message->forceFill([ + 'conversation_id' => $conversation->getKey(), + 'sender_id' => $sender->getKey(), + 'body' => (string) $payload['body'], + 'read_at' => $readAt, + 'created_at' => $createdAt, + 'updated_at' => $readAt ?? $createdAt, + ])->save(); + + $lastMessageAt = $createdAt; + } + + $conversation->forceFill([ + 'seller_id' => $admin->getKey(), + 'last_message_at' => $lastMessageAt, + 'updated_at' => $lastMessageAt, + ])->saveQuietly(); + } +} diff --git a/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php new file mode 100644 index 000000000..0a73e3acc --- /dev/null +++ b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php @@ -0,0 +1,173 @@ +favoriteTablesExist()) { + return; + } + + $admin = User::query()->where('email', 'a@a.com')->first(); + $partner = User::query()->where('email', 'b@b.com')->first(); + + if (! $admin || ! $partner) { + return; + } + + $adminListings = Listing::query() + ->where('user_id', $admin->getKey()) + ->orderByDesc('is_featured') + ->orderBy('id') + ->get(); + + if ($adminListings->isEmpty()) { + return; + } + + $activeAdminListings = $adminListings->where('status', 'active')->values(); + + $this->seedFavoriteListings( + $partner, + $activeAdminListings->take(6) + ); + + $this->seedFavoriteListings( + $admin, + $adminListings->take(3)->values() + ); + + $this->seedFavoriteSeller($partner, $admin, now()->subDays(2)); + $this->seedFavoriteSeller($admin, $partner, now()->subDays(1)); + + $this->seedFavoriteSearches($partner, $this->partnerSearchPayloads()); + $this->seedFavoriteSearches($admin, $this->adminSearchPayloads()); + } + + private function favoriteTablesExist(): bool + { + return Schema::hasTable('favorite_listings') + && Schema::hasTable('favorite_sellers') + && Schema::hasTable('favorite_searches'); + } + + private function seedFavoriteListings(User $user, Collection $listings): void + { + $rows = $listings + ->values() + ->map(function (Listing $listing, int $index) use ($user): array { + $timestamp = now()->subHours(12 + ($index * 5)); + + return [ + 'user_id' => $user->getKey(), + 'listing_id' => $listing->getKey(), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]; + }) + ->all(); + + if ($rows === []) { + return; + } + + DB::table('favorite_listings')->upsert( + $rows, + ['user_id', 'listing_id'], + ['updated_at'] + ); + } + + private function seedFavoriteSeller(User $user, User $seller, \Illuminate\Support\Carbon $timestamp): void + { + if ((int) $user->getKey() === (int) $seller->getKey()) { + return; + } + + DB::table('favorite_sellers')->upsert( + [[ + 'user_id' => $user->getKey(), + 'seller_id' => $seller->getKey(), + 'created_at' => $timestamp, + 'updated_at' => $timestamp, + ]], + ['user_id', 'seller_id'], + ['updated_at'] + ); + } + + private function seedFavoriteSearches(User $user, array $payloads): void + { + foreach ($payloads as $index => $payload) { + $filters = FavoriteSearch::normalizeFilters([ + 'search' => $payload['search'] ?? null, + 'category' => $payload['category_id'] ?? null, + ]); + + if ($filters === []) { + continue; + } + + $signature = FavoriteSearch::signatureFor($filters); + $categoryName = null; + + if (! empty($payload['category_id'])) { + $categoryName = Category::query()->whereKey($payload['category_id'])->value('name'); + } + + $favoriteSearch = FavoriteSearch::updateOrCreate( + [ + 'user_id' => $user->getKey(), + 'signature' => $signature, + ], + [ + 'label' => FavoriteSearch::labelFor($filters, is_string($categoryName) ? $categoryName : null), + 'search_term' => $filters['search'] ?? null, + 'category_id' => $filters['category'] ?? null, + 'filters' => $filters, + ] + ); + + $timestamp = now()->subDays($index + 1); + $favoriteSearch->forceFill([ + 'created_at' => $favoriteSearch->wasRecentlyCreated ? $timestamp : $favoriteSearch->created_at, + 'updated_at' => $timestamp, + ])->saveQuietly(); + } + } + + private function partnerSearchPayloads(): array + { + $electronicsId = Category::query()->where('name', 'Electronics')->value('id'); + $vehiclesId = Category::query()->where('name', 'Vehicles')->value('id'); + $realEstateId = Category::query()->where('name', 'Real Estate')->value('id'); + + return [ + ['search' => 'iphone', 'category_id' => $electronicsId], + ['search' => 'sedan', 'category_id' => $vehiclesId], + ['search' => 'apartment', 'category_id' => $realEstateId], + ]; + } + + private function adminSearchPayloads(): array + { + $fashionId = Category::query()->where('name', 'Fashion')->value('id'); + $homeGardenId = Category::query()->where('name', 'Home & Garden')->value('id'); + + return [ + ['search' => 'vintage', 'category_id' => $fashionId], + ['search' => 'garden', 'category_id' => $homeGardenId], + ]; + } +} diff --git a/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php new file mode 100644 index 000000000..46cfaa747 --- /dev/null +++ b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php @@ -0,0 +1,175 @@ + 'admin-demo-sold-camera', + 'title' => 'Admin Demo Camera Bundle', + 'description' => 'Sample sold listing for the panel filters and activity cards.', + 'price' => 18450, + 'status' => 'sold', + 'city' => 'Istanbul', + 'country' => 'Turkey', + 'image' => 'sample_image/macbook.jpg', + 'expires_offset_days' => 12, + 'is_featured' => false, + ], + [ + 'slug' => 'admin-demo-expired-sofa', + 'title' => 'Admin Demo Sofa Set', + 'description' => 'Sample expired listing for the panel filters and republish flow.', + 'price' => 9800, + 'status' => 'expired', + 'city' => 'Ankara', + 'country' => 'Turkey', + 'image' => 'sample_image/cup.jpg', + 'expires_offset_days' => -5, + 'is_featured' => false, + ], + [ + 'slug' => 'admin-demo-expired-bike', + 'title' => 'Admin Demo City Bike', + 'description' => 'Extra expired sample listing so My Listings is not empty in filtered views.', + 'price' => 6200, + 'status' => 'expired', + 'city' => 'Izmir', + 'country' => 'Turkey', + 'image' => 'sample_image/car2.jpeg', + 'expires_offset_days' => -11, + 'is_featured' => false, + ], + ]; + + public function run(): void + { + $admin = $this->resolveAdminUser(); + + if (! $admin) { + return; + } + + $this->claimAllListingsForAdmin($admin); + + $categories = $this->resolveCategories(); + + if ($categories->isEmpty()) { + return; + } + + foreach (self::PANEL_LISTINGS as $index => $payload) { + $category = $categories->get($index % $categories->count()); + + if (! $category instanceof Category) { + continue; + } + + $listing = Listing::updateOrCreate( + ['slug' => $payload['slug']], + [ + 'slug' => $payload['slug'], + 'title' => $payload['title'], + 'description' => $payload['description'], + 'price' => $payload['price'], + 'currency' => 'TRY', + 'city' => $payload['city'], + 'country' => $payload['country'], + 'category_id' => $category->getKey(), + 'user_id' => $admin->getKey(), + 'status' => $payload['status'], + 'contact_email' => $admin->email, + 'contact_phone' => '+905551112233', + 'is_featured' => $payload['is_featured'], + 'expires_at' => now()->addDays((int) $payload['expires_offset_days']), + ] + ); + + $this->syncListingImage($listing, (string) $payload['image']); + } + } + + private function resolveAdminUser(): ?User + { + return User::query()->where('email', 'a@a.com')->first() + ?? User::query()->whereHas('roles', fn ($query) => $query->where('name', 'admin'))->first() + ?? User::query()->first(); + } + + private function claimAllListingsForAdmin(User $admin): void + { + Listing::query() + ->where(function ($query) use ($admin): void { + $query + ->whereNull('user_id') + ->orWhere('user_id', '!=', $admin->getKey()); + }) + ->update([ + 'user_id' => $admin->getKey(), + 'contact_email' => $admin->email, + 'updated_at' => now(), + ]); + } + + private function resolveCategories(): Collection + { + $leafCategories = Category::query() + ->where('is_active', true) + ->whereDoesntHave('children') + ->orderBy('sort_order') + ->orderBy('name') + ->get(); + + if ($leafCategories->isNotEmpty()) { + return $leafCategories->values(); + } + + return Category::query() + ->where('is_active', true) + ->orderBy('sort_order') + ->orderBy('name') + ->get() + ->values(); + } + + private function syncListingImage(Listing $listing, string $imageRelativePath): void + { + $imageAbsolutePath = public_path($imageRelativePath); + + if (! is_file($imageAbsolutePath)) { + return; + } + + $targetFileName = basename($imageAbsolutePath); + $existingMedia = $listing->getMedia('listing-images')->first(); + + if ( + $existingMedia + && (string) $existingMedia->file_name === $targetFileName + && (string) $existingMedia->disk === 'public' + ) { + try { + if (is_file($existingMedia->getPath())) { + return; + } + } catch (\Throwable) { + } + } + + $listing->clearMediaCollection('listing-images'); + + $listing + ->addMedia($imageAbsolutePath) + ->usingFileName(Str::slug($listing->slug).'-'.basename($imageAbsolutePath)) + ->preservingOriginal() + ->toMediaCollection('listing-images', 'public'); + } +} diff --git a/Modules/Listing/Database/Seeders/ListingSeeder.php b/Modules/Listing/Database/Seeders/ListingSeeder.php index 5bc325267..f037f9db2 100644 --- a/Modules/Listing/Database/Seeders/ListingSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingSeeder.php @@ -55,10 +55,12 @@ class ListingSeeder extends Seeder private function resolveSeederUser(): ?User { - return User::query() - ->where('email', 'b@b.com') - ->orWhere('email', 'partner@openclassify.com') - ->first(); + return User::query()->where('email', 'a@a.com')->first() + ?? User::query()->where('email', 'admin@openclassify.com')->first() + ?? User::query() + ->whereHas('roles', fn ($query) => $query->where('name', 'admin')) + ->first() + ?? User::query()->first(); } private function resolveSeedableCategories(): Collection diff --git a/Modules/Listing/Http/Controllers/ListingController.php b/Modules/Listing/Http/Controllers/ListingController.php index acc1fc67c..f39665b17 100644 --- a/Modules/Listing/Http/Controllers/ListingController.php +++ b/Modules/Listing/Http/Controllers/ListingController.php @@ -164,12 +164,14 @@ class ListingController extends Controller 'category:id,name,parent_id,slug', 'category.parent:id,name,parent_id,slug', 'category.parent.parent:id,name,parent_id,slug', + 'videos' => fn ($query) => $query->published()->ordered(), ]); $presentableCustomFields = ListingCustomFieldSchemaBuilder::presentableValues( $listing->category_id ? (int) $listing->category_id : null, $listing->custom_fields ?? [], ); $gallery = $listing->themeGallery(); + $listingVideos = $listing->getRelation('videos'); $relatedListings = $listing->relatedSuggestions(12); $themePillCategories = Category::themePills(10); $breadcrumbCategories = $listing->category @@ -210,6 +212,7 @@ class ListingController extends Controller 'presentableCustomFields', 'existingConversationId', 'gallery', + 'listingVideos', 'relatedListings', 'themePillCategories', 'breadcrumbCategories', diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index 3c7c24fa5..1e801237b 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -11,6 +11,7 @@ use Illuminate\Support\Str; use Modules\Category\Models\Category; use Modules\Listing\States\ListingStatus; use Modules\Listing\Support\ListingPanelHelper; +use Modules\Video\Models\Video; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity; use Spatie\MediaLibrary\HasMedia; @@ -71,6 +72,11 @@ class Listing extends Model implements HasMedia return $this->hasMany(\Modules\Conversation\App\Models\Conversation::class); } + public function videos() + { + return $this->hasMany(Video::class)->ordered(); + } + public function scopePublicFeed(Builder $query): Builder { return $query @@ -178,7 +184,7 @@ class Listing extends Model implements HasMedia { $baseQuery = static::query() ->publicFeed() - ->with('category:id,name') + ->with(['category:id,name', 'videos']) ->whereKeyNot($this->getKey()); $primary = (clone $baseQuery) diff --git a/Modules/Listing/resources/views/themes/default/show.blade.php b/Modules/Listing/resources/views/themes/default/show.blade.php index a219646e4..58a27991f 100644 --- a/Modules/Listing/resources/views/themes/default/show.blade.php +++ b/Modules/Listing/resources/views/themes/default/show.blade.php @@ -77,6 +77,19 @@

Description

{{ $displayDescription }}

+ @if(($listingVideos ?? collect())->isNotEmpty()) +
+

Videos

+
+ @foreach($listingVideos as $video) +
+ +

{{ $video->titleLabel() }}

+
+ @endforeach +
+
+ @endif @if(($presentableCustomFields ?? []) !== [])

İlan Özellikleri

diff --git a/Modules/Listing/resources/views/themes/otoplus/show.blade.php b/Modules/Listing/resources/views/themes/otoplus/show.blade.php index 208f3d19a..ad9414ef3 100644 --- a/Modules/Listing/resources/views/themes/otoplus/show.blade.php +++ b/Modules/Listing/resources/views/themes/otoplus/show.blade.php @@ -126,6 +126,20 @@ @endforeach
+ + @if(($listingVideos ?? collect())->isNotEmpty()) +
+

Videolar

+
+ @foreach($listingVideos as $video) +
+ +

{{ $video->titleLabel() }}

+
+ @endforeach +
+
+ @endif