Refactor modules for mobile UI
@ -3,12 +3,12 @@
|
|||||||
namespace Modules\Conversation\Database\Seeders;
|
namespace Modules\Conversation\Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\Conversation\App\Models\Conversation;
|
use Modules\Conversation\App\Models\Conversation;
|
||||||
use Modules\Conversation\App\Models\ConversationMessage;
|
use Modules\Conversation\App\Models\ConversationMessage;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
|
|
||||||
class ConversationDemoSeeder extends Seeder
|
class ConversationDemoSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@ -18,72 +18,59 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$admin = User::query()->where('email', 'a@a.com')->first();
|
$users = User::query()
|
||||||
$member = User::query()->where('email', 'b@b.com')->first();
|
->whereIn('email', DemoUserCatalog::emails())
|
||||||
|
->orderBy('email')
|
||||||
|
->get()
|
||||||
|
->values();
|
||||||
|
|
||||||
if (! $admin || ! $member) {
|
if ($users->count() < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminListings = Listing::query()
|
ConversationMessage::query()
|
||||||
->where('user_id', $admin->getKey())
|
->whereHas('conversation', fn ($query) => $query->whereIn('buyer_id', $users->pluck('id'))->orWhereIn('seller_id', $users->pluck('id')))
|
||||||
->where('status', 'active')
|
->delete();
|
||||||
->orderBy('id')
|
|
||||||
->take(2)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$memberListings = Listing::query()
|
Conversation::query()
|
||||||
->where('user_id', $member->getKey())
|
->whereIn('buyer_id', $users->pluck('id'))
|
||||||
->where('status', 'active')
|
->orWhereIn('seller_id', $users->pluck('id'))
|
||||||
->orderBy('id')
|
->delete();
|
||||||
->take(2)
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($adminListings->count() < 2 || $memberListings->count() < 2) {
|
foreach ($users as $index => $buyer) {
|
||||||
return;
|
$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
|
private function conversationTablesExist(): bool
|
||||||
@ -97,7 +84,7 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
?Listing $listing,
|
?Listing $listing,
|
||||||
array $messages
|
array $messages
|
||||||
): void {
|
): void {
|
||||||
if (! $listing) {
|
if (! $listing || (int) $seller->getKey() === (int) $buyer->getKey()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,10 +99,6 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
ConversationMessage::query()
|
|
||||||
->where('conversation_id', $conversation->getKey())
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
$lastMessageAt = null;
|
$lastMessageAt = null;
|
||||||
|
|
||||||
foreach ($messages as $payload) {
|
foreach ($messages as $payload) {
|
||||||
@ -137,10 +120,47 @@ class ConversationDemoSeeder extends Seeder
|
|||||||
$lastMessageAt = $createdAt;
|
$lastMessageAt = $createdAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
$conversation->forceFill([
|
if ($lastMessageAt) {
|
||||||
'seller_id' => $seller->getKey(),
|
$conversation->forceFill([
|
||||||
'last_message_at' => $lastMessageAt,
|
'seller_id' => $seller->getKey(),
|
||||||
'updated_at' => $lastMessageAt,
|
'last_message_at' => $lastMessageAt,
|
||||||
])->saveQuietly();
|
'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],
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use Modules\Category\Models\Category;
|
|||||||
use Modules\Favorite\App\Models\FavoriteSearch;
|
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
|
|
||||||
class FavoriteDemoSeeder extends Seeder
|
class FavoriteDemoSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@ -19,47 +20,40 @@ class FavoriteDemoSeeder extends Seeder
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$admin = User::query()->where('email', 'a@a.com')->first();
|
$users = User::query()
|
||||||
$member = User::query()->where('email', 'b@b.com')->first();
|
->whereIn('email', DemoUserCatalog::emails())
|
||||||
|
->orderBy('email')
|
||||||
|
->get()
|
||||||
|
->values();
|
||||||
|
|
||||||
if (! $admin || ! $member) {
|
if ($users->count() < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminListings = Listing::query()
|
DB::table('favorite_listings')->whereIn('user_id', $users->pluck('id'))->delete();
|
||||||
->where('user_id', $admin->getKey())
|
DB::table('favorite_sellers')->whereIn('user_id', $users->pluck('id'))->delete();
|
||||||
->orderByDesc('is_featured')
|
FavoriteSearch::query()->whereIn('user_id', $users->pluck('id'))->delete();
|
||||||
->orderBy('id')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
$memberListings = Listing::query()
|
foreach ($users as $index => $user) {
|
||||||
->where('user_id', $member->getKey())
|
$favoriteSeller = $users->get(($index + 1) % $users->count());
|
||||||
->orderByDesc('is_featured')
|
$secondarySeller = $users->get(($index + 2) % $users->count());
|
||||||
->orderBy('id')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($adminListings->isEmpty() || $memberListings->isEmpty()) {
|
if (! $favoriteSeller instanceof User || ! $secondarySeller instanceof User) {
|
||||||
return;
|
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
|
private function favoriteTablesExist(): bool
|
||||||
@ -74,7 +68,7 @@ class FavoriteDemoSeeder extends Seeder
|
|||||||
$rows = $listings
|
$rows = $listings
|
||||||
->values()
|
->values()
|
||||||
->map(function (Listing $listing, int $index) use ($user): array {
|
->map(function (Listing $listing, int $index) use ($user): array {
|
||||||
$timestamp = now()->subHours(12 + ($index * 5));
|
$timestamp = now()->subHours(8 + ($index * 3));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'user_id' => $user->getKey(),
|
'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');
|
$blueprints = [
|
||||||
$vehiclesId = Category::query()->where('slug', 'vehicles')->value('id');
|
['search' => 'phone', 'slug' => 'electronics'],
|
||||||
$realEstateId = Category::query()->where('slug', 'real-estate')->value('id');
|
['search' => 'car', 'slug' => 'vehicles'],
|
||||||
$servicesId = Category::query()->where('slug', 'services')->value('id');
|
['search' => 'apartment', 'slug' => 'real-estate'],
|
||||||
|
['search' => 'style', 'slug' => 'fashion'],
|
||||||
return [
|
['search' => 'furniture', 'slug' => 'home-garden'],
|
||||||
['search' => 'iphone', 'category_id' => $electronicsId],
|
['search' => 'fitness', 'slug' => 'sports'],
|
||||||
['search' => 'sedan', 'category_id' => $vehiclesId],
|
['search' => 'remote', 'slug' => 'jobs'],
|
||||||
['search' => 'apartment', 'category_id' => $realEstateId],
|
['search' => 'cleaning', 'slug' => 'services'],
|
||||||
['search' => 'repair', 'category_id' => $servicesId],
|
|
||||||
];
|
];
|
||||||
}
|
|
||||||
|
|
||||||
private function adminSearchPayloads(): array
|
return collect(range(0, 2))
|
||||||
{
|
->map(function (int $offset) use ($blueprints, $index): array {
|
||||||
$fashionId = Category::query()->where('slug', 'fashion')->value('id');
|
$blueprint = $blueprints[($index + $offset) % count($blueprints)];
|
||||||
$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' => $blueprint['search'],
|
||||||
['search' => 'garden', 'category_id' => $homeGardenId],
|
'category_id' => Category::query()->where('slug', $blueprint['slug'])->value('id'),
|
||||||
['search' => 'fitness', 'category_id' => $sportsId],
|
];
|
||||||
];
|
})
|
||||||
|
->all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,231 +3,79 @@
|
|||||||
namespace Modules\Listing\Database\Seeders;
|
namespace Modules\Listing\Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Modules\Category\Models\Category;
|
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
|
|
||||||
class ListingPanelDemoSeeder extends Seeder
|
class ListingPanelDemoSeeder extends Seeder
|
||||||
{
|
{
|
||||||
private const USER_PANEL_LISTINGS = [
|
private const LEGACY_SLUGS = [
|
||||||
'a@a.com' => [
|
'admin-demo-pro-workstation',
|
||||||
[
|
'admin-demo-sold-camera',
|
||||||
'slug' => 'admin-demo-pro-workstation',
|
'admin-demo-expired-sofa',
|
||||||
'title' => 'Admin Demo Pro Workstation',
|
'member-demo-iphone',
|
||||||
'description' => 'Active demo listing for inbox, favorites, and video testing.',
|
'member-demo-city-bike',
|
||||||
'price' => 28450,
|
'member-demo-vintage-chair',
|
||||||
'status' => 'active',
|
'member-demo-garden-tools',
|
||||||
'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,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public function run(): void
|
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) {
|
foreach (DemoUserCatalog::emails() as $email) {
|
||||||
return;
|
$user = User::query()->where('email', $email)->first();
|
||||||
}
|
|
||||||
|
|
||||||
$this->claimAllListingsForAdmin($admin);
|
if (! $user) {
|
||||||
|
|
||||||
$categories = $this->resolveCategories();
|
|
||||||
|
|
||||||
if ($categories->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (self::USER_PANEL_LISTINGS as $email => $payloads) {
|
|
||||||
$owner = $this->resolveUserByEmail($email);
|
|
||||||
|
|
||||||
if (! $owner) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($payloads as $index => $payload) {
|
$this->applyPanelStates($user);
|
||||||
$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']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveUserByEmail(string $email): ?User
|
private function applyPanelStates(User $user): void
|
||||||
{
|
{
|
||||||
return User::query()->where('email', $email)->first();
|
$listings = Listing::query()
|
||||||
}
|
->where('user_id', $user->getKey())
|
||||||
|
->where('slug', 'like', 'demo-%')
|
||||||
private function claimAllListingsForAdmin(User $admin): void
|
->orderBy('created_at')
|
||||||
{
|
|
||||||
Listing::query()
|
|
||||||
->where(function ($query) use ($admin): void {
|
|
||||||
$query
|
|
||||||
->whereNull('user_id')
|
|
||||||
->orWhere('user_id', '!=', $admin->getKey());
|
|
||||||
})
|
|
||||||
->update([
|
|
||||||
'user_id' => $admin->getKey(),
|
|
||||||
'contact_email' => $admin->email,
|
|
||||||
'updated_at' => now(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function resolveCategories(): Collection
|
|
||||||
{
|
|
||||||
$leafCategories = Category::query()
|
|
||||||
->where('is_active', true)
|
|
||||||
->whereDoesntHave('children')
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->orderBy('name')
|
|
||||||
->get();
|
|
||||||
|
|
||||||
if ($leafCategories->isNotEmpty()) {
|
|
||||||
return $leafCategories->values();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Category::query()
|
|
||||||
->where('is_active', true)
|
|
||||||
->orderBy('sort_order')
|
|
||||||
->orderBy('name')
|
|
||||||
->get()
|
->get()
|
||||||
->values();
|
->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)) {
|
private function expiresAtForIndex(int $index): \Illuminate\Support\Carbon
|
||||||
return;
|
{
|
||||||
}
|
return match ($this->statusForIndex($index)) {
|
||||||
|
'expired' => now()->subDays(4 + ($index % 5)),
|
||||||
$targetFileName = basename($imageAbsolutePath);
|
'sold' => now()->addDays(8 + ($index % 4)),
|
||||||
$existingMedia = $listing->getMedia('listing-images')->first();
|
'pending' => now()->addDays(5 + ($index % 4)),
|
||||||
|
default => now()->addDays(20 + ($index % 9)),
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,11 @@ use Illuminate\Support\Facades\Schema;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
|
use Modules\Listing\Support\DemoListingImageFactory;
|
||||||
use Modules\Location\Models\City;
|
use Modules\Location\Models\City;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
|
|
||||||
class ListingSeeder extends Seeder
|
class ListingSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@ -26,31 +28,45 @@ class ListingSeeder extends Seeder
|
|||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$user = $this->resolveSeederUser();
|
$users = $this->resolveSeederUsers();
|
||||||
$categories = $this->resolveSeedableCategories();
|
$categories = $this->resolveSeedableCategories();
|
||||||
|
|
||||||
if (! $user || $categories->isEmpty()) {
|
if ($users->isEmpty() || $categories->isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$countries = $this->resolveCountries();
|
$countries = $this->resolveCountries();
|
||||||
$turkeyCities = $this->resolveTurkeyCities();
|
$turkeyCities = $this->resolveTurkeyCities();
|
||||||
|
$plannedSlugs = [];
|
||||||
|
|
||||||
foreach ($categories as $index => $category) {
|
foreach ($users as $userIndex => $user) {
|
||||||
$listingData = $this->buildListingData($category, $index, $countries, $turkeyCities);
|
foreach ($categories as $categoryIndex => $category) {
|
||||||
$listing = $this->upsertListing($index, $listingData, $category, $user);
|
$listingIndex = ($userIndex * max(1, $categories->count())) + $categoryIndex;
|
||||||
$this->syncListingImage($listing, $listingData['image']);
|
$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()
|
return User::query()
|
||||||
?? User::query()->where('email', 'admin@openclassify.com')->first()
|
->whereIn('email', DemoUserCatalog::emails())
|
||||||
?? User::query()
|
->orderBy('email')
|
||||||
->whereHas('roles', fn ($query) => $query->where('name', 'admin'))
|
->get()
|
||||||
->first()
|
->values();
|
||||||
?? User::query()->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function resolveSeedableCategories(): Collection
|
private function resolveSeedableCategories(): Collection
|
||||||
@ -58,6 +74,7 @@ class ListingSeeder extends Seeder
|
|||||||
$leafCategories = Category::query()
|
$leafCategories = Category::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->whereDoesntHave('children')
|
->whereDoesntHave('children')
|
||||||
|
->with('parent:id,name')
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get();
|
->get();
|
||||||
@ -68,6 +85,7 @@ class ListingSeeder extends Seeder
|
|||||||
|
|
||||||
return Category::query()
|
return Category::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
|
->with('parent:id,name')
|
||||||
->orderBy('sort_order')
|
->orderBy('sort_order')
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get()
|
->get()
|
||||||
@ -119,17 +137,32 @@ class ListingSeeder extends Seeder
|
|||||||
Category $category,
|
Category $category,
|
||||||
int $index,
|
int $index,
|
||||||
Collection $countries,
|
Collection $countries,
|
||||||
Collection $turkeyCities
|
Collection $turkeyCities,
|
||||||
|
User $user
|
||||||
): array {
|
): array {
|
||||||
$location = $this->resolveLocation($index, $countries, $turkeyCities);
|
$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 [
|
return [
|
||||||
'title' => $this->buildTitle($category, $index),
|
'slug' => $slug,
|
||||||
'description' => $this->buildDescription($category, $location['city'], $location['country']),
|
'title' => $title,
|
||||||
|
'description' => $this->buildDescription($category, $location['city'], $location['country'], $user),
|
||||||
'price' => $this->priceForIndex($index),
|
'price' => $this->priceForIndex($index),
|
||||||
'city' => $location['city'],
|
'city' => $location['city'],
|
||||||
'country' => $location['country'],
|
'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');
|
$turkeyCountry = $countries->first(fn ($country): bool => strtoupper((string) $country->code) === 'TR');
|
||||||
$turkeyName = trim((string) ($turkeyCountry->name ?? 'Turkey')) ?: 'Turkey';
|
$turkeyName = trim((string) ($turkeyCountry->name ?? 'Turkey')) ?: 'Turkey';
|
||||||
|
|
||||||
$useForeignCountry = $countries->count() > 1 && $index % 4 === 0;
|
$useForeignCountry = $countries->count() > 1 && $index % 4 === 0;
|
||||||
|
|
||||||
if ($useForeignCountry) {
|
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)];
|
$prefix = self::TITLE_PREFIXES[$index % count(self::TITLE_PREFIXES)];
|
||||||
$categoryName = trim((string) $category->name);
|
$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);
|
$categoryName = trim((string) $category->name);
|
||||||
$location = trim(collect([$city, $country])->filter()->join(', '));
|
$location = trim(collect([$city, $country])->filter()->join(', '));
|
||||||
|
|
||||||
return sprintf(
|
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',
|
$categoryName !== '' ? $categoryName : 'Item',
|
||||||
|
trim((string) $user->name) !== '' ? trim((string) $user->name) : 'a marketplace user',
|
||||||
$location !== '' ? $location : 'Turkey'
|
$location !== '' ? $location : 'Turkey'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -203,14 +242,12 @@ class ListingSeeder extends Seeder
|
|||||||
return $base + $step;
|
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);
|
$listing = Listing::updateOrCreate(
|
||||||
|
['slug' => $data['slug']],
|
||||||
return Listing::updateOrCreate(
|
|
||||||
['slug' => $slug],
|
|
||||||
[
|
[
|
||||||
'slug' => $slug,
|
'slug' => $data['slug'],
|
||||||
'title' => $data['title'],
|
'title' => $data['title'],
|
||||||
'description' => $data['description'],
|
'description' => $data['description'],
|
||||||
'price' => $data['price'],
|
'price' => $data['price'],
|
||||||
@ -221,63 +258,22 @@ class ListingSeeder extends Seeder
|
|||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'contact_email' => $user->email,
|
'contact_email' => $user->email,
|
||||||
'contact_phone' => '+905551112233',
|
'contact_phone' => $data['contact_phone'],
|
||||||
'is_featured' => $index < 8,
|
'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->replacePublicImage($imageAbsolutePath, $listing->slug.'.svg');
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ use Spatie\Activitylog\Traits\LogsActivity;
|
|||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||||
use Spatie\ModelStates\HasStates;
|
use Spatie\ModelStates\HasStates;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
class Listing extends Model implements HasMedia
|
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
|
public function statusValue(): string
|
||||||
{
|
{
|
||||||
return $this->status instanceof ListingStatus
|
return $this->status instanceof ListingStatus
|
||||||
|
|||||||
84
Modules/Listing/Support/DemoListingImageFactory.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Listing\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
final class DemoListingImageFactory
|
||||||
|
{
|
||||||
|
private const PALETTES = [
|
||||||
|
['#0f172a', '#1d4ed8', '#dbeafe'],
|
||||||
|
['#172554', '#2563eb', '#dbeafe'],
|
||||||
|
['#0f3b2e', '#059669', '#d1fae5'],
|
||||||
|
['#3f2200', '#ea580c', '#ffedd5'],
|
||||||
|
['#3b0764', '#9333ea', '#f3e8ff'],
|
||||||
|
['#3f3f46', '#e11d48', '#ffe4e6'],
|
||||||
|
['#0b3b66', '#0891b2', '#cffafe'],
|
||||||
|
['#422006', '#ca8a04', '#fef3c7'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function ensure(
|
||||||
|
string $slug,
|
||||||
|
string $title,
|
||||||
|
string $categoryName,
|
||||||
|
string $ownerName,
|
||||||
|
int $seed
|
||||||
|
): string {
|
||||||
|
$directory = public_path('generated/demo-listings');
|
||||||
|
|
||||||
|
if (! is_dir($directory)) {
|
||||||
|
mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filePath = $directory.'/'.Str::slug($slug).'.svg';
|
||||||
|
$palette = self::PALETTES[$seed % count(self::PALETTES)];
|
||||||
|
[$baseColor, $accentColor, $surfaceColor] = $palette;
|
||||||
|
|
||||||
|
$shortTitle = self::escape(Str::limit($title, 36, ''));
|
||||||
|
$shortCategory = self::escape(Str::limit($categoryName, 18, ''));
|
||||||
|
$shortOwner = self::escape(Str::limit($ownerName, 18, ''));
|
||||||
|
$code = self::escape(strtoupper(Str::substr(md5($slug), 0, 6)));
|
||||||
|
|
||||||
|
$svg = <<<SVG
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1200" viewBox="0 0 1600 1200" fill="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="120" y1="80" x2="1480" y2="1120" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="{$baseColor}"/>
|
||||||
|
<stop offset="1" stop-color="{$accentColor}"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="glass" x1="320" y1="220" x2="1200" y2="920" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="white" stop-opacity="0.95"/>
|
||||||
|
<stop offset="1" stop-color="{$surfaceColor}" stop-opacity="0.86"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect width="1600" height="1200" rx="64" fill="url(#bg)"/>
|
||||||
|
<circle cx="1320" cy="230" r="170" fill="white" fill-opacity="0.08"/>
|
||||||
|
<circle cx="240" cy="1010" r="220" fill="white" fill-opacity="0.06"/>
|
||||||
|
<rect x="170" y="146" width="1260" height="908" rx="58" fill="url(#glass)" stroke="white" stroke-opacity="0.22" stroke-width="6"/>
|
||||||
|
<rect x="260" y="248" width="420" height="700" rx="44" fill="{$baseColor}" fill-opacity="0.94"/>
|
||||||
|
<rect x="740" y="248" width="520" height="200" rx="34" fill="white" fill-opacity="0.72"/>
|
||||||
|
<rect x="740" y="490" width="520" height="210" rx="34" fill="white" fill-opacity="0.52"/>
|
||||||
|
<rect x="740" y="742" width="240" height="180" rx="30" fill="white" fill-opacity="0.58"/>
|
||||||
|
<rect x="1020" y="742" width="240" height="180" rx="30" fill="white" fill-opacity="0.32"/>
|
||||||
|
<rect x="340" y="338" width="260" height="260" rx="130" fill="white" fill-opacity="0.12"/>
|
||||||
|
<rect x="830" y="310" width="230" height="38" rx="19" fill="{$accentColor}" fill-opacity="0.16"/>
|
||||||
|
<rect x="830" y="548" width="340" height="26" rx="13" fill="{$baseColor}" fill-opacity="0.12"/>
|
||||||
|
<rect x="830" y="596" width="260" height="26" rx="13" fill="{$baseColor}" fill-opacity="0.08"/>
|
||||||
|
<text x="262" y="214" fill="white" fill-opacity="0.92" font-family="Arial, Helvetica, sans-serif" font-size="40" font-weight="700" letter-spacing="10">OPENCLASSIFY DEMO</text>
|
||||||
|
<text x="258" y="760" fill="white" font-family="Arial, Helvetica, sans-serif" font-size="86" font-weight="700">{$shortCategory}</text>
|
||||||
|
<text x="258" y="840" fill="white" fill-opacity="0.78" font-family="Arial, Helvetica, sans-serif" font-size="44" font-weight="500">{$shortOwner}</text>
|
||||||
|
<text x="818" y="390" fill="{$baseColor}" font-family="Arial, Helvetica, sans-serif" font-size="72" font-weight="700">{$shortTitle}</text>
|
||||||
|
<text x="818" y="474" fill="{$accentColor}" font-family="Arial, Helvetica, sans-serif" font-size="34" font-weight="700" letter-spacing="8">{$code}</text>
|
||||||
|
</svg>
|
||||||
|
SVG;
|
||||||
|
|
||||||
|
file_put_contents($filePath, $svg);
|
||||||
|
|
||||||
|
return $filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function escape(string $value): string
|
||||||
|
{
|
||||||
|
return htmlspecialchars($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Modules/User/App/Support/DemoUserCatalog.php
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\User\App\Support;
|
||||||
|
|
||||||
|
final class DemoUserCatalog
|
||||||
|
{
|
||||||
|
public static function records(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'email' => '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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,29 +5,22 @@ namespace Modules\User\Database\Seeders;
|
|||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
use Spatie\Permission\Models\Role;
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
class AuthUserSeeder extends Seeder
|
class AuthUserSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$admin = User::query()->updateOrCreate(
|
$users = collect(DemoUserCatalog::records())
|
||||||
['email' => 'a@a.com'],
|
->map(fn (array $record): User => User::query()->updateOrCreate(
|
||||||
[
|
['email' => $record['email']],
|
||||||
'name' => 'Admin',
|
[
|
||||||
'password' => '236330',
|
'name' => $record['name'],
|
||||||
'status' => 'active',
|
'password' => $record['password'],
|
||||||
],
|
'status' => 'active',
|
||||||
);
|
],
|
||||||
|
));
|
||||||
User::query()->updateOrCreate(
|
|
||||||
['email' => 'b@b.com'],
|
|
||||||
[
|
|
||||||
'name' => 'Member',
|
|
||||||
'password' => '236330',
|
|
||||||
'status' => 'active',
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (! class_exists(Role::class) || ! Schema::hasTable((new Role())->getTable())) {
|
if (! class_exists(Role::class) || ! Schema::hasTable((new Role())->getTable())) {
|
||||||
return;
|
return;
|
||||||
@ -38,6 +31,14 @@ class AuthUserSeeder extends Seeder
|
|||||||
'guard_name' => 'web',
|
'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([]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,72 +6,34 @@ use Illuminate\Database\Seeder;
|
|||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
use Modules\User\App\Models\User;
|
use Modules\User\App\Models\User;
|
||||||
|
use Modules\User\App\Support\DemoUserCatalog;
|
||||||
use Modules\Video\Enums\VideoStatus;
|
use Modules\Video\Enums\VideoStatus;
|
||||||
use Modules\Video\Models\Video;
|
use Modules\Video\Models\Video;
|
||||||
|
|
||||||
class VideoDemoSeeder extends Seeder
|
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
|
public function run(): void
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('videos') || ! Schema::hasTable('listings')) {
|
if (! Schema::hasTable('videos') || ! Schema::hasTable('listings')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (self::VIDEO_BLUEPRINTS as $email => $blueprints) {
|
$users = User::query()
|
||||||
$user = User::query()->where('email', $email)->first();
|
->whereIn('email', DemoUserCatalog::emails())
|
||||||
|
->orderBy('email')
|
||||||
if (! $user) {
|
->get()
|
||||||
continue;
|
->values();
|
||||||
}
|
|
||||||
|
|
||||||
|
foreach ($users as $userIndex => $user) {
|
||||||
$listings = Listing::query()
|
$listings = Listing::query()
|
||||||
->where('user_id', $user->getKey())
|
->where('user_id', $user->getKey())
|
||||||
->where('status', 'active')
|
->where('status', 'active')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->take(count($blueprints))
|
->take(2)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
foreach ($blueprints as $index => $blueprint) {
|
foreach ($listings as $listingIndex => $listing) {
|
||||||
$listing = $listings->get($index);
|
$blueprint = $this->blueprintFor($userIndex, $listingIndex);
|
||||||
|
|
||||||
if (! $listing) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$video = Video::query()->firstOrNew([
|
$video = Video::query()->firstOrNew([
|
||||||
'listing_id' => $listing->getKey(),
|
'listing_id' => $listing->getKey(),
|
||||||
@ -91,7 +53,7 @@ class VideoDemoSeeder extends Seeder
|
|||||||
'upload_path' => null,
|
'upload_path' => null,
|
||||||
'mime_type' => 'video/mp4',
|
'mime_type' => 'video/mp4',
|
||||||
'size' => null,
|
'size' => null,
|
||||||
'sort_order' => $index + 1,
|
'sort_order' => $listingIndex + 1,
|
||||||
'is_active' => $blueprint['is_active'],
|
'is_active' => $blueprint['is_active'],
|
||||||
'processing_error' => $blueprint['processing_error'],
|
'processing_error' => $blueprint['processing_error'],
|
||||||
'processed_at' => null,
|
'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.',
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/sample_image/ watch_band.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
|
After Width: | Height: | Size: 8.0 MiB |
BIN
public/sample_image/fashion natural wedding product shoes.jpg
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 8.2 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/sample_image/jobs.jpg
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
public/sample_image/nike-sport-wear.png
Normal file
|
After Width: | Height: | Size: 499 KiB |
|
After Width: | Height: | Size: 3.6 MiB |
|
After Width: | Height: | Size: 10 MiB |
|
After Width: | Height: | Size: 6.3 MiB |
|
After Width: | Height: | Size: 970 KiB |
BIN
public/sample_image/roof large house fence gate.jpg
Normal file
|
After Width: | Height: | Size: 3.4 MiB |
BIN
public/sample_image/smart-watch.jpg
Normal file
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
BIN
public/sample_image/sportscars car sports car vehicle.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
public/sample_image/sunglasses.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |