diff --git a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php index a67737e39..9ecdccf11 100644 --- a/Modules/Admin/Filament/Pages/ManageGeneralSettings.php +++ b/Modules/Admin/Filament/Pages/ManageGeneralSettings.php @@ -6,6 +6,7 @@ use App\Support\CountryCodeManager; use App\Settings\GeneralSettings; use BackedEnum; use Filament\Forms\Components\FileUpload; +use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\TextInput; @@ -43,6 +44,39 @@ class ManageGeneralSettings extends SettingsPage ->label('Site Description') ->rows(3) ->maxLength(500), + Repeater::make('home_slides') + ->label('Home Slider') + ->schema([ + TextInput::make('badge') + ->label('Badge') + ->required() + ->maxLength(255), + TextInput::make('title') + ->label('Title') + ->required() + ->maxLength(255), + Textarea::make('subtitle') + ->label('Subtitle') + ->rows(2) + ->required() + ->maxLength(500), + TextInput::make('primary_button_text') + ->label('Primary Button Text') + ->required() + ->maxLength(120), + TextInput::make('secondary_button_text') + ->label('Secondary Button Text') + ->required() + ->maxLength(120), + ]) + ->default($this->defaultHomeSlides()) + ->minItems(1) + ->collapsible() + ->reorderableWithButtons() + ->addActionLabel('Add Slide') + ->itemLabel(fn (array $state): ?string => filled($state['title'] ?? null) ? (string) $state['title'] : 'Slide') + ->afterStateHydrated(fn (Repeater $component, $state) => $component->state($this->normalizeHomeSlides($state))) + ->dehydrateStateUsing(fn ($state) => $this->normalizeHomeSlides($state)), FileUpload::make('site_logo') ->label('Site Logo') ->image() @@ -178,4 +212,50 @@ class ManageGeneralSettings extends SettingsPage return $normalized !== [] ? $normalized : ['USD']; } + + private function defaultHomeSlides(): array + { + return [ + [ + 'badge' => 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ], + ]; + } + + private function normalizeHomeSlides(mixed $state): array + { + $slides = is_array($state) ? $state : []; + $fallbackSlide = $this->defaultHomeSlides()[0]; + + $normalized = collect($slides) + ->filter(fn ($slide): bool => is_array($slide)) + ->map(function (array $slide) use ($fallbackSlide): ?array { + $badge = trim((string) ($slide['badge'] ?? '')); + $title = trim((string) ($slide['title'] ?? '')); + $subtitle = trim((string) ($slide['subtitle'] ?? '')); + $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); + $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + + if ($title === '') { + return null; + } + + return [ + 'badge' => $badge !== '' ? $badge : $fallbackSlide['badge'], + 'title' => $title, + 'subtitle' => $subtitle !== '' ? $subtitle : $fallbackSlide['subtitle'], + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallbackSlide['primary_button_text'], + 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallbackSlide['secondary_button_text'], + ]; + }) + ->filter(fn ($slide): bool => is_array($slide)) + ->values() + ->all(); + + return $normalized !== [] ? $normalized : $this->defaultHomeSlides(); + } } diff --git a/Modules/Category/Models/Category.php b/Modules/Category/Models/Category.php index 6b2b2870f..dcd591a2b 100644 --- a/Modules/Category/Models/Category.php +++ b/Modules/Category/Models/Category.php @@ -72,7 +72,7 @@ class Category extends Model ->active() ->whereNull('parent_id') ->with([ - 'children' => fn (Builder $query) => $query->active()->ordered(), + 'children' => fn (HasMany $query) => $query->active()->ordered(), ]) ->ordered() ->get(); diff --git a/Modules/Listing/database/seeders/ListingSeeder.php b/Modules/Listing/database/seeders/ListingSeeder.php index ca130eeaf..0267b90d1 100644 --- a/Modules/Listing/database/seeders/ListingSeeder.php +++ b/Modules/Listing/database/seeders/ListingSeeder.php @@ -2,6 +2,7 @@ namespace Modules\Listing\Database\Seeders; use Illuminate\Database\Seeder; +use Illuminate\Support\Str; use Modules\Category\Models\Category; use Modules\Listing\Models\Listing; @@ -17,34 +18,116 @@ class ListingSeeder extends Seeder if (!$user || $categories->isEmpty()) return; $listings = [ - ['title' => 'iPhone 14 Pro - Excellent Condition', 'price' => 799, 'city' => 'Istanbul', 'country' => 'Turkey'], - ['title' => 'MacBook Pro 2023', 'price' => 1499, 'city' => 'Ankara', 'country' => 'Turkey'], - ['title' => '2020 Toyota Corolla', 'price' => 18000, 'city' => 'New York', 'country' => 'United States'], - ['title' => '3-Bedroom Apartment for Sale', 'price' => 250000, 'city' => 'Istanbul', 'country' => 'Turkey'], - ['title' => 'Nike Running Shoes Size 42', 'price' => 89, 'city' => 'Berlin', 'country' => 'Germany'], - ['title' => 'IKEA Dining Table', 'price' => 150, 'city' => 'London', 'country' => 'United Kingdom'], - ['title' => 'Yoga Mat - Brand New', 'price' => 35, 'city' => 'Paris', 'country' => 'France'], - ['title' => 'Web Developer for Hire', 'price' => 0, 'city' => 'Remote', 'country' => 'Turkey'], - ['title' => 'Samsung 55" 4K TV', 'price' => 599, 'city' => 'Madrid', 'country' => 'Spain'], - ['title' => 'Honda CBR500R Motorcycle 2021', 'price' => 6500, 'city' => 'Tokyo', 'country' => 'Japan'], + [ + 'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış', + 'description' => 'Cihaz sorunsuz çalışıyor, pil sağlığı iyi durumda. Kutusu ve şarj kablosu ile teslim edilecektir.', + 'price' => 44999, + 'city' => 'İstanbul', + 'country' => 'Türkiye', + ], + [ + 'title' => 'MacBook Pro M2 16 GB / 512 GB', + 'description' => 'Yazılım geliştirme için kullanıldı. Kozmetik olarak çok iyi durumda, faturası mevcut.', + 'price' => 62999, + 'city' => 'Ankara', + 'country' => 'Türkiye', + ], + [ + 'title' => '2020 Toyota Corolla 1.6 Dream', + 'description' => 'Boyalı parça yok, düzenli bakımlı aile aracı. Detaylı ekspertiz raporu paylaşılabilir.', + 'price' => 980000, + 'city' => 'İzmir', + 'country' => 'Türkiye', + ], + [ + 'title' => 'Bluetooth Kulaklık - Aktif Gürültü Engelleme', + 'description' => 'Uzun pil ömrü ve net mikrofon performansı. Kutu içeriği tamdır.', + 'price' => 3499, + 'city' => 'Bursa', + 'country' => 'Türkiye', + ], + [ + 'title' => 'Masaüstü için 15 inç dizüstü bilgisayar', + 'description' => 'Günlük kullanım ve ofis işleri için ideal. SSD sayesinde hızlı açılış.', + 'price' => 18450, + 'city' => 'Antalya', + 'country' => 'Türkiye', + ], + [ + 'title' => 'Seramik Kahve Kupası Seti (6 Adet)', + 'description' => 'Az kullanıldı, kırık/çatlak yok. Mutfak yenileme nedeniyle satılıktır.', + 'price' => 650, + 'city' => 'Adana', + 'country' => 'Türkiye', + ], + [ + 'title' => 'Sedan Araç - Düşük Kilometre', + 'description' => 'Şehir içi kullanıldı, tüm bakımları zamanında yapıldı. Ciddi alıcılarla paylaşım yapılır.', + 'price' => 845000, + 'city' => 'Konya', + 'country' => 'Türkiye', + ], ]; + $sampleImages = [ + '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', + ]; + + $sampleImageFileNames = collect($sampleImages) + ->map(fn (string $path): string => basename($path)) + ->values(); + foreach ($listings as $i => $listing) { $category = $categories->get($i % $categories->count()); - Listing::firstOrCreate( - ['slug' => \Illuminate\Support\Str::slug($listing['title']) . '-' . ($i + 1)], + $slug = Str::slug($listing['title']) . '-' . ($i + 1); + + $listingModel = Listing::updateOrCreate( + ['slug' => $slug], array_merge($listing, [ - 'slug' => \Illuminate\Support\Str::slug($listing['title']) . '-' . ($i + 1), - 'description' => 'This is a sample listing description for ' . $listing['title'], - 'currency' => $listing['price'] > 0 ? 'USD' : 'USD', + 'slug' => $slug, + 'description' => $listing['description'], + 'currency' => 'TRY', 'category_id' => $category?->id, 'user_id' => $user->id, 'status' => 'active', 'contact_email' => $user->email, - 'contact_phone' => '+1234567890', + 'contact_phone' => '+905551112233', 'is_featured' => $i < 3, ]) ); + + $imageRelativePath = $sampleImages[$i % count($sampleImages)]; + $imageAbsolutePath = public_path($imageRelativePath); + + if (! is_file($imageAbsolutePath)) { + continue; + } + + $currentMedia = $listingModel->getMedia('listing-images'); + $currentHasSampleImage = $currentMedia->contains( + fn ($media): bool => $sampleImageFileNames->contains((string) $media->file_name) + ); + + if (! $currentHasSampleImage) { + $listingModel->clearMediaCollection('listing-images'); + } + + $targetFileName = basename($imageAbsolutePath); + $alreadyHasTargetImage = $listingModel->getMedia('listing-images') + ->contains(fn ($media): bool => (string) $media->file_name === $targetFileName); + + if (! $alreadyHasTargetImage) { + $listingModel + ->addMedia($imageAbsolutePath) + ->preservingOriginal() + ->toMediaCollection('listing-images'); + } } } } diff --git a/Modules/Location/database/seeders/LocationSeeder.php b/Modules/Location/database/seeders/LocationSeeder.php index 0ce628513..a911fa91c 100644 --- a/Modules/Location/database/seeders/LocationSeeder.php +++ b/Modules/Location/database/seeders/LocationSeeder.php @@ -1,45 +1,207 @@ 'Turkey', 'code' => 'TR', 'phone_code' => '+90'], - ['name' => 'United States', 'code' => 'US', 'phone_code' => '+1'], - ['name' => 'Germany', 'code' => 'DE', 'phone_code' => '+49'], - ['name' => 'France', 'code' => 'FR', 'phone_code' => '+33'], - ['name' => 'United Kingdom', 'code' => 'GB', 'phone_code' => '+44'], - ['name' => 'Spain', 'code' => 'ES', 'phone_code' => '+34'], - ['name' => 'Italy', 'code' => 'IT', 'phone_code' => '+39'], - ['name' => 'Russia', 'code' => 'RU', 'phone_code' => '+7'], - ['name' => 'China', 'code' => 'CN', 'phone_code' => '+86'], - ['name' => 'Japan', 'code' => 'JP', 'phone_code' => '+81'], + foreach ($this->countries() as $country) { + Country::updateOrCreate( + ['code' => $country['code']], + [ + 'name' => $country['name'], + 'phone_code' => $country['phone_code'], + 'is_active' => true, + ] + ); + } + + $turkey = Country::query()->where('code', 'TR')->first(); + + if (! $turkey) { + return; + } + + $turkeyCities = $this->turkeyCities(); + + foreach ($turkeyCities as $city) { + City::updateOrCreate( + ['country_id' => (int) $turkey->id, 'name' => $city], + ['is_active' => true] + ); + } + + City::query() + ->where('country_id', (int) $turkey->id) + ->whereNotIn('name', $turkeyCities) + ->delete(); + } + + /** + * @return array + */ + private function countries(): array + { + $countries = []; + + foreach (CountriesEnum::cases() as $countryEnum) { + $value = $countryEnum->value; + $phoneCode = $this->normalizePhoneCode($countryEnum->getCountryCode()); + + if ($value === 'us_ca') { + $countries['US'] = [ + 'code' => 'US', + 'name' => 'Amerika Birleşik Devletleri', + 'phone_code' => $phoneCode, + ]; + $countries['CA'] = [ + 'code' => 'CA', + 'name' => 'Kanada', + 'phone_code' => $phoneCode, + ]; + + continue; + } + + if ($value === 'ru_kz') { + $countries['RU'] = [ + 'code' => 'RU', + 'name' => 'Rusya', + 'phone_code' => $phoneCode, + ]; + $countries['KZ'] = [ + 'code' => 'KZ', + 'name' => 'Kazakistan', + 'phone_code' => $phoneCode, + ]; + + continue; + } + + $key = 'filament-country-code-field::countries.' . $value; + $labelTr = trim((string) trans($key, [], 'tr')); + $labelEn = trim((string) trans($key, [], 'en')); + + $name = $labelTr !== '' && $labelTr !== $key + ? $labelTr + : ($labelEn !== '' && $labelEn !== $key ? $labelEn : strtoupper($value)); + + $iso2 = strtoupper(explode('_', $value)[0] ?? $value); + + $countries[$iso2] = [ + 'code' => $iso2, + 'name' => $name, + 'phone_code' => $phoneCode, + ]; + } + + return collect($countries) + ->sortBy('name', SORT_NATURAL | SORT_FLAG_CASE) + ->values() + ->all(); + } + + private function normalizePhoneCode(string $phoneCode): string + { + $normalized = trim(explode(',', $phoneCode)[0]); + $normalized = str_replace(' ', '', $normalized); + + return substr($normalized, 0, 10); + } + + /** + * @return array + */ + private function turkeyCities(): array + { + return [ + 'Adana', + 'Adıyaman', + 'Afyonkarahisar', + 'Ağrı', + 'Aksaray', + 'Amasya', + 'Ankara', + 'Antalya', + 'Ardahan', + 'Artvin', + 'Aydın', + 'Balıkesir', + 'Bartın', + 'Batman', + 'Bayburt', + 'Bilecik', + 'Bingöl', + 'Bitlis', + 'Bolu', + 'Burdur', + 'Bursa', + 'Çanakkale', + 'Çankırı', + 'Çorum', + 'Denizli', + 'Diyarbakır', + 'Düzce', + 'Edirne', + 'Elazığ', + 'Erzincan', + 'Erzurum', + 'Eskişehir', + 'Gaziantep', + 'Giresun', + 'Gümüşhane', + 'Hakkari', + 'Hatay', + 'Iğdır', + 'Isparta', + 'İstanbul', + 'İzmir', + 'Kahramanmaraş', + 'Karabük', + 'Karaman', + 'Kars', + 'Kastamonu', + 'Kayseri', + 'Kilis', + 'Kırıkkale', + 'Kırklareli', + 'Kırşehir', + 'Kocaeli', + 'Konya', + 'Kütahya', + 'Malatya', + 'Manisa', + 'Mardin', + 'Mersin', + 'Muğla', + 'Muş', + 'Nevşehir', + 'Niğde', + 'Ordu', + 'Osmaniye', + 'Rize', + 'Sakarya', + 'Samsun', + 'Siirt', + 'Sinop', + 'Sivas', + 'Şanlıurfa', + 'Şırnak', + 'Tekirdağ', + 'Tokat', + 'Trabzon', + 'Tunceli', + 'Uşak', + 'Van', + 'Yalova', + 'Yozgat', + 'Zonguldak', ]; - - foreach ($countries as $country) { - Country::firstOrCreate(['code' => $country['code']], array_merge($country, ['is_active' => true])); - } - - $tr = Country::where('code', 'TR')->first(); - if ($tr) { - $cities = ['Istanbul', 'Ankara', 'Izmir', 'Bursa', 'Antalya']; - foreach ($cities as $city) { - City::firstOrCreate(['name' => $city, 'country_id' => $tr->id]); - } - } - - $us = Country::where('code', 'US')->first(); - if ($us) { - $cities = ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix']; - foreach ($cities as $city) { - City::firstOrCreate(['name' => $city, 'country_id' => $us->id]); - } - } } } diff --git a/Modules/Location/routes/web.php b/Modules/Location/routes/web.php index b5e9263e3..4f7622317 100644 --- a/Modules/Location/routes/web.php +++ b/Modules/Location/routes/web.php @@ -1,8 +1,35 @@ cities() +use Illuminate\Support\Facades\Route; +use Modules\Location\Models\Country; + +Route::get('/locations/cities/{country}', function (string $country) { + $lookupValue = trim($country); + + if ($lookupValue === '') { + return response()->json([]); + } + + $lookupCode = strtoupper($lookupValue); + $lookupName = mb_strtolower($lookupValue); + + $countryModel = Country::query() + ->where(function ($query) use ($lookupValue, $lookupCode, $lookupName): void { + if (ctype_digit($lookupValue)) { + $query->orWhereKey((int) $lookupValue); + } + + $query + ->orWhereRaw('UPPER(code) = ?', [$lookupCode]) + ->orWhereRaw('LOWER(name) = ?', [$lookupName]); + }) + ->first(); + + if (! $countryModel) { + return response()->json([]); + } + + $activeCities = $countryModel->cities() ->where('is_active', true) ->orderBy('name') ->get(['id', 'name', 'country_id']); @@ -12,12 +39,12 @@ Route::get('/locations/cities/{country}', function(\Modules\Location\Models\Coun } return response()->json( - $country->cities() + $countryModel->cities() ->orderBy('name') ->get(['id', 'name', 'country_id']) ); })->name('locations.cities'); -Route::get('/locations/districts/{city}', function(\Modules\Location\Models\City $city) { +Route::get('/locations/districts/{city}', function (\Modules\Location\Models\City $city) { return response()->json($city->districts); })->name('locations.districts'); diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 0fb14e909..8269c4229 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -39,6 +39,7 @@ class AppServiceProvider extends ServiceProvider $fallbackLocale = config('app.locale', 'en'); $fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD'])); $fallbackDescription = 'The marketplace for buying and selling everything.'; + $fallbackHomeSlides = $this->defaultHomeSlides(); $fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY'); $fallbackGoogleClientId = env('GOOGLE_CLIENT_ID'); $fallbackGoogleClientSecret = env('GOOGLE_CLIENT_SECRET'); @@ -51,6 +52,7 @@ class AppServiceProvider extends ServiceProvider $generalSettings = [ 'site_name' => $fallbackName, 'site_description' => $fallbackDescription, + 'home_slides' => $fallbackHomeSlides, 'site_logo_url' => null, 'default_language' => $fallbackLocale, 'default_country_code' => $fallbackDefaultCountryCode, @@ -98,10 +100,12 @@ class AppServiceProvider extends ServiceProvider $appleClientId = trim((string) ($settings->apple_client_id ?: $fallbackAppleClientId)); $appleClientSecret = trim((string) ($settings->apple_client_secret ?: $fallbackAppleClientSecret)); $defaultCountryCode = CountryCodeManager::normalizeCountryCode($settings->default_country_code ?? $fallbackDefaultCountryCode); + $homeSlides = $this->normalizeHomeSlides($settings->home_slides ?? [], $fallbackHomeSlides); $generalSettings = [ 'site_name' => trim((string) ($settings->site_name ?: $fallbackName)), 'site_description' => trim((string) ($settings->site_description ?: $fallbackDescription)), + 'home_slides' => $homeSlides, 'site_logo_url' => filled($settings->site_logo) ? Storage::disk('public')->url($settings->site_logo) : null, @@ -253,4 +257,59 @@ class AppServiceProvider extends ServiceProvider return $normalized !== [] ? $normalized : ['USD']; } + + private function defaultHomeSlides(): array + { + return [ + [ + 'badge' => 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ], + ]; + } + + private function normalizeHomeSlides(mixed $slides, array $fallbackSlides): array + { + if (! is_array($slides)) { + return $fallbackSlides; + } + + $fallbackSlide = $fallbackSlides[0] ?? [ + 'badge' => 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ]; + + $normalized = collect($slides) + ->filter(fn ($slide): bool => is_array($slide)) + ->map(function (array $slide) use ($fallbackSlide): ?array { + $badge = trim((string) ($slide['badge'] ?? '')); + $title = trim((string) ($slide['title'] ?? '')); + $subtitle = trim((string) ($slide['subtitle'] ?? '')); + $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); + $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + + if ($title === '') { + return null; + } + + return [ + 'badge' => $badge !== '' ? $badge : $fallbackSlide['badge'], + 'title' => $title, + 'subtitle' => $subtitle !== '' ? $subtitle : $fallbackSlide['subtitle'], + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallbackSlide['primary_button_text'], + 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallbackSlide['secondary_button_text'], + ]; + }) + ->filter(fn ($slide): bool => is_array($slide)) + ->values() + ->all(); + + return $normalized !== [] ? $normalized : $fallbackSlides; + } } diff --git a/app/Settings/GeneralSettings.php b/app/Settings/GeneralSettings.php index f2107fd56..6ca21f49c 100644 --- a/app/Settings/GeneralSettings.php +++ b/app/Settings/GeneralSettings.php @@ -50,6 +50,8 @@ class GeneralSettings extends Settings public ?string $apple_client_secret; + public array $home_slides; + public static function group(): string { return 'general'; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1510d4648..3808b700c 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -29,6 +29,7 @@ class DatabaseSeeder extends Seeder } $this->call([ + HomeSliderSettingsSeeder::class, \Modules\Location\Database\Seeders\LocationSeeder::class, \Modules\Category\Database\Seeders\CategorySeeder::class, \Modules\Listing\Database\Seeders\ListingSeeder::class, diff --git a/database/seeders/HomeSliderSettingsSeeder.php b/database/seeders/HomeSliderSettingsSeeder.php new file mode 100644 index 000000000..a980e3e47 --- /dev/null +++ b/database/seeders/HomeSliderSettingsSeeder.php @@ -0,0 +1,60 @@ +defaultHomeSlides()[0]; + + $slides = is_array($settings->home_slides ?? null) ? $settings->home_slides : []; + + $normalized = collect($slides) + ->filter(fn ($slide): bool => is_array($slide)) + ->map(function (array $slide) use ($fallbackSlide): ?array { + $title = trim((string) ($slide['title'] ?? '')); + + if ($title === '') { + return null; + } + + $badge = trim((string) ($slide['badge'] ?? '')); + $subtitle = trim((string) ($slide['subtitle'] ?? '')); + $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); + $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + + return [ + 'badge' => $badge !== '' ? $badge : $fallbackSlide['badge'], + 'title' => $title, + 'subtitle' => $subtitle !== '' ? $subtitle : $fallbackSlide['subtitle'], + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallbackSlide['primary_button_text'], + 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallbackSlide['secondary_button_text'], + ]; + }) + ->filter(fn ($slide): bool => is_array($slide)) + ->values() + ->all(); + + $settings->home_slides = $normalized !== [] ? $normalized : $this->defaultHomeSlides(); + + $settings->save(); + } + + private function defaultHomeSlides(): array + { + return [ + [ + 'badge' => 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ], + ]; + } +} diff --git a/database/settings/2026_03_04_120000_add_home_slider_settings.php b/database/settings/2026_03_04_120000_add_home_slider_settings.php new file mode 100644 index 000000000..9b907c657 --- /dev/null +++ b/database/settings/2026_03_04_120000_add_home_slider_settings.php @@ -0,0 +1,15 @@ +migrator->add('general.home_slider_badge', 'OpenClassify Marketplace'); + $this->migrator->add('general.home_slider_title', 'İlan ücreti ödemeden ürününü hızla sat!'); + $this->migrator->add('general.home_slider_subtitle', 'Buy and sell everything in your area'); + $this->migrator->add('general.home_slider_primary_button_text', 'İncele'); + $this->migrator->add('general.home_slider_secondary_button_text', 'Post Listing'); + } +}; diff --git a/database/settings/2026_03_04_130000_convert_home_slider_fields_to_home_slides.php b/database/settings/2026_03_04_130000_convert_home_slider_fields_to_home_slides.php new file mode 100644 index 000000000..57e3616d5 --- /dev/null +++ b/database/settings/2026_03_04_130000_convert_home_slider_fields_to_home_slides.php @@ -0,0 +1,97 @@ + 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ]; + + if (! $this->migrator->exists('general.home_slides')) { + $this->migrator->add('general.home_slides', [[ + 'badge' => $this->legacySetting('home_slider_badge', $defaultSlide['badge']), + 'title' => $this->legacySetting('home_slider_title', $defaultSlide['title']), + 'subtitle' => $this->legacySetting('home_slider_subtitle', $defaultSlide['subtitle']), + 'primary_button_text' => $this->legacySetting('home_slider_primary_button_text', $defaultSlide['primary_button_text']), + 'secondary_button_text' => $this->legacySetting('home_slider_secondary_button_text', $defaultSlide['secondary_button_text']), + ]]); + } else { + $this->migrator->update('general.home_slides', function ($slides) use ($defaultSlide) { + return $this->normalizeSlides($slides, $defaultSlide); + }); + } + + $this->migrator->deleteIfExists('general.home_slider_badge'); + $this->migrator->deleteIfExists('general.home_slider_title'); + $this->migrator->deleteIfExists('general.home_slider_subtitle'); + $this->migrator->deleteIfExists('general.home_slider_primary_button_text'); + $this->migrator->deleteIfExists('general.home_slider_secondary_button_text'); + } + + private function legacySetting(string $name, string $default): string + { + $payload = DB::table('settings') + ->where('group', 'general') + ->where('name', $name) + ->value('payload'); + + $decoded = $this->decodePayload($payload); + + return is_string($decoded) && trim($decoded) !== '' + ? trim($decoded) + : $default; + } + + private function normalizeSlides(mixed $slides, array $defaultSlide): array + { + if (! is_array($slides)) { + return [$defaultSlide]; + } + + $normalized = collect($slides) + ->filter(fn ($slide): bool => is_array($slide)) + ->map(function (array $slide) use ($defaultSlide): ?array { + $badge = trim((string) ($slide['badge'] ?? '')); + $title = trim((string) ($slide['title'] ?? '')); + $subtitle = trim((string) ($slide['subtitle'] ?? '')); + $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); + $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + + if ($title === '') { + return null; + } + + return [ + 'badge' => $badge !== '' ? $badge : $defaultSlide['badge'], + 'title' => $title, + 'subtitle' => $subtitle !== '' ? $subtitle : $defaultSlide['subtitle'], + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $defaultSlide['primary_button_text'], + 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $defaultSlide['secondary_button_text'], + ]; + }) + ->filter(fn ($slide): bool => is_array($slide)) + ->values() + ->all(); + + return $normalized !== [] ? $normalized : [$defaultSlide]; + } + + private function decodePayload(mixed $payload): mixed + { + if (! is_string($payload)) { + return $payload; + } + + $decoded = json_decode($payload, true); + + return json_last_error() === JSON_ERROR_NONE ? $decoded : $payload; + } +}; diff --git a/public/sample_image/car.jpeg b/public/sample_image/car.jpeg new file mode 100644 index 000000000..c897de083 Binary files /dev/null and b/public/sample_image/car.jpeg differ diff --git a/public/sample_image/car2.jpeg b/public/sample_image/car2.jpeg new file mode 100644 index 000000000..f3738cc7e Binary files /dev/null and b/public/sample_image/car2.jpeg differ diff --git a/public/sample_image/cup.jpg b/public/sample_image/cup.jpg new file mode 100644 index 000000000..eae28aa3e Binary files /dev/null and b/public/sample_image/cup.jpg differ diff --git a/public/sample_image/headphones.jpg b/public/sample_image/headphones.jpg new file mode 100644 index 000000000..b2dda65f4 Binary files /dev/null and b/public/sample_image/headphones.jpg differ diff --git a/public/sample_image/laptop.jpg b/public/sample_image/laptop.jpg new file mode 100644 index 000000000..5b2a67d0c Binary files /dev/null and b/public/sample_image/laptop.jpg differ diff --git a/public/sample_image/macbook.jpg b/public/sample_image/macbook.jpg new file mode 100644 index 000000000..9f7d210c7 Binary files /dev/null and b/public/sample_image/macbook.jpg differ diff --git a/public/sample_image/phone.jpeg b/public/sample_image/phone.jpeg new file mode 100644 index 000000000..20469e4f4 Binary files /dev/null and b/public/sample_image/phone.jpeg differ diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index fe62af3b3..df9099292 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -5,15 +5,56 @@ $heroListing = $featuredListings->first() ?? $recentListings->first(); $heroImage = $heroListing?->getFirstMediaUrl('listing-images'); $listingCards = $recentListings->take(6); - $trendGradients = [ - 'from-emerald-500 to-teal-600', - 'from-rose-500 to-pink-600', - 'from-fuchsia-500 to-rose-600', - 'from-sky-500 to-blue-600', - 'from-amber-500 to-orange-600', - 'from-cyan-500 to-indigo-600', - 'from-red-500 to-rose-600', - 'from-violet-500 to-purple-600', + $homeSlides = collect($generalSettings['home_slides'] ?? []) + ->filter(fn ($slide): bool => is_array($slide)) + ->map(function (array $slide): array { + $badge = trim((string) ($slide['badge'] ?? '')); + $title = trim((string) ($slide['title'] ?? '')); + $subtitle = trim((string) ($slide['subtitle'] ?? '')); + $primaryButtonText = trim((string) ($slide['primary_button_text'] ?? '')); + $secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? '')); + + return [ + 'badge' => $badge !== '' ? $badge : 'OpenClassify Marketplace', + 'title' => $title !== '' ? $title : 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => $subtitle !== '' ? $subtitle : 'Buy and sell everything in your area', + 'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'İncele', + 'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : 'Post Listing', + ]; + }) + ->values(); + + if ($homeSlides->isEmpty()) { + $homeSlides = collect([ + [ + 'badge' => 'OpenClassify Marketplace', + 'title' => 'İlan ücreti ödemeden ürününü hızla sat!', + 'subtitle' => 'Buy and sell everything in your area', + 'primary_button_text' => 'İncele', + 'secondary_button_text' => 'Post Listing', + ], + ]); + } + + $trendSkins = [ + ['gradient' => 'from-emerald-800 via-emerald-700 to-emerald-600', 'glow' => 'bg-emerald-200/45'], + ['gradient' => 'from-rose-700 via-rose-600 to-pink-500', 'glow' => 'bg-rose-200/40'], + ['gradient' => 'from-rose-700 via-pink-600 to-fuchsia-500', 'glow' => 'bg-pink-200/40'], + ['gradient' => 'from-rose-700 via-rose-600 to-orange-500', 'glow' => 'bg-orange-200/40'], + ['gradient' => 'from-rose-700 via-pink-600 to-red-500', 'glow' => 'bg-rose-200/40'], + ['gradient' => 'from-fuchsia-700 via-pink-600 to-rose-500', 'glow' => 'bg-fuchsia-200/40'], + ['gradient' => 'from-rose-700 via-rose-600 to-pink-500', 'glow' => 'bg-rose-200/40'], + ['gradient' => 'from-red-700 via-rose-600 to-pink-500', 'glow' => 'bg-red-200/40'], + ]; + $trendIcons = [ + 'gift', + 'computer', + 'bike', + 'sparkles', + 'coffee', + 'laptop', + 'fitness', + 'game', ]; @endphp @@ -22,34 +63,71 @@
-
-

OpenClassify Marketplace

-

- İlan ücreti ödemeden ürününü hızla sat! -

-

- {{ __('messages.hero_subtitle') }} -

-
- - İncele - - @auth - - {{ __('messages.post_listing') }} - - @else - - {{ __('messages.post_listing') }} - - @endauth +
+
+ @foreach($homeSlides as $index => $slide) +
$index !== 0]) + aria-hidden="{{ $index === 0 ? 'false' : 'true' }}" + > +

{{ $slide['badge'] }}

+

{{ $slide['title'] }}

+

{{ $slide['subtitle'] }}

+ +
+ @endforeach
+ + @if($homeSlides->count() > 1)
- - - - + + @foreach($homeSlides as $index => $slide) + + @endforeach +
+ @else +
+ +
+ @endif
+ @endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 291ec0b5f..79c303bc3 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -426,17 +426,36 @@ return Array.isArray(payload?.data) ? payload.data : []; }; + const buildCitiesUrl = (template, countryId) => { + const normalizedTemplate = (template ?? '').toString().trim(); + const normalizedCountryId = (countryId ?? '').toString().trim(); + const encodedCountryId = encodeURIComponent(normalizedCountryId); + + if (normalizedTemplate === '' || normalizedCountryId === '') { + return ''; + } + + if (normalizedTemplate.includes('__COUNTRY__')) { + return normalizedTemplate.replace('__COUNTRY__', encodedCountryId); + } + + return normalizedTemplate.endsWith('/') + ? normalizedTemplate + encodedCountryId + : `${normalizedTemplate}/${encodedCountryId}`; + }; + const loadCities = async (root, countryId, selectedCityId = null, selectedCityName = null) => { const citySelect = root.querySelector('[data-location-city]'); const countrySelect = root.querySelector('[data-location-country]'); const statusText = root.querySelector('[data-location-status]'); const template = root.dataset.citiesUrlTemplate ?? ''; + const normalizedCountryId = (countryId ?? '').toString().trim(); if (!citySelect || !countrySelect) { return; } - if (!countryId || template === '') { + if (normalizedCountryId === '' || template === '') { citySelect.innerHTML = ''; citySelect.disabled = true; return; @@ -446,7 +465,12 @@ citySelect.innerHTML = ''; try { - const primaryUrl = template.replace('__COUNTRY__', encodeURIComponent(String(countryId))); + const primaryUrl = buildCitiesUrl(template, normalizedCountryId); + + if (primaryUrl === '') { + throw new Error('city_url_invalid'); + } + let cityOptions; try { @@ -474,6 +498,12 @@ citySelect.innerHTML = ''; + if (cityOptions.length === 0) { + citySelect.innerHTML = ''; + citySelect.disabled = true; + return; + } + cityOptions.forEach((city) => { const option = document.createElement('option'); option.value = String(city.id ?? ''); @@ -593,10 +623,28 @@ } const applyStored = async () => { - if (stored?.countryId) { - countrySelect.value = String(stored.countryId); - await loadCities(root, stored.countryId, stored.cityId, stored.cityName); - return; + if (stored && typeof stored === 'object') { + const matchedStoredCountry = Array.from(countrySelect.options).find((option) => { + if (stored.countryId && option.value === String(stored.countryId)) { + return true; + } + + if (stored.countryCode && option.dataset.code === String(stored.countryCode).toUpperCase()) { + return true; + } + + if (stored.countryName) { + return normalize(option.dataset.name) === normalize(stored.countryName); + } + + return false; + }); + + if (matchedStoredCountry) { + countrySelect.value = matchedStoredCountry.value; + await loadCities(root, matchedStoredCountry.value, stored.cityId, stored.cityName); + return; + } } const defaultOption = Array.from(countrySelect.options).find((option) => option.dataset.default === '1');