mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 11:12:09 -05:00
Compare commits
3 Commits
7e9d77c0a8
...
08d0b68349
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08d0b68349 | ||
|
|
aa7d2e27c0 | ||
|
|
165585cdc4 |
@ -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 Açı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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,7 +189,12 @@ 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')
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,6 +19,11 @@ class ConversationController extends Controller
|
||||
$userId = (int) $request->user()->getKey();
|
||||
$messageFilter = $this->resolveMessageFilter($request);
|
||||
|
||||
$conversations = collect();
|
||||
$selectedConversation = null;
|
||||
|
||||
if ($this->messagingTablesReady()) {
|
||||
try {
|
||||
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
|
||||
|
||||
@ -32,6 +39,11 @@ class ConversationController extends Controller
|
||||
return $conversation;
|
||||
});
|
||||
}
|
||||
} catch (Throwable) {
|
||||
$conversations = collect();
|
||||
$selectedConversation = null;
|
||||
}
|
||||
}
|
||||
|
||||
return view('conversation::inbox', [
|
||||
'conversations' => $conversations,
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,19 +40,24 @@ class FavoriteController extends Controller
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$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') {
|
||||
try {
|
||||
if ($this->tableExists('favorite_listings')) {
|
||||
$favoriteListings = $user->favoriteListings()
|
||||
->with(['category:id,name', 'user:id,name'])
|
||||
->wherePivot('created_at', '>=', now()->subYear())
|
||||
@ -58,7 +66,9 @@ class FavoriteController extends Controller
|
||||
->orderByPivot('created_at', 'desc')
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
}
|
||||
|
||||
if ($this->tableExists('conversations') && $this->tableExists('conversation_messages')) {
|
||||
$userId = (int) $user->getKey();
|
||||
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||
$buyerConversationListingMap = $conversations
|
||||
@ -82,16 +92,31 @@ class FavoriteController extends Controller
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (Throwable) {
|
||||
$favoriteListings = $this->emptyPaginator();
|
||||
$conversations = collect();
|
||||
$selectedConversation = null;
|
||||
$buyerConversationListingMap = [];
|
||||
}
|
||||
}
|
||||
|
||||
if ($activeTab === 'searches') {
|
||||
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') {
|
||||
try {
|
||||
if ($this->tableExists('favorite_sellers')) {
|
||||
$favoriteSellers = $user->favoriteSellers()
|
||||
->withCount([
|
||||
'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
|
||||
@ -100,6 +125,10 @@ class FavoriteController extends Controller
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
}
|
||||
} catch (Throwable) {
|
||||
$favoriteSellers = $this->emptyPaginator();
|
||||
}
|
||||
}
|
||||
|
||||
return view('favorite::index', [
|
||||
'activeTab' => $activeTab,
|
||||
@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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ı açı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,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
77
app/Support/HomeSlideDefaults.php
Normal file
77
app/Support/HomeSlideDefaults.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user