beta
@ -6,6 +6,7 @@ use App\Support\CountryCodeManager;
|
|||||||
use App\Settings\GeneralSettings;
|
use App\Settings\GeneralSettings;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TagsInput;
|
use Filament\Forms\Components\TagsInput;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
@ -43,6 +44,39 @@ class ManageGeneralSettings extends SettingsPage
|
|||||||
->label('Site Description')
|
->label('Site Description')
|
||||||
->rows(3)
|
->rows(3)
|
||||||
->maxLength(500),
|
->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')
|
FileUpload::make('site_logo')
|
||||||
->label('Site Logo')
|
->label('Site Logo')
|
||||||
->image()
|
->image()
|
||||||
@ -178,4 +212,50 @@ class ManageGeneralSettings extends SettingsPage
|
|||||||
|
|
||||||
return $normalized !== [] ? $normalized : ['USD'];
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ class Category extends Model
|
|||||||
->active()
|
->active()
|
||||||
->whereNull('parent_id')
|
->whereNull('parent_id')
|
||||||
->with([
|
->with([
|
||||||
'children' => fn (Builder $query) => $query->active()->ordered(),
|
'children' => fn (HasMany $query) => $query->active()->ordered(),
|
||||||
])
|
])
|
||||||
->ordered()
|
->ordered()
|
||||||
->get();
|
->get();
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
namespace Modules\Listing\Database\Seeders;
|
namespace Modules\Listing\Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Modules\Category\Models\Category;
|
use Modules\Category\Models\Category;
|
||||||
use Modules\Listing\Models\Listing;
|
use Modules\Listing\Models\Listing;
|
||||||
|
|
||||||
@ -17,34 +18,116 @@ class ListingSeeder extends Seeder
|
|||||||
if (!$user || $categories->isEmpty()) return;
|
if (!$user || $categories->isEmpty()) return;
|
||||||
|
|
||||||
$listings = [
|
$listings = [
|
||||||
['title' => 'iPhone 14 Pro - Excellent Condition', 'price' => 799, 'city' => 'Istanbul', 'country' => 'Turkey'],
|
[
|
||||||
['title' => 'MacBook Pro 2023', 'price' => 1499, 'city' => 'Ankara', 'country' => 'Turkey'],
|
'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış',
|
||||||
['title' => '2020 Toyota Corolla', 'price' => 18000, 'city' => 'New York', 'country' => 'United States'],
|
'description' => 'Cihaz sorunsuz çalışıyor, pil sağlığı iyi durumda. Kutusu ve şarj kablosu ile teslim edilecektir.',
|
||||||
['title' => '3-Bedroom Apartment for Sale', 'price' => 250000, 'city' => 'Istanbul', 'country' => 'Turkey'],
|
'price' => 44999,
|
||||||
['title' => 'Nike Running Shoes Size 42', 'price' => 89, 'city' => 'Berlin', 'country' => 'Germany'],
|
'city' => 'İstanbul',
|
||||||
['title' => 'IKEA Dining Table', 'price' => 150, 'city' => 'London', 'country' => 'United Kingdom'],
|
'country' => 'Türkiye',
|
||||||
['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' => 'MacBook Pro M2 16 GB / 512 GB',
|
||||||
['title' => 'Honda CBR500R Motorcycle 2021', 'price' => 6500, 'city' => 'Tokyo', 'country' => 'Japan'],
|
'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) {
|
foreach ($listings as $i => $listing) {
|
||||||
$category = $categories->get($i % $categories->count());
|
$category = $categories->get($i % $categories->count());
|
||||||
Listing::firstOrCreate(
|
$slug = Str::slug($listing['title']) . '-' . ($i + 1);
|
||||||
['slug' => \Illuminate\Support\Str::slug($listing['title']) . '-' . ($i + 1)],
|
|
||||||
|
$listingModel = Listing::updateOrCreate(
|
||||||
|
['slug' => $slug],
|
||||||
array_merge($listing, [
|
array_merge($listing, [
|
||||||
'slug' => \Illuminate\Support\Str::slug($listing['title']) . '-' . ($i + 1),
|
'slug' => $slug,
|
||||||
'description' => 'This is a sample listing description for ' . $listing['title'],
|
'description' => $listing['description'],
|
||||||
'currency' => $listing['price'] > 0 ? 'USD' : 'USD',
|
'currency' => 'TRY',
|
||||||
'category_id' => $category?->id,
|
'category_id' => $category?->id,
|
||||||
'user_id' => $user->id,
|
'user_id' => $user->id,
|
||||||
'status' => 'active',
|
'status' => 'active',
|
||||||
'contact_email' => $user->email,
|
'contact_email' => $user->email,
|
||||||
'contact_phone' => '+1234567890',
|
'contact_phone' => '+905551112233',
|
||||||
'is_featured' => $i < 3,
|
'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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,45 +1,207 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace Modules\Location\Database\Seeders;
|
namespace Modules\Location\Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Modules\Location\Models\City;
|
use Modules\Location\Models\City;
|
||||||
use Modules\Location\Models\Country;
|
use Modules\Location\Models\Country;
|
||||||
|
use Tapp\FilamentCountryCodeField\Enums\CountriesEnum;
|
||||||
|
|
||||||
class LocationSeeder extends Seeder
|
class LocationSeeder extends Seeder
|
||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$countries = [
|
foreach ($this->countries() as $country) {
|
||||||
['name' => 'Turkey', 'code' => 'TR', 'phone_code' => '+90'],
|
Country::updateOrCreate(
|
||||||
['name' => 'United States', 'code' => 'US', 'phone_code' => '+1'],
|
['code' => $country['code']],
|
||||||
['name' => 'Germany', 'code' => 'DE', 'phone_code' => '+49'],
|
[
|
||||||
['name' => 'France', 'code' => 'FR', 'phone_code' => '+33'],
|
'name' => $country['name'],
|
||||||
['name' => 'United Kingdom', 'code' => 'GB', 'phone_code' => '+44'],
|
'phone_code' => $country['phone_code'],
|
||||||
['name' => 'Spain', 'code' => 'ES', 'phone_code' => '+34'],
|
'is_active' => true,
|
||||||
['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'],
|
|
||||||
|
$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<int, array{code: string, name: string, phone_code: string}>
|
||||||
|
*/
|
||||||
|
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<int, string>
|
||||||
|
*/
|
||||||
|
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,35 @@
|
|||||||
<?php
|
<?php
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
|
|
||||||
Route::get('/locations/cities/{country}', function(\Modules\Location\Models\Country $country) {
|
use Illuminate\Support\Facades\Route;
|
||||||
$activeCities = $country->cities()
|
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)
|
->where('is_active', true)
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name', 'country_id']);
|
->get(['id', 'name', 'country_id']);
|
||||||
@ -12,12 +39,12 @@ Route::get('/locations/cities/{country}', function(\Modules\Location\Models\Coun
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(
|
return response()->json(
|
||||||
$country->cities()
|
$countryModel->cities()
|
||||||
->orderBy('name')
|
->orderBy('name')
|
||||||
->get(['id', 'name', 'country_id'])
|
->get(['id', 'name', 'country_id'])
|
||||||
);
|
);
|
||||||
})->name('locations.cities');
|
})->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);
|
return response()->json($city->districts);
|
||||||
})->name('locations.districts');
|
})->name('locations.districts');
|
||||||
|
|||||||
@ -39,6 +39,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$fallbackLocale = config('app.locale', 'en');
|
$fallbackLocale = config('app.locale', 'en');
|
||||||
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
|
||||||
$fallbackDescription = 'The marketplace for buying and selling everything.';
|
$fallbackDescription = 'The marketplace for buying and selling everything.';
|
||||||
|
$fallbackHomeSlides = $this->defaultHomeSlides();
|
||||||
$fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY');
|
$fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY');
|
||||||
$fallbackGoogleClientId = env('GOOGLE_CLIENT_ID');
|
$fallbackGoogleClientId = env('GOOGLE_CLIENT_ID');
|
||||||
$fallbackGoogleClientSecret = env('GOOGLE_CLIENT_SECRET');
|
$fallbackGoogleClientSecret = env('GOOGLE_CLIENT_SECRET');
|
||||||
@ -51,6 +52,7 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$generalSettings = [
|
$generalSettings = [
|
||||||
'site_name' => $fallbackName,
|
'site_name' => $fallbackName,
|
||||||
'site_description' => $fallbackDescription,
|
'site_description' => $fallbackDescription,
|
||||||
|
'home_slides' => $fallbackHomeSlides,
|
||||||
'site_logo_url' => null,
|
'site_logo_url' => null,
|
||||||
'default_language' => $fallbackLocale,
|
'default_language' => $fallbackLocale,
|
||||||
'default_country_code' => $fallbackDefaultCountryCode,
|
'default_country_code' => $fallbackDefaultCountryCode,
|
||||||
@ -98,10 +100,12 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
$appleClientId = trim((string) ($settings->apple_client_id ?: $fallbackAppleClientId));
|
$appleClientId = trim((string) ($settings->apple_client_id ?: $fallbackAppleClientId));
|
||||||
$appleClientSecret = trim((string) ($settings->apple_client_secret ?: $fallbackAppleClientSecret));
|
$appleClientSecret = trim((string) ($settings->apple_client_secret ?: $fallbackAppleClientSecret));
|
||||||
$defaultCountryCode = CountryCodeManager::normalizeCountryCode($settings->default_country_code ?? $fallbackDefaultCountryCode);
|
$defaultCountryCode = CountryCodeManager::normalizeCountryCode($settings->default_country_code ?? $fallbackDefaultCountryCode);
|
||||||
|
$homeSlides = $this->normalizeHomeSlides($settings->home_slides ?? [], $fallbackHomeSlides);
|
||||||
|
|
||||||
$generalSettings = [
|
$generalSettings = [
|
||||||
'site_name' => trim((string) ($settings->site_name ?: $fallbackName)),
|
'site_name' => trim((string) ($settings->site_name ?: $fallbackName)),
|
||||||
'site_description' => trim((string) ($settings->site_description ?: $fallbackDescription)),
|
'site_description' => trim((string) ($settings->site_description ?: $fallbackDescription)),
|
||||||
|
'home_slides' => $homeSlides,
|
||||||
'site_logo_url' => filled($settings->site_logo)
|
'site_logo_url' => filled($settings->site_logo)
|
||||||
? Storage::disk('public')->url($settings->site_logo)
|
? Storage::disk('public')->url($settings->site_logo)
|
||||||
: null,
|
: null,
|
||||||
@ -253,4 +257,59 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
return $normalized !== [] ? $normalized : ['USD'];
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,6 +50,8 @@ class GeneralSettings extends Settings
|
|||||||
|
|
||||||
public ?string $apple_client_secret;
|
public ?string $apple_client_secret;
|
||||||
|
|
||||||
|
public array $home_slides;
|
||||||
|
|
||||||
public static function group(): string
|
public static function group(): string
|
||||||
{
|
{
|
||||||
return 'general';
|
return 'general';
|
||||||
|
|||||||
@ -29,6 +29,7 @@ class DatabaseSeeder extends Seeder
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->call([
|
$this->call([
|
||||||
|
HomeSliderSettingsSeeder::class,
|
||||||
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
\Modules\Location\Database\Seeders\LocationSeeder::class,
|
||||||
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
\Modules\Category\Database\Seeders\CategorySeeder::class,
|
||||||
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
\Modules\Listing\Database\Seeders\ListingSeeder::class,
|
||||||
|
|||||||
60
database/seeders/HomeSliderSettingsSeeder.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Settings\GeneralSettings;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class HomeSliderSettingsSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$settings = app(GeneralSettings::class);
|
||||||
|
$fallbackSlide = $this->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',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||||
|
|
||||||
|
return new class extends SettingsMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Spatie\LaravelSettings\Migrations\SettingsMigration;
|
||||||
|
|
||||||
|
return new class extends SettingsMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$defaultSlide = [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
BIN
public/sample_image/car.jpeg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
public/sample_image/car2.jpeg
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
public/sample_image/cup.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
public/sample_image/headphones.jpg
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/sample_image/laptop.jpg
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
public/sample_image/macbook.jpg
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
public/sample_image/phone.jpeg
Normal file
|
After Width: | Height: | Size: 79 KiB |
@ -5,15 +5,56 @@
|
|||||||
$heroListing = $featuredListings->first() ?? $recentListings->first();
|
$heroListing = $featuredListings->first() ?? $recentListings->first();
|
||||||
$heroImage = $heroListing?->getFirstMediaUrl('listing-images');
|
$heroImage = $heroListing?->getFirstMediaUrl('listing-images');
|
||||||
$listingCards = $recentListings->take(6);
|
$listingCards = $recentListings->take(6);
|
||||||
$trendGradients = [
|
$homeSlides = collect($generalSettings['home_slides'] ?? [])
|
||||||
'from-emerald-500 to-teal-600',
|
->filter(fn ($slide): bool => is_array($slide))
|
||||||
'from-rose-500 to-pink-600',
|
->map(function (array $slide): array {
|
||||||
'from-fuchsia-500 to-rose-600',
|
$badge = trim((string) ($slide['badge'] ?? ''));
|
||||||
'from-sky-500 to-blue-600',
|
$title = trim((string) ($slide['title'] ?? ''));
|
||||||
'from-amber-500 to-orange-600',
|
$subtitle = trim((string) ($slide['subtitle'] ?? ''));
|
||||||
'from-cyan-500 to-indigo-600',
|
$primaryButtonText = trim((string) ($slide['primary_button_text'] ?? ''));
|
||||||
'from-red-500 to-rose-600',
|
$secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? ''));
|
||||||
'from-violet-500 to-purple-600',
|
|
||||||
|
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
|
@endphp
|
||||||
|
|
||||||
@ -22,34 +63,71 @@
|
|||||||
<div class="absolute -top-20 -left-24 w-80 h-80 rounded-full bg-blue-400/20 blur-3xl"></div>
|
<div class="absolute -top-20 -left-24 w-80 h-80 rounded-full bg-blue-400/20 blur-3xl"></div>
|
||||||
<div class="absolute -bottom-24 right-10 w-80 h-80 rounded-full bg-cyan-300/20 blur-3xl"></div>
|
<div class="absolute -bottom-24 right-10 w-80 h-80 rounded-full bg-cyan-300/20 blur-3xl"></div>
|
||||||
<div class="relative grid lg:grid-cols-[1fr,1.1fr] gap-6 items-center px-8 md:px-12 py-12 md:py-14">
|
<div class="relative grid lg:grid-cols-[1fr,1.1fr] gap-6 items-center px-8 md:px-12 py-12 md:py-14">
|
||||||
<div>
|
<div data-home-slider>
|
||||||
<p class="text-sm uppercase tracking-[0.22em] text-blue-200 font-semibold mb-4">OpenClassify Marketplace</p>
|
<div class="relative min-h-[250px]">
|
||||||
<h1 class="text-4xl md:text-5xl leading-tight font-extrabold max-w-xl">
|
@foreach($homeSlides as $index => $slide)
|
||||||
İlan ücreti ödemeden ürününü hızla sat!
|
<div
|
||||||
</h1>
|
data-home-slide
|
||||||
<p class="mt-4 text-blue-100 text-base md:text-lg max-w-xl">
|
@class(['transition-opacity duration-300', 'hidden' => $index !== 0])
|
||||||
{{ __('messages.hero_subtitle') }}
|
aria-hidden="{{ $index === 0 ? 'false' : 'true' }}"
|
||||||
</p>
|
>
|
||||||
<div class="mt-8 flex flex-wrap items-center gap-3">
|
<p class="text-sm uppercase tracking-[0.22em] text-blue-200 font-semibold mb-4">{{ $slide['badge'] }}</p>
|
||||||
<a href="{{ route('listings.index') }}" class="bg-white text-blue-900 px-8 py-3 rounded-full font-semibold hover:bg-blue-50 transition">
|
<h1 class="text-4xl md:text-5xl leading-tight font-extrabold max-w-xl">{{ $slide['title'] }}</h1>
|
||||||
İncele
|
<p class="mt-4 text-blue-100 text-base md:text-lg max-w-xl">{{ $slide['subtitle'] }}</p>
|
||||||
</a>
|
<div class="mt-8 flex flex-wrap items-center gap-3">
|
||||||
@auth
|
<a href="{{ route('listings.index') }}" class="bg-white text-blue-900 px-8 py-3 rounded-full font-semibold hover:bg-blue-50 transition">
|
||||||
<a href="{{ route('panel.listings.create') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
{{ $slide['primary_button_text'] }}
|
||||||
{{ __('messages.post_listing') }}
|
</a>
|
||||||
</a>
|
@auth
|
||||||
@else
|
<a href="{{ route('panel.listings.create') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||||||
<a href="{{ route('login') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
{{ $slide['secondary_button_text'] }}
|
||||||
{{ __('messages.post_listing') }}
|
</a>
|
||||||
</a>
|
@else
|
||||||
@endauth
|
<a href="{{ route('login') }}" class="border border-blue-200/60 px-8 py-3 rounded-full font-semibold hover:bg-white/10 transition">
|
||||||
|
{{ $slide['secondary_button_text'] }}
|
||||||
|
</a>
|
||||||
|
@endauth
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if($homeSlides->count() > 1)
|
||||||
<div class="mt-8 flex items-center gap-2">
|
<div class="mt-8 flex items-center gap-2">
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-white/40"></span>
|
<button
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-white/40"></span>
|
type="button"
|
||||||
<span class="w-7 h-2.5 rounded-full bg-white"></span>
|
data-home-slide-prev
|
||||||
<span class="w-2.5 h-2.5 rounded-full bg-white/40"></span>
|
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||||||
|
aria-label="Önceki slide"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">‹</span>
|
||||||
|
</button>
|
||||||
|
@foreach($homeSlides as $index => $slide)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-home-slide-dot="{{ $index }}"
|
||||||
|
@class([
|
||||||
|
'h-2.5 rounded-full transition-all',
|
||||||
|
'w-7 bg-white' => $index === 0,
|
||||||
|
'w-2.5 bg-white/40 hover:bg-white/60' => $index !== 0,
|
||||||
|
])
|
||||||
|
aria-label="Slide {{ $index + 1 }}"
|
||||||
|
></button>
|
||||||
|
@endforeach
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-home-slide-next
|
||||||
|
class="w-8 h-8 rounded-full border border-white/45 text-white grid place-items-center hover:bg-white/15 transition"
|
||||||
|
aria-label="Sonraki slide"
|
||||||
|
>
|
||||||
|
<span aria-hidden="true">›</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="mt-8 flex items-center gap-2">
|
||||||
|
<span class="w-7 h-2.5 rounded-full bg-white"></span>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="relative h-[310px] md:h-[360px]">
|
<div class="relative h-[310px] md:h-[360px]">
|
||||||
<div class="absolute left-6 md:left-10 bottom-0 w-32 md:w-40 h-[250px] md:h-[300px] bg-slate-950 rounded-[32px] shadow-2xl p-2 rotate-[-8deg]">
|
<div class="absolute left-6 md:left-10 bottom-0 w-32 md:w-40 h-[250px] md:h-[300px] bg-slate-950 rounded-[32px] shadow-2xl p-2 rotate-[-8deg]">
|
||||||
@ -87,21 +165,93 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<h2 class="text-2xl font-bold text-slate-900">Trend Kategoriler</h2>
|
<h2 class="text-3xl font-extrabold tracking-tight text-slate-900">Trend Kategoriler</h2>
|
||||||
<a href="{{ route('categories.index') }}" class="text-sm font-semibold text-rose-500 hover:text-rose-600 transition">Tümünü Gör</a>
|
<a href="{{ route('categories.index') }}" class="hidden sm:inline-flex text-sm font-semibold text-rose-500 hover:text-rose-600 transition">
|
||||||
|
Tümünü Gör
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-8 gap-3">
|
<div class="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-trend-prev
|
||||||
|
class="hidden lg:inline-flex absolute left-0 top-1/2 -translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||||||
|
aria-label="Önceki trend kategori"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 18-6-6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div data-trend-track class="flex items-stretch gap-2 overflow-x-auto pb-2 pr-1 scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
|
||||||
@foreach($menuCategories as $index => $category)
|
@foreach($menuCategories as $index => $category)
|
||||||
<a href="{{ route('categories.show', $category) }}" class="group rounded-2xl overflow-hidden bg-white border border-slate-200 hover:shadow-md transition">
|
@php
|
||||||
<div class="h-20 bg-gradient-to-r {{ $trendGradients[$index % count($trendGradients)] }} relative">
|
$trendSkin = $trendSkins[$index % count($trendSkins)];
|
||||||
<span class="absolute -bottom-4 right-4 text-3xl drop-shadow">{{ $category->icon ?? '📦' }}</span>
|
$trendIcon = $trendIcons[$index % count($trendIcons)];
|
||||||
|
@endphp
|
||||||
|
<a href="{{ route('categories.show', $category) }}" class="group shrink-0 w-[170px] rounded-xl overflow-hidden border border-slate-300/80 bg-white hover:shadow-md transition snap-start">
|
||||||
|
<div class="h-[68px] bg-gradient-to-r {{ $trendSkin['gradient'] }} relative overflow-hidden">
|
||||||
|
<span class="absolute -left-5 top-2 w-20 h-20 rounded-full {{ $trendSkin['glow'] }} blur-2xl"></span>
|
||||||
|
<span class="absolute left-5 bottom-2 h-2.5 w-24 rounded-full bg-black/20"></span>
|
||||||
|
<span class="absolute right-3 bottom-2 w-11 h-11 rounded-lg border border-white/35 bg-white/10 backdrop-blur-sm text-white grid place-items-center shadow-sm">
|
||||||
|
@switch($trendIcon)
|
||||||
|
@case('gift')
|
||||||
|
<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="M20 12v8a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-8m16 0H4m16 0V8a1 1 0 0 0-1-1h-3.5M4 12V8a1 1 0 0 1 1-1h3.5m0 0a2 2 0 1 1 0-4c2.5 0 3.5 4 3.5 4m-3.5 0h7m0 0a2 2 0 1 0 0-4c-2.5 0-3.5 4-3.5 4"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('computer')
|
||||||
|
<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 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm4 16h10m-8 0 1.5-3m4 3L13 18"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('bike')
|
||||||
|
<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="M5.5 17.5A3.5 3.5 0 1 0 5.5 10a3.5 3.5 0 0 0 0 7.5Zm13 0A3.5 3.5 0 1 0 18.5 10a3.5 3.5 0 0 0 0 7.5ZM8.5 14l3-5h3l2 5m-5-5L9 6h3"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('sparkles')
|
||||||
|
<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="m9 5 1.4 2.6L13 9l-2.6 1.4L9 13l-1.4-2.6L5 9l2.6-1.4L9 5Zm8 4 1 1.9L20 12l-2 1.1-1 1.9-1-1.9-2-1.1 2-1.1L17 9ZM7 16l1.5 2.8L11.5 20 8.5 21.2 7 24l-1.5-2.8L2.5 20l3-1.2L7 16Z"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('coffee')
|
||||||
|
<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="M7 8v7a3 3 0 0 0 3 3h4a3 3 0 0 0 3-3V8H7Zm10 2h1a2 2 0 1 1 0 4h-1M8 4v2m4-2v2m4-2v2M6 21h12"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('laptop')
|
||||||
|
<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 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8H4V6Zm-1 10h18l-1.2 3H4.2L3 16Z"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@case('fitness')
|
||||||
|
<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 10h2v4H3v-4Zm16 0h2v4h-2v-4ZM7 8h2v8H7V8Zm8 0h2v8h-2V8Zm-4 2h2v4h-2v-4Z"/>
|
||||||
|
</svg>
|
||||||
|
@break
|
||||||
|
@default
|
||||||
|
<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="M15 6h3a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-3m-6 0H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3 4h.01m-1 8h2a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2Z"/>
|
||||||
|
</svg>
|
||||||
|
@endswitch
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-3 py-3">
|
<div class="px-3 py-2.5">
|
||||||
<p class="text-xs font-semibold text-slate-800 leading-tight h-9 overflow-hidden">{{ $category->name }}</p>
|
<p class="text-[12px] sm:text-[13px] font-semibold text-slate-900 leading-tight truncate">{{ $category->name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-trend-next
|
||||||
|
class="hidden lg:inline-flex absolute right-0 top-1/2 translate-x-1/2 -translate-y-1/2 z-10 w-11 h-11 rounded-full border border-slate-300 bg-white text-slate-700 items-center justify-center shadow-sm hover:bg-slate-50 transition"
|
||||||
|
aria-label="Sonraki trend kategori"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 18 6-6-6-6"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -198,4 +348,107 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const setupTrendCategories = () => {
|
||||||
|
const track = document.querySelector('[data-trend-track]');
|
||||||
|
const previousButton = document.querySelector('[data-trend-prev]');
|
||||||
|
const nextButton = document.querySelector('[data-trend-next]');
|
||||||
|
|
||||||
|
if (!track || !previousButton || !nextButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollAmount = () => Math.max(240, Math.floor(track.clientWidth * 0.7));
|
||||||
|
|
||||||
|
previousButton.addEventListener('click', () => {
|
||||||
|
track.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton.addEventListener('click', () => {
|
||||||
|
track.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupHomeSlider = () => {
|
||||||
|
const slider = document.querySelector('[data-home-slider]');
|
||||||
|
|
||||||
|
if (!slider) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slides = Array.from(slider.querySelectorAll('[data-home-slide]'));
|
||||||
|
const dots = Array.from(slider.querySelectorAll('[data-home-slide-dot]'));
|
||||||
|
const previousButton = slider.querySelector('[data-home-slide-prev]');
|
||||||
|
const nextButton = slider.querySelector('[data-home-slide-next]');
|
||||||
|
|
||||||
|
if (slides.length <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeIndex = 0;
|
||||||
|
let intervalId = null;
|
||||||
|
|
||||||
|
const activateSlide = (index) => {
|
||||||
|
activeIndex = (index + slides.length) % slides.length;
|
||||||
|
|
||||||
|
slides.forEach((slide, slideIndex) => {
|
||||||
|
const isActive = slideIndex === activeIndex;
|
||||||
|
|
||||||
|
slide.classList.toggle('hidden', !isActive);
|
||||||
|
slide.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot, dotIndex) => {
|
||||||
|
const isActive = dotIndex === activeIndex;
|
||||||
|
|
||||||
|
dot.classList.toggle('w-7', isActive);
|
||||||
|
dot.classList.toggle('bg-white', isActive);
|
||||||
|
dot.classList.toggle('w-2.5', !isActive);
|
||||||
|
dot.classList.toggle('bg-white/40', !isActive);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopAutoPlay = () => {
|
||||||
|
if (intervalId !== null) {
|
||||||
|
window.clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startAutoPlay = () => {
|
||||||
|
stopAutoPlay();
|
||||||
|
intervalId = window.setInterval(() => activateSlide(activeIndex + 1), 6000);
|
||||||
|
};
|
||||||
|
|
||||||
|
previousButton?.addEventListener('click', () => {
|
||||||
|
activateSlide(activeIndex - 1);
|
||||||
|
startAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
nextButton?.addEventListener('click', () => {
|
||||||
|
activateSlide(activeIndex + 1);
|
||||||
|
startAutoPlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
dots.forEach((dot, index) => {
|
||||||
|
dot.addEventListener('click', () => {
|
||||||
|
activateSlide(index);
|
||||||
|
startAutoPlay();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
slider.addEventListener('mouseenter', stopAutoPlay);
|
||||||
|
slider.addEventListener('mouseleave', startAutoPlay);
|
||||||
|
slider.addEventListener('focusin', stopAutoPlay);
|
||||||
|
slider.addEventListener('focusout', startAutoPlay);
|
||||||
|
|
||||||
|
activateSlide(0);
|
||||||
|
startAutoPlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
setupHomeSlider();
|
||||||
|
setupTrendCategories();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -426,17 +426,36 @@
|
|||||||
return Array.isArray(payload?.data) ? payload.data : [];
|
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 loadCities = async (root, countryId, selectedCityId = null, selectedCityName = null) => {
|
||||||
const citySelect = root.querySelector('[data-location-city]');
|
const citySelect = root.querySelector('[data-location-city]');
|
||||||
const countrySelect = root.querySelector('[data-location-country]');
|
const countrySelect = root.querySelector('[data-location-country]');
|
||||||
const statusText = root.querySelector('[data-location-status]');
|
const statusText = root.querySelector('[data-location-status]');
|
||||||
const template = root.dataset.citiesUrlTemplate ?? '';
|
const template = root.dataset.citiesUrlTemplate ?? '';
|
||||||
|
const normalizedCountryId = (countryId ?? '').toString().trim();
|
||||||
|
|
||||||
if (!citySelect || !countrySelect) {
|
if (!citySelect || !countrySelect) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!countryId || template === '') {
|
if (normalizedCountryId === '' || template === '') {
|
||||||
citySelect.innerHTML = '<option value="">Önce ülke seç</option>';
|
citySelect.innerHTML = '<option value="">Önce ülke seç</option>';
|
||||||
citySelect.disabled = true;
|
citySelect.disabled = true;
|
||||||
return;
|
return;
|
||||||
@ -446,7 +465,12 @@
|
|||||||
citySelect.innerHTML = '<option value="">Şehir yükleniyor...</option>';
|
citySelect.innerHTML = '<option value="">Şehir yükleniyor...</option>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const primaryUrl = template.replace('__COUNTRY__', encodeURIComponent(String(countryId)));
|
const primaryUrl = buildCitiesUrl(template, normalizedCountryId);
|
||||||
|
|
||||||
|
if (primaryUrl === '') {
|
||||||
|
throw new Error('city_url_invalid');
|
||||||
|
}
|
||||||
|
|
||||||
let cityOptions;
|
let cityOptions;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -474,6 +498,12 @@
|
|||||||
|
|
||||||
citySelect.innerHTML = '<option value="">Şehir seç</option>';
|
citySelect.innerHTML = '<option value="">Şehir seç</option>';
|
||||||
|
|
||||||
|
if (cityOptions.length === 0) {
|
||||||
|
citySelect.innerHTML = '<option value="">Şehir bulunamadı</option>';
|
||||||
|
citySelect.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
cityOptions.forEach((city) => {
|
cityOptions.forEach((city) => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = String(city.id ?? '');
|
option.value = String(city.id ?? '');
|
||||||
@ -593,10 +623,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const applyStored = async () => {
|
const applyStored = async () => {
|
||||||
if (stored?.countryId) {
|
if (stored && typeof stored === 'object') {
|
||||||
countrySelect.value = String(stored.countryId);
|
const matchedStoredCountry = Array.from(countrySelect.options).find((option) => {
|
||||||
await loadCities(root, stored.countryId, stored.cityId, stored.cityName);
|
if (stored.countryId && option.value === String(stored.countryId)) {
|
||||||
return;
|
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');
|
const defaultOption = Array.from(countrySelect.options).find((option) => option.dataset.default === '1');
|
||||||
|
|||||||