This commit is contained in:
fatihalp 2026-03-04 23:23:53 +03:00
parent bf4069d779
commit d91e38d475
20 changed files with 989 additions and 102 deletions

View File

@ -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();
}
} }

View File

@ -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();

View File

@ -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ıı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');
}
} }
} }
} }

View File

@ -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]);
}
}
} }
} }

View File

@ -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');

View File

@ -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;
}
} }

View File

@ -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';

View File

@ -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,

View 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',
],
];
}
}

View File

@ -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');
}
};

View File

@ -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;
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
public/sample_image/cup.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@ -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

View File

@ -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');