diff --git a/Modules/Conversation/database/seeders/ConversationDemoSeeder.php b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php index a8b2cd53f..4018ee4a9 100644 --- a/Modules/Conversation/database/seeders/ConversationDemoSeeder.php +++ b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php @@ -3,12 +3,12 @@ namespace Modules\Conversation\Database\Seeders; use Illuminate\Database\Seeder; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; use Modules\Conversation\App\Models\Conversation; use Modules\Conversation\App\Models\ConversationMessage; use Modules\Listing\Models\Listing; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; class ConversationDemoSeeder extends Seeder { @@ -18,72 +18,59 @@ class ConversationDemoSeeder extends Seeder return; } - $admin = User::query()->where('email', 'a@a.com')->first(); - $member = User::query()->where('email', 'b@b.com')->first(); + $users = User::query() + ->whereIn('email', DemoUserCatalog::emails()) + ->orderBy('email') + ->get() + ->values(); - if (! $admin || ! $member) { + if ($users->count() < 2) { return; } - $adminListings = Listing::query() - ->where('user_id', $admin->getKey()) - ->where('status', 'active') - ->orderBy('id') - ->take(2) - ->get(); + ConversationMessage::query() + ->whereHas('conversation', fn ($query) => $query->whereIn('buyer_id', $users->pluck('id'))->orWhereIn('seller_id', $users->pluck('id'))) + ->delete(); - $memberListings = Listing::query() - ->where('user_id', $member->getKey()) - ->where('status', 'active') - ->orderBy('id') - ->take(2) - ->get(); + Conversation::query() + ->whereIn('buyer_id', $users->pluck('id')) + ->orWhereIn('seller_id', $users->pluck('id')) + ->delete(); - if ($adminListings->count() < 2 || $memberListings->count() < 2) { - return; + foreach ($users as $index => $buyer) { + $primarySeller = $users->get(($index + 1) % $users->count()); + $secondarySeller = $users->get(($index + 2) % $users->count()); + + if (! $primarySeller instanceof User || ! $secondarySeller instanceof User) { + continue; + } + + $primaryListing = Listing::query() + ->where('user_id', $primarySeller->getKey()) + ->where('status', 'active') + ->orderBy('id') + ->first(); + + $secondaryListing = Listing::query() + ->where('user_id', $secondarySeller->getKey()) + ->where('status', 'active') + ->orderBy('id') + ->first(); + + $this->seedConversationThread( + $primarySeller, + $buyer, + $primaryListing, + $this->messagePayloads($index, false) + ); + + $this->seedConversationThread( + $secondarySeller, + $buyer, + $secondaryListing, + $this->messagePayloads($index, true) + ); } - - $this->seedConversationThread( - $admin, - $member, - $adminListings->get(0), - [ - ['sender' => 'buyer', 'body' => 'Hi, is this still available?', 'hours_ago' => 30, 'read_after_minutes' => 4], - ['sender' => 'seller', 'body' => 'Yes, it is. I can share more photos.', 'hours_ago' => 29, 'read_after_minutes' => 7], - ['sender' => 'buyer', 'body' => 'Perfect. Can we meet tomorrow afternoon?', 'hours_ago' => 4, 'read_after_minutes' => null], - ] - ); - - $this->seedConversationThread( - $admin, - $member, - $adminListings->get(1), - [ - ['sender' => 'buyer', 'body' => 'Can you confirm the final price?', 'hours_ago' => 20, 'read_after_minutes' => 8], - ['sender' => 'seller', 'body' => 'I can do a small discount if you pick it up today.', 'hours_ago' => 18, 'read_after_minutes' => null], - ] - ); - - $this->seedConversationThread( - $member, - $admin, - $memberListings->get(0), - [ - ['sender' => 'buyer', 'body' => 'Hello, does this listing include the original accessories?', 'hours_ago' => 14, 'read_after_minutes' => 6], - ['sender' => 'seller', 'body' => 'Yes, box and accessories are included.', 'hours_ago' => 13, 'read_after_minutes' => 9], - ['sender' => 'buyer', 'body' => 'Great. I can pick it up tonight.', 'hours_ago' => 2, 'read_after_minutes' => null], - ] - ); - - $this->seedConversationThread( - $member, - $admin, - $memberListings->get(1), - [ - ['sender' => 'buyer', 'body' => 'Would you accept a bank transfer?', 'hours_ago' => 11, 'read_after_minutes' => 5], - ['sender' => 'seller', 'body' => 'Yes, that works for me.', 'hours_ago' => 10, 'read_after_minutes' => null], - ] - ); } private function conversationTablesExist(): bool @@ -97,7 +84,7 @@ class ConversationDemoSeeder extends Seeder ?Listing $listing, array $messages ): void { - if (! $listing) { + if (! $listing || (int) $seller->getKey() === (int) $buyer->getKey()) { return; } @@ -112,10 +99,6 @@ class ConversationDemoSeeder extends Seeder ] ); - ConversationMessage::query() - ->where('conversation_id', $conversation->getKey()) - ->delete(); - $lastMessageAt = null; foreach ($messages as $payload) { @@ -137,10 +120,47 @@ class ConversationDemoSeeder extends Seeder $lastMessageAt = $createdAt; } - $conversation->forceFill([ - 'seller_id' => $seller->getKey(), - 'last_message_at' => $lastMessageAt, - 'updated_at' => $lastMessageAt, - ])->saveQuietly(); + if ($lastMessageAt) { + $conversation->forceFill([ + 'seller_id' => $seller->getKey(), + 'last_message_at' => $lastMessageAt, + 'updated_at' => $lastMessageAt, + ])->saveQuietly(); + } + } + + private function messagePayloads(int $index, bool $secondary): array + { + $openingMessages = [ + 'Is this listing still available?', + 'Can you share the best price?', + 'Would pickup this evening work for you?', + 'Can you confirm the condition details?', + 'Do you have any more photos?', + ]; + + $sellerReplies = [ + 'Yes, it is available.', + 'I can offer a small discount.', + 'This evening works for me.', + 'Everything is in clean condition.', + 'I can send more photos in a minute.', + ]; + + $closingMessages = [ + 'Great, I will message again before I leave.', + 'Perfect. I can arrange pickup.', + 'Thanks. That sounds good to me.', + 'Understood. I am interested.', + 'Nice. I will keep this saved.', + ]; + + $offset = ($index + ($secondary ? 2 : 0)) % count($openingMessages); + + return [ + ['sender' => 'buyer', 'body' => $openingMessages[$offset], 'hours_ago' => 30 - $index, 'read_after_minutes' => 5], + ['sender' => 'seller', 'body' => $sellerReplies[$offset], 'hours_ago' => 28 - $index, 'read_after_minutes' => 8], + ['sender' => 'buyer', 'body' => $closingMessages[$offset], 'hours_ago' => 4 + $index, 'read_after_minutes' => $secondary ? 6 : null], + ]; } } diff --git a/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php index b5f421e00..d55ad15dd 100644 --- a/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php +++ b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php @@ -10,6 +10,7 @@ use Modules\Category\Models\Category; use Modules\Favorite\App\Models\FavoriteSearch; use Modules\Listing\Models\Listing; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; class FavoriteDemoSeeder extends Seeder { @@ -19,47 +20,40 @@ class FavoriteDemoSeeder extends Seeder return; } - $admin = User::query()->where('email', 'a@a.com')->first(); - $member = User::query()->where('email', 'b@b.com')->first(); + $users = User::query() + ->whereIn('email', DemoUserCatalog::emails()) + ->orderBy('email') + ->get() + ->values(); - if (! $admin || ! $member) { + if ($users->count() < 2) { return; } - $adminListings = Listing::query() - ->where('user_id', $admin->getKey()) - ->orderByDesc('is_featured') - ->orderBy('id') - ->get(); + DB::table('favorite_listings')->whereIn('user_id', $users->pluck('id'))->delete(); + DB::table('favorite_sellers')->whereIn('user_id', $users->pluck('id'))->delete(); + FavoriteSearch::query()->whereIn('user_id', $users->pluck('id'))->delete(); - $memberListings = Listing::query() - ->where('user_id', $member->getKey()) - ->orderByDesc('is_featured') - ->orderBy('id') - ->get(); + foreach ($users as $index => $user) { + $favoriteSeller = $users->get(($index + 1) % $users->count()); + $secondarySeller = $users->get(($index + 2) % $users->count()); - if ($adminListings->isEmpty() || $memberListings->isEmpty()) { - return; + if (! $favoriteSeller instanceof User || ! $secondarySeller instanceof User) { + continue; + } + + $favoriteListings = Listing::query() + ->whereIn('user_id', [$favoriteSeller->getKey(), $secondarySeller->getKey()]) + ->where('status', 'active') + ->orderByDesc('is_featured') + ->orderBy('id') + ->take(4) + ->get(); + + $this->seedFavoriteListings($user, $favoriteListings); + $this->seedFavoriteSeller($user, $favoriteSeller, now()->subDays($index + 1)); + $this->seedFavoriteSearches($user, $this->searchPayloadsForUser($index)); } - - $activeAdminListings = $adminListings->where('status', 'active')->values(); - $activeMemberListings = $memberListings->where('status', 'active')->values(); - - $this->seedFavoriteListings( - $member, - $activeAdminListings->take(6) - ); - - $this->seedFavoriteListings( - $admin, - $activeMemberListings->take(4) - ); - - $this->seedFavoriteSeller($member, $admin, now()->subDays(2)); - $this->seedFavoriteSeller($admin, $member, now()->subDays(1)); - - $this->seedFavoriteSearches($member, $this->memberSearchPayloads()); - $this->seedFavoriteSearches($admin, $this->adminSearchPayloads()); } private function favoriteTablesExist(): bool @@ -74,7 +68,7 @@ class FavoriteDemoSeeder extends Seeder $rows = $listings ->values() ->map(function (Listing $listing, int $index) use ($user): array { - $timestamp = now()->subHours(12 + ($index * 5)); + $timestamp = now()->subHours(8 + ($index * 3)); return [ 'user_id' => $user->getKey(), @@ -154,31 +148,28 @@ class FavoriteDemoSeeder extends Seeder } } - private function memberSearchPayloads(): array + private function searchPayloadsForUser(int $index): array { - $electronicsId = Category::query()->where('slug', 'electronics')->value('id'); - $vehiclesId = Category::query()->where('slug', 'vehicles')->value('id'); - $realEstateId = Category::query()->where('slug', 'real-estate')->value('id'); - $servicesId = Category::query()->where('slug', 'services')->value('id'); - - return [ - ['search' => 'iphone', 'category_id' => $electronicsId], - ['search' => 'sedan', 'category_id' => $vehiclesId], - ['search' => 'apartment', 'category_id' => $realEstateId], - ['search' => 'repair', 'category_id' => $servicesId], + $blueprints = [ + ['search' => 'phone', 'slug' => 'electronics'], + ['search' => 'car', 'slug' => 'vehicles'], + ['search' => 'apartment', 'slug' => 'real-estate'], + ['search' => 'style', 'slug' => 'fashion'], + ['search' => 'furniture', 'slug' => 'home-garden'], + ['search' => 'fitness', 'slug' => 'sports'], + ['search' => 'remote', 'slug' => 'jobs'], + ['search' => 'cleaning', 'slug' => 'services'], ]; - } - private function adminSearchPayloads(): array - { - $fashionId = Category::query()->where('slug', 'fashion')->value('id'); - $homeGardenId = Category::query()->where('slug', 'home-garden')->value('id'); - $sportsId = Category::query()->where('slug', 'sports')->value('id'); + return collect(range(0, 2)) + ->map(function (int $offset) use ($blueprints, $index): array { + $blueprint = $blueprints[($index + $offset) % count($blueprints)]; - return [ - ['search' => 'vintage', 'category_id' => $fashionId], - ['search' => 'garden', 'category_id' => $homeGardenId], - ['search' => 'fitness', 'category_id' => $sportsId], - ]; + return [ + 'search' => $blueprint['search'], + 'category_id' => Category::query()->where('slug', $blueprint['slug'])->value('id'), + ]; + }) + ->all(); } } diff --git a/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php index 712da266b..8a2db0a37 100644 --- a/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php @@ -3,231 +3,79 @@ namespace Modules\Listing\Database\Seeders; use Illuminate\Database\Seeder; -use Illuminate\Support\Collection; -use Illuminate\Support\Str; -use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; class ListingPanelDemoSeeder extends Seeder { - private const USER_PANEL_LISTINGS = [ - 'a@a.com' => [ - [ - 'slug' => 'admin-demo-pro-workstation', - 'title' => 'Admin Demo Pro Workstation', - 'description' => 'Active demo listing for inbox, favorites, and video testing.', - 'price' => 28450, - 'status' => 'active', - 'city' => 'Istanbul', - 'country' => 'Turkey', - 'image' => 'sample_image/macbook.jpg', - 'expires_offset_days' => 21, - 'is_featured' => true, - ], - [ - 'slug' => 'admin-demo-sold-camera', - 'title' => 'Admin Demo Camera Bundle', - 'description' => 'Sold demo listing for panel status testing.', - 'price' => 18450, - 'status' => 'sold', - 'city' => 'Ankara', - 'country' => 'Turkey', - 'image' => 'sample_image/headphones.jpg', - 'expires_offset_days' => 12, - 'is_featured' => false, - ], - [ - 'slug' => 'admin-demo-expired-sofa', - 'title' => 'Admin Demo Sofa Set', - 'description' => 'Expired demo listing for republish testing.', - 'price' => 9800, - 'status' => 'expired', - 'city' => 'Izmir', - 'country' => 'Turkey', - 'image' => 'sample_image/cup.jpg', - 'expires_offset_days' => -5, - 'is_featured' => false, - ], - ], - 'b@b.com' => [ - [ - 'slug' => 'member-demo-iphone', - 'title' => 'Member Demo iPhone Bundle', - 'description' => 'Active demo listing owned by the member account.', - 'price' => 21990, - 'status' => 'active', - 'city' => 'Bursa', - 'country' => 'Turkey', - 'image' => 'sample_image/phone.jpeg', - 'expires_offset_days' => 24, - 'is_featured' => true, - ], - [ - 'slug' => 'member-demo-city-bike', - 'title' => 'Member Demo City Bike', - 'description' => 'Second active listing so conversations and favorites are easy to test.', - 'price' => 7600, - 'status' => 'active', - 'city' => 'Antalya', - 'country' => 'Turkey', - 'image' => 'sample_image/car2.jpeg', - 'expires_offset_days' => 17, - 'is_featured' => false, - ], - [ - 'slug' => 'member-demo-vintage-chair', - 'title' => 'Member Demo Vintage Chair', - 'description' => 'Sold listing for panel filters on the member account.', - 'price' => 4350, - 'status' => 'sold', - 'city' => 'Istanbul', - 'country' => 'Turkey', - 'image' => 'sample_image/car.jpeg', - 'expires_offset_days' => 8, - 'is_featured' => false, - ], - [ - 'slug' => 'member-demo-garden-tools', - 'title' => 'Member Demo Garden Tools Set', - 'description' => 'Expired listing for republish flow on the member account.', - 'price' => 3150, - 'status' => 'expired', - 'city' => 'Ankara', - 'country' => 'Turkey', - 'image' => 'sample_image/laptop.jpg', - 'expires_offset_days' => -7, - 'is_featured' => false, - ], - ], + private const LEGACY_SLUGS = [ + 'admin-demo-pro-workstation', + 'admin-demo-sold-camera', + 'admin-demo-expired-sofa', + 'member-demo-iphone', + 'member-demo-city-bike', + 'member-demo-vintage-chair', + 'member-demo-garden-tools', ]; public function run(): void { - $admin = $this->resolveUserByEmail('a@a.com'); + Listing::query() + ->whereIn('slug', self::LEGACY_SLUGS) + ->get() + ->each(function (Listing $listing): void { + $listing->clearMediaCollection('listing-images'); + $listing->delete(); + }); - if (! $admin) { - return; - } + foreach (DemoUserCatalog::emails() as $email) { + $user = User::query()->where('email', $email)->first(); - $this->claimAllListingsForAdmin($admin); - - $categories = $this->resolveCategories(); - - if ($categories->isEmpty()) { - return; - } - - foreach (self::USER_PANEL_LISTINGS as $email => $payloads) { - $owner = $this->resolveUserByEmail($email); - - if (! $owner) { + if (! $user) { continue; } - foreach ($payloads 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' => $owner->getKey(), - 'status' => $payload['status'], - 'contact_email' => $owner->email, - 'contact_phone' => $email === 'a@a.com' ? '+905551112233' : '+905551112244', - 'is_featured' => $payload['is_featured'], - 'expires_at' => now()->addDays((int) $payload['expires_offset_days']), - ] - ); - - $this->syncListingImage($listing, (string) $payload['image']); - } + $this->applyPanelStates($user); } } - private function resolveUserByEmail(string $email): ?User + private function applyPanelStates(User $user): void { - return User::query()->where('email', $email)->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') + $listings = Listing::query() + ->where('user_id', $user->getKey()) + ->where('slug', 'like', 'demo-%') + ->orderBy('created_at') ->get() ->values(); + + foreach ($listings as $index => $listing) { + $listing->forceFill([ + 'status' => $this->statusForIndex($index), + 'is_featured' => $index < 2, + 'expires_at' => $this->expiresAtForIndex($index), + 'updated_at' => now()->subHours($index), + ])->saveQuietly(); + } } - private function syncListingImage(Listing $listing, string $imageRelativePath): void + private function statusForIndex(int $index): string { - $imageAbsolutePath = public_path($imageRelativePath); + return match ($index % 9) { + 2 => 'sold', + 3 => 'expired', + 4 => 'pending', + default => 'active', + }; + } - 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'); + private function expiresAtForIndex(int $index): \Illuminate\Support\Carbon + { + return match ($this->statusForIndex($index)) { + 'expired' => now()->subDays(4 + ($index % 5)), + 'sold' => now()->addDays(8 + ($index % 4)), + 'pending' => now()->addDays(5 + ($index % 4)), + default => now()->addDays(20 + ($index % 9)), + }; } } diff --git a/Modules/Listing/Database/Seeders/ListingSeeder.php b/Modules/Listing/Database/Seeders/ListingSeeder.php index 85b945c88..7a8da61d2 100644 --- a/Modules/Listing/Database/Seeders/ListingSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingSeeder.php @@ -8,9 +8,11 @@ use Illuminate\Support\Facades\Schema; use Illuminate\Support\Str; use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; +use Modules\Listing\Support\DemoListingImageFactory; use Modules\Location\Models\City; use Modules\Location\Models\Country; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; class ListingSeeder extends Seeder { @@ -26,31 +28,45 @@ class ListingSeeder extends Seeder public function run(): void { - $user = $this->resolveSeederUser(); + $users = $this->resolveSeederUsers(); $categories = $this->resolveSeedableCategories(); - if (! $user || $categories->isEmpty()) { + if ($users->isEmpty() || $categories->isEmpty()) { return; } $countries = $this->resolveCountries(); $turkeyCities = $this->resolveTurkeyCities(); + $plannedSlugs = []; - foreach ($categories as $index => $category) { - $listingData = $this->buildListingData($category, $index, $countries, $turkeyCities); - $listing = $this->upsertListing($index, $listingData, $category, $user); - $this->syncListingImage($listing, $listingData['image']); + foreach ($users as $userIndex => $user) { + foreach ($categories as $categoryIndex => $category) { + $listingIndex = ($userIndex * max(1, $categories->count())) + $categoryIndex; + $listingData = $this->buildListingData($category, $listingIndex, $countries, $turkeyCities, $user); + $listing = $this->upsertListing($listingData, $category, $user); + $plannedSlugs[] = $listing->slug; + $this->syncListingImage($listing, $listingData['image_path']); + } } + + Listing::query() + ->whereIn('user_id', $users->pluck('id')) + ->where('slug', 'like', 'demo-%') + ->whereNotIn('slug', $plannedSlugs) + ->get() + ->each(function (Listing $listing): void { + $listing->clearMediaCollection('listing-images'); + $listing->delete(); + }); } - private function resolveSeederUser(): ?User + private function resolveSeederUsers(): Collection { - 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(); + return User::query() + ->whereIn('email', DemoUserCatalog::emails()) + ->orderBy('email') + ->get() + ->values(); } private function resolveSeedableCategories(): Collection @@ -58,6 +74,7 @@ class ListingSeeder extends Seeder $leafCategories = Category::query() ->where('is_active', true) ->whereDoesntHave('children') + ->with('parent:id,name') ->orderBy('sort_order') ->orderBy('name') ->get(); @@ -68,6 +85,7 @@ class ListingSeeder extends Seeder return Category::query() ->where('is_active', true) + ->with('parent:id,name') ->orderBy('sort_order') ->orderBy('name') ->get() @@ -119,17 +137,32 @@ class ListingSeeder extends Seeder Category $category, int $index, Collection $countries, - Collection $turkeyCities + Collection $turkeyCities, + User $user ): array { $location = $this->resolveLocation($index, $countries, $turkeyCities); + $title = $this->buildTitle($category, $index, $user); + $slug = 'demo-'.Str::slug($user->email).'-'.$category->slug; + $familyName = trim((string) ($category->parent?->name ?? $category->name)); return [ - 'title' => $this->buildTitle($category, $index), - 'description' => $this->buildDescription($category, $location['city'], $location['country']), + 'slug' => $slug, + 'title' => $title, + 'description' => $this->buildDescription($category, $location['city'], $location['country'], $user), 'price' => $this->priceForIndex($index), 'city' => $location['city'], 'country' => $location['country'], - 'image' => null, + 'contact_phone' => DemoUserCatalog::phoneFor($user->email), + 'is_featured' => $index % 7 === 0, + 'expires_at' => now()->addDays(21 + ($index % 9)), + 'created_at' => now()->subHours(6 + $index), + 'image_path' => DemoListingImageFactory::ensure( + $slug, + $title, + $familyName, + $user->name, + $index + ), ]; } @@ -137,7 +170,6 @@ class ListingSeeder extends Seeder { $turkeyCountry = $countries->first(fn ($country): bool => strtoupper((string) $country->code) === 'TR'); $turkeyName = trim((string) ($turkeyCountry->name ?? 'Turkey')) ?: 'Turkey'; - $useForeignCountry = $countries->count() > 1 && $index % 4 === 0; if ($useForeignCountry) { @@ -164,22 +196,29 @@ class ListingSeeder extends Seeder ]; } - private function buildTitle(Category $category, int $index): string + private function buildTitle(Category $category, int $index, User $user): string { $prefix = self::TITLE_PREFIXES[$index % count(self::TITLE_PREFIXES)]; $categoryName = trim((string) $category->name); + $ownerFragment = trim(Str::before($user->name, ' ')); - return sprintf('%s %s listing', $prefix, $categoryName !== '' ? $categoryName : 'item'); + return sprintf( + '%s %s for %s', + $prefix, + $categoryName !== '' ? $categoryName : 'item', + $ownerFragment !== '' ? $ownerFragment : 'demo' + ); } - private function buildDescription(Category $category, string $city, string $country): string + private function buildDescription(Category $category, string $city, string $country, User $user): string { $categoryName = trim((string) $category->name); $location = trim(collect([$city, $country])->filter()->join(', ')); return sprintf( - 'Listed in %s, in clean condition and ready to use. Pickup area: %s. Message for more details.', + '%s listed by %s. Clean demo condition, unique seeded media, and ready for browsing, favorites, inbox, and panel testing. Pickup area: %s.', $categoryName !== '' ? $categoryName : 'Item', + trim((string) $user->name) !== '' ? trim((string) $user->name) : 'a marketplace user', $location !== '' ? $location : 'Turkey' ); } @@ -203,14 +242,12 @@ class ListingSeeder extends Seeder return $base + $step; } - private function upsertListing(int $index, array $data, Category $category, User $user): Listing + private function upsertListing(array $data, Category $category, User $user): Listing { - $slug = Str::slug($category->slug.'-'.$data['title']).'-'.($index + 1); - - return Listing::updateOrCreate( - ['slug' => $slug], + $listing = Listing::updateOrCreate( + ['slug' => $data['slug']], [ - 'slug' => $slug, + 'slug' => $data['slug'], 'title' => $data['title'], 'description' => $data['description'], 'price' => $data['price'], @@ -221,63 +258,22 @@ class ListingSeeder extends Seeder 'user_id' => $user->id, 'status' => 'active', 'contact_email' => $user->email, - 'contact_phone' => '+905551112233', - 'is_featured' => $index < 8, + 'contact_phone' => $data['contact_phone'], + 'is_featured' => $data['is_featured'], + 'expires_at' => $data['expires_at'], ] ); + + $listing->forceFill([ + 'created_at' => $data['created_at'], + 'updated_at' => $data['created_at'], + ])->saveQuietly(); + + return $listing; } - private function syncListingImage(Listing $listing, ?string $imageRelativePath): void + private function syncListingImage(Listing $listing, string $imageAbsolutePath): void { - if (blank($imageRelativePath)) { - $listing->clearMediaCollection('listing-images'); - - return; - } - - $imageAbsolutePath = public_path($imageRelativePath); - - if (! is_file($imageAbsolutePath)) { - if ($this->command) { - $this->command->warn("Image not found: {$imageRelativePath}"); - } - - return; - } - - $targetFileName = basename($imageAbsolutePath); - $mediaItems = $listing->getMedia('listing-images'); - - if (! $this->hasSingleHealthyTargetMedia($mediaItems, $targetFileName)) { - $listing->clearMediaCollection('listing-images'); - - $listing - ->addMedia($imageAbsolutePath) - ->preservingOriginal() - ->toMediaCollection('listing-images', 'public'); - } - } - - private function hasSingleHealthyTargetMedia(Collection $mediaItems, string $targetFileName): bool - { - if ($mediaItems->count() !== 1) { - return false; - } - - $media = $mediaItems->first(); - - if ( - ! $media - || (string) $media->file_name !== $targetFileName - || (string) $media->disk !== 'public' - ) { - return false; - } - - try { - return is_file($media->getPath()); - } catch (\Throwable) { - return false; - } + $listing->replacePublicImage($imageAbsolutePath, $listing->slug.'.svg'); } } diff --git a/Modules/Listing/Models/Listing.php b/Modules/Listing/Models/Listing.php index f9255a805..ee6dcf4c2 100644 --- a/Modules/Listing/Models/Listing.php +++ b/Modules/Listing/Models/Listing.php @@ -18,6 +18,7 @@ use Spatie\Activitylog\Traits\LogsActivity; use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\ModelStates\HasStates; +use Throwable; class Listing extends Model implements HasMedia { @@ -347,6 +348,41 @@ class Listing extends Model implements HasMedia ]; } + public function replacePublicImage(string $absolutePath, ?string $fileName = null): void + { + if (! is_file($absolutePath)) { + return; + } + + $targetFileName = trim((string) ($fileName ?: basename($absolutePath))); + $existingMediaItems = $this->getMedia('listing-images'); + + if ($existingMediaItems->count() === 1) { + $existingMedia = $existingMediaItems->first(); + + if ( + $existingMedia + && (string) $existingMedia->file_name === $targetFileName + && (string) $existingMedia->disk === 'public' + ) { + try { + if (is_file($existingMedia->getPath())) { + return; + } + } catch (Throwable) { + } + } + } + + $this->clearMediaCollection('listing-images'); + + $this + ->addMedia($absolutePath) + ->usingFileName($targetFileName) + ->preservingOriginal() + ->toMediaCollection('listing-images', 'public'); + } + public function statusValue(): string { return $this->status instanceof ListingStatus diff --git a/Modules/Listing/Support/DemoListingImageFactory.php b/Modules/Listing/Support/DemoListingImageFactory.php new file mode 100644 index 000000000..c80e3b5f8 --- /dev/null +++ b/Modules/Listing/Support/DemoListingImageFactory.php @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + OPENCLASSIFY DEMO + {$shortCategory} + {$shortOwner} + {$shortTitle} + {$code} + +SVG; + + file_put_contents($filePath, $svg); + + return $filePath; + } + + private static function escape(string $value): string + { + return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8'); + } +} diff --git a/Modules/User/App/Support/DemoUserCatalog.php b/Modules/User/App/Support/DemoUserCatalog.php new file mode 100644 index 000000000..dcec58bd2 --- /dev/null +++ b/Modules/User/App/Support/DemoUserCatalog.php @@ -0,0 +1,74 @@ + 'a@a.com', + 'name' => 'Admin', + 'password' => '236330', + 'phone' => '+905551112233', + 'is_admin' => true, + ], + [ + 'email' => 'b@b.com', + 'name' => 'Member', + 'password' => '236330', + 'phone' => '+905551112244', + 'is_admin' => false, + ], + [ + 'email' => 'c@c.com', + 'name' => 'Ava Carter', + 'password' => '236330', + 'phone' => '+905551112255', + 'is_admin' => false, + ], + [ + 'email' => 'd@d.com', + 'name' => 'Liam Stone', + 'password' => '236330', + 'phone' => '+905551112266', + 'is_admin' => false, + ], + [ + 'email' => 'e@e.com', + 'name' => 'Mila Reed', + 'password' => '236330', + 'phone' => '+905551112277', + 'is_admin' => false, + ], + ]; + } + + public static function emails(): array + { + return array_column(self::records(), 'email'); + } + + public static function phoneFor(string $email): string + { + foreach (self::records() as $record) { + if ($record['email'] === $email) { + return $record['phone']; + } + } + + return '+905551110000'; + } + + public static function isAdmin(string $email): bool + { + foreach (self::records() as $record) { + if ($record['email'] === $email) { + return (bool) $record['is_admin']; + } + } + + return false; + } +} diff --git a/Modules/User/database/seeders/AuthUserSeeder.php b/Modules/User/database/seeders/AuthUserSeeder.php index bd17d1b94..61d23c9e7 100644 --- a/Modules/User/database/seeders/AuthUserSeeder.php +++ b/Modules/User/database/seeders/AuthUserSeeder.php @@ -5,29 +5,22 @@ namespace Modules\User\Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Schema; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; use Spatie\Permission\Models\Role; class AuthUserSeeder extends Seeder { public function run(): void { - $admin = User::query()->updateOrCreate( - ['email' => 'a@a.com'], - [ - 'name' => 'Admin', - 'password' => '236330', - 'status' => 'active', - ], - ); - - User::query()->updateOrCreate( - ['email' => 'b@b.com'], - [ - 'name' => 'Member', - 'password' => '236330', - 'status' => 'active', - ], - ); + $users = collect(DemoUserCatalog::records()) + ->map(fn (array $record): User => User::query()->updateOrCreate( + ['email' => $record['email']], + [ + 'name' => $record['name'], + 'password' => $record['password'], + 'status' => 'active', + ], + )); if (! class_exists(Role::class) || ! Schema::hasTable((new Role())->getTable())) { return; @@ -38,6 +31,14 @@ class AuthUserSeeder extends Seeder 'guard_name' => 'web', ]); - $admin->syncRoles([$adminRole->name]); + $users->each(function (User $user) use ($adminRole): void { + if (DemoUserCatalog::isAdmin($user->email)) { + $user->syncRoles([$adminRole->name]); + + return; + } + + $user->syncRoles([]); + }); } } diff --git a/Modules/Video/database/seeders/VideoDemoSeeder.php b/Modules/Video/database/seeders/VideoDemoSeeder.php index b8b43d6e5..b52bdd45f 100644 --- a/Modules/Video/database/seeders/VideoDemoSeeder.php +++ b/Modules/Video/database/seeders/VideoDemoSeeder.php @@ -6,72 +6,34 @@ use Illuminate\Database\Seeder; use Illuminate\Support\Facades\Schema; use Modules\Listing\Models\Listing; use Modules\User\App\Models\User; +use Modules\User\App\Support\DemoUserCatalog; use Modules\Video\Enums\VideoStatus; use Modules\Video\Models\Video; class VideoDemoSeeder extends Seeder { - private const VIDEO_BLUEPRINTS = [ - 'a@a.com' => [ - [ - 'title' => 'Workspace walkaround', - 'description' => 'Pending demo video for upload and processing states.', - 'status' => VideoStatus::Pending, - 'is_active' => true, - 'processing_error' => null, - ], - [ - 'title' => 'Packaging close-up', - 'description' => 'Failed demo video to test retry and edit flows.', - 'status' => VideoStatus::Failed, - 'is_active' => false, - 'processing_error' => 'Demo processing was skipped intentionally.', - ], - ], - 'b@b.com' => [ - [ - 'title' => 'Short product overview', - 'description' => 'Pending demo video for the member workspace.', - 'status' => VideoStatus::Pending, - 'is_active' => true, - 'processing_error' => null, - ], - [ - 'title' => 'Condition details clip', - 'description' => 'Failed demo video to show a second panel state.', - 'status' => VideoStatus::Failed, - 'is_active' => false, - 'processing_error' => 'Demo processing was skipped intentionally.', - ], - ], - ]; - public function run(): void { if (! Schema::hasTable('videos') || ! Schema::hasTable('listings')) { return; } - foreach (self::VIDEO_BLUEPRINTS as $email => $blueprints) { - $user = User::query()->where('email', $email)->first(); - - if (! $user) { - continue; - } + $users = User::query() + ->whereIn('email', DemoUserCatalog::emails()) + ->orderBy('email') + ->get() + ->values(); + foreach ($users as $userIndex => $user) { $listings = Listing::query() ->where('user_id', $user->getKey()) ->where('status', 'active') ->orderBy('id') - ->take(count($blueprints)) + ->take(2) ->get(); - foreach ($blueprints as $index => $blueprint) { - $listing = $listings->get($index); - - if (! $listing) { - continue; - } + foreach ($listings as $listingIndex => $listing) { + $blueprint = $this->blueprintFor($userIndex, $listingIndex); $video = Video::query()->firstOrNew([ 'listing_id' => $listing->getKey(), @@ -91,7 +53,7 @@ class VideoDemoSeeder extends Seeder 'upload_path' => null, 'mime_type' => 'video/mp4', 'size' => null, - 'sort_order' => $index + 1, + 'sort_order' => $listingIndex + 1, 'is_active' => $blueprint['is_active'], 'processing_error' => $blueprint['processing_error'], 'processed_at' => null, @@ -99,4 +61,25 @@ class VideoDemoSeeder extends Seeder } } } + + private function blueprintFor(int $userIndex, int $listingIndex): array + { + if ($listingIndex === 0) { + return [ + 'title' => 'Quick walkthrough '.($userIndex + 1), + 'description' => 'Pending demo video for uploader and panel testing.', + 'status' => VideoStatus::Pending, + 'is_active' => true, + 'processing_error' => null, + ]; + } + + return [ + 'title' => 'Condition details '.($userIndex + 1), + 'description' => 'Failed demo video for status handling and retry UI testing.', + 'status' => VideoStatus::Failed, + 'is_active' => false, + 'processing_error' => 'Demo processing was skipped intentionally.', + ]; + } } diff --git a/public/sample_image/ watch_band.jpg b/public/sample_image/ watch_band.jpg new file mode 100644 index 000000000..6403f940c Binary files /dev/null and b/public/sample_image/ watch_band.jpg differ diff --git a/public/sample_image/business white career hiring recruitment academic jobs.jpg b/public/sample_image/business white career hiring recruitment academic jobs.jpg new file mode 100644 index 000000000..c0566492f Binary files /dev/null and b/public/sample_image/business white career hiring recruitment academic jobs.jpg differ diff --git a/public/sample_image/fashion natural wedding product shoes.jpg b/public/sample_image/fashion natural wedding product shoes.jpg new file mode 100644 index 000000000..bc493c530 Binary files /dev/null and b/public/sample_image/fashion natural wedding product shoes.jpg differ diff --git a/public/sample_image/grey product photography hat sustainable fashion beanie ethical fashion ambleside .jpg b/public/sample_image/grey product photography hat sustainable fashion beanie ethical fashion ambleside .jpg new file mode 100644 index 000000000..dda02c393 Binary files /dev/null and b/public/sample_image/grey product photography hat sustainable fashion beanie ethical fashion ambleside .jpg differ diff --git a/public/sample_image/house interior design home interior bedroom.jpg b/public/sample_image/house interior design home interior bedroom.jpg new file mode 100644 index 000000000..7001ed052 Binary files /dev/null and b/public/sample_image/house interior design home interior bedroom.jpg differ diff --git a/public/sample_image/jobs.jpg b/public/sample_image/jobs.jpg new file mode 100644 index 000000000..d809db289 Binary files /dev/null and b/public/sample_image/jobs.jpg differ diff --git a/public/sample_image/nike-sport-wear.png b/public/sample_image/nike-sport-wear.png new file mode 100644 index 000000000..5c538f732 Binary files /dev/null and b/public/sample_image/nike-sport-wear.png differ diff --git a/public/sample_image/office building black laptop programming grey interior desk men .jpg b/public/sample_image/office building black laptop programming grey interior desk men .jpg new file mode 100644 index 000000000..af761c34f Binary files /dev/null and b/public/sample_image/office building black laptop programming grey interior desk men .jpg differ diff --git a/public/sample_image/office business people laptop work team classroom grey teamwork table.jpg b/public/sample_image/office business people laptop work team classroom grey teamwork table.jpg new file mode 100644 index 000000000..60034f99f Binary files /dev/null and b/public/sample_image/office business people laptop work team classroom grey teamwork table.jpg differ diff --git a/public/sample_image/office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg b/public/sample_image/office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg new file mode 100644 index 000000000..e339e97e2 Binary files /dev/null and b/public/sample_image/office business technology meeting coding grey engineering engineer software engineer professional woman whiteboard tutor.jpg differ diff --git a/public/sample_image/office business work team white customer service studio office building.jpg b/public/sample_image/office business work team white customer service studio office building.jpg new file mode 100644 index 000000000..abbbcc7bb Binary files /dev/null and b/public/sample_image/office business work team white customer service studio office building.jpg differ diff --git a/public/sample_image/roof large house fence gate.jpg b/public/sample_image/roof large house fence gate.jpg new file mode 100644 index 000000000..e90a2352a Binary files /dev/null and b/public/sample_image/roof large house fence gate.jpg differ diff --git a/public/sample_image/smart-watch.jpg b/public/sample_image/smart-watch.jpg new file mode 100644 index 000000000..87a2d5154 Binary files /dev/null and b/public/sample_image/smart-watch.jpg differ diff --git a/public/sample_image/sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg b/public/sample_image/sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg new file mode 100644 index 000000000..b2072f758 Binary files /dev/null and b/public/sample_image/sport motorbike product photography products moto sports bike enduro motorsports clothing.jpg differ diff --git a/public/sample_image/sportscars car sports car vehicle.jpg b/public/sample_image/sportscars car sports car vehicle.jpg new file mode 100644 index 000000000..576dde886 Binary files /dev/null and b/public/sample_image/sportscars car sports car vehicle.jpg differ diff --git a/public/sample_image/sunglasses.jpg b/public/sample_image/sunglasses.jpg new file mode 100644 index 000000000..70b6de03c Binary files /dev/null and b/public/sample_image/sunglasses.jpg differ diff --git a/public/sample_image/tech product macbook digital image render macbook pro.jpg b/public/sample_image/tech product macbook digital image render macbook pro.jpg new file mode 100644 index 000000000..0f3b34f92 Binary files /dev/null and b/public/sample_image/tech product macbook digital image render macbook pro.jpg differ diff --git a/public/sample_image/vintage red text retro machine sign blur bokeh flag hiring.jpg b/public/sample_image/vintage red text retro machine sign blur bokeh flag hiring.jpg new file mode 100644 index 000000000..0c359b9b0 Binary files /dev/null and b/public/sample_image/vintage red text retro machine sign blur bokeh flag hiring.jpg differ