mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Refactor home layout and seed data
This commit is contained in:
parent
46b70a91f7
commit
8c0365e710
@ -13,7 +13,7 @@ class CategorySeeder extends Seeder
|
|||||||
['name' => 'Vehicles', 'slug' => 'vehicles', 'icon' => 'img/category/car.png', 'children' => ['Cars', 'Motorcycles', 'Trucks', 'Boats']],
|
['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' => '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' => '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' => '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' => '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']],
|
['name' => 'Services', 'slug' => 'services', 'icon' => 'img/category/home_tools.png', 'children' => ['Cleaning', 'Repair', 'Education']],
|
||||||
|
|||||||
@ -165,4 +165,15 @@ class Conversation extends Model
|
|||||||
|
|
||||||
return is_null($value) ? null : (int) $value;
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,41 +19,69 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$admin = User::query()->where('email', 'a@a.com')->first();
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$listings = Listing::query()
|
$adminListings = Listing::query()
|
||||||
->where('user_id', $admin->getKey())
|
->where('user_id', $admin->getKey())
|
||||||
->where('status', 'active')
|
->where('status', 'active')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->take(2)
|
->take(2)
|
||||||
->get();
|
->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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->seedConversationThread(
|
$this->seedConversationThread(
|
||||||
$listings->get(0),
|
|
||||||
$admin,
|
$admin,
|
||||||
$partner,
|
$member,
|
||||||
|
$adminListings->get(0),
|
||||||
[
|
[
|
||||||
['sender' => 'partner', 'body' => 'Hi, is this still available?', 'hours_ago' => 30, 'read_after_minutes' => 4],
|
['sender' => 'buyer', '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' => 'seller', 'body' => 'Yes, it is. 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' => 'Perfect. Can we meet tomorrow afternoon?', 'hours_ago' => 4, 'read_after_minutes' => null],
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->seedConversationThread(
|
$this->seedConversationThread(
|
||||||
$listings->get(1),
|
|
||||||
$admin,
|
$admin,
|
||||||
$partner,
|
$member,
|
||||||
|
$adminListings->get(1),
|
||||||
[
|
[
|
||||||
['sender' => 'partner', 'body' => 'Can you confirm the final price?', 'hours_ago' => 20, 'read_after_minutes' => 8],
|
['sender' => 'buyer', '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' => '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(
|
private function seedConversationThread(
|
||||||
|
User $seller,
|
||||||
|
User $buyer,
|
||||||
?Listing $listing,
|
?Listing $listing,
|
||||||
User $admin,
|
|
||||||
User $partner,
|
|
||||||
array $messages
|
array $messages
|
||||||
): void {
|
): void {
|
||||||
if (! $listing) {
|
if (! $listing) {
|
||||||
@ -76,10 +104,10 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
$conversation = Conversation::updateOrCreate(
|
$conversation = Conversation::updateOrCreate(
|
||||||
[
|
[
|
||||||
'listing_id' => $listing->getKey(),
|
'listing_id' => $listing->getKey(),
|
||||||
'buyer_id' => $partner->getKey(),
|
'buyer_id' => $buyer->getKey(),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'seller_id' => $admin->getKey(),
|
'seller_id' => $seller->getKey(),
|
||||||
'last_message_at' => now(),
|
'last_message_at' => now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -92,7 +120,7 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
|
|
||||||
foreach ($messages as $payload) {
|
foreach ($messages as $payload) {
|
||||||
$createdAt = now()->subHours((int) $payload['hours_ago']);
|
$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'];
|
$readAfterMinutes = $payload['read_after_minutes'];
|
||||||
$readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null;
|
$readAt = is_numeric($readAfterMinutes) ? $createdAt->copy()->addMinutes((int) $readAfterMinutes) : null;
|
||||||
|
|
||||||
@ -110,7 +138,7 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$conversation->forceFill([
|
$conversation->forceFill([
|
||||||
'seller_id' => $admin->getKey(),
|
'seller_id' => $seller->getKey(),
|
||||||
'last_message_at' => $lastMessageAt,
|
'last_message_at' => $lastMessageAt,
|
||||||
'updated_at' => $lastMessageAt,
|
'updated_at' => $lastMessageAt,
|
||||||
])->saveQuietly();
|
])->saveQuietly();
|
||||||
|
|||||||
@ -9,10 +9,7 @@ class DemoContentSeeder extends Seeder
|
|||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call([
|
$this->call([
|
||||||
\Modules\User\Database\Seeders\AuthUserSeeder::class,
|
\Modules\User\Database\Seeders\UserWorkspaceSeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingPanelDemoSeeder::class,
|
|
||||||
\Modules\Favorite\Database\Seeders\FavoriteDemoSeeder::class,
|
|
||||||
\Modules\Conversation\Database\Seeders\ConversationDemoSeeder::class,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,9 +20,9 @@ class FavoriteDemoSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$admin = User::query()->where('email', 'a@a.com')->first();
|
$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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,26 +32,33 @@ class FavoriteDemoSeeder extends Seeder
|
|||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($adminListings->isEmpty()) {
|
$memberListings = Listing::query()
|
||||||
|
->where('user_id', $member->getKey())
|
||||||
|
->orderByDesc('is_featured')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
if ($adminListings->isEmpty() || $memberListings->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$activeAdminListings = $adminListings->where('status', 'active')->values();
|
$activeAdminListings = $adminListings->where('status', 'active')->values();
|
||||||
|
$activeMemberListings = $memberListings->where('status', 'active')->values();
|
||||||
|
|
||||||
$this->seedFavoriteListings(
|
$this->seedFavoriteListings(
|
||||||
$partner,
|
$member,
|
||||||
$activeAdminListings->take(6)
|
$activeAdminListings->take(6)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->seedFavoriteListings(
|
$this->seedFavoriteListings(
|
||||||
$admin,
|
$admin,
|
||||||
$adminListings->take(3)->values()
|
$activeMemberListings->take(4)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->seedFavoriteSeller($partner, $admin, now()->subDays(2));
|
$this->seedFavoriteSeller($member, $admin, now()->subDays(2));
|
||||||
$this->seedFavoriteSeller($admin, $partner, now()->subDays(1));
|
$this->seedFavoriteSeller($admin, $member, now()->subDays(1));
|
||||||
|
|
||||||
$this->seedFavoriteSearches($partner, $this->partnerSearchPayloads());
|
$this->seedFavoriteSearches($member, $this->memberSearchPayloads());
|
||||||
$this->seedFavoriteSearches($admin, $this->adminSearchPayloads());
|
$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');
|
$electronicsId = Category::query()->where('slug', 'electronics')->value('id');
|
||||||
$vehiclesId = Category::query()->where('name', 'Vehicles')->value('id');
|
$vehiclesId = Category::query()->where('slug', 'vehicles')->value('id');
|
||||||
$realEstateId = Category::query()->where('name', 'Real Estate')->value('id');
|
$realEstateId = Category::query()->where('slug', 'real-estate')->value('id');
|
||||||
|
$servicesId = Category::query()->where('slug', 'services')->value('id');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
['search' => 'iphone', 'category_id' => $electronicsId],
|
['search' => 'iphone', 'category_id' => $electronicsId],
|
||||||
['search' => 'sedan', 'category_id' => $vehiclesId],
|
['search' => 'sedan', 'category_id' => $vehiclesId],
|
||||||
['search' => 'apartment', 'category_id' => $realEstateId],
|
['search' => 'apartment', 'category_id' => $realEstateId],
|
||||||
|
['search' => 'repair', 'category_id' => $servicesId],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function adminSearchPayloads(): array
|
private function adminSearchPayloads(): array
|
||||||
{
|
{
|
||||||
$fashionId = Category::query()->where('name', 'Fashion')->value('id');
|
$fashionId = Category::query()->where('slug', 'fashion')->value('id');
|
||||||
$homeGardenId = Category::query()->where('name', 'Home & Garden')->value('id');
|
$homeGardenId = Category::query()->where('slug', 'home-garden')->value('id');
|
||||||
|
$sportsId = Category::query()->where('slug', 'sports')->value('id');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
['search' => 'vintage', 'category_id' => $fashionId],
|
['search' => 'vintage', 'category_id' => $fashionId],
|
||||||
['search' => 'garden', 'category_id' => $homeGardenId],
|
['search' => 'garden', 'category_id' => $homeGardenId],
|
||||||
|
['search' => 'fitness', 'category_id' => $sportsId],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,48 +11,100 @@ use Modules\User\App\Models\User;
|
|||||||
|
|
||||||
class ListingPanelDemoSeeder extends Seeder
|
class ListingPanelDemoSeeder extends Seeder
|
||||||
{
|
{
|
||||||
private const PANEL_LISTINGS = [
|
private const USER_PANEL_LISTINGS = [
|
||||||
|
'a@a.com' => [
|
||||||
[
|
[
|
||||||
'slug' => 'admin-demo-sold-camera',
|
'slug' => 'admin-demo-pro-workstation',
|
||||||
'title' => 'Admin Demo Camera Bundle',
|
'title' => 'Admin Demo Pro Workstation',
|
||||||
'description' => 'Sample sold listing for the panel filters and activity cards.',
|
'description' => 'Active demo listing for inbox, favorites, and video testing.',
|
||||||
'price' => 18450,
|
'price' => 28450,
|
||||||
'status' => 'sold',
|
'status' => 'active',
|
||||||
'city' => 'Istanbul',
|
'city' => 'Istanbul',
|
||||||
'country' => 'Turkey',
|
'country' => 'Turkey',
|
||||||
'image' => 'sample_image/macbook.jpg',
|
'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,
|
'expires_offset_days' => 12,
|
||||||
'is_featured' => false,
|
'is_featured' => false,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'slug' => 'admin-demo-expired-sofa',
|
'slug' => 'admin-demo-expired-sofa',
|
||||||
'title' => 'Admin Demo Sofa Set',
|
'title' => 'Admin Demo Sofa Set',
|
||||||
'description' => 'Sample expired listing for the panel filters and republish flow.',
|
'description' => 'Expired demo listing for republish testing.',
|
||||||
'price' => 9800,
|
'price' => 9800,
|
||||||
'status' => 'expired',
|
'status' => 'expired',
|
||||||
'city' => 'Ankara',
|
'city' => 'Izmir',
|
||||||
'country' => 'Turkey',
|
'country' => 'Turkey',
|
||||||
'image' => 'sample_image/cup.jpg',
|
'image' => 'sample_image/cup.jpg',
|
||||||
'expires_offset_days' => -5,
|
'expires_offset_days' => -5,
|
||||||
'is_featured' => false,
|
'is_featured' => false,
|
||||||
],
|
],
|
||||||
|
],
|
||||||
|
'b@b.com' => [
|
||||||
[
|
[
|
||||||
'slug' => 'admin-demo-expired-bike',
|
'slug' => 'member-demo-iphone',
|
||||||
'title' => 'Admin Demo City Bike',
|
'title' => 'Member Demo iPhone Bundle',
|
||||||
'description' => 'Extra expired sample listing so My Listings is not empty in filtered views.',
|
'description' => 'Active demo listing owned by the member account.',
|
||||||
'price' => 6200,
|
'price' => 21990,
|
||||||
'status' => 'expired',
|
'status' => 'active',
|
||||||
'city' => 'Izmir',
|
'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',
|
'country' => 'Turkey',
|
||||||
'image' => 'sample_image/car2.jpeg',
|
'image' => 'sample_image/car2.jpeg',
|
||||||
'expires_offset_days' => -11,
|
'expires_offset_days' => 17,
|
||||||
'is_featured' => false,
|
'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
|
public function run(): void
|
||||||
{
|
{
|
||||||
$admin = $this->resolveAdminUser();
|
$admin = $this->resolveUserByEmail('a@a.com');
|
||||||
|
|
||||||
if (! $admin) {
|
if (! $admin) {
|
||||||
return;
|
return;
|
||||||
@ -66,7 +118,14 @@ class ListingPanelDemoSeeder extends Seeder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::PANEL_LISTINGS as $index => $payload) {
|
foreach (self::USER_PANEL_LISTINGS as $email => $payloads) {
|
||||||
|
$owner = $this->resolveUserByEmail($email);
|
||||||
|
|
||||||
|
if (! $owner) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($payloads as $index => $payload) {
|
||||||
$category = $categories->get($index % $categories->count());
|
$category = $categories->get($index % $categories->count());
|
||||||
|
|
||||||
if (! $category instanceof Category) {
|
if (! $category instanceof Category) {
|
||||||
@ -84,10 +143,10 @@ class ListingPanelDemoSeeder extends Seeder
|
|||||||
'city' => $payload['city'],
|
'city' => $payload['city'],
|
||||||
'country' => $payload['country'],
|
'country' => $payload['country'],
|
||||||
'category_id' => $category->getKey(),
|
'category_id' => $category->getKey(),
|
||||||
'user_id' => $admin->getKey(),
|
'user_id' => $owner->getKey(),
|
||||||
'status' => $payload['status'],
|
'status' => $payload['status'],
|
||||||
'contact_email' => $admin->email,
|
'contact_email' => $owner->email,
|
||||||
'contact_phone' => '+905551112233',
|
'contact_phone' => $email === 'a@a.com' ? '+905551112233' : '+905551112244',
|
||||||
'is_featured' => $payload['is_featured'],
|
'is_featured' => $payload['is_featured'],
|
||||||
'expires_at' => now()->addDays((int) $payload['expires_offset_days']),
|
'expires_at' => now()->addDays((int) $payload['expires_offset_days']),
|
||||||
]
|
]
|
||||||
@ -96,12 +155,11 @@ class ListingPanelDemoSeeder extends Seeder
|
|||||||
$this->syncListingImage($listing, (string) $payload['image']);
|
$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()
|
return User::query()->where('email', $email)->first();
|
||||||
?? User::query()->whereHas('roles', fn ($query) => $query->where('name', 'admin'))->first()
|
|
||||||
?? User::query()->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function claimAllListingsForAdmin(User $admin): void
|
private function claimAllListingsForAdmin(User $admin): void
|
||||||
|
|||||||
@ -14,16 +14,6 @@ use Modules\User\App\Models\User;
|
|||||||
|
|
||||||
class ListingSeeder extends Seeder
|
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 = [
|
private const TITLE_PREFIXES = [
|
||||||
'Clean',
|
'Clean',
|
||||||
'Lightly used',
|
'Lightly used',
|
||||||
@ -132,7 +122,6 @@ class ListingSeeder extends Seeder
|
|||||||
Collection $turkeyCities
|
Collection $turkeyCities
|
||||||
): array {
|
): array {
|
||||||
$location = $this->resolveLocation($index, $countries, $turkeyCities);
|
$location = $this->resolveLocation($index, $countries, $turkeyCities);
|
||||||
$image = self::SAMPLE_IMAGES[$index % count(self::SAMPLE_IMAGES)];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'title' => $this->buildTitle($category, $index),
|
'title' => $this->buildTitle($category, $index),
|
||||||
@ -140,7 +129,7 @@ class ListingSeeder extends Seeder
|
|||||||
'price' => $this->priceForIndex($index),
|
'price' => $this->priceForIndex($index),
|
||||||
'city' => $location['city'],
|
'city' => $location['city'],
|
||||||
'country' => $location['country'],
|
'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);
|
$imageAbsolutePath = public_path($imageRelativePath);
|
||||||
|
|
||||||
if (! is_file($imageAbsolutePath)) {
|
if (! is_file($imageAbsolutePath)) {
|
||||||
|
|||||||
@ -61,12 +61,11 @@
|
|||||||
$reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode);
|
$reportUrl = 'mailto:'.$reportEmail.'?subject='.rawurlencode('Report listing '.$referenceCode);
|
||||||
$shareUrl = route('listings.show', $listing);
|
$shareUrl = route('listings.show', $listing);
|
||||||
|
|
||||||
$specRows = collect([
|
$detailRows = collect([
|
||||||
['label' => 'Price', 'value' => $priceLabel],
|
|
||||||
['label' => 'Published', 'value' => $publishedAt],
|
|
||||||
['label' => 'Listing ID', 'value' => $referenceCode],
|
['label' => 'Listing ID', 'value' => $referenceCode],
|
||||||
|
['label' => 'Published', 'value' => $publishedAt],
|
||||||
['label' => 'Category', 'value' => $listing->category?->name ?? 'General'],
|
['label' => 'Category', 'value' => $listing->category?->name ?? 'General'],
|
||||||
['label' => 'Location', 'value' => $locationLabel !== '' ? str_replace(' / ', ' / ', $locationLabel) : 'Not specified'],
|
['label' => 'Location', 'value' => $locationLabel !== '' ? $locationLabel : 'Not specified'],
|
||||||
])
|
])
|
||||||
->merge(
|
->merge(
|
||||||
collect($presentableCustomFields ?? [])->map(fn (array $field) => [
|
collect($presentableCustomFields ?? [])->map(fn (array $field) => [
|
||||||
@ -77,6 +76,9 @@
|
|||||||
->filter(fn (array $item) => $item['label'] !== '' && $item['value'] !== '')
|
->filter(fn (array $item) => $item['label'] !== '' && $item['value'] !== '')
|
||||||
->unique(fn (array $item) => mb_strtolower($item['label']))
|
->unique(fn (array $item) => mb_strtolower($item['label']))
|
||||||
->values();
|
->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
|
@endphp
|
||||||
|
|
||||||
<div class="lt-wrap">
|
<div class="lt-wrap">
|
||||||
@ -90,28 +92,21 @@
|
|||||||
<span>{{ $displayTitle }}</span>
|
<span>{{ $displayTitle }}</span>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="lt-card lt-hero-card">
|
<section class="lt-card ld-header-card">
|
||||||
<div class="lt-hero-main">
|
<div class="ld-header-copy">
|
||||||
<div class="lt-hero-copy">
|
<p class="ld-header-ref">{{ $referenceCode }}</p>
|
||||||
<p class="lt-overline">{{ $listing->category?->name ?? 'Marketplace listing' }}</p>
|
<h1 class="ld-header-title">{{ $displayTitle }}</h1>
|
||||||
<h1 class="lt-hero-title">{{ $displayTitle }}</h1>
|
<div class="ld-header-meta">
|
||||||
<div class="lt-hero-meta">
|
<span>{{ $listing->category?->name ?? 'Marketplace listing' }}</span>
|
||||||
<span>{{ $referenceCode }}</span>
|
|
||||||
<span>{{ $sellerName }}</span>
|
<span>{{ $sellerName }}</span>
|
||||||
<span>{{ $postedAgo }}</span>
|
<span>{{ $postedAgo }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lt-hero-side">
|
<div class="ld-header-actions">
|
||||||
<div class="lt-hero-price">{{ $priceLabel }}</div>
|
|
||||||
@if($locationLabel !== '')
|
|
||||||
<div class="lt-address-chip">{{ $locationLabel }}</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="lt-hero-tools">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="lt-link-action"
|
class="ld-header-action"
|
||||||
data-listing-share
|
data-listing-share
|
||||||
data-share-url="{{ $shareUrl }}"
|
data-share-url="{{ $shareUrl }}"
|
||||||
data-share-title="{{ $displayTitle }}"
|
data-share-title="{{ $displayTitle }}"
|
||||||
@ -120,27 +115,27 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}" class="lt-inline-form">
|
<form method="POST" action="{{ route('favorites.listings.toggle', $listing) }}" class="ld-inline-form">
|
||||||
@csrf
|
@csrf
|
||||||
<button type="submit" class="lt-link-action">
|
<button type="submit" class="ld-header-action {{ $isListingFavorited ? 'is-active' : '' }}">
|
||||||
{{ $isListingFavorited ? 'Saved' : 'Save listing' }}
|
{{ $isListingFavorited ? 'Saved' : 'Save listing' }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@else
|
@else
|
||||||
<a href="{{ route('login') }}" class="lt-link-action">Save listing</a>
|
<a href="{{ route('login') }}" class="ld-header-action">Save listing</a>
|
||||||
@endauth
|
@endauth
|
||||||
</div>
|
|
||||||
</div>
|
<button type="button" class="ld-header-action" onclick="window.print()">
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="lt-grid">
|
<div class="ld-stage">
|
||||||
<div class="lt-main-column">
|
<section class="lt-card ld-gallery-card" data-gallery>
|
||||||
<div class="lt-media-spec-grid">
|
|
||||||
<section class="lt-card lt-media-card" data-gallery>
|
|
||||||
<div class="lt-gallery-main">
|
<div class="lt-gallery-main">
|
||||||
<div class="lt-gallery-top">
|
<div class="lt-gallery-top">
|
||||||
<div class="lt-gallery-spacer"></div>
|
<div class="ld-gallery-chip">Photo gallery</div>
|
||||||
|
|
||||||
<div class="lt-gallery-utility">
|
<div class="lt-gallery-utility">
|
||||||
<button
|
<button
|
||||||
@ -223,21 +218,16 @@
|
|||||||
@endif
|
@endif
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="lt-info-column">
|
<section class="lt-card ld-summary-card">
|
||||||
<section class="lt-card lt-spec-card lt-desktop-only">
|
<div class="ld-summary-head">
|
||||||
<div class="lt-card-head">
|
<div class="ld-summary-price">{{ $priceLabel }}</div>
|
||||||
<div>
|
<div class="ld-summary-date">{{ $publishedAt }}</div>
|
||||||
<h2 class="lt-section-title">Listing details</h2>
|
|
||||||
<p class="lt-section-copy">Everything important, laid out cleanly.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($locationLabel !== '')
|
<div class="ld-summary-location">{{ $locationText }}</div>
|
||||||
<div class="lt-address-chip lt-address-chip-soft">{{ $locationLabel }}</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="lt-spec-table">
|
<div class="lt-spec-table">
|
||||||
@foreach($specRows as $row)
|
@foreach($summaryRows as $row)
|
||||||
<div class="lt-spec-row">
|
<div class="lt-spec-row">
|
||||||
<span>{{ $row['label'] }}</span>
|
<span>{{ $row['label'] }}</span>
|
||||||
<strong>{{ $row['value'] }}</strong>
|
<strong>{{ $row['value'] }}</strong>
|
||||||
@ -248,32 +238,82 @@
|
|||||||
<a href="{{ $reportUrl }}" class="lt-inline-link">Report this listing</a>
|
<a href="{{ $reportUrl }}" class="lt-inline-link">Report this listing</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="lt-card lt-description-card lt-desktop-only">
|
<aside class="lt-card ld-seller-card">
|
||||||
<div class="lt-card-head">
|
<div class="lt-seller-head">
|
||||||
<div>
|
|
||||||
<h2 class="lt-section-title">Description</h2>
|
|
||||||
<p class="lt-section-copy">Seller notes, condition, and extra context.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lt-description">
|
|
||||||
{!! nl2br(e($displayDescription)) !!}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="lt-card lt-mobile-seller-card lt-mobile-only">
|
|
||||||
<div class="lt-mobile-seller-row">
|
|
||||||
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
|
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="lt-mobile-seller-name">{{ $sellerName }}</p>
|
<p class="lt-seller-kicker">Seller</p>
|
||||||
<p class="lt-mobile-seller-meta">{{ $sellerMemberText }}</p>
|
<p class="lt-seller-name">{{ $sellerName }}</p>
|
||||||
|
<div class="lt-seller-meta">{{ $sellerMemberText }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="lt-card lt-mobile-tabs lt-mobile-only" data-detail-tabs>
|
@if($listing->user)
|
||||||
|
<div class="ld-seller-links">
|
||||||
|
<a href="{{ $sellerListingsUrl }}" class="ld-seller-link">All listings</a>
|
||||||
|
@if($listing->user && ! $isOwnListing)
|
||||||
|
@auth
|
||||||
|
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="ld-inline-form">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="redirect_to" value="{{ request()->fullUrl() }}">
|
||||||
|
<button type="submit" class="ld-seller-link">
|
||||||
|
{{ $isSellerFavorited ? 'Saved seller' : 'Save seller' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
@else
|
||||||
|
<a href="{{ $loginRedirectRoute }}" class="ld-seller-link">Save seller</a>
|
||||||
|
@endauth
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(filled($listing->contact_phone) || filled($listing->contact_email))
|
||||||
|
<div class="lt-contact-panel">
|
||||||
|
@if(filled($listing->contact_phone))
|
||||||
|
<a href="tel:{{ preg_replace('/\s+/', '', (string) $listing->contact_phone) }}" class="lt-contact-primary">
|
||||||
|
{{ $listing->contact_phone }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if(filled($listing->contact_email))
|
||||||
|
<a href="mailto:{{ $listing->contact_email }}" class="lt-contact-secondary">
|
||||||
|
{{ $listing->contact_email }}
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="ld-empty-contact">No contact details provided.</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="lt-actions">
|
||||||
|
<div class="lt-row-2">
|
||||||
|
@if(! $listing->user)
|
||||||
|
<button type="button" class="lt-btn" disabled>Unavailable</button>
|
||||||
|
@elseif($canStartConversation)
|
||||||
|
<button type="button" class="lt-btn" data-inline-chat-open>Message</button>
|
||||||
|
@elseif($isOwnListing)
|
||||||
|
<button type="button" class="lt-btn" disabled>Your listing</button>
|
||||||
|
@else
|
||||||
|
<a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($primaryContactHref)
|
||||||
|
<a href="{{ $primaryContactHref }}" class="lt-btn lt-btn-outline">{{ $primaryContactLabel }}</a>
|
||||||
|
@else
|
||||||
|
<button type="button" class="lt-btn lt-btn-outline" disabled>No contact</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($listing->user && ! $isOwnListing)
|
||||||
|
<a href="{{ $sellerListingsUrl }}" class="lt-btn lt-btn-outline">View listings</a>
|
||||||
|
@elseif($isOwnListing)
|
||||||
|
<button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="lt-card ld-tab-card" data-detail-tabs>
|
||||||
<div class="lt-tab-list" role="tablist" aria-label="Listing sections">
|
<div class="lt-tab-list" role="tablist" aria-label="Listing sections">
|
||||||
<button type="button" class="lt-tab-button is-active" data-detail-tab-button data-tab="details" role="tab" aria-selected="true">
|
<button type="button" class="lt-tab-button is-active" data-detail-tab-button data-tab="details" role="tab" aria-selected="true">
|
||||||
Listing details
|
Listing details
|
||||||
@ -284,12 +324,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lt-tab-panel is-active" data-detail-tab-panel data-panel="details" role="tabpanel">
|
<div class="lt-tab-panel is-active" data-detail-tab-panel data-panel="details" role="tabpanel">
|
||||||
@if($locationLabel !== '')
|
|
||||||
<div class="lt-address-chip lt-address-chip-soft">{{ $locationLabel }}</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="lt-spec-table">
|
<div class="lt-spec-table">
|
||||||
@foreach($specRows as $row)
|
@foreach($detailRows as $row)
|
||||||
<div class="lt-spec-row">
|
<div class="lt-spec-row">
|
||||||
<span>{{ $row['label'] }}</span>
|
<span>{{ $row['label'] }}</span>
|
||||||
<strong>{{ $row['value'] }}</strong>
|
<strong>{{ $row['value'] }}</strong>
|
||||||
@ -324,79 +360,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
|
|
||||||
<aside class="lt-side-rail">
|
|
||||||
<section class="lt-card lt-side-card">
|
|
||||||
<div class="lt-seller-head">
|
|
||||||
<div class="lt-avatar">{{ $sellerInitial !== '' ? $sellerInitial : 'S' }}</div>
|
|
||||||
<div>
|
|
||||||
<p class="lt-seller-kicker">Seller</p>
|
|
||||||
<p class="lt-seller-name">{{ $sellerName }}</p>
|
|
||||||
<div class="lt-seller-meta">{{ $sellerMemberText }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if(filled($listing->contact_phone) || filled($listing->contact_email))
|
|
||||||
<div class="lt-contact-panel">
|
|
||||||
@if(filled($listing->contact_phone))
|
|
||||||
<a href="tel:{{ preg_replace('/\s+/', '', (string) $listing->contact_phone) }}" class="lt-contact-primary">
|
|
||||||
{{ $listing->contact_phone }}
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if(filled($listing->contact_email))
|
|
||||||
<a href="mailto:{{ $listing->contact_email }}" class="lt-contact-secondary">
|
|
||||||
{{ $listing->contact_email }}
|
|
||||||
</a>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
<div class="lt-actions">
|
|
||||||
<div class="lt-row-2">
|
|
||||||
@if(! $listing->user)
|
|
||||||
<button type="button" class="lt-btn" disabled>Unavailable</button>
|
|
||||||
@elseif($canStartConversation)
|
|
||||||
<button type="button" class="lt-btn" data-inline-chat-open>Message</button>
|
|
||||||
@elseif($isOwnListing)
|
|
||||||
<button type="button" class="lt-btn" disabled>Your listing</button>
|
|
||||||
@else
|
|
||||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn">Message</a>
|
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($primaryContactHref)
|
|
||||||
<a href="{{ $primaryContactHref }}" class="lt-btn lt-btn-outline">{{ $primaryContactLabel }}</a>
|
|
||||||
@else
|
|
||||||
<button type="button" class="lt-btn lt-btn-outline" disabled>No contact</button>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if($listing->user && ! $isOwnListing)
|
|
||||||
@auth
|
|
||||||
<form method="POST" action="{{ route('favorites.sellers.toggle', $listing->user) }}" class="lt-action-form">
|
|
||||||
@csrf
|
|
||||||
<input type="hidden" name="redirect_to" value="{{ request()->fullUrl() }}">
|
|
||||||
<button type="submit" class="lt-btn lt-btn-outline">
|
|
||||||
{{ $isSellerFavorited ? 'Saved seller' : 'Save seller' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
@else
|
|
||||||
<a href="{{ $loginRedirectRoute }}" class="lt-btn lt-btn-outline">Save seller</a>
|
|
||||||
@endauth
|
|
||||||
@elseif($isOwnListing)
|
|
||||||
<button type="button" class="lt-btn lt-btn-outline" disabled>Your account</button>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="lt-card lt-safety-card">
|
|
||||||
<h3 class="lt-safety-title">Safety tips</h3>
|
|
||||||
<p class="lt-safety-copy">Inspect the item in person, avoid sending money in advance, and confirm the seller identity before closing the deal.</p>
|
|
||||||
<a href="{{ $reportUrl }}" class="lt-inline-link">Report this listing</a>
|
|
||||||
</section>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="lt-mobile-actions">
|
<div class="lt-mobile-actions">
|
||||||
<div class="lt-mobile-actions-shell">
|
<div class="lt-mobile-actions-shell">
|
||||||
|
|||||||
@ -22,6 +22,7 @@ use Spatie\Activitylog\LogOptions;
|
|||||||
use Spatie\Activitylog\Traits\LogsActivity;
|
use Spatie\Activitylog\Traits\LogsActivity;
|
||||||
use Spatie\ModelStates\HasStates;
|
use Spatie\ModelStates\HasStates;
|
||||||
use Spatie\Permission\Traits\HasRoles;
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class User extends Authenticatable implements FilamentUser, HasAvatar
|
class User extends Authenticatable implements FilamentUser, HasAvatar
|
||||||
{
|
{
|
||||||
@ -186,4 +187,36 @@ class User extends Authenticatable implements FilamentUser, HasAvatar
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function unreadInboxCount(): int
|
||||||
|
{
|
||||||
|
return Conversation::unreadCountForUser((int) $this->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function unreadNotificationCount(): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (int) $this->unreadNotifications()->count();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savedListingsCount(): int
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return (int) $this->favoriteListings()->count();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headerBadgeCounts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'messages' => $this->unreadInboxCount(),
|
||||||
|
'notifications' => $this->unreadNotificationCount(),
|
||||||
|
'favorites' => $this->savedListingsCount(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
Modules/User/database/seeders/UserWorkspaceSeeder.php
Normal file
18
Modules/User/database/seeders/UserWorkspaceSeeder.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\User\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class UserWorkspaceSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$this->call([
|
||||||
|
\Modules\Listing\Database\Seeders\ListingPanelDemoSeeder::class,
|
||||||
|
\Modules\Favorite\Database\Seeders\FavoriteDemoSeeder::class,
|
||||||
|
\Modules\Conversation\Database\Seeders\ConversationDemoSeeder::class,
|
||||||
|
\Modules\Video\Database\Seeders\VideoDemoSeeder::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
Modules/Video/database/seeders/VideoDemoSeeder.php
Normal file
102
Modules/Video/database/seeders/VideoDemoSeeder.php
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Video\Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$listings = Listing::query()
|
||||||
|
->where('user_id', $user->getKey())
|
||||||
|
->where('status', 'active')
|
||||||
|
->orderBy('id')
|
||||||
|
->take(count($blueprints))
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($blueprints as $index => $blueprint) {
|
||||||
|
$listing = $listings->get($index);
|
||||||
|
|
||||||
|
if (! $listing) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$video = Video::query()->firstOrNew([
|
||||||
|
'listing_id' => $listing->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'title' => $blueprint['title'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$video->forceFill([
|
||||||
|
'listing_id' => $listing->getKey(),
|
||||||
|
'user_id' => $user->getKey(),
|
||||||
|
'title' => $blueprint['title'],
|
||||||
|
'description' => $blueprint['description'],
|
||||||
|
'status' => $blueprint['status'],
|
||||||
|
'disk' => 'public',
|
||||||
|
'path' => null,
|
||||||
|
'upload_disk' => 'public',
|
||||||
|
'upload_path' => null,
|
||||||
|
'mime_type' => 'video/mp4',
|
||||||
|
'size' => null,
|
||||||
|
'sort_order' => $index + 1,
|
||||||
|
'is_active' => $blueprint['is_active'],
|
||||||
|
'processing_error' => $blueprint['processing_error'],
|
||||||
|
'processed_at' => null,
|
||||||
|
])->saveQuietly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -48,7 +48,7 @@ final class HomeSlideDefaults
|
|||||||
$normalized = collect($source)
|
$normalized = collect($source)
|
||||||
->filter(fn ($slide): bool => is_array($slide))
|
->filter(fn ($slide): bool => is_array($slide))
|
||||||
->values()
|
->values()
|
||||||
->map(function (array $slide, int $index) use ($defaults): ?array {
|
->map(function (array $slide, int $index) use ($defaults, $defaultDisk): ?array {
|
||||||
$fallback = $defaults[$index] ?? $defaults[array_key_last($defaults)];
|
$fallback = $defaults[$index] ?? $defaults[array_key_last($defaults)];
|
||||||
$badge = trim((string) ($slide['badge'] ?? ''));
|
$badge = trim((string) ($slide['badge'] ?? ''));
|
||||||
$title = trim((string) ($slide['title'] ?? ''));
|
$title = trim((string) ($slide['title'] ?? ''));
|
||||||
|
|||||||
@ -9,6 +9,7 @@ use Illuminate\Support\Facades\View;
|
|||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use Modules\S3\Support\MediaStorage;
|
use Modules\S3\Support\MediaStorage;
|
||||||
|
use Modules\User\App\Models\User;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class RequestAppData
|
final class RequestAppData
|
||||||
@ -22,6 +23,7 @@ final class RequestAppData
|
|||||||
View::share('generalSettings', $generalSettings);
|
View::share('generalSettings', $generalSettings);
|
||||||
View::share('headerLocationCountries', $this->resolveHeaderLocationCountries());
|
View::share('headerLocationCountries', $this->resolveHeaderLocationCountries());
|
||||||
View::share('headerNavCategories', $this->resolveHeaderNavCategories());
|
View::share('headerNavCategories', $this->resolveHeaderNavCategories());
|
||||||
|
View::share('headerAccountMeta', $this->resolveHeaderAccountMeta());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveGeneralSettings(): array
|
private function resolveGeneralSettings(): array
|
||||||
@ -214,6 +216,24 @@ final class RequestAppData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveHeaderAccountMeta(): ?array
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $user instanceof User) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$badgeCounts = $user->headerBadgeCounts();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $user->getDisplayName(),
|
||||||
|
'messages' => max(0, (int) ($badgeCounts['messages'] ?? 0)),
|
||||||
|
'notifications' => max(0, (int) ($badgeCounts['notifications'] ?? 0)),
|
||||||
|
'favorites' => max(0, (int) ($badgeCounts['favorites'] ?? 0)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeCurrencies(array $currencies): array
|
private function normalizeCurrencies(array $currencies): array
|
||||||
{
|
{
|
||||||
$normalized = collect($currencies)
|
$normalized = collect($currencies)
|
||||||
|
|||||||
@ -15,12 +15,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class,
|
\Modules\Listing\Database\Seeders\ListingCustomFieldSeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
||||||
|
\Modules\User\Database\Seeders\UserWorkspaceSeeder::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ((bool) config('demo.enabled') || (bool) config('demo.provisioning')) {
|
|
||||||
$this->call([
|
|
||||||
\Modules\Demo\Database\Seeders\DemoContentSeeder::class,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -192,6 +192,11 @@ h6 {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-location[open],
|
||||||
|
.oc-account-menu[open] {
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
.oc-location-trigger {
|
.oc-location-trigger {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -435,7 +440,14 @@ h6 {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-category-track::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.oc-category-pill {
|
.oc-category-pill {
|
||||||
@ -534,6 +546,116 @@ h6 {
|
|||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-header-icon {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-header-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -0.35rem;
|
||||||
|
right: -0.2rem;
|
||||||
|
min-width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
padding: 0 0.28rem;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
background: #e66767;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 6px 14px rgba(230, 103, 103, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-header-badge.is-neutral {
|
||||||
|
background: #5f6f89;
|
||||||
|
box-shadow: 0 6px 14px rgba(95, 111, 137, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-menu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-trigger {
|
||||||
|
min-height: 2.9rem;
|
||||||
|
padding: 0 0.9rem 0 1rem;
|
||||||
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(61, 69, 97, 0.92);
|
||||||
|
color: #f8fafc;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.16);
|
||||||
|
transition: background 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-trigger:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
background: rgba(55, 63, 90, 0.98);
|
||||||
|
box-shadow: 0 14px 28px rgba(15, 23, 42, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-name {
|
||||||
|
max-width: 9rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-chevron {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.84;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 12px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 20;
|
||||||
|
min-width: 13rem;
|
||||||
|
padding: 0.6rem;
|
||||||
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.98);
|
||||||
|
box-shadow: 0 24px 48px rgba(15, 23, 42, 0.14);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-link {
|
||||||
|
min-height: 2.7rem;
|
||||||
|
padding: 0 0.9rem;
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-link:hover,
|
||||||
|
.oc-account-link-button:hover {
|
||||||
|
background: #f3f6fa;
|
||||||
|
color: var(--oc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-account-link-button {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.header-utility.oc-compact-menu-trigger,
|
.header-utility.oc-compact-menu-trigger,
|
||||||
.header-utility.oc-mobile-menu-close {
|
.header-utility.oc-mobile-menu-close {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -547,6 +669,7 @@ h6 {
|
|||||||
|
|
||||||
.location-panel {
|
.location-panel {
|
||||||
width: min(calc(100vw - 32px), 360px);
|
width: min(calc(100vw - 32px), 360px);
|
||||||
|
z-index: 120;
|
||||||
}
|
}
|
||||||
|
|
||||||
.location-panel select {
|
.location-panel select {
|
||||||
@ -682,6 +805,10 @@ h6 {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oc-account-trigger {
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.oc-text-link {
|
.oc-text-link {
|
||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -707,6 +834,15 @@ h6 {
|
|||||||
|
|
||||||
.oc-category-row {
|
.oc-category-row {
|
||||||
display: block;
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oc-category-track {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
row-gap: 10px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: visible;
|
||||||
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1674,6 +1810,231 @@ summary::-webkit-details-marker {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ld-header-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-ref {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
color: #98a2b3;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--oc-text);
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.12;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 14px;
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-inline-form {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-action {
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.88);
|
||||||
|
color: #344054;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-action:hover,
|
||||||
|
.ld-seller-link:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08);
|
||||||
|
background: #ffffff;
|
||||||
|
color: var(--oc-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-action.is-active {
|
||||||
|
border-color: rgba(0, 113, 227, 0.18);
|
||||||
|
background: rgba(0, 113, 227, 0.08);
|
||||||
|
color: var(--oc-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-stage {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-areas:
|
||||||
|
"gallery"
|
||||||
|
"summary"
|
||||||
|
"seller";
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card {
|
||||||
|
grid-area: gallery;
|
||||||
|
padding: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card .lt-gallery-main {
|
||||||
|
min-height: 360px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: linear-gradient(180deg, #edf2f7 0%, #dbe3ee 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card .lt-gallery-main img {
|
||||||
|
min-height: 360px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card .lt-gallery-top {
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.78);
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-card,
|
||||||
|
.ld-seller-card,
|
||||||
|
.ld-tab-card {
|
||||||
|
padding: 18px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-card {
|
||||||
|
grid-area: summary;
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 10px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-price {
|
||||||
|
color: var(--oc-text);
|
||||||
|
font-size: clamp(2rem, 4vw, 2.8rem);
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-date {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-location {
|
||||||
|
color: #0b4a8a;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-card .lt-spec-table,
|
||||||
|
.ld-tab-card .lt-spec-table {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-card .lt-spec-row:first-child,
|
||||||
|
.ld-tab-card .lt-spec-row:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-seller-card {
|
||||||
|
grid-area: seller;
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-seller-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-seller-link {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--oc-primary);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-seller-card .lt-contact-panel {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-empty-contact {
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: linear-gradient(180deg, #ffffff 0%, #f7f9fc 100%);
|
||||||
|
color: #667085;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-tab-card {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-tab-card .lt-tab-list {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-tab-card .lt-description {
|
||||||
|
max-width: 78ch;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 721px) {
|
@media (min-width: 721px) {
|
||||||
.lt-wrap {
|
.lt-wrap {
|
||||||
padding: 22px 16px 56px;
|
padding: 22px 16px 56px;
|
||||||
@ -1703,6 +2064,34 @@ summary::-webkit-details-marker {
|
|||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ld-header-card {
|
||||||
|
padding: 22px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-header-title {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card .lt-gallery-main {
|
||||||
|
min-height: 520px;
|
||||||
|
border-radius: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-gallery-card .lt-gallery-main img {
|
||||||
|
min-height: 520px;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-summary-card,
|
||||||
|
.ld-seller-card,
|
||||||
|
.ld-tab-card {
|
||||||
|
padding: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.lt-hero-side {
|
.lt-hero-side {
|
||||||
justify-items: flex-end;
|
justify-items: flex-end;
|
||||||
}
|
}
|
||||||
@ -1786,6 +2175,19 @@ summary::-webkit-details-marker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 900px) {
|
@media (min-width: 900px) {
|
||||||
|
.ld-stage {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 360px;
|
||||||
|
grid-template-areas:
|
||||||
|
"gallery summary"
|
||||||
|
"gallery seller";
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ld-seller-card {
|
||||||
|
position: sticky;
|
||||||
|
top: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
.lt-media-spec-grid {
|
.lt-media-spec-grid {
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 360px);
|
grid-template-columns: minmax(0, 1fr) minmax(320px, 360px);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
@ -1801,6 +2203,11 @@ summary::-webkit-details-marker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1180px) {
|
@media (min-width: 1180px) {
|
||||||
|
.ld-stage {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 420px 300px;
|
||||||
|
grid-template-areas: "gallery summary seller";
|
||||||
|
}
|
||||||
|
|
||||||
.lt-grid {
|
.lt-grid {
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
|||||||
@ -14,6 +14,8 @@
|
|||||||
$panelListingsRoute = auth()->check() ? route('panel.listings.index') : $loginRoute;
|
$panelListingsRoute = auth()->check() ? route('panel.listings.index') : $loginRoute;
|
||||||
$inboxRoute = auth()->check() ? route('panel.inbox.index') : $loginRoute;
|
$inboxRoute = auth()->check() ? route('panel.inbox.index') : $loginRoute;
|
||||||
$favoritesRoute = auth()->check() ? route('favorites.index') : $loginRoute;
|
$favoritesRoute = auth()->check() ? route('favorites.index') : $loginRoute;
|
||||||
|
$profileRoute = auth()->check() ? route('panel.profile.edit') : $loginRoute;
|
||||||
|
$notificationsRoute = auth()->check() ? route('panel.listings.index') : $loginRoute;
|
||||||
$demoEnabled = (bool) config('demo.enabled');
|
$demoEnabled = (bool) config('demo.enabled');
|
||||||
$hasDemoSession = (bool) session('is_demo_session') || filled(session('demo_uuid'));
|
$hasDemoSession = (bool) session('is_demo_session') || filled(session('demo_uuid'));
|
||||||
$demoLandingMode = $demoEnabled && request()->routeIs('home') && !auth()->check() && !$hasDemoSession;
|
$demoLandingMode = $demoEnabled && request()->routeIs('home') && !auth()->check() && !$hasDemoSession;
|
||||||
@ -62,6 +64,11 @@
|
|||||||
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
? route('locations.cities', ['country' => '__COUNTRY__'], false)
|
||||||
: '';
|
: '';
|
||||||
$simplePage = trim((string) $__env->yieldContent('simple_page')) === '1';
|
$simplePage = trim((string) $__env->yieldContent('simple_page')) === '1';
|
||||||
|
$headerAccount = is_array($headerAccountMeta ?? null) ? $headerAccountMeta : null;
|
||||||
|
$headerMessageCount = max(0, (int) ($headerAccount['messages'] ?? 0));
|
||||||
|
$headerNotificationCount = max(0, (int) ($headerAccount['notifications'] ?? 0));
|
||||||
|
$headerFavoritesCount = max(0, (int) ($headerAccount['favorites'] ?? 0));
|
||||||
|
$headerBadgeLabel = static fn (int $count): string => $count > 99 ? '99+' : (string) $count;
|
||||||
@endphp
|
@endphp
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ in_array(app()->getLocale(), ['ar']) ? 'rtl' : 'ltr' }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" dir="{{ in_array(app()->getLocale(), ['ar']) ? 'rtl' : 'ltr' }}">
|
||||||
@ -191,29 +198,53 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
@auth
|
@auth
|
||||||
<a href="{{ $favoritesRoute }}" class="header-utility oc-desktop-utility" aria-label="Favorites">
|
<details class="oc-account-menu oc-desktop-utility">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<summary class="oc-account-trigger list-none cursor-pointer">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
<span class="oc-account-name">{{ $headerAccount['name'] ?? auth()->user()->name }}</span>
|
||||||
|
<svg class="oc-account-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 9l6 6 6-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</summary>
|
||||||
<a href="{{ $inboxRoute }}" class="header-utility oc-desktop-utility" aria-label="Inbox">
|
<div class="oc-account-panel">
|
||||||
|
<a href="{{ $panelListingsRoute }}" class="oc-account-link">My Listings</a>
|
||||||
|
<a href="{{ $profileRoute }}" class="oc-account-link">My Profile</a>
|
||||||
|
<a href="{{ $favoritesRoute }}" class="oc-account-link">Favorites</a>
|
||||||
|
<a href="{{ $inboxRoute }}" class="oc-account-link">Inbox</a>
|
||||||
|
<form method="POST" action="{{ $logoutRoute }}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="oc-account-link oc-account-link-button">Logout</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<a href="{{ $inboxRoute }}" class="header-utility oc-desktop-utility oc-header-icon" aria-label="Inbox">
|
||||||
|
@if($headerMessageCount > 0)
|
||||||
|
<span class="oc-header-badge">{{ $headerBadgeLabel($headerMessageCount) }}</span>
|
||||||
|
@endif
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $panelListingsRoute }}" class="header-utility oc-desktop-utility" aria-label="Dashboard">
|
<a href="{{ $notificationsRoute }}" class="header-utility oc-desktop-utility oc-header-icon" aria-label="Notifications">
|
||||||
|
@if($headerNotificationCount > 0)
|
||||||
|
<span class="oc-header-badge">{{ $headerBadgeLabel($headerNotificationCount) }}</span>
|
||||||
|
@endif
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M15 17h5l-1.4-1.4a2 2 0 0 1-.6-1.4V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M10 17a2 2 0 0 0 4 0"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="{{ $favoritesRoute }}" class="header-utility oc-desktop-utility oc-header-icon" aria-label="Favorites">
|
||||||
|
@if($headerFavoritesCount > 0)
|
||||||
|
<span class="oc-header-badge is-neutral">{{ $headerBadgeLabel($headerFavoritesCount) }}</span>
|
||||||
|
@endif
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="m12 3 2.8 5.67 6.2.9-4.5 4.39 1.06 6.2L12 17.21 6.44 20.16 7.5 13.96 3 9.57l6.2-.9L12 3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
||||||
Sell
|
Sell
|
||||||
</a>
|
</a>
|
||||||
<form method="POST" action="{{ $logoutRoute }}" class="oc-logout">
|
|
||||||
@csrf
|
|
||||||
<button type="submit" class="oc-text-link">{{ __('messages.logout') }}</button>
|
|
||||||
</form>
|
|
||||||
@else
|
@else
|
||||||
<a href="{{ $loginRoute }}" class="oc-text-link oc-auth-link">
|
<a href="{{ $loginRoute }}" class="oc-text-link oc-auth-link">
|
||||||
{{ __('messages.login') }}
|
{{ __('messages.login') }}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user