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