Listing details
-Everything important, laid out cleanly.
-Description
-Seller notes, condition, and extra context.
-diff --git a/Modules/Category/Database/Seeders/CategorySeeder.php b/Modules/Category/Database/Seeders/CategorySeeder.php index f81af9e90..f929c1b91 100644 --- a/Modules/Category/Database/Seeders/CategorySeeder.php +++ b/Modules/Category/Database/Seeders/CategorySeeder.php @@ -13,7 +13,7 @@ class CategorySeeder extends Seeder ['name' => 'Vehicles', 'slug' => 'vehicles', 'icon' => 'img/category/car.png', 'children' => ['Cars', 'Motorcycles', 'Trucks', 'Boats']], ['name' => 'Real Estate', 'slug' => 'real-estate', 'icon' => 'img/category/home_garden.png', 'children' => ['For Sale', 'For Rent', 'Commercial']], ['name' => 'Fashion', 'slug' => 'fashion', 'icon' => 'img/category/phone.png', 'children' => ['Men', 'Women', 'Kids', 'Shoes']], - ['name' => 'Home & Garden', 'slug' => 'home-garden', 'icon' => 'img/category/home_tools.png', 'children' => ['Furniture', 'Garden', 'Appliances']], + ['name' => 'Home', 'slug' => 'home-garden', 'icon' => 'img/category/home_tools.png', 'children' => ['Furniture', 'Garden', 'Appliances']], ['name' => 'Sports', 'slug' => 'sports', 'icon' => 'img/category/sports.png', 'children' => ['Outdoor', 'Fitness', 'Team Sports']], ['name' => 'Jobs', 'slug' => 'jobs', 'icon' => 'img/category/education.png', 'children' => ['Full Time', 'Part Time', 'Freelance']], ['name' => 'Services', 'slug' => 'services', 'icon' => 'img/category/home_tools.png', 'children' => ['Cleaning', 'Repair', 'Education']], diff --git a/Modules/Conversation/App/Models/Conversation.php b/Modules/Conversation/App/Models/Conversation.php index 987baec96..8b355a8ca 100644 --- a/Modules/Conversation/App/Models/Conversation.php +++ b/Modules/Conversation/App/Models/Conversation.php @@ -165,4 +165,15 @@ class Conversation extends Model return is_null($value) ? null : (int) $value; } + + public static function unreadCountForUser(int $userId): int + { + return (int) ConversationMessage::query() + ->whereHas('conversation', function (Builder $query) use ($userId): void { + $query->forUser($userId); + }) + ->where('sender_id', '!=', $userId) + ->whereNull('read_at') + ->count(); + } } diff --git a/Modules/Conversation/database/seeders/ConversationDemoSeeder.php b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php index e9e6f7cbb..a8b2cd53f 100644 --- a/Modules/Conversation/database/seeders/ConversationDemoSeeder.php +++ b/Modules/Conversation/database/seeders/ConversationDemoSeeder.php @@ -19,41 +19,69 @@ class ConversationDemoSeeder extends Seeder } $admin = User::query()->where('email', 'a@a.com')->first(); - $partner = User::query()->where('email', 'b@b.com')->first(); + $member = User::query()->where('email', 'b@b.com')->first(); - if (! $admin || ! $partner) { + if (! $admin || ! $member) { return; } - $listings = Listing::query() + $adminListings = Listing::query() ->where('user_id', $admin->getKey()) ->where('status', 'active') ->orderBy('id') ->take(2) ->get(); - if ($listings->count() < 2) { + $memberListings = Listing::query() + ->where('user_id', $member->getKey()) + ->where('status', 'active') + ->orderBy('id') + ->take(2) + ->get(); + + if ($adminListings->count() < 2 || $memberListings->count() < 2) { return; } $this->seedConversationThread( - $listings->get(0), $admin, - $partner, + $member, + $adminListings->get(0), [ - ['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], + ['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( - $listings->get(1), $admin, - $partner, + $member, + $adminListings->get(1), [ - ['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], + ['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], ] ); } @@ -64,9 +92,9 @@ class ConversationDemoSeeder extends Seeder } private function seedConversationThread( + User $seller, + User $buyer, ?Listing $listing, - User $admin, - User $partner, array $messages ): void { if (! $listing) { @@ -76,10 +104,10 @@ class ConversationDemoSeeder extends Seeder $conversation = Conversation::updateOrCreate( [ 'listing_id' => $listing->getKey(), - 'buyer_id' => $partner->getKey(), + 'buyer_id' => $buyer->getKey(), ], [ - 'seller_id' => $admin->getKey(), + 'seller_id' => $seller->getKey(), 'last_message_at' => now(), ] ); @@ -92,7 +120,7 @@ class ConversationDemoSeeder extends Seeder foreach ($messages as $payload) { $createdAt = now()->subHours((int) $payload['hours_ago']); - $sender = ($payload['sender'] ?? 'partner') === 'admin' ? $admin : $partner; + $sender = ($payload['sender'] ?? 'buyer') === 'seller' ? $seller : $buyer; $readAfterMinutes = $payload['read_after_minutes']; $readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null; @@ -110,7 +138,7 @@ class ConversationDemoSeeder extends Seeder } $conversation->forceFill([ - 'seller_id' => $admin->getKey(), + 'seller_id' => $seller->getKey(), 'last_message_at' => $lastMessageAt, 'updated_at' => $lastMessageAt, ])->saveQuietly(); diff --git a/Modules/Demo/database/Seeders/DemoContentSeeder.php b/Modules/Demo/database/Seeders/DemoContentSeeder.php index cb991dd69..b1d6adf32 100644 --- a/Modules/Demo/database/Seeders/DemoContentSeeder.php +++ b/Modules/Demo/database/Seeders/DemoContentSeeder.php @@ -9,10 +9,7 @@ class DemoContentSeeder extends Seeder public function run(): void { $this->call([ - \Modules\User\Database\Seeders\AuthUserSeeder::class, - \Modules\Listing\Database\Seeders\ListingPanelDemoSeeder::class, - \Modules\Favorite\Database\Seeders\FavoriteDemoSeeder::class, - \Modules\Conversation\Database\Seeders\ConversationDemoSeeder::class, + \Modules\User\Database\Seeders\UserWorkspaceSeeder::class, ]); } } diff --git a/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php index 0a73e3acc..b5f421e00 100644 --- a/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php +++ b/Modules/Favorite/database/seeders/FavoriteDemoSeeder.php @@ -20,9 +20,9 @@ class FavoriteDemoSeeder extends Seeder } $admin = User::query()->where('email', 'a@a.com')->first(); - $partner = User::query()->where('email', 'b@b.com')->first(); + $member = User::query()->where('email', 'b@b.com')->first(); - if (! $admin || ! $partner) { + if (! $admin || ! $member) { return; } @@ -32,26 +32,33 @@ class FavoriteDemoSeeder extends Seeder ->orderBy('id') ->get(); - if ($adminListings->isEmpty()) { + $memberListings = Listing::query() + ->where('user_id', $member->getKey()) + ->orderByDesc('is_featured') + ->orderBy('id') + ->get(); + + if ($adminListings->isEmpty() || $memberListings->isEmpty()) { return; } $activeAdminListings = $adminListings->where('status', 'active')->values(); + $activeMemberListings = $memberListings->where('status', 'active')->values(); $this->seedFavoriteListings( - $partner, + $member, $activeAdminListings->take(6) ); $this->seedFavoriteListings( $admin, - $adminListings->take(3)->values() + $activeMemberListings->take(4) ); - $this->seedFavoriteSeller($partner, $admin, now()->subDays(2)); - $this->seedFavoriteSeller($admin, $partner, now()->subDays(1)); + $this->seedFavoriteSeller($member, $admin, now()->subDays(2)); + $this->seedFavoriteSeller($admin, $member, now()->subDays(1)); - $this->seedFavoriteSearches($partner, $this->partnerSearchPayloads()); + $this->seedFavoriteSearches($member, $this->memberSearchPayloads()); $this->seedFavoriteSearches($admin, $this->adminSearchPayloads()); } @@ -147,27 +154,31 @@ class FavoriteDemoSeeder extends Seeder } } - private function partnerSearchPayloads(): array + private function memberSearchPayloads(): 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'); + $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], ]; } private function adminSearchPayloads(): array { - $fashionId = Category::query()->where('name', 'Fashion')->value('id'); - $homeGardenId = Category::query()->where('name', 'Home & Garden')->value('id'); + $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 [ ['search' => 'vintage', 'category_id' => $fashionId], ['search' => 'garden', 'category_id' => $homeGardenId], + ['search' => 'fitness', 'category_id' => $sportsId], ]; } } diff --git a/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php index 46cfaa747..712da266b 100644 --- a/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingPanelDemoSeeder.php @@ -11,48 +11,100 @@ use Modules\User\App\Models\User; class ListingPanelDemoSeeder extends Seeder { - private const PANEL_LISTINGS = [ - [ - 'slug' => '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, + 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, + ], ], - [ - '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, + '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, + ], ], ]; public function run(): void { - $admin = $this->resolveAdminUser(); + $admin = $this->resolveUserByEmail('a@a.com'); if (! $admin) { return; @@ -66,42 +118,48 @@ class ListingPanelDemoSeeder extends Seeder return; } - foreach (self::PANEL_LISTINGS as $index => $payload) { - $category = $categories->get($index % $categories->count()); + foreach (self::USER_PANEL_LISTINGS as $email => $payloads) { + $owner = $this->resolveUserByEmail($email); - if (! $category instanceof Category) { + if (! $owner) { 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']), - ] - ); + foreach ($payloads as $index => $payload) { + $category = $categories->get($index % $categories->count()); - $this->syncListingImage($listing, (string) $payload['image']); + 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']); + } } } - private function resolveAdminUser(): ?User + private function resolveUserByEmail(string $email): ?User { - return User::query()->where('email', 'a@a.com')->first() - ?? User::query()->whereHas('roles', fn ($query) => $query->where('name', 'admin'))->first() - ?? User::query()->first(); + return User::query()->where('email', $email)->first(); } private function claimAllListingsForAdmin(User $admin): void diff --git a/Modules/Listing/Database/Seeders/ListingSeeder.php b/Modules/Listing/Database/Seeders/ListingSeeder.php index ea7d84f50..85b945c88 100644 --- a/Modules/Listing/Database/Seeders/ListingSeeder.php +++ b/Modules/Listing/Database/Seeders/ListingSeeder.php @@ -14,16 +14,6 @@ use Modules\User\App\Models\User; class ListingSeeder extends Seeder { - private const SAMPLE_IMAGES = [ - 'sample_image/phone.jpeg', - 'sample_image/macbook.jpg', - 'sample_image/car.jpeg', - 'sample_image/headphones.jpg', - 'sample_image/laptop.jpg', - 'sample_image/cup.jpg', - 'sample_image/car2.jpeg', - ]; - private const TITLE_PREFIXES = [ 'Clean', 'Lightly used', @@ -132,7 +122,6 @@ class ListingSeeder extends Seeder Collection $turkeyCities ): array { $location = $this->resolveLocation($index, $countries, $turkeyCities); - $image = self::SAMPLE_IMAGES[$index % count(self::SAMPLE_IMAGES)]; return [ 'title' => $this->buildTitle($category, $index), @@ -140,7 +129,7 @@ class ListingSeeder extends Seeder 'price' => $this->priceForIndex($index), 'city' => $location['city'], 'country' => $location['country'], - 'image' => $image, + 'image' => null, ]; } @@ -238,8 +227,14 @@ class ListingSeeder extends Seeder ); } - private function syncListingImage(Listing $listing, string $imageRelativePath): void + private function syncListingImage(Listing $listing, ?string $imageRelativePath): void { + if (blank($imageRelativePath)) { + $listing->clearMediaCollection('listing-images'); + + return; + } + $imageAbsolutePath = public_path($imageRelativePath); if (! is_file($imageAbsolutePath)) { diff --git a/Modules/Listing/resources/views/themes/otoplus/show.blade.php b/Modules/Listing/resources/views/themes/otoplus/show.blade.php index e2930b6f2..01c63e4fe 100644 --- a/Modules/Listing/resources/views/themes/otoplus/show.blade.php +++ b/Modules/Listing/resources/views/themes/otoplus/show.blade.php @@ -61,12 +61,11 @@ $reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode); $shareUrl = route('listings.show', $listing); - $specRows = collect([ - ['label' => 'Price', 'value' => $priceLabel], - ['label' => 'Published', 'value' => $publishedAt], + $detailRows = collect([ ['label' => 'Listing ID', 'value' => $referenceCode], + ['label' => 'Published', 'value' => $publishedAt], ['label' => 'Category', 'value' => $listing->category?->name ?? 'General'], - ['label' => 'Location', 'value' => $locationLabel !== '' ? str_replace(' / ', ' / ', $locationLabel) : 'Not specified'], + ['label' => 'Location', 'value' => $locationLabel !== '' ? $locationLabel : 'Not specified'], ]) ->merge( collect($presentableCustomFields ?? [])->map(fn (array $field) => [ @@ -77,6 +76,9 @@ ->filter(fn (array $item) => $item['label'] !== '' && $item['value'] !== '') ->unique(fn (array $item) => mb_strtolower($item['label'])) ->values(); + $summaryRows = $detailRows->take(8); + $sellerListingsUrl = $listing->user ? route('listings.index', ['user' => $listing->user->getKey()]) : route('listings.index'); + $locationText = $locationLabel !== '' ? $locationLabel : 'Location not specified'; @endphp
{{ $listing->category?->name ?? 'Marketplace listing' }}
-{{ $referenceCode }}
+Everything important, laid out cleanly.
-Seller notes, condition, and extra context.
-