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