This commit is contained in:
31
resources/views/admin/auth/login.blade.php
Normal file
31
resources/views/admin/auth/login.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Вход в админку</h2>
|
||||
<p>Доступ только для администраторов.</p>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form pc-auth-form" method="post" action="{{ route('admin.login.attempt') }}">
|
||||
@csrf
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Пароль
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<label>
|
||||
Капча: решите пример {{ $captchaQuestion }}
|
||||
<input type="text" name="captcha" inputmode="numeric" autocomplete="off" required>
|
||||
</label>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="remember" value="1" @checked(old('remember'))>
|
||||
<span>Запомнить меня</span>
|
||||
</label>
|
||||
<button class="pc-btn primary" type="submit">Войти</button>
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
21
resources/views/admin/categories/_form.blade.php
Normal file
21
resources/views/admin/categories/_form.blade.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<label>
|
||||
Название
|
||||
<input type="text" name="name" value="{{ old('name', $category->name ?? '') }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Slug
|
||||
<input type="text" name="slug" value="{{ old('slug', $category->slug ?? '') }}" placeholder="Можно оставить пустым">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Описание
|
||||
<textarea name="description">{{ old('description', $category->description ?? '') }}</textarea>
|
||||
</label>
|
||||
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $category->is_active ?? true))>
|
||||
<span>Категория активна</span>
|
||||
</label>
|
||||
|
||||
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>
|
||||
22
resources/views/admin/categories/create.blade.php
Normal file
22
resources/views/admin/categories/create.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Категории', 'url' => route('admin.categories.index')],
|
||||
['label' => 'Новая категория', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Новая категория</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.categories.store') }}">
|
||||
@csrf
|
||||
@include('admin.categories._form', ['submitLabel' => 'Создать'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
23
resources/views/admin/categories/edit.blade.php
Normal file
23
resources/views/admin/categories/edit.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Категории', 'url' => route('admin.categories.index')],
|
||||
['label' => 'Редактирование', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Редактирование категории</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.categories.update', $category) }}">
|
||||
@csrf
|
||||
@method('put')
|
||||
@include('admin.categories._form', ['category' => $category, 'submitLabel' => 'Сохранить'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
45
resources/views/admin/categories/index.blade.php
Normal file
45
resources/views/admin/categories/index.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Категории', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>Категории</h2>
|
||||
</div>
|
||||
<a class="pc-btn primary" href="{{ route('admin.categories.create') }}">Добавить категорию</a>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
@if ($categories->isEmpty())
|
||||
<p>Категории пока не созданы.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($categories as $category)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $category->name }} ({{ $category->slug }})</span>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn ghost" href="{{ route('admin.categories.edit', $category) }}">Редактировать</a>
|
||||
<form method="post" action="{{ route('admin.categories.destroy', $category) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $categories->links() }}
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
205
resources/views/admin/chats/index.blade.php
Normal file
205
resources/views/admin/chats/index.blade.php
Normal file
@@ -0,0 +1,205 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'Чаты с клиентами')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Чаты', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Чаты с клиентами</h2>
|
||||
<p>Отвечайте клиентам прямо из админки.</p>
|
||||
</div>
|
||||
|
||||
@if ($conversations->isEmpty())
|
||||
<div class="pc-card">
|
||||
<p>Пока нет сообщений от клиентов.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="pc-admin-chat-layout">
|
||||
<aside class="pc-card pc-admin-chat-list">
|
||||
@foreach ($conversations as $conversation)
|
||||
@php
|
||||
$isActive = $selectedConversation && $selectedConversation->id === $conversation->id;
|
||||
$subtitle = $conversation->user?->email ?: ('Токен: ' . \Illuminate\Support\Str::limit($conversation->visitor_token, 14, '...'));
|
||||
$preview = $conversation->latestMessage?->body ? \Illuminate\Support\Str::limit($conversation->latestMessage->body, 80) : 'Нет сообщений';
|
||||
@endphp
|
||||
<a class="pc-admin-chat-item {{ $isActive ? 'is-active' : '' }}" href="{{ route('admin.chats.index', ['conversation' => $conversation->id]) }}">
|
||||
<div class="pc-admin-chat-item-top">
|
||||
<strong>{{ $conversation->display_name }}</strong>
|
||||
@if ($conversation->unread_count > 0)
|
||||
<span class="pc-admin-chat-badge">{{ $conversation->unread_count }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<span class="pc-muted">{{ $subtitle }}</span>
|
||||
@if ($conversation->status === \App\Models\ChatConversation::STATUS_CLOSED)
|
||||
<span class="pc-muted">Чат закрыт</span>
|
||||
@endif
|
||||
<span class="pc-muted">{{ $preview }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</aside>
|
||||
|
||||
<div class="pc-card pc-admin-chat-panel">
|
||||
@if ($selectedConversation)
|
||||
@php
|
||||
$isClosed = $selectedConversation->status === \App\Models\ChatConversation::STATUS_CLOSED;
|
||||
@endphp
|
||||
<div
|
||||
id="pc-admin-chat"
|
||||
data-fetch-url="{{ route('admin.chats.messages', $selectedConversation) }}"
|
||||
data-send-url="{{ route('admin.chats.messages.store', $selectedConversation) }}"
|
||||
data-csrf="{{ csrf_token() }}"
|
||||
>
|
||||
<div class="pc-admin-chat-head">
|
||||
<div>
|
||||
<h3>{{ $selectedConversation->display_name }}</h3>
|
||||
<p class="pc-muted">{{ $selectedConversation->user?->email ?: 'Гость' }}</p>
|
||||
<p class="pc-muted">{{ $isClosed ? 'Чат закрыт' : 'Чат открыт' }}</p>
|
||||
</div>
|
||||
<div class="pc-admin-chat-actions">
|
||||
<form method="post" action="{{ route('admin.chats.status', $selectedConversation) }}">
|
||||
@csrf
|
||||
@method('patch')
|
||||
<input type="hidden" name="status" value="{{ $isClosed ? \App\Models\ChatConversation::STATUS_OPEN : \App\Models\ChatConversation::STATUS_CLOSED }}">
|
||||
<button class="pc-btn ghost" type="submit">{{ $isClosed ? 'Открыть чат' : 'Закрыть чат' }}</button>
|
||||
</form>
|
||||
<form method="post" action="{{ route('admin.chats.destroy', $selectedConversation) }}" onsubmit="return confirm('Удалить чат и все сообщения?')">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить чат</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-admin-chat-messages" id="pc-admin-chat-messages"></div>
|
||||
|
||||
<form class="pc-admin-chat-form" id="pc-admin-chat-form">
|
||||
<textarea name="message" rows="3" maxlength="2000" placeholder="{{ $isClosed ? 'Чат закрыт. Сначала откройте его.' : 'Введите ответ клиенту...' }}" required @disabled($isClosed)></textarea>
|
||||
<button class="pc-btn primary" type="submit" @disabled($isClosed)>{{ $isClosed ? 'Чат закрыт' : 'Отправить' }}</button>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
<p>Выберите чат слева.</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $conversations->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
|
||||
@if ($selectedConversation)
|
||||
@push('scripts')
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.getElementById('pc-admin-chat');
|
||||
const list = document.getElementById('pc-admin-chat-messages');
|
||||
const form = document.getElementById('pc-admin-chat-form');
|
||||
if (!root || !list || !form) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchUrl = root.dataset.fetchUrl;
|
||||
const sendUrl = root.dataset.sendUrl;
|
||||
const csrf = root.dataset.csrf;
|
||||
const initialMessages = @json($initialMessages);
|
||||
|
||||
const renderMessages = (messages) => {
|
||||
if (!Array.isArray(messages)) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = '';
|
||||
messages.forEach((message) => {
|
||||
const item = document.createElement('div');
|
||||
item.className = `pc-admin-chat-message ${message.sender === 'admin' ? 'is-admin' : 'is-customer'}`;
|
||||
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = 'pc-admin-chat-bubble';
|
||||
bubble.textContent = message.body || '';
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.className = 'pc-muted';
|
||||
meta.textContent = message.time || '';
|
||||
|
||||
item.appendChild(bubble);
|
||||
item.appendChild(meta);
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
list.scrollTop = list.scrollHeight;
|
||||
};
|
||||
|
||||
const fetchMessages = async () => {
|
||||
try {
|
||||
const response = await fetch(fetchUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
renderMessages(payload.messages || []);
|
||||
} catch (error) {
|
||||
// Ignore temporary connection issues and continue polling.
|
||||
}
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const textarea = form.querySelector('textarea[name="message"]');
|
||||
const button = form.querySelector('button[type="submit"]');
|
||||
if (!textarea || !button) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = textarea.value.trim();
|
||||
if (message === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(sendUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': csrf,
|
||||
},
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.value = '';
|
||||
await fetchMessages();
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
renderMessages(initialMessages);
|
||||
fetchMessages();
|
||||
window.setInterval(fetchMessages, 4000);
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@endif
|
||||
63
resources/views/admin/dashboard.blade.php
Normal file
63
resources/views/admin/dashboard.blade.php
Normal file
@@ -0,0 +1,63 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Панель администратора</h2>
|
||||
<p>Управление товарами, категориями, заказами, чатами и слайдерами.</p>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-4">
|
||||
<div class="pc-card">
|
||||
<h3>Категории</h3>
|
||||
<p>{{ $stats['categories'] }}</p>
|
||||
</div>
|
||||
<div class="pc-card">
|
||||
<h3>Товары</h3>
|
||||
<p>{{ $stats['products'] }}</p>
|
||||
</div>
|
||||
<div class="pc-card">
|
||||
<h3>Заказы</h3>
|
||||
<p>{{ $stats['orders'] }}</p>
|
||||
</div>
|
||||
<div class="pc-card">
|
||||
<h3>Выручка</h3>
|
||||
<p>{{ number_format($stats['revenue'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn primary" href="{{ route('admin.products.index') }}">Товары</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.categories.index') }}">Категории</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.orders.index') }}">Заказы</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.home-slides.index') }}">Слайдеры</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.chats.index') }}">Чаты</a>
|
||||
<form method="post" action="{{ route('admin.logout') }}">
|
||||
@csrf
|
||||
<button class="pc-btn ghost" type="submit">Выйти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Последние заказы</h3>
|
||||
@if ($recentOrders->isEmpty())
|
||||
<p>Пока нет заказов.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($recentOrders as $order)
|
||||
<a class="pc-account-order" href="{{ route('admin.orders.show', $order) }}">
|
||||
<span>Заказ #{{ $order->id }} ({{ $order->status }})</span>
|
||||
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
71
resources/views/admin/home-slides/_form.blade.php
Normal file
71
resources/views/admin/home-slides/_form.blade.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<label>
|
||||
Блок на главной
|
||||
<select name="zone" required>
|
||||
@foreach ($zoneLabels as $zoneKey => $zoneLabel)
|
||||
<option value="{{ $zoneKey }}" @selected(old('zone', $slide->zone ?? 'left') === $zoneKey)>{{ $zoneLabel }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Заголовок
|
||||
<input type="text" name="title" value="{{ old('title', $slide->title ?? '') }}" maxlength="160" placeholder="Например: Скидки на видеокарты">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Подзаголовок
|
||||
<textarea name="subtitle" rows="3" maxlength="1500" placeholder="Короткий текст на баннере">{{ old('subtitle', $slide->subtitle ?? '') }}</textarea>
|
||||
</label>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<label>
|
||||
Текст кнопки
|
||||
<input type="text" name="button_text" value="{{ old('button_text', $slide->button_text ?? '') }}" maxlength="60" placeholder="Подробнее">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Ссылка кнопки
|
||||
<input type="text" name="button_url" value="{{ old('button_url', $slide->button_url ?? '') }}" maxlength="255" placeholder="/catalog или https://...">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-3">
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="show_title" value="1" @checked(old('show_title', $slide->show_title ?? true))>
|
||||
<span>Показывать заголовок</span>
|
||||
</label>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="show_subtitle" value="1" @checked(old('show_subtitle', $slide->show_subtitle ?? true))>
|
||||
<span>Показывать описание</span>
|
||||
</label>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="show_button" value="1" @checked(old('show_button', $slide->show_button ?? true))>
|
||||
<span>Показывать кнопку</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<label>
|
||||
Порядок
|
||||
<input type="number" min="0" max="9999" name="sort_order" value="{{ old('sort_order', $slide->sort_order ?? 100) }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Изображение (jpg, png, webp)
|
||||
<input class="pc-file-input" type="file" name="image" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp" @required(empty($slide?->id))>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (!empty($slide?->image_url))
|
||||
<div class="pc-admin-slide-current">
|
||||
<div class="pc-admin-slide-current-preview" style="background-image: url('{{ $slide->image_url }}')"></div>
|
||||
<span class="pc-muted">Текущее изображение. Загрузите новое, если хотите заменить.</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $slide->is_active ?? true))>
|
||||
<span>Слайд активен</span>
|
||||
</label>
|
||||
|
||||
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>
|
||||
22
resources/views/admin/home-slides/create.blade.php
Normal file
22
resources/views/admin/home-slides/create.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.index')],
|
||||
['label' => 'Новый слайд', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Новый слайд</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.home-slides.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@include('admin.home-slides._form', ['submitLabel' => 'Создать'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
23
resources/views/admin/home-slides/edit.blade.php
Normal file
23
resources/views/admin/home-slides/edit.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Слайдеры главной', 'url' => route('admin.home-slides.index')],
|
||||
['label' => 'Редактирование', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Редактирование слайда</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.home-slides.update', $slide) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('put')
|
||||
@include('admin.home-slides._form', ['slide' => $slide, 'submitLabel' => 'Сохранить'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
57
resources/views/admin/home-slides/index.blade.php
Normal file
57
resources/views/admin/home-slides/index.blade.php
Normal file
@@ -0,0 +1,57 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Слайдеры главной', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>Слайдеры главной страницы</h2>
|
||||
<p>Загружайте баннеры для блока 2/3 и 1/3 на первом экране.</p>
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn primary" href="{{ route('admin.home-slides.create', ['zone' => 'left']) }}">Добавить в 2/3</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.home-slides.create', ['zone' => 'right']) }}">Добавить в 1/3</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
@foreach ($zoneLabels as $zone => $zoneLabel)
|
||||
@php
|
||||
$zoneSlides = $slides->get($zone, collect());
|
||||
@endphp
|
||||
<div class="pc-card">
|
||||
<h3>{{ $zoneLabel }}</h3>
|
||||
@if ($zoneSlides->isEmpty())
|
||||
<p>Слайды пока не добавлены.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($zoneSlides as $slide)
|
||||
<div class="pc-admin-slide-item">
|
||||
<div class="pc-admin-slide-preview" style="background-image: url('{{ $slide->image_url }}')"></div>
|
||||
<div class="pc-admin-slide-main">
|
||||
<strong>{{ $slide->title ?: 'Без заголовка' }}</strong>
|
||||
<span class="pc-muted">Порядок: {{ $slide->sort_order }} · {{ $slide->is_active ? 'Активен' : 'Скрыт' }}</span>
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn ghost" href="{{ route('admin.home-slides.edit', $slide) }}">Редактировать</a>
|
||||
<form method="post" action="{{ route('admin.home-slides.destroy', $slide) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
35
resources/views/admin/orders/index.blade.php
Normal file
35
resources/views/admin/orders/index.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Заказы', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Заказы</h2>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
@if ($orders->isEmpty())
|
||||
<p>Заказов пока нет.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($orders as $order)
|
||||
<a class="pc-account-order" href="{{ route('admin.orders.show', $order) }}">
|
||||
<span>Заказ #{{ $order->id }} ({{ $order->status }})</span>
|
||||
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $orders->links() }}
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
68
resources/views/admin/orders/show.blade.php
Normal file
68
resources/views/admin/orders/show.blade.php
Normal file
@@ -0,0 +1,68 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Заказы', 'url' => route('admin.orders.index')],
|
||||
['label' => 'Заказ #' . $order->id, 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Заказ #{{ $order->id }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<div class="pc-card">
|
||||
<h3>Статус</h3>
|
||||
<form class="pc-form" method="post" action="{{ route('admin.orders.update', $order) }}">
|
||||
@csrf
|
||||
@method('put')
|
||||
<label>
|
||||
Текущий статус
|
||||
<select name="status">
|
||||
@foreach (['new' => 'Новый', 'processing' => 'В обработке', 'paid' => 'Оплачен', 'shipped' => 'Отправлен', 'completed' => 'Завершен', 'cancelled' => 'Отменен'] as $value => $label)
|
||||
<option value="{{ $value }}" @selected($order->status === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<button class="pc-btn primary" type="submit">Обновить статус</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Покупатель</h3>
|
||||
<p><strong>{{ $order->customer_name }}</strong></p>
|
||||
<p>{{ $order->email }}</p>
|
||||
<p>Оплата: {{ $order->payment_method_label }}</p>
|
||||
@if ($order->phone)
|
||||
<p>{{ $order->phone }}</p>
|
||||
@endif
|
||||
@if ($order->address)
|
||||
<p>{{ $order->address }}</p>
|
||||
@endif
|
||||
@if ($order->comment)
|
||||
<p>Комментарий: {{ $order->comment }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Состав заказа</h3>
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($order->items as $item)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $item->name }} × {{ $item->quantity }}</span>
|
||||
<strong>{{ number_format($item->subtotal, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Итого</span>
|
||||
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
219
resources/views/admin/products/_form.blade.php
Normal file
219
resources/views/admin/products/_form.blade.php
Normal file
@@ -0,0 +1,219 @@
|
||||
@php
|
||||
$currentProduct = $product ?? null;
|
||||
$selectedCategoryId = (int) old('category_id', $currentProduct->category_id ?? 0);
|
||||
$specValues = old('specs', $currentProduct->specs ?? []);
|
||||
$selectedGalleryPathsForRemoval = collect(old('remove_gallery_paths', []))
|
||||
->filter(fn ($path) => is_string($path) && trim($path) !== '')
|
||||
->map(fn (string $path) => trim($path))
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
$galleryItems = collect((array) ($currentProduct->gallery_paths ?? []))
|
||||
->filter(fn ($path) => is_string($path) && trim($path) !== '')
|
||||
->map(fn (string $path) => trim($path))
|
||||
->unique()
|
||||
->filter(fn (string $path) => $path !== ($currentProduct->image_path ?? null))
|
||||
->map(function (string $path) {
|
||||
$url = $path;
|
||||
|
||||
if (!\Illuminate\Support\Str::startsWith($path, ['http://', 'https://', '/'])) {
|
||||
$url = \Illuminate\Support\Str::startsWith($path, 'uploads/')
|
||||
? asset($path)
|
||||
: '/storage/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'url' => $url,
|
||||
];
|
||||
})
|
||||
->values()
|
||||
->all();
|
||||
@endphp
|
||||
|
||||
<label>
|
||||
Категория
|
||||
<select id="product-category-select" name="category_id" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
@foreach ($categories as $categoryOption)
|
||||
<option value="{{ $categoryOption->id }}" @selected((int) old('category_id', $currentProduct->category_id ?? 0) === $categoryOption->id)>
|
||||
{{ $categoryOption->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Название
|
||||
<input type="text" name="name" value="{{ old('name', $currentProduct->name ?? '') }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Slug
|
||||
<input type="text" name="slug" value="{{ old('slug', $currentProduct->slug ?? '') }}" placeholder="Можно оставить пустым">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Артикул
|
||||
<input type="text" name="sku" value="{{ old('sku', $currentProduct->sku ?? '') }}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Изображение товара (jpg, png, webp)
|
||||
<input class="pc-file-input" type="file" name="image" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Дополнительные изображения (до 12 файлов)
|
||||
<input
|
||||
class="pc-file-input"
|
||||
type="file"
|
||||
name="gallery_images[]"
|
||||
accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<label>
|
||||
Цена
|
||||
<input type="number" step="0.01" min="0" name="price" value="{{ old('price', $currentProduct->price ?? '') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Старая цена
|
||||
<input type="number" step="0.01" min="0" name="old_price" value="{{ old('old_price', $currentProduct->old_price ?? '') }}">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
Остаток
|
||||
<input type="number" min="0" name="stock" value="{{ old('stock', $currentProduct->stock ?? 0) }}" required>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Короткое описание
|
||||
<input type="text" name="short_description" value="{{ old('short_description', $currentProduct->short_description ?? '') }}">
|
||||
</label>
|
||||
|
||||
<label>
|
||||
Описание
|
||||
<textarea name="description">{{ old('description', $currentProduct->description ?? '') }}</textarea>
|
||||
</label>
|
||||
|
||||
@if (!empty($currentProduct?->image_url))
|
||||
<div class="pc-admin-product-image-current">
|
||||
<img class="pc-admin-product-image-preview" src="{{ $currentProduct->image_url }}" alt="{{ $currentProduct->name }}" loading="lazy">
|
||||
<span class="pc-muted">Текущее изображение. Загрузите новое, чтобы заменить.</span>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="remove_image" value="1" @checked(old('remove_image'))>
|
||||
<span>Удалить текущее изображение</span>
|
||||
</label>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($galleryItems !== [])
|
||||
<div class="pc-admin-product-gallery-current">
|
||||
<span class="pc-muted">Отметьте изображения, которые нужно удалить. При новой загрузке текущая галерея будет заменена.</span>
|
||||
<div class="pc-admin-product-gallery-grid">
|
||||
@foreach ($galleryItems as $galleryItem)
|
||||
<label class="pc-admin-product-gallery-item">
|
||||
<img class="pc-admin-product-gallery-preview" src="{{ $galleryItem['url'] }}" alt="Дополнительное изображение товара" loading="lazy">
|
||||
<span class="pc-checkbox">
|
||||
<input type="checkbox" name="remove_gallery_paths[]" value="{{ $galleryItem['path'] }}" @checked(in_array($galleryItem['path'], $selectedGalleryPathsForRemoval, true))>
|
||||
<span>Удалить</span>
|
||||
</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pc-spec-fields-wrap">
|
||||
<h3 class="pc-spec-fields-title">Характеристики категории</h3>
|
||||
<div id="pc-spec-fields" class="pc-grid pc-grid-2"></div>
|
||||
</div>
|
||||
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $currentProduct->is_active ?? true))>
|
||||
<span>Товар активен</span>
|
||||
</label>
|
||||
|
||||
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const categorySelect = document.getElementById('product-category-select');
|
||||
const specsContainer = document.getElementById('pc-spec-fields');
|
||||
if (!categorySelect || !specsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const specsByCategory = @json($categorySpecsById ?? []);
|
||||
const specValues = @json($specValues);
|
||||
|
||||
const renderSpecFields = () => {
|
||||
const selectedCategoryId = categorySelect.value;
|
||||
const fields = specsByCategory[selectedCategoryId] || [];
|
||||
specsContainer.innerHTML = '';
|
||||
|
||||
if (fields.length === 0) {
|
||||
const muted = document.createElement('p');
|
||||
muted.className = 'pc-muted';
|
||||
muted.textContent = 'Для выбранной категории нет дополнительных полей.';
|
||||
specsContainer.appendChild(muted);
|
||||
return;
|
||||
}
|
||||
|
||||
fields.forEach((field) => {
|
||||
const label = document.createElement('label');
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.textContent = field.label;
|
||||
|
||||
label.appendChild(title);
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.name = `specs[${field.key}]`;
|
||||
input.value = typeof specValues[field.key] === 'string' ? specValues[field.key] : '';
|
||||
|
||||
const options = Array.isArray(field.options)
|
||||
? [...new Set(field.options.map((value) => String(value).trim()).filter(Boolean))]
|
||||
: [];
|
||||
|
||||
if (options.length > 0) {
|
||||
const listId = `pc-spec-options-${field.key}`;
|
||||
input.setAttribute('list', listId);
|
||||
input.setAttribute('autocomplete', 'off');
|
||||
|
||||
const datalist = document.createElement('datalist');
|
||||
datalist.id = listId;
|
||||
|
||||
options.forEach((optionValue) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = optionValue;
|
||||
datalist.appendChild(option);
|
||||
});
|
||||
|
||||
const hint = document.createElement('small');
|
||||
hint.className = 'pc-field-hint';
|
||||
hint.textContent = 'Выберите из списка или введите свое значение';
|
||||
|
||||
label.appendChild(input);
|
||||
label.appendChild(datalist);
|
||||
label.appendChild(hint);
|
||||
} else {
|
||||
label.appendChild(input);
|
||||
}
|
||||
|
||||
specsContainer.appendChild(label);
|
||||
});
|
||||
};
|
||||
|
||||
categorySelect.addEventListener('change', renderSpecFields);
|
||||
if (categorySelect.value === '' && '{{ $selectedCategoryId }}' !== '0') {
|
||||
categorySelect.value = '{{ $selectedCategoryId }}';
|
||||
}
|
||||
renderSpecFields();
|
||||
})();
|
||||
</script>
|
||||
22
resources/views/admin/products/create.blade.php
Normal file
22
resources/views/admin/products/create.blade.php
Normal file
@@ -0,0 +1,22 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => route('admin.products.index')],
|
||||
['label' => 'Новый товар', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Новый товар</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@include('admin.products._form', ['submitLabel' => 'Создать'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
23
resources/views/admin/products/edit.blade.php
Normal file
23
resources/views/admin/products/edit.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => route('admin.products.index')],
|
||||
['label' => 'Редактирование', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Редактирование товара</h2>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.update', $product) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('put')
|
||||
@include('admin.products._form', ['product' => $product, 'submitLabel' => 'Сохранить'])
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
58
resources/views/admin/products/index.blade.php
Normal file
58
resources/views/admin/products/index.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Админка', 'url' => route('admin.dashboard')],
|
||||
['label' => 'Товары', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>Товары</h2>
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn primary" href="{{ route('admin.products.create') }}">Добавить товар</a>
|
||||
<a class="pc-btn ghost" href="{{ route('admin.products.export') }}">Экспорт CSV</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Импорт из CSV</h3>
|
||||
<p class="pc-muted">Загрузите CSV файл (UTF-8). Поддерживаются колонки category_slug/category_name, name, price, stock и дополнительные характеристики.</p>
|
||||
<form class="pc-product-actions" method="post" action="{{ route('admin.products.import') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input class="pc-file-input" type="file" name="csv_file" accept=".csv,text/csv" required>
|
||||
<button class="pc-btn ghost" type="submit">Импорт CSV</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
@if ($products->isEmpty())
|
||||
<p>Товары пока не созданы.</p>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($products as $product)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $product->name }} ({{ $product->category?->name ?? 'Без категории' }})</span>
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn ghost" href="{{ route('admin.products.edit', $product) }}">Редактировать</a>
|
||||
<form method="post" action="{{ route('admin.products.destroy', $product) }}">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $products->links() }}
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
35
resources/views/components/chat-widget.blade.php
Normal file
35
resources/views/components/chat-widget.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<div
|
||||
class="pc-chat-widget"
|
||||
data-chat-widget="true"
|
||||
data-fetch-url="{{ route('chat.messages') }}"
|
||||
data-send-url="{{ route('chat.send') }}"
|
||||
data-csrf="{{ csrf_token() }}"
|
||||
>
|
||||
<button class="pc-chat-toggle" type="button" data-chat-toggle>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 5h16v10H8l-4 4V5zm2 2v7.17L7.17 13H18V7H6z"></path>
|
||||
</svg>
|
||||
<span>Чат</span>
|
||||
</button>
|
||||
|
||||
<section class="pc-chat-panel" data-chat-panel hidden>
|
||||
<header class="pc-chat-header">
|
||||
<h3>Онлайн-чат</h3>
|
||||
<button class="pc-chat-close" type="button" data-chat-close aria-label="Свернуть чат">×</button>
|
||||
</header>
|
||||
<p class="pc-chat-note" data-chat-note>Задайте вопрос — администратор ответит в этом окне.</p>
|
||||
|
||||
<div class="pc-chat-messages" data-chat-messages></div>
|
||||
|
||||
<form class="pc-chat-form" data-chat-form>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Введите сообщение..."
|
||||
maxlength="2000"
|
||||
rows="3"
|
||||
required
|
||||
></textarea>
|
||||
<button class="pc-btn primary" type="submit">Отправить</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
75
resources/views/components/footer.blade.php
Normal file
75
resources/views/components/footer.blade.php
Normal file
@@ -0,0 +1,75 @@
|
||||
@php
|
||||
$companyName = config('shop.company_name', config('app.name'));
|
||||
$companyDescription = config('shop.company_description');
|
||||
$contactPhone = trim((string) config('shop.contact_phone'));
|
||||
$contactEmail = trim((string) config('shop.contact_email'));
|
||||
$contactTelegram = trim((string) config('shop.contact_telegram'));
|
||||
$contactHours = trim((string) config('shop.contact_hours'));
|
||||
$telegramUrl = '';
|
||||
$phoneUrl = '';
|
||||
$emailUrl = '';
|
||||
|
||||
if ($contactPhone !== '') {
|
||||
$phoneUrl = 'tel:' . preg_replace('/[^\d+]/', '', $contactPhone);
|
||||
}
|
||||
|
||||
if ($contactEmail !== '') {
|
||||
$emailUrl = 'mailto:' . $contactEmail;
|
||||
}
|
||||
|
||||
if ($contactTelegram !== '') {
|
||||
$telegramUrl = str_starts_with($contactTelegram, 'http://') || str_starts_with($contactTelegram, 'https://')
|
||||
? $contactTelegram
|
||||
: 'https://t.me/' . ltrim($contactTelegram, '@/');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<footer class="pc-footer">
|
||||
<div class="pc-footer-top">
|
||||
<div class="pc-footer-brand">
|
||||
<h3>{{ $companyName }}</h3>
|
||||
<p>{{ $companyDescription }}</p>
|
||||
</div>
|
||||
<div class="pc-footer-columns">
|
||||
<div class="pc-footer-col">
|
||||
<h4>Каталог</h4>
|
||||
<ul class="pc-footer-list">
|
||||
<li><a href="{{ route('catalog.index') }}">Все категории</a></li>
|
||||
<li><a href="{{ route('catalog.category', 'processors') }}">Процессоры</a></li>
|
||||
<li><a href="{{ route('catalog.category', 'graphics-cards') }}">Видеокарты</a></li>
|
||||
<li><a href="{{ route('catalog.category', 'laptops') }}">Ноутбуки</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pc-footer-col">
|
||||
<h4>Покупателю</h4>
|
||||
<ul class="pc-footer-list">
|
||||
<li><a href="{{ route('cart.index') }}">Корзина</a></li>
|
||||
<li><a href="{{ route('favorites.index') }}">Избранное</a></li>
|
||||
<li><a href="{{ route('compare.index') }}">Сравнение</a></li>
|
||||
<li><a href="{{ route('pages.shipping-payment') }}">Доставка и оплата</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pc-footer-col">
|
||||
<h4>Контакты</h4>
|
||||
<div class="pc-footer-contact">
|
||||
@if ($phoneUrl !== '')
|
||||
<span>Телефон: <a href="{{ $phoneUrl }}">{{ $contactPhone }}</a></span>
|
||||
@endif
|
||||
@if ($emailUrl !== '')
|
||||
<span>Email: <a href="{{ $emailUrl }}">{{ $contactEmail }}</a></span>
|
||||
@endif
|
||||
@if ($telegramUrl !== '')
|
||||
<span>Telegram: <a href="{{ $telegramUrl }}" target="_blank" rel="noopener noreferrer">{{ $contactTelegram }}</a></span>
|
||||
@endif
|
||||
@if ($contactHours !== '')
|
||||
<span>{{ $contactHours }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pc-footer-bottom">
|
||||
<span>(c) {{ date('Y') }} {{ $companyName }}. Все права защищены.</span>
|
||||
<span><a href="{{ route('pages.about') }}">О компании</a> / <a href="{{ route('pages.contacts') }}">Контакты</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
100
resources/views/components/header.blade.php
Normal file
100
resources/views/components/header.blade.php
Normal file
@@ -0,0 +1,100 @@
|
||||
@php
|
||||
$favoritesCount = count((array) session('favorites', []));
|
||||
$compareCount = count((array) session('compare', []));
|
||||
$cartCount = collect((array) session('cart', []))->sum(fn ($quantity) => (int) $quantity);
|
||||
$companyName = config('shop.company_name', config('app.name'));
|
||||
$navItems = [
|
||||
['label' => 'Главная', 'route' => route('home'), 'active' => request()->routeIs('home')],
|
||||
[
|
||||
'label' => 'Каталог',
|
||||
'route' => route('catalog.index'),
|
||||
'active' => request()->routeIs('catalog.*') || request()->routeIs('products.show') || request()->routeIs('search.index'),
|
||||
],
|
||||
['label' => 'О нас', 'route' => route('pages.about'), 'active' => request()->routeIs('pages.about')],
|
||||
[
|
||||
'label' => 'Доставка и оплата',
|
||||
'route' => route('pages.shipping-payment'),
|
||||
'active' => request()->routeIs('pages.shipping-payment'),
|
||||
],
|
||||
['label' => 'Контакты', 'route' => route('pages.contacts'), 'active' => request()->routeIs('pages.contacts')],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<header class="pc-header pc-animate" style="--delay: 0s">
|
||||
<input type="checkbox" id="pc-mobile-menu-toggle" class="pc-mobile-menu-toggle">
|
||||
<label for="pc-mobile-menu-toggle" class="pc-hamburger" aria-label="Меню">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</label>
|
||||
<div class="pc-mobile-menu-head">
|
||||
<label for="pc-mobile-menu-toggle" class="pc-mobile-menu-close" aria-label="Закрыть меню">×</label>
|
||||
</div>
|
||||
<div class="pc-header-left">
|
||||
<a class="pc-logo" href="{{ route('home') }}">
|
||||
<span class="pc-logo-mark"></span>
|
||||
{{ $companyName }}
|
||||
</a>
|
||||
<a class="pc-btn pc-catalog-btn" href="{{ route('catalog.index') }}">Каталог</a>
|
||||
</div>
|
||||
<div class="pc-header-center">
|
||||
<form class="pc-search" action="{{ route('search.index') }}" method="get">
|
||||
<input type="text" name="q" placeholder="Поиск товаров по наименованию" value="{{ request('q') }}" />
|
||||
<button class="pc-search-submit" type="submit" aria-label="Искать">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M15.5 14h-.79l-.28-.27a6 6 0 1 0-.71.71l.27.28v.79L20 20.5 21.5 19 15.5 14zm-5.5 0A4.5 4.5 0 1 1 10 5a4.5 4.5 0 0 1 0 9z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="pc-header-icons">
|
||||
<a class="pc-icon-link" href="{{ route('favorites.index') }}" aria-label="Избранное">
|
||||
<span class="pc-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 20s-7-4.5-9-9c-1.2-2.7.7-6 4.2-6 2 0 3.2 1 3.8 2 0.6-1 1.8-2 3.8-2 3.5 0 5.4 3.3 4.2 6-2 4.5-9 9-9 9z"></path>
|
||||
</svg>
|
||||
@if ($favoritesCount > 0)
|
||||
<span class="pc-icon-count">{{ $favoritesCount }}</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="pc-icon-label">Избранное</span>
|
||||
</a>
|
||||
<a class="pc-icon-link" href="{{ route('compare.index') }}" aria-label="Сравнение">
|
||||
<span class="pc-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 4h3v16H5zM16 4h3v16h-3zM10.5 8h3v12h-3z"></path>
|
||||
</svg>
|
||||
@if ($compareCount > 0)
|
||||
<span class="pc-icon-count">{{ $compareCount }}</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="pc-icon-label">Сравнение</span>
|
||||
</a>
|
||||
<a class="pc-icon-link" href="{{ route('cart.index') }}" aria-label="Корзина">
|
||||
<span class="pc-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M6 6h14l-2 9H8L6 6zm-2-2h3l1 2h-4zM9 20a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zm8 0a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z"></path>
|
||||
</svg>
|
||||
@if ($cartCount > 0)
|
||||
<span class="pc-icon-count">{{ $cartCount }}</span>
|
||||
@endif
|
||||
</span>
|
||||
<span class="pc-icon-label">Корзина</span>
|
||||
</a>
|
||||
<a class="pc-icon-link" href="{{ auth()->check() ? route('account') : route('login') }}" aria-label="Личный кабинет">
|
||||
<span class="pc-icon">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 12a4 4 0 1 0-4-4 4 4 0 0 0 4 4zm0 2c-4.4 0-8 2.2-8 5v1h16v-1c0-2.8-3.6-5-8-5z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="pc-icon-label">{{ auth()->check() ? 'Кабинет' : 'Войти' }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="pc-header-nav" aria-label="Разделы сайта">
|
||||
@foreach ($navItems as $item)
|
||||
<a class="pc-header-nav-link {{ $item['active'] ? 'is-active' : '' }}" href="{{ $item['route'] }}">
|
||||
{{ $item['label'] }}
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</header>
|
||||
146
resources/views/layouts/shop.blade.php
Normal file
146
resources/views/layouts/shop.blade.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
@php
|
||||
$siteName = config('seo.site_name', config('app.name', 'PC Shop'));
|
||||
$metaTitleRaw = trim($__env->yieldContent('meta_title'));
|
||||
$metaTitle = $metaTitleRaw !== '' ? "{$metaTitleRaw} - {$siteName}" : config('seo.default_title', $siteName);
|
||||
$metaDescription = trim($__env->yieldContent('meta_description')) ?: config('seo.default_description');
|
||||
$metaKeywords = trim($__env->yieldContent('meta_keywords')) ?: config('seo.default_keywords');
|
||||
$metaCanonical = trim($__env->yieldContent('meta_canonical')) ?: url()->current();
|
||||
$metaOgType = trim($__env->yieldContent('meta_og_type')) ?: 'website';
|
||||
$request = request();
|
||||
$defaultNoIndex = $request->is('admin*')
|
||||
|| $request->routeIs(
|
||||
'favorites.*',
|
||||
'compare.*',
|
||||
'cart.*',
|
||||
'checkout.*',
|
||||
'account*',
|
||||
'login',
|
||||
'login.attempt',
|
||||
'register',
|
||||
'register.store'
|
||||
);
|
||||
$metaRobots = trim($__env->yieldContent('meta_robots')) ?: ($defaultNoIndex ? 'noindex,nofollow' : 'index,follow');
|
||||
$metaImagePath = trim($__env->yieldContent('meta_image')) ?: config('seo.default_image');
|
||||
$metaImage = str_starts_with($metaImagePath, 'http://') || str_starts_with($metaImagePath, 'https://')
|
||||
? $metaImagePath
|
||||
: url($metaImagePath);
|
||||
$metaImageAlt = trim($__env->yieldContent('meta_image_alt')) ?: $metaTitle;
|
||||
$siteUrl = url('/');
|
||||
$companyName = config('shop.company_name', $siteName);
|
||||
$companyDescription = config('shop.company_description', $metaDescription);
|
||||
$companyPhone = trim((string) config('shop.contact_phone', ''));
|
||||
$companyEmail = trim((string) config('shop.contact_email', ''));
|
||||
$companyAddress = trim((string) config('shop.contact_address', ''));
|
||||
$companyTelegram = trim((string) config('shop.contact_telegram', ''));
|
||||
if ($companyTelegram !== '' && str_starts_with($companyTelegram, '@')) {
|
||||
$companyTelegram = 'https://t.me/' . ltrim($companyTelegram, '@');
|
||||
}
|
||||
|
||||
$organizationSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Organization',
|
||||
'name' => $companyName,
|
||||
'url' => $siteUrl,
|
||||
'description' => $companyDescription,
|
||||
];
|
||||
|
||||
if ($companyPhone !== '') {
|
||||
$organizationSchema['telephone'] = $companyPhone;
|
||||
}
|
||||
|
||||
if ($companyEmail !== '') {
|
||||
$organizationSchema['email'] = $companyEmail;
|
||||
}
|
||||
|
||||
if ($companyAddress !== '') {
|
||||
$organizationSchema['address'] = [
|
||||
'@type' => 'PostalAddress',
|
||||
'streetAddress' => $companyAddress,
|
||||
];
|
||||
}
|
||||
|
||||
if ($companyTelegram !== '' && (str_starts_with($companyTelegram, 'http://') || str_starts_with($companyTelegram, 'https://'))) {
|
||||
$organizationSchema['sameAs'] = [$companyTelegram];
|
||||
}
|
||||
|
||||
$websiteSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'WebSite',
|
||||
'name' => $siteName,
|
||||
'url' => $siteUrl,
|
||||
'potentialAction' => [
|
||||
'@type' => 'SearchAction',
|
||||
'target' => route('search.index') . '?q={search_term_string}',
|
||||
'query-input' => 'required name=search_term_string',
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ $metaTitle }}</title>
|
||||
<meta name="description" content="{{ $metaDescription }}">
|
||||
<meta name="keywords" content="{{ $metaKeywords }}">
|
||||
<meta name="robots" content="{{ $metaRobots }}">
|
||||
<link rel="canonical" href="{{ $metaCanonical }}">
|
||||
<link rel="alternate" hreflang="ru-RU" href="{{ $metaCanonical }}">
|
||||
<link rel="alternate" hreflang="x-default" href="{{ $metaCanonical }}">
|
||||
<meta property="og:type" content="{{ $metaOgType }}">
|
||||
<meta property="og:locale" content="ru_RU">
|
||||
<meta property="og:site_name" content="{{ $siteName }}">
|
||||
<meta property="og:title" content="{{ $metaTitle }}">
|
||||
<meta property="og:description" content="{{ $metaDescription }}">
|
||||
<meta property="og:url" content="{{ $metaCanonical }}">
|
||||
<meta property="og:image" content="{{ $metaImage }}">
|
||||
<meta property="og:image:alt" content="{{ $metaImageAlt }}">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="{{ $metaTitle }}">
|
||||
<meta name="twitter:description" content="{{ $metaDescription }}">
|
||||
<meta name="twitter:image" content="{{ $metaImage }}">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=space-grotesk:400,500,600,700" rel="stylesheet" />
|
||||
<link href="https://fonts.bunny.net/css?family=manrope:400,500,600,700" rel="stylesheet" />
|
||||
|
||||
@if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot')))
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@endif
|
||||
|
||||
@unless($request->is('admin*'))
|
||||
<script type="application/ld+json">
|
||||
@json($organizationSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
<script type="application/ld+json">
|
||||
@json($websiteSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endunless
|
||||
|
||||
@stack('structured_data')
|
||||
</head>
|
||||
<body class="pc-body">
|
||||
<div class="pc-shell">
|
||||
<x-header />
|
||||
|
||||
<main class="pc-main">
|
||||
@if (session('status'))
|
||||
<div class="pc-alert">{{ session('status') }}</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div class="pc-alert pc-alert-error">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
@yield('content')
|
||||
</main>
|
||||
|
||||
@if (!request()->is('admin*'))
|
||||
<x-chat-widget />
|
||||
@endif
|
||||
|
||||
<x-footer />
|
||||
</div>
|
||||
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
44
resources/views/pages/about.blade.php
Normal file
44
resources/views/pages/about.blade.php
Normal file
@@ -0,0 +1,44 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'О компании')
|
||||
@section('meta_description', 'О компании: помощь в подборе комплектующих, консультации и поддержка при сборке ПК.')
|
||||
@section('meta_keywords', 'о компании, магазин комплектующих, поддержка')
|
||||
@section('meta_canonical', route('pages.about'))
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$companyName = config('shop.company_name', config('app.name'));
|
||||
@endphp
|
||||
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'О нас', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>{{ $companyName }} — интернет-магазин компьютерных комплектующих.</h2>
|
||||
<p>Мы помогаем подобрать совместимую сборку, оформить заказ и получить технику с понятной поддержкой после покупки.</p>
|
||||
</div>
|
||||
|
||||
<div class="pc-about-content">
|
||||
<h3>Кто мы</h3>
|
||||
<p>{{ $companyName }} работает для тех, кому важно собрать быстрый и надежный ПК без ошибок по совместимости. В каталоге есть комплектующие для домашних, рабочих и игровых систем: процессоры, материнские платы, видеокарты, память, накопители, блоки питания, корпуса, системы охлаждения, ноутбуки и периферия.</p>
|
||||
<p>Мы делаем акцент на понятном выборе: категории с фильтрами, сравнение товаров, избранное, корзина и личный кабинет с историей заказов. Это помогает быстрее принять решение и не потерять важные позиции при подборе сборки.</p>
|
||||
|
||||
<h3>Как мы помогаем клиентам</h3>
|
||||
<ul class="pc-list">
|
||||
<li>Проверяем ключевые характеристики и совместимость комплектующих.</li>
|
||||
<li>Подсказываем оптимальные варианты под бюджет и задачи.</li>
|
||||
<li>Сопровождаем заказ от оформления до получения.</li>
|
||||
<li>Объясняем условия доставки, оплаты, возврата и гарантии простым языком.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Наш подход</h3>
|
||||
<p>Для нас важны прозрачность и сервис: актуальные цены, понятные характеристики и честная обратная связь. Мы стремимся, чтобы покупка техники была удобной как для новичков, так и для опытных пользователей, которые собирают ПК самостоятельно.</p>
|
||||
<p>Если вам нужна консультация перед покупкой, команда {{ $companyName }} поможет подобрать комплектующие и предложит сбалансированные варианты под ваши задачи.</p>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
81
resources/views/pages/contacts.blade.php
Normal file
81
resources/views/pages/contacts.blade.php
Normal file
@@ -0,0 +1,81 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'Контакты')
|
||||
@section('meta_description', 'Контакты магазина: телефон, email, адрес и часы работы поддержки.')
|
||||
@section('meta_keywords', 'контакты магазина, телефон, email, адрес')
|
||||
@section('meta_canonical', route('pages.contacts'))
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$companyName = config('shop.company_name', config('app.name'));
|
||||
$contactPhone = trim((string) config('shop.contact_phone'));
|
||||
$contactEmail = trim((string) config('shop.contact_email'));
|
||||
$contactTelegram = trim((string) config('shop.contact_telegram'));
|
||||
$contactHours = trim((string) config('shop.contact_hours'));
|
||||
$telegramUrl = '';
|
||||
$phoneUrl = '';
|
||||
$emailUrl = '';
|
||||
|
||||
if ($contactPhone !== '') {
|
||||
$phoneUrl = 'tel:' . preg_replace('/[^\d+]/', '', $contactPhone);
|
||||
}
|
||||
|
||||
if ($contactEmail !== '') {
|
||||
$emailUrl = 'mailto:' . $contactEmail;
|
||||
}
|
||||
|
||||
if ($contactTelegram !== '') {
|
||||
$telegramUrl = str_starts_with($contactTelegram, 'http://') || str_starts_with($contactTelegram, 'https://')
|
||||
? $contactTelegram
|
||||
: 'https://t.me/' . ltrim($contactTelegram, '@/');
|
||||
}
|
||||
@endphp
|
||||
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Контакты', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Поможем с вашей сборкой.</h2>
|
||||
<p>Оставьте заявку — поможем подобрать комплектующие и ответим по доставке.</p>
|
||||
</div>
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<article class="pc-card">
|
||||
<div class="pc-card-meta">Поддержка</div>
|
||||
<h3>{{ $companyName }} — поддержка клиентов</h3>
|
||||
@if ($phoneUrl !== '')
|
||||
<p>Телефон: <a class="pc-contact-link" href="{{ $phoneUrl }}">{{ $contactPhone }}</a></p>
|
||||
@endif
|
||||
@if ($emailUrl !== '')
|
||||
<p>Почта: <a class="pc-contact-link" href="{{ $emailUrl }}">{{ $contactEmail }}</a></p>
|
||||
@endif
|
||||
@if ($telegramUrl !== '')
|
||||
<p>Telegram: <a class="pc-contact-link" href="{{ $telegramUrl }}" target="_blank" rel="noopener noreferrer">{{ $contactTelegram }}</a></p>
|
||||
@endif
|
||||
@if ($contactHours !== '')
|
||||
<p>Часы: {{ $contactHours }}</p>
|
||||
@endif
|
||||
</article>
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('pages.contacts.submit') }}">
|
||||
@csrf
|
||||
<label>
|
||||
Имя
|
||||
<input type="text" name="name" value="{{ old('name') }}" placeholder="Ваше имя" required />
|
||||
</label>
|
||||
<label>
|
||||
Почта
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="mail@example.com" required />
|
||||
</label>
|
||||
<label>
|
||||
Сообщение
|
||||
<textarea name="message" placeholder="Расскажите о вашей сборке" required>{{ old('message') }}</textarea>
|
||||
</label>
|
||||
<button type="submit" class="pc-btn primary">Отправить сообщение</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
42
resources/views/pages/shipping-payment.blade.php
Normal file
42
resources/views/pages/shipping-payment.blade.php
Normal file
@@ -0,0 +1,42 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'Доставка и оплата')
|
||||
@section('meta_description', 'Условия доставки и способы оплаты заказов в интернет-магазине комплектующих.')
|
||||
@section('meta_keywords', 'доставка, оплата, условия заказа')
|
||||
@section('meta_canonical', route('pages.shipping-payment'))
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Доставка и оплата', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Быстрая доставка и удобная оплата.</h2>
|
||||
<p>Выбирайте курьера или доставку по времени с безопасной оплатой.</p>
|
||||
</div>
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<article class="pc-card">
|
||||
<div class="pc-card-meta">Доставка</div>
|
||||
<h3>Варианты доставки</h3>
|
||||
<ul class="pc-list">
|
||||
<li>День в день в пределах города</li>
|
||||
<li>1-3 дня по стране</li>
|
||||
<li>Онлайн‑треккинг</li>
|
||||
</ul>
|
||||
</article>
|
||||
<article class="pc-card">
|
||||
<div class="pc-card-meta">Оплата</div>
|
||||
<h3>Способы оплаты</h3>
|
||||
<ul class="pc-list">
|
||||
<li>Оплата банковской картой</li>
|
||||
<li>Безналичный расчет для юрлиц</li>
|
||||
<li>Подтверждение оплаты в личном кабинете</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
50
resources/views/partials/breadcrumbs.blade.php
Normal file
50
resources/views/partials/breadcrumbs.blade.php
Normal file
@@ -0,0 +1,50 @@
|
||||
@if (!empty($items))
|
||||
@php
|
||||
$breadcrumbSchemaItems = collect($items)
|
||||
->values()
|
||||
->map(function ($item, $index) {
|
||||
if (!is_array($item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$label = trim((string) ($item['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$url = !empty($item['url']) ? (string) $item['url'] : url()->current();
|
||||
|
||||
return [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'name' => $label,
|
||||
'item' => $url,
|
||||
];
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
$breadcrumbSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'BreadcrumbList',
|
||||
'itemListElement' => $breadcrumbSchemaItems,
|
||||
];
|
||||
@endphp
|
||||
|
||||
<nav class="pc-breadcrumbs" aria-label="Хлебные крошки">
|
||||
@foreach ($items as $item)
|
||||
@if (!$loop->last && !empty($item['url']))
|
||||
<a href="{{ $item['url'] }}">{{ $item['label'] }}</a>
|
||||
<span class="pc-breadcrumbs-sep">/</span>
|
||||
@else
|
||||
<span class="pc-breadcrumbs-current">{{ $item['label'] }}</span>
|
||||
@endif
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@if (!empty($breadcrumbSchemaItems))
|
||||
<script type="application/ld+json">
|
||||
@json($breadcrumbSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endif
|
||||
@endif
|
||||
49
resources/views/partials/home-slider.blade.php
Normal file
49
resources/views/partials/home-slider.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@php
|
||||
$slides = $slides ?? collect();
|
||||
$isSingle = $slides->count() <= 1;
|
||||
@endphp
|
||||
|
||||
<div class="pc-home-slider {{ $sliderClass ?? '' }}" data-home-slider>
|
||||
<div class="pc-home-slider-track">
|
||||
@forelse ($slides as $slide)
|
||||
@php
|
||||
$hasTitle = $slide->show_title && !empty($slide->title);
|
||||
$hasSubtitle = $slide->show_subtitle && !empty($slide->subtitle);
|
||||
$hasButton = $slide->show_button && !empty($slide->button_text) && !empty($slide->button_url);
|
||||
$hasContent = $hasTitle || $hasSubtitle || $hasButton;
|
||||
$altText = $hasTitle ? $slide->title : 'Слайд на главной странице';
|
||||
@endphp
|
||||
<article class="pc-home-slide {{ $loop->first ? 'is-active' : '' }}" data-home-slide>
|
||||
<img src="{{ $slide->image_url }}" alt="{{ $altText }}" loading="{{ $loop->first ? 'eager' : 'lazy' }}">
|
||||
<div class="pc-home-slide-overlay"></div>
|
||||
@if ($hasContent)
|
||||
<div class="pc-home-slide-content">
|
||||
@if ($hasTitle)
|
||||
<h2>{{ $slide->title }}</h2>
|
||||
@endif
|
||||
@if ($hasSubtitle)
|
||||
<p>{{ $slide->subtitle }}</p>
|
||||
@endif
|
||||
@if ($hasButton)
|
||||
<a class="pc-btn primary" href="{{ $slide->button_url }}">{{ $slide->button_text }}</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</article>
|
||||
@empty
|
||||
<article class="pc-home-slide is-active is-fallback" data-home-slide>
|
||||
<div class="pc-home-slide-overlay"></div>
|
||||
<div class="pc-home-slide-content">
|
||||
<h2>{{ $fallbackTitle ?? 'Собирайте ПК быстрее' }}</h2>
|
||||
<p>{{ $fallbackText ?? 'Загрузите баннеры в админке, чтобы вывести акции и подборки товаров на главной странице.' }}</p>
|
||||
@if (!empty($fallbackUrl))
|
||||
<a class="pc-btn primary" href="{{ $fallbackUrl }}">{{ $fallbackButton ?? 'Открыть каталог' }}</a>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<button class="pc-home-slider-btn is-prev" type="button" data-home-slider-prev aria-label="Предыдущий слайд" @disabled($isSingle)>‹</button>
|
||||
<button class="pc-home-slider-btn is-next" type="button" data-home-slider-next aria-label="Следующий слайд" @disabled($isSingle)>›</button>
|
||||
</div>
|
||||
41
resources/views/partials/pagination.blade.php
Normal file
41
resources/views/partials/pagination.blade.php
Normal file
@@ -0,0 +1,41 @@
|
||||
@if ($paginator->hasPages())
|
||||
<nav class="pc-pager" role="navigation" aria-label="Пагинация">
|
||||
<ul class="pc-pager-list">
|
||||
<li>
|
||||
@if ($paginator->onFirstPage())
|
||||
<span class="pc-pager-link is-disabled" aria-disabled="true">Назад</span>
|
||||
@else
|
||||
<a class="pc-pager-link" href="{{ $paginator->previousPageUrl() }}" rel="prev">Назад</a>
|
||||
@endif
|
||||
</li>
|
||||
|
||||
@foreach ($elements as $element)
|
||||
@if (is_string($element))
|
||||
<li>
|
||||
<span class="pc-pager-link is-gap">{{ $element }}</span>
|
||||
</li>
|
||||
@endif
|
||||
|
||||
@if (is_array($element))
|
||||
@foreach ($element as $page => $url)
|
||||
<li>
|
||||
@if ($page == $paginator->currentPage())
|
||||
<span class="pc-pager-link is-active" aria-current="page">{{ $page }}</span>
|
||||
@else
|
||||
<a class="pc-pager-link" href="{{ $url }}">{{ $page }}</a>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<li>
|
||||
@if ($paginator->hasMorePages())
|
||||
<a class="pc-pager-link" href="{{ $paginator->nextPageUrl() }}" rel="next">Вперед</a>
|
||||
@else
|
||||
<span class="pc-pager-link is-disabled" aria-disabled="true">Вперед</span>
|
||||
@endif
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@endif
|
||||
47
resources/views/partials/payment-requisites.blade.php
Normal file
47
resources/views/partials/payment-requisites.blade.php
Normal file
@@ -0,0 +1,47 @@
|
||||
@php
|
||||
$paymentBank = config('shop.payment_bank');
|
||||
$paymentCardHolder = config('shop.payment_card_holder');
|
||||
$paymentCardNumber = config('shop.payment_card_number');
|
||||
$telegram = trim((string) config('shop.contact_telegram'));
|
||||
$telegramUrl = '';
|
||||
|
||||
if ($telegram !== '') {
|
||||
$telegramUrl = str_starts_with($telegram, 'http://') || str_starts_with($telegram, 'https://')
|
||||
? $telegram
|
||||
: 'https://t.me/' . ltrim($telegram, '@/');
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pc-payment-details {{ $class ?? '' }}">
|
||||
<h3>{{ $title ?? 'Оплата по реквизитам' }}</h3>
|
||||
|
||||
@isset($amount)
|
||||
<p class="pc-payment-total">Сумма к оплате: <strong>{{ number_format((float) $amount, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong></p>
|
||||
@endisset
|
||||
|
||||
<div class="pc-payment-grid">
|
||||
<div class="pc-payment-row">
|
||||
<span class="pc-payment-key">Банк</span>
|
||||
<strong class="pc-payment-value">{{ $paymentBank }}</strong>
|
||||
</div>
|
||||
<div class="pc-payment-row">
|
||||
<span class="pc-payment-key">Получатель</span>
|
||||
<strong class="pc-payment-value">{{ $paymentCardHolder }}</strong>
|
||||
</div>
|
||||
<div class="pc-payment-row">
|
||||
<span class="pc-payment-key">Номер карты</span>
|
||||
<strong class="pc-payment-value">{{ $paymentCardNumber }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@isset($purpose)
|
||||
<p class="pc-payment-purpose">Назначение платежа: <strong>{{ $purpose }}</strong></p>
|
||||
@endisset
|
||||
|
||||
@if (!empty($showHelp))
|
||||
<p class="pc-muted">После оплаты отправьте чек в поддержку для подтверждения заказа.</p>
|
||||
@if ($telegramUrl !== '')
|
||||
<p class="pc-muted">Telegram: <a class="pc-contact-link" href="{{ $telegramUrl }}" target="_blank" rel="noopener noreferrer">{{ $telegram }}</a></p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
60
resources/views/partials/product-card.blade.php
Normal file
60
resources/views/partials/product-card.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
@php
|
||||
$favoriteIds = array_map('intval', (array) session('favorites', []));
|
||||
$compareIds = array_map('intval', (array) session('compare', []));
|
||||
$isFavorite = in_array($product->id, $favoriteIds, true);
|
||||
$isCompared = in_array($product->id, $compareIds, true);
|
||||
$cartItems = (array) session('cart', []);
|
||||
$isInCart = isset($cartItems[$product->id]);
|
||||
@endphp
|
||||
|
||||
<article class="pc-card pc-product-card">
|
||||
<div class="pc-product-media">
|
||||
<div class="pc-product-tools">
|
||||
<form method="post" action="{{ route('favorites.toggle', $product) }}" class="pc-product-tool-form" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-product-tool {{ $isFavorite ? 'is-active' : '' }}" type="submit" aria-label="Добавить в избранное" title="В избранное">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 20s-7-4.5-9-9c-1.2-2.7.7-6 4.2-6 2 0 3.2 1 3.8 2 0.6-1 1.8-2 3.8-2 3.5 0 5.4 3.3 4.2 6-2 4.5-9 9-9 9z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ route('compare.toggle', $product) }}" class="pc-product-tool-form" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-product-tool {{ $isCompared ? 'is-active' : '' }}" type="submit" aria-label="Добавить в сравнение" title="Сравнить">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M5 4h3v16H5zM16 4h3v16h-3zM10.5 8h3v12h-3z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@if (!empty($product->image_url))
|
||||
<img class="pc-product-image" src="{{ $product->image_url }}" alt="{{ $product->name }}" loading="lazy" decoding="async">
|
||||
@else
|
||||
<div class="pc-product-image" role="img" aria-label="{{ $product->name }}"></div>
|
||||
@endif
|
||||
</div>
|
||||
<h3>
|
||||
<a class="pc-product-link" href="{{ route('products.show', $product) }}">{{ $product->name }}</a>
|
||||
</h3>
|
||||
@if (!empty($product->short_description))
|
||||
<p>{{ $product->short_description }}</p>
|
||||
@endif
|
||||
<div class="pc-product-meta">
|
||||
<strong>{{ number_format($product->price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
@if (!empty($product->old_price))
|
||||
<span class="pc-product-old">{{ number_format($product->old_price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
@if ($product->stock > 0)
|
||||
<form method="post" action="{{ route('cart.add', $product) }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-btn ghost {{ $isInCart ? 'is-active' : '' }}" type="submit">
|
||||
{{ $isInCart ? 'В корзине' : 'В корзину' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<button class="pc-btn ghost" type="button" disabled>Нет в наличии</button>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
28
resources/views/partials/product-carousel.blade.php
Normal file
28
resources/views/partials/product-carousel.blade.php
Normal file
@@ -0,0 +1,28 @@
|
||||
@php
|
||||
$items = $products ?? collect();
|
||||
@endphp
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>{{ $title ?? 'Товары' }}</h2>
|
||||
@if (!empty($description))
|
||||
<p>{{ $description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($items->isEmpty())
|
||||
<div class="pc-card">{{ $emptyText ?? 'Пока нет товаров.' }}</div>
|
||||
@else
|
||||
<div class="pc-product-carousel" data-product-carousel>
|
||||
<button class="pc-product-carousel-btn is-prev" type="button" data-product-carousel-prev aria-label="Предыдущие товары">‹</button>
|
||||
<div class="pc-product-carousel-track" data-product-carousel-track>
|
||||
@foreach ($items as $product)
|
||||
<div class="pc-product-carousel-item">
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<button class="pc-product-carousel-btn is-next" type="button" data-product-carousel-next aria-label="Следующие товары">›</button>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
12
resources/views/seo/sitemap.blade.php
Normal file
12
resources/views/seo/sitemap.blade.php
Normal file
@@ -0,0 +1,12 @@
|
||||
{!! '<?xml version="1.0" encoding="UTF-8"?>' !!}
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
@foreach ($urls as $url)
|
||||
<url>
|
||||
<loc>{{ $url['loc'] }}</loc>
|
||||
<lastmod>{{ $url['lastmod'] }}</lastmod>
|
||||
<changefreq>{{ $url['changefreq'] }}</changefreq>
|
||||
<priority>{{ $url['priority'] }}</priority>
|
||||
</url>
|
||||
@endforeach
|
||||
</urlset>
|
||||
|
||||
58
resources/views/shop/account.blade.php
Normal file
58
resources/views/shop/account.blade.php
Normal file
@@ -0,0 +1,58 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Личный кабинет', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Личный кабинет</h2>
|
||||
<p>Управляйте данными профиля и просматривайте историю заказов.</p>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<div class="pc-card">
|
||||
<form class="pc-form" method="post" action="{{ route('account.update') }}">
|
||||
@csrf
|
||||
<h3>Данные аккаунта</h3>
|
||||
<label>
|
||||
Имя
|
||||
<input type="text" name="name" value="{{ old('name', $user->name) }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value="{{ old('email', $user->email) }}" required>
|
||||
</label>
|
||||
<div class="pc-product-actions">
|
||||
<button class="pc-btn primary" type="submit">Сохранить</button>
|
||||
</div>
|
||||
</form>
|
||||
<form method="post" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="pc-btn ghost" type="submit">Выйти</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Мои заказы</h3>
|
||||
@if ($orders->isEmpty())
|
||||
<p>Пока нет заказов.</p>
|
||||
<a class="pc-btn ghost" href="{{ route('catalog.index') }}">Перейти в каталог</a>
|
||||
@else
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($orders as $order)
|
||||
<a class="pc-account-order" href="{{ route('account.orders.show', $order) }}">
|
||||
<span>Заказ #{{ $order->id }}</span>
|
||||
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
41
resources/views/shop/auth/login.blade.php
Normal file
41
resources/views/shop/auth/login.blade.php
Normal file
@@ -0,0 +1,41 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Вход', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section pc-auth-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Вход</h2>
|
||||
<p>Введите email и пароль для доступа к заказам и профилю.</p>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form pc-auth-form" method="post" action="{{ route('login.attempt') }}">
|
||||
@csrf
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Пароль
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<label>
|
||||
Капча: решите пример {{ $captchaQuestion }}
|
||||
<input type="text" name="captcha" inputmode="numeric" autocomplete="off" required>
|
||||
</label>
|
||||
<label class="pc-checkbox">
|
||||
<input type="checkbox" name="remember" value="1" @checked(old('remember'))>
|
||||
<span>Запомнить меня</span>
|
||||
</label>
|
||||
<div class="pc-product-actions">
|
||||
<button class="pc-btn primary" type="submit">Войти</button>
|
||||
<a class="pc-btn ghost" href="{{ route('register') }}">Создать аккаунт</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
45
resources/views/shop/auth/register.blade.php
Normal file
45
resources/views/shop/auth/register.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Регистрация', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section pc-auth-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Регистрация</h2>
|
||||
<p>Создайте аккаунт, чтобы отслеживать заказы и сохранять избранное.</p>
|
||||
</div>
|
||||
|
||||
<form class="pc-card pc-form pc-auth-form" method="post" action="{{ route('register.store') }}">
|
||||
@csrf
|
||||
<label>
|
||||
Имя
|
||||
<input type="text" name="name" value="{{ old('name') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value="{{ old('email') }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Пароль
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<label>
|
||||
Подтверждение пароля
|
||||
<input type="password" name="password_confirmation" required>
|
||||
</label>
|
||||
<label>
|
||||
Капча: решите пример {{ $captchaQuestion }}
|
||||
<input type="text" name="captcha" inputmode="numeric" autocomplete="off" required>
|
||||
</label>
|
||||
<div class="pc-product-actions">
|
||||
<button class="pc-btn primary" type="submit">Зарегистрироваться</button>
|
||||
<a class="pc-btn ghost" href="{{ route('login') }}">Уже есть аккаунт</a>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
69
resources/views/shop/cart.blade.php
Normal file
69
resources/views/shop/cart.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Корзина', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Товары в корзине</h2>
|
||||
</div>
|
||||
|
||||
@if ($items->isEmpty())
|
||||
<div class="pc-card">
|
||||
<h3>Корзина пустая</h3>
|
||||
<p>Добавьте товары из каталога, чтобы оформить заказ.</p>
|
||||
<a class="pc-btn primary" href="{{ route('catalog.index') }}">Перейти в каталог</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="pc-cart-layout">
|
||||
<div class="pc-cart-list">
|
||||
@foreach ($items as $item)
|
||||
@php($product = $item['product'])
|
||||
<article class="pc-card pc-cart-item">
|
||||
<div class="pc-cart-item-main">
|
||||
<h3>
|
||||
<a class="pc-product-link" href="{{ route('products.show', $product) }}">{{ $product->name }}</a>
|
||||
</h3>
|
||||
@if ($product->short_description)
|
||||
<p>{{ $product->short_description }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pc-cart-item-side">
|
||||
<form method="post" action="{{ route('cart.update', $product) }}" class="pc-cart-qty-form" data-preserve-scroll="true">
|
||||
@csrf
|
||||
@method('patch')
|
||||
<input type="number" name="quantity" min="1" max="{{ max(1, $product->stock) }}" value="{{ $item['quantity'] }}">
|
||||
<button class="pc-btn ghost" type="submit">Обновить</button>
|
||||
</form>
|
||||
<strong class="pc-cart-subtotal">{{ number_format($item['subtotal'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
<form method="post" action="{{ route('cart.remove', $product) }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<aside class="pc-card pc-cart-summary">
|
||||
<h3>Итого</h3>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Товаров</span>
|
||||
<strong>{{ $itemsCount }}</strong>
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Сумма</span>
|
||||
<strong>{{ number_format($total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
<a class="pc-btn primary" href="{{ route('checkout.show') }}">Перейти к оформлению</a>
|
||||
</aside>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
92
resources/views/shop/catalog.blade.php
Normal file
92
resources/views/shop/catalog.blade.php
Normal file
@@ -0,0 +1,92 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@php
|
||||
$searchQuery = trim((string) request('q', ''));
|
||||
$hasCatalogQuery = $searchQuery !== '' || request()->filled('category') || request()->filled('page');
|
||||
$catalogCategoryList = collect($categories ?? [])
|
||||
->values()
|
||||
->map(fn ($category, $index) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'url' => route('catalog.category', $category),
|
||||
'name' => $category->name,
|
||||
])
|
||||
->all();
|
||||
$catalogSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => 'Каталог товаров',
|
||||
'url' => route('catalog.index'),
|
||||
'description' => 'Каталог компьютерных комплектующих и техники.',
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'numberOfItems' => count($catalogCategoryList),
|
||||
'itemListElement' => $catalogCategoryList,
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
@section('meta_title', $searchQuery !== '' ? "Поиск: {$searchQuery}" : 'Каталог товаров')
|
||||
@section(
|
||||
'meta_description',
|
||||
$searchQuery !== ''
|
||||
? "Результаты поиска по запросу «{$searchQuery}». Подберите нужные комплектующие по наименованию."
|
||||
: 'Каталог компьютерных комплектующих: процессоры, материнские платы, видеокарты, память, накопители и ноутбуки.'
|
||||
)
|
||||
@section('meta_keywords', 'каталог комплектующих, поиск товаров, процессоры, материнские платы, видеокарты')
|
||||
@section('meta_canonical', route('catalog.index'))
|
||||
@section('meta_robots', $hasCatalogQuery ? 'noindex,follow' : 'index,follow')
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
@json($catalogSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Каталог', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Категории товаров</h2>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-4 pc-category-grid">
|
||||
@forelse ($categories as $category)
|
||||
<a class="pc-card pc-category-card pc-category-link" href="{{ route('catalog.category', $category) }}">
|
||||
<div class="pc-category-image" role="img" aria-label="{{ $category->name }}"></div>
|
||||
<h3 class="pc-category-title">{{ $category->name }}</h3>
|
||||
</a>
|
||||
@empty
|
||||
<div class="pc-card">Категории пока не добавлены.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (request()->filled('q'))
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>Результаты по запросу: "{{ request('q') }}"</h2>
|
||||
</div>
|
||||
<p class="pc-muted">Найдено: <strong>{{ $products->total() }}</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="pc-products-grid">
|
||||
@forelse ($products as $product)
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
@empty
|
||||
<div class="pc-card">По вашему запросу ничего не найдено.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $products->links('partials.pagination') }}
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
@endsection
|
||||
266
resources/views/shop/category.blade.php
Normal file
266
resources/views/shop/category.blade.php
Normal file
@@ -0,0 +1,266 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@php
|
||||
$hasSeoFilters = request()->filled('q')
|
||||
|| request()->filled('sort')
|
||||
|| request()->filled('page')
|
||||
|| request()->filled('price_from')
|
||||
|| request()->filled('price_to')
|
||||
|| collect((array) request('filters', []))
|
||||
->contains(fn ($value) => is_scalar($value) && trim((string) $value) !== '')
|
||||
|| collect(request()->query())
|
||||
->keys()
|
||||
->contains(fn ($key) => is_string($key) && (str_ends_with($key, '_from') || str_ends_with($key, '_to')));
|
||||
|
||||
$categoryItemList = collect($products->items())
|
||||
->values()
|
||||
->map(fn ($product, $index) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'url' => route('products.show', $product),
|
||||
'name' => $product->name,
|
||||
])
|
||||
->all();
|
||||
$categorySchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'CollectionPage',
|
||||
'name' => $category->name,
|
||||
'url' => route('catalog.category', $category),
|
||||
'description' => $category->description ?: 'Категория товаров ' . $category->name,
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'numberOfItems' => $products->total(),
|
||||
'itemListElement' => $categoryItemList,
|
||||
],
|
||||
];
|
||||
@endphp
|
||||
|
||||
@section('meta_title', $category->name)
|
||||
@section('meta_description', ($category->description ?: 'Товары категории ' . $category->name . '.') . ' Фильтры и сортировка для быстрого подбора.')
|
||||
@section('meta_keywords', $category->name . ', комплектующие, купить, фильтры товаров')
|
||||
@section('meta_canonical', route('catalog.category', $category))
|
||||
@section('meta_robots', $hasSeoFilters ? 'noindex,follow' : 'index,follow')
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
@json($categorySchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Каталог', 'url' => route('catalog.index')],
|
||||
['label' => $category->name, 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section pc-category-page">
|
||||
<div class="pc-section-title">
|
||||
<h2>{{ $category->name }}</h2>
|
||||
</div>
|
||||
|
||||
@php
|
||||
$activeFilters = collect((array) ($appliedFilters ?? []))
|
||||
->filter(fn ($value) => is_scalar($value) && trim((string) $value) !== '')
|
||||
->values();
|
||||
|
||||
if (request()->filled('price_from') || request()->filled('price_to')) {
|
||||
$priceFromLabel = trim((string) request('price_from', ''));
|
||||
$priceToLabel = trim((string) request('price_to', ''));
|
||||
$activeFilters->push("Цена: {$priceFromLabel} - {$priceToLabel}");
|
||||
}
|
||||
|
||||
foreach ((array) ($filters ?? []) as $filter) {
|
||||
if ((string) ($filter['filter'] ?? 'select') !== 'range') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rangeKey = (string) ($filter['key'] ?? '');
|
||||
if ($rangeKey === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fromParam = $rangeKey . '_from';
|
||||
$toParam = $rangeKey . '_to';
|
||||
if (!request()->filled($fromParam) && !request()->filled($toParam)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fromLabel = trim((string) request($fromParam, ''));
|
||||
$toLabel = trim((string) request($toParam, ''));
|
||||
$activeFilters->push(($filter['label'] ?? $rangeKey) . ": {$fromLabel} - {$toLabel}");
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="pc-category-toolbar">
|
||||
<p class="pc-muted">Найдено товаров: <strong>{{ $products->total() }}</strong></p>
|
||||
<div class="pc-category-toolbar-controls">
|
||||
<form class="pc-sort-form" method="get">
|
||||
@foreach ((array) ($appliedFilters ?? []) as $key => $value)
|
||||
@if (is_scalar($value) && trim((string) $value) !== '')
|
||||
<input type="hidden" name="filters[{{ $key }}]" value="{{ $value }}">
|
||||
@endif
|
||||
@endforeach
|
||||
@if (request()->filled('price_from'))
|
||||
<input type="hidden" name="price_from" value="{{ request('price_from') }}">
|
||||
@endif
|
||||
@if (request()->filled('price_to'))
|
||||
<input type="hidden" name="price_to" value="{{ request('price_to') }}">
|
||||
@endif
|
||||
@foreach ((array) ($filters ?? []) as $filter)
|
||||
@php
|
||||
$rangeKey = (string) ($filter['key'] ?? '');
|
||||
@endphp
|
||||
@continue($rangeKey === '' || (string) ($filter['filter'] ?? 'select') !== 'range')
|
||||
@if (request()->filled($rangeKey . '_from'))
|
||||
<input type="hidden" name="{{ $rangeKey }}_from" value="{{ request($rangeKey . '_from') }}">
|
||||
@endif
|
||||
@if (request()->filled($rangeKey . '_to'))
|
||||
<input type="hidden" name="{{ $rangeKey }}_to" value="{{ request($rangeKey . '_to') }}">
|
||||
@endif
|
||||
@endforeach
|
||||
@if (request()->filled('q'))
|
||||
<input type="hidden" name="q" value="{{ request('q') }}">
|
||||
@endif
|
||||
<label for="sort">Сортировка:</label>
|
||||
<select id="sort" name="sort" onchange="this.form.submit()">
|
||||
<option value="newest" @selected($sort === 'newest')>Сначала новые</option>
|
||||
<option value="price_asc" @selected($sort === 'price_asc')>Сначала дешевле</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Сначала дороже</option>
|
||||
<option value="name_asc" @selected($sort === 'name_asc')>По названию</option>
|
||||
</select>
|
||||
</form>
|
||||
<button
|
||||
class="pc-filter-inline-toggle"
|
||||
type="button"
|
||||
data-filter-toggle
|
||||
aria-controls="pc-category-filters"
|
||||
aria-expanded="{{ $activeFilters->isNotEmpty() ? 'true' : 'false' }}"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 6h16v2H4V6zm3 5h10v2H7v-2zm3 5h4v2h-4v-2z"></path>
|
||||
</svg>
|
||||
<span>Фильтр</span>
|
||||
@if ($activeFilters->isNotEmpty())
|
||||
<span class="pc-filter-toggle-count">{{ $activeFilters->count() }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if ($activeFilters->isNotEmpty())
|
||||
<div class="pc-filter-tags">
|
||||
@foreach ($activeFilters as $key => $value)
|
||||
<span class="pc-filter-chip">{{ $value }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="pc-catalog-layout">
|
||||
<aside class="pc-filters">
|
||||
<div id="pc-category-filters" class="pc-filter-details {{ $activeFilters->isNotEmpty() ? 'is-open' : '' }}">
|
||||
<form method="get">
|
||||
<div class="pc-filter-title">Фильтры</div>
|
||||
@if ($sort !== 'newest')
|
||||
<input type="hidden" name="sort" value="{{ $sort }}">
|
||||
@endif
|
||||
@if (request()->filled('q'))
|
||||
<input type="hidden" name="q" value="{{ request('q') }}">
|
||||
@endif
|
||||
<label class="pc-filter-block">
|
||||
<span>Цена</span>
|
||||
<div class="pc-range-fields">
|
||||
<input
|
||||
type="number"
|
||||
name="price_from"
|
||||
step="1"
|
||||
value="{{ $priceFilter['from'] ?? '' }}"
|
||||
@if (!empty($priceFilter['min'])) min="{{ $priceFilter['min'] }}" @endif
|
||||
@if (!empty($priceFilter['max'])) max="{{ $priceFilter['max'] }}" @endif
|
||||
placeholder="От"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="price_to"
|
||||
step="1"
|
||||
value="{{ $priceFilter['to'] ?? '' }}"
|
||||
@if (!empty($priceFilter['min'])) min="{{ $priceFilter['min'] }}" @endif
|
||||
@if (!empty($priceFilter['max'])) max="{{ $priceFilter['max'] }}" @endif
|
||||
placeholder="До"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
@forelse ($filters as $filter)
|
||||
@php
|
||||
$filterKey = (string) ($filter['key'] ?? '');
|
||||
$isRangeFilter = (string) ($filter['filter'] ?? 'select') === 'range';
|
||||
$rangeData = (array) ($rangeFilters[$filterKey] ?? []);
|
||||
@endphp
|
||||
@continue($filterKey === '')
|
||||
@if ($isRangeFilter)
|
||||
<label class="pc-filter-block">
|
||||
<span>{{ $filter['label'] }}</span>
|
||||
<div class="pc-range-fields">
|
||||
<input
|
||||
type="number"
|
||||
name="{{ $filterKey }}_from"
|
||||
step="1"
|
||||
value="{{ $rangeData['from'] ?? '' }}"
|
||||
@if (!empty($rangeData['min'])) min="{{ $rangeData['min'] }}" @endif
|
||||
@if (!empty($rangeData['max'])) max="{{ $rangeData['max'] }}" @endif
|
||||
placeholder="От"
|
||||
>
|
||||
<input
|
||||
type="number"
|
||||
name="{{ $filterKey }}_to"
|
||||
step="1"
|
||||
value="{{ $rangeData['to'] ?? '' }}"
|
||||
@if (!empty($rangeData['min'])) min="{{ $rangeData['min'] }}" @endif
|
||||
@if (!empty($rangeData['max'])) max="{{ $rangeData['max'] }}" @endif
|
||||
placeholder="До"
|
||||
>
|
||||
</div>
|
||||
</label>
|
||||
@else
|
||||
<label class="pc-filter-block">
|
||||
<span>{{ $filter['label'] }}</span>
|
||||
<select name="filters[{{ $filterKey }}]">
|
||||
<option value="">Все</option>
|
||||
@foreach ($filterOptions[$filterKey] ?? [] as $option)
|
||||
@php
|
||||
$optionValue = trim((string) $option);
|
||||
@endphp
|
||||
@continue($optionValue === '')
|
||||
<option value="{{ $optionValue }}" @selected(($appliedFilters[$filterKey] ?? '') === $optionValue)>
|
||||
{{ $optionValue }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
@endif
|
||||
@empty
|
||||
<p class="pc-muted">Для этой категории фильтры пока не заданы.</p>
|
||||
@endforelse
|
||||
<div class="pc-filter-actions">
|
||||
<button type="submit" class="pc-btn primary">Показать</button>
|
||||
<a class="pc-btn ghost" href="{{ route('catalog.category', $category) }}">Сбросить</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="pc-products-grid">
|
||||
@forelse ($products as $product)
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
@empty
|
||||
<div class="pc-card">Пока нет товаров в этой категории.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $products->links('partials.pagination') }}
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
69
resources/views/shop/checkout-payment.blade.php
Normal file
69
resources/views/shop/checkout-payment.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Корзина', 'url' => route('cart.index')],
|
||||
['label' => 'Данные получателя', 'url' => route('checkout.show')],
|
||||
['label' => 'Реквизиты для оплаты', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Реквизиты для оплаты</h2>
|
||||
<p>Переведите сумму заказа по реквизитам ниже и подтвердите оформление.</p>
|
||||
</div>
|
||||
|
||||
<div class="pc-cart-layout">
|
||||
<div class="pc-card">
|
||||
<h3>Данные получателя</h3>
|
||||
<p><strong>{{ $customer['customer_name'] }}</strong></p>
|
||||
<p>{{ $customer['email'] }}</p>
|
||||
@if (!empty($customer['phone']))
|
||||
<p>{{ $customer['phone'] }}</p>
|
||||
@endif
|
||||
<p>{{ $customer['address'] }}</p>
|
||||
@if (!empty($customer['comment']))
|
||||
<p>Комментарий: {{ $customer['comment'] }}</p>
|
||||
@endif
|
||||
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn ghost" href="{{ route('checkout.show') }}">Изменить данные</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="pc-card pc-cart-summary">
|
||||
<h3>Ваш заказ</h3>
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($items as $item)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $item['product']->name }} × {{ $item['quantity'] }}</span>
|
||||
<strong>{{ number_format($item['subtotal'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Товаров</span>
|
||||
<strong>{{ $itemsCount }}</strong>
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Итого</span>
|
||||
<strong>{{ number_format($total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@include('partials.payment-requisites', [
|
||||
'amount' => $total,
|
||||
'purpose' => 'Номер заказа будет присвоен после подтверждения',
|
||||
'showHelp' => true,
|
||||
])
|
||||
|
||||
<form method="post" action="{{ route('checkout.store') }}">
|
||||
@csrf
|
||||
<button class="pc-btn primary" type="submit">Подтвердить оформление заказа</button>
|
||||
</form>
|
||||
</section>
|
||||
@endsection
|
||||
32
resources/views/shop/checkout-success.blade.php
Normal file
32
resources/views/shop/checkout-success.blade.php
Normal file
@@ -0,0 +1,32 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Заказ оформлен', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-card">
|
||||
<h2>Заказ №{{ $order->id }} успешно оформлен</h2>
|
||||
<p>Мы приняли заказ в обработку. Статус заказа: <strong>{{ $order->status }}</strong>.</p>
|
||||
<p>Способ оплаты: <strong>{{ $order->payment_method_label }}</strong>.</p>
|
||||
<p>Сумма заказа: <strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>.</p>
|
||||
|
||||
@include('partials.payment-requisites', [
|
||||
'amount' => $order->total,
|
||||
'purpose' => 'Заказ #' . $order->id,
|
||||
'showHelp' => true,
|
||||
])
|
||||
|
||||
<div class="pc-product-actions">
|
||||
<a class="pc-btn primary" href="{{ route('catalog.index') }}">Продолжить покупки</a>
|
||||
@auth
|
||||
<a class="pc-btn ghost" href="{{ route('account.orders.show', $order) }}">Открыть заказ</a>
|
||||
@endauth
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
65
resources/views/shop/checkout.blade.php
Normal file
65
resources/views/shop/checkout.blade.php
Normal file
@@ -0,0 +1,65 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Корзина', 'url' => route('cart.index')],
|
||||
['label' => 'Данные получателя', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Данные получателя</h2>
|
||||
<p>Заполните контакты и перейдите на страницу с реквизитами для оплаты.</p>
|
||||
</div>
|
||||
|
||||
<div class="pc-cart-layout">
|
||||
<form class="pc-card pc-form" method="post" action="{{ route('checkout.prepare') }}">
|
||||
@csrf
|
||||
<label>
|
||||
Имя получателя
|
||||
<input type="text" name="customer_name" value="{{ old('customer_name', session('checkout.customer.customer_name', auth()->user()->name ?? '')) }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" value="{{ old('email', session('checkout.customer.email', auth()->user()->email ?? '')) }}" required>
|
||||
</label>
|
||||
<label>
|
||||
Телефон
|
||||
<input type="text" name="phone" value="{{ old('phone', session('checkout.customer.phone')) }}">
|
||||
</label>
|
||||
<label>
|
||||
Адрес доставки
|
||||
<textarea name="address" required>{{ old('address', session('checkout.customer.address')) }}</textarea>
|
||||
</label>
|
||||
<label>
|
||||
Комментарий к заказу
|
||||
<textarea name="comment">{{ old('comment', session('checkout.customer.comment')) }}</textarea>
|
||||
</label>
|
||||
<button class="pc-btn primary" type="submit">Перейти к реквизитам</button>
|
||||
</form>
|
||||
|
||||
<aside class="pc-card pc-cart-summary">
|
||||
<h3>Ваш заказ</h3>
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($items as $item)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $item['product']->name }} × {{ $item['quantity'] }}</span>
|
||||
<strong>{{ number_format($item['subtotal'], 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Товаров</span>
|
||||
<strong>{{ $itemsCount }}</strong>
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Итого</span>
|
||||
<strong>{{ number_format($total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
65
resources/views/shop/compare.blade.php
Normal file
65
resources/views/shop/compare.blade.php
Normal file
@@ -0,0 +1,65 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Сравнение', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Сравнение товаров</h2>
|
||||
</div>
|
||||
|
||||
@if ($products->isEmpty())
|
||||
<div class="pc-card">
|
||||
<h3>Список сравнения пуст</h3>
|
||||
<p>Добавьте товары в сравнение из карточек каталога.</p>
|
||||
<a class="pc-btn primary" href="{{ route('catalog.index') }}">Перейти в каталог</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="pc-compare-actions">
|
||||
<form method="post" action="{{ route('compare.clear') }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
@method('delete')
|
||||
<button class="pc-btn ghost" type="submit">Очистить сравнение</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="pc-products-grid">
|
||||
@foreach ($products as $product)
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($specKeys->isNotEmpty())
|
||||
<div class="pc-card pc-compare-table-wrap">
|
||||
<div class="pc-table-scroll">
|
||||
<table class="pc-compare-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Характеристика</th>
|
||||
@foreach ($products as $product)
|
||||
<th>{{ $product->name }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($specKeys as $key)
|
||||
<tr>
|
||||
<th>{{ $specLabels[$key] ?? $key }}</th>
|
||||
@foreach ($products as $product)
|
||||
<td>{{ data_get($product->specs, $key, '—') }}</td>
|
||||
@endforeach
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
30
resources/views/shop/favorites.blade.php
Normal file
30
resources/views/shop/favorites.blade.php
Normal file
@@ -0,0 +1,30 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Избранное', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Избранные товары</h2>
|
||||
</div>
|
||||
|
||||
@if ($products->isEmpty())
|
||||
<div class="pc-card">
|
||||
<h3>Список пуст</h3>
|
||||
<p>Добавьте товары в избранное из каталога или со страницы товара.</p>
|
||||
<a class="pc-btn primary" href="{{ route('catalog.index') }}">Перейти в каталог</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="pc-products-grid">
|
||||
@foreach ($products as $product)
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
71
resources/views/shop/home.blade.php
Normal file
71
resources/views/shop/home.blade.php
Normal file
@@ -0,0 +1,71 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('meta_title', 'Главная')
|
||||
@section('meta_description', 'Интернет-магазин комплектующих для ПК: процессоры, материнские платы, видеокарты, ноутбуки и периферия.')
|
||||
@section('meta_keywords', 'интернет-магазин пк, комплектующие, процессоры, видеокарты, ноутбуки')
|
||||
@section('meta_canonical', route('home'))
|
||||
|
||||
@section('content')
|
||||
<section class="pc-home-sliders">
|
||||
@include('partials.home-slider', [
|
||||
'slides' => $leftSlides,
|
||||
'sliderClass' => 'is-main',
|
||||
'fallbackTitle' => 'Собирайте ПК быстрее',
|
||||
'fallbackText' => 'Процессоры, материнские платы, видеокарты, ноутбуки и периферия в одном каталоге.',
|
||||
'fallbackUrl' => route('catalog.index'),
|
||||
'fallbackButton' => 'Перейти в каталог',
|
||||
])
|
||||
@include('partials.home-slider', [
|
||||
'slides' => $rightSlides,
|
||||
'sliderClass' => 'is-side',
|
||||
'fallbackTitle' => 'Доставка и оплата',
|
||||
'fallbackText' => 'Узнайте сроки доставки и способы оплаты заказа.',
|
||||
'fallbackUrl' => route('pages.shipping-payment'),
|
||||
'fallbackButton' => 'Подробнее',
|
||||
])
|
||||
</section>
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Категории</h2>
|
||||
</div>
|
||||
<div class="pc-grid pc-grid-4 pc-category-grid">
|
||||
@foreach ($categories as $category)
|
||||
<a class="pc-card pc-category-card pc-category-link" href="{{ route('catalog.category', $category) }}">
|
||||
<div class="pc-category-image" role="img" aria-label="{{ $category->name }}"></div>
|
||||
<h3 class="pc-category-title">{{ $category->name }}</h3>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@include('partials.product-carousel', [
|
||||
'title' => 'Популярные товары',
|
||||
'products' => $featured,
|
||||
'emptyText' => 'Пока нет популярных товаров.',
|
||||
])
|
||||
|
||||
@include('partials.product-carousel', [
|
||||
'title' => 'Новые товары',
|
||||
'products' => $newProducts,
|
||||
'emptyText' => 'Пока нет новых товаров.',
|
||||
])
|
||||
|
||||
<section class="pc-grid pc-grid-3">
|
||||
<a class="pc-card pc-category-link" href="{{ route('pages.shipping-payment') }}">
|
||||
<div class="pc-card-meta">Сервис</div>
|
||||
<h3>Доставка и оплата</h3>
|
||||
<p>Условия доставки, способы оплаты и сроки отправки.</p>
|
||||
</a>
|
||||
<a class="pc-card pc-category-link" href="{{ route('pages.about') }}">
|
||||
<div class="pc-card-meta">Компания</div>
|
||||
<h3>О нас</h3>
|
||||
<p>Чем занимаемся и как помогаем выбрать комплектующие.</p>
|
||||
</a>
|
||||
<a class="pc-card pc-category-link" href="{{ route('pages.contacts') }}">
|
||||
<div class="pc-card-meta">Поддержка</div>
|
||||
<h3>Контакты</h3>
|
||||
<p>Свяжитесь с нами для консультации по вашей сборке.</p>
|
||||
</a>
|
||||
</section>
|
||||
@endsection
|
||||
60
resources/views/shop/order.blade.php
Normal file
60
resources/views/shop/order.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Личный кабинет', 'url' => route('account')],
|
||||
['label' => 'Заказ #' . $order->id, 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Заказ #{{ $order->id }}</h2>
|
||||
<p>Статус: <strong>{{ $order->status }}</strong></p>
|
||||
<p>Способ оплаты: <strong>{{ $order->payment_method_label }}</strong></p>
|
||||
</div>
|
||||
|
||||
<div class="pc-grid pc-grid-2">
|
||||
<div class="pc-card">
|
||||
<h3>Состав заказа</h3>
|
||||
<div class="pc-account-orders">
|
||||
@foreach ($order->items as $item)
|
||||
<div class="pc-account-order">
|
||||
<span>{{ $item->name }} × {{ $item->quantity }}</span>
|
||||
<strong>{{ number_format($item->subtotal, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="pc-cart-summary-row">
|
||||
<span>Итого</span>
|
||||
<strong>{{ number_format($order->total, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-card">
|
||||
<h3>Данные получателя</h3>
|
||||
<p><strong>{{ $order->customer_name }}</strong></p>
|
||||
<p>{{ $order->email }}</p>
|
||||
@if ($order->phone)
|
||||
<p>{{ $order->phone }}</p>
|
||||
@endif
|
||||
@if ($order->address)
|
||||
<p>{{ $order->address }}</p>
|
||||
@endif
|
||||
@if ($order->comment)
|
||||
<p>Комментарий: {{ $order->comment }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!in_array($order->status, ['paid', 'shipped', 'completed'], true))
|
||||
@include('partials.payment-requisites', [
|
||||
'amount' => $order->total,
|
||||
'purpose' => 'Заказ #' . $order->id,
|
||||
'showHelp' => true,
|
||||
])
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
221
resources/views/shop/product.blade.php
Normal file
221
resources/views/shop/product.blade.php
Normal file
@@ -0,0 +1,221 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@php
|
||||
$productGallery = $product->gallery_urls;
|
||||
$productImage = $productGallery[0] ?? ($product->image_url ?: config('seo.default_image'));
|
||||
$productImageUrl = str_starts_with($productImage, 'http://') || str_starts_with($productImage, 'https://')
|
||||
? $productImage
|
||||
: url($productImage);
|
||||
$productSchemaImages = array_map(
|
||||
fn (string $image) => str_starts_with($image, 'http://') || str_starts_with($image, 'https://')
|
||||
? $image
|
||||
: url($image),
|
||||
$productGallery !== [] ? array_values($productGallery) : [$productImageUrl]
|
||||
);
|
||||
|
||||
$manufacturer = trim((string) ($product->specs['manufacturer'] ?? ''));
|
||||
$conditionRaw = mb_strtolower(trim((string) ($product->specs['condition'] ?? '')));
|
||||
$itemCondition = str_contains($conditionRaw, 'б/у') || str_contains($conditionRaw, 'used')
|
||||
? 'https://schema.org/UsedCondition'
|
||||
: 'https://schema.org/NewCondition';
|
||||
|
||||
$productSchema = [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'Product',
|
||||
'name' => $product->name,
|
||||
'description' => $product->short_description ?: ($product->description ?: "Купить {$product->name} по выгодной цене."),
|
||||
'sku' => $product->sku ?: null,
|
||||
'category' => $product->category?->name,
|
||||
'image' => $productSchemaImages,
|
||||
'url' => route('products.show', $product),
|
||||
'offers' => [
|
||||
'@type' => 'Offer',
|
||||
'priceCurrency' => config('shop.currency_code', 'RUB'),
|
||||
'price' => (string) $product->price,
|
||||
'availability' => $product->stock > 0 ? 'https://schema.org/InStock' : 'https://schema.org/OutOfStock',
|
||||
'itemCondition' => $itemCondition,
|
||||
'url' => route('products.show', $product),
|
||||
],
|
||||
];
|
||||
|
||||
if ($manufacturer !== '') {
|
||||
$productSchema['brand'] = [
|
||||
'@type' => 'Brand',
|
||||
'name' => $manufacturer,
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
|
||||
@section('meta_title', $product->name)
|
||||
@section('meta_description', \Illuminate\Support\Str::limit(strip_tags($product->short_description ?: ($product->description ?: "Купить {$product->name} по выгодной цене.")), 160))
|
||||
@section('meta_keywords', $product->name . ', ' . ($product->category?->name ?? 'товар') . ', купить')
|
||||
@section('meta_canonical', route('products.show', $product))
|
||||
@section('meta_image', $productImageUrl)
|
||||
@section('meta_image_alt', $product->name)
|
||||
@section('meta_og_type', 'product')
|
||||
|
||||
@push('structured_data')
|
||||
<script type="application/ld+json">
|
||||
@json($productSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$favoriteIds = array_map('intval', (array) session('favorites', []));
|
||||
$compareIds = array_map('intval', (array) session('compare', []));
|
||||
$isFavorite = in_array($product->id, $favoriteIds, true);
|
||||
$isCompared = in_array($product->id, $compareIds, true);
|
||||
$cartItems = (array) session('cart', []);
|
||||
$isInCart = isset($cartItems[$product->id]);
|
||||
@endphp
|
||||
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Каталог', 'url' => route('catalog.index')],
|
||||
['label' => $product->category?->name ?? 'Категория', 'url' => $product->category ? route('catalog.category', $product->category) : null],
|
||||
['label' => $product->name, 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section pc-product-page">
|
||||
<div class="pc-product-hero">
|
||||
<div class="pc-product-gallery" data-product-gallery>
|
||||
@if ($productGallery !== [])
|
||||
<img
|
||||
class="pc-product-image-lg"
|
||||
src="{{ $productGallery[0] }}"
|
||||
alt="{{ $product->name }}"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
data-product-gallery-main
|
||||
>
|
||||
@else
|
||||
<div class="pc-product-image-lg" role="img" aria-label="{{ $product->name }}"></div>
|
||||
@endif
|
||||
|
||||
@if (count($productGallery) > 1)
|
||||
<div class="pc-product-thumbs" aria-label="Дополнительные изображения товара">
|
||||
@foreach ($productGallery as $imageUrl)
|
||||
<button
|
||||
class="pc-product-thumb {{ $loop->first ? 'is-active' : '' }}"
|
||||
type="button"
|
||||
data-product-gallery-thumb
|
||||
data-image-src="{{ $imageUrl }}"
|
||||
data-image-alt="{{ $product->name }} - фото {{ $loop->iteration }}"
|
||||
aria-label="Показать фото {{ $loop->iteration }}"
|
||||
aria-pressed="{{ $loop->first ? 'true' : 'false' }}"
|
||||
>
|
||||
<img
|
||||
src="{{ $imageUrl }}"
|
||||
alt="{{ $product->name }} - миниатюра {{ $loop->iteration }}"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pc-product-info">
|
||||
<h1>{{ $product->name }}</h1>
|
||||
<p class="pc-muted">{{ $product->short_description }}</p>
|
||||
<div class="pc-product-badges">
|
||||
@if ($product->sku)
|
||||
<span class="pc-sku">Артикул: {{ $product->sku }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pc-product-price">
|
||||
<strong>{{ number_format($product->price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</strong>
|
||||
@if (!empty($product->old_price))
|
||||
<span class="pc-product-old">{{ number_format($product->old_price, 0, '.', ' ') }} {{ config('shop.currency_symbol', '₽') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="pc-product-actions">
|
||||
@if ($product->stock > 0)
|
||||
<form method="post" action="{{ route('cart.add', $product) }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-btn primary {{ $isInCart ? 'is-active' : '' }}" type="submit">
|
||||
{{ $isInCart ? 'В корзине' : 'В корзину' }}
|
||||
</button>
|
||||
</form>
|
||||
@else
|
||||
<button class="pc-btn primary" type="button" disabled>Нет в наличии</button>
|
||||
@endif
|
||||
<form method="post" action="{{ route('favorites.toggle', $product) }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-btn ghost {{ $isFavorite ? 'is-active' : '' }}" type="submit">
|
||||
{{ $isFavorite ? 'Убрать из избранного' : 'В избранное' }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ route('compare.toggle', $product) }}" data-preserve-scroll="true">
|
||||
@csrf
|
||||
<button class="pc-btn ghost {{ $isCompared ? 'is-active' : '' }}" type="submit">
|
||||
{{ $isCompared ? 'Убрать из сравнения' : 'В сравнение' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pc-tabs">
|
||||
<input type="radio" name="product-tabs" id="tab-specs" checked>
|
||||
<input type="radio" name="product-tabs" id="tab-desc">
|
||||
<input type="radio" name="product-tabs" id="tab-shipping">
|
||||
<input type="radio" name="product-tabs" id="tab-payment">
|
||||
|
||||
<div class="pc-tab-labels">
|
||||
<label for="tab-specs">Характеристики</label>
|
||||
<label for="tab-desc">Описание</label>
|
||||
<label for="tab-shipping">Доставка</label>
|
||||
<label for="tab-payment">Оплата</label>
|
||||
</div>
|
||||
|
||||
<div class="pc-tab-content">
|
||||
<section class="pc-tab-panel pc-tab-specs">
|
||||
<div class="pc-specs-grid">
|
||||
@forelse (($product->specs ?? []) as $key => $value)
|
||||
<div class="pc-spec-row">
|
||||
<span>{{ $specLabels[$key] ?? str_replace('_', ' ', $key) }}</span>
|
||||
<strong>{{ $value }}</strong>
|
||||
</div>
|
||||
@empty
|
||||
<p class="pc-muted">Характеристики еще не добавлены.</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
<section class="pc-tab-panel pc-tab-desc">
|
||||
<p class="pc-muted">{{ $product->description ?? 'Описание товара будет добавлено позже.' }}</p>
|
||||
</section>
|
||||
<section class="pc-tab-panel pc-tab-shipping">
|
||||
<ul class="pc-list">
|
||||
<li>Доставка курьером по городу</li>
|
||||
<li>Самовывоз из пункта выдачи</li>
|
||||
<li>Отправка по стране 1-3 дня</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="pc-tab-panel pc-tab-payment">
|
||||
<ul class="pc-list">
|
||||
<li>Оплата картой онлайн</li>
|
||||
<li>Банковский перевод</li>
|
||||
<li>Рассрочка на крупные заказы</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if ($related->isNotEmpty())
|
||||
<section class="pc-section">
|
||||
<div class="pc-section-title">
|
||||
<h2>Что еще посмотреть в этой категории</h2>
|
||||
</div>
|
||||
<div class="pc-products-grid">
|
||||
@foreach ($related as $item)
|
||||
@include('partials.product-card', ['product' => $item])
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
||||
@endif
|
||||
@endsection
|
||||
112
resources/views/shop/search.blade.php
Normal file
112
resources/views/shop/search.blade.php
Normal file
@@ -0,0 +1,112 @@
|
||||
@extends('layouts.shop')
|
||||
|
||||
@php
|
||||
$searchItemList = collect($products->items())
|
||||
->values()
|
||||
->map(fn ($product, $index) => [
|
||||
'@type' => 'ListItem',
|
||||
'position' => $index + 1,
|
||||
'url' => route('products.show', $product),
|
||||
'name' => $product->name,
|
||||
])
|
||||
->all();
|
||||
$searchSchema = $searchQuery !== ''
|
||||
? [
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'SearchResultsPage',
|
||||
'name' => "Результаты поиска: {$searchQuery}",
|
||||
'url' => route('search.index', ['q' => $searchQuery]),
|
||||
'mainEntity' => [
|
||||
'@type' => 'ItemList',
|
||||
'numberOfItems' => $products->total(),
|
||||
'itemListElement' => $searchItemList,
|
||||
],
|
||||
]
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
@section('meta_title', $searchQuery !== '' ? "Поиск: {$searchQuery}" : 'Поиск товаров')
|
||||
@section(
|
||||
'meta_description',
|
||||
$searchQuery !== ''
|
||||
? "Найденные товары по запросу «{$searchQuery}». Выберите подходящий товар и откройте подробную карточку."
|
||||
: 'Поиск товаров по наименованию: процессоры, видеокарты, материнские платы, ноутбуки и периферия.'
|
||||
)
|
||||
@section('meta_keywords', 'поиск товаров, результаты поиска, комплектующие пк, ноутбуки')
|
||||
@section('meta_canonical', route('search.index'))
|
||||
@section('meta_robots', 'noindex,follow')
|
||||
|
||||
@push('structured_data')
|
||||
@if ($searchSchema !== null)
|
||||
<script type="application/ld+json">
|
||||
@json($searchSchema, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
|
||||
</script>
|
||||
@endif
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
@include('partials.breadcrumbs', [
|
||||
'items' => [
|
||||
['label' => 'Главная', 'url' => route('home')],
|
||||
['label' => 'Поиск', 'url' => null],
|
||||
],
|
||||
])
|
||||
|
||||
<section class="pc-section">
|
||||
<div class="pc-category-toolbar">
|
||||
<div class="pc-section-title">
|
||||
<h2>{{ $searchQuery !== '' ? 'Результаты поиска' : 'Поиск товаров' }}</h2>
|
||||
<p>
|
||||
@if ($searchQuery !== '')
|
||||
Запрос: "{{ $searchQuery }}"
|
||||
@else
|
||||
Введите название товара, чтобы увидеть найденные позиции.
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($searchQuery !== '')
|
||||
<form class="pc-sort-form" method="get" action="{{ route('search.index') }}">
|
||||
<input type="hidden" name="q" value="{{ $searchQuery }}">
|
||||
<label for="sort">Сортировка:</label>
|
||||
<select id="sort" name="sort" onchange="this.form.submit()">
|
||||
<option value="newest" @selected($sort === 'newest')>Сначала новые</option>
|
||||
<option value="price_asc" @selected($sort === 'price_asc')>Сначала дешевле</option>
|
||||
<option value="price_desc" @selected($sort === 'price_desc')>Сначала дороже</option>
|
||||
<option value="name_asc" @selected($sort === 'name_asc')>По названию</option>
|
||||
</select>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form class="pc-search-page-form" method="get" action="{{ route('search.index') }}">
|
||||
<div class="pc-search">
|
||||
<input type="text" name="q" placeholder="Например, Ryzen 7 или RTX 4060" value="{{ $searchQuery }}">
|
||||
</div>
|
||||
<button class="pc-btn primary" type="submit">Найти</button>
|
||||
@if ($searchQuery !== '')
|
||||
<a class="pc-btn ghost" href="{{ route('search.index') }}">Очистить</a>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
@if ($searchQuery === '')
|
||||
<div class="pc-card">
|
||||
Введите запрос в строку поиска, чтобы открыть список найденных товаров.
|
||||
</div>
|
||||
@else
|
||||
<p class="pc-muted">Найдено товаров: <strong>{{ $products->total() }}</strong></p>
|
||||
|
||||
<div class="pc-products-grid">
|
||||
@forelse ($products as $product)
|
||||
@include('partials.product-card', ['product' => $product])
|
||||
@empty
|
||||
<div class="pc-card">По вашему запросу ничего не найдено.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="pc-pagination">
|
||||
{{ $products->links('partials.pagination') }}
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user