mirror of
https://github.com/openclassify/openclassify.git
synced 2026-04-14 19:22:10 -05:00
Compare commits
No commits in common. "63f2c95fd7c45f01881f2f4a6de7e70ff8c530b7" and "08d0b6834999cd53dd902e33822bc6d901e5db2c" have entirely different histories.
63f2c95fd7
...
08d0b68349
@ -4,4 +4,4 @@ Act as a Senior Laravel & FilamentPHP Architect. Refactor the attached code as a
|
||||
3. Refactoring: Move all database logic into Models and extract repetitive Filament code into dedicated Helper classes. Identify and fix any existing logical errors.
|
||||
4. Database: Consolidate migrations into a single file per table or topic (e.g., users, cache, jobs) to reduce the overall number of migration files.
|
||||
5. Modularity: Use the `laravel-modules` package to encapsulate all features, routing, and Filament resources strictly inside their respective modules.
|
||||
6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output.
|
||||
6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output.
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
Act as a Senior Laravel & FilamentPHP Architect. Refactor the attached code as a greenfield project adhering to the following strict constraints:
|
||||
1. Architecture: Enforce strict SOLID principles, prioritize brevity, and completely ignore backward compatibility.
|
||||
2. Cleanup: Remove all legacy code, comments, tests, and PHPDocs.
|
||||
3. Refactoring: Move all database logic into Models and extract repetitive Filament code into dedicated Helper classes. Identify and fix any existing logical errors.
|
||||
4. Database: Consolidate migrations into a single file per table or topic (e.g., users, cache, jobs) to reduce the overall number of migration files.
|
||||
5. Modularity: Use the `laravel-modules` package to encapsulate all features, routing, and Filament resources strictly inside their respective modules.
|
||||
6. Frontend: Optimize and reduce the CSS footprint while maintaining the exact same visual output.
|
||||
@ -7,6 +7,7 @@ use App\Support\CountryCodeManager;
|
||||
use App\Settings\GeneralSettings;
|
||||
use BackedEnum;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
@ -14,7 +15,6 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Pages\SettingsPage;
|
||||
use Filament\Schemas\Schema;
|
||||
use Modules\Admin\Support\HomeSlideFormSchema;
|
||||
use Tapp\FilamentCountryCodeField\Forms\Components\CountryCodeSelect;
|
||||
use UnitEnum;
|
||||
use Ysfkaya\FilamentPhoneInput\Forms\PhoneInput;
|
||||
@ -33,57 +33,51 @@ class ManageGeneralSettings extends SettingsPage
|
||||
|
||||
protected static ?int $navigationSort = 1;
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
$defaults = $this->defaultFormData();
|
||||
|
||||
return [
|
||||
'site_name' => filled($data['site_name'] ?? null) ? $data['site_name'] : $defaults['site_name'],
|
||||
'site_description' => filled($data['site_description'] ?? null) ? $data['site_description'] : $defaults['site_description'],
|
||||
'home_slides' => $this->normalizeHomeSlides($data['home_slides'] ?? $defaults['home_slides']),
|
||||
'site_logo' => $data['site_logo'] ?? null,
|
||||
'sender_name' => filled($data['sender_name'] ?? null) ? $data['sender_name'] : $defaults['sender_name'],
|
||||
'sender_email' => filled($data['sender_email'] ?? null) ? $data['sender_email'] : $defaults['sender_email'],
|
||||
'default_language' => filled($data['default_language'] ?? null) ? $data['default_language'] : $defaults['default_language'],
|
||||
'default_country_code' => filled($data['default_country_code'] ?? null) ? $data['default_country_code'] : $defaults['default_country_code'],
|
||||
'currencies' => $this->normalizeCurrencies($data['currencies'] ?? $defaults['currencies']),
|
||||
'linkedin_url' => filled($data['linkedin_url'] ?? null) ? $data['linkedin_url'] : $defaults['linkedin_url'],
|
||||
'instagram_url' => filled($data['instagram_url'] ?? null) ? $data['instagram_url'] : $defaults['instagram_url'],
|
||||
'whatsapp' => filled($data['whatsapp'] ?? null) ? $data['whatsapp'] : $defaults['whatsapp'],
|
||||
'enable_google_maps' => (bool) ($data['enable_google_maps'] ?? $defaults['enable_google_maps']),
|
||||
'google_maps_api_key' => $data['google_maps_api_key'] ?? null,
|
||||
'enable_google_login' => (bool) ($data['enable_google_login'] ?? $defaults['enable_google_login']),
|
||||
'google_client_id' => $data['google_client_id'] ?? null,
|
||||
'google_client_secret' => $data['google_client_secret'] ?? null,
|
||||
'enable_facebook_login' => (bool) ($data['enable_facebook_login'] ?? $defaults['enable_facebook_login']),
|
||||
'facebook_client_id' => $data['facebook_client_id'] ?? null,
|
||||
'facebook_client_secret' => $data['facebook_client_secret'] ?? null,
|
||||
'enable_apple_login' => (bool) ($data['enable_apple_login'] ?? $defaults['enable_apple_login']),
|
||||
'apple_client_id' => $data['apple_client_id'] ?? null,
|
||||
'apple_client_secret' => $data['apple_client_secret'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
$defaults = $this->defaultFormData();
|
||||
|
||||
return $schema
|
||||
->components([
|
||||
TextInput::make('site_name')
|
||||
->label('Site Adı')
|
||||
->default($defaults['site_name'])
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('site_description')
|
||||
->label('Site Açıklaması')
|
||||
->default($defaults['site_description'])
|
||||
->rows(3)
|
||||
->maxLength(500),
|
||||
HomeSlideFormSchema::make(
|
||||
$defaults['home_slides'],
|
||||
fn ($state): array => $this->normalizeHomeSlides($state),
|
||||
),
|
||||
Repeater::make('home_slides')
|
||||
->label('Ana Sayfa Slider')
|
||||
->schema([
|
||||
TextInput::make('badge')
|
||||
->label('Rozet')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
TextInput::make('title')
|
||||
->label('Başlık')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('subtitle')
|
||||
->label('Alt Başlık')
|
||||
->rows(2)
|
||||
->required()
|
||||
->maxLength(500),
|
||||
TextInput::make('primary_button_text')
|
||||
->label('Birincil Buton Metni')
|
||||
->required()
|
||||
->maxLength(120),
|
||||
TextInput::make('secondary_button_text')
|
||||
->label('İkincil Buton Metni')
|
||||
->required()
|
||||
->maxLength(120),
|
||||
])
|
||||
->default($this->defaultHomeSlides())
|
||||
->minItems(1)
|
||||
->collapsible()
|
||||
->reorderableWithButtons()
|
||||
->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 Logosu')
|
||||
->image()
|
||||
@ -92,30 +86,26 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->visibility('public'),
|
||||
TextInput::make('sender_name')
|
||||
->label('Gönderici Adı')
|
||||
->default($defaults['sender_name'])
|
||||
->required()
|
||||
->maxLength(120),
|
||||
TextInput::make('sender_email')
|
||||
->label('Gönderici E-postası')
|
||||
->email()
|
||||
->default($defaults['sender_email'])
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Select::make('default_language')
|
||||
->label('Varsayılan Dil')
|
||||
->options($this->localeOptions())
|
||||
->default($defaults['default_language'])
|
||||
->required()
|
||||
->searchable(),
|
||||
CountryCodeSelect::make('default_country_code')
|
||||
->label('Varsayılan Ülke')
|
||||
->default($defaults['default_country_code'])
|
||||
->default('+90')
|
||||
->required()
|
||||
->helperText('Panel formlarında varsayılan ülke olarak kullanılır.'),
|
||||
TagsInput::make('currencies')
|
||||
->label('Para Birimleri')
|
||||
->placeholder('TRY')
|
||||
->default($defaults['currencies'])
|
||||
->helperText('TRY, USD, EUR gibi 3 harfli para birimi kodları ekleyin.')
|
||||
->required()
|
||||
->rules(['array', 'min:1'])
|
||||
@ -124,25 +114,22 @@ class ManageGeneralSettings extends SettingsPage
|
||||
TextInput::make('linkedin_url')
|
||||
->label('LinkedIn URL')
|
||||
->url()
|
||||
->default($defaults['linkedin_url'])
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
TextInput::make('instagram_url')
|
||||
->label('Instagram URL')
|
||||
->url()
|
||||
->default($defaults['instagram_url'])
|
||||
->nullable()
|
||||
->maxLength(255),
|
||||
PhoneInput::make('whatsapp')
|
||||
->label('WhatsApp')
|
||||
->defaultCountry(CountryCodeManager::defaultCountryIso2())
|
||||
->default($defaults['whatsapp'])
|
||||
->nullable()
|
||||
->formatAsYouType()
|
||||
->helperText('Uluslararası format kullanın. Örnek: +905551112233'),
|
||||
Toggle::make('enable_google_maps')
|
||||
->label('Google Maps Aktif')
|
||||
->default($defaults['enable_google_maps']),
|
||||
->default(false),
|
||||
TextInput::make('google_maps_api_key')
|
||||
->label('Google Maps API Anahtarı')
|
||||
->password()
|
||||
@ -152,7 +139,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->helperText('İlan formlarındaki harita alanlarını açmak için gereklidir.'),
|
||||
Toggle::make('enable_google_login')
|
||||
->label('Google ile Giriş Aktif')
|
||||
->default($defaults['enable_google_login']),
|
||||
->default(false),
|
||||
TextInput::make('google_client_id')
|
||||
->label('Google Client ID')
|
||||
->nullable()
|
||||
@ -165,7 +152,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->maxLength(255),
|
||||
Toggle::make('enable_facebook_login')
|
||||
->label('Facebook ile Giriş Aktif')
|
||||
->default($defaults['enable_facebook_login']),
|
||||
->default(false),
|
||||
TextInput::make('facebook_client_id')
|
||||
->label('Facebook Client ID')
|
||||
->nullable()
|
||||
@ -178,7 +165,7 @@ class ManageGeneralSettings extends SettingsPage
|
||||
->maxLength(255),
|
||||
Toggle::make('enable_apple_login')
|
||||
->label('Apple ile Giriş Aktif')
|
||||
->default($defaults['enable_apple_login']),
|
||||
->default(false),
|
||||
TextInput::make('apple_client_id')
|
||||
->label('Apple Client ID')
|
||||
->nullable()
|
||||
@ -192,30 +179,6 @@ class ManageGeneralSettings extends SettingsPage
|
||||
]);
|
||||
}
|
||||
|
||||
private function defaultFormData(): array
|
||||
{
|
||||
$siteName = (string) config('app.name', 'OpenClassify');
|
||||
$siteHost = parse_url((string) config('app.url', 'https://oc2.test'), PHP_URL_HOST) ?: 'oc2.test';
|
||||
|
||||
return [
|
||||
'site_name' => $siteName,
|
||||
'site_description' => 'Alim satim icin hizli ve guvenli ilan platformu.',
|
||||
'home_slides' => $this->defaultHomeSlides(),
|
||||
'sender_name' => $siteName,
|
||||
'sender_email' => (string) config('mail.from.address', 'info@' . $siteHost),
|
||||
'default_language' => in_array(config('app.locale'), array_keys($this->localeOptions()), true) ? (string) config('app.locale') : 'tr',
|
||||
'default_country_code' => CountryCodeManager::normalizeCountryCode(config('app.default_country_code', '+90')),
|
||||
'currencies' => $this->normalizeCurrencies(config('app.currencies', ['TRY'])),
|
||||
'linkedin_url' => 'https://www.linkedin.com/company/openclassify',
|
||||
'instagram_url' => 'https://www.instagram.com/openclassify',
|
||||
'whatsapp' => '+905551112233',
|
||||
'enable_google_maps' => false,
|
||||
'enable_google_login' => false,
|
||||
'enable_facebook_login' => false,
|
||||
'enable_apple_login' => false,
|
||||
];
|
||||
}
|
||||
|
||||
private function localeOptions(): array
|
||||
{
|
||||
$labels = [
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Admin\Filament\Pages;
|
||||
|
||||
use App\Settings\GeneralSettings;
|
||||
use App\Support\HomeSlideDefaults;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\SettingsPage;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Support\Enums\Width;
|
||||
use Modules\Admin\Support\HomeSlideFormSchema;
|
||||
use UnitEnum;
|
||||
|
||||
class ManageHomeSlides extends SettingsPage
|
||||
{
|
||||
protected static string $settings = GeneralSettings::class;
|
||||
|
||||
protected static ?string $title = 'Home Slides';
|
||||
|
||||
protected static ?string $navigationLabel = 'Home Slides';
|
||||
|
||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-photo';
|
||||
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Content';
|
||||
|
||||
protected static ?int $navigationSort = 2;
|
||||
|
||||
protected Width | string | null $maxContentWidth = Width::Full;
|
||||
|
||||
protected function mutateFormDataBeforeFill(array $data): array
|
||||
{
|
||||
return [
|
||||
'home_slides' => $this->normalizeHomeSlides($data['home_slides'] ?? $this->defaultHomeSlides()),
|
||||
];
|
||||
}
|
||||
|
||||
public function form(Schema $schema): Schema
|
||||
{
|
||||
return $schema
|
||||
->components([
|
||||
HomeSlideFormSchema::make(
|
||||
$this->defaultHomeSlides(),
|
||||
fn ($state): array => $this->normalizeHomeSlides($state),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
private function defaultHomeSlides(): array
|
||||
{
|
||||
return HomeSlideDefaults::defaults();
|
||||
}
|
||||
|
||||
private function normalizeHomeSlides(mixed $state): array
|
||||
{
|
||||
return HomeSlideDefaults::normalize($state);
|
||||
}
|
||||
}
|
||||
@ -40,21 +40,12 @@ class CategoryResource extends Resource
|
||||
{
|
||||
return $table->columns([
|
||||
TextColumn::make('id')->sortable(),
|
||||
TextColumn::make('name')
|
||||
->searchable()
|
||||
->formatStateUsing(fn (string $state, Category $record): string => $record->parent_id === null ? $state : '↳ ' . $state)
|
||||
->weight(fn (Category $record): string => $record->parent_id === null ? 'semi-bold' : 'normal'),
|
||||
TextColumn::make('name')->searchable()->sortable(),
|
||||
TextColumn::make('parent.name')->label('Parent')->default('-'),
|
||||
TextColumn::make('children_count')->label('Subcategories'),
|
||||
TextColumn::make('listings_count')->label('Listings'),
|
||||
TextColumn::make('listings_count')->counts('listings')->label('Listings'),
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('sort_order')->sortable(),
|
||||
])->actions([
|
||||
Action::make('toggleChildren')
|
||||
->label(fn (Category $record, Pages\ListCategories $livewire): string => $livewire->hasExpandedChildren($record) ? 'Hide subcategories' : 'Show subcategories')
|
||||
->icon(fn (Category $record, Pages\ListCategories $livewire): string => $livewire->hasExpandedChildren($record) ? 'heroicon-o-chevron-down' : 'heroicon-o-chevron-right')
|
||||
->action(fn (Category $record, Pages\ListCategories $livewire) => $livewire->toggleChildren($record))
|
||||
->visible(fn (Category $record): bool => $record->parent_id === null && $record->children_count > 0),
|
||||
EditAction::make(),
|
||||
Action::make('activities')
|
||||
->icon('heroicon-o-clock')
|
||||
|
||||
@ -3,48 +3,10 @@ namespace Modules\Admin\Filament\Resources\CategoryResource\Pages;
|
||||
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Livewire\Attributes\Url;
|
||||
use Modules\Admin\Filament\Resources\CategoryResource;
|
||||
use Modules\Category\Models\Category;
|
||||
|
||||
class ListCategories extends ListRecords
|
||||
{
|
||||
protected static string $resource = CategoryResource::class;
|
||||
|
||||
#[Url(as: 'expanded')]
|
||||
public array $expandedParents = [];
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [CreateAction::make()];
|
||||
}
|
||||
|
||||
public function toggleChildren(Category $record): void
|
||||
{
|
||||
if ($record->parent_id !== null || $record->children_count < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$recordId = (int) $record->getKey();
|
||||
|
||||
if (in_array($recordId, $this->expandedParents, true)) {
|
||||
$this->expandedParents = array_values(array_diff($this->expandedParents, [$recordId]));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->expandedParents[] = $recordId;
|
||||
$this->expandedParents = array_values(array_unique(array_map('intval', $this->expandedParents)));
|
||||
}
|
||||
|
||||
public function hasExpandedChildren(Category $record): bool
|
||||
{
|
||||
return in_array((int) $record->getKey(), $this->expandedParents, true);
|
||||
}
|
||||
|
||||
protected function getTableQuery(): Builder
|
||||
{
|
||||
return Category::query()->forAdminHierarchy($this->expandedParents);
|
||||
}
|
||||
protected function getHeaderActions(): array { return [CreateAction::make()]; }
|
||||
}
|
||||
|
||||
@ -15,7 +15,6 @@ use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Modules\Admin\Filament\Resources\CityResource\Pages;
|
||||
use Modules\Location\Models\City;
|
||||
use UnitEnum;
|
||||
@ -24,7 +23,7 @@ class CityResource extends Resource
|
||||
{
|
||||
protected static ?string $model = City::class;
|
||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-building-office-2';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Location';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Settings';
|
||||
protected static ?string $label = 'City';
|
||||
protected static ?string $pluralLabel = 'Cities';
|
||||
protected static ?int $navigationSort = 3;
|
||||
@ -47,19 +46,12 @@ class CityResource extends Resource
|
||||
TextColumn::make('districts_count')->counts('districts')->label('Districts')->sortable(),
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
|
||||
])->defaultSort('id', 'desc')->filters([
|
||||
])->filters([
|
||||
SelectFilter::make('country_id')
|
||||
->label('Country')
|
||||
->relationship('country', 'name')
|
||||
->searchable()
|
||||
->preload(),
|
||||
TernaryFilter::make('has_districts')
|
||||
->label('Has districts')
|
||||
->queries(
|
||||
true: fn (Builder $query): Builder => $query->has('districts'),
|
||||
false: fn (Builder $query): Builder => $query->doesntHave('districts'),
|
||||
blank: fn (Builder $query): Builder => $query,
|
||||
),
|
||||
TernaryFilter::make('is_active')->label('Active'),
|
||||
])->actions([
|
||||
EditAction::make(),
|
||||
|
||||
@ -25,7 +25,7 @@ class DistrictResource extends Resource
|
||||
{
|
||||
protected static ?string $model = District::class;
|
||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-map';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Location';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Settings';
|
||||
protected static ?string $label = 'District';
|
||||
protected static ?string $pluralLabel = 'Districts';
|
||||
protected static ?int $navigationSort = 4;
|
||||
@ -48,7 +48,7 @@ class DistrictResource extends Resource
|
||||
TextColumn::make('city.country.name')->label('Country'),
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
|
||||
])->defaultSort('id', 'desc')->filters([
|
||||
])->filters([
|
||||
SelectFilter::make('country_id')
|
||||
->label('Country')
|
||||
->options(fn (): array => Country::query()->orderBy('name')->pluck('name', 'id')->all())
|
||||
|
||||
@ -105,7 +105,7 @@ class ListingCustomFieldResource extends Resource
|
||||
IconColumn::make('is_active')->boolean()->label('Active'),
|
||||
TextColumn::make('sort_order')->sortable(),
|
||||
])
|
||||
->defaultSort('id', 'desc')
|
||||
->defaultSort('sort_order')
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
DeleteAction::make(),
|
||||
|
||||
@ -194,7 +194,6 @@ class ListingResource extends Resource
|
||||
->filtersFormColumns(3)
|
||||
->filtersFormWidth('7xl')
|
||||
->persistFiltersInSession()
|
||||
->defaultSort('id', 'desc')
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Action::make('activities')
|
||||
|
||||
@ -11,10 +11,8 @@ use Filament\Resources\Resource;
|
||||
use Filament\Schemas\Schema;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Filters\TernaryFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Modules\Admin\Filament\Resources\LocationResource\Pages;
|
||||
use Modules\Location\Models\Country;
|
||||
use UnitEnum;
|
||||
@ -23,7 +21,7 @@ class LocationResource extends Resource
|
||||
{
|
||||
protected static ?string $model = Country::class;
|
||||
protected static string | BackedEnum | null $navigationIcon = 'heroicon-o-globe-alt';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Location';
|
||||
protected static string | UnitEnum | null $navigationGroup = 'Settings';
|
||||
protected static ?string $label = 'Country';
|
||||
protected static ?string $pluralLabel = 'Countries';
|
||||
protected static ?int $navigationSort = 2;
|
||||
@ -48,17 +46,7 @@ class LocationResource extends Resource
|
||||
TextColumn::make('cities_count')->counts('cities')->label('Cities')->sortable(),
|
||||
IconColumn::make('is_active')->boolean(),
|
||||
TextColumn::make('created_at')->dateTime()->sortable()->toggleable(isToggledHiddenByDefault: true),
|
||||
])->defaultSort('id', 'desc')->filters([
|
||||
SelectFilter::make('code')
|
||||
->label('Code')
|
||||
->options(fn (): array => Country::query()->orderBy('code')->pluck('code', 'code')->all()),
|
||||
TernaryFilter::make('has_cities')
|
||||
->label('Has cities')
|
||||
->queries(
|
||||
true: fn (Builder $query): Builder => $query->has('cities'),
|
||||
false: fn (Builder $query): Builder => $query->doesntHave('cities'),
|
||||
blank: fn (Builder $query): Builder => $query,
|
||||
),
|
||||
])->filters([
|
||||
TernaryFilter::make('is_active')->label('Active'),
|
||||
])->actions([
|
||||
EditAction::make(),
|
||||
|
||||
@ -43,7 +43,7 @@ class UserResource extends Resource
|
||||
TextColumn::make('roles.name')->badge()->label('Roles'),
|
||||
StateFusionSelectColumn::make('status'),
|
||||
TextColumn::make('created_at')->dateTime()->sortable(),
|
||||
])->defaultSort('id', 'desc')->filters([
|
||||
])->filters([
|
||||
StateFusionSelectFilter::make('status'),
|
||||
])->actions([
|
||||
EditAction::make(),
|
||||
|
||||
@ -7,7 +7,6 @@ use Filament\Http\Middleware\Authenticate;
|
||||
use Filament\Http\Middleware\AuthenticateSession;
|
||||
use Filament\Http\Middleware\DisableBladeIconComponents;
|
||||
use Filament\Http\Middleware\DispatchServingFilamentEvent;
|
||||
use Filament\Navigation\MenuItem;
|
||||
use Filament\Pages\Dashboard;
|
||||
use Filament\Panel;
|
||||
use Filament\PanelProvider;
|
||||
@ -25,6 +24,7 @@ use Modules\Admin\Filament\Resources\CategoryResource;
|
||||
use Modules\Admin\Filament\Resources\ListingResource;
|
||||
use Modules\Admin\Filament\Resources\LocationResource;
|
||||
use Modules\Admin\Filament\Resources\UserResource;
|
||||
use TallCms\Cms\TallCmsPlugin;
|
||||
|
||||
class AdminPanelProvider extends PanelProvider
|
||||
{
|
||||
@ -39,15 +39,10 @@ class AdminPanelProvider extends PanelProvider
|
||||
->discoverResources(in: module_path('Admin', 'Filament/Resources'), for: 'Modules\\Admin\\Filament\\Resources')
|
||||
->discoverPages(in: module_path('Admin', 'Filament/Pages'), for: 'Modules\\Admin\\Filament\\Pages')
|
||||
->discoverWidgets(in: module_path('Admin', 'Filament/Widgets'), for: 'Modules\\Admin\\Filament\\Widgets')
|
||||
->userMenuItems([
|
||||
'view-site' => MenuItem::make()
|
||||
->label('View Site')
|
||||
->icon('heroicon-o-globe-alt')
|
||||
->url(fn (): string => url('/'))
|
||||
->sort(-2),
|
||||
])
|
||||
->plugins([
|
||||
FilamentStateFusionPlugin::make(),
|
||||
TallCmsPlugin::make()
|
||||
->withoutUsers(),
|
||||
BreezyCore::make()
|
||||
->myProfile(
|
||||
shouldRegisterNavigation: true,
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Admin\Support;
|
||||
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
|
||||
final class HomeSlideFormSchema
|
||||
{
|
||||
public static function make(array $defaults, callable $normalizeSlides): Repeater
|
||||
{
|
||||
return Repeater::make('home_slides')
|
||||
->label('Homepage Slides')
|
||||
->helperText('Use 1 to 5 slides. Upload a wide image for each slide to improve the hero area.')
|
||||
->schema([
|
||||
FileUpload::make('image_path')
|
||||
->label('Slide Image')
|
||||
->image()
|
||||
->disk('public')
|
||||
->directory('home-slides')
|
||||
->visibility('public')
|
||||
->imageEditor()
|
||||
->imagePreviewHeight('200')
|
||||
->helperText('Recommended: 1600x1000 or wider.')
|
||||
->columnSpanFull(),
|
||||
TextInput::make('badge')
|
||||
->label('Badge')
|
||||
->maxLength(255),
|
||||
TextInput::make('title')
|
||||
->label('Title')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
Textarea::make('subtitle')
|
||||
->label('Subtitle')
|
||||
->rows(3)
|
||||
->required()
|
||||
->maxLength(500)
|
||||
->columnSpanFull(),
|
||||
TextInput::make('primary_button_text')
|
||||
->label('Primary Button')
|
||||
->required()
|
||||
->maxLength(120),
|
||||
TextInput::make('secondary_button_text')
|
||||
->label('Secondary Button')
|
||||
->required()
|
||||
->maxLength(120),
|
||||
])
|
||||
->columns(2)
|
||||
->default($defaults)
|
||||
->minItems(1)
|
||||
->maxItems(5)
|
||||
->collapsible()
|
||||
->collapsed()
|
||||
->cloneable()
|
||||
->reorderableWithButtons()
|
||||
->addActionLabel('Add Slide')
|
||||
->itemLabel(fn (array $state): string => filled($state['title'] ?? null) ? (string) $state['title'] : 'New Slide')
|
||||
->afterStateHydrated(fn (Repeater $component, $state) => $component->state($normalizeSlides($state)))
|
||||
->dehydrateStateUsing(fn ($state) => $normalizeSlides($state));
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -17,4 +18,22 @@ class CategoryController extends Controller
|
||||
|
||||
return view($this->themes->view('category', 'index'), compact('categories'));
|
||||
}
|
||||
|
||||
public function show(Category $category)
|
||||
{
|
||||
$category->loadMissing([
|
||||
'children' => fn ($query) => $query->active()->ordered(),
|
||||
]);
|
||||
|
||||
$categoryIds = $category->descendantAndSelfIds()->all();
|
||||
|
||||
$listings = Listing::query()
|
||||
->where('status', 'active')
|
||||
->whereIn('category_id', $categoryIds)
|
||||
->with('category:id,name')
|
||||
->latest('id')
|
||||
->paginate(12);
|
||||
|
||||
return view($this->themes->view('category', 'show'), compact('category', 'listings'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,9 @@ namespace Modules\Category\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\Listing\Models\Listing;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
|
||||
@ -50,34 +49,6 @@ class Category extends Model
|
||||
return $query->orderBy('sort_order')->orderBy('name');
|
||||
}
|
||||
|
||||
public function scopeForAdminHierarchy(Builder $query, array $expandedParentIds = []): Builder
|
||||
{
|
||||
$expandedParentIds = collect($expandedParentIds)
|
||||
->map(fn ($id): int => (int) $id)
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $query
|
||||
->select('categories.*')
|
||||
->leftJoin('categories as parent_categories', 'categories.parent_id', '=', 'parent_categories.id')
|
||||
->with(['parent:id,name'])
|
||||
->withCount(['children', 'listings'])
|
||||
->where(function (Builder $nestedQuery) use ($expandedParentIds): void {
|
||||
$nestedQuery->whereNull('categories.parent_id');
|
||||
|
||||
if ($expandedParentIds !== []) {
|
||||
$nestedQuery->orWhereIn('categories.parent_id', $expandedParentIds);
|
||||
}
|
||||
})
|
||||
->orderByRaw('COALESCE(parent_categories.sort_order, categories.sort_order)')
|
||||
->orderByRaw('COALESCE(parent_categories.name, categories.name)')
|
||||
->orderByRaw('CASE WHEN categories.parent_id IS NULL THEN 0 ELSE 1 END')
|
||||
->orderBy('categories.sort_order')
|
||||
->orderBy('categories.name');
|
||||
}
|
||||
|
||||
public static function filterOptions(): Collection
|
||||
{
|
||||
return static::query()
|
||||
@ -107,58 +78,6 @@ class Category extends Model
|
||||
->get();
|
||||
}
|
||||
|
||||
public static function listingDirectory(?int $selectedCategoryId): array
|
||||
{
|
||||
$categories = static::query()
|
||||
->active()
|
||||
->ordered()
|
||||
->get(['id', 'name', 'parent_id']);
|
||||
|
||||
$activeListingCounts = Listing::query()
|
||||
->active()
|
||||
->whereNotNull('category_id')
|
||||
->selectRaw('category_id, count(*) as aggregate')
|
||||
->groupBy('category_id')
|
||||
->pluck('aggregate', 'category_id')
|
||||
->map(fn ($count): int => (int) $count);
|
||||
|
||||
return [
|
||||
'categories' => static::buildListingDirectoryTree($categories, $activeListingCounts),
|
||||
'selectedCategory' => $selectedCategoryId
|
||||
? $categories->firstWhere('id', $selectedCategoryId)
|
||||
: null,
|
||||
'filterIds' => static::listingFilterIds($selectedCategoryId, $categories),
|
||||
];
|
||||
}
|
||||
|
||||
public static function listingFilterIds(?int $selectedCategoryId, ?Collection $categories = null): ?array
|
||||
{
|
||||
if (! $selectedCategoryId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($categories instanceof Collection) {
|
||||
$selectedCategory = $categories->firstWhere('id', $selectedCategoryId);
|
||||
|
||||
if (! $selectedCategory instanceof self) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return static::descendantAndSelfIdsFromCollection($selectedCategoryId, $categories);
|
||||
}
|
||||
|
||||
$selectedCategory = static::query()
|
||||
->active()
|
||||
->whereKey($selectedCategoryId)
|
||||
->first(['id']);
|
||||
|
||||
if (! $selectedCategory) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $selectedCategory->descendantAndSelfIds()->all();
|
||||
}
|
||||
|
||||
public function descendantAndSelfIds(): Collection
|
||||
{
|
||||
$ids = collect([(int) $this->getKey()]);
|
||||
@ -208,54 +127,4 @@ class Category extends Model
|
||||
{
|
||||
return $this->hasMany(\Modules\Listing\Models\Listing::class)->where('status', 'active');
|
||||
}
|
||||
|
||||
private static function buildListingDirectoryTree(Collection $categories, Collection $activeListingCounts, ?int $parentId = null): Collection
|
||||
{
|
||||
return $categories
|
||||
->filter(fn (Category $category): bool => $parentId === null
|
||||
? $category->parent_id === null
|
||||
: (int) $category->parent_id === $parentId)
|
||||
->values()
|
||||
->map(function (Category $category) use ($categories, $activeListingCounts): Category {
|
||||
$children = static::buildListingDirectoryTree($categories, $activeListingCounts, (int) $category->getKey());
|
||||
$directActiveListingsCount = (int) $activeListingCounts->get((int) $category->getKey(), 0);
|
||||
$activeListingTotal = $directActiveListingsCount + $children->sum(
|
||||
fn (Category $child): int => (int) $child->getAttribute('active_listing_total')
|
||||
);
|
||||
|
||||
$category->setRelation('children', $children);
|
||||
$category->setAttribute('direct_active_listings_count', $directActiveListingsCount);
|
||||
$category->setAttribute('active_listing_total', $activeListingTotal);
|
||||
|
||||
return $category;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
private static function descendantAndSelfIdsFromCollection(int $selectedCategoryId, Collection $categories): array
|
||||
{
|
||||
$ids = collect([$selectedCategoryId]);
|
||||
$frontier = collect([$selectedCategoryId]);
|
||||
|
||||
while ($frontier->isNotEmpty()) {
|
||||
$children = $categories
|
||||
->filter(fn (Category $category): bool => $category->parent_id !== null && in_array((int) $category->parent_id, $frontier->all(), true))
|
||||
->pluck('id')
|
||||
->map(fn ($id): int => (int) $id)
|
||||
->values();
|
||||
|
||||
if ($children->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$ids = $ids
|
||||
->merge($children)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$frontier = $children;
|
||||
}
|
||||
|
||||
return $ids->all();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@foreach($categories as $category)
|
||||
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<a href="{{ route('categories.show', $category) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
|
||||
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
|
||||
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
|
||||
|
||||
33
Modules/Category/resources/views/show.blade.php
Normal file
33
Modules/Category/resources/views/show.blade.php
Normal file
@ -0,0 +1,33 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold">{{ $category->icon ?? '' }} {{ $category->name }}</h1>
|
||||
@if($category->description)<p class="text-gray-600 mt-2">{{ $category->description }}</p>@endif
|
||||
</div>
|
||||
@if($category->children->count())
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
@foreach($category->children as $child)
|
||||
<a href="{{ route('categories.show', $child) }}" class="bg-blue-50 rounded-lg p-4 text-center hover:bg-blue-100 transition">
|
||||
<h3 class="font-medium text-blue-800">{{ $child->name }}</h3>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<h2 class="text-xl font-bold mb-4">Listings in {{ $category->name }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@forelse($listings as $listing)
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold">{{ $listing->title }}</h3>
|
||||
<p class="text-green-600 font-bold">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
|
||||
<a href="{{ route('listings.show', $listing) }}" class="mt-2 block text-blue-600 hover:underline">View →</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 col-span-3">No listings in this category yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-6">{{ $listings->links() }}</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -4,7 +4,7 @@
|
||||
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@foreach($categories as $category)
|
||||
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<a href="{{ route('categories.show', $category) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
|
||||
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
|
||||
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold">{{ $category->icon ?? '' }} {{ $category->name }}</h1>
|
||||
@if($category->description)<p class="text-gray-600 mt-2">{{ $category->description }}</p>@endif
|
||||
</div>
|
||||
@if($category->children->count())
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
@foreach($category->children as $child)
|
||||
<a href="{{ route('categories.show', $child) }}" class="bg-blue-50 rounded-lg p-4 text-center hover:bg-blue-100 transition">
|
||||
<h3 class="font-medium text-blue-800">{{ $child->name }}</h3>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<h2 class="text-xl font-bold mb-4">Listings in {{ $category->name }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@forelse($listings as $listing)
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold">{{ $listing->title }}</h3>
|
||||
<p class="text-green-600 font-bold">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
|
||||
<a href="{{ route('listings.show', $listing) }}" class="mt-2 block text-blue-600 hover:underline">View →</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 col-span-3">No listings in this category yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-6">{{ $listings->links() }}</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -4,7 +4,7 @@
|
||||
<h1 class="text-3xl font-bold mb-6">{{ __('messages.categories') }}</h1>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
@foreach($categories as $category)
|
||||
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<a href="{{ route('categories.show', $category) }}" class="bg-white rounded-lg shadow-md p-6 text-center hover:shadow-lg transition hover:bg-blue-50">
|
||||
<div class="text-4xl mb-3">{{ $category->icon ?? '📦' }}</div>
|
||||
<h3 class="font-semibold text-gray-900">{{ $category->name }}</h3>
|
||||
<p class="text-gray-500 text-sm mt-1">{{ $category->children->count() }} subcategories</p>
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold">{{ $category->icon ?? '' }} {{ $category->name }}</h1>
|
||||
@if($category->description)<p class="text-gray-600 mt-2">{{ $category->description }}</p>@endif
|
||||
</div>
|
||||
@if($category->children->count())
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
@foreach($category->children as $child)
|
||||
<a href="{{ route('categories.show', $child) }}" class="bg-blue-50 rounded-lg p-4 text-center hover:bg-blue-100 transition">
|
||||
<h3 class="font-medium text-blue-800">{{ $child->name }}</h3>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
<h2 class="text-xl font-bold mb-4">Listings in {{ $category->name }}</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@forelse($listings as $listing)
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="p-4">
|
||||
<h3 class="font-semibold">{{ $listing->title }}</h3>
|
||||
<p class="text-green-600 font-bold">{{ $listing->price ? number_format($listing->price, 0).' '.$listing->currency : 'Free' }}</p>
|
||||
<a href="{{ route('listings.show', $listing) }}" class="mt-2 block text-blue-600 hover:underline">View →</a>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-gray-500 col-span-3">No listings in this category yet.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="mt-6">{{ $listings->links() }}</div>
|
||||
</div>
|
||||
@endsection
|
||||
@ -4,4 +4,5 @@ use Modules\Category\Http\Controllers\CategoryController;
|
||||
|
||||
Route::prefix('categories')->name('categories.')->group(function () {
|
||||
Route::get('/', [CategoryController::class, 'index'])->name('index');
|
||||
Route::get('/{category}', [CategoryController::class, 'show'])->name('show');
|
||||
});
|
||||
|
||||
@ -16,15 +16,13 @@ class ConversationController extends Controller
|
||||
{
|
||||
public function inbox(Request $request): View
|
||||
{
|
||||
$user = $request->user();
|
||||
$userId = $user ? (int) $user->getKey() : null;
|
||||
$requiresLogin = ! $user;
|
||||
$userId = (int) $request->user()->getKey();
|
||||
$messageFilter = $this->resolveMessageFilter($request);
|
||||
|
||||
$conversations = collect();
|
||||
$selectedConversation = null;
|
||||
|
||||
if ($userId && $this->messagingTablesReady()) {
|
||||
if ($this->messagingTablesReady()) {
|
||||
try {
|
||||
$conversations = Conversation::inboxForUser($userId, $messageFilter);
|
||||
$selectedConversation = Conversation::resolveSelected($conversations, $request->integer('conversation'));
|
||||
@ -52,7 +50,6 @@ class ConversationController extends Controller
|
||||
'selectedConversation' => $selectedConversation,
|
||||
'messageFilter' => $messageFilter,
|
||||
'quickMessages' => QuickMessageCatalog::all(),
|
||||
'requiresLogin' => $requiresLogin,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -8,18 +8,6 @@
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'inbox'])
|
||||
|
||||
<section class="bg-white border border-slate-200 rounded-xl p-0 overflow-hidden">
|
||||
@if($requiresLogin ?? false)
|
||||
<div class="border-b border-slate-200 px-5 py-4 bg-slate-50 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Inbox</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Stay on this page and log in when you want to access your conversations.</p>
|
||||
</div>
|
||||
<a href="{{ route('login', ['redirect' => request()->fullUrl()]) }}" class="inline-flex items-center justify-center rounded-full bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-slate-800 transition">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-[420px,1fr] min-h-[620px]">
|
||||
<div class="border-b xl:border-b-0 xl:border-r border-slate-200">
|
||||
<div class="px-6 py-5 border-b border-slate-200 flex items-center justify-between gap-3">
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Conversation\App\Http\Controllers\ConversationController;
|
||||
|
||||
Route::prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/inbox', [ConversationController::class, 'inbox'])->name('inbox.index');
|
||||
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/gelen-kutusu', [ConversationController::class, 'inbox'])->name('inbox.index');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->name('conversations.')->group(function () {
|
||||
|
||||
@ -39,7 +39,6 @@ class FavoriteController extends Controller
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$requiresLogin = ! $user;
|
||||
|
||||
$categories = collect();
|
||||
if ($this->tableExists('categories')) {
|
||||
@ -56,7 +55,7 @@ class FavoriteController extends Controller
|
||||
$selectedConversation = null;
|
||||
$buyerConversationListingMap = [];
|
||||
|
||||
if ($user && $activeTab === 'listings') {
|
||||
if ($activeTab === 'listings') {
|
||||
try {
|
||||
if ($this->tableExists('favorite_listings')) {
|
||||
$favoriteListings = $user->favoriteListings()
|
||||
@ -101,7 +100,7 @@ class FavoriteController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $activeTab === 'searches') {
|
||||
if ($activeTab === 'searches') {
|
||||
try {
|
||||
if ($this->tableExists('favorite_searches')) {
|
||||
$favoriteSearches = $user->favoriteSearches()
|
||||
@ -115,7 +114,7 @@ class FavoriteController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
if ($user && $activeTab === 'sellers') {
|
||||
if ($activeTab === 'sellers') {
|
||||
try {
|
||||
if ($this->tableExists('favorite_sellers')) {
|
||||
$favoriteSellers = $user->favoriteSellers()
|
||||
@ -144,7 +143,6 @@ class FavoriteController extends Controller
|
||||
'selectedConversation' => $selectedConversation,
|
||||
'buyerConversationListingMap' => $buyerConversationListingMap,
|
||||
'quickMessages' => QuickMessageCatalog::all(),
|
||||
'requiresLogin' => $requiresLogin,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -8,18 +8,6 @@
|
||||
@include('panel.partials.sidebar', ['activeMenu' => 'favorites', 'activeFavoritesTab' => $activeTab])
|
||||
|
||||
<section class="bg-white border border-slate-200">
|
||||
@if($requiresLogin ?? false)
|
||||
<div class="border-b border-slate-200 px-5 py-4 bg-slate-50 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-slate-900">Favorites</h1>
|
||||
<p class="text-sm text-slate-500 mt-1">Stay on this page and log in when you want to sync saved listings, searches, and sellers.</p>
|
||||
</div>
|
||||
<a href="{{ route('login', ['redirect' => request()->fullUrl()]) }}" class="inline-flex items-center justify-center rounded-full bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white hover:bg-slate-800 transition">
|
||||
Log in
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($activeTab === 'listings')
|
||||
@php
|
||||
$listingTabQuery = array_filter([
|
||||
|
||||
@ -3,11 +3,8 @@
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Favorite\App\Http\Controllers\FavoriteController;
|
||||
|
||||
Route::prefix('favorites')->name('favorites.')->group(function () {
|
||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||
});
|
||||
|
||||
Route::middleware('auth')->prefix('favorites')->name('favorites.')->group(function () {
|
||||
Route::get('/', [FavoriteController::class, 'index'])->name('index');
|
||||
Route::post('/listings/{listing}/toggle', [FavoriteController::class, 'toggleListing'])->name('listings.toggle');
|
||||
Route::post('/sellers/{seller}/toggle', [FavoriteController::class, 'toggleSeller'])->name('sellers.toggle');
|
||||
Route::post('/searches', [FavoriteController::class, 'storeSearch'])->name('searches.store');
|
||||
|
||||
@ -4,6 +4,7 @@ namespace Modules\Listing\Http\Controllers;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Modules\Conversation\App\Models\Conversation;
|
||||
use Modules\Favorite\App\Models\FavoriteSearch;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\Location\Models\City;
|
||||
@ -64,37 +65,45 @@ class ListingController extends Controller
|
||||
$selectedCityName
|
||||
);
|
||||
|
||||
$listingDirectory = Category::listingDirectory($categoryId);
|
||||
|
||||
$browseFilters = [
|
||||
'search' => $search,
|
||||
'country' => $selectedCountryName,
|
||||
'city' => $selectedCityName,
|
||||
'min_price' => $minPrice,
|
||||
'max_price' => $maxPrice,
|
||||
'date_filter' => $dateFilter,
|
||||
];
|
||||
|
||||
$allListingsTotal = Listing::query()
|
||||
->active()
|
||||
->forBrowseFilters($browseFilters)
|
||||
->count();
|
||||
|
||||
$listingsQuery = Listing::query()
|
||||
->active()
|
||||
->where('status', 'active')
|
||||
->with('category:id,name')
|
||||
->forBrowseFilters([
|
||||
...$browseFilters,
|
||||
'category_ids' => $listingDirectory['filterIds'],
|
||||
])
|
||||
->applyBrowseSort($sort);
|
||||
->searchTerm($search)
|
||||
->forCategory($categoryId)
|
||||
->when($selectedCountryName, fn ($query) => $query->where('country', $selectedCountryName))
|
||||
->when($selectedCityName, fn ($query) => $query->where('city', $selectedCityName))
|
||||
->when(! is_null($minPrice), fn ($query) => $query->whereNotNull('price')->where('price', '>=', $minPrice))
|
||||
->when(! is_null($maxPrice), fn ($query) => $query->whereNotNull('price')->where('price', '<=', $maxPrice));
|
||||
|
||||
$this->applyDateFilter($listingsQuery, $dateFilter);
|
||||
$this->applySorting($listingsQuery, $sort);
|
||||
|
||||
$listings = $listingsQuery
|
||||
->paginate(16)
|
||||
->withQueryString();
|
||||
|
||||
$categories = $listingDirectory['categories'];
|
||||
$selectedCategory = $listingDirectory['selectedCategory'];
|
||||
$categories = Category::query()
|
||||
->where('is_active', true)
|
||||
->whereNull('parent_id')
|
||||
->withCount([
|
||||
'listings as active_listings_count' => fn ($query) => $query->where('status', 'active'),
|
||||
])
|
||||
->with([
|
||||
'children' => fn ($query) => $query
|
||||
->where('is_active', true)
|
||||
->withCount([
|
||||
'listings as active_listings_count' => fn ($childQuery) => $childQuery->where('status', 'active'),
|
||||
])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name'),
|
||||
])
|
||||
->orderBy('sort_order')
|
||||
->orderBy('name')
|
||||
->get(['id', 'name', 'parent_id']);
|
||||
|
||||
$selectedCategory = $categoryId
|
||||
? Category::query()->whereKey($categoryId)->first(['id', 'name'])
|
||||
: null;
|
||||
|
||||
$favoriteListingIds = [];
|
||||
$isCurrentSearchSaved = false;
|
||||
@ -145,7 +154,6 @@ class ListingController extends Controller
|
||||
'favoriteListingIds',
|
||||
'isCurrentSearchSaved',
|
||||
'conversationListingMap',
|
||||
'allListingsTotal',
|
||||
));
|
||||
}
|
||||
|
||||
@ -312,4 +320,24 @@ class ListingController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
private function applyDateFilter($query, string $dateFilter): void
|
||||
{
|
||||
match ($dateFilter) {
|
||||
'today' => $query->where('created_at', '>=', Carbon::now()->startOfDay()),
|
||||
'week' => $query->where('created_at', '>=', Carbon::now()->subDays(7)),
|
||||
'month' => $query->where('created_at', '>=', Carbon::now()->subDays(30)),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function applySorting($query, string $sort): void
|
||||
{
|
||||
match ($sort) {
|
||||
'newest' => $query->reorder()->orderByDesc('created_at'),
|
||||
'oldest' => $query->reorder()->orderBy('created_at'),
|
||||
'price_asc' => $query->reorder()->orderByRaw('price is null')->orderBy('price'),
|
||||
'price_desc' => $query->reorder()->orderByRaw('price is null')->orderByDesc('price'),
|
||||
default => $query->reorder()->orderByDesc('is_featured')->orderByDesc('created_at'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,13 +2,11 @@
|
||||
namespace Modules\Listing\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Category\Models\Category;
|
||||
use Modules\Listing\States\ListingStatus;
|
||||
use Modules\Listing\Support\ListingPanelHelper;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
@ -74,16 +72,11 @@ class Listing extends Model implements HasMedia
|
||||
public function scopePublicFeed(Builder $query): Builder
|
||||
{
|
||||
return $query
|
||||
->active()
|
||||
->where('status', 'active')
|
||||
->orderByDesc('is_featured')
|
||||
->orderByDesc('created_at');
|
||||
}
|
||||
|
||||
public function scopeActive(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', 'active');
|
||||
}
|
||||
|
||||
public function scopeSearchTerm(Builder $query, string $search): Builder
|
||||
{
|
||||
$search = trim($search);
|
||||
@ -103,57 +96,11 @@ class Listing extends Model implements HasMedia
|
||||
|
||||
public function scopeForCategory(Builder $query, ?int $categoryId): Builder
|
||||
{
|
||||
return $query->forCategoryIds(Category::listingFilterIds($categoryId));
|
||||
}
|
||||
|
||||
public function scopeForCategoryIds(Builder $query, ?array $categoryIds): Builder
|
||||
{
|
||||
if ($categoryIds === null) {
|
||||
if (! $categoryId) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ($categoryIds === []) {
|
||||
return $query->whereRaw('1 = 0');
|
||||
}
|
||||
|
||||
return $query->whereIn('category_id', $categoryIds);
|
||||
}
|
||||
|
||||
public function scopeForBrowseFilters(Builder $query, array $filters): Builder
|
||||
{
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
$country = isset($filters['country']) ? trim((string) $filters['country']) : null;
|
||||
$city = isset($filters['city']) ? trim((string) $filters['city']) : null;
|
||||
$minPrice = is_numeric($filters['min_price'] ?? null) ? max((float) $filters['min_price'], 0) : null;
|
||||
$maxPrice = is_numeric($filters['max_price'] ?? null) ? max((float) $filters['max_price'], 0) : null;
|
||||
$dateFilter = (string) ($filters['date_filter'] ?? 'all');
|
||||
$categoryIds = $filters['category_ids'] ?? null;
|
||||
|
||||
$query
|
||||
->searchTerm($search)
|
||||
->forCategoryIds(is_array($categoryIds) ? $categoryIds : null)
|
||||
->when($country !== null && $country !== '', fn (Builder $builder) => $builder->where('country', $country))
|
||||
->when($city !== null && $city !== '', fn (Builder $builder) => $builder->where('city', $city))
|
||||
->when(! is_null($minPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '>=', $minPrice))
|
||||
->when(! is_null($maxPrice), fn (Builder $builder) => $builder->whereNotNull('price')->where('price', '<=', $maxPrice));
|
||||
|
||||
return match ($dateFilter) {
|
||||
'today' => $query->where('created_at', '>=', Carbon::now()->startOfDay()),
|
||||
'week' => $query->where('created_at', '>=', Carbon::now()->subDays(7)),
|
||||
'month' => $query->where('created_at', '>=', Carbon::now()->subDays(30)),
|
||||
default => $query,
|
||||
};
|
||||
}
|
||||
|
||||
public function scopeApplyBrowseSort(Builder $query, string $sort): Builder
|
||||
{
|
||||
return match ($sort) {
|
||||
'newest' => $query->reorder()->orderByDesc('created_at'),
|
||||
'oldest' => $query->reorder()->orderBy('created_at'),
|
||||
'price_asc' => $query->reorder()->orderByRaw('price is null')->orderBy('price'),
|
||||
'price_desc' => $query->reorder()->orderByRaw('price is null')->orderByDesc('price'),
|
||||
default => $query->reorder()->orderByDesc('is_featured')->orderByDesc('created_at'),
|
||||
};
|
||||
return $query->where('category_id', $categoryId);
|
||||
}
|
||||
|
||||
public function themeGallery(): array
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
@php
|
||||
$totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$totalListings = (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
@ -43,7 +43,8 @@
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$childCount = (int) $category->children->sum('active_listings_count');
|
||||
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
@ -63,7 +64,7 @@
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listings_count, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
@php
|
||||
$totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$totalListings = (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
@ -43,7 +43,8 @@
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$childCount = (int) $category->children->sum('active_listings_count');
|
||||
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
@ -63,7 +64,7 @@
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listings_count, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
@extends('app::layouts.app')
|
||||
@section('content')
|
||||
@php
|
||||
$totalListings = isset($allListingsTotal) ? (int) $allListingsTotal : (int) $listings->total();
|
||||
$totalListings = (int) $listings->total();
|
||||
$activeCategoryName = $selectedCategory?->name ? trim((string) $selectedCategory->name) : '';
|
||||
$pageTitle = $activeCategoryName !== ''
|
||||
? 'İkinci El '.$activeCategoryName.' İlanları ve Fiyatları'
|
||||
@ -43,7 +43,8 @@
|
||||
|
||||
@foreach($categories as $category)
|
||||
@php
|
||||
$categoryCount = (int) $category->active_listing_total;
|
||||
$childCount = (int) $category->children->sum('active_listings_count');
|
||||
$categoryCount = (int) $category->active_listings_count + $childCount;
|
||||
$isSelectedParent = (int) $categoryId === (int) $category->id;
|
||||
$categoryUrl = route('listings.index', array_filter(array_merge($baseCategoryQuery, [
|
||||
'category' => $category->id,
|
||||
@ -63,7 +64,7 @@
|
||||
@endphp
|
||||
<a href="{{ $childUrl }}" class="ml-2 flex items-center justify-between rounded-lg px-2 py-1.5 text-[13px] font-medium {{ $isSelectedChild ? 'bg-rose-50 text-rose-600' : 'text-slate-600 hover:bg-slate-100' }}">
|
||||
<span>{{ $childCategory->name }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listing_total, 0, ',', '.') }}</span>
|
||||
<span>{{ number_format((int) $childCategory->active_listings_count, 0, ',', '.') }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@endforeach
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<a href="{{ route('home') }}">Anasayfa</a>
|
||||
@foreach(($breadcrumbCategories ?? collect()) as $crumb)
|
||||
<span>›</span>
|
||||
<a href="{{ route('listings.index', ['category' => $crumb->id]) }}">{{ $crumb->name }}</a>
|
||||
<a href="{{ route('categories.show', $crumb) }}">{{ $crumb->name }}</a>
|
||||
@endforeach
|
||||
<span>›</span>
|
||||
<span>{{ $displayTitle }}</span>
|
||||
|
||||
@ -174,7 +174,7 @@ class ListingResource extends Resource
|
||||
StateFusionSelectColumn::make('status'),
|
||||
TextColumn::make('city'),
|
||||
TextColumn::make('created_at')->dateTime()->sortable(),
|
||||
])->defaultSort('id', 'desc')->filters([
|
||||
])->filters([
|
||||
StateFusionSelectFilter::make('status'),
|
||||
SelectFilter::make('category_id')
|
||||
->label('Category')
|
||||
|
||||
@ -135,16 +135,6 @@ class User extends Authenticatable implements FilamentUser, HasTenants, HasAvata
|
||||
return filled($this->avatar_url) ? Storage::disk('public')->url($this->avatar_url) : null;
|
||||
}
|
||||
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return trim((string) ($this->name ?: $this->email ?: 'User'));
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return trim((string) $this->email);
|
||||
}
|
||||
|
||||
public function toggleFavoriteListing(Listing $listing): bool
|
||||
{
|
||||
$isFavorite = $this->favoriteListings()->whereKey($listing->getKey())->exists();
|
||||
|
||||
@ -11,27 +11,19 @@ use Illuminate\View\View;
|
||||
|
||||
class AuthenticatedSessionController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the login view.
|
||||
*/
|
||||
public function create(): View
|
||||
{
|
||||
$redirectTo = $this->sanitizeRedirectTarget(request()->query('redirect'));
|
||||
|
||||
if ($redirectTo) {
|
||||
request()->session()->put('url.intended', $redirectTo);
|
||||
}
|
||||
|
||||
return view('auth.login', [
|
||||
'redirectTo' => $redirectTo,
|
||||
]);
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming authentication request.
|
||||
*/
|
||||
public function store(LoginRequest $request): RedirectResponse
|
||||
{
|
||||
$redirectTo = $this->sanitizeRedirectTarget($request->input('redirect'));
|
||||
|
||||
if ($redirectTo) {
|
||||
$request->session()->put('url.intended', $redirectTo);
|
||||
}
|
||||
|
||||
$request->authenticate();
|
||||
|
||||
$request->session()->regenerate();
|
||||
@ -39,6 +31,9 @@ class AuthenticatedSessionController extends Controller
|
||||
return redirect()->intended(route('dashboard', absolute: false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy an authenticated session.
|
||||
*/
|
||||
public function destroy(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
@ -49,34 +44,4 @@ class AuthenticatedSessionController extends Controller
|
||||
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
private function sanitizeRedirectTarget(?string $target): ?string
|
||||
{
|
||||
$target = trim((string) $target);
|
||||
|
||||
if ($target === '' || str_starts_with($target, '//')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_starts_with($target, '/')) {
|
||||
return $target;
|
||||
}
|
||||
|
||||
if (! filter_var($target, FILTER_VALIDATE_URL)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$applicationUrl = parse_url(url('/'));
|
||||
$targetUrl = parse_url($target);
|
||||
|
||||
if (($applicationUrl['host'] ?? null) !== ($targetUrl['host'] ?? null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = $targetUrl['path'] ?? '/';
|
||||
$query = isset($targetUrl['query']) ? '?' . $targetUrl['query'] : '';
|
||||
$fragment = isset($targetUrl['fragment']) ? '#' . $targetUrl['fragment'] : '';
|
||||
|
||||
return $path . $query . $fragment;
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,13 +188,13 @@ class PanelQuickListingForm extends Component
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$this->isPublishing = false;
|
||||
session()->flash('error', 'The listing could not be created. Please try again.');
|
||||
session()->flash('error', 'İlan oluşturulamadı. Lütfen tekrar deneyin.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->isPublishing = false;
|
||||
session()->flash('success', 'Your listing has been created successfully.');
|
||||
session()->flash('success', 'İlan başarıyla oluşturuldu.');
|
||||
|
||||
$this->redirectRoute('panel.listings.index');
|
||||
}
|
||||
@ -243,23 +243,23 @@ class PanelQuickListingForm extends Component
|
||||
public function getCurrentParentNameProperty(): string
|
||||
{
|
||||
if (! $this->activeParentCategoryId) {
|
||||
return 'Category Selection';
|
||||
return 'Kategori Seçimi';
|
||||
}
|
||||
|
||||
$category = collect($this->categories)->firstWhere('id', $this->activeParentCategoryId);
|
||||
|
||||
return (string) ($category['name'] ?? 'Category Selection');
|
||||
return (string) ($category['name'] ?? 'Kategori Seçimi');
|
||||
}
|
||||
|
||||
public function getCurrentStepTitleProperty(): string
|
||||
{
|
||||
return match ($this->currentStep) {
|
||||
1 => 'Photos',
|
||||
2 => 'Category Selection',
|
||||
3 => 'Listing Details',
|
||||
4 => 'Attributes',
|
||||
5 => 'Preview',
|
||||
default => 'Create Listing',
|
||||
1 => 'Fotoğraf',
|
||||
2 => 'Kategori Seçimi',
|
||||
3 => 'İlan Bilgileri',
|
||||
4 => 'İlan Özellikleri',
|
||||
5 => 'İlan Önizlemesi',
|
||||
default => 'İlan Ver',
|
||||
};
|
||||
}
|
||||
|
||||
@ -352,7 +352,7 @@ class PanelQuickListingForm extends Component
|
||||
|
||||
public function getCurrentUserNameProperty(): string
|
||||
{
|
||||
return (string) (auth()->user()?->name ?: 'User');
|
||||
return (string) (auth()->user()?->name ?: 'Kullanıcı');
|
||||
}
|
||||
|
||||
public function getCurrentUserInitialProperty(): string
|
||||
@ -402,8 +402,8 @@ class PanelQuickListingForm extends Component
|
||||
Rule::in(collect($this->categories)->pluck('id')->all()),
|
||||
],
|
||||
], [
|
||||
'selectedCategoryId.required' => 'Please choose a category.',
|
||||
'selectedCategoryId.in' => 'Please choose a valid category.',
|
||||
'selectedCategoryId.required' => 'Lütfen bir kategori seçin.',
|
||||
'selectedCategoryId.in' => 'Geçerli bir kategori seçin.',
|
||||
]);
|
||||
}
|
||||
|
||||
@ -426,18 +426,18 @@ class PanelQuickListingForm extends Component
|
||||
->contains(fn (array $city): bool => $city['id'] === (int) $value);
|
||||
|
||||
if (! $cityExists) {
|
||||
$fail('The selected city does not belong to the chosen country.');
|
||||
$fail('Seçtiğiniz şehir, seçilen ülkeye ait değil.');
|
||||
}
|
||||
},
|
||||
],
|
||||
], [
|
||||
'listingTitle.required' => 'A title is required.',
|
||||
'listingTitle.max' => 'The title may not exceed 70 characters.',
|
||||
'price.required' => 'A price is required.',
|
||||
'price.numeric' => 'The price must be numeric.',
|
||||
'description.required' => 'A description is required.',
|
||||
'description.max' => 'The description may not exceed 1450 characters.',
|
||||
'selectedCountryId.required' => 'Please choose a country.',
|
||||
'listingTitle.required' => 'İlan başlığı zorunludur.',
|
||||
'listingTitle.max' => 'İlan başlığı en fazla 70 karakter olabilir.',
|
||||
'price.required' => 'Fiyat zorunludur.',
|
||||
'price.numeric' => 'Fiyat sayısal olmalıdır.',
|
||||
'description.required' => 'Açıklama zorunludur.',
|
||||
'description.max' => 'Açıklama en fazla 1450 karakter olabilir.',
|
||||
'selectedCountryId.required' => 'Ülke seçimi zorunludur.',
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -2,45 +2,40 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
final class HomeSlideDefaults
|
||||
{
|
||||
/**
|
||||
* @return array<int, array{badge: string, title: string, subtitle: string, primary_button_text: string, secondary_button_text: string, image_path: string}>
|
||||
* @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' => 'Featured Marketplace',
|
||||
'title' => 'List products in minutes and reach local buyers faster.',
|
||||
'subtitle' => 'A calm, simple marketplace for everyday electronics, home finds, and local deals.',
|
||||
'primary_button_text' => 'Browse Listings',
|
||||
'secondary_button_text' => 'Post Listing',
|
||||
'image_path' => 'images/home-slides/slide-marketplace.svg',
|
||||
'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' => 'Fresh Categories',
|
||||
'title' => 'Explore electronics, vehicles, fashion, and home in one clean flow.',
|
||||
'subtitle' => 'Move between categories quickly, compare listings, and message sellers without friction.',
|
||||
'primary_button_text' => 'See Categories',
|
||||
'secondary_button_text' => 'Start Now',
|
||||
'image_path' => 'images/home-slides/slide-categories.svg',
|
||||
'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' => 'Local Shopping',
|
||||
'title' => 'Discover nearby second-hand picks with a more polished storefront.',
|
||||
'subtitle' => 'Filter by city, save favorites, and turn local demand into quick conversations.',
|
||||
'primary_button_text' => 'Nearby Deals',
|
||||
'secondary_button_text' => 'Sell for Free',
|
||||
'image_path' => 'images/home-slides/slide-local.svg',
|
||||
'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, image_path: string|null}>
|
||||
* @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
|
||||
{
|
||||
@ -57,7 +52,6 @@ final class HomeSlideDefaults
|
||||
$subtitle = trim((string) ($slide['subtitle'] ?? ''));
|
||||
$primaryButtonText = trim((string) ($slide['primary_button_text'] ?? ''));
|
||||
$secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? ''));
|
||||
$imagePath = self::normalizeImagePath($slide['image_path'] ?? null);
|
||||
|
||||
if ($title === '') {
|
||||
return null;
|
||||
@ -69,7 +63,6 @@ final class HomeSlideDefaults
|
||||
'subtitle' => $subtitle !== '' ? $subtitle : $fallback['subtitle'],
|
||||
'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : $fallback['primary_button_text'],
|
||||
'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : $fallback['secondary_button_text'],
|
||||
'image_path' => $imagePath !== '' ? $imagePath : ($fallback['image_path'] ?? null),
|
||||
];
|
||||
})
|
||||
->filter(fn ($slide): bool => is_array($slide))
|
||||
@ -81,19 +74,4 @@ final class HomeSlideDefaults
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private static function normalizeImagePath(mixed $value): string
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
$firstValue = Arr::first($value, fn ($item): bool => is_string($item) && trim($item) !== '');
|
||||
|
||||
return is_string($firstValue) ? trim($firstValue) : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
"spatie/laravel-permission": "^6.24",
|
||||
"spatie/laravel-settings": "^3.7",
|
||||
"stechstudio/filament-impersonate": "^5.1",
|
||||
"tallcms/cms": "^3.2",
|
||||
"tapp/filament-country-code-field": "^2.0",
|
||||
"ysfkaya/filament-phone-input": "^4.1"
|
||||
},
|
||||
|
||||
569
config/tallcms.php
Normal file
569
config/tallcms.php
Normal file
@ -0,0 +1,569 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| TallCMS Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The current version of TallCMS. Read dynamically from composer.json
|
||||
| to ensure it's always in sync with the installed package version.
|
||||
|
|
||||
*/
|
||||
'version' => (function () {
|
||||
$composerJson = dirname(__DIR__).'/composer.json';
|
||||
if (file_exists($composerJson)) {
|
||||
$data = json_decode(file_get_contents($composerJson), true);
|
||||
|
||||
return $data['version'] ?? 'unknown';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
})(),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Operation Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determines how TallCMS operates. Auto-detection works in most cases:
|
||||
| - 'standalone': Full TallCMS installation (tallcms/tallcms skeleton)
|
||||
| - 'plugin': Installed as a plugin in existing Filament app
|
||||
| - null: Auto-detect based on .tallcms-standalone marker file
|
||||
|
|
||||
*/
|
||||
'mode' => env('TALLCMS_MODE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Table prefix for all TallCMS tables. Default 'tallcms_' maintains
|
||||
| compatibility with v1.x installations. Can be customized in plugin
|
||||
| mode to avoid conflicts with existing tables.
|
||||
|
|
||||
*/
|
||||
'database' => [
|
||||
'prefix' => env('TALLCMS_TABLE_PREFIX', 'tallcms_'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Plugin Mode Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration specific to plugin mode operation. These settings are
|
||||
| ignored in standalone mode.
|
||||
|
|
||||
*/
|
||||
'plugin_mode' => [
|
||||
// Enable frontend CMS page routes.
|
||||
// When enabled, TallCMS registers both / (homepage) and /{slug} routes.
|
||||
// WARNING: Without a prefix, this will override your app's homepage route!
|
||||
'routes_enabled' => env('TALLCMS_ROUTES_ENABLED', false),
|
||||
|
||||
// Optional URL prefix for CMS routes (e.g., 'cms' results in /cms and /cms/{slug})
|
||||
// Leave empty for root-level routes (/, /about, /contact)
|
||||
// When empty, smart exclusions prevent conflicts with your app routes.
|
||||
'routes_prefix' => env('TALLCMS_ROUTES_PREFIX', ''),
|
||||
|
||||
// Route name prefix for plugin mode (e.g., 'tallcms.' results in tallcms.cms.page)
|
||||
'route_name_prefix' => env('TALLCMS_PLUGIN_ROUTE_NAME_PREFIX', 'tallcms.'),
|
||||
|
||||
// Route exclusion pattern - paths matching this regex are excluded from CMS routing.
|
||||
// Default excludes common Laravel/Filament paths. Panel path is auto-excluded.
|
||||
//
|
||||
// In NON-i18n mode with standard format (^(?!foo|bar).*$): Merged with base exclusions.
|
||||
// In NON-i18n mode with custom regex: Used as-is, replaces default pattern entirely.
|
||||
// NOTE: When using custom regex, 'additional_exclusions' is ignored.
|
||||
// In i18n mode: Only standard negative lookahead format is merged; other formats ignored.
|
||||
'route_exclusions' => env('TALLCMS_PLUGIN_ROUTE_EXCLUSIONS',
|
||||
env('TALLCMS_ROUTE_EXCLUSIONS', // backward compat
|
||||
'^(?!admin|app|api|livewire|sanctum|storage|build|vendor|health|_).*$'
|
||||
)
|
||||
),
|
||||
|
||||
// Additional route exclusions as pipe-separated list (e.g., 'dashboard|settings|profile').
|
||||
// Merged with base exclusions when using standard route_exclusions format.
|
||||
// NOTE: Ignored when route_exclusions is set to a non-standard custom regex.
|
||||
// Recommended for i18n mode where custom regex is not supported.
|
||||
'additional_exclusions' => env('TALLCMS_ADDITIONAL_EXCLUSIONS', ''),
|
||||
|
||||
// Enable preview routes (/preview/page/{id}, /preview/post/{id})
|
||||
'preview_routes_enabled' => env('TALLCMS_PREVIEW_ROUTES_ENABLED', true),
|
||||
|
||||
// Enable API routes (/api/contact)
|
||||
'api_routes_enabled' => env('TALLCMS_API_ROUTES_ENABLED', true),
|
||||
|
||||
// Optional prefix for essential routes (preview, contact API) to avoid conflicts
|
||||
// e.g., 'tallcms' results in /tallcms/preview/page/{id}
|
||||
'essential_routes_prefix' => env('TALLCMS_ESSENTIAL_ROUTES_PREFIX', ''),
|
||||
|
||||
// Enable core SEO routes (sitemap.xml, robots.txt).
|
||||
// These are always registered at root level (no prefix) since search
|
||||
// engines expect them at standard locations. Safe to enable.
|
||||
'seo_routes_enabled' => env('TALLCMS_SEO_ROUTES_ENABLED', true),
|
||||
|
||||
// Enable archive routes (RSS feed, category archives, author archives).
|
||||
// These routes (/feed, /category/{slug}, /author/{slug}) may conflict
|
||||
// with your app's routes. Disabled by default in plugin mode.
|
||||
'archive_routes_enabled' => env('TALLCMS_ARCHIVE_ROUTES_ENABLED', false),
|
||||
|
||||
// Optional prefix for archive routes to avoid conflicts.
|
||||
// e.g., 'blog' results in /blog/feed, /blog/category/{slug}, /blog/author/{slug}
|
||||
'archive_routes_prefix' => env('TALLCMS_ARCHIVE_ROUTES_PREFIX', ''),
|
||||
|
||||
// Enable the TallCMS plugin system.
|
||||
// When enabled, the Plugin Manager page is visible and third-party plugins can be loaded.
|
||||
'plugins_enabled' => env('TALLCMS_PLUGINS_ENABLED', true),
|
||||
|
||||
// Enable the TallCMS theme system.
|
||||
// When enabled, the Theme Manager page is visible and themes can be loaded.
|
||||
'themes_enabled' => env('TALLCMS_THEMES_ENABLED', true),
|
||||
|
||||
// User model class. Must implement TallCmsUserContract.
|
||||
// Default works with standard Laravel User model with HasRoles trait.
|
||||
'user_model' => env('TALLCMS_USER_MODEL', 'App\\Models\\User'),
|
||||
|
||||
// Skip installer.lock check for maintenance mode in plugin mode.
|
||||
// In plugin mode, the host app doesn't use TallCMS's installer,
|
||||
// so we assume the app is properly installed. Default: true
|
||||
'skip_installer_check' => env('TALLCMS_SKIP_INSTALLER_CHECK', true),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for authentication guards used by TallCMS roles and
|
||||
| permissions. This should match your Filament panel's guard.
|
||||
|
|
||||
*/
|
||||
'auth' => [
|
||||
// Guard name for roles and permissions (should match Filament panel guard)
|
||||
'guard' => env('TALLCMS_AUTH_GUARD', 'web'),
|
||||
|
||||
// Login route for preview authentication redirect
|
||||
// Can be a route name (e.g., 'filament.admin.auth.login') or URL
|
||||
// Leave null to auto-detect Filament's login route
|
||||
'login_route' => env('TALLCMS_LOGIN_ROUTE'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filament Panel Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These settings are dynamically set by TallCmsPlugin when registered.
|
||||
| They allow customization of navigation group and sort order.
|
||||
|
|
||||
*/
|
||||
'filament' => [
|
||||
// Panel ID for route generation in notifications
|
||||
// Used for constructing admin panel URLs like filament.{panel_id}.resources.*
|
||||
'panel_id' => env('TALLCMS_PANEL_ID', 'admin'),
|
||||
|
||||
// Panel path for URL construction and middleware exclusions
|
||||
'panel_path' => env('TALLCMS_PANEL_PATH', 'admin'),
|
||||
|
||||
// Navigation group override - when set, CMS resources/pages use this group.
|
||||
// Note: UserResource stays in 'User Management' regardless of this setting.
|
||||
// Leave unset (null) to use per-resource defaults (Content Management, Settings, etc.)
|
||||
'navigation_group' => env('TALLCMS_NAVIGATION_GROUP'),
|
||||
|
||||
// Navigation sort override - when set, CMS resources/pages use this sort.
|
||||
// Leave unset (null) to use per-resource defaults.
|
||||
'navigation_sort' => env('TALLCMS_NAVIGATION_SORT') !== null
|
||||
? (int) env('TALLCMS_NAVIGATION_SORT')
|
||||
: null,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Contact Information
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Default contact information used in templates and merge tags.
|
||||
|
|
||||
*/
|
||||
'contact_email' => env('TALLCMS_CONTACT_EMAIL'),
|
||||
'company_name' => env('TALLCMS_COMPANY_NAME'),
|
||||
'company_address' => env('TALLCMS_COMPANY_ADDRESS'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Publishing Workflow
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the content publishing workflow including
|
||||
| revision history and preview tokens.
|
||||
|
|
||||
*/
|
||||
'publishing' => [
|
||||
// Maximum number of automatic revisions to keep per content item.
|
||||
// Set to null for unlimited. Default: 100
|
||||
'revision_limit' => env('CMS_REVISION_LIMIT', 100),
|
||||
|
||||
// Maximum number of manual (pinned) snapshots to keep per content item.
|
||||
// Set to null for unlimited. Default: 50
|
||||
'revision_manual_limit' => env('CMS_REVISION_MANUAL_LIMIT', 50),
|
||||
|
||||
// Notification channels for workflow events
|
||||
// Available: 'mail', 'database'
|
||||
'notification_channels' => explode(',', env('CMS_NOTIFICATION_CHANNELS', 'mail,database')),
|
||||
|
||||
// Default preview token expiry in hours
|
||||
'default_preview_expiry_hours' => 24,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Plugin System
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the TallCMS plugin system including license management.
|
||||
| The Plugin Manager UI is always available, but local plugin loading
|
||||
| requires explicit opt-in via plugin_mode.plugins_enabled.
|
||||
|
|
||||
*/
|
||||
'plugins' => [
|
||||
// Path where plugins are stored
|
||||
'path' => env('TALLCMS_PLUGINS_PATH', base_path('plugins')),
|
||||
|
||||
// Allow ZIP-based plugin uploads through admin UI
|
||||
'allow_uploads' => env('TALLCMS_PLUGIN_ALLOW_UPLOADS', env('PLUGIN_ALLOW_UPLOADS', true)),
|
||||
|
||||
// Maximum upload size for plugin ZIP files (bytes). Default: 50MB
|
||||
'max_upload_size' => env('TALLCMS_PLUGIN_MAX_UPLOAD_SIZE', env('PLUGIN_MAX_UPLOAD_SIZE', 50 * 1024 * 1024)),
|
||||
|
||||
// Plugin discovery caching
|
||||
'cache_enabled' => env('TALLCMS_PLUGIN_CACHE_ENABLED', env('PLUGIN_CACHE_ENABLED', true)),
|
||||
'cache_ttl' => 3600, // 1 hour
|
||||
|
||||
// Automatically run plugin migrations on install
|
||||
'auto_migrate' => env('TALLCMS_PLUGIN_AUTO_MIGRATE', env('PLUGIN_AUTO_MIGRATE', true)),
|
||||
|
||||
// License management settings
|
||||
'license' => [
|
||||
// License proxy URL for official TallCMS plugins
|
||||
'proxy_url' => env('TALLCMS_LICENSE_PROXY_URL', 'https://tallcms.com'),
|
||||
|
||||
// Cache TTL for license validation results (seconds). Default: 6 hours
|
||||
'cache_ttl' => 21600,
|
||||
|
||||
// Grace period when license server unreachable (days). Default: 7
|
||||
'offline_grace_days' => 7,
|
||||
|
||||
// Grace period after license expiration (days). Default: 14
|
||||
'renewal_grace_days' => 14,
|
||||
|
||||
// How often to check for updates (seconds). Default: 24 hours
|
||||
'update_check_interval' => 86400,
|
||||
|
||||
// Purchase URLs for plugins (shown when no license is active)
|
||||
'purchase_urls' => [
|
||||
'tallcms/pro' => 'https://checkout.anystack.sh/tallcms-pro-plugin',
|
||||
'tallcms/mega-menu' => 'https://checkout.anystack.sh/tallcms-mega-menu-plugin',
|
||||
],
|
||||
|
||||
// Download URLs for plugins (shown when license is valid)
|
||||
'download_urls' => [
|
||||
'tallcms/pro' => 'https://anystack.sh/download/tallcms-pro-plugin',
|
||||
'tallcms/mega-menu' => 'https://anystack.sh/download/tallcms-mega-menu-plugin',
|
||||
],
|
||||
],
|
||||
|
||||
// Official plugin catalog (shown in Plugin Manager)
|
||||
'catalog' => [
|
||||
'tallcms/pro' => [
|
||||
'name' => 'TallCMS Pro',
|
||||
'slug' => 'pro',
|
||||
'vendor' => 'tallcms',
|
||||
'description' => 'Advanced blocks, analytics, and integrations for TallCMS.',
|
||||
'author' => 'TallCMS',
|
||||
'homepage' => 'https://tallcms.com/pro',
|
||||
'icon' => 'heroicon-o-sparkles',
|
||||
'category' => 'official',
|
||||
'featured' => true,
|
||||
'download_url' => 'https://anystack.sh/download/tallcms-pro-plugin',
|
||||
'purchase_url' => 'https://checkout.anystack.sh/tallcms-pro-plugin',
|
||||
],
|
||||
'tallcms/mega-menu' => [
|
||||
'name' => 'TallCMS Mega Menu',
|
||||
'slug' => 'mega-menu',
|
||||
'vendor' => 'tallcms',
|
||||
'description' => 'Create stunning mega menus for your website with ease. Build rich, multi-column dropdown menus with images, icons, and custom layouts.',
|
||||
'author' => 'TallCMS',
|
||||
'homepage' => 'https://tallcms.com/mega-menu',
|
||||
'icon' => 'heroicon-o-bars-3-bottom-left',
|
||||
'category' => 'official',
|
||||
'featured' => true,
|
||||
'download_url' => 'https://anystack.sh/download/tallcms-mega-menu-plugin',
|
||||
'purchase_url' => 'https://checkout.anystack.sh/tallcms-mega-menu-plugin',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Theme System
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the TallCMS theme system. The Theme Manager UI is
|
||||
| always available, but theme loading requires explicit opt-in via
|
||||
| plugin_mode.themes_enabled in plugin mode.
|
||||
|
|
||||
*/
|
||||
'themes' => [
|
||||
// Path where themes are stored
|
||||
'path' => env('TALLCMS_THEMES_PATH', base_path('themes')),
|
||||
|
||||
// Allow ZIP-based theme uploads through admin UI
|
||||
'allow_uploads' => env('TALLCMS_THEME_ALLOW_UPLOADS', true),
|
||||
|
||||
// Maximum upload size for theme ZIP files (bytes). Default: 100MB
|
||||
'max_upload_size' => env('TALLCMS_THEME_MAX_UPLOAD_SIZE', 100 * 1024 * 1024),
|
||||
|
||||
// Theme discovery caching
|
||||
'cache_enabled' => env('TALLCMS_THEME_CACHE_ENABLED', false),
|
||||
'cache_ttl' => 3600, // 1 hour
|
||||
|
||||
// Preview session duration (minutes)
|
||||
'preview_duration' => 30,
|
||||
|
||||
// Rollback availability window (hours)
|
||||
'rollback_duration' => 24,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| REST API
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the TallCMS REST API. The API provides full CRUD
|
||||
| operations for Pages, Posts, Categories, and Media with authentication
|
||||
| via Laravel Sanctum tokens.
|
||||
|
|
||||
*/
|
||||
'api' => [
|
||||
// Enable or disable the REST API
|
||||
'enabled' => env('TALLCMS_API_ENABLED', false),
|
||||
|
||||
// API route prefix (e.g., 'api/v1/tallcms' results in /api/v1/tallcms/pages)
|
||||
'prefix' => env('TALLCMS_API_PREFIX', 'api/v1/tallcms'),
|
||||
|
||||
// Standard rate limit (requests per minute)
|
||||
'rate_limit' => env('TALLCMS_API_RATE_LIMIT', 60),
|
||||
|
||||
// Authentication rate limit (failed attempts before lockout)
|
||||
'auth_rate_limit' => env('TALLCMS_API_AUTH_RATE_LIMIT', 5),
|
||||
|
||||
// Authentication lockout duration (minutes)
|
||||
'auth_lockout_minutes' => env('TALLCMS_API_AUTH_LOCKOUT', 15),
|
||||
|
||||
// Default token expiry (days)
|
||||
'token_expiry_days' => env('TALLCMS_API_TOKEN_EXPIRY', 365),
|
||||
|
||||
// Maximum items per page for pagination
|
||||
'max_per_page' => 100,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Webhooks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for webhook delivery to external services. Webhooks notify
|
||||
| external systems when content is created, updated, published, or deleted.
|
||||
|
|
||||
*/
|
||||
'webhooks' => [
|
||||
// Enable or disable webhooks
|
||||
'enabled' => env('TALLCMS_WEBHOOKS_ENABLED', false),
|
||||
|
||||
// Request timeout (seconds)
|
||||
'timeout' => env('TALLCMS_WEBHOOK_TIMEOUT', 30),
|
||||
|
||||
// Maximum retry attempts
|
||||
'max_retries' => env('TALLCMS_WEBHOOK_MAX_RETRIES', 3),
|
||||
|
||||
// Delay before retry attempts (seconds) - retry 1, 2, 3
|
||||
'retry_backoff' => [60, 300, 900],
|
||||
|
||||
// Maximum response body size to store (bytes)
|
||||
'response_max_size' => 10000,
|
||||
|
||||
// Allowed hosts (empty = allow all public IPs)
|
||||
'allowed_hosts' => [],
|
||||
|
||||
// Explicitly blocked hosts
|
||||
'blocked_hosts' => [],
|
||||
|
||||
// Queue name for webhook jobs
|
||||
'queue' => env('TALLCMS_WEBHOOK_QUEUE', 'default'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Internationalization (i18n)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Core i18n configuration. Locales are merged from multiple sources:
|
||||
| - Config: Base locales (always available)
|
||||
| - Plugins: Can ADD new locale codes (cannot override config)
|
||||
| - DB: Can MODIFY existing locales (enable/disable/rename, cannot add)
|
||||
|
|
||||
*/
|
||||
'i18n' => [
|
||||
// Master switch for multilingual features
|
||||
'enabled' => env('TALLCMS_I18N_ENABLED', false),
|
||||
|
||||
// Base locales (always available, plugins can add new ones, DB can modify existing)
|
||||
'locales' => [
|
||||
'en' => [
|
||||
'label' => 'English',
|
||||
'native' => 'English',
|
||||
'rtl' => false,
|
||||
],
|
||||
'zh_CN' => [
|
||||
'label' => 'Chinese (Simplified)',
|
||||
'native' => '简体中文',
|
||||
'rtl' => false,
|
||||
],
|
||||
],
|
||||
|
||||
// Default/fallback locale (must exist in registry)
|
||||
'default_locale' => env('TALLCMS_DEFAULT_LOCALE', 'en'),
|
||||
|
||||
// URL strategy: 'prefix' (/en/about) or 'none' (query param fallback)
|
||||
'url_strategy' => 'prefix',
|
||||
|
||||
// Hide default locale from URL (/ instead of /en/)
|
||||
'hide_default_locale' => env('TALLCMS_HIDE_DEFAULT_LOCALE', true),
|
||||
|
||||
// Fallback when translation missing: 'default', 'empty', 'key'
|
||||
'fallback_behavior' => 'default',
|
||||
|
||||
// Remember locale preference in session
|
||||
'remember_locale' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Comments
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the blog post commenting system. Comments require
|
||||
| admin approval before appearing publicly.
|
||||
|
|
||||
*/
|
||||
'comments' => [
|
||||
'enabled' => env('TALLCMS_COMMENTS_ENABLED', true),
|
||||
'moderation' => env('TALLCMS_COMMENTS_MODERATION', 'manual'), // 'manual' = require approval, 'auto' = publish immediately
|
||||
'max_depth' => 2, // top-level + 1 reply level (min 1)
|
||||
'max_length' => 5000, // max comment content length
|
||||
'rate_limit' => 5, // max comments per IP per window
|
||||
'rate_limit_decay' => 600, // rate limit window in seconds
|
||||
'notification_channels' => ['mail', 'database'],
|
||||
'notify_on_approval' => true, // email commenter when approved
|
||||
'guest_comments' => true, // allow non-authenticated comments
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Media Library
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for media library features including image optimization,
|
||||
| variant generation, and responsive image handling.
|
||||
|
|
||||
*/
|
||||
'media' => [
|
||||
'optimization' => [
|
||||
// Enable or disable automatic image optimization
|
||||
'enabled' => env('TALLCMS_MEDIA_OPTIMIZATION', true),
|
||||
|
||||
// Queue name for optimization jobs
|
||||
'queue' => env('TALLCMS_MEDIA_QUEUE', 'default'),
|
||||
|
||||
// WebP quality (0-100)
|
||||
'quality' => env('TALLCMS_MEDIA_QUALITY', 80),
|
||||
|
||||
// Variant presets - customize sizes as needed
|
||||
'variants' => [
|
||||
'thumbnail' => ['width' => 300, 'height' => 300, 'fit' => 'crop'],
|
||||
'medium' => ['width' => 800, 'height' => 600, 'fit' => 'contain'],
|
||||
'large' => ['width' => 1200, 'height' => 800, 'fit' => 'contain'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Full-Text Search
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the full-text search functionality using Laravel Scout.
|
||||
| Requires SCOUT_DRIVER=database in your .env file.
|
||||
|
|
||||
*/
|
||||
'search' => [
|
||||
// Enable or disable search functionality
|
||||
'enabled' => env('TALLCMS_SEARCH_ENABLED', true),
|
||||
|
||||
// Minimum query length required before searching
|
||||
'min_query_length' => 2,
|
||||
|
||||
// Number of results per page on the search results page
|
||||
'results_per_page' => 10,
|
||||
|
||||
// Maximum results per model type to avoid memory issues
|
||||
'max_results_per_type' => 50,
|
||||
|
||||
// Which content types to include in search
|
||||
'searchable_types' => ['pages', 'posts'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| System Updates (Standalone Mode Only)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configuration for the one-click update system. These settings are
|
||||
| IGNORED in plugin mode - use Composer for updates instead.
|
||||
|
|
||||
*/
|
||||
'updates' => [
|
||||
// Enable or disable the update system (standalone mode only)
|
||||
'enabled' => env('TALLCMS_UPDATES_ENABLED', true),
|
||||
|
||||
// How often to check for updates (seconds). Default: 24 hours
|
||||
'check_interval' => 86400,
|
||||
|
||||
// Cache TTL for GitHub API responses (seconds). Default: 1 hour
|
||||
'cache_ttl' => 3600,
|
||||
|
||||
// GitHub repository for updates
|
||||
'github_repo' => 'tallcms/tallcms',
|
||||
|
||||
// Optional GitHub token for higher API rate limits
|
||||
'github_token' => env('TALLCMS_GITHUB_TOKEN'),
|
||||
|
||||
// Number of backup sets to retain
|
||||
'backup_retention' => 3,
|
||||
|
||||
// Automatically backup files before updating
|
||||
'auto_backup' => true,
|
||||
|
||||
// Require database backup before update
|
||||
'require_db_backup' => true,
|
||||
|
||||
// Maximum database size for automatic backup (bytes). Default: 100MB
|
||||
'db_backup_size_limit' => 100 * 1024 * 1024,
|
||||
|
||||
// Ed25519 public key for release signature verification (hex-encoded)
|
||||
'public_key' => env('TALLCMS_UPDATE_PUBLIC_KEY', '6c41c964c60dd5341f7ba649dcda6e6de4b0b7afac2fbb9489527987907d35a9'),
|
||||
],
|
||||
];
|
||||
@ -11,8 +11,13 @@ class HomeSliderSettingsSeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$settings = app(GeneralSettings::class);
|
||||
$settings->home_slides = HomeSlideDefaults::defaults();
|
||||
$settings->home_slides = HomeSlideDefaults::normalize($settings->home_slides ?? []);
|
||||
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
private function defaultHomeSlides(): array
|
||||
{
|
||||
return HomeSlideDefaults::defaults();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
<?php
|
||||
return [
|
||||
'site_name' => 'OpenClassify',
|
||||
'home' => 'Home',
|
||||
'categories' => 'Categories',
|
||||
'listings' => 'Listings',
|
||||
'search' => 'Search',
|
||||
'search_placeholder' => 'Search for anything...',
|
||||
'login' => 'Login',
|
||||
'register' => 'Register',
|
||||
'logout' => 'Logout',
|
||||
'find_what_you_need' => 'Find What You Need',
|
||||
'hero_subtitle' => 'Buy and sell everything in your area',
|
||||
'browse_categories' => 'Browse Categories',
|
||||
'recent_listings' => 'Recent Listings',
|
||||
'featured_listings' => 'Featured Listings',
|
||||
'post_listing' => 'Post Listing',
|
||||
'sell_something' => 'Have something to sell?',
|
||||
'free' => 'Free',
|
||||
'view' => 'View',
|
||||
'contact_seller' => 'Contact Seller',
|
||||
'home' => 'Ana Sayfa',
|
||||
'categories' => 'Kategoriler',
|
||||
'listings' => 'İlanlar',
|
||||
'search' => 'Ara',
|
||||
'search_placeholder' => 'Her şeyi arayın...',
|
||||
'login' => 'Giriş',
|
||||
'register' => 'Kayıt Ol',
|
||||
'logout' => 'Çıkış',
|
||||
'find_what_you_need' => 'İhtiyacınızı Bulun',
|
||||
'hero_subtitle' => 'Bölgenizdeki her şeyi alın ve satın',
|
||||
'browse_categories' => 'Kategorilere Göz At',
|
||||
'recent_listings' => 'Son İlanlar',
|
||||
'featured_listings' => 'Öne Çıkan İlanlar',
|
||||
'post_listing' => 'İlan Ver',
|
||||
'sell_something' => 'Satılık bir şeyiniz mi var?',
|
||||
'free' => 'Ücretsiz',
|
||||
'view' => 'Görüntüle',
|
||||
'contact_seller' => 'Satıcıyla İletişim',
|
||||
];
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
<svg width="1600" height="1000" viewBox="0 0 1600 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1600" height="1000" rx="48" fill="#F4F7FB"/>
|
||||
<rect x="70" y="70" width="1460" height="860" rx="40" fill="url(#bg)"/>
|
||||
<circle cx="1280" cy="220" r="180" fill="#E4ECF9"/>
|
||||
<rect x="170" y="156" width="310" height="56" rx="28" fill="#FFFFFF"/>
|
||||
<rect x="170" y="256" width="420" height="76" rx="22" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="170" y="358" width="500" height="34" rx="17" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="170" y="410" width="430" height="34" rx="17" fill="#0F172A" fill-opacity="0.06"/>
|
||||
<rect x="170" y="500" width="150" height="58" rx="29" fill="#0F172A"/>
|
||||
<rect x="780" y="196" width="600" height="610" rx="40" fill="#FFFFFF"/>
|
||||
<rect x="830" y="256" width="220" height="184" rx="28" fill="#E0ECFF"/>
|
||||
<rect x="1070" y="256" width="220" height="184" rx="28" fill="#E8EEF8"/>
|
||||
<rect x="830" y="466" width="220" height="184" rx="28" fill="#EEF4FF"/>
|
||||
<rect x="1070" y="466" width="220" height="184" rx="28" fill="#DCE8FF"/>
|
||||
<rect x="876" y="678" width="368" height="22" rx="11" fill="#CBD5E1"/>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="70" y1="70" x2="1530" y2="930" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FBFDFF"/>
|
||||
<stop offset="1" stop-color="#E8EFF8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,20 +0,0 @@
|
||||
<svg width="1600" height="1000" viewBox="0 0 1600 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1600" height="1000" rx="48" fill="#EEF5FF"/>
|
||||
<rect x="70" y="70" width="1460" height="860" rx="40" fill="url(#bg)"/>
|
||||
<circle cx="1220" cy="210" r="170" fill="#D7E7FF"/>
|
||||
<rect x="180" y="170" width="280" height="56" rx="28" fill="#FFFFFF"/>
|
||||
<rect x="180" y="258" width="520" height="76" rx="22" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="180" y="360" width="470" height="34" rx="17" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="180" y="412" width="390" height="34" rx="17" fill="#0F172A" fill-opacity="0.06"/>
|
||||
<rect x="180" y="500" width="180" height="60" rx="30" fill="#0F172A"/>
|
||||
<rect x="840" y="214" width="440" height="520" rx="40" fill="#FFFFFF"/>
|
||||
<path d="M1060 318C996 318 944 370 944 434C944 524 1060 640 1060 640C1060 640 1176 524 1176 434C1176 370 1124 318 1060 318Z" fill="#DBEAFE"/>
|
||||
<circle cx="1060" cy="434" r="54" fill="#93C5FD"/>
|
||||
<rect x="894" y="766" width="332" height="22" rx="11" fill="#CBD5E1"/>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="70" y1="70" x2="1530" y2="930" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F8FBFF"/>
|
||||
<stop offset="1" stop-color="#DEEAFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,24 +0,0 @@
|
||||
<svg width="1600" height="1000" viewBox="0 0 1600 1000" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1600" height="1000" rx="48" fill="#E9F1FF"/>
|
||||
<rect x="70" y="70" width="1460" height="860" rx="40" fill="url(#bg)"/>
|
||||
<circle cx="1230" cy="250" r="210" fill="#C9DCFF"/>
|
||||
<circle cx="1360" cy="170" r="90" fill="#F8FBFF"/>
|
||||
<rect x="180" y="170" width="520" height="74" rx="20" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="180" y="276" width="620" height="36" rx="18" fill="#0F172A" fill-opacity="0.08"/>
|
||||
<rect x="180" y="332" width="560" height="36" rx="18" fill="#0F172A" fill-opacity="0.06"/>
|
||||
<rect x="180" y="430" width="176" height="64" rx="32" fill="#0F172A"/>
|
||||
<rect x="376" y="430" width="176" height="64" rx="32" fill="#F8FBFF"/>
|
||||
<rect x="930" y="250" width="360" height="490" rx="48" fill="#111827"/>
|
||||
<rect x="958" y="282" width="304" height="426" rx="32" fill="#F8FAFC"/>
|
||||
<rect x="994" y="332" width="232" height="160" rx="28" fill="#D9E7FF"/>
|
||||
<rect x="994" y="522" width="168" height="18" rx="9" fill="#CBD5E1"/>
|
||||
<rect x="994" y="560" width="216" height="18" rx="9" fill="#E2E8F0"/>
|
||||
<rect x="994" y="608" width="210" height="68" rx="24" fill="#FFFFFF"/>
|
||||
<rect x="1180" y="608" width="46" height="68" rx="23" fill="#DBEAFE"/>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="70" y1="70" x2="1530" y2="930" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F8FBFF"/>
|
||||
<stop offset="1" stop-color="#D8E7FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@ -84,178 +84,6 @@ h6 {
|
||||
border-bottom: 1px solid var(--oc-border);
|
||||
}
|
||||
|
||||
.oc-nav-wrap {
|
||||
max-width: 1320px;
|
||||
margin: 0 auto;
|
||||
padding: 18px 16px 14px;
|
||||
}
|
||||
|
||||
.oc-nav-main {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(320px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.oc-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.oc-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 56px;
|
||||
padding: 0 14px 0 18px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.oc-search-icon {
|
||||
color: #6e6e73;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.oc-search-input {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--oc-text);
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.oc-search-input::placeholder {
|
||||
color: #8d8d92;
|
||||
}
|
||||
|
||||
.oc-search-submit {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #4b5563;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.oc-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.oc-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 48px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
color: #4b5563;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.oc-pill-strong {
|
||||
color: #fff;
|
||||
background: linear-gradient(180deg, #2997ff, var(--oc-primary));
|
||||
border-color: transparent;
|
||||
box-shadow: 0 10px 22px rgba(0, 113, 227, 0.18);
|
||||
}
|
||||
|
||||
.oc-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
padding: 0 22px;
|
||||
font-size: 0.96rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oc-text-link {
|
||||
color: #6e6e73;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.oc-text-link:hover {
|
||||
color: var(--oc-text);
|
||||
}
|
||||
|
||||
.oc-mobile-tools {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.oc-mobile-pills {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.oc-category-row {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid rgba(29, 29, 31, 0.08);
|
||||
}
|
||||
|
||||
.oc-category-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.oc-category-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 46px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(29, 29, 31, 0.08);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
color: var(--oc-text);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.oc-category-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 46px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
color: #4b5563;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
}
|
||||
|
||||
.oc-category-link:hover,
|
||||
.oc-category-pill:hover,
|
||||
.oc-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
color: var(--oc-text);
|
||||
}
|
||||
|
||||
.search-shell {
|
||||
border: 1px solid var(--oc-border);
|
||||
background: #ffffff;
|
||||
@ -312,58 +140,6 @@ h6 {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
.oc-nav-main {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.oc-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.oc-nav-wrap {
|
||||
padding-top: 14px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.oc-nav-main {
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-size: 1.42rem;
|
||||
}
|
||||
|
||||
.oc-actions {
|
||||
grid-column: auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.oc-nav-main {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.oc-brand {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.oc-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.oc-category-row {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@ -15,13 +15,6 @@
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
@php
|
||||
$redirectInput = old('redirect', $redirectTo ?? request('redirect'));
|
||||
@endphp
|
||||
|
||||
@if(filled($redirectInput))
|
||||
<input type="hidden" name="redirect" value="{{ $redirectInput }}">
|
||||
@endif
|
||||
|
||||
<!-- Email Address -->
|
||||
<div>
|
||||
|
||||
@ -13,21 +13,13 @@
|
||||
$subtitle = trim((string) ($slide['subtitle'] ?? ''));
|
||||
$primaryButtonText = trim((string) ($slide['primary_button_text'] ?? ''));
|
||||
$secondaryButtonText = trim((string) ($slide['secondary_button_text'] ?? ''));
|
||||
$imagePath = trim((string) ($slide['image_path'] ?? ''));
|
||||
|
||||
return [
|
||||
'badge' => $badge !== '' ? $badge : 'OpenClassify Marketplace',
|
||||
'title' => $title !== '' ? $title : 'Sell faster with a cleaner local marketplace.',
|
||||
'title' => $title !== '' ? $title : 'İlan ücreti ödemeden ürününü hızla sat!',
|
||||
'subtitle' => $subtitle !== '' ? $subtitle : 'Buy and sell everything in your area',
|
||||
'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'Browse Listings',
|
||||
'primary_button_text' => $primaryButtonText !== '' ? $primaryButtonText : 'İncele',
|
||||
'secondary_button_text' => $secondaryButtonText !== '' ? $secondaryButtonText : 'Post Listing',
|
||||
'image_url' => $imagePath !== ''
|
||||
? (str_starts_with($imagePath, 'http://') || str_starts_with($imagePath, 'https://')
|
||||
? $imagePath
|
||||
: (str_starts_with($imagePath, 'images/')
|
||||
? asset($imagePath)
|
||||
: \Illuminate\Support\Facades\Storage::disk('public')->url($imagePath)))
|
||||
: null,
|
||||
];
|
||||
})
|
||||
->values();
|
||||
@ -36,11 +28,10 @@
|
||||
$homeSlides = collect([
|
||||
[
|
||||
'badge' => 'OpenClassify Marketplace',
|
||||
'title' => 'Sell faster with a cleaner local marketplace.',
|
||||
'title' => 'İlan ücreti ödemeden ürününü hızla sat!',
|
||||
'subtitle' => 'Buy and sell everything in your area',
|
||||
'primary_button_text' => 'Browse Listings',
|
||||
'primary_button_text' => 'İncele',
|
||||
'secondary_button_text' => 'Post Listing',
|
||||
'image_url' => null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
@ -143,7 +134,7 @@
|
||||
<div class="w-full h-full rounded-[24px] bg-white overflow-hidden">
|
||||
<div class="px-3 py-2 border-b border-slate-100">
|
||||
<p class="text-rose-500 text-sm font-bold">OpenClassify</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Search listings, categories, and sellers</p>
|
||||
<p class="text-[10px] text-slate-400 mt-1">Ürün, kategori, satıcı ara</p>
|
||||
</div>
|
||||
<div class="p-2 space-y-2">
|
||||
<div class="h-10 rounded-xl bg-slate-100"></div>
|
||||
@ -159,25 +150,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute right-0 bottom-0 w-[78%] h-[88%] rounded-[28px] bg-gradient-to-br from-white/20 to-blue-500/40 border border-white/20 shadow-2xl flex items-end justify-center p-4 overflow-hidden">
|
||||
@foreach($homeSlides as $index => $slide)
|
||||
<div
|
||||
data-home-slide-visual
|
||||
@class(['absolute inset-4 transition-opacity duration-300', 'hidden' => $index !== 0])
|
||||
aria-hidden="{{ $index === 0 ? 'false' : 'true' }}"
|
||||
>
|
||||
@if($slide['image_url'])
|
||||
<img src="{{ $slide['image_url'] }}" alt="{{ $slide['title'] }}" class="w-full h-full object-cover rounded-2xl">
|
||||
@elseif($heroImage)
|
||||
<img src="{{ $heroImage }}" alt="{{ $heroListing?->title }}" class="w-full h-full object-cover rounded-2xl">
|
||||
@else
|
||||
<div class="w-full h-full rounded-2xl bg-white/90 text-slate-800 flex flex-col justify-center items-center gap-3">
|
||||
<span class="text-6xl">◌</span>
|
||||
<p class="text-sm font-semibold px-4 text-center">Upload a slide image to make this area feel complete.</p>
|
||||
</div>
|
||||
@endif
|
||||
<div class="absolute right-0 bottom-0 w-[78%] h-[88%] rounded-[28px] bg-gradient-to-br from-white/20 to-blue-500/40 border border-white/20 shadow-2xl flex items-end justify-center p-4">
|
||||
@if($heroImage)
|
||||
<img src="{{ $heroImage }}" alt="{{ $heroListing?->title }}" class="w-full h-full object-cover rounded-2xl">
|
||||
@else
|
||||
<div class="w-full h-full rounded-2xl bg-white/90 text-slate-800 flex flex-col justify-center items-center gap-3">
|
||||
<span class="text-6xl">🚗</span>
|
||||
<p class="text-sm font-semibold px-4 text-center">Görsel eklendiğinde burada öne çıkan ilan yer alacak.</p>
|
||||
</div>
|
||||
@endforeach
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -207,7 +188,7 @@
|
||||
$trendSkin = $trendSkins[$index % count($trendSkins)];
|
||||
$trendIcon = $trendIcons[$index % count($trendIcons)];
|
||||
@endphp
|
||||
<a href="{{ route('listings.index', ['category' => $category->id]) }}" class="group shrink-0 w-[170px] rounded-xl overflow-hidden border border-slate-300/80 bg-white hover:shadow-md transition snap-start">
|
||||
<a href="{{ route('categories.show', $category) }}" class="group shrink-0 w-[170px] rounded-xl overflow-hidden border border-slate-300/80 bg-white hover:shadow-md transition snap-start">
|
||||
<div class="h-[68px] bg-gradient-to-r {{ $trendSkin['gradient'] }} relative overflow-hidden">
|
||||
<span class="absolute -left-5 top-2 w-20 h-20 rounded-full {{ $trendSkin['glow'] }} blur-2xl"></span>
|
||||
<span class="absolute left-5 bottom-2 h-2.5 w-24 rounded-full bg-black/20"></span>
|
||||
@ -394,7 +375,6 @@
|
||||
}
|
||||
|
||||
const slides = Array.from(slider.querySelectorAll('[data-home-slide]'));
|
||||
const visuals = Array.from(document.querySelectorAll('[data-home-slide-visual]'));
|
||||
const dots = Array.from(slider.querySelectorAll('[data-home-slide-dot]'));
|
||||
const previousButton = slider.querySelector('[data-home-slide-prev]');
|
||||
const nextButton = slider.querySelector('[data-home-slide-next]');
|
||||
@ -416,13 +396,6 @@
|
||||
slide.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||||
});
|
||||
|
||||
visuals.forEach((visual, visualIndex) => {
|
||||
const isActive = visualIndex === activeIndex;
|
||||
|
||||
visual.classList.toggle('hidden', !isActive);
|
||||
visual.setAttribute('aria-hidden', isActive ? 'false' : 'true');
|
||||
});
|
||||
|
||||
dots.forEach((dot, dotIndex) => {
|
||||
const isActive = dotIndex === activeIndex;
|
||||
|
||||
|
||||
@ -46,19 +46,19 @@
|
||||
</head>
|
||||
<body class="min-h-screen font-sans antialiased">
|
||||
<nav class="market-nav-surface sticky top-0 z-50">
|
||||
<div class="oc-nav-wrap">
|
||||
<div class="oc-nav-main">
|
||||
<a href="{{ route('home') }}" class="oc-brand">
|
||||
<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.5">
|
||||
@if($siteLogoUrl)
|
||||
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="h-9 w-auto rounded-xl">
|
||||
<img src="{{ $siteLogoUrl }}" alt="{{ $siteName }}" class="h-9 w-auto rounded">
|
||||
@else
|
||||
<span class="brand-logo" aria-hidden="true"></span>
|
||||
@endif
|
||||
<span class="brand-text leading-none">{{ $siteName }}</span>
|
||||
</a>
|
||||
|
||||
<form action="{{ route('listings.index') }}" method="GET" class="oc-search hidden lg:flex">
|
||||
<svg class="w-5 h-5 oc-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<form action="{{ route('listings.index') }}" method="GET" class="hidden lg:flex flex-1 search-shell items-center gap-2 px-4 py-2.5">
|
||||
<svg class="w-5 h-5 text-rose-500 shrink-0" 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
|
||||
@ -66,95 +66,95 @@
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="{{ __('messages.search_placeholder') }}"
|
||||
class="oc-search-input"
|
||||
class="w-full bg-transparent text-sm text-slate-700 placeholder:text-slate-400 focus:outline-none"
|
||||
>
|
||||
<button type="submit" class="oc-search-submit">
|
||||
<button type="submit" class="text-xs font-semibold text-slate-500 hover:text-slate-700 transition">
|
||||
{{ __('messages.search') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="oc-actions">
|
||||
<details class="relative hidden md:block" data-location-widget data-cities-url-template="{{ $citiesRouteTemplate }}">
|
||||
<summary class="oc-pill list-none cursor-pointer">
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11z"/>
|
||||
<circle cx="12" cy="10" r="2.3" stroke-width="1.8" />
|
||||
</svg>
|
||||
<span data-location-label class="max-w-40 truncate">Choose location</span>
|
||||
<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="location-panel absolute right-0 top-full mt-3 bg-white border border-slate-200 shadow-xl rounded-2xl p-4 space-y-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-slate-900">Location</p>
|
||||
<button type="button" data-location-detect class="text-xs font-semibold text-slate-600 hover:text-slate-900 transition">Use my location</button>
|
||||
</div>
|
||||
<p data-location-status class="text-xs text-slate-500">Auto-select country and city from your browser location.</p>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-600">Country</label>
|
||||
<select data-location-country class="w-full">
|
||||
<option value="">Select country</option>
|
||||
@foreach($locationCountries as $country)
|
||||
<option
|
||||
value="{{ $country['id'] }}"
|
||||
data-code="{{ strtoupper($country['code'] ?? '') }}"
|
||||
data-name="{{ $country['name'] }}"
|
||||
data-default="{{ strtoupper($country['code'] ?? '') === $defaultCountryIso2 ? '1' : '0' }}"
|
||||
>
|
||||
{{ $country['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-600">City</label>
|
||||
<select data-location-city class="w-full" disabled>
|
||||
<option value="">Select country first</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" data-location-save class="w-full btn-primary px-4 py-2.5 text-sm font-semibold transition">Apply</button>
|
||||
<details class="relative hidden md:block" data-location-widget data-cities-url-template="{{ $citiesRouteTemplate }}">
|
||||
<summary class="chip-btn list-none cursor-pointer px-4 py-2.5 text-sm text-slate-700 inline-flex items-center gap-2">
|
||||
<svg class="w-4 h-4 text-slate-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21s7-6.2 7-11a7 7 0 10-14 0c0 4.8 7 11 7 11z"/>
|
||||
<circle cx="12" cy="10" r="2.3" stroke-width="1.8" />
|
||||
</svg>
|
||||
<span data-location-label class="max-w-44 truncate">Konum seç</span>
|
||||
<svg class="w-4 h-4 text-slate-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="location-panel absolute right-0 mt-2 bg-white border border-slate-200 shadow-xl rounded-2xl p-4 space-y-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-slate-900">Konum Tercihi</p>
|
||||
<button type="button" data-location-detect class="text-xs font-semibold text-rose-500 hover:text-rose-600 transition">Konumumu Bul</button>
|
||||
</div>
|
||||
</details>
|
||||
<p data-location-status class="text-xs text-slate-500">Tarayıcı konumuna göre ülke ve şehir otomatik seçilebilir.</p>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-600">Ülke</label>
|
||||
<select data-location-country class="w-full">
|
||||
<option value="">Ülke seç</option>
|
||||
@foreach($locationCountries as $country)
|
||||
<option
|
||||
value="{{ $country['id'] }}"
|
||||
data-code="{{ strtoupper($country['code'] ?? '') }}"
|
||||
data-name="{{ $country['name'] }}"
|
||||
data-default="{{ strtoupper($country['code'] ?? '') === $defaultCountryIso2 ? '1' : '0' }}"
|
||||
>
|
||||
{{ $country['name'] }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-xs font-semibold text-slate-600">Şehir</label>
|
||||
<select data-location-city class="w-full" disabled>
|
||||
<option value="">Önce ülke seç</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" data-location-save class="w-full btn-primary px-4 py-2.5 text-sm font-semibold hover:brightness-95 transition">Uygula</button>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2 md:gap-3">
|
||||
@auth
|
||||
<a href="{{ $favoritesRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Favorites">
|
||||
<a href="{{ $favoritesRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Favoriler">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M12 21l-1.45-1.32C5.4 15.03 2 12.01 2 8.31 2 5.3 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.08A6.04 6.04 0 0116.5 3C19.58 3 22 5.3 22 8.31c0 3.7-3.4 6.72-8.55 11.39L12 21z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ $inboxRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Inbox">
|
||||
<a href="{{ $inboxRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Gelen Kutusu">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16a1 1 0 011 1v10a1 1 0 01-1 1H4a1 1 0 01-1-1V7a1 1 0 011-1z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 8l9 6 9-6"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ $panelListingsRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Dashboard">
|
||||
<a href="{{ $panelListingsRoute }}" class="header-utility hidden xl:inline-flex" aria-label="Panel">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M3 12l9-9 9 9M5 10v10h14V10"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
||||
Sell
|
||||
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
||||
İlan Ver
|
||||
</a>
|
||||
<form method="POST" action="{{ $logoutRoute }}" class="hidden xl:block">
|
||||
@csrf
|
||||
<button type="submit" class="oc-text-link">{{ __('messages.logout') }}</button>
|
||||
<button type="submit" class="text-sm text-slate-500 hover:text-rose-500 transition">{{ __('messages.logout') }}</button>
|
||||
</form>
|
||||
@else
|
||||
<a href="{{ $loginRoute }}" class="oc-text-link hidden md:inline-flex">
|
||||
<a href="{{ $loginRoute }}" class="bg-rose-50 text-rose-500 px-4 md:px-5 py-2.5 rounded-full text-sm font-semibold hover:bg-rose-100 transition">
|
||||
{{ __('messages.login') }}
|
||||
</a>
|
||||
<a href="{{ $panelCreateRoute }}" class="btn-primary oc-cta">
|
||||
Sell
|
||||
<a href="{{ $panelCreateRoute }}" class="btn-primary px-4 md:px-5 py-2.5 text-sm font-semibold shadow-sm hover:brightness-95 transition">
|
||||
İlan Ver
|
||||
</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oc-mobile-tools lg:hidden">
|
||||
<form action="{{ route('listings.index') }}" method="GET" class="oc-search">
|
||||
<svg class="w-5 h-5 oc-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<div class="mt-3 space-y-2 lg:hidden">
|
||||
<form action="{{ route('listings.index') }}" method="GET" class="search-shell flex items-center gap-2 px-3 py-2.5">
|
||||
<svg class="w-4 h-4 text-rose-500 shrink-0" 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
|
||||
@ -162,32 +162,31 @@
|
||||
name="search"
|
||||
value="{{ request('search') }}"
|
||||
placeholder="{{ __('messages.search_placeholder') }}"
|
||||
class="oc-search-input"
|
||||
class="w-full bg-transparent text-sm text-slate-700 placeholder:text-slate-400 focus:outline-none"
|
||||
>
|
||||
<button type="submit" class="oc-search-submit">{{ __('messages.search') }}</button>
|
||||
<button type="submit" class="text-xs text-slate-500">{{ __('messages.search') }}</button>
|
||||
</form>
|
||||
|
||||
<div class="oc-mobile-pills">
|
||||
<span class="oc-pill" data-location-label-mobile>Choose location</span>
|
||||
<a href="{{ $panelCreateRoute }}" class="oc-pill oc-pill-strong">Sell</a>
|
||||
<div class="flex items-center gap-2 overflow-x-auto pb-1">
|
||||
<span class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-slate-700" data-location-label-mobile>Konum seç</span>
|
||||
<a href="{{ $panelCreateRoute }}" class="chip-btn whitespace-nowrap px-4 py-2 text-sm text-rose-600 font-semibold">İlan Ver</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="oc-category-row">
|
||||
<div class="oc-category-track">
|
||||
<a href="{{ route('categories.index') }}" class="oc-category-pill">
|
||||
<div class="mt-4 border-t border-slate-200 pt-3 overflow-x-auto">
|
||||
<div class="flex items-center gap-2 min-w-max pb-1">
|
||||
<a href="{{ route('categories.index') }}" class="chip-btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-700 hover:bg-slate-100 transition">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.8" d="M4 6h16M4 12h16M4 18h16"/>
|
||||
</svg>
|
||||
<span>All Categories</span>
|
||||
Tüm Kategoriler
|
||||
</a>
|
||||
@forelse($headerCategories as $headerCategory)
|
||||
<a href="{{ route('listings.index', ['category' => $headerCategory['id']]) }}" class="oc-category-link">
|
||||
<a href="{{ route('categories.show', ['category' => $headerCategory['id']]) }}" class="px-4 py-2.5 rounded-full text-sm font-medium text-slate-700 hover:bg-slate-100 transition whitespace-nowrap">
|
||||
{{ $headerCategory['name'] }}
|
||||
</a>
|
||||
@empty
|
||||
<a href="{{ route('home') }}" class="oc-category-link">{{ __('messages.home') }}</a>
|
||||
<a href="{{ route('listings.index') }}" class="oc-category-link">{{ __('messages.listings') }}</a>
|
||||
<a href="{{ route('home') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.home') }}</a>
|
||||
<a href="{{ route('listings.index') }}" class="chip-btn whitespace-nowrap px-4 py-2 hover:bg-slate-100 transition">{{ __('messages.listings') }}</a>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@ -212,22 +211,22 @@
|
||||
<p class="text-sm text-slate-500 leading-relaxed">{{ $siteDescription }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-slate-900 font-medium mb-4">Quick Links</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-slate-900 transition">Home</a></li>
|
||||
<li><a href="{{ route('categories.index') }}" class="hover:text-slate-900 transition">Categories</a></li>
|
||||
<li><a href="{{ route('listings.index') }}" class="hover:text-slate-900 transition">All Listings</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-slate-900 font-medium mb-4">Account</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-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-slate-900 font-medium mb-4">Links</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-slate-900 transition">LinkedIn</a></li>
|
||||
@ -239,10 +238,10 @@
|
||||
<li><a href="{{ $whatsappUrl }}" target="_blank" rel="noopener" class="hover:text-slate-900 transition">WhatsApp</a></li>
|
||||
@endif
|
||||
@if(!$linkedinUrl && !$instagramUrl && !$whatsappUrl)
|
||||
<li>No social links added yet.</li>
|
||||
<li>Henüz sosyal bağlantı eklenmedi.</li>
|
||||
@endif
|
||||
</ul>
|
||||
<h4 class="text-slate-900 font-medium mb-3">Languages</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-slate-900' : 'hover:text-slate-900' }} transition">{{ strtoupper($locale) }}</a>
|
||||
@ -292,7 +291,7 @@
|
||||
|
||||
const formatLocationLabel = (location) => {
|
||||
if (!location || typeof location !== 'object') {
|
||||
return 'Choose location';
|
||||
return 'Konum seç';
|
||||
}
|
||||
|
||||
const cityName = (location.cityName ?? '').toString().trim();
|
||||
@ -306,7 +305,7 @@
|
||||
return countryName;
|
||||
}
|
||||
|
||||
return 'Choose location';
|
||||
return 'Konum seç';
|
||||
};
|
||||
|
||||
const updateLabels = (location) => {
|
||||
@ -373,13 +372,13 @@
|
||||
}
|
||||
|
||||
if (normalizedCountryId === '' || template === '') {
|
||||
citySelect.innerHTML = '<option value="">Select country first</option>';
|
||||
citySelect.innerHTML = '<option value="">Önce ülke seç</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
citySelect.disabled = true;
|
||||
citySelect.innerHTML = '<option value="">Loading cities...</option>';
|
||||
citySelect.innerHTML = '<option value="">Şehir yükleniyor...</option>';
|
||||
|
||||
try {
|
||||
const primaryUrl = buildCitiesUrl(template, normalizedCountryId);
|
||||
@ -413,10 +412,10 @@
|
||||
cityOptions = await fetchCityOptions(fallbackUrl);
|
||||
}
|
||||
|
||||
citySelect.innerHTML = '<option value="">Select city</option>';
|
||||
citySelect.innerHTML = '<option value="">Şehir seç</option>';
|
||||
|
||||
if (cityOptions.length === 0) {
|
||||
citySelect.innerHTML = '<option value="">No cities found</option>';
|
||||
citySelect.innerHTML = '<option value="">Şehir bulunamadı</option>';
|
||||
citySelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
@ -440,55 +439,21 @@
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
citySelect.innerHTML = '<option value="">Could not load cities</option>';
|
||||
citySelect.innerHTML = '<option value="">Şehir yüklenemedi</option>';
|
||||
citySelect.disabled = true;
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Could not load the city list. Please try again.';
|
||||
statusText.textContent = 'Şehir listesi alınamadı. Lütfen tekrar deneyin.';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const findMatchingCityOption = (citySelect, candidates) => {
|
||||
const normalizedCandidates = candidates
|
||||
.map((candidate) => normalize(candidate))
|
||||
.filter((candidate) => candidate !== '');
|
||||
|
||||
if (normalizedCandidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = Array.from(citySelect.options).filter((option) => option.value !== '');
|
||||
|
||||
for (const candidate of normalizedCandidates) {
|
||||
const exactMatch = options.find((option) => normalize(option.dataset.name || option.textContent) === candidate);
|
||||
|
||||
if (exactMatch) {
|
||||
return exactMatch;
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of normalizedCandidates) {
|
||||
const containsMatch = options.find((option) => {
|
||||
const optionName = normalize(option.dataset.name || option.textContent);
|
||||
|
||||
return optionName.includes(candidate) || candidate.includes(optionName);
|
||||
});
|
||||
|
||||
if (containsMatch) {
|
||||
return containsMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const saveFromInputs = (root, extra = {}) => {
|
||||
const countrySelect = root.querySelector('[data-location-country]');
|
||||
const citySelect = root.querySelector('[data-location-city]');
|
||||
const details = root.closest('details');
|
||||
|
||||
if (!countrySelect || !citySelect || !countrySelect.value) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
const countryOption = countrySelect.options[countrySelect.selectedIndex];
|
||||
@ -511,8 +476,6 @@
|
||||
if (details && details.hasAttribute('open')) {
|
||||
details.removeAttribute('open');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const reverseLookup = async (latitude, longitude) => {
|
||||
@ -539,9 +502,7 @@
|
||||
return {
|
||||
countryCode: (address.country_code ?? '').toUpperCase(),
|
||||
countryName: address.country ?? '',
|
||||
cityName: address.city ?? address.town ?? address.village ?? address.municipality ?? '',
|
||||
regionName: address.state ?? address.province ?? '',
|
||||
districtName: address.state_district ?? address.county ?? '',
|
||||
cityName: address.city ?? address.town ?? address.village ?? address.municipality ?? address.state_district ?? address.state ?? '',
|
||||
};
|
||||
};
|
||||
|
||||
@ -613,26 +574,25 @@
|
||||
|
||||
countrySelect.addEventListener('change', async () => {
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Updating cities for the selected country...';
|
||||
statusText.textContent = 'Ülkeye göre şehirler güncelleniyor...';
|
||||
}
|
||||
await loadCities(root, countrySelect.value, null, null);
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Select a city and apply.';
|
||||
statusText.textContent = 'Şehir seçimini tamamlayıp uygulayabilirsiniz.';
|
||||
}
|
||||
});
|
||||
|
||||
saveButton.addEventListener('click', () => {
|
||||
const saved = saveFromInputs(root);
|
||||
|
||||
if (saved && statusText) {
|
||||
statusText.textContent = 'Location saved.';
|
||||
saveFromInputs(root);
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Konum kaydedildi.';
|
||||
}
|
||||
});
|
||||
|
||||
if (detectButton) {
|
||||
detectButton.addEventListener('click', async () => {
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Getting your location...';
|
||||
statusText.textContent = 'Konumunuz alınıyor...';
|
||||
}
|
||||
|
||||
try {
|
||||
@ -649,47 +609,23 @@
|
||||
|
||||
if (!matchedCountry) {
|
||||
if (statusText) {
|
||||
statusText.textContent = 'No matching country found. Please choose it manually.';
|
||||
statusText.textContent = 'Ülke eşleşmesi bulunamadı, lütfen manuel seçim yapın.';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
countrySelect.value = matchedCountry.value;
|
||||
await loadCities(root, matchedCountry.value, null, null);
|
||||
await loadCities(root, matchedCountry.value, null, guessed.cityName);
|
||||
saveFromInputs(root, { latitude, longitude });
|
||||
|
||||
const matchedCity = findMatchingCityOption(citySelect, [
|
||||
guessed.cityName,
|
||||
guessed.regionName,
|
||||
guessed.districtName,
|
||||
]);
|
||||
|
||||
if (matchedCity) {
|
||||
citySelect.value = matchedCity.value;
|
||||
}
|
||||
|
||||
if (!matchedCity && !citySelect.disabled && citySelect.options.length > 1) {
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Country was selected, but the city could not be matched automatically. Please choose your city.';
|
||||
}
|
||||
|
||||
const details = root.closest('details');
|
||||
if (details) {
|
||||
details.setAttribute('open', 'open');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const saved = saveFromInputs(root, { latitude, longitude });
|
||||
|
||||
if (saved && statusText) {
|
||||
statusText.textContent = 'Location selected automatically.';
|
||||
if (statusText) {
|
||||
statusText.textContent = 'Konum otomatik seçildi.';
|
||||
}
|
||||
} catch (error) {
|
||||
if (statusText) {
|
||||
statusText.textContent = error?.message === 'secure_context_required'
|
||||
? 'HTTPS is required for browser location. Open the site over a secure connection.'
|
||||
: 'Could not access location. Check your browser permissions.';
|
||||
? 'Tarayıcı konumu için HTTPS gerekli. Lütfen siteyi güvenli bağlantıdan açın.'
|
||||
: 'Konum alınamadı. Tarayıcı izinlerini kontrol edin.';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
@extends('app::layouts.app')
|
||||
|
||||
@section('title', 'Create Listing')
|
||||
@section('title', 'İlan Ver')
|
||||
|
||||
@section('simple_page', '1')
|
||||
|
||||
|
||||
@ -5,24 +5,24 @@
|
||||
|
||||
<aside class="bg-white border border-slate-200 rounded-xl overflow-hidden">
|
||||
<a href="{{ route('panel.listings.create') }}" class="block px-5 py-4 text-base {{ $activeMenu === 'create' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||
Sell
|
||||
İlan Ver
|
||||
</a>
|
||||
<a href="{{ route('panel.listings.index') }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'listings' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||
My Listings
|
||||
İlanlarım
|
||||
</a>
|
||||
<a href="{{ route('favorites.index', ['tab' => 'listings']) }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'favorites' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||
Favorites
|
||||
Favorilerim
|
||||
</a>
|
||||
<a href="{{ route('favorites.index', ['tab' => 'listings']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'listings' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||
Saved Listings
|
||||
Favori İlanlar
|
||||
</a>
|
||||
<a href="{{ route('favorites.index', ['tab' => 'searches']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'searches' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||
Saved Searches
|
||||
Favori Aramalar
|
||||
</a>
|
||||
<a href="{{ route('favorites.index', ['tab' => 'sellers']) }}" class="block px-9 py-3 border-t border-slate-100 text-sm {{ $activeFavoritesTab === 'sellers' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-600 hover:bg-slate-50' }}">
|
||||
Saved Sellers
|
||||
Favori Satıcılar
|
||||
</a>
|
||||
<a href="{{ route('panel.inbox.index') }}" class="block px-5 py-4 border-t border-slate-200 text-base {{ $activeMenu === 'inbox' ? 'bg-rose-50 text-rose-600 font-semibold' : 'text-slate-700 hover:bg-slate-50' }}">
|
||||
Inbox
|
||||
Gelen Kutusu
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-5 sm:py-8">
|
||||
<div class="max-w-[1320px] mx-auto px-4 py-8">
|
||||
<style>
|
||||
.qc-shell {
|
||||
--qc-card: #ffffff;
|
||||
@ -853,404 +853,32 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.qc-shell {
|
||||
--qc-card: #ffffff;
|
||||
--qc-border: #dbe3ee;
|
||||
--qc-text: #0f172a;
|
||||
--qc-muted: #64748b;
|
||||
--qc-primary: #111827;
|
||||
--qc-primary-soft: #f3f4f6;
|
||||
--qc-warn: #f8fafc;
|
||||
color: var(--qc-text);
|
||||
font-family: "SF Pro Text", "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.qc-hero {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.qc-hero-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.qc-eyebrow {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: .72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
padding: .42rem .7rem;
|
||||
}
|
||||
|
||||
.qc-title {
|
||||
margin-top: .5rem;
|
||||
font-size: 1.9rem;
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
font-weight: 700;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qc-subtitle {
|
||||
margin-top: .45rem;
|
||||
color: var(--qc-muted);
|
||||
font-size: .95rem;
|
||||
line-height: 1.55;
|
||||
max-width: 56rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.qc-head {
|
||||
display: grid;
|
||||
gap: .55rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qc-progress-wrap {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
gap: .9rem;
|
||||
}
|
||||
|
||||
.qc-progress {
|
||||
width: 100%;
|
||||
gap: .45rem;
|
||||
}
|
||||
|
||||
.qc-progress > span {
|
||||
height: .3rem;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.qc-progress > span.is-on {
|
||||
background: var(--qc-primary);
|
||||
}
|
||||
|
||||
.qc-step-label {
|
||||
font-size: .92rem;
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.qc-stage {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qc-card {
|
||||
border: 1px solid var(--qc-border);
|
||||
border-radius: 1rem;
|
||||
background: var(--qc-card);
|
||||
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.qc-body {
|
||||
min-height: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.qc-body > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.qc-footer {
|
||||
padding: 1rem;
|
||||
justify-content: stretch;
|
||||
flex-direction: column-reverse;
|
||||
align-items: stretch;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qc-btn,
|
||||
.qc-publish,
|
||||
.qc-muted-btn,
|
||||
.qc-upload-btn {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 3rem;
|
||||
padding: .82rem 1rem;
|
||||
font-size: .95rem;
|
||||
}
|
||||
|
||||
.qc-btn-primary,
|
||||
.qc-publish,
|
||||
.qc-upload-btn {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qc-btn-primary:hover,
|
||||
.qc-publish:hover,
|
||||
.qc-upload-btn:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qc-btn-secondary,
|
||||
.qc-muted-btn {
|
||||
background: #f8fafc;
|
||||
color: #0f172a;
|
||||
border: 1px solid var(--qc-border);
|
||||
}
|
||||
|
||||
.qc-upload-zone,
|
||||
.qc-warning,
|
||||
.qc-summary,
|
||||
.qc-info-box,
|
||||
.qc-preview-panel,
|
||||
.qc-seller-card,
|
||||
.qc-strip {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.qc-upload-zone {
|
||||
min-height: 220px;
|
||||
padding: 1.25rem 1rem;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.qc-upload-zone > * {
|
||||
max-width: 760px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.qc-upload-title,
|
||||
.qc-ai-note h3 {
|
||||
font-size: 1.35rem;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.qc-photo-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.qc-root-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.qc-root-item {
|
||||
border: 1px solid var(--qc-border);
|
||||
border-radius: .9rem;
|
||||
padding: .85rem .6rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qc-root-item.is-selected {
|
||||
background: #f8fafc;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.qc-root-icon {
|
||||
background: #f8fafc;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.qc-search input,
|
||||
.qc-input,
|
||||
.qc-select,
|
||||
.qc-textarea {
|
||||
background: #fff;
|
||||
border-color: var(--qc-border);
|
||||
border-radius: .85rem;
|
||||
padding: .85rem .95rem;
|
||||
}
|
||||
|
||||
.qc-summary {
|
||||
border-top: 0;
|
||||
margin-top: 1rem;
|
||||
padding-top: 0;
|
||||
border: 1px solid var(--qc-border);
|
||||
background: #f8fafc;
|
||||
padding: 1rem;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
}
|
||||
|
||||
.qc-strip {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.qc-dynamic-grid,
|
||||
.qc-two-col,
|
||||
.qc-preview-grid,
|
||||
.qc-seller-actions {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.qc-preview-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.qc-preview-panel,
|
||||
.qc-seller-card {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.qc-warning {
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--qc-border);
|
||||
}
|
||||
|
||||
.qc-chip,
|
||||
.qc-pill {
|
||||
border-color: var(--qc-border);
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.qc-avatar {
|
||||
background: #f3f4f6;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.qc-gallery {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.qc-gallery-item {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.qc-feature-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: .2rem;
|
||||
}
|
||||
|
||||
.qc-publish-wrap {
|
||||
display: grid;
|
||||
gap: .6rem;
|
||||
margin-top: .9rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.qc-title {
|
||||
font-size: 2.35rem;
|
||||
}
|
||||
|
||||
.qc-body,
|
||||
.qc-footer {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.qc-photo-grid,
|
||||
.qc-strip {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.qc-gallery {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.qc-hero {
|
||||
grid-template-columns: minmax(0, 1fr) 220px;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.qc-head {
|
||||
justify-items: end;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.qc-footer {
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.qc-btn,
|
||||
.qc-publish,
|
||||
.qc-muted-btn {
|
||||
width: auto;
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.qc-root-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.qc-dynamic-grid,
|
||||
.qc-two-col {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.qc-preview-grid {
|
||||
grid-template-columns: minmax(0, 1fr) 280px;
|
||||
}
|
||||
|
||||
.qc-gallery {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.qc-gallery-item {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.qc-seller-actions {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.qc-body {
|
||||
padding: 1.4rem;
|
||||
}
|
||||
|
||||
.qc-preview-grid {
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="qc-shell">
|
||||
<div class="qc-hero">
|
||||
<div class="qc-hero-copy">
|
||||
<span class="qc-eyebrow">Create listing</span>
|
||||
<h1 class="qc-title">{{ $this->currentStepTitle }}</h1>
|
||||
<p class="qc-subtitle">A clean, simple flow to publish faster.</p>
|
||||
</div>
|
||||
<div class="qc-head">
|
||||
<div class="qc-step-label">Step {{ $currentStep }} of 5</div>
|
||||
<div class="qc-progress-wrap">
|
||||
<div class="qc-progress" aria-hidden="true">
|
||||
@for ($step = 1; $step <= 5; $step++)
|
||||
<span @class(['is-on' => $step <= $currentStep])></span>
|
||||
@endfor
|
||||
</div>
|
||||
<div class="qc-head">
|
||||
<h1 class="qc-title">{{ $this->currentStepTitle }}</h1>
|
||||
<div class="qc-progress-wrap">
|
||||
<div class="qc-progress" aria-hidden="true">
|
||||
@for ($step = 1; $step <= 5; $step++)
|
||||
<span @class(['is-on' => $step <= $currentStep])></span>
|
||||
@endfor
|
||||
</div>
|
||||
<div class="qc-step-label">{{ $currentStep }}/5</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qc-stage">
|
||||
<div class="qc-card">
|
||||
@if ($currentStep === 1)
|
||||
<div class="qc-body">
|
||||
<label class="qc-upload-zone" for="quick-listing-photo-input">
|
||||
<x-heroicon-o-photo class="h-10 w-10 text-gray-700" />
|
||||
<div class="qc-upload-title">Start with photos</div>
|
||||
<div class="qc-upload-desc">Add clear images first.</div>
|
||||
<span class="qc-upload-btn">Choose Photos</span>
|
||||
<div class="qc-upload-title">Ürün fotoğraflarını yükle</div>
|
||||
<div class="qc-upload-desc">
|
||||
Yüklemeye başlamak için ürün fotoğraflarını
|
||||
<strong>bu alana sürükleyip bırakın</strong> veya
|
||||
</div>
|
||||
<span class="qc-upload-btn">Fotoğraf Seç</span>
|
||||
</label>
|
||||
|
||||
<input
|
||||
@ -1262,7 +890,10 @@
|
||||
class="hidden"
|
||||
/>
|
||||
|
||||
<p class="qc-help">1 to {{ (int) config('quick-listing.max_photo_count', 20) }} images. JPG and PNG only.</p>
|
||||
<p class="qc-help">
|
||||
<strong>İpucu:</strong> En az 1 fotoğraf, en çok {{ (int) config('quick-listing.max_photo_count', 20) }} fotoğraf yükleyebilirsin.<br>
|
||||
Desteklenen formatlar: <strong>.jpg, .jpeg ve .png</strong>
|
||||
</p>
|
||||
|
||||
@error('photos')
|
||||
<div class="qc-error">{{ $message }}</div>
|
||||
@ -1273,17 +904,17 @@
|
||||
@enderror
|
||||
|
||||
@if (count($photos) > 0)
|
||||
<h3 class="qc-photo-title">Selected photos</h3>
|
||||
<div class="qc-photo-sub">Drag to reorder</div>
|
||||
<h3 class="qc-photo-title">Seçtiğin Fotoğraflar</h3>
|
||||
<div class="qc-photo-sub">Fotoğrafları sıralamak için tut ve sürükle</div>
|
||||
|
||||
<div class="qc-photo-grid">
|
||||
@for ($index = 0; $index < (int) config('quick-listing.max_photo_count', 20); $index++)
|
||||
<div class="qc-photo-slot">
|
||||
@if (isset($photos[$index]))
|
||||
<img src="{{ $photos[$index]->temporaryUrl() }}" alt="Uploaded photo {{ $index + 1 }}">
|
||||
<img src="{{ $photos[$index]->temporaryUrl() }}" alt="Yüklenen fotoğraf {{ $index + 1 }}">
|
||||
<button type="button" class="qc-remove" wire:click="removePhoto({{ $index }})">×</button>
|
||||
@if ($index === 0)
|
||||
<div class="qc-cover">COVER</div>
|
||||
<div class="qc-cover">KAPAK</div>
|
||||
@endif
|
||||
@else
|
||||
<x-heroicon-o-photo class="h-9 w-9 text-gray-400" />
|
||||
@ -1294,8 +925,11 @@
|
||||
@else
|
||||
<div class="qc-ai-note">
|
||||
<x-heroicon-o-sparkles class="h-10 w-10 text-pink-500" />
|
||||
<h3>Add at least one photo</h3>
|
||||
<p>We can suggest a category after the first image.</p>
|
||||
<h3>Ürün fotoğraflarını yükle</h3>
|
||||
<p>
|
||||
Hızlı ilan vermek için en az 1 fotoğraf yükleyin.<br>
|
||||
<strong>Laravel AI</strong> sizin için otomatik kategori önerileri sunar.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -1307,7 +941,7 @@
|
||||
wire:click="goToCategoryStep"
|
||||
@disabled(count($photos) === 0 || $isDetecting)
|
||||
>
|
||||
Continue
|
||||
Devam Et
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@ -1316,18 +950,26 @@
|
||||
@if ($isDetecting)
|
||||
<div class="qc-warning">
|
||||
<x-heroicon-o-arrow-path class="h-5 w-5 animate-spin text-gray-700" />
|
||||
<span>Finding the best category...</span>
|
||||
<span>Fotoğraf analiz ediliyor, kategori önerisi hazırlanıyor...</span>
|
||||
</div>
|
||||
@elseif ($detectedCategoryId)
|
||||
<div class="qc-warning">
|
||||
<x-heroicon-o-sparkles class="h-5 w-5 text-pink-500" />
|
||||
<span>Suggested category: <strong>{{ $this->selectedCategoryName }}</strong></span>
|
||||
<span>
|
||||
AI kategori önerdi: <strong>{{ $this->selectedCategoryName }}</strong>
|
||||
@if ($detectedConfidence)
|
||||
(Güven: {{ number_format($detectedConfidence * 100, 0) }}%)
|
||||
@endif
|
||||
@if ($detectedReason)
|
||||
<span class="qc-warning-sub">{{ $detectedReason }}</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@else
|
||||
<div class="qc-warning">
|
||||
<x-heroicon-o-sparkles class="h-5 w-5 text-pink-500" />
|
||||
<span>
|
||||
Choose a category.
|
||||
AI ile kategori tespit edilemedi, lütfen kategori seçimi yapın.
|
||||
@if ($detectedError)
|
||||
<span class="qc-warning-sub">{{ $detectedError }}</span>
|
||||
@endif
|
||||
@ -1353,9 +995,9 @@
|
||||
@if (is_null($activeParentCategoryId))
|
||||
<div class="qc-browser-header">
|
||||
<span></span>
|
||||
<strong>Choose a category</strong>
|
||||
<strong>Ne Satıyorsun?</strong>
|
||||
<button type="button" class="qc-chip" wire:click="detectCategoryFromImage" @disabled($isDetecting || count($photos) === 0)>
|
||||
Refresh suggestion
|
||||
AI ile Tekrar Dene
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -1377,14 +1019,14 @@
|
||||
<div class="qc-browser-header">
|
||||
<button type="button" class="qc-back-btn" wire:click="backToRootCategories">
|
||||
<x-heroicon-o-arrow-left class="h-5 w-5" />
|
||||
Back
|
||||
Geri
|
||||
</button>
|
||||
<strong>{{ $this->currentParentName }}</strong>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<div class="qc-search">
|
||||
<input type="text" placeholder="Search categories" wire:model.live.debounce.300ms="categorySearch">
|
||||
<input type="text" placeholder="Kategori Ara" wire:model.live.debounce.300ms="categorySearch">
|
||||
</div>
|
||||
|
||||
<div class="qc-list">
|
||||
@ -1414,7 +1056,7 @@
|
||||
</div>
|
||||
@empty
|
||||
<div class="qc-row">
|
||||
<span class="qc-row-main">No categories found.</span>
|
||||
<span class="qc-row-main">Aramaya uygun kategori bulunamadı.</span>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
@ -1425,18 +1067,18 @@
|
||||
@endif
|
||||
|
||||
@if ($this->selectedCategoryName)
|
||||
<div class="qc-selection">Selected: <strong>{{ $this->selectedCategoryName }}</strong></div>
|
||||
<div class="qc-selection">Seçilen kategori: <strong>{{ $this->selectedCategoryName }}</strong></div>
|
||||
@endif
|
||||
|
||||
<div class="qc-footer">
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(1)">Back</button>
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(1)">Geri</button>
|
||||
<button
|
||||
type="button"
|
||||
class="qc-btn qc-btn-primary"
|
||||
wire:click="goToDetailsStep"
|
||||
@disabled(! $selectedCategoryId)
|
||||
>
|
||||
Continue
|
||||
Devam Et
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@ -1446,10 +1088,10 @@
|
||||
<div class="qc-strip">
|
||||
@foreach (array_slice($photos, 0, 7) as $index => $photo)
|
||||
<div class="qc-photo-slot">
|
||||
<img src="{{ $photo->temporaryUrl() }}" alt="Selected photo {{ $index + 1 }}">
|
||||
<img src="{{ $photo->temporaryUrl() }}" alt="Seçilen fotoğraf {{ $index + 1 }}">
|
||||
<button type="button" class="qc-remove" wire:click="removePhoto({{ $index }})">×</button>
|
||||
@if ($index === 0)
|
||||
<div class="qc-cover">COVER</div>
|
||||
<div class="qc-cover">KAPAK</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
@ -1457,45 +1099,45 @@
|
||||
|
||||
<div class="qc-summary">
|
||||
<div>
|
||||
<h4>Category</h4>
|
||||
<h4>Seçilen Kategori</h4>
|
||||
<p>{{ $this->selectedCategoryPath ?: '-' }}</p>
|
||||
</div>
|
||||
<button type="button" class="qc-link-btn" wire:click="goToStep(2)">Change</button>
|
||||
<button type="button" class="qc-link-btn" wire:click="goToStep(2)">Değiştir</button>
|
||||
</div>
|
||||
|
||||
<div class="qc-form-grid">
|
||||
<div class="qc-field">
|
||||
<label for="quick-title">Listing Title *</label>
|
||||
<input id="quick-title" type="text" class="qc-input" placeholder="Enter a title" wire:model.live.debounce.300ms="listingTitle" maxlength="70">
|
||||
<p class="qc-hint">Keep it short and clear.</p>
|
||||
<label for="quick-title">İlan Başlığı *</label>
|
||||
<input id="quick-title" type="text" class="qc-input" placeholder="Başlık girin" wire:model.live.debounce.300ms="listingTitle" maxlength="70">
|
||||
<p class="qc-hint">Ürünün temel özelliklerinden bahset (ör. marka, model, yaş, tip)</p>
|
||||
<div class="qc-counter">{{ $this->titleCharacters }}/70</div>
|
||||
@error('listingTitle')<div class="qc-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="qc-field">
|
||||
<label for="quick-price">Price *</label>
|
||||
<label for="quick-price">Fiyat *</label>
|
||||
<div class="qc-input-row">
|
||||
<input id="quick-price" type="number" step="0.01" class="qc-input" placeholder="Enter a price" wire:model.live.debounce.300ms="price">
|
||||
<input id="quick-price" type="number" step="0.01" class="qc-input" placeholder="Fiyat giriniz" wire:model.live.debounce.300ms="price">
|
||||
<span class="qc-input-suffix">{{ \Modules\Listing\Support\ListingPanelHelper::defaultCurrency() }}</span>
|
||||
</div>
|
||||
<p class="qc-hint">Use the final asking price.</p>
|
||||
<p class="qc-hint">Lütfen unutma; doğru fiyat daha hızlı satmanıza yardımcı olacaktır</p>
|
||||
@error('price')<div class="qc-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="qc-field">
|
||||
<label for="quick-description">Description *</label>
|
||||
<textarea id="quick-description" class="qc-textarea" placeholder="Write a description" wire:model.live.debounce.300ms="description" maxlength="1450"></textarea>
|
||||
<p class="qc-hint">Condition, key details, and anything important.</p>
|
||||
<label for="quick-description">Açıklama *</label>
|
||||
<textarea id="quick-description" class="qc-textarea" placeholder="Açıklama girin" wire:model.live.debounce.300ms="description" maxlength="1450"></textarea>
|
||||
<p class="qc-hint">Durum, özellik ve satma nedeni gibi bilgileri ekle</p>
|
||||
<div class="qc-counter">{{ $this->descriptionCharacters }}/1450</div>
|
||||
@error('description')<div class="qc-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<div class="qc-field">
|
||||
<label>Location *</label>
|
||||
<label>Konum *</label>
|
||||
<div class="qc-two-col">
|
||||
<div>
|
||||
<select class="qc-select" wire:model.live="selectedCountryId">
|
||||
<option value="">Select a country</option>
|
||||
<option value="">Ülke seçin</option>
|
||||
@foreach ($countries as $country)
|
||||
<option value="{{ $country['id'] }}">{{ $country['name'] }}</option>
|
||||
@endforeach
|
||||
@ -1504,7 +1146,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<select class="qc-select" wire:model.live="selectedCityId" @disabled(! $selectedCountryId)>
|
||||
<option value="">Select a city</option>
|
||||
<option value="">Şehir seçin</option>
|
||||
@foreach ($this->availableCities as $city)
|
||||
<option value="{{ $city['id'] }}">{{ $city['name'] }}</option>
|
||||
@endforeach
|
||||
@ -1517,8 +1159,8 @@
|
||||
</div>
|
||||
|
||||
<div class="qc-footer">
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(2)">Back</button>
|
||||
<button type="button" class="qc-btn qc-btn-primary" wire:click="goToFeaturesStep">Continue</button>
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(2)">Geri</button>
|
||||
<button type="button" class="qc-btn qc-btn-primary" wire:click="goToFeaturesStep">Devam Et</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@ -1526,15 +1168,15 @@
|
||||
<div class="qc-body">
|
||||
<div class="qc-summary" style="margin-top: 0; border-top: 0; padding-top: 0;">
|
||||
<div>
|
||||
<h4>Category</h4>
|
||||
<h4>Seçilen Kategori</h4>
|
||||
<p>{{ $this->selectedCategoryPath ?: '-' }}</p>
|
||||
</div>
|
||||
<button type="button" class="qc-link-btn" wire:click="goToStep(2)">Change</button>
|
||||
<button type="button" class="qc-link-btn" wire:click="goToStep(2)">Değiştir</button>
|
||||
</div>
|
||||
|
||||
@if ($listingCustomFields === [])
|
||||
<div class="qc-info-box">
|
||||
No extra details needed for this category.
|
||||
Bu kategori için ek ilan özelliği tanımlı değil. Devam ederek önizleme adımına geçebilirsin.
|
||||
</div>
|
||||
@else
|
||||
<div class="qc-dynamic-grid">
|
||||
@ -1570,7 +1212,7 @@
|
||||
>
|
||||
@elseif ($field['type'] === 'select')
|
||||
<select class="qc-select" wire:model.live="customFieldValues.{{ $field['name'] }}">
|
||||
<option value="">Select an option</option>
|
||||
<option value="">Seçiniz</option>
|
||||
@foreach ($field['options'] as $option)
|
||||
<option value="{{ $option }}">{{ $option }}</option>
|
||||
@endforeach
|
||||
@ -1578,7 +1220,7 @@
|
||||
@elseif ($field['type'] === 'boolean')
|
||||
<label class="qc-toggle-line">
|
||||
<input type="checkbox" wire:model.live="customFieldValues.{{ $field['name'] }}">
|
||||
<span>Yes</span>
|
||||
<span>Evet</span>
|
||||
</label>
|
||||
@elseif ($field['type'] === 'date')
|
||||
<input type="date" class="qc-input" wire:model.live="customFieldValues.{{ $field['name'] }}">
|
||||
@ -1598,21 +1240,21 @@
|
||||
</div>
|
||||
|
||||
<div class="qc-footer">
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(3)">Back</button>
|
||||
<button type="button" class="qc-btn qc-btn-primary" wire:click="goToPreviewStep">Continue</button>
|
||||
<button type="button" class="qc-btn qc-btn-secondary" wire:click="goToStep(3)">Geri</button>
|
||||
<button type="button" class="qc-btn qc-btn-primary" wire:click="goToPreviewStep">Devam Et</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($currentStep === 5)
|
||||
<div class="qc-body">
|
||||
<div class="qc-preview-breadcrumb">Home › {{ $this->selectedCategoryPath }}</div>
|
||||
<div class="qc-preview-breadcrumb">Anasayfa › {{ $this->selectedCategoryPath }}</div>
|
||||
|
||||
<div class="qc-preview-grid">
|
||||
<div class="qc-preview-panel">
|
||||
<div class="qc-gallery">
|
||||
@foreach (array_slice($photos, 0, 3) as $photo)
|
||||
<div class="qc-gallery-item">
|
||||
<img src="{{ $photo->temporaryUrl() }}" alt="Preview photo">
|
||||
<img src="{{ $photo->temporaryUrl() }}" alt="Önizleme fotoğrafı">
|
||||
</div>
|
||||
@endforeach
|
||||
@for ($empty = count(array_slice($photos, 0, 3)); $empty < 3; $empty++)
|
||||
@ -1638,7 +1280,7 @@
|
||||
</div>
|
||||
|
||||
<div class="qc-preview-features">
|
||||
<h5>Details</h5>
|
||||
<h5>İlan Özellikleri</h5>
|
||||
@if ($this->previewCustomFields !== [])
|
||||
@foreach ($this->previewCustomFields as $field)
|
||||
<div class="qc-feature-row">
|
||||
@ -1648,8 +1290,8 @@
|
||||
@endforeach
|
||||
@else
|
||||
<div class="qc-feature-row">
|
||||
<div class="qc-feature-label">Details</div>
|
||||
<div class="qc-feature-value">No extra details added</div>
|
||||
<div class="qc-feature-label">Ek özellik</div>
|
||||
<div class="qc-feature-value">Bu kategori için seçilmedi</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@ -1666,8 +1308,8 @@
|
||||
</div>
|
||||
|
||||
<div class="qc-seller-actions">
|
||||
<div class="qc-pill">Map</div>
|
||||
<div class="qc-pill">Profile</div>
|
||||
<div class="qc-pill">Harita</div>
|
||||
<div class="qc-pill">Satıcı Profili</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1678,15 +1320,14 @@
|
||||
wire:click="publishListing"
|
||||
@disabled($isPublishing)
|
||||
>
|
||||
{{ $isPublishing ? 'Publishing...' : 'Publish Listing' }}
|
||||
{{ $isPublishing ? 'Yayınlanıyor...' : 'İlanı Şimdi Yayınla' }}
|
||||
</button>
|
||||
<button type="button" class="qc-muted-btn" wire:click="goToStep(4)">Back</button>
|
||||
<button type="button" class="qc-muted-btn" wire:click="goToStep(4)">Geri Dön</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@ Route::get('/dashboard', fn () => auth()->check()
|
||||
Route::middleware('auth')->prefix('panel')->name('panel.')->group(function () {
|
||||
Route::get('/', [PanelController::class, 'index'])->name('index');
|
||||
Route::get('/ilanlarim', [PanelController::class, 'listings'])->name('listings.index');
|
||||
Route::get('/create-listing', [PanelController::class, 'create'])->name('listings.create');
|
||||
Route::get('/ilan-ver', [PanelController::class, 'create'])->name('listings.create');
|
||||
Route::post('/ilanlarim/{listing}/kaldir', [PanelController::class, 'destroyListing'])->name('listings.destroy');
|
||||
Route::post('/ilanlarim/{listing}/satildi', [PanelController::class, 'markListingAsSold'])->name('listings.mark-sold');
|
||||
Route::post('/ilanlarim/{listing}/yeniden-yayinla', [PanelController::class, 'republishListing'])->name('listings.republish');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user