Compare commits

...

3 Commits

Author SHA1 Message Date
fatihalp
08d0b68349 22 2026-03-06 02:28:46 +03:00
fatihalp
aa7d2e27c0 Unify fonts and fix category seed 2026-03-06 02:15:13 +03:00
fatihalp
165585cdc4 Document listing updates 2026-03-06 01:00:23 +03:00
25 changed files with 1784 additions and 663 deletions

View File

@ -2,6 +2,7 @@
namespace Modules\Admin\Filament\Pages;
use App\Support\HomeSlideDefaults;
use App\Support\CountryCodeManager;
use App\Settings\GeneralSettings;
use BackedEnum;
@ -22,13 +23,13 @@ class ManageGeneralSettings extends SettingsPage
{
protected static string $settings = GeneralSettings::class;
protected static ?string $title = 'General Settings';
protected static ?string $title = 'Genel Ayarlar';
protected static ?string $navigationLabel = 'General Settings';
protected static ?string $navigationLabel = 'Genel Ayarlar';
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-cog-6-tooth';
protected static string | UnitEnum | null $navigationGroup = 'Settings';
protected static string | UnitEnum | null $navigationGroup = 'Ayarlar';
protected static ?int $navigationSort = 1;
@ -37,35 +38,35 @@ class ManageGeneralSettings extends SettingsPage
return $schema
->components([
TextInput::make('site_name')
->label('Site Name')
->label('Site Adı')
->required()
->maxLength(255),
Textarea::make('site_description')
->label('Site Description')
->label('Site ıklaması')
->rows(3)
->maxLength(500),
Repeater::make('home_slides')
->label('Home Slider')
->label('Ana Sayfa Slider')
->schema([
TextInput::make('badge')
->label('Badge')
->label('Rozet')
->required()
->maxLength(255),
TextInput::make('title')
->label('Title')
->label('Başlık')
->required()
->maxLength(255),
Textarea::make('subtitle')
->label('Subtitle')
->label('Alt Başlık')
->rows(2)
->required()
->maxLength(500),
TextInput::make('primary_button_text')
->label('Primary Button Text')
->label('Birincil Buton Metni')
->required()
->maxLength(120),
TextInput::make('secondary_button_text')
->label('Secondary Button Text')
->label('İkincil Buton Metni')
->required()
->maxLength(120),
])
@ -73,39 +74,39 @@ class ManageGeneralSettings extends SettingsPage
->minItems(1)
->collapsible()
->reorderableWithButtons()
->addActionLabel('Add Slide')
->addActionLabel('Slide Ekle')
->itemLabel(fn (array $state): ?string => filled($state['title'] ?? null) ? (string) $state['title'] : 'Slide')
->afterStateHydrated(fn (Repeater $component, $state) => $component->state($this->normalizeHomeSlides($state)))
->dehydrateStateUsing(fn ($state) => $this->normalizeHomeSlides($state)),
FileUpload::make('site_logo')
->label('Site Logo')
->label('Site Logosu')
->image()
->disk('public')
->directory('settings')
->visibility('public'),
TextInput::make('sender_name')
->label('Sender Name')
->label('Gönderici Adı')
->required()
->maxLength(120),
TextInput::make('sender_email')
->label('Sender Email')
->label('Gönderici E-postası')
->email()
->required()
->maxLength(255),
Select::make('default_language')
->label('Default Language')
->label('Varsayılan Dil')
->options($this->localeOptions())
->required()
->searchable(),
CountryCodeSelect::make('default_country_code')
->label('Default Country')
->label('Varsayılan Ülke')
->default('+90')
->required()
->helperText('Used as default country in panel forms.'),
->helperText('Panel formlarında varsayılan ülke olarak kullanılır.'),
TagsInput::make('currencies')
->label('Currencies')
->placeholder('USD')
->helperText('Add 3-letter currency codes like USD, EUR, TRY.')
->label('Para Birimleri')
->placeholder('TRY')
->helperText('TRY, USD, EUR gibi 3 harfli para birimi kodları ekleyin.')
->required()
->rules(['array', 'min:1'])
->afterStateHydrated(fn (TagsInput $component, $state) => $component->state($this->normalizeCurrencies($state)))
@ -125,19 +126,19 @@ class ManageGeneralSettings extends SettingsPage
->defaultCountry(CountryCodeManager::defaultCountryIso2())
->nullable()
->formatAsYouType()
->helperText('Use international format, e.g. +905551112233.'),
->helperText('Uluslararası format kullanın. Örnek: +905551112233'),
Toggle::make('enable_google_maps')
->label('Enable Google Maps')
->label('Google Maps Aktif')
->default(false),
TextInput::make('google_maps_api_key')
->label('Google Maps API Key')
->label('Google Maps API Anahtarı')
->password()
->revealable()
->nullable()
->maxLength(255)
->helperText('Required to enable map fields in listing forms.'),
->helperText('İlan formlarındaki harita alanlarını açmak için gereklidir.'),
Toggle::make('enable_google_login')
->label('Enable Google Login')
->label('Google ile Giriş Aktif')
->default(false),
TextInput::make('google_client_id')
->label('Google Client ID')
@ -150,7 +151,7 @@ class ManageGeneralSettings extends SettingsPage
->nullable()
->maxLength(255),
Toggle::make('enable_facebook_login')
->label('Enable Facebook Login')
->label('Facebook ile Giriş Aktif')
->default(false),
TextInput::make('facebook_client_id')
->label('Facebook Client ID')
@ -163,7 +164,7 @@ class ManageGeneralSettings extends SettingsPage
->nullable()
->maxLength(255),
Toggle::make('enable_apple_login')
->label('Enable Apple Login')
->label('Apple ile Giriş Aktif')
->default(false),
TextInput::make('apple_client_id')
->label('Apple Client ID')
@ -183,14 +184,6 @@ class ManageGeneralSettings extends SettingsPage
$labels = [
'en' => 'English',
'tr' => 'Türkçe',
'ar' => 'العربية',
'zh' => '中文',
'es' => 'Español',
'fr' => 'Français',
'de' => 'Deutsch',
'pt' => 'Português',
'ru' => 'Русский',
'ja' => '日本語',
];
return collect(config('app.available_locales', ['en']))
@ -215,47 +208,11 @@ class ManageGeneralSettings extends SettingsPage
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',
],
];
return HomeSlideDefaults::defaults();
}
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();
return HomeSlideDefaults::normalize($state);
}
}

View File

@ -23,6 +23,7 @@ use Filament\Schemas\Schema;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\SpatieMediaLibraryImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Filters\TernaryFilter;
@ -135,15 +136,15 @@ class ListingResource extends Resource
->circular(),
TextColumn::make('id')->sortable(),
TextColumn::make('title')->searchable()->sortable()->limit(40),
TextColumn::make('category.name')->label('Category'),
TextColumn::make('user.email')->label('Owner')->searchable()->toggleable(),
TextColumn::make('category.name')->label('Category')->sortable(),
TextColumn::make('user.email')->label('Owner')->searchable()->toggleable()->sortable(),
TextColumn::make('price')
->currency(fn (Listing $record): string => $record->currency ?: ListingPanelHelper::defaultCurrency())
->sortable(),
StateFusionSelectColumn::make('status'),
IconColumn::make('is_featured')->boolean()->label('Featured'),
TextColumn::make('city'),
TextColumn::make('country'),
StateFusionSelectColumn::make('status')->sortable(),
IconColumn::make('is_featured')->boolean()->label('Featured')->sortable(),
TextColumn::make('city')->sortable(),
TextColumn::make('country')->sortable(),
TextColumn::make('created_at')->dateTime()->sortable(),
])->filters([
StateFusionSelectFilter::make('status'),
@ -188,13 +189,18 @@ class ListingResource extends Resource
->query(fn (Builder $query, array $data): Builder => $query
->when($data['min'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '>=', (float) $amount))
->when($data['max'] ?? null, fn (Builder $query, string $amount): Builder => $query->where('price', '<=', (float) $amount))),
])->actions([
])
->filtersLayout(FiltersLayout::AboveContent)
->filtersFormColumns(3)
->filtersFormWidth('7xl')
->persistFiltersInSession()
->actions([
EditAction::make(),
Action::make('activities')
->icon('heroicon-o-clock')
->url(fn (Listing $record): string => static::getUrl('activities', ['record' => $record])),
DeleteAction::make(),
]);
]);
}
public static function getPages(): array

View File

@ -3,6 +3,7 @@ namespace Modules\Category\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Modules\Theme\Support\ThemeManager;
class CategoryController extends Controller
@ -24,7 +25,11 @@ class CategoryController extends Controller
'children' => fn ($query) => $query->active()->ordered(),
]);
$listings = $category->activeListings()
$categoryIds = $category->descendantAndSelfIds()->all();
$listings = Listing::query()
->where('status', 'active')
->whereIn('category_id', $categoryIds)
->with('category:id,name')
->latest('id')
->paginate(12);

View File

@ -72,12 +72,39 @@ class Category extends Model
->active()
->whereNull('parent_id')
->with([
'children' => fn (HasMany $query) => $query->active()->ordered(),
'children' => fn ($query) => $query->active()->ordered(),
])
->ordered()
->get();
}
public function descendantAndSelfIds(): Collection
{
$ids = collect([(int) $this->getKey()]);
$frontier = $ids;
while ($frontier->isNotEmpty()) {
$children = static::query()
->whereIn('parent_id', $frontier->all())
->pluck('id')
->map(fn ($id): int => (int) $id)
->values();
if ($children->isEmpty()) {
break;
}
$ids = $ids
->merge($children)
->unique()
->values();
$frontier = $children;
}
return $ids;
}
public function breadcrumbTrail(): Collection
{
$trail = collect();

View File

@ -5,10 +5,12 @@ namespace Modules\Conversation\App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Schema;
use Illuminate\View\View;
use Modules\Conversation\App\Models\Conversation;
use Modules\Conversation\App\Support\QuickMessageCatalog;
use Modules\Listing\Models\Listing;
use Throwable;
class ConversationController extends Controller
{
@ -17,20 +19,30 @@ class ConversationController extends Controller
$userId = (int) $request->user()->getKey();
$messageFilter = $this->resolveMessageFilter($request);
$conversations = Conversation::inboxForUser($userId, $messageFilter);
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
$conversations = collect();
$selectedConversation = null;
if ($selectedConversation) {
$selectedConversation->loadThread();
$selectedConversation->markAsReadFor($userId);
if ($this->messagingTablesReady()) {
try {
$conversations = Conversation::inboxForUser($userId, $messageFilter);
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
$conversation->unread_count = 0;
if ($selectedConversation) {
$selectedConversation->loadThread();
$selectedConversation->markAsReadFor($userId);
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
$conversation->unread_count = 0;
}
return $conversation;
});
}
return $conversation;
});
} catch (Throwable) {
$conversations = collect();
$selectedConversation = null;
}
}
return view('conversation::inbox', [
@ -43,6 +55,10 @@ class ConversationController extends Controller
public function start(Request $request, Listing $listing): RedirectResponse
{
if (! $this->messagingTablesReady()) {
return back()->with('error', 'Mesajlaşma altyapısı henüz hazır değil.');
}
$user = $request->user();
if (! $listing->user_id) {
@ -75,6 +91,10 @@ class ConversationController extends Controller
public function send(Request $request, Conversation $conversation): RedirectResponse
{
if (! $this->messagingTablesReady()) {
return back()->with('error', 'Mesajlaşma altyapısı henüz hazır değil.');
}
$user = $request->user();
$userId = (int) $user->getKey();
@ -117,4 +137,13 @@ class ConversationController extends Controller
return in_array($messageFilter, ['all', 'unread', 'important'], true) ? $messageFilter : 'all';
}
private function messagingTablesReady(): bool
{
try {
return Schema::hasTable('conversations') && Schema::hasTable('conversation_messages');
} catch (Throwable) {
return false;
}
}
}

View File

@ -4,12 +4,15 @@ namespace Modules\Favorite\App\Http\Controllers;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Schema;
use Modules\Category\Models\Category;
use Modules\Conversation\App\Models\Conversation;
use Modules\Conversation\App\Support\QuickMessageCatalog;
use Modules\Favorite\App\Models\FavoriteSearch;
use Modules\Listing\Models\Listing;
use Modules\User\App\Models\User;
use Throwable;
class FavoriteController extends Controller
{
@ -37,68 +40,94 @@ class FavoriteController extends Controller
$user = $request->user();
$categories = Category::query()
->where('is_active', true)
->orderBy('name')
->get(['id', 'name']);
$categories = collect();
if ($this->tableExists('categories')) {
$categories = Category::query()
->where('is_active', true)
->orderBy('name')
->get(['id', 'name']);
}
$favoriteListings = null;
$favoriteSearches = null;
$favoriteSellers = null;
$favoriteListings = $this->emptyPaginator();
$favoriteSearches = $this->emptyPaginator();
$favoriteSellers = $this->emptyPaginator();
$conversations = collect();
$selectedConversation = null;
$buyerConversationListingMap = [];
if ($activeTab === 'listings') {
$favoriteListings = $user->favoriteListings()
->with(['category:id,name', 'user:id,name'])
->wherePivot('created_at', '>=', now()->subYear())
->when($statusFilter === 'active', fn ($query) => $query->where('status', 'active'))
->when($selectedCategoryId, fn ($query) => $query->where('category_id', $selectedCategoryId))
->orderByPivot('created_at', 'desc')
->paginate(10)
->withQueryString();
try {
if ($this->tableExists('favorite_listings')) {
$favoriteListings = $user->favoriteListings()
->with(['category:id,name', 'user:id,name'])
->wherePivot('created_at', '>=', now()->subYear())
->when($statusFilter === 'active', fn ($query) => $query->where('status', 'active'))
->when($selectedCategoryId, fn ($query) => $query->where('category_id', $selectedCategoryId))
->orderByPivot('created_at', 'desc')
->paginate(10)
->withQueryString();
}
$userId = (int) $user->getKey();
$conversations = Conversation::inboxForUser($userId, $messageFilter);
$buyerConversationListingMap = $conversations
->where('buyer_id', $userId)
->pluck('id', 'listing_id')
->map(fn ($conversationId) => (int) $conversationId)
->all();
if ($this->tableExists('conversations') && $this->tableExists('conversation_messages')) {
$userId = (int) $user->getKey();
$conversations = Conversation::inboxForUser($userId, $messageFilter);
$buyerConversationListingMap = $conversations
->where('buyer_id', $userId)
->pluck('id', 'listing_id')
->map(fn ($conversationId) => (int) $conversationId)
->all();
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
if ($selectedConversation) {
$selectedConversation->loadThread();
$selectedConversation->markAsReadFor($userId);
if ($selectedConversation) {
$selectedConversation->loadThread();
$selectedConversation->markAsReadFor($userId);
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
$conversation->unread_count = 0;
$conversations = $conversations->map(function (Conversation $conversation) use ($selectedConversation): Conversation {
if ((int) $conversation->getKey() === (int) $selectedConversation->getKey()) {
$conversation->unread_count = 0;
}
return $conversation;
});
}
return $conversation;
});
}
} catch (Throwable) {
$favoriteListings = $this->emptyPaginator();
$conversations = collect();
$selectedConversation = null;
$buyerConversationListingMap = [];
}
}
if ($activeTab === 'searches') {
$favoriteSearches = $user->favoriteSearches()
->with('category:id,name')
->latest()
->paginate(10)
->withQueryString();
try {
if ($this->tableExists('favorite_searches')) {
$favoriteSearches = $user->favoriteSearches()
->with('category:id,name')
->latest()
->paginate(10)
->withQueryString();
}
} catch (Throwable) {
$favoriteSearches = $this->emptyPaginator();
}
}
if ($activeTab === 'sellers') {
$favoriteSellers = $user->favoriteSellers()
->withCount([
'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
])
->orderByPivot('created_at', 'desc')
->paginate(10)
->withQueryString();
try {
if ($this->tableExists('favorite_sellers')) {
$favoriteSellers = $user->favoriteSellers()
->withCount([
'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
])
->orderByPivot('created_at', 'desc')
->paginate(10)
->withQueryString();
}
} catch (Throwable) {
$favoriteSellers = $this->emptyPaginator();
}
}
return view('favorite::index', [
@ -189,4 +218,21 @@ class FavoriteController extends Controller
return back()->with('success', 'Favori arama silindi.');
}
private function tableExists(string $table): bool
{
try {
return Schema::hasTable($table);
} catch (Throwable) {
return false;
}
}
private function emptyPaginator(): LengthAwarePaginator
{
return new LengthAwarePaginator([], 0, 10, 1, [
'path' => request()->url(),
'query' => request()->query(),
]);
}
}

View File

@ -2,96 +2,54 @@
namespace Modules\Listing\Database\Seeders;
use Modules\User\App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Modules\Category\Models\Category;
use Modules\Listing\Models\Listing;
use Modules\Location\Models\City;
use Modules\Location\Models\Country;
use Modules\User\App\Models\User;
class ListingSeeder extends Seeder
{
private const LISTINGS = [
[
'title' => 'iPhone 14 Pro 256 GB, temiz kullanılmış',
'description' => 'Cihaz sorunsuz çalışıyor, pil sağlığı iyi durumda. Kutusu ve şarj kablosu ile teslim edilecektir.',
'price' => 44999,
'city' => 'İstanbul',
'country' => 'Türkiye',
'image' => 'sample_image/phone.jpeg',
],
[
'title' => 'MacBook Pro M2 16 GB / 512 GB',
'description' => 'Yazılım geliştirme için kullanıldı. Kozmetik olarak çok iyi durumda, faturası mevcut.',
'price' => 62999,
'city' => 'Ankara',
'country' => 'Türkiye',
'image' => 'sample_image/macbook.jpg',
],
[
'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',
'image' => 'sample_image/car.jpeg',
],
[
'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',
'image' => 'sample_image/headphones.jpg',
],
[
'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',
'image' => 'sample_image/laptop.jpg',
],
[
'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',
'image' => 'sample_image/cup.jpg',
],
[
'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',
'image' => 'sample_image/car2.jpeg',
],
private const SAMPLE_IMAGES = [
'sample_image/phone.jpeg',
'sample_image/macbook.jpg',
'sample_image/car.jpeg',
'sample_image/headphones.jpg',
'sample_image/laptop.jpg',
'sample_image/cup.jpg',
'sample_image/car2.jpeg',
];
private const TITLE_PREFIXES = [
'Temiz kullanılmış',
'Az kullanılmış',
'Fırsat ürün',
'Uygun fiyatlı',
'Sahibinden',
'Kaçırılmayacak',
'Bakımlı',
];
public function run(): void
{
$user = $this->resolveSeederUser();
$categories = Category::query()
->where('level', 0)
->orderBy('sort_order')
->orderBy('name')
->get();
$categories = $this->resolveSeedableCategories();
if (! $user || $categories->isEmpty()) {
return;
}
foreach (self::LISTINGS as $index => $data) {
$listing = $this->upsertListing(
index: $index,
data: $data,
categories: $categories,
user: $user,
);
$countries = $this->resolveCountries();
$turkeyCities = $this->resolveTurkeyCities();
$this->syncListingImage($listing, $data['image']);
foreach ($categories as $index => $category) {
$listingData = $this->buildListingData($category, $index, $countries, $turkeyCities);
$listing = $this->upsertListing($index, $listingData, $category, $user);
$this->syncListingImage($listing, $listingData['image']);
}
}
@ -103,10 +61,160 @@ class ListingSeeder extends Seeder
->first();
}
private function upsertListing(int $index, array $data, Collection $categories, User $user): Listing
private function resolveSeedableCategories(): Collection
{
$slug = Str::slug($data['title']) . '-' . ($index + 1);
$category = $categories->get($index % $categories->count());
$leafCategories = Category::query()
->where('is_active', true)
->whereDoesntHave('children')
->orderBy('sort_order')
->orderBy('name')
->get();
if ($leafCategories->isNotEmpty()) {
return $leafCategories->values();
}
return Category::query()
->where('is_active', true)
->orderBy('sort_order')
->orderBy('name')
->get()
->values();
}
private function resolveCountries(): Collection
{
if (! class_exists(Country::class) || ! Schema::hasTable('countries')) {
return collect();
}
return Country::query()
->where('is_active', true)
->orderBy('name')
->get(['id', 'name', 'code'])
->values();
}
private function resolveTurkeyCities(): Collection
{
if (! class_exists(City::class) || ! Schema::hasTable('cities') || ! Schema::hasTable('countries')) {
return collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
}
$turkey = Country::query()
->where('code', 'TR')
->first(['id']);
if (! $turkey) {
return collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
}
$cities = City::query()
->where('country_id', (int) $turkey->id)
->where('is_active', true)
->orderBy('name')
->pluck('name')
->map(fn ($name): string => trim((string) $name))
->filter(fn (string $name): bool => $name !== '')
->values();
return $cities->isNotEmpty()
? $cities
: collect(['İstanbul', 'Ankara', 'İzmir', 'Bursa', 'Antalya']);
}
private function buildListingData(
Category $category,
int $index,
Collection $countries,
Collection $turkeyCities
): array {
$location = $this->resolveLocation($index, $countries, $turkeyCities);
$image = self::SAMPLE_IMAGES[$index % count(self::SAMPLE_IMAGES)];
return [
'title' => $this->buildTitle($category, $index),
'description' => $this->buildDescription($category, $location['city'], $location['country']),
'price' => $this->priceForIndex($index),
'city' => $location['city'],
'country' => $location['country'],
'image' => $image,
];
}
private function resolveLocation(int $index, Collection $countries, Collection $turkeyCities): array
{
$turkeyCountry = $countries->first(fn ($country): bool => strtoupper((string) $country->code) === 'TR');
$turkeyName = trim((string) ($turkeyCountry->name ?? 'Türkiye')) ?: 'Türkiye';
$useForeignCountry = $countries->count() > 1 && $index % 4 === 0;
if ($useForeignCountry) {
$foreignCountries = $countries
->filter(fn ($country): bool => strtoupper((string) $country->code) !== 'TR')
->values();
if ($foreignCountries->isNotEmpty()) {
$selected = $foreignCountries->get($index % $foreignCountries->count());
$countryName = trim((string) ($selected->name ?? ''));
return [
'country' => $countryName !== '' ? $countryName : 'Türkiye',
'city' => $countryName !== '' ? $countryName : 'İstanbul',
];
}
}
$city = trim((string) $turkeyCities->get($index % max(1, $turkeyCities->count())));
return [
'country' => $turkeyName,
'city' => $city !== '' ? $city : 'İstanbul',
];
}
private function buildTitle(Category $category, int $index): string
{
$prefix = self::TITLE_PREFIXES[$index % count(self::TITLE_PREFIXES)];
$categoryName = trim((string) $category->name);
return sprintf('%s %s ilanı', $prefix, $categoryName !== '' ? $categoryName : 'ürün');
}
private function buildDescription(Category $category, string $city, string $country): string
{
$categoryName = trim((string) $category->name);
$location = trim(collect([$city, $country])->filter()->join(', '));
return sprintf(
'%s kategorisinde, durum olarak sorunsuz ve kullanıma hazırdır. Teslimat noktası: %s. Detaylar için mesaj atabilirsiniz.',
$categoryName !== '' ? $categoryName : 'Ürün',
$location !== '' ? $location : 'Türkiye'
);
}
private function priceForIndex(int $index): int
{
$basePrices = [
1499,
3250,
6490,
11800,
26500,
44990,
82000,
135000,
];
$base = $basePrices[$index % count($basePrices)];
$step = (int) floor($index / count($basePrices)) * 750;
return $base + $step;
}
private function upsertListing(int $index, array $data, Category $category, User $user): Listing
{
$slug = Str::slug($category->slug.'-'.$data['title']).'-'.($index + 1);
return Listing::updateOrCreate(
['slug' => $slug],
@ -118,12 +226,12 @@ class ListingSeeder extends Seeder
'currency' => 'TRY',
'city' => $data['city'],
'country' => $data['country'],
'category_id' => $category?->id,
'category_id' => $category->id,
'user_id' => $user->id,
'status' => 'active',
'contact_email' => $user->email,
'contact_phone' => '+905551112233',
'is_featured' => $index < 3,
'is_featured' => $index < 8,
]
);
}

View File

@ -22,42 +22,14 @@
], $normalizeQuery);
@endphp
<style>
.listing-filter-card {
border: 1px solid #d7dbe7;
border-radius: 14px;
background: #ffffff;
}
.listing-card {
border: 1px solid #d7dbe7;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
transition: box-shadow .2s ease, transform .2s ease;
}
.listing-card:hover {
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
transform: translateY(-2px);
}
.listing-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
<h1 class="text-[30px] leading-tight font-extrabold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
<aside class="space-y-4">
<section class="listing-filter-card p-4">
<div class="flex items-center justify-between gap-3 mb-3">
<h2 class="text-[26px] font-extrabold text-slate-900 leading-none">Kategoriler</h2>
<h2 class="text-2xl font-bold text-slate-900 leading-none">Kategoriler</h2>
</div>
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
@ -301,7 +273,7 @@
<div class="px-3.5 py-3">
<a href="{{ route('listings.show', $listing) }}" class="block">
<p class="text-[29px] leading-none font-extrabold text-slate-900">
<p class="text-3xl leading-none font-bold text-slate-900">
@if(!is_null($priceValue) && $priceValue > 0)
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
@else

View File

@ -22,42 +22,14 @@
], $normalizeQuery);
@endphp
<style>
.listing-filter-card {
border: 1px solid #d7dbe7;
border-radius: 14px;
background: #ffffff;
}
.listing-card {
border: 1px solid #d7dbe7;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
transition: box-shadow .2s ease, transform .2s ease;
}
.listing-card:hover {
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
transform: translateY(-2px);
}
.listing-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
<h1 class="text-[30px] leading-tight font-extrabold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
<aside class="space-y-4">
<section class="listing-filter-card p-4">
<div class="flex items-center justify-between gap-3 mb-3">
<h2 class="text-[26px] font-extrabold text-slate-900 leading-none">Kategoriler</h2>
<h2 class="text-2xl font-bold text-slate-900 leading-none">Kategoriler</h2>
</div>
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
@ -301,7 +273,7 @@
<div class="px-3.5 py-3">
<a href="{{ route('listings.show', $listing) }}" class="block">
<p class="text-[29px] leading-none font-extrabold text-slate-900">
<p class="text-3xl leading-none font-bold text-slate-900">
@if(!is_null($priceValue) && $priceValue > 0)
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
@else

View File

@ -22,42 +22,14 @@
], $normalizeQuery);
@endphp
<style>
.listing-filter-card {
border: 1px solid #d7dbe7;
border-radius: 14px;
background: #ffffff;
}
.listing-card {
border: 1px solid #d7dbe7;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
transition: box-shadow .2s ease, transform .2s ease;
}
.listing-card:hover {
box-shadow: 0 16px 32px rgba(22, 29, 57, 0.11);
transform: translateY(-2px);
}
.listing-title {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
</style>
<div class="max-w-[1320px] mx-auto px-4 py-7 lg:py-8">
<h1 class="text-[30px] leading-tight font-extrabold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<h1 class="text-3xl md:text-4xl leading-tight font-bold text-slate-900 mb-6">{{ $pageTitle }}</h1>
<div class="grid grid-cols-1 lg:grid-cols-[260px,1fr] gap-4 lg:gap-5">
<aside class="space-y-4">
<section class="listing-filter-card p-4">
<div class="flex items-center justify-between gap-3 mb-3">
<h2 class="text-[26px] font-extrabold text-slate-900 leading-none">Kategoriler</h2>
<h2 class="text-2xl font-bold text-slate-900 leading-none">Kategoriler</h2>
</div>
<div class="space-y-1 max-h-[330px] overflow-y-auto pr-1">
@ -301,7 +273,7 @@
<div class="px-3.5 py-3">
<a href="{{ route('listings.show', $listing) }}" class="block">
<p class="text-[29px] leading-none font-extrabold text-slate-900">
<p class="text-3xl leading-none font-bold text-slate-900">
@if(!is_null($priceValue) && $priceValue > 0)
{{ number_format($priceValue, 0, ',', '.') }} {{ $listing->currency }}
@else

View File

@ -28,108 +28,6 @@
: 'Yeni üye';
@endphp
<style>
.lt-wrap { max-width: 1320px; margin: 0 auto; padding: 24px 16px 46px; }
.lt-breadcrumb { display: flex; flex-wrap: wrap; gap: 8px; font-size: 13px; color: #6b7280; margin-bottom: 14px; }
.lt-breadcrumb a { color: #4b5563; text-decoration: none; }
.lt-breadcrumb span:last-child { color: #111827; font-weight: 700; }
.lt-grid { display: grid; grid-template-columns: minmax(0, 1fr) 352px; gap: 18px; align-items: start; }
.lt-card { border: 1px solid #d8dce4; border-radius: 14px; background: #f7f7f8; box-shadow: 0 1px 2px rgba(16, 24, 40, .05); }
.lt-gallery-main { position: relative; border-radius: 10px; background: #1f2937; overflow: hidden; min-height: 440px; }
.lt-gallery-main img { width: 100%; height: 100%; object-fit: cover; min-height: 440px; }
.lt-gallery-main-empty { min-height: 440px; display: grid; place-items: center; color: #cbd5e1; font-size: 14px; }
.lt-gallery-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 44px; height: 44px; border: 0; border-radius: 999px; background: rgba(255,255,255,.92); color: #111827; display: grid; place-items: center; cursor: pointer; }
.lt-gallery-nav[data-gallery-prev] { left: 14px; }
.lt-gallery-nav[data-gallery-next] { right: 14px; }
.lt-gallery-top { position: absolute; top: 12px; left: 12px; right: 12px; display: flex; justify-content: space-between; align-items: center; }
.lt-badge { border-radius: 999px; background: #ffd814; color: #111827; font-size: 12px; font-weight: 700; padding: 6px 10px; }
.lt-icon-btn { width: 38px; height: 38px; border: 0; border-radius: 999px; background: rgba(17, 24, 39, .86); color: #fff; display: inline-flex; align-items: center; justify-content: center; }
.lt-thumbs { display: flex; gap: 10px; overflow-x: auto; padding: 12px 0 2px; }
.lt-thumb { width: 86px; min-width: 86px; height: 64px; border: 2px solid transparent; border-radius: 10px; overflow: hidden; background: #d1d5db; cursor: pointer; }
.lt-thumb.is-active { border-color: #ff3a59; }
.lt-thumb img { width: 100%; height: 100%; object-fit: cover; }
.lt-media-card { padding: 14px; }
.lt-detail-card { margin-top: 14px; padding: 18px 20px; }
.lt-price-row { display: flex; flex-wrap: wrap; gap: 12px; justify-content: space-between; align-items: flex-start; }
.lt-price { font-size: 46px; line-height: 1; font-weight: 900; color: #0f172a; }
.lt-title { margin-top: 8px; font-size: 21px; font-weight: 700; color: #111827; }
.lt-meta { text-align: right; color: #4b5563; font-size: 14px; }
.lt-meta strong { color: #111827; font-weight: 700; }
.lt-credit { margin-top: 14px; border: 1px solid #e3e7ee; border-radius: 12px; padding: 14px; background: #fafafb; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.lt-credit h4 { margin: 0; font-size: 20px; color: #0f172a; }
.lt-credit p { margin: 3px 0 0; font-size: 14px; color: #4b5563; }
.lt-tag { border-radius: 999px; background: #2f80ed; color: #fff; font-size: 13px; font-weight: 700; padding: 7px 12px; }
.lt-section-title { margin: 18px 0 10px; font-size: 30px; font-weight: 900; color: #111827; }
.lt-features { border-top: 1px solid #e2e8f0; margin-top: 12px; }
.lt-feature-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; border-top: 1px solid #e7ebf2; padding: 12px 0; }
.lt-feature-row:first-child { border-top: 0; }
.lt-f-item { display: flex; justify-content: space-between; gap: 8px; color: #334155; font-size: 15px; }
.lt-f-item strong { color: #111827; font-weight: 800; text-align: right; }
.lt-side-card { position: sticky; top: 96px; padding: 16px; }
.lt-seller-head { display: flex; align-items: center; gap: 10px; }
.lt-avatar { width: 44px; height: 44px; border-radius: 999px; background: #f3f4f6; color: #111827; display: grid; place-items: center; font-weight: 800; }
.lt-seller-name { margin: 0; font-size: 31px; font-weight: 800; color: #111827; line-height: 1.1; }
.lt-seller-meta { margin-top: 2px; font-size: 13px; color: #6b7280; }
.lt-actions { margin-top: 14px; display: grid; gap: 10px; }
.lt-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.lt-btn { height: 46px; border-radius: 999px; border: 1px solid #f3ced6; background: #f8e6ea; color: #e11d48; font-size: 20px; font-weight: 800; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; }
.lt-btn:disabled { opacity: .45; cursor: not-allowed; }
.lt-btn-main { border: 0; background: #ff3a59; color: #fff; width: 100%; }
.lt-btn-soft { border-color: #efdde1; background: #f5eaed; }
.lt-btn-outline { border-color: #d4d8e0; background: #fff; color: #334155; }
.lt-report { margin-top: 16px; height: 54px; border: 1px solid #e3e7ee; border-radius: 999px; background: #f7f7f8; color: #e11d48; font-size: 16px; font-weight: 700; display: grid; place-items: center; text-decoration: none; }
.lt-policy { margin-top: 16px; text-align: center; color: #6b7280; font-size: 13px; }
.lt-related { margin-top: 26px; }
.lt-related-head { display: flex; justify-content: space-between; align-items: center; }
.lt-related-title { font-size: 30px; font-weight: 900; margin: 0; }
.lt-scroll-wrap { position: relative; margin-top: 14px; }
.lt-scroll-track { display: flex; gap: 12px; overflow-x: auto; scroll-behavior: smooth; padding: 2px 2px 8px; }
.lt-rel-card { min-width: 232px; width: 232px; border: 1px solid #d8dce4; border-radius: 10px; background: #f7f7f8; overflow: hidden; text-decoration: none; color: inherit; }
.lt-rel-photo { height: 168px; background: #d1d5db; }
.lt-rel-photo img { width: 100%; height: 100%; object-fit: cover; }
.lt-rel-body { padding: 10px; }
.lt-rel-price { font-size: 32px; font-weight: 900; color: #111827; line-height: 1.1; }
.lt-rel-title { margin-top: 4px; font-size: 20px; font-weight: 700; color: #111827; line-height: 1.3; min-height: 52px; }
.lt-rel-city { margin-top: 6px; font-size: 13px; color: #6b7280; }
.lt-scroll-btn { position: absolute; top: 42%; transform: translateY(-50%); width: 44px; height: 44px; border: 0; border-radius: 999px; background: rgba(255,255,255,.92); box-shadow: 0 1px 4px rgba(15,23,42,.18); display: grid; place-items: center; cursor: pointer; }
.lt-scroll-btn.prev { left: -16px; }
.lt-scroll-btn.next { right: -16px; }
.lt-pill-wrap { margin-top: 20px; }
.lt-pill-title { margin: 0 0 10px; font-size: 30px; font-weight: 900; }
.lt-pills { display: flex; flex-wrap: wrap; gap: 10px; }
.lt-pill { border: 1px solid #d4d8e0; background: #f4f5f7; border-radius: 999px; padding: 8px 14px; color: #374151; text-decoration: none; font-size: 14px; font-weight: 600; }
@media (max-width: 1080px) {
.lt-grid { grid-template-columns: 1fr; }
.lt-side-card { position: static; }
.lt-scroll-btn { display: none; }
.lt-price { font-size: 39px; }
.lt-seller-name, .lt-section-title, .lt-related-title, .lt-pill-title { font-size: 24px; }
}
@media (max-width: 640px) {
.lt-wrap { padding: 16px 10px 30px; }
.lt-detail-card, .lt-media-card, .lt-side-card { padding: 12px; }
.lt-gallery-main, .lt-gallery-main img, .lt-gallery-main-empty { min-height: 260px; }
.lt-feature-row { grid-template-columns: 1fr; gap: 10px; }
.lt-price-row { flex-direction: column; }
.lt-meta { text-align: left; }
.lt-rel-card { min-width: 196px; width: 196px; }
.lt-rel-photo { height: 140px; }
}
</style>
<div class="lt-wrap">
<nav class="lt-breadcrumb" aria-label="breadcrumb">
<a href="{{ route('home') }}">Anasayfa</a>

View File

@ -16,7 +16,7 @@ Route::get('/locations/cities/{country}', function (string $country) {
$countryModel = Country::query()
->where(function ($query) use ($lookupValue, $lookupCode, $lookupName): void {
if (ctype_digit($lookupValue)) {
$query->orWhereKey((int) $lookupValue);
$query->orWhere('id', (int) $lookupValue);
}
$query

View File

@ -479,9 +479,13 @@ class QuickCreateListing extends Page
'description' => ['required', 'string', 'max:1450'],
'selectedCountryId' => ['required', 'integer', Rule::in(collect($this->countries)->pluck('id')->all())],
'selectedCityId' => [
'required',
'nullable',
'integer',
function (string $attribute, mixed $value, \Closure $fail): void {
if (is_null($value) || $value === '') {
return;
}
$cityExists = collect($this->availableCities)
->contains(fn (array $city): bool => $city['id'] === (int) $value);
@ -498,7 +502,6 @@ class QuickCreateListing extends Page
'description.required' => 'Açıklama zorunludur.',
'description.max' => 'Açıklama en fazla 1450 karakter olabilir.',
'selectedCountryId.required' => 'Ülke seçimi zorunludur.',
'selectedCityId.required' => 'Şehir seçimi zorunludur.',
]);
}

View File

@ -415,9 +415,13 @@ class PanelQuickListingForm extends Component
'description' => ['required', 'string', 'max:1450'],
'selectedCountryId' => ['required', 'integer', Rule::in(collect($this->countries)->pluck('id')->all())],
'selectedCityId' => [
'required',
'nullable',
'integer',
function (string $attribute, mixed $value, \Closure $fail): void {
if (is_null($value) || $value === '') {
return;
}
$cityExists = collect($this->availableCities)
->contains(fn (array $city): bool => $city['id'] === (int) $value);
@ -434,7 +438,6 @@ class PanelQuickListingForm extends Component
'description.required' => 'Açıklama zorunludur.',
'description.max' => 'Açıklama en fazla 1450 karakter olabilir.',
'selectedCountryId.required' => 'Ülke seçimi zorunludur.',
'selectedCityId.required' => 'Şehir seçimi zorunludur.',
]);
}

View File

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Support\CountryCodeManager;
use App\Support\HomeSlideDefaults;
use App\Settings\GeneralSettings;
use BezhanSalleh\LanguageSwitch\LanguageSwitch;
use Illuminate\Support\ServiceProvider;
@ -36,9 +37,9 @@ class AppServiceProvider extends ServiceProvider
View::addNamespace('app', resource_path('views'));
$fallbackName = config('app.name', 'OpenClassify');
$fallbackLocale = config('app.locale', 'en');
$fallbackLocale = config('app.locale', 'tr');
$fallbackCurrencies = $this->normalizeCurrencies(config('app.currencies', ['USD']));
$fallbackDescription = 'The marketplace for buying and selling everything.';
$fallbackDescription = 'Alım satım için hızlı ve güvenli ilan platformu.';
$fallbackHomeSlides = $this->defaultHomeSlides();
$fallbackGoogleMapsApiKey = env('GOOGLE_MAPS_API_KEY');
$fallbackGoogleClientId = env('GOOGLE_CLIENT_ID');
@ -100,7 +101,7 @@ class AppServiceProvider extends ServiceProvider
$appleClientId = trim((string) ($settings->apple_client_id ?: $fallbackAppleClientId));
$appleClientSecret = trim((string) ($settings->apple_client_secret ?: $fallbackAppleClientSecret));
$defaultCountryCode = CountryCodeManager::normalizeCountryCode($settings->default_country_code ?? $fallbackDefaultCountryCode);
$homeSlides = $this->normalizeHomeSlides($settings->home_slides ?? [], $fallbackHomeSlides);
$homeSlides = $this->normalizeHomeSlides($settings->home_slides ?? []);
$generalSettings = [
'site_name' => trim((string) ($settings->site_name ?: $fallbackName)),
@ -178,14 +179,6 @@ class AppServiceProvider extends ServiceProvider
$localeLabels = [
'en' => 'English',
'tr' => 'Türkçe',
'ar' => 'العربية',
'zh' => '中文',
'es' => 'Español',
'fr' => 'Français',
'de' => 'Deutsch',
'pt' => 'Português',
'ru' => 'Русский',
'ja' => '日本語',
];
LanguageSwitch::configureUsing(function (LanguageSwitch $switch) use ($availableLocales, $localeLabels): void {
@ -260,56 +253,11 @@ class AppServiceProvider extends ServiceProvider
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',
],
];
return HomeSlideDefaults::defaults();
}
private function normalizeHomeSlides(mixed $slides, array $fallbackSlides): array
private function normalizeHomeSlides(mixed $slides): 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;
return HomeSlideDefaults::normalize($slides);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace App\Support;
final class HomeSlideDefaults
{
/**
* @return array<int, array{badge: string, title: string, subtitle: string, primary_button_text: string, secondary_button_text: string}>
*/
public static function defaults(): array
{
return [
[
'badge' => 'Vitrin İlanları',
'title' => 'İlan ücreti ödemeden ürününü dakikalar içinde yayına al.',
'subtitle' => 'Mahallendeki alıcılarla hızlıca buluş, pazarlığı doğrudan mesajla tamamla.',
'primary_button_text' => 'İlanları İncele',
'secondary_button_text' => 'İlan Ver',
],
[
'badge' => 'Günün Fırsatları',
'title' => 'Elektronikten araca kadar her kategoride canlı ilanlar seni bekliyor.',
'subtitle' => 'Kategorilere göz at, favorilerine ekle ve satıcılarla tek tıkla iletişime geç.',
'primary_button_text' => 'Kategorileri Gör',
'secondary_button_text' => 'Hemen Başla',
],
[
'badge' => 'Yerel Alışveriş',
'title' => 'Konumuna en yakın ikinci el fırsatları tek ekranda keşfet.',
'subtitle' => 'Şehrini seç, sana en yakın ilanları filtrele ve güvenle alışveriş yap.',
'primary_button_text' => 'Yakındaki İlanlar',
'secondary_button_text' => 'Ücretsiz İlan Ver',
],
];
}
/**
* @return array<int, array{badge: string, title: string, subtitle: string, primary_button_text: string, secondary_button_text: string}>
*/
public static function normalize(mixed $slides): array
{
$defaults = self::defaults();
$source = is_array($slides) ? $slides : [];
$normalized = collect($source)
->filter(fn ($slide): bool => is_array($slide))
->values()
->map(function (array $slide, int $index) use ($defaults): ?array {
$fallback = $defaults[$index] ?? $defaults[array_key_last($defaults)];
$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 : $fallback['badge'],
'title' => $title,
'subtitle' => $subtitle !== '' ? $subtitle : $fallback['subtitle'],
'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallback['primary_button_text'],
'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallback['secondary_button_text'],
];
})
->filter(fn ($slide): bool => is_array($slide))
->values();
return $normalized
->concat(collect($defaults)->slice($normalized->count()))
->take(count($defaults))
->values()
->all();
}
}

View File

@ -41,7 +41,7 @@ return [
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
'url' => env('APP_PUBLIC_STORAGE_URL', '/storage'),
'visibility' => 'public',
'throw' => false,
'report' => false,

View File

@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Support\HomeSlideDefaults;
use App\Settings\GeneralSettings;
use Illuminate\Database\Seeder;
@ -10,51 +11,13 @@ 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->home_slides = HomeSlideDefaults::normalize($settings->home_slides ?? []);
$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',
],
];
return HomeSlideDefaults::defaults();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>403 Forbidden</title>
<script src="https://cdn.tailwindcss.com"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<body class="min-h-screen font-sans antialiased bg-gray-100 flex items-center justify-center p-6">
<div class="max-w-md w-full bg-white rounded-xl shadow p-6 text-center">
<h1 class="text-2xl font-bold text-gray-900">403</h1>
<p class="mt-2 text-gray-700">Bu sayfaya erişim izniniz yok.</p>

View File

@ -300,9 +300,6 @@
</div>
</div>
<div class="p-4">
<div class="rounded-lg bg-emerald-50 text-emerald-700 text-xs font-semibold px-3 py-1.5 text-center mb-3">
Elden al, kartla öde!
</div>
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-3xl font-extrabold tracking-tight text-slate-900">{{ $priceLabel }}</p>

View File

@ -41,104 +41,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $siteName }} @hasSection('title') - @yield('title') @endif</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Pacifico&family=Sora:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--oc-bg: #f5f6fa;
--oc-surface: #ffffff;
--oc-border: #e4e7ef;
--oc-text: #191e2b;
--oc-muted: #667085;
--oc-primary: #ff4365;
--oc-primary-soft: #ffe7ed;
--oc-chip: #f1f3f8;
}
body {
font-family: 'Sora', sans-serif;
background: radial-gradient(circle at top right, #fce6ef 0%, #f5f6fa 28%);
color: var(--oc-text);
}
.brand-mark {
font-family: 'Pacifico', cursive;
}
.market-nav-surface {
background: rgba(255, 255, 255, 0.88);
backdrop-filter: saturate(180%) blur(8px);
border-bottom: 1px solid var(--oc-border);
}
.search-shell {
border: 1px solid #d9ddea;
background: #fbfcff;
border-radius: 999px;
}
.chip-btn {
border: 1px solid #d9ddea;
background: var(--oc-chip);
border-radius: 999px;
}
.btn-primary {
background: linear-gradient(120deg, #ff516e, #ff2f57);
color: #fff;
border-radius: 999px;
}
.header-utility {
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
border: 1px solid #d9ddea;
background: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
color: #64748b;
transition: all 0.2s ease;
}
.header-utility:hover {
border-color: #fda4af;
color: #f43f5e;
}
.location-panel {
width: min(90vw, 360px);
}
.location-panel select {
border: 1px solid #d9ddea;
border-radius: 0.75rem;
background: #f8fafc;
color: #334155;
padding: 0.55rem 0.75rem;
font-size: 0.875rem;
}
summary::-webkit-details-marker {
display: none;
}
[dir="rtl"] {
text-align: right;
}
</style>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="min-h-screen">
<body class="min-h-screen font-sans antialiased">
<nav class="market-nav-surface sticky top-0 z-50">
<div class="max-w-[1320px] mx-auto px-4 py-4">
<div class="flex items-center gap-3 md:gap-4">
<a href="{{ route('home') }}" class="shrink-0 flex items-center gap-2">
<a href="{{ route('home') }}" class="shrink-0 flex items-center gap-2.5">
@if($siteLogoUrl)
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="h-9 w-auto rounded">
@else
<span class="brand-logo" aria-hidden="true"></span>
@endif
<span class="brand-mark text-3xl text-rose-500 leading-none">{{ $siteName }}</span>
<span class="brand-text leading-none">{{ $siteName }}</span>
</a>
<form action="{{ route('listings.index') }}" method="GET" class="hidden lg:flex flex-1 search-shell items-center gap-2 px-4 py-2.5">
@ -286,54 +202,54 @@
<div class="bg-rose-100 border border-rose-300 text-rose-700 px-4 py-3 rounded-xl text-sm">{{ session('error') }}</div>
</div>
@endif
<main>@yield('content')</main>
<footer class="mt-14 bg-slate-900 text-slate-300">
<main class="site-main">@yield('content')</main>
<footer class="mt-14 bg-slate-100 text-slate-600 border-t border-slate-200">
<div class="max-w-[1320px] mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
<div>
<h3 class="text-white font-semibold text-lg mb-3">{{ $siteName }}</h3>
<p class="text-sm text-slate-400 leading-relaxed">{{ $siteDescription }}</p>
<h3 class="text-slate-900 font-semibold text-lg mb-3">{{ $siteName }}</h3>
<p class="text-sm text-slate-500 leading-relaxed">{{ $siteDescription }}</p>
</div>
<div>
<h4 class="text-white font-medium mb-4">Hızlı Linkler</h4>
<h4 class="text-slate-900 font-medium mb-4">Hızlı Linkler</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ route('home') }}" class="hover:text-white transition">Ana Sayfa</a></li>
<li><a href="{{ route('categories.index') }}" class="hover:text-white transition">Kategoriler</a></li>
<li><a href="{{ route('listings.index') }}" class="hover:text-white transition">Tüm İlanlar</a></li>
<li><a href="{{ route('home') }}" class="hover:text-slate-900 transition">Ana Sayfa</a></li>
<li><a href="{{ route('categories.index') }}" class="hover:text-slate-900 transition">Kategoriler</a></li>
<li><a href="{{ route('listings.index') }}" class="hover:text-slate-900 transition">Tüm İlanlar</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-medium mb-4">Hesap</h4>
<h4 class="text-slate-900 font-medium mb-4">Hesap</h4>
<ul class="space-y-2 text-sm">
<li><a href="{{ $loginRoute }}" class="hover:text-white transition">{{ __('messages.login') }}</a></li>
<li><a href="{{ $registerRoute }}" class="hover:text-white transition">{{ __('messages.register') }}</a></li>
<li><a href="{{ $loginRoute }}" class="hover:text-slate-900 transition">{{ __('messages.login') }}</a></li>
<li><a href="{{ $registerRoute }}" class="hover:text-slate-900 transition">{{ __('messages.register') }}</a></li>
</ul>
</div>
<div>
<h4 class="text-white font-medium mb-4">Bağlantılar</h4>
<h4 class="text-slate-900 font-medium mb-4">Bağlantılar</h4>
<ul class="space-y-2 text-sm mb-4">
@if($linkedinUrl)
<li><a href="{{ $linkedinUrl }}" target="_blank" rel="noopener" class="hover:text-white transition">LinkedIn</a></li>
<li><a href="{{ $linkedinUrl }}" target="_blank" rel="noopener" class="hover:text-slate-900 transition">LinkedIn</a></li>
@endif
@if($instagramUrl)
<li><a href="{{ $instagramUrl }}" target="_blank" rel="noopener" class="hover:text-white transition">Instagram</a></li>
<li><a href="{{ $instagramUrl }}" target="_blank" rel="noopener" class="hover:text-slate-900 transition">Instagram</a></li>
@endif
@if($whatsappUrl)
<li><a href="{{ $whatsappUrl }}" target="_blank" rel="noopener" class="hover:text-white transition">WhatsApp</a></li>
<li><a href="{{ $whatsappUrl }}" target="_blank" rel="noopener" class="hover:text-slate-900 transition">WhatsApp</a></li>
@endif
@if(!$linkedinUrl && !$instagramUrl && !$whatsappUrl)
<li>Henüz sosyal bağlantı eklenmedi.</li>
@endif
</ul>
<h4 class="text-white font-medium mb-3">Diller</h4>
<h4 class="text-slate-900 font-medium mb-3">Diller</h4>
<div class="flex flex-wrap gap-2">
@foreach($availableLocales as $locale)
<a href="{{ route('lang.switch', $locale) }}" class="text-xs {{ app()->getLocale() === $locale ? 'text-white' : 'hover:text-white' }} transition">{{ strtoupper($locale) }}</a>
<a href="{{ route('lang.switch', $locale) }}" class="text-xs {{ app()->getLocale() === $locale ? 'text-slate-900' : 'hover:text-slate-900' }} transition">{{ strtoupper($locale) }}</a>
@endforeach
</div>
</div>
</div>
<div class="border-t border-slate-700 mt-8 pt-8 text-center text-sm text-slate-400">
<div class="border-t border-slate-300 mt-8 pt-8 text-center text-sm text-slate-500">
<p>© {{ date('Y') }} {{ $siteName }}. All rights reserved.</p>
</div>
</div>

View File

@ -6,11 +6,6 @@
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>

View File

@ -7,30 +7,30 @@
<div class="grid grid-cols-1 lg:grid-cols-[220px,1fr] gap-4">
@include('panel.partials.sidebar', ['activeMenu' => 'listings'])
<section class="bg-white border border-slate-200 rounded-xl p-4 sm:p-6">
<div class="flex flex-col xl:flex-row xl:items-center gap-3 xl:gap-4 mb-5">
<form method="GET" action="{{ route('panel.listings.index') }}" class="relative flex-1 max-w-xl">
<svg class="w-6 h-6 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<section class="panel-surface">
<div class="panel-toolbar">
<form method="GET" action="{{ route('panel.listings.index') }}" class="panel-search">
<svg class="panel-search-icon 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="M21 21l-4.35-4.35m1.6-5.05a7.25 7.25 0 11-14.5 0 7.25 7.25 0 0114.5 0z"/>
</svg>
<input type="text" name="search" value="{{ $search }}" placeholder="İlan başlığına göre ara" class="w-full h-14 rounded-2xl border border-slate-300 pl-14 pr-4 text-lg font-semibold text-slate-700 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-rose-200">
<input type="text" name="search" value="{{ $search }}" placeholder="İlan başlığına göre ara" class="panel-search-input focus:outline-none focus:ring-2 focus:ring-rose-200">
<input type="hidden" name="status" value="{{ $status }}">
</form>
<div class="flex flex-wrap items-center gap-2">
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'all']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'all' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
<div class="panel-filter-tabs">
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'all']) }}" class="panel-filter-tab {{ $status === 'all' ? 'is-active' : '' }}">
Tüm İlanlar ({{ $counts['all'] }})
</a>
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'sold']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'sold' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'sold']) }}" class="panel-filter-tab {{ $status === 'sold' ? 'is-active' : '' }}">
Satıldı ({{ $counts['sold'] }})
</a>
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'expired']) }}" class="inline-flex items-center h-12 px-6 rounded-full border text-xl font-semibold {{ $status === 'expired' ? 'border-rose-500 text-rose-500 bg-rose-50' : 'border-slate-300 text-slate-700 hover:bg-slate-100' }}">
<a href="{{ route('panel.listings.index', ['search' => $search, 'status' => 'expired']) }}" class="panel-filter-tab {{ $status === 'expired' ? 'is-active' : '' }}">
Süresi Dolmuş ({{ $counts['expired'] }})
</a>
</div>
</div>
<div class="space-y-4">
<div class="space-y-4 panel-list-section">
@forelse($listings as $listing)
@php
$listingImage = $listing->getFirstMediaUrl('listing-images');
@ -53,9 +53,9 @@
$viewCount = (int) ($listing->view_count ?? 0);
$expiresAt = $listing->expires_at?->format('d/m/Y');
@endphp
<article class="rounded-2xl border border-slate-300 bg-slate-50 p-4 sm:p-5">
<div class="flex flex-col xl:flex-row gap-4 xl:items-stretch">
<div class="w-full xl:w-[260px] h-[180px] bg-slate-200 rounded-xl overflow-hidden shrink-0">
<article class="panel-list-card">
<div class="panel-list-card-body">
<div class="panel-list-media bg-slate-200">
@if($listingImage)
<img src="{{ $listingImage }}" alt="{{ $listing->title }}" class="w-full h-full object-cover">
@else
@ -63,17 +63,17 @@
@endif
</div>
<div class="flex-1 min-w-0 flex flex-col">
<div class="flex flex-wrap items-center gap-3">
<p class="text-4xl font-black text-slate-900">{{ $priceLabel }}</p>
<span class="inline-flex items-center h-10 px-4 rounded-full text-lg font-bold {{ $statusBadgeClass }}">{{ $statusLabel }}</span>
<div class="panel-list-main">
<div class="panel-list-summary">
<p class="panel-list-price text-slate-900">{{ $priceLabel }}</p>
<span class="panel-status-badge {{ $statusBadgeClass }}">{{ $statusLabel }}</span>
</div>
<h2 class="text-2xl font-semibold text-slate-800 mt-3 leading-tight break-words">{{ $listing->title }}</h2>
<h2 class="panel-list-title text-slate-800">{{ $listing->title }}</h2>
<div class="mt-auto pt-5 flex flex-wrap items-center gap-2">
<div class="panel-list-actions">
<form method="POST" action="{{ route('panel.listings.destroy', $listing) }}">
@csrf
<button type="submit" class="h-12 px-6 rounded-full border-2 border-rose-500 text-rose-500 text-2xl font-bold hover:bg-rose-50 transition">
<button type="submit" class="panel-action-btn panel-action-btn-secondary">
İlanı Kaldır
</button>
</form>
@ -81,7 +81,7 @@
@if((string) $listing->status !== 'sold')
<form method="POST" action="{{ route('panel.listings.mark-sold', $listing) }}">
@csrf
<button type="submit" class="h-12 px-6 rounded-full bg-rose-500 text-white text-2xl font-bold hover:bg-rose-600 transition">
<button type="submit" class="panel-action-btn panel-action-btn-primary">
Satıldı İşaretle
</button>
</form>
@ -90,7 +90,7 @@
@if((string) $listing->status === 'expired')
<form method="POST" action="{{ route('panel.listings.republish', $listing) }}">
@csrf
<button type="submit" class="h-12 px-6 rounded-full border-2 border-rose-500 text-rose-500 text-2xl font-bold hover:bg-rose-50 transition">
<button type="submit" class="panel-action-btn panel-action-btn-secondary">
Yeniden Yayınla
</button>
</form>
@ -98,19 +98,19 @@
</div>
</div>
<div class="xl:w-[260px] flex xl:flex-col items-start xl:items-end justify-between gap-3">
<div class="flex items-center gap-3">
<div class="h-12 min-w-24 px-4 rounded-2xl bg-slate-200 text-slate-500 text-xl font-bold inline-flex items-center justify-center gap-2">
<div class="panel-list-aside">
<div class="panel-stats">
<div class="panel-stat-box">
<span>👁</span>
<span>{{ $viewCount }}</span>
</div>
<div class="h-12 min-w-24 px-4 rounded-2xl bg-slate-200 text-slate-500 text-xl font-bold inline-flex items-center justify-center gap-2">
<div class="panel-stat-box">
<span></span>
<span>{{ $favoriteCount }}</span>
</div>
</div>
<p class="text-lg text-slate-500 text-left xl:text-right">
<p class="panel-list-dates">
Yayın Tarihi & Bitiş Tarihi:
<strong class="text-slate-700">
{{ $listing->created_at?->format('d/m/Y') ?? '-' }} - {{ $expiresAt ?: '-' }}
@ -120,13 +120,13 @@
</div>
@if((string) $listing->status === 'expired')
<div class="mt-4 rounded-xl bg-sky-100 px-4 py-3 text-base text-slate-700">
<div class="panel-inline-note">
<strong>Bu ilanın süresi doldu.</strong> Eğer sattıysan, lütfen satıldı olarak işaretle.
</div>
@endif
</article>
@empty
<div class="rounded-xl border border-dashed border-slate-300 py-16 text-center text-slate-500">
<div class="panel-empty-state">
Bu filtreye uygun ilan bulunamadı.
</div>
@endforelse

View File

@ -4,6 +4,9 @@ import forms from '@tailwindcss/forms';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./app/**/*.php',
'./Modules/**/*.php',
'./Modules/**/*.blade.php',
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
@ -12,7 +15,28 @@ export default {
theme: {
extend: {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
sans: [
'-apple-system',
'BlinkMacSystemFont',
'SF Pro Text',
'SF Pro Display',
'Helvetica Neue',
'Helvetica',
'Arial',
...defaultTheme.fontFamily.sans,
],
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.55rem' }],
lg: ['1.0625rem', { lineHeight: '1.6rem' }],
xl: ['1.125rem', { lineHeight: '1.65rem' }],
'2xl': ['1.35rem', { lineHeight: '1.4' }],
'3xl': ['1.65rem', { lineHeight: '1.2' }],
'4xl': ['1.95rem', { lineHeight: '1.1' }],
'5xl': ['2.45rem', { lineHeight: '1.05' }],
'6xl': ['3.1rem', { lineHeight: '1' }],
},
},
},