This commit is contained in:
2026-04-06 18:44:02 +05:00
commit d775fbb9ba
431 changed files with 31234 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
@extends('layouts.app')
@section('title', 'О нас')
@section('content')
<div class="container py-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
<li class="breadcrumb-item active">О нас</li>
</ol>
</nav>
{{-- Основной блок --}}
<div class="row justify-content-center">
<div class="col-lg-8 text-center pt-3">
<p class="fs-4">
TailAndPaws это команда профессионалов, объединенных любовью к животным.
</p>
</div>
</div>
{{-- Наши преимущества --}}
<div class="row mt-5">
<div class="col-md-4 mb-4">
<div class="text-center p-4">
<i class="bi bi-truck fs-1 tw-text-dark-green"></i>
<h4 class="mt-3">Быстрая доставка</h4>
<p class="text-muted">Доставляем заказы по всей России</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="text-center p-4">
<i class="bi bi-shield-check fs-1 tw-text-dark-green"></i>
<h4 class="mt-3">Гарантия качества</h4>
<p class="text-muted">Только сертифицированные товары</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="text-center p-4">
<i class="bi bi-headset fs-1 tw-text-dark-green"></i>
<h4 class="mt-3">Поддержка 24/7</h4>
<p class="text-muted">Всегда готовы помочь</p>
</div>
</div>
</div>
{{-- История --}}
<div class="row mt-5">
<div class="col-12">
<div class="tw-bg-orange tw-text-light-gray p-5 rounded-0">
<div class="row">
<div class="col-md-10 mx-auto d-flex flex-column gap-4">
<p><strong>Миссия компании</strong> предоставлять владельцам домашних животных доступ к качественным, безопасным и проверенным товарам по доступным ценам.</p>
<p><strong>Что мы предлагаем:</strong></p>
<ul>
<li>--- Широкий ассортимент кормов, амуниции, игрушек и аксессуаров</li>
<li>--- Продукцию только от проверенных производителей</li>
<li>--- Регулярное обновление ассортимента</li>
<li>--- Доставку по всей России</li>
<li>--- Консультации специалистов по выбору товаров</li>
</ul>
<p><strong>Наши приоритеты:</strong></p>
<ul>
<li>--- Качество каждого товара на полке</li>
<li>--- Честные цены без скрытых наценок</li>
<li>--- Быстрая обработка и отправка заказов</li>
<li>--- Открытость и прозрачность перед клиентами</li>
</ul>
<p class="mb-0 mt-3 fst-italic text-center">TailAndPaws забота, которой доверяют.</p>
</div>
</div>
</div>
</div>
</div>
{{-- Наши партнеры (топ бренды) --}}
@if($topBrands->count())
<div class="row mt-5">
<div class="col-12">
<h2 class="text-center fs-4 mb-4">Наши партнеры</h2>
<p class="text-center text-muted mb-5">Мы сотрудничаем с лучшими производителями товаров для животных</p>
<div class="row g-4 justify-content-center">
@foreach($topBrands as $brand)
<div class="col-6 col-md-3 col-lg-2">
<div class="d-flex flex-column align-items-center p-3">
@if($brand->logo)
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
class="img-fluid"
style="max-height: 80px; object-fit: contain;">
@else
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center mx-auto" style="width: 80px; height: 80px;">
<i class="bi bi-building fs-1 text-secondary"></i>
</div>
@endif
<p class="mt-2 mb-0 fw-semibold">{{ $brand->name }}</p>
</div>
</div>
@endforeach
</div>
</div>
</div>
@endif
<div class="row mt-5">
<div class="col-12">
<h2 class="text-center fs-4 mb-4">Где нас найти</h2>
</div>
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h4 class="mb-3">Наши контакты</h4>
<ul class="list-unstyled">
<li class="mb-3">
<i class="bi bi-geo-alt tw-text-dark-green me-2"></i>
<strong>Адрес:</strong><br>
{{ $menuContacts->address ?? 'г. Челябинск, ул. Дружбы, д. 15' }}
</li>
<li class="mb-3">
<i class="bi bi-telephone tw-text-dark-green me-2"></i>
<strong>Телефон:</strong><br>
<a href="tel:{{ $menuContacts->phone ?? '+7(985)-070-56-33' }}" class="text-decoration-none">
{{ $menuContacts->phone ?? '+7(985)-070-56-33' }}
</a>
</li>
<li class="mb-3">
<i class="bi bi-envelope tw-text-dark-green me-2"></i>
<strong>Email:</strong><br>
<a href="mailto:{{ $menuContacts->email ?? 'tailandpaws_info@gmail.com' }}" class="text-decoration-none">
{{ $menuContacts->email ?? 'tailandpaws_info@gmail.com' }}
</a>
</li>
<li class="mb-3">
<i class="bi bi-clock tw-text-dark-green me-2"></i>
<strong>Режим работы:</strong><br>
{{ $menuContacts->work_hours ?? 'Пн-Пт: 10:00 - 18:00, Сб-Вс: выходной' }}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100 overflow-hidden">
<div class="card-body p-0">
<div style="position:relative;overflow:hidden; height: 100%; min-height: 350px;">
<iframe
src="https://yandex.com/map-widget/v1/?ll=61.445115%2C55.142641&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NjAyNDI0ORJA0KDQvtGB0YHQuNGPLCDQp9C10LvRj9Cx0LjQvdGB0LosINGD0LvQuNGG0LAg0JPQsNCz0LDRgNC40L3QsCwgNyIKDUDHdUIVYpJcQg%2C%2C&z=17.99"
width="100%"
height="350"
frameborder="0"
allowfullscreen="true"
style="border:0;">
</iframe>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,160 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="brands-table col-12">
<div class="card">
<div class="card-header">
<h5>Бренды</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<th>Название</th>
<th>Описание</th>
<th>Лого</th>
<th>Сайт</th>
<th>Страна</th>
<th class="text-center">Активен?</th>
<th class="text-center">Действия</th>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.brands.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<textarea name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
rows="2" placeholder="Описание">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="logo"
class="form-control form-control-sm @error('logo') is-invalid @enderror"
accept="image/*">
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="url" name="website"
class="form-control form-control-sm @error('website') is-invalid @enderror"
placeholder="https://" value="{{ old('website') }}">
@error('website')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="country"
class="form-control form-control-sm @error('country') is-invalid @enderror"
placeholder="Страна" value="{{ old('country') }}">
@error('country')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<input type="checkbox" name="is_active" value="1" class="form-check-input"
{{ old('is_active', true) ? 'checked' : '' }}>
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($brands as $brand)
<tr>
<td>{{ $brand->name }}</td>
<td style="max-width: 200px;">
@if ($brand->description)
{{ Str::limit($brand->description, 50) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->logo)
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
style="max-height: 40px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->website)
<a href="{{ $brand->website }}" target="_blank">{{ Str::limit($brand->website, 30) }}</a>
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($brand->country)
{{ $brand->country }}
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-center">
@if ($brand->is_active)
<i class="bi bi-check-circle text-success"></i>
@else
<i class="bi bi-x-circle text-danger"></i>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.brands.edit', $brand->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.brands.destroy', $brand->id) }}" method="post"
onsubmit="return confirm('Удалить бренд «{{ $brand->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
@if(method_exists($brands, 'hasPages') && $brands->hasPages())
{{ $brands->links() }}
@endif
</div>
@endsection

View File

@@ -0,0 +1,101 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование бренда: {{ $brand->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.brands.update', $brand->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $brand->name) }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Страна</label>
<input type="text" name="country"
class="form-control @error('country') is-invalid @enderror"
value="{{ old('country', $brand->country) }}">
@error('country')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea name="description"
class="form-control @error('description') is-invalid @enderror"
rows="4">{{ old('description', $brand->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Сайт</label>
<input type="url" name="website"
class="form-control @error('website') is-invalid @enderror"
value="{{ old('website', $brand->website) }}"
placeholder="https://">
@error('website')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Логотип</label>
@if($brand->logo)
<div class="mb-2">
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="logo"
class="form-control @error('logo') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $brand->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">
Активен
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-orange p-2">
<img src="{{ asset('assets/images/icons/save-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
</button>
<a href="{{ route('admin.brands') }}" class="btn btn-dark-green p-2 d-flex gap-2 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,245 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="categories-table col-12 mb-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Категории</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<th style="width: 60px">Порядок</th>
<th>Название</th>
<th>Описание</th>
<th>Иконка</th>
<th>Изображение</th>
<th class="text-center">Активна?</th>
<th class="text-center">Действия</th>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.categories.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<td>
<select name="parent_id" class="form-select form-select-sm @error('parent_id') is-invalid @enderror">
<option value="">Корневая категория</option>
@foreach($allCategories as $cat)
<option value="{{ $cat->id }}" {{ old('parent_id') == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('parent_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}" required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<textarea name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
rows="2" placeholder="Описание">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="icon"
class="form-control form-control-sm @error('icon') is-invalid @enderror"
accept="image/*">
@error('icon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="file" name="image"
class="form-control form-control-sm @error('image') is-invalid @enderror"
accept="image/*">
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<input type="checkbox" name="is_active" value="1" class="form-check-input"
{{ old('is_active', true) ? 'checked' : '' }}>
@error('is_active')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить категорию">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($categories as $index => $category)
<tr class="category-row" data-level="{{ $category->level ?? 0 }}">
<td class="text-nowrap">
<div class="d-flex gap-1">
@php
$canMoveUp = false;
for ($i = 0; $i < $index; $i++) {
if ($categories[$i]->parent_id == $category->parent_id) {
$canMoveUp = true;
break;
}
}
@endphp
@if($canMoveUp)
<form action="{{ route('admin.categories.move-up', $category->id) }}" method="POST" class="d-inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-link p-0 text-dark" title="Вверх">
<i class="bi bi-arrow-up-short fs-4"></i>
</button>
</form>
@else
<span style="width: 28px"></span>
@endif
@php
$canMoveDown = false;
for ($i = $index + 1; $i < count($categories); $i++) {
if ($categories[$i]->parent_id == $category->parent_id) {
$canMoveDown = true;
break;
}
}
@endphp
@if($canMoveDown)
<form action="{{ route('admin.categories.move-down', $category->id) }}" method="POST" class="d-inline">
@csrf
@method('PATCH')
<button type="submit" class="btn btn-sm btn-link p-0 text-dark" title="Вниз">
<i class="bi bi-arrow-down-short fs-4"></i>
</button>
</form>
@else
<span style="width: 28px"></span>
@endif
</div>
</td>
<td>
<div class="category-name" style="display: flex; align-items: center;">
@for($i = 0; $i < ($category->level ?? 0); $i++)
<span style="width: 24px; display: inline-block;"></span>
@endfor
@if(($category->level ?? 0) > 0)
@php
$hasNext = false;
if (isset($categories[$index + 1])) {
$nextLevel = $categories[$index + 1]->level ?? 0;
$currentLevel = $category->level ?? 0;
if ($nextLevel >= $currentLevel) {
$hasNext = true;
}
}
@endphp
@if($hasNext)
<span class="text-muted" style="font-family: monospace;">├─ </span>
@else
<span class="text-muted" style="font-family: monospace;">└─ </span>
@endif
@endif
@if($category->children->count() > 0)
<i class="bi bi-folder-fill tw-text-orange me-2"></i>
@else
<i class="bi bi-file-earmark me-2 text-secondary"></i>
@endif
<strong>{{ $category->name }}</strong>
@if($category->children->count())
<span class="badge bg-secondary ms-2">{{ $category->children->count() }}</span>
@endif
</div>
</td>
<td style="max-width: 200px;">
@if($category->description)
{{ Str::limit($category->description, 50) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($category->icon)
<img src="{{ asset('assets/images/categories/icons/' . $category->icon) }}" alt="{{ $category->name }}" style="max-height: 30px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if ($category->image)
<img src="{{ asset('assets/images/categories/images/' . $category->image) }}" alt="{{ $category->name }}" style="max-height: 30px;">
@else
<span class="text-muted"></span>
@endif
</td>
<td class="text-center">
@if ($category->is_active)
<i class="bi bi-check-circle text-success"></i>
@else
<i class="bi bi-x-circle text-danger"></i>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.categories.edit', $category->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.categories.destroy', $category->id) }}" method="post"
onsubmit="return confirm('Удалить категорию «{{ $category->name }}»? Все подкатегории также будут удалены.');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
@if(method_exists($categories, 'hasPages') && $categories->hasPages())
<div class="ps-2 pe-2">
{{ $categories->links() }}
</div>
@endif
@endsection

View File

@@ -0,0 +1,132 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование категории: {{ $category->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.categories.update', $category->id) }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-12 mb-4">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $category->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-12">
<select name="parent_id" class="form-select @error('parent_id') is-invalid @enderror">
<option value="">Корневая категория</option>
@foreach($allCategories as $cat)
<option value="{{ $cat->id }}"
{{ old('parent_id', $category->parent_id) == $cat->id ? 'selected' : '' }}
{{ $cat->disabled ? 'disabled' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
@if($cat->disabled) (недоступно) @endif
</option>
@endforeach
</select>
@error('parent_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Описание</label>
<textarea name="description"
class="form-control @error('description') is-invalid @enderror"
rows="4">{{ old('description', $category->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Иконка</label>
<div class="form-check mb-2">
<input type="checkbox" name="remove_icon" value="1"
class="form-check-input" id="remove_icon"
{{ old('remove_icon') ? 'checked' : '' }}>
<label class="form-check-label" for="remove_icon">
Удалить иконку
</label>
</div>
@if($category->icon)
<div class="mb-2">
<img src="{{ asset('assets/images/categories/icons/' . $category->icon) }}"
alt="{{ $category->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="icon"
class="form-control @error('icon') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('icon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Изображение</label>
<div class="form-check mb-2">
<input type="checkbox" name="remove_image" value="1"
class="form-check-input" id="remove_image"
{{ old('remove_image') ? 'checked' : '' }}>
<label class="form-check-label" for="remove_image">
Удалить изображение
</label>
</div>
@if($category->image)
<div class="mb-2">
<img src="{{ asset('assets/images/categories/images/' . $category->image) }}"
alt="{{ $category->name }}"
style="max-height: 60px;">
</div>
@endif
<input type="file" name="image"
class="form-control @error('image') is-invalid @enderror"
accept="image/*">
<small class="text-muted">Оставьте пустым, чтобы не менять</small>
@error('image')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $category->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">
Активен
</label>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-orange p-2">
<img src="{{ asset('assets/images/icons/save-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
</button>
<a href="{{ route('admin.categories') }}" class="btn btn-dark-green p-2 d-flex gap-2 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Обновить" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,347 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="card">
<div class="card-header">
<h5>Редактирование данных сайта</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.contacts.update') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<ul class="nav nav-tabs mb-4" id="contactTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab">
Основные данные
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="images-tab" data-bs-toggle="tab" data-bs-target="#images" type="button" role="tab">
Логотип и фавикон
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contacts-tab" data-bs-toggle="tab" data-bs-target="#contacts" type="button" role="tab">
Контакты
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="social-tab" data-bs-toggle="tab" data-bs-target="#social" type="button" role="tab">
Социальные сети
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="seo-tab" data-bs-toggle="tab" data-bs-target="#seo" type="button" role="tab">
SEO
</button>
</li>
</ul>
<div class="tab-content" id="contactTabsContent">
{{-- Основные данные --}}
<div class="tab-pane fade show active" id="basic" role="tabpanel">
<div class="mb-3">
<label class="form-label">Название сайта <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $contact->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание сайта</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $contact->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Режим работы</label>
<input type="text"
class="form-control @error('work_hours') is-invalid @enderror"
name="work_hours"
value="{{ old('work_hours', $contact->work_hours) }}"
placeholder="Например: Пн-Пт: 10:00 - 18:00, Сб-Вс: выходной">
@error('work_hours')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Логотип и фавикон --}}
<div class="tab-pane fade" id="images" role="tabpanel">
<div class="mb-4">
<label class="form-label">Логотип</label>
<div class="row align-items-end">
<div class="col-md-6">
<input type="file"
class="form-control @error('logo') is-invalid @enderror"
name="logo"
id="logo-input"
accept="image/jpeg,image/png,image/jpg,image/gif,image/svg+xml,image/webp">
@error('logo')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="text-muted">Рекомендуемый размер: 200x200px. Максимум 2MB</small>
</div>
<div class="col-md-6">
@if($contact->logo)
<div class="current-image">
<p class="mb-2">Текущий логотип:</p>
<img src="{{ asset('assets/images/logo/' . $contact->logo) }}"
alt="Логотип"
id="logo-preview"
style="max-height: 100px; border: 1px solid #ddd; padding: 5px; border-radius: 4px;">
</div>
@else
<div class="current-image">
<p class="mb-2 text-muted">Логотип не загружен</p>
<img src=""
id="logo-preview"
style="max-height: 100px; display: none;">
</div>
@endif
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Фавикон (иконка вкладки браузера)</label>
<div class="row align-items-end">
<div class="col-md-6">
<input type="file"
class="form-control @error('favicon') is-invalid @enderror"
name="favicon"
id="favicon-input"
accept="image/x-icon,image/png,image/svg+xml">
@error('favicon')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="text-muted">Рекомендуемый размер: 16x16px или 32x32px. Максимум 1MB</small>
</div>
<div class="col-md-6">
@if($contact->favicon)
<div class="current-image">
<p class="mb-2">Текущий фавикон:</p>
<img src="{{ asset('assets/images/logo/' . $contact->favicon) }}"
alt="Фавикон"
id="favicon-preview"
style="max-height: 32px; border: 1px solid #ddd; padding: 2px; border-radius: 4px;">
</div>
@else
<div class="current-image">
<p class="mb-2 text-muted">Фавикон не загружен</p>
<img src=""
id="favicon-preview"
style="max-height: 32px; display: none;">
</div>
@endif
</div>
</div>
</div>
</div>
{{-- Контакты --}}
<div class="tab-pane fade" id="contacts" role="tabpanel">
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone"
value="{{ old('phone', $contact->phone) }}"
placeholder="+7(000)-000-00-00">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email', $contact->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Адрес</label>
<input type="text"
class="form-control @error('address') is-invalid @enderror"
name="address"
value="{{ old('address', $contact->address) }}">
@error('address')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Социальные сети --}}
<div class="tab-pane fade" id="social" role="tabpanel">
<div class="mb-3">
<label class="form-label">Telegram</label>
<div class="input-group">
<span class="input-group-text">t.me/</span>
<input type="text"
class="form-control @error('telegram') is-invalid @enderror"
name="telegram"
value="{{ old('telegram', $contact->telegram) }}"
placeholder="username">
</div>
@error('telegram')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">WhatsApp</label>
<div class="input-group">
<span class="input-group-text">wa.me/</span>
<input type="text"
class="form-control @error('whatsapp') is-invalid @enderror"
name="whatsapp"
value="{{ old('whatsapp', $contact->whatsapp) }}"
placeholder="79991234567">
</div>
@error('whatsapp')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">ВКонтакте</label>
<div class="input-group">
<span class="input-group-text">vk.com/</span>
<input type="text"
class="form-control @error('vkontakte') is-invalid @enderror"
name="vkontakte"
value="{{ old('vkontakte', $contact->vkontakte) }}"
placeholder="club00000000">
</div>
@error('vkontakte')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- SEO --}}
<div class="tab-pane fade" id="seo" role="tabpanel">
<div class="mb-3">
<label class="form-label">Meta Title (заголовок страницы)</label>
<input type="text"
class="form-control @error('meta_title') is-invalid @enderror"
name="meta_title"
value="{{ old('meta_title', $contact->meta_title) }}">
<small class="text-muted">Рекомендуемая длина: 50-60 символов</small>
@error('meta_title')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Meta Description (описание страницы)</label>
<textarea class="form-control @error('meta_description') is-invalid @enderror"
name="meta_description"
rows="3">{{ old('meta_description', $contact->meta_description) }}</textarea>
<small class="text-muted">Рекомендуемая длина: 150-160 символов</small>
@error('meta_description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords (ключевые слова)</label>
<input type="text"
class="form-control @error('meta_keywords') is-invalid @enderror"
name="meta_keywords"
value="{{ old('meta_keywords', $contact->meta_keywords) }}"
placeholder="магазин, зоотовары, корм, игрушки">
<small class="text-muted">Ключевые слова через запятую</small>
@error('meta_keywords')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить изменения
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script>
document.addEventListener('DOMContentLoaded', function() {
const logoInput = document.getElementById('logo-input');
const logoPreview = document.getElementById('logo-preview');
if (logoInput) {
logoInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
logoPreview.src = event.target.result;
logoPreview.style.display = 'block';
if (logoPreview.closest('.current-image')) {
logoPreview.closest('.current-image').querySelector('.text-muted')?.remove();
}
};
reader.readAsDataURL(file);
}
});
}
const faviconInput = document.getElementById('favicon-input');
const faviconPreview = document.getElementById('favicon-preview');
if (faviconInput) {
faviconInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(event) {
faviconPreview.src = event.target.result;
faviconPreview.style.display = 'block';
if (faviconPreview.closest('.current-image')) {
faviconPreview.closest('.current-image').querySelector('.text-muted')?.remove();
}
};
reader.readAsDataURL(file);
}
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,113 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="d-none d-lg-block">
<h1 class="h2 mb-4">Панель управления</h1>
<div class="row g-4 mb-4">
<div class="col-md-3">
<div class="card tw-bg-dark-green tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Пользователи</h5>
<p class="display-5">{{ $totalUsers }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-orange tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Товары</h5>
<p class="display-5">{{ $totalProducts }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-dark-green tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Заказы</h5>
<p class="display-5">{{ $totalOrders }}</p>
<a href="" class="text-white">Управление </a>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card tw-bg-orange tw-text-light-gray border-0">
<div class="card-body">
<h5 class="card-title">Выручка</h5>
<p class="display-5">{{ $monthlyRevenue }} </p>
<span class="text-white">За текущий месяц</span>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5>Последние заказы</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th></th>
<th>Клиент</th>
<th>Сумма</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
@foreach ($recentOrders as $ord)
<tr>
<td>{{ $ord->order_number }}</td>
<td>{{ $ord->customer_name }}</td>
<td>{{ $ord->total }}</td>
<td>
<span class="badge bg-{{ $ord->delivery_status_color }}">
{{ $ord->delivery_status_name }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card">
<div class="card-header">
<h5>Популярные товары</h5>
</div>
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Товар</th>
<th>Кол-во продаж</th>
</tr>
</thead>
<tbody>
@foreach ($popularProducts as $product)
<tr>
<td>{{ $product->variation_name }}</td>
<td class="text-center">{{ $product->total_quantity }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,82 @@
@extends('layouts.admin')
@section('title', 'Заказы')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5>Заказы</h5>
<form method="GET" action="{{ route('admin.orders') }}" class="d-flex gap-2">
<input type="text" name="search" class="form-control form-control-sm" placeholder="Поиск по номеру..." value="{{ request('search') }}" style="width: 200px;">
<select name="status" class="form-select form-select-sm" style="width: 150px;">
<option value="">Все статусы</option>
<option value="pending" {{ request('status') == 'pending' ? 'selected' : '' }}>Ожидает</option>
<option value="processing" {{ request('status') == 'processing' ? 'selected' : '' }}>В обработке</option>
<option value="shipped" {{ request('status') == 'shipped' ? 'selected' : '' }}>Отправлен</option>
<option value="delivered" {{ request('status') == 'delivered' ? 'selected' : '' }}>Доставлен</option>
<option value="cancelled" {{ request('status') == 'cancelled' ? 'selected' : '' }}>Отменён</option>
</select>
<button type="submit" class="btn btn-sm btn-dark-green">Фильтр</button>
@if(request('search') || request('status'))
<a href="{{ route('admin.orders') }}" class="btn btn-sm btn-secondary">Сбросить</a>
@endif
</form>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th> заказа</th>
<th>Клиент</th>
<th>Сумма</th>
<th>Статус доставки</th>
<th>Статус оплаты</th>
<th>Дата</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse($orders as $order)
<tr>
<td>{{ $order->order_number }}</td>
<td>{{ $order->customer_name }}<br><small class="text-muted">{{ $order->customer_email }}</small></td>
<td>{{ number_format($order->total, 0, '.', ' ') }} </td>
<td>
<span class="badge bg-{{ $order->delivery_status_color }}">
{{ $order->delivery_status_name }}
</span>
</td>
<td>
<span class="badge bg-{{ $order->payment_status === 'paid' ? 'success' : 'warning' }}">
{{ $order->payment_status_name }}
</span>
</td>
<td>{{ $order->created_at->format('d.m.Y H:i') }}</td>
<td>
<a href="{{ route('admin.orders.show', $order->id) }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted">Заказов не найдено</td>
</tr>
@endforelse
</tbody>
</table>
</div>
{{ $orders->links() }}
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,109 @@
@extends('layouts.admin')
@section('title', 'Заказ ' . $order->order_number)
@section('content')
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4>Заказ #{{ $order->order_number }}</h4>
<a href="{{ route('admin.orders') }}" class="btn btn-secondary btn-sm"> Назад к списку</a>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Информация о заказе</h5>
</div>
<div class="card-body">
<p><strong>Дата:</strong> {{ $order->created_at->format('d.m.Y H:i') }}</p>
<p><strong>Клиент:</strong> {{ $order->customer_name }}</p>
<p><strong>Email:</strong> {{ $order->customer_email }}</p>
<p><strong>Телефон:</strong> {{ $order->customer_phone }}</p>
@if($order->shipping_address)
<p><strong>Адрес доставки:</strong> {{ $order->shipping_address }}</p>
@endif
@if($order->comment)
<p><strong>Комментарий:</strong> {{ $order->comment }}</p>
@endif
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Обновить статус</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.orders.update-status', $order->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Статус доставки</label>
<select name="delivery_status" class="form-select">
@foreach($order::getDeliveryStatuses() as $value => $label)
<option value="{{ $value }}" {{ $order->delivery_status == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<div class="mb-3">
<label class="form-label">Статус оплаты</label>
<select name="payment_status" class="form-select">
@foreach($order::getPaymentStatuses() as $value => $label)
<option value="{{ $value }}" {{ $order->payment_status == $value ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</div>
<button type="submit" class="btn btn-dark-green">Обновить статус</button>
</form>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Товары в заказе</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Товар</th>
<th>Вариация</th>
<th>SKU</th>
<th>Кол-во</th>
<th>Цена</th>
<th>Сумма</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $item)
<tr>
<td>{{ $item->product_name }}</td>
<td>{{ $item->variation_name ?? '—' }}</td>
<td>{{ $item->sku ?? '—' }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ number_format($item->price, 0, '.', ' ') }} </td>
<td>{{ number_format($item->total, 0, '.', ' ') }} </td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="5" class="text-end fw-bold">Итого:</td>
<td class="fw-bold">{{ number_format($order->total, 0, '.', ' ') }} </td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,5 @@
<div class="container-fluid">
<div class="row g-2 flex-wrap">
<h1 class="h2">{{ auth()->user()->name }} | {{ auth()->user()->role->name }}</h1>
</div>
</div>

View File

@@ -0,0 +1,97 @@
<nav class="navbar navbar-dark tw-bg-dark-green">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center gap-2" href="/">
<img src="{{ asset('assets/images/logo/' . $menuContacts->logo) }}" width="35" height="35">
Админ-панель
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mobileMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>
<div class="collapse" id="mobileMenu">
<div class="tw-bg-dark-green p-3">
<nav class="nav flex-column">
@can('admin_access')
<a href="{{ route('admin.dashboard') }}"
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.dashboard') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-speedometer2 me-2"></i> Панель управления
</a>
@endcan
@can('view_orders')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.orders*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-bag me-2"></i> Заказы
</a>
@endcan
@can('view_products')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.products*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-box me-2"></i> Товары
</a>
@endcan
@can('manage_categories')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.categories*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-folder me-2"></i> Категории
</a>
@endcan
@can('manage_brands')
<a href="{{ route('admin.brands') }}"
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.brands*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-tag me-2"></i> Бренды
</a>
@endcan
@can('view_users')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.users*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-people me-2"></i> Пользователи
</a>
@endcan
<hr class="tw-border-light-gray my-2">
@can('manage_roles')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.roles*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-shield me-2"></i> Роли
</a>
@endcan
@can('manage_permissions')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.permissions*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-key me-2"></i> Права
</a>
@endcan
@can('edit_shop_settings')
<a href=""
class="nav-link tw-text-light-gray {{ request()->routeIs('admin.settings') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-gear me-2"></i> Настройки
</a>
@endcan
<hr class="tw-border-light-gray my-2">
<a href="/" class="nav-link tw-text-light-gray rounded mb-1">
<i class="bi bi-house me-2"></i> На сайт
</a>
<div class="mt-3 pt-2">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-orange w-100">
<i class="bi bi-box-arrow-right me-1"></i> Выйти
</button>
</form>
</div>
</nav>
</div>
</div>

View File

@@ -0,0 +1,105 @@
<aside class="tw-bg-dark-green tw-text-light-gray">
<div class="p-4">
<a href="/" class="d-flex align-items-center gap-2 text-decoration-none mb-4">
<img src="{{ asset('assets/images/logo/' . $menuContacts->logo) }}" alt="{{ $menuContacts->name }}" width="50" height="50">
<span class="tw-text-light-gray fw-bold fs-4">Админ-панель</span>
</a>
{{-- Меню --}}
<nav class="nav flex-column">
{{-- Дашборд (доступен всем с admin_access) --}}
@can('admin_access')
<a href="{{ route('admin.dashboard') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/dashboard') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-speedometer2 me-2"></i>
Панель управления
</a>
@endcan
{{-- Заказы --}}
@can('view_orders')
<a href="{{ route('admin.orders') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/orders*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-bag me-2"></i>
Заказы
</a>
@endcan
{{-- Товары --}}
@can('view_products')
<a href="/admin/products"
class="nav-link tw-text-light-gray {{ request()->is('admin/products*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-box me-2"></i>
Товары
</a>
@endcan
{{-- Категории --}}
@can('manage_categories')
<a href="/admin/categories"
class="nav-link tw-text-light-gray {{ request()->is('admin/categories*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-folder me-2"></i>
Категории
</a>
@endcan
{{-- Бренды --}}
@can('manage_brands')
<a href="{{ route('admin.brands') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/brands*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-tag me-2"></i>
Бренды
</a>
@endcan
{{-- Пользователи --}}
@can('view_users')
<a href="/admin/users"
class="nav-link tw-text-light-gray {{ request()->is('admin/users*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-people me-2"></i>
Юзеры
</a>
@endcan
@canany(['manage_roles', 'manage_permissions', 'edit_shop_settings'])
<hr class="tw-border-light-gray my-3">
@endcanany
{{-- Роли --}}
@can('manage_roles')
<a href="{{ route('admin.roles') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/roles*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-shield me-2"></i>
Роли
</a>
@endcan
{{-- Права --}}
@can('manage_permissions')
<a href="{{ route('admin.permissions') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/permissions*') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-key me-2"></i>
Права
</a>
@endcan
{{-- Настройки магазина --}}
@can('edit_shop_settings')
<a href="{{ route('admin.contacts.edit') }}"
class="nav-link tw-text-light-gray {{ request()->is('admin/settings') ? 'tw-bg-orange' : '' }} rounded mb-1">
<i class="bi bi-gear me-2"></i>
Настройки
</a>
@endcan
{{-- Выход на сайт (для всех) --}}
<hr class="tw-border-light-gray my-3">
<a href="/"
class="nav-link tw-text-light-gray rounded mb-1">
<i class="bi bi-house me-2"></i>
На сайт
</a>
</nav>
</div>
</aside>

View File

@@ -0,0 +1,74 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование права: {{ $permission->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.permissions.update', $permission->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $permission->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug', $permission->slug) }}">
<small class="text-muted">Уникальный идентификатор (например: manage_cart, admin_access)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Группа</label>
<input type="text"
class="form-control @error('group') is-invalid @enderror"
name="group"
value="{{ old('group', $permission->group) }}"
placeholder="Например: cart, orders, products, admin">
<small class="text-muted">Для группировки прав в интерфейсе</small>
@error('group')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $permission->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.permissions') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,118 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="permissions-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Права доступа</h5>
</div>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th style="width: 25%">Название</th>
<th style="width: 15%">Slug</th>
<th style="width: 15%">Группа</th>
<th style="width: 35%">Описание</th>
<th style="width: 10%">Действия</th>
</tr>
</thead>
<tbody>
<tr class="bg-light">
<form action="{{ route('admin.permissions.store') }}" method="POST">
@csrf
<td>
<input type="text" name="name"
class="form-control form-control-sm @error('name') is-invalid @enderror"
placeholder="Название" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="slug"
class="form-control form-control-sm @error('slug') is-invalid @enderror"
placeholder="slug" value="{{ old('slug') }}">
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="group"
class="form-control form-control-sm @error('group') is-invalid @enderror"
placeholder="Группа" value="{{ old('group') }}">
@error('group')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td>
<input type="text" name="description"
class="form-control form-control-sm @error('description') is-invalid @enderror"
placeholder="Описание" value="{{ old('description') }}">
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</td>
<td class="text-center">
<button type="submit" class="btn btn-dark-green p-1" title="Добавить">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
</td>
</form>
</tr>
@foreach ($permissions as $permission)
<tr>
<td>{{ $permission->name }}</td>
<td><code>{{ $permission->slug }}</code></td>
<td>
@if ($permission->group)
<span class="badge bg-secondary">{{ $permission->group }}</span>
@else
<span class="text-muted"></span>
@endif
</td>
<td>{{ $permission->description ?? '—' }}</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.permissions.edit', $permission->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.permissions.destroy', $permission->id) }}" method="POST"
onsubmit="return confirm('Удалить право «{{ $permission->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $permissions->links() }}
</div>
@endsection

View File

@@ -0,0 +1,650 @@
@extends('layouts.admin')
@section('title', 'Добавить товар')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<form action="{{ route('admin.products.store') }}" method="POST" enctype="multipart/form-data" id="productForm">
@csrf
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Основная информация</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror" value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Бренд</label>
<select name="brand_id" class="form-select">
<option value="">Выберите бренд</option>
@foreach($brands as $brand)
<option value="{{ $brand->id }}" {{ old('brand_id') == $brand->id ? 'selected' : '' }}>
{{ $brand->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Категория <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror">
<option value="">Выберите категорию</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}" {{ old('category_id') == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Полное описание</label>
<textarea name="description" class="form-control" rows="5">{{ old('description') }}</textarea>
</div>
<div class="row">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', true) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Характеристики товара</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addSpec">
<i class="bi bi-plus-lg"></i> Добавить характеристику
</button>
</div>
<div class="card-body">
<div id="specs-container">
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[0][key]" class="form-control form-control-sm"
placeholder="Название (например, Состав)">
</div>
<div class="col-md-6">
<input type="text" name="attributes[0][value]" class="form-control form-control-sm"
placeholder="Значение (например, индейка 30%)">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Вариации (размеры, вес, цвет)</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addVariation">
<i class="bi bi-plus-lg"></i> Добавить вариацию
</button>
</div>
<div class="card-body">
<div id="variations-container">
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">SEO</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" name="meta_title" class="form-control"
value="{{ old('meta_title') }}">
<small class="text-muted">До 60 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea name="meta_description" class="form-control" rows="3">{{ old('meta_description') }}</textarea>
<small class="text-muted">До 160 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords</label>
<input type="text" name="meta_keywords" class="form-control"
value="{{ old('meta_keywords') }}">
<small class="text-muted">Ключевые слова через запятую</small>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2 mb-4">
<input type="hidden" id="variationCount" value="{{ old('variations') ? count(old('variations')) : 0 }}">
<input type="hidden" id="specCount" value="{{ old('attributes') ? count(old('attributes')) : 1 }}">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить товар">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
<a href="{{ route('admin.products') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
@endsection
@section('script')
<script>
let errors = <?php echo json_encode($errors->getMessages()); ?>;
let oldInput = <?php echo json_encode(old()); ?>;
let variationIndex = parseInt(document.getElementById('variationCount').value);
let specIndex = parseInt(document.getElementById('specCount').value);
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function getVariationTemplate(index) {
let nameError = errors[`variations.${index}.name`] ? errors[`variations.${index}.name`][0] : null;
let skuError = errors[`variations.${index}.sku`] ? errors[`variations.${index}.sku`][0] : null;
let priceError = errors[`variations.${index}.price`] ? errors[`variations.${index}.price`][0] : null;
let stockError = errors[`variations.${index}.stock`] ? errors[`variations.${index}.stock`][0] : null;
let nameOld = oldInput.variations?.[index]?.name || '';
let skuOld = oldInput.variations?.[index]?.sku || '';
let priceOld = oldInput.variations?.[index]?.price || '';
let stockOld = oldInput.variations?.[index]?.stock || 0;
let oldPriceOld = oldInput.variations?.[index]?.old_price || '';
let weightOld = oldInput.variations?.[index]?.attributes?.weight || '';
const isDefaultChecked = (oldInput.variations?.[index]?.is_default !== undefined) ?
oldInput.variations?.[index]?.is_default :
(index === 0);
return `
<div class="variation-item border p-3 mb-3 rounded bg-light">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][name]"
class="form-control form-control-sm ${nameError ? 'is-invalid' : ''}"
value="${escapeHtml(nameOld)}">
${nameError ? `<div class="invalid-feedback">${escapeHtml(nameError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][sku]"
class="form-control form-control-sm ${skuError ? 'is-invalid' : ''}"
value="${escapeHtml(skuOld)}">
${skuError ? `<div class="invalid-feedback">${escapeHtml(skuError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[${index}][price]"
class="form-control form-control-sm ${priceError ? 'is-invalid' : ''}"
value="${priceOld}">
${priceError ? `<div class="invalid-feedback">${escapeHtml(priceError)}</div>` : ''}
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[${index}][old_price]"
class="form-control form-control-sm"
value="${oldPriceOld}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[${index}][stock]"
class="form-control form-control-sm ${stockError ? 'is-invalid' : ''}"
value="${stockOld}">
${stockError ? `<div class="invalid-feedback">${escapeHtml(stockError)}</div>` : ''}
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
<input type="number" step="0.01" name="variations[${index}][attributes][weight]"
class="form-control form-control-sm"
value="${weightOld}">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="sortable-${index}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[${index}][images][]"
class="form-control form-control-sm variation-images-input"
data-index="${index}"
multiple accept="image/*">
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки. Максимальный размер 2 МБ.</small>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
<input type="text" name="variations[${index}][attributes][flavor]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.flavor || '')}">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
<input type="text" name="variations[${index}][attributes][color]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.color || '')}">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
<input type="text" name="variations[${index}][attributes][size]"
class="form-control form-control-sm"
value="${escapeHtml(oldInput.variations?.[index]?.attributes?.size || '')}">
</div>
</div>
<div class="col-12">
<div class="form-check mt-3">
<input type="checkbox" name="variations[${index}][is_default]" class="form-check-input" value="1" id="default_${index}"
${isDefaultChecked ? 'checked' : ''}>
<label class="form-check-label small" for="default_${index}">
Основная вариация
</label>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation d-flex gap-1 align-items-center justify-content-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20"> Удалить
</button>
</div>
</div>
`;
}
function getSpecTemplate(index) {
return `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${index}][key]" class="form-control form-control-sm"
placeholder="Название характеристики">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${index}][value]" class="form-control form-control-sm"
placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
`;
}
function initSortable(container, fileInput) {
let filesArray = [];
new Sortable(container, {
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.file-item').forEach(item => {
const idx = parseInt(item.dataset.index);
newOrder.push(filesArray[idx]);
});
filesArray = newOrder;
updateFileInput(fileInput, filesArray);
renderPreviews(container, filesArray);
}
});
fileInput.addEventListener('change', function(e) {
filesArray = Array.from(e.target.files);
renderPreviews(container, filesArray);
updateFileInput(fileInput, filesArray);
});
}
function renderPreviews(container, files) {
container.innerHTML = files.map((file, idx) => `
<div class="file-item position-relative" data-index="${idx}" style="cursor: move;">
<img src="${URL.createObjectURL(file)}" style="max-height: 60px; width: auto;" class="border rounded">
<span class="badge bg-dark position-absolute top-0 start-0">${idx + 1}</span>
</div>
`).join('');
}
function updateFileInput(fileInput, files) {
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
fileInput.files = dataTransfer.files;
}
// ========== УПРАВЛЕНИЕ ОСНОВНОЙ ВАРИАЦИЕЙ ==========
function initDefaultVariation() {
const defaultCheckboxes = document.querySelectorAll('input[name$="[is_default]"]');
defaultCheckboxes.forEach((checkbox, index) => {
checkbox.addEventListener('change', function() {
if (this.checked) {
defaultCheckboxes.forEach((cb, i) => {
if (i !== index) {
cb.checked = false;
}
});
}
});
});
}
// ========== ПРОВЕРКА SKU ==========
function checkSkuOnInput(input, variationIndex) {
const sku = input.value.trim();
if (!sku) return;
const checkUrl = document.querySelector('meta[name="sku-check-url"]').getAttribute('content');
fetch(checkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
sku: sku,
variation_id: null,
product_id: null
})
})
.then(response => response.json())
.then(data => {
if (!data.unique) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = data.message;
} else {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
}
});
}
function initSkuValidation() {
document.querySelectorAll('input[name$="[sku]"]').forEach((input, index) => {
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
checkSkuOnInput(this, index);
}, 500);
});
});
}
// ========== ОТОБРАЖЕНИЕ ОШИБОК ==========
function displayValidationErrors() {
for (let i = 0; i < variationIndex; i++) {
if (errors[`variations.${i}.name`]) {
const input = document.querySelector(`input[name="variations[${i}][name]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.name`][0];
}
}
if (errors[`variations.${i}.sku`]) {
const input = document.querySelector(`input[name="variations[${i}][sku]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.sku`][0];
}
}
if (errors[`variations.${i}.price`]) {
const input = document.querySelector(`input[name="variations[${i}][price]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.price`][0];
}
}
if (errors[`variations.${i}.stock`]) {
const input = document.querySelector(`input[name="variations[${i}][stock]"]`);
if (input && !input.classList.contains('is-invalid')) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.stock`][0];
}
}
}
}
// ========== ПРОВЕРКА ВСЕХ SKU ПЕРЕД ОТПРАВКОЙ ==========
function validateAllSkus() {
const skuInputs = document.querySelectorAll('input[name$="[sku]"]');
let hasError = false;
const skuValues = [];
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku) {
if (skuValues.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Артикул не может дублироваться в одной форме';
hasError = true;
} else {
skuValues.push(sku);
}
}
});
if (hasError) {
alert('Пожалуйста, исправьте ошибки в артикулах');
}
return !hasError;
}
// ========== ИНИЦИАЛИЗАЦИЯ ==========
document.addEventListener('DOMContentLoaded', function() {
const variationsContainer = document.getElementById('variations-container');
const addVariationBtn = document.getElementById('addVariation');
const specsContainer = document.getElementById('specs-container');
const addSpecBtn = document.getElementById('addSpec');
// Заполняем вариации
if (oldInput.variations && Object.keys(oldInput.variations).length > 0) {
variationsContainer.innerHTML = '';
for (let i = 0; i < Object.keys(oldInput.variations).length; i++) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(i));
if (i >= variationIndex) variationIndex = i + 1;
}
} else if (variationIndex === 0) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(0));
variationIndex = 1;
} else {
for (let i = 0; i < variationIndex; i++) {
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(i));
}
}
// Инициализация Sortable для всех вариаций
setTimeout(() => {
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput) {
initSortable(container, fileInput);
}
});
}, 100);
// Добавление вариации
if (addVariationBtn) {
addVariationBtn.addEventListener('click', function() {
const newIndex = variationIndex; // Сохраняем текущий индекс
variationsContainer.insertAdjacentHTML('beforeend', getVariationTemplate(newIndex));
// Увеличиваем индекс ПОСЛЕ вставки HTML
variationIndex++;
setTimeout(() => {
const newContainer = document.getElementById(`sortable-${newIndex}`);
const newFileInput = document.querySelector(`.variation-images-input[data-index="${newIndex}"]`);
console.log('Adding new variation:', newIndex, newContainer, newFileInput);
if (newContainer && newFileInput) {
initSortable(newContainer, newFileInput);
}
initDefaultVariation();
initSkuValidation();
}, 100);
});
}
// Удаление вариаций и характеристик
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-variation') ||
e.target.parentElement?.classList.contains('remove-variation')) {
const btn = e.target.classList.contains('remove-variation') ? e.target : e.target.parentElement;
const item = btn.closest('.variation-item');
if (item) item.remove();
}
if (e.target.classList.contains('remove-spec') ||
e.target.parentElement?.classList.contains('remove-spec')) {
const btn = e.target.classList.contains('remove-spec') ? e.target : e.target.parentElement;
const item = btn.closest('.spec-item');
if (item) item.remove();
}
});
// Заполняем характеристики
if (oldInput.attributes && Object.keys(oldInput.attributes).length > 0) {
specsContainer.innerHTML = '';
for (let i = 0; i < Object.keys(oldInput.attributes).length; i++) {
const attr = oldInput.attributes[i];
const specHtml = `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${i}][key]" class="form-control form-control-sm"
placeholder="Название характеристики" value="${escapeHtml(attr.key || '')}">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${i}][value]" class="form-control form-control-sm"
placeholder="Значение" value="${escapeHtml(attr.value || '')}">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20" class="mx-auto">
</button>
</div>
</div>
`;
specsContainer.insertAdjacentHTML('beforeend', specHtml);
if (i >= specIndex) specIndex = i + 1;
}
}
// Добавление характеристики
if (addSpecBtn) {
addSpecBtn.addEventListener('click', function() {
specsContainer.insertAdjacentHTML('beforeend', getSpecTemplate(specIndex));
specIndex++;
});
}
// Инициализация всех обработчиков
initDefaultVariation();
initSkuValidation();
displayValidationErrors();
// Блокировка отправки формы при ошибках
const productForm = document.getElementById('productForm');
if (productForm) {
productForm.addEventListener('submit', function(e) {
if (!validateAllSkus()) {
e.preventDefault();
}
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,832 @@
@extends('layouts.admin')
@section('title', 'Редактировать товар')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<form action="{{ route('admin.products.update', $product->id) }}" method="POST" enctype="multipart/form-data" id="productForm">
@csrf
@method('PUT')
@php
$variationCount = old('variations') !== null ? count(old('variations')) : $product->variations->count();
$specCount = old('attributes') !== null ? count(old('attributes')) : $product->attributes->count();
@endphp
<input type="hidden" id="variationCount" value="{{ $variationCount }}">
<input type="hidden" id="specCount" value="{{ $specCount ?: 1 }}">
<div class="row">
<div class="col-md-8">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">Основная информация</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-12 mb-3">
<label class="form-label">Название <span class="text-danger">*</span></label>
<input type="text" name="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $product->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Бренд</label>
<select name="brand_id" class="form-select">
<option value="">Выберите бренд</option>
@foreach($brands as $brand)
<option value="{{ $brand->id }}"
{{ old('brand_id', $product->brand_id) == $brand->id ? 'selected' : '' }}>
{{ $brand->name }}
</option>
@endforeach
</select>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Категория <span class="text-danger">*</span></label>
<select name="category_id" class="form-select @error('category_id') is-invalid @enderror">
<option value="">Выберите категорию</option>
@foreach($categories as $cat)
<option value="{{ $cat->id }}"
{{ old('category_id', $product->category_id) == $cat->id ? 'selected' : '' }}>
@for($i = 0; $i < $cat->level; $i++) @endfor {{ $cat->name }}
</option>
@endforeach
</select>
@error('category_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea name="description" class="form-control" rows="5">{{ old('description', $product->description) }}</textarea>
</div>
<div class="row">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="is_active" value="1"
class="form-check-input" id="is_active"
{{ old('is_active', $product->is_active) ? 'checked' : '' }}>
<label class="form-check-label" for="is_active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Характеристики товара</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addSpec">
<i class="bi bi-plus-lg"></i> Добавить характеристику
</button>
</div>
<div class="card-body">
<div id="specs-container">
@php
$productAttributes = old('attributes', $product->attributes->toArray());
$attrIndex = 0;
@endphp
@foreach($productAttributes as $key => $value)
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[{{ $attrIndex }}][key]"
class="form-control form-control-sm"
value="{{ is_array($value) ? ($value['key'] ?? '') : $key }}" placeholder="Название">
</div>
<div class="col-md-6">
<input type="text" name="attributes[{{ $attrIndex }}][value]"
class="form-control form-control-sm"
value="{{ is_array($value) ? ($value['value'] ?? '') : $value }}" placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
@php $attrIndex++; @endphp
@endforeach
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Вариации (размеры, вес, цвет)</h5>
<button type="button" class="btn btn-sm btn-dark-green" id="addVariation">
<i class="bi bi-plus-lg"></i> Добавить вариацию
</button>
</div>
<div class="card-body">
<div id="variations-container">
@foreach(old('variations', $product->variations) as $index => $variation)
@include('admin.products.partials.variation-form', [
'index' => $index,
'variation' => $variation,
'product' => $product
])
@endforeach
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0">SEO</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Meta Title</label>
<input type="text" name="meta_title" class="form-control"
value="{{ old('meta_title', $product->meta_title) }}">
<small class="text-muted">До 60 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Description</label>
<textarea name="meta_description" class="form-control" rows="3">{{ old('meta_description', $product->meta_description) }}</textarea>
<small class="text-muted">До 160 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Meta Keywords</label>
<input type="text" name="meta_keywords" class="form-control"
value="{{ old('meta_keywords', $product->meta_keywords) }}">
<small class="text-muted">Ключевые слова через запятую</small>
</div>
</div>
</div>
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green">
<i class="bi bi-save"></i> Сохранить
</button>
<a href="{{ route('admin.products') }}" class="btn btn-secondary">
Отмена
</a>
</div>
</form>
</div>
@endsection
@section('script')
<script>
let variationIndex = parseInt(document.getElementById('variationCount').value);
let specIndex = parseInt(document.getElementById('specCount').value);
let errors = <?php echo json_encode($errors->getMessages()); ?>;
let oldInput = <?php echo json_encode(old()); ?>;
let deletedSkus = [];
function escapeHtml(text) {
if (!text) return '';
return String(text).replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
function showNotification(type, message) {
const alertClass = type === 'success' ? 'alert-success' : (type === 'warning' ? 'alert-warning' : 'alert-danger');
const icon = type === 'success' ? 'bi-check-circle-fill' : (type === 'warning' ? 'bi-exclamation-triangle-fill' : 'bi-x-circle-fill');
const alertHtml = `
<div class="alert ${alertClass} alert-dismissible fade show position-fixed top-0 end-0 m-3" style="z-index: 9999; min-width: 350px;" role="alert">
<i class="bi ${icon} me-2"></i> ${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
`;
document.body.insertAdjacentHTML('beforeend', alertHtml);
setTimeout(() => {
const alert = document.querySelector('.alert-dismissible');
if (alert) alert.remove();
}, 5000);
}
function handleDefaultVariation(clickedIndex) {
const allCheckboxes = document.querySelectorAll('input[name$="[is_default]"]');
allCheckboxes.forEach((checkbox, index) => {
checkbox.checked = (index === clickedIndex);
});
}
function initDefaultVariationHandlers() {
document.querySelectorAll('input[name$="[is_default]"]').forEach((checkbox, index) => {
const newCheckbox = checkbox.cloneNode(true);
checkbox.parentNode.replaceChild(newCheckbox, checkbox);
newCheckbox.addEventListener('change', function() {
if (this.checked) {
handleDefaultVariation(index);
}
});
});
}
function checkSkuOnInput(input, variationIndex, variationId = null) {
const sku = input.value.trim();
if (!sku) {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
return;
}
if (deletedSkus.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Этот артикул будет удален после сохранения. Сохраните изменения и попробуйте снова.';
return;
}
const checkUrl = document.querySelector('meta[name="sku-check-url"]').getAttribute('content');
const productId = document.querySelector('meta[name="product-id"]')?.getAttribute('content') || null;
fetch(checkUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
},
body: JSON.stringify({
sku: sku,
variation_id: variationId,
product_id: productId
})
})
.then(response => response.json())
.then(data => {
if (input.value.trim() !== sku) return;
if (!data.unique) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = data.message;
} else {
input.classList.remove('is-invalid');
const errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (errorDiv) errorDiv.remove();
}
})
.catch(error => {
console.error('Error checking SKU:', error);
});
}
function initSkuValidation() {
document.querySelectorAll('input[name$="[sku]"]').forEach((input, index) => {
const variationItem = input.closest('.variation-item');
const variationIdInput = variationItem?.querySelector('input[name$="[id]"]');
const variationId = variationIdInput ? variationIdInput.value : null;
let timeout;
input.addEventListener('input', function() {
clearTimeout(timeout);
timeout = setTimeout(() => {
checkSkuOnInput(this, index, variationId);
}, 500);
});
});
}
function validateAllSkus() {
const skuInputs = document.querySelectorAll('input[name$="[sku]"]');
let hasError = false;
const skuValues = [];
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku) {
if (skuValues.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Артикул не может дублироваться в одной форме';
hasError = true;
} else {
skuValues.push(sku);
}
}
});
skuInputs.forEach((input) => {
const sku = input.value.trim();
if (sku && deletedSkus.includes(sku)) {
input.classList.add('is-invalid');
let errorDiv = input.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
input.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = 'Этот артикул будет удален после сохранения. Сохраните изменения и попробуйте снова.';
hasError = true;
}
});
if (hasError) {
showNotification('warning', 'Пожалуйста, исправьте ошибки в артикулах');
}
return !hasError;
}
function markVariationAsDeleted(item, sku) {
item.style.opacity = '0.6';
item.style.backgroundColor = '#f8d7da';
let deleteMark = item.querySelector('.deletion-mark');
if (!deleteMark) {
deleteMark = document.createElement('div');
deleteMark.className = 'deletion-mark text-danger small mt-2';
deleteMark.innerHTML = '<i class="bi bi-trash"></i> Будет удалена после сохранения';
item.appendChild(deleteMark);
}
const inputs = item.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.setAttribute('data-disabled', 'true');
input.style.pointerEvents = 'none';
input.style.opacity = '0.6';
});
showNotification('warning', `Артикул "${sku}" будет удален после сохранения. Не используйте его для новых вариаций до сохранения.`);
}
function updateVariationIndices() {
const variationItems = document.querySelectorAll('.variation-item');
variationItems.forEach((item, newIndex) => {
item.setAttribute('data-variation-index', newIndex);
const inputs = item.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
const name = input.getAttribute('name');
if (name) {
const newName = name.replace(/variations\[\d+\]/, `variations[${newIndex}]`);
if (newName !== name) {
input.setAttribute('name', newName);
}
}
});
const elementsWithId = item.querySelectorAll('[id]');
elementsWithId.forEach(el => {
const oldId = el.id;
if (oldId.includes('existing-images-') || oldId.includes('sortable-') || oldId.includes('default_')) {
const newId = oldId.replace(/\d+/, newIndex);
el.id = newId;
}
});
const fileInputs = item.querySelectorAll('.variation-images-input');
fileInputs.forEach(input => {
input.setAttribute('data-index', newIndex);
});
});
variationIndex = variationItems.length;
}
function getNewVariationTemplate(index) {
return `
<div class="variation-item border p-3 mb-3 rounded bg-light" data-variation-index="${index}">
<input type="hidden" name="variations[${index}][id]" value="">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][name]" class="form-control form-control-sm" placeholder="Например: 250г, Красный, Мятный">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[${index}][sku]" class="form-control form-control-sm" placeholder="Уникальный артикул">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[${index}][price]" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[${index}][old_price]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[${index}][stock]" class="form-control form-control-sm" value="0">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
<input type="number" step="0.01" name="variations[${index}][attributes][weight]" class="form-control form-control-sm">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="existing-images-${index}" class="existing-images-container d-flex flex-wrap gap-2 mb-2"></div>
<div id="sortable-${index}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[${index}][images][]" class="form-control form-control-sm variation-images-input" data-index="${index}" multiple accept="image/jpeg,image/png,image/webp">
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки.</small>
</div>
</div>
<div class="row g-2 mt-3">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
<input type="text" name="variations[${index}][attributes][flavor]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
<input type="text" name="variations[${index}][attributes][color]" class="form-control form-control-sm">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
<input type="text" name="variations[${index}][attributes][size]" class="form-control form-control-sm">
</div>
</div>
<div class="row mt-3">
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="variations[${index}][is_default]" class="form-check-input" value="1" id="default_${index}">
<label class="form-check-label small" for="default_${index}">Основная вариация</label>
</div>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation">
<i class="bi bi-trash"></i> Удалить вариацию
</button>
</div>
</div>
`;
}
function getSpecTemplate(index) {
return `
<div class="row spec-item mb-2">
<div class="col-md-5">
<input type="text" name="attributes[${index}][key]" class="form-control form-control-sm" placeholder="Название">
</div>
<div class="col-md-6">
<input type="text" name="attributes[${index}][value]" class="form-control form-control-sm" placeholder="Значение">
</div>
<div class="col-md-1">
<button type="button" class="btn btn-sm btn-danger remove-spec w-100">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`;
}
function initSortable(container, fileInput) {
let filesArray = [];
new Sortable(container, {
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.file-item').forEach(item => {
const idx = parseInt(item.dataset.index);
newOrder.push(filesArray[idx]);
});
filesArray = newOrder;
updateFileInput(fileInput, filesArray);
renderNewPreviews(container, filesArray);
}
});
fileInput.addEventListener('change', function(e) {
filesArray = Array.from(e.target.files);
renderNewPreviews(container, filesArray);
updateFileInput(fileInput, filesArray);
});
}
function initExistingSortable(container) {
if (!container || container.children.length === 0) return;
new Sortable(container, {
animation: 150,
handle: '.existing-image-item',
onEnd: function() {
const newOrder = [];
container.querySelectorAll('.existing-image-item').forEach(item => {
newOrder.push(item.dataset.image);
});
updateExistingImagesOrder(container.id, newOrder);
updateMainBadge(container);
}
});
}
function renderNewPreviews(container, files) {
if (!container) return;
container.innerHTML = files.map((file, idx) => `
<div class="file-item position-relative" data-index="${idx}" style="cursor: move;">
<img src="${URL.createObjectURL(file)}" style="max-height: 60px; width: auto;" class="border rounded">
<span class="badge bg-dark position-absolute top-0 start-0">${idx + 1}</span>
</div>
`).join('');
}
function updateFileInput(fileInput, files) {
const dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
fileInput.files = dataTransfer.files;
}
function updateExistingImagesOrder(containerId, newOrder) {
const container = document.getElementById(containerId);
if (!container) return;
const parts = containerId.split('-');
const variationIdx = parts[parts.length - 1];
container.querySelectorAll('input[name$="[existing_images][]"]').forEach(input => input.remove());
newOrder.forEach(imagePath => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = `variations[${variationIdx}][existing_images][]`;
input.value = imagePath;
container.appendChild(input);
});
}
function updateMainBadge(container) {
const items = container.querySelectorAll('.existing-image-item');
items.forEach((item, idx) => {
const oldBadge = item.querySelector('.badge.bg-success');
if (oldBadge) oldBadge.remove();
if (idx === 0) {
const badge = document.createElement('span');
badge.className = 'badge bg-success position-absolute top-0 start-0';
badge.style.fontSize = '10px';
badge.textContent = 'Главное';
item.style.position = 'relative';
item.appendChild(badge);
}
});
}
window.removeVariationImage = function(button) {
let variationIdx = parseInt(button.dataset.variationIndex);
let imagePath = button.dataset.image;
let removedContainer = document.getElementById(`removed-images-${variationIdx}`);
if (!removedContainer) {
removedContainer = document.createElement('div');
removedContainer.id = `removed-images-${variationIdx}`;
removedContainer.style.display = 'none';
let existingContainer = document.getElementById(`existing-images-${variationIdx}`);
if (existingContainer) {
existingContainer.appendChild(removedContainer);
}
}
let removedInput = document.createElement('input');
removedInput.type = 'hidden';
removedInput.name = `variations[${variationIdx}][removed_images][]`;
removedInput.value = imagePath;
removedContainer.appendChild(removedInput);
let imageDiv = button.closest('.existing-image-item');
if (imageDiv) {
imageDiv.remove();
}
updateOrderAfterRemoval(variationIdx);
}
function updateOrderAfterRemoval(variationIdx) {
const container = document.getElementById(`existing-images-${variationIdx}`);
if (!container) return;
const items = container.querySelectorAll('.existing-image-item');
const newOrder = Array.from(items).map(item => item.dataset.image);
updateExistingImagesOrder(container.id, newOrder);
updateMainBadge(container);
}
function initDeleteVariationHandler() {
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-variation') || e.target.closest('.remove-variation')) {
const btn = e.target.classList.contains('remove-variation') ? e.target : e.target.closest('.remove-variation');
const item = btn.closest('.variation-item');
if (item && confirm('Удалить эту вариацию?')) {
const skuInput = item.querySelector('input[name$="[sku]"]');
const sku = skuInput ? skuInput.value.trim() : null;
if (sku) {
deletedSkus.push(sku);
markVariationAsDeleted(item, sku);
}
item.remove();
updateVariationIndices();
if (sku) {
showNotification('warning', `Артикул "${sku}" будет удален после сохранения. Не используйте его для новых вариаций до сохранения.`);
}
}
}
});
}
document.addEventListener('DOMContentLoaded', function() {
deletedSkus = [];
setTimeout(function() {
document.querySelectorAll('.existing-images-container').forEach(container => {
if (container.children.length > 0) {
initExistingSortable(container);
}
});
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput) {
initSortable(container, fileInput);
}
});
}, 500);
initSkuValidation();
initDefaultVariationHandlers();
initDeleteVariationHandler();
document.getElementById('addVariation')?.addEventListener('click', function() {
const container = document.getElementById('variations-container');
container.insertAdjacentHTML('beforeend', getNewVariationTemplate(variationIndex));
setTimeout(() => {
updateVariationIndices();
initSkuValidation();
initDefaultVariationHandlers();
document.querySelectorAll('.sortable-container').forEach(container => {
const variationIdx = container.id.split('-')[1];
const fileInput = document.querySelector(`.variation-images-input[data-index="${variationIdx}"]`);
if (fileInput && !container.hasAttribute('data-initialized')) {
initSortable(container, fileInput);
container.setAttribute('data-initialized', 'true');
}
});
}, 100);
});
document.getElementById('addSpec')?.addEventListener('click', function() {
const container = document.getElementById('specs-container');
container.insertAdjacentHTML('beforeend', getSpecTemplate(specIndex));
specIndex++;
});
document.addEventListener('click', function(e) {
if (e.target.classList.contains('remove-spec') || e.target.closest('.remove-spec')) {
const btn = e.target.classList.contains('remove-spec') ? e.target : e.target.closest('.remove-spec');
const item = btn.closest('.spec-item');
if (item) item.remove();
}
});
const productForm = document.getElementById('productForm');
if (productForm) {
productForm.addEventListener('submit', function(e) {
if (!validateAllSkus()) {
e.preventDefault();
}
});
}
});
function displayValidationErrors() {
for (let i = 0; i < variationIndex; i++) {
if (errors[`variations.${i}.name`]) {
const nameInput = document.querySelector(`input[name="variations[${i}][name]"]`);
if (nameInput && !nameInput.classList.contains('is-invalid')) {
nameInput.classList.add('is-invalid');
let errorDiv = nameInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
nameInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.name`][0];
}
}
if (errors[`variations.${i}.sku`]) {
const skuInput = document.querySelector(`input[name="variations[${i}][sku]"]`);
if (skuInput && !skuInput.classList.contains('is-invalid')) {
skuInput.classList.add('is-invalid');
let errorDiv = skuInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
skuInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.sku`][0];
}
}
if (errors[`variations.${i}.price`]) {
const priceInput = document.querySelector(`input[name="variations[${i}][price]"]`);
if (priceInput && !priceInput.classList.contains('is-invalid')) {
priceInput.classList.add('is-invalid');
let errorDiv = priceInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
priceInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.price`][0];
}
}
if (errors[`variations.${i}.stock`]) {
const stockInput = document.querySelector(`input[name="variations[${i}][stock]"]`);
if (stockInput && !stockInput.classList.contains('is-invalid')) {
stockInput.classList.add('is-invalid');
let errorDiv = stockInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
stockInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[`variations.${i}.stock`][0];
}
}
const imageErrors = Object.keys(errors).filter(key => key.startsWith(`variations.${i}.images.`));
if (imageErrors.length > 0) {
const fileInput = document.querySelector(`.variation-images-input[data-index="${i}"]`);
if (fileInput && !fileInput.classList.contains('is-invalid')) {
fileInput.classList.add('is-invalid');
let errorDiv = fileInput.parentElement.querySelector('.invalid-feedback');
if (!errorDiv) {
errorDiv = document.createElement('div');
errorDiv.className = 'invalid-feedback';
fileInput.parentElement.appendChild(errorDiv);
}
errorDiv.textContent = errors[imageErrors[0]][0];
}
}
}
}
document.addEventListener('DOMContentLoaded', function() {
setTimeout(displayValidationErrors, 100);
});
const addVariationBtn = document.getElementById('addVariation');
if (addVariationBtn) {
addVariationBtn.addEventListener('click', function() {
setTimeout(displayValidationErrors, 200);
});
}
</script>
@endsection

View File

@@ -0,0 +1,180 @@
<div class="variation-item border p-3 mb-3 rounded bg-light" data-variation-index="{{ $index }}">
<input type="hidden" name="variations[{{ $index }}][id]" value="{{ is_array($variation) ? ($variation['id'] ?? '') : ($variation->id ?? '') }}">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Название вариации <span class="text-danger">*</span></label>
<input type="text" name="variations[{{ $index }}][name]"
class="form-control form-control-sm @error(" variations.$index.name") is-invalid @enderror"
value="{{ old("variations.$index.name", is_array($variation) ? ($variation['name'] ?? '') : ($variation->name ?? '')) }}">
@error("variations.$index.name")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Артикул (SKU) <span class="text-danger">*</span></label>
<input type="text" name="variations[{{ $index }}][sku]"
class="form-control form-control-sm @error(" variations.$index.sku") is-invalid @enderror"
value="{{ old("variations.$index.sku", is_array($variation) ? ($variation['sku'] ?? '') : ($variation->sku ?? '')) }}">
@error("variations.$index.sku")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Цена () <span class="text-danger">*</span></label>
<input type="number" step="0.01" name="variations[{{ $index }}][price]"
class="form-control form-control-sm @error(" variations.$index.price") is-invalid @enderror"
value="{{ old("variations.$index.price", is_array($variation) ? ($variation['price'] ?? '') : ($variation->price ?? '')) }}">
@error("variations.$index.price")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small fw-bold">Старая цена</label>
<input type="number" step="0.01" name="variations[{{ $index }}][old_price]"
class="form-control form-control-sm"
value="{{ old("variations.$index.old_price", is_array($variation) ? ($variation['old_price'] ?? '') : ($variation->old_price ?? '')) }}">
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Количество <span class="text-danger">*</span></label>
<input type="number" name="variations[{{ $index }}][stock]"
class="form-control form-control-sm @error(" variations.$index.stock") is-invalid @enderror"
value="{{ old("variations.$index.stock", is_array($variation) ? ($variation['stock'] ?? 0) : ($variation->stock ?? 0)) }}">
@error("variations.$index.stock")
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-md-4">
<label class="form-label small fw-bold">Вес (кг)</label>
@php
$weightValue = '';
if (is_array($variation)) {
$weightValue = $variation['attributes']['weight'] ?? '';
} else {
$weightValue = $variation->attributes->where('key', 'weight')->first()?->value ?? '';
}
@endphp
<input type="number" step="0.01" name="variations[{{ $index }}][attributes][weight]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.weight", $weightValue) }}">
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-12">
<label class="form-label small fw-bold">Фото для этой вариации</label>
<div id="existing-images-{{ $index }}" class="existing-images-container d-flex flex-wrap gap-2 mb-2">
@php
$imagesArray = [];
if (!is_array($variation)) {
$imagesArray = $variation->images->pluck('path')->toArray();
} else {
$variationId = $variation['id'] ?? null;
if ($variationId && isset($product) && $product->variations) {
$originalVariation = $product->variations->where('id', $variationId)->first();
if ($originalVariation) {
$imagesArray = $originalVariation->images->pluck('path')->toArray();
}
}
}
@endphp
@foreach($imagesArray as $idx => $imagePath)
<div class="existing-image-item position-relative" data-image="{{ $imagePath }}" data-index="{{ $idx }}" style="cursor: move;">
<img src="{{ asset($imagePath) }}" style="max-height: 60px; width: auto;" class="border rounded">
@if($idx === 0)
<span class="badge bg-success position-absolute top-0 start-0" style="font-size: 10px;">Главное</span>
@endif
<button type="button"
class="btn btn-sm btn-danger position-absolute top-0 end-0 p-0"
style="font-size: 14px; line-height: 1; width: 20px; height: 20px; border-radius: 50%;"
data-variation-index="{{ $index }}"
data-image="{{ $imagePath }}"
onclick="removeVariationImage(this)">
</button>
</div>
@endforeach
</div>
<div id="sortable-{{ $index }}" class="sortable-container d-flex flex-wrap gap-2 mb-2"></div>
<input type="file" name="variations[{{ $index }}][images][]"
class="form-control form-control-sm variation-images-input @error(" variations.{$index}.images.*") is-invalid @enderror"
data-index="{{ $index }}"
multiple accept="image/*">
@error("variations.{$index}.images.*")
<div class="invalid-feedback d-block">{{ $message }}</div>
@enderror
<small class="text-muted">Можно выбрать несколько фото. Первое будет главным. Перетаскивайте для сортировки.</small>
</div>
</div>
<div class="row g-2 mt-2">
<div class="col-md-4">
<label class="form-label small">Вкус</label>
@php
$flavorValue = '';
if (is_array($variation)) {
$flavorValue = $variation['attributes']['flavor'] ?? '';
} else {
$flavorValue = $variation->attributes->where('key', 'flavor')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][flavor]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.flavor", $flavorValue) }}">
</div>
<div class="col-md-4">
<label class="form-label small">Цвет</label>
@php
$colorValue = '';
if (is_array($variation)) {
$colorValue = $variation['attributes']['color'] ?? '';
} else {
$colorValue = $variation->attributes->where('key', 'color')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][color]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.color", $colorValue) }}">
</div>
<div class="col-md-4">
<label class="form-label small">Размер</label>
@php
$sizeValue = '';
if (is_array($variation)) {
$sizeValue = $variation['attributes']['size'] ?? '';
} else {
$sizeValue = $variation->attributes->where('key', 'size')->first()?->value ?? '';
}
@endphp
<input type="text" name="variations[{{ $index }}][attributes][size]"
class="form-control form-control-sm"
value="{{ old("variations.$index.attributes.size", $sizeValue) }}">
</div>
</div>
<div class="col-12">
<div class="form-check mt-3">
<input type="checkbox" name="variations[{{ $index }}][is_default]"
class="form-check-input" value="1" id="default_{{ $index }}"
{{ old("variations.$index.is_default", is_array($variation) ? ($variation['is_default'] ?? false) : ($variation->is_default ?? false)) ? 'checked' : '' }}>
<label class="form-check-label small" for="default_{{ $index }}">
Основная вариация
</label>
</div>
</div>
<div class="mt-3">
<button type="button" class="btn btn-sm btn-danger remove-variation">
<i class="bi bi-trash"></i> Удалить вариацию
</button>
</div>
</div>

View File

@@ -0,0 +1,164 @@
@extends('layouts.admin')
@section('title', 'Товары')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="products-table col-12 mb-5">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Товары</h5>
<a href="{{ route('admin.products.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить товар
</a>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Фото</th>
<th>Название</th>
<th>Категория</th>
<th>Бренд</th>
<th>Вариации</th>
<th>Цена</th>
<th>Остаток</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse($products as $product)
<tr>
<td>
<img src="{{ $product->main_image }}"
alt="{{ $product->name }}"
style="width: 50px; height: 50px; object-fit: cover;"
class="rounded">
</td>
<td>
<strong>{{ $product->name }}</strong>
</td>
<td>{{ $product->category->name ?? '-' }}</td>
<td>{{ $product->brand->name ?? '-' }}</td>
<td style="min-width: 180px;">
<div class="variations-table">
@foreach($product->variations as $index => $variation)
<div class="d-flex justify-content-between align-items-center small mb-1 tw-bg-orange rounded p-1 tw-text-light-gray
{{ $index >= 3 ? 'd-none variations-more' : '' }}">
<span class="text-truncate" style="max-width: 100px;"
title="{{ $variation->name }}">
{{ Str::limit($variation->name, 15) }}
</span>
<span class="text-nowrap">
<span>{{ number_format($variation->price) }} </span>
<span class="ms-1">×{{ $variation->stock }}</span>
</span>
</div>
@endforeach
@if($product->variations->count() > 3)
<a href="#" class="small toggle-variations" data-target="{{ $product->id }}">
Показать ещё {{ $product->variations->count() - 3 }}
</a>
@endif
</div>
</td>
<td>{{ $product->price_range }}</td>
<td>
@if($product->total_stock > 0)
<span class="badge bg-success">{{ $product->total_stock }} шт.</span>
@else
<span class="badge bg-danger">Нет</span>
@endif
</td>
<td>
@if($product->is_active)
<span class="badge bg-success">Активен</span>
@else
<span class="badge bg-secondary">Неактивен</span>
@endif
</td>
<td class="text-nowrap">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.products.edit', $product->id) }}"
class="btn btn-orange p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.products.destroy', $product->id) }}"
method="POST"
onsubmit="return confirm('Удалить товар «{{ $product->name }}»? Все вариации также будут удалены.');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
<form action="{{ route('admin.products.duplicate', $product->id) }}"
method="POST">
@csrf
<button type="submit" class="btn btn-dark-green p-1 d-inline-flex align-items-center justify-content-center"
style="width: 32px; height: 32px;"
title="Копировать">
<img src="{{ asset('assets/images/icons/copy-svgrepo-com.svg') }}" alt="Копировать" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="10" class="text-center text-muted py-4">
<i class="bi bi-box fs-1 d-block mb-2"></i>
Нет товаров.
<a href="{{ route('admin.products.create') }}">Добавить первый товар</a>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="ps-2 pe-2">
@if(method_exists($products, 'hasPages') && $products->hasPages())
{{ $products->links() }}
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
@section('script')
<script>
document.querySelectorAll('.toggle-variations').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const container = this.closest('.variations-table');
const hidden = container.querySelectorAll('.variations-more');
hidden.forEach(el => el.classList.toggle('d-none'));
this.textContent = this.textContent.includes('ещё') ? 'Свернуть' : 'Показать ещё';
});
});
</script>
@endsection

View File

@@ -0,0 +1,79 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Создание роли</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.store') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Название роли <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug') }}">
<small class="text-muted">Уникальный идентификатор роли (например: admin, manager)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Права доступа</label>
<div class="row">
@foreach($permissions as $permission)
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="permissions[]"
value="{{ $permission->id }}"
id="perm_{{ $permission->id }}">
<label class="form-check-label" for="perm_{{ $permission->id }}">
{{ $permission->name }}
</label>
</div>
</div>
@endforeach
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить роль">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
</button>
<a href="{{ route('admin.roles') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,91 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование роли: {{ $role->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.roles.update', $role->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Название роли <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $role->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
name="slug"
value="{{ old('slug', $role->slug) }}">
<small class="text-muted">Уникальный идентификатор роли (например: admin, manager)</small>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Описание</label>
<textarea class="form-control @error('description') is-invalid @enderror"
name="description"
rows="3">{{ old('description', $role->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Права доступа</label>
<div class="row">
@foreach($permissions as $permission)
@php
$checked = false;
if (old('permissions')) {
$checked = in_array($permission->id, old('permissions'));
}
else {
$checked = $role->permissions->contains($permission->id);
}
@endphp
<div class="col-md-3">
<div class="form-check">
<input class="form-check-input"
type="checkbox"
name="permissions[]"
value="{{ $permission->id }}"
id="perm_{{ $permission->id }}"
{{ $checked ? 'checked' : '' }}>
<label class="form-check-label" for="perm_{{ $permission->id }}">
{{ $permission->name }}
</label>
</div>
</div>
@endforeach
</div>
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.roles') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,90 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="roles-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Роли пользователей</h5>
<a href="{{ route('admin.roles.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить роль
</a>
</div>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>Название</th>
<th>Slug</th>
<th>Описание</th>
<th>Права</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@foreach ($roles as $role)
<tr>
<td>{{ $role->name }}</td>
<td>
<code>{{ $role->slug }}</code>
</td>
<td>
@if ($role->description)
{{ Str::limit($role->description, 100) }}
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@if($role->permissions->count())
@foreach ($role->permissions as $permission)
<span class="badge bg-secondary mb-1">{{ $permission->name }}</span>
@endforeach
@elseif($role->slug === 'super_admin')
<span class="text-muted">Все права</span>
@else
<span class="text-muted">Нет прав</span>
@endif
</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.roles.edit', $role->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
<form action="{{ route('admin.roles.destroy', $role->id) }}" method="POST"
onsubmit="return confirm('Удалить роль «{{ $role->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $roles->links() }}
</div>
@endsection

View File

@@ -0,0 +1,100 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Создание пользователя</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.users.store') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name') }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email') }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone" id="phone-mask" placeholder="+7(___)-___-__-__"
value="{{ old('phone') }}">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Пароль <span class="text-danger">*</span></label>
<input type="password"
class="form-control @error('password') is-invalid @enderror"
name="password">
<small class="text-muted">Минимум 8 символов</small>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Подтверждение пароля <span class="text-danger">*</span></label>
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
name="password_confirmation">
@error('password_confirmation')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Роль <span class="text-danger">*</span></label>
<select class="form-select @error('role_id') is-invalid @enderror" name="role_id">
<option value="">Выберите роль</option>
@foreach($roles as $role)
<option value="{{ $role->id }}" {{ old('role_id') == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
@error('role_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Добавить пользователя">
<img src="{{ asset('assets/images/icons/add-folder-svgrepo-com.svg') }}" alt="Добавить" width="20" height="20">
Добавить
</button>
<a href="{{ route('admin.users') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script src="{{ asset('assets/js/phone_mask.js') }}"></script>
@endsection

View File

@@ -0,0 +1,98 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
<div class="card">
<div class="card-header">
<h5>Редактирование пользователя: {{ $user->name }}</h5>
</div>
<div class="card-body">
<form action="{{ route('admin.users.update', $user->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
name="name"
value="{{ old('name', $user->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email"
class="form-control @error('email') is-invalid @enderror"
name="email"
value="{{ old('email', $user->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text"
class="form-control @error('phone') is-invalid @enderror"
name="phone" id="phone-mask" placeholder="+7(___)-___-__-__"
value="{{ old('phone', $user->phone) }}">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Новый пароль</label>
<input type="password"
class="form-control @error('password') is-invalid @enderror"
name="password">
<small class="text-muted">Оставьте пустым, если не хотите менять пароль. Минимум 8 символов</small>
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Подтверждение пароля</label>
<input type="password"
class="form-control @error('password_confirmation') is-invalid @enderror"
name="password_confirmation">
</div>
<div class="mb-3">
<label class="form-label">Роль <span class="text-danger">*</span></label>
<select class="form-select @error('role_id') is-invalid @enderror" name="role_id">
<option value="">Выберите роль</option>
@foreach($roles as $role)
<option value="{{ $role->id }}" {{ old('role_id', $user->role_id) == $role->id ? 'selected' : '' }}>
{{ $role->name }}
</option>
@endforeach
</select>
@error('role_id')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2 mb-4">
<button type="submit" class="btn btn-dark-green p-2 d-flex gap-1 align-items-center justify-content-center" title="Сохранить изменения">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Сохранить" width="20" height="20">
Сохранить
</button>
<a href="{{ route('admin.users') }}" class="btn btn-orange p-2 d-flex gap-1 align-items-center">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Отмена" width="20" height="20">
Отмена
</a>
</div>
</form>
</div>
</div>
</div>
@endsection
@section('script')
<script src="{{ asset('assets/js/phone_mask.js') }}"></script>
@endsection

View File

@@ -0,0 +1,104 @@
@extends('layouts.admin')
@section('content')
<div class="container-fluid">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i> {{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i> {{ session('error') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<div class="users-table col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<h5>Пользователи</h5>
<div class="d-flex gap-2">
<form method="GET" action="{{ route('admin.users') }}" class="d-flex gap-2">
<input type="text" id="search"
name="search"
class="form-control form-control-sm"
placeholder="Поиск по имени или email..."
value="{{ request('search') }}"
style="width: 250px;">
<button type="submit" class="btn btn-sm btn-dark-green">
<i class="bi bi-search"></i> Найти
</button>
@if(request('search'))
<a href="{{ route('admin.users') }}" class="btn btn-sm btn-orange">
<i class="bi bi-x-lg"></i> Сбросить
</a>
@endif
</form>
<a href="{{ route('admin.users.create') }}" class="btn btn-sm btn-dark-green">
<i class="bi bi-plus-lg"></i> Добавить пользователя
</a>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Имя</th>
<th>Email</th>
<th>Телефон</th>
<th>Роль</th>
<th>Дата регистрации</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
@forelse ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->phone ?? '—' }}</td>
<td>
<span class="badge bg-{{ $user->role?->slug === 'super_admin' ? 'danger' : ($user->role?->slug === 'shop_admin' ? 'warning' : 'secondary') }}">
{{ $user->role?->name ?? 'Нет роли' }}
</span>
</td>
<td>{{ $user->created_at->format('d.m.Y H:i') }}</td>
<td class="text-center">
<div class="d-flex gap-2 justify-content-center">
<a href="{{ route('admin.users.edit', $user->id) }}" class="btn btn-orange p-1" title="Редактировать">
<img src="{{ asset('assets/images/icons/edit-svgrepo-com.svg') }}" alt="Редактировать" width="20" height="20">
</a>
@if($user->id !== auth()->id())
<form action="{{ route('admin.users.destroy', $user->id) }}" method="POST"
onsubmit="return confirm('Удалить пользователя «{{ $user->name }}»?');">
@csrf
@method('DELETE')
<button class="btn btn-danger p-1" type="submit" title="Удалить">
<img src="{{ asset('assets/images/icons/delete-svgrepo-com.svg') }}" alt="Удалить" width="20" height="20">
</button>
</form>
@endif
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center text-muted">Пользователи не найдены</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="ps-2 pe-2">
{{ $users->links() }}
</div>
@endsection

View File

@@ -0,0 +1,51 @@
@extends('layouts.app')
@php $style = 'login'; @endphp
@section('content')
<div class="container">
<form action="{{ route('login') }}" method="POST" class="login-form tw-bg-orange tw-text-light-gray w-75 mx-auto p-4 d-flex flex-column justify-content-center gap-4">
@csrf
<h3 class="fs-2">Вход на {{ $menuContacts->name }}</h3>
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="d-flex flex-column gap-3">
<input type="email"
name="email"
class="form-control w-100 @error('email') is-invalid @enderror"
placeholder="Введите адрес электронной почты"
value="{{ old('email') }}">
<input type="password"
name="password"
class="form-control w-100 @error('password') is-invalid @enderror"
placeholder="Введите пароль">
<div class="save-checkbox d-flex gap-2 align-items-center">
<input type="checkbox"
id="save_checkbox"
name="save_checkbox"
class="form-check-input"
{{ old('save_checkbox') ? 'checked' : '' }}>
<label for="save_checkbox">Запомнить меня</label>
</div>
</div>
<input type="submit" class="btn btn-dark-green" value="Войти">
<a class="text-decoration-underline text-center" href="{{ route('register') }}">
Еще не зарегистрированы? Зарегистрироваться...
</a>
</form>
</div>
@endsection

View File

@@ -0,0 +1,56 @@
@extends('layouts.app')
@php $style = 'login'; @endphp
@section('content')
<div class="container">
<form action="{{ route('register') }}" method="post" class="login-form tw-bg-orange tw-text-light-gray w-75 mx-auto p-4 d-flex flex-column justify-content-center gap-4">
@csrf
<h3 class="fs-2">Регистрация на {{ $menuContacts->name }}</h3>
@if($errors->any())
<div class="alert alert-danger">
<ul class="mb-0">
@foreach($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="d-flex flex-column gap-3">
<input type="text" name="name" class="form-control w-100 @error('name') is-invalid @enderror"
placeholder="Введите Ваше имя" value="{{ old('name') }}">
<input type="email" name="email" class="form-control w-100 @error('email') is-invalid @enderror"
placeholder="Введите адрес электронной почты" value="{{ old('email') }}">
<input type="tel" name="phone" id="phone-mask" class="form-control w-100 @error('phone') is-invalid @enderror"
placeholder="+7(___)-___-__-__" value="{{ old('phone') }}">
<input type="password" name="password" class="form-control w-100 @error('password') is-invalid @enderror"
placeholder="Введите пароль" oncopy="return false">
<input type="password" name="password_confirmation" class="form-control w-100"
placeholder="Повторите пароль" oncopy="return false">
</div>
<div class="save-processing d-flex gap-2 align-items-center">
<input type="checkbox" id="save_processing" name="save_checkbox" class="form-check-input @error('save_checkbox') is-invalid @enderror"
{{ old('save_checkbox') ? 'checked' : '' }}>
<label for="save_processing">Согласие на обработку персональных данных</label>
</div>
<input type="submit" class="btn btn-dark-green" value="Зарегистрироваться">
<a class="text-decoration-underline text-center" href="{{ route('login') }}">
Уже зарегистрированы? Войти...
</a>
</form>
</div>
@endsection
@section('script')
<script src="{{ asset('assets/js/phone_mask.js') }}"></script>
@endsection

View File

@@ -0,0 +1,34 @@
@extends('layouts.app')
@section('title', 'Бренды')
@php $style = 'brand'; @endphp
@section('content')
<div class="container py-4">
<h2 class="text-center h2 mb-4">Бренды</h2>
<div class="row g-4">
@foreach($brands as $brand)
<div class="col-6 col-md-4 col-lg-3">
<a href="{{ route('brands.show', $brand->slug) }}" class="text-decoration-none">
<div class="card h-100 text-center hover-shadow border-0 shadow-sm">
@if($brand->logo)
<img src="{{ $brand->logo_url }}"
class="card-img-top p-3"
alt="{{ $brand->name }}"
style="height: 120px; object-fit: contain;">
@endif
<div class="card-body">
<h5 class="card-title">{{ $brand->name }}</h5>
<small class="text-muted">{{ $brand->products_count }} товаров</small>
</div>
</div>
</a>
</div>
@endforeach
</div>
{{ $brands->links() }}
</div>
@endsection

View File

@@ -0,0 +1,180 @@
@extends('layouts.app')
@section('title', $brand->name)
@php $style = 'category'; @endphp
@section('content')
<div class="container py-4">
{{-- Хлебные крошки --}}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
<li class="breadcrumb-item">
<a href="{{ route('brands.index') }}">Бренды</a>
</li>
<li class="breadcrumb-item active">{{ $brand->name }}</li>
</ol>
</nav>
{{-- Шапка бренда --}}
<div class="d-flex flex-column gap-3 p-5 mb-4 tw-bg-dark-green tw-text-light-gray align-items-center">
@if($brand->logo)
<img src="{{ $brand->logo_url }}" alt="{{ $brand->name }}" class="mb-3" style="max-height: 100px;">
@endif
<h1 class="fs-2">{{ $brand->name }}</h1>
@if($brand->description)
<small class="lead text-center d-none d-lg-block mt-3">{{ $brand->description }}</small>
@endif
</div>
{{-- Продукты --}}
<div class="row">
<div class="col-12 col-md-4 col-lg-3">
<div class="d-md-none col-12 mb-3">
<button class="btn-dark-green w-100 p-3" type="button" onclick="toggleFilters()">
<span id="btn-text">Показать фильтры</span>
</button>
</div>
<div id="mobile-filters" class="d-none d-md-block filters tw-bg-orange tw-text-light-gray p-3">
<button type="button" class="d-md-none close-btn" onclick="toggleFilters()">&times;</button>
<form method="GET" action="{{ route('brands.show', $brand->slug) }}" class="filters tw-bg-orange tw-text-light-gray p-3" id="filter-form">
{{-- Категории с полным путем --}}
@if($categoriesWithPath->count())
<div class="row align-items-start">
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<p class="form-label fw-semibold mb-3">Категории</p>
<div class="categories-list" style="max-height: 50%; overflow-y: auto;">
@foreach($categoriesWithPath as $category)
<div class="form-check mb-2">
<input class="form-check-input"
type="checkbox"
name="categories[]"
value="{{ $category->id }}"
id="category_{{ $category->id }}"
{{ in_array($category->id, request('categories', [])) ? 'checked' : '' }}>
<label class="form-check-label" for="category_{{ $category->id }}">
<small class="text-muted d-block">{{ $category->full_path }}</small>
</label>
</div>
@endforeach
</div>
</div>
</div>
@endif
{{-- Цена --}}
<div class="row align-items-start">
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<p class="form-label fw-semibold mb-2">Цена</p>
@include('categories.partials.ui-slider', [
'field' => 'price',
'max' => $maxPrice,
'min' => $minPrice,
'step' => 25,
'currentMin' => request('price-min', $minPrice),
'currentMax' => request('price-max', $maxPrice)
])
</div>
</div>
{{-- Кнопки --}}
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<button type="submit" class="btn-dark-green" id="apply-filters-btn">Применить фильтры</button>
<a href="{{ route('brands.show', $brand->slug) }}" class="btn btn-danger mt-2 text-center d-block" id="reset-filters-btn">Сбросить</a>
</div>
</form>
</div>
</div>
{{-- БЛОК С КАРТОЧКАМИ --}}
<div class="col-12 col-md-8 col-lg-9" id="products-container">
@if($products->count())
<div class="row g-2 g-lg-4" id="products-grid">
@foreach($products as $product)
@include('products.partials.product-card', compact('product'))
@endforeach
</div>
{{ $products->links() }}
@else
<div class="alert alert-dark-green">
Товары этого бренда пока отсутствуют.
</div>
@endif
</div>
</div>
</div>
@endsection
@section('script')
<script>
const filters = document.getElementById('mobile-filters');
const btnText = document.getElementById('btn-text');
const body = document.body;
function toggleFilters() {
if (filters.classList.contains('d-none')) {
filters.classList.remove('d-none');
filters.classList.add('mobile-open');
body.classList.add('filters-open');
btnText.textContent = 'Скрыть фильтры';
} else {
filters.classList.add('d-none');
filters.classList.remove('mobile-open');
body.classList.remove('filters-open');
btnText.textContent = 'Показать фильтры';
}
}
function scrollToProducts() {
const productsContainer = document.getElementById('products-container');
if (productsContainer) {
const offset = 80;
const elementPosition = productsContainer.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const filterForm = document.getElementById('filter-form');
const resetBtn = document.getElementById('reset-filters-btn');
if (filterForm) {
filterForm.addEventListener('submit', function(e) {
localStorage.setItem('scrollToProducts', 'true');
});
}
if (resetBtn) {
resetBtn.addEventListener('click', function(e) {
localStorage.setItem('scrollToProducts', 'true');
});
}
if (localStorage.getItem('scrollToProducts') === 'true') {
setTimeout(() => {
scrollToProducts();
localStorage.removeItem('scrollToProducts');
}, 300);
}
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString().length > 0) {
setTimeout(() => {
scrollToProducts();
}, 300);
}
});
</script>
@endsection

View File

@@ -0,0 +1,270 @@
@extends('layouts.app')
@section('title', 'Оформление заказа')
@php $style = 'checkout'; @endphp
@section('content')
<div class="container py-4">
<div class="row">
<div class="col-12">
<h1 class="h2 mb-4">Оформление заказа</h1>
</div>
</div>
<div class="row">
<div class="col-12 col-lg-7">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<form action="{{ route('checkout.process') }}" method="POST" id="checkout-form">
@csrf
{{-- Контактные данные --}}
<h5 class="mb-3">Контактные данные</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<label class="form-label">Ваше имя <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('customer_name') is-invalid @enderror"
name="customer_name"
value="{{ old('customer_name', auth()->user()->name ?? '') }}"
required>
@error('customer_name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-12 col-md-6">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email"
class="form-control @error('customer_email') is-invalid @enderror"
name="customer_email"
value="{{ old('customer_email', auth()->user()->email ?? '') }}"
required>
@error('customer_email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="col-12 col-md-6">
<label class="form-label">Телефон <span class="text-danger">*</span></label>
<input type="tel"
class="form-control @error('customer_phone') is-invalid @enderror"
name="customer_phone"
value="{{ old('customer_phone', auth()->user()->phone ?? '') }}"
required>
@error('customer_phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Способ доставки --}}
<h5 class="mb-3">Способ доставки</h5>
<div class="row g-3 mb-4 bg-light">
<div class="col-12">
<div class="delivery-options">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="delivery_method"
id="delivery_courier" value="courier" checked>
<label class="form-check-label" for="delivery_courier">
<strong>Курьером</strong> бесплатно
<small class="text-muted d-block">Доставка по городу в течение 1-2 дней</small>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="delivery_method"
id="delivery_pickup" value="pickup">
<label class="form-check-label" for="delivery_pickup">
<strong>Самовывоз</strong> бесплатно
<small class="text-muted d-block">Заберите заказ из нашего магазина</small>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="delivery_method"
id="delivery_express" value="express">
<label class="form-check-label" for="delivery_express">
<strong>Экспресс-доставка</strong> 300
<small class="text-muted d-block">Доставка в течение 1-2 часов</small>
</label>
</div>
</div>
@error('delivery_method')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Адрес доставки (показывается только для курьера и экспресса) --}}
<div id="address-block" style="display: none;">
<h5 class="mb-3">Адрес доставки</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<label class="form-label">Адрес <span class="text-danger">*</span></label>
<textarea class="form-control @error('shipping_address') is-invalid @enderror"
name="shipping_address"
rows="3"
placeholder="Город, улица, дом, квартира">{{ old('shipping_address') }}</textarea>
@error('shipping_address')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
</div>
</div>
{{-- Способ оплаты --}}
<h5 class="mb-3">Способ оплаты</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<div class="payment-options">
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="payment_method"
id="payment_cash" value="cash" checked>
<label class="form-check-label" for="payment_cash">
<strong>Наличными при получении</strong>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="payment_method"
id="payment_card" value="card">
<label class="form-check-label" for="payment_card">
<strong>Картой при получении</strong>
</label>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="radio" name="payment_method"
id="payment_online" value="online">
<label class="form-check-label" for="payment_online">
<strong>Онлайн-оплата</strong>
<small class="text-muted d-block">Картой на сайте, Яндекс.Деньги и др.</small>
</label>
</div>
</div>
@error('payment_method')
<div class="text-danger small">{{ $message }}</div>
@enderror
</div>
</div>
{{-- Комментарий --}}
<h5 class="mb-3">Комментарий к заказу</h5>
<div class="row g-3 mb-4">
<div class="col-12">
<textarea class="form-control" name="comment" rows="3"
placeholder="Если есть особые пожелания, напишите их здесь...">{{ old('comment') }}</textarea>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn-dark-green flex-grow-1 py-3" id="submit-btn">
Оформить заказ
</button>
<a href="{{ route('cart.index') }}" class="btn-outline-mint py-3 px-4 text-decoration-none">
Назад
</a>
</div>
</form>
</div>
</div>
</div>
{{-- Информация о заказе --}}
<div class="col-12 col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<h5 class="card-title mb-4">Ваш заказ</h5>
<div class="order-items mb-3">
@foreach($cartItems as $item)
<div class="d-flex justify-content-between mb-2 pb-2 border-bottom">
<div>
<span class="fw-semibold">{{ $item->variation->product->name }}</span>
@if($item->variation->name)
<br><small class="text-muted">{{ $item->variation->name }}</small>
@endif
<br><small class="text-muted">{{ $item->quantity }} шт.</small>
</div>
<div class="text-end">
<span>{{ number_format($item->total, 0, '.', ' ') }} </span>
</div>
</div>
@endforeach
</div>
<div class="order-total">
<div class="d-flex justify-content-between mb-2">
<span>Товары ({{ $cartItems->sum('quantity') }} шт.)</span>
<span id="order-subtotal">{{ number_format($total, 0, '.', ' ') }} </span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Доставка</span>
<span id="order-shipping">200 </span>
</div>
<hr>
<div class="d-flex justify-content-between mb-2">
<strong>Итого к оплате:</strong>
<strong class="fs-5 text-success" id="order-total">{{ number_format($total + 200, 0, '.', ' ') }} </strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('script')
<script>
document.addEventListener('DOMContentLoaded', function() {
const deliveryRadios = document.querySelectorAll('input[name="delivery_method"]');
const addressBlock = document.getElementById('address-block');
const shippingAddress = document.querySelector('textarea[name="shipping_address"]');
const orderShipping = document.getElementById('order-shipping');
const orderTotal = document.getElementById('order-total');
const orderSubtotal = document.getElementById('order-subtotal');
const subtotal = parseInt(orderSubtotal.textContent.replace(/[^0-9]/g, '')) || 0;
function updateShippingCost() {
let shippingCost = 0;
let selectedDelivery = document.querySelector('input[name="delivery_method"]:checked');
if (selectedDelivery) {
switch (selectedDelivery.value) {
case 'courier':
shippingCost = 0;
addressBlock.style.display = 'block';
break;
case 'express':
shippingCost = 300;
addressBlock.style.display = 'block';
break;
case 'pickup':
shippingCost = 0;
addressBlock.style.display = 'none';
break;
}
}
if (orderShipping) orderShipping.textContent = shippingCost + ' ₽';
if (orderTotal) orderTotal.textContent = (subtotal + shippingCost).toLocaleString() + ' ₽';
}
deliveryRadios.forEach(radio => {
radio.addEventListener('change', updateShippingCost);
});
// Инициализация
updateShippingCost();
// Блокировка кнопки при отправке
const form = document.getElementById('checkout-form');
const submitBtn = document.getElementById('submit-btn');
if (form && submitBtn) {
form.addEventListener('submit', function() {
submitBtn.disabled = true;
submitBtn.textContent = 'Оформление...';
});
}
});
</script>
@endsection

View File

@@ -0,0 +1,299 @@
@extends('layouts.app')
@section('title', 'Корзина')
@php $style = 'cart'; @endphp
@section('content')
<div class="container py-4">
<div class="row">
<div class="col-12">
<h1 class="h2 mb-4">Корзина</h1>
</div>
</div>
@if($cartItems->count())
<div class="row">
{{-- Список товаров --}}
<div class="col-12 col-lg-8">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-0">
@foreach($cartItems as $item)
<div class="cart-item p-3 border-bottom" data-id="{{ $item->id }}">
<div class="row align-items-center">
{{-- Изображение --}}
<div class="col-3 col-md-2">
<a href="{{ route('product.show', $item->variation->product->slug) }}">
<img src="{{ $item->variation->product->main_image }}"
alt="{{ $item->variation->product->name }}"
class="img-fluid rounded"
style="max-height: 80px; object-fit: contain;">
</a>
</div>
{{-- Информация о товаре --}}
<div class="col-9 col-md-6">
<a href="{{ route('product.show', $item->variation->product->slug) }}"
class="text-decoration-none text-dark">
<h6 class="mb-1">{{ $item->variation->product->name }}</h6>
</a>
@if($item->variation->name)
<small class="text-muted d-block">{{ $item->variation->name }}</small>
@endif
<small class="text-muted">Артикул: {{ $item->variation->sku }}</small>
</div>
{{-- Количество и цена --}}
<div class="col-12 col-md-4 mt-3 mt-md-0">
<div class="d-flex justify-content-between align-items-center">
{{-- Количество (как на странице продукта) --}}
<div class="quantity-control d-flex align-items-center border rounded" style="width: 130px;">
<button class="btn btn-link text-dark decrement-quantity"
data-id="{{ $item->id }}"
data-quantity="{{ $item->quantity }}"
style="width: 32px;"></button>
<input type="number"
class="form-control border-0 text-center quantity-input"
value="{{ $item->quantity }}"
data-id="{{ $item->id }}"
min="1"
max="{{ $item->variation->stock }}"
style="width: 60px;">
<button class="btn btn-link text-dark increment-quantity"
data-id="{{ $item->id }}"
data-quantity="{{ $item->quantity }}"
style="width: 32px;">+</button>
</div>
{{-- Цена --}}
<div class="text-end ms-2">
<strong class="item-total">{{ number_format($item->total, 0, '.', ' ') }} </strong>
<div class="small text-muted">{{ number_format($item->price, 0, '.', ' ') }} / шт</div>
</div>
{{-- Удалить --}}
<button class="btn btn-link text-danger remove-item"
data-id="{{ $item->id }}"
data-url="{{ route('cart.remove', $item->id) }}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
{{-- Итого --}}
<div class="col-12 col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-body">
<h5 class="card-title mb-4">Итого</h5>
<div class="d-flex justify-content-between mb-2">
<span>Товары (<span id="total-items">{{ $cartItems->sum('quantity') }}</span> шт.)</span>
<span id="cart-subtotal">{{ number_format($total, 0, '.', ' ') }} </span>
</div>
<div class="d-flex justify-content-between mb-2">
<span>Скидка</span>
<span class="text-success">0 </span>
</div>
<hr>
<div class="d-flex justify-content-between mb-4">
<strong>Итого к оплате:</strong>
<strong class="fs-5" id="cart-total">{{ number_format($total, 0, '.', ' ') }} </strong>
</div>
<a href="{{ route('checkout.index') }}" class="btn-dark-green text-center w-100 py-3">
Перейти к оформлению
</a>
<div class="text-center mt-3">
<a href="{{ route('index') }}" class="text-decoration-none small">
Продолжить покупки
</a>
</div>
</div>
</div>
</div>
</div>
@else
{{-- Пустая корзина --}}
<div class="row justify-content-center">
<div class="col-12 col-md-6 text-center py-5">
<h3 class="mt-4">Корзина пуста</h3>
<p class="text-muted mb-4">Добавьте товары в корзину, чтобы оформить заказ</p>
<a href="{{ route('index') }}" class="btn-dark-green">
Перейти к покупкам
</a>
</div>
</div>
@endif
</div>
@endsection
@section('script')
<script>
document.addEventListener('DOMContentLoaded', function() {
// Хранилище для отслеживания активных запросов
let activeRequests = new Map();
// Debounce функция
function debounce(func, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
// Функция обновления количества
async function updateQuantity(itemId, quantity) {
if (activeRequests.has(itemId)) return;
activeRequests.set(itemId, true);
try {
const response = await fetch(`/cart/update/${itemId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
quantity: quantity
})
});
const data = await response.json();
if (data.success) {
const itemElement = document.querySelector(`.cart-item[data-id="${itemId}"]`);
if (!itemElement) return;
const itemTotalSpan = itemElement.querySelector('.item-total');
if (itemTotalSpan) {
itemTotalSpan.textContent = data.total.toLocaleString() + ' ₽';
}
const cartSubtotalSpan = document.getElementById('cart-subtotal');
const cartTotalSpan = document.getElementById('cart-total');
const totalItemsSpan = document.getElementById('total-items');
if (cartSubtotalSpan) cartSubtotalSpan.textContent = data.cart_total.toLocaleString() + ' ₽';
if (cartTotalSpan) cartTotalSpan.textContent = data.cart_total.toLocaleString() + ' ₽';
if (totalItemsSpan) totalItemsSpan.textContent = data.total_items;
const quantityInput = itemElement.querySelector('.quantity-input');
if (quantityInput) {
quantityInput.value = quantity;
quantityInput.max = data.max_stock;
}
const decrementBtn = itemElement.querySelector('.decrement-quantity');
const incrementBtn = itemElement.querySelector('.increment-quantity');
if (decrementBtn) decrementBtn.dataset.quantity = quantity;
if (incrementBtn) incrementBtn.dataset.quantity = quantity;
}
} catch (error) {
console.error('Error:', error);
} finally {
activeRequests.delete(itemId);
}
}
const debouncedUpdate = debounce(updateQuantity, 300);
function changeQuantity(itemId, delta) {
const itemElement = document.querySelector(`.cart-item[data-id="${itemId}"]`);
if (!itemElement) return;
const quantityInput = itemElement.querySelector('.quantity-input');
if (!quantityInput) return;
let newValue = parseInt(quantityInput.value) + delta;
const maxStock = parseInt(quantityInput.max);
if (newValue < 1) newValue = 1;
if (newValue > maxStock) {
if (delta > 0) alert('Недостаточно товара на складе');
return;
}
quantityInput.value = newValue;
debouncedUpdate(itemId, newValue);
}
// Обработчики кнопок +/-
document.querySelectorAll('.increment-quantity').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
changeQuantity(this.dataset.id, 1);
});
});
document.querySelectorAll('.decrement-quantity').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
changeQuantity(this.dataset.id, -1);
});
});
// Обработчик ручного ввода
document.querySelectorAll('.quantity-input').forEach(input => {
input.addEventListener('change', function() {
const itemId = this.dataset.id;
let quantity = parseInt(this.value);
const maxStock = parseInt(this.max);
const minStock = parseInt(this.min) || 1;
if (isNaN(quantity) || quantity < minStock) quantity = minStock;
if (quantity > maxStock) {
quantity = maxStock;
alert('Недостаточно товара на складе');
}
this.value = quantity;
debouncedUpdate(itemId, quantity);
});
});
// Удаление товара (через форму, без AJAX)
document.querySelectorAll('.remove-item').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('Удалить товар из корзины?')) return;
// Создаем форму для отправки DELETE запроса
const url = this.dataset.url;
const form = document.createElement('form');
form.method = 'POST';
form.action = url;
form.style.display = 'none';
// Добавляем CSRF токен
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = '_token';
csrfInput.value = document.querySelector('meta[name="csrf-token"]').content;
form.appendChild(csrfInput);
// Добавляем метод DELETE
const methodInput = document.createElement('input');
methodInput.type = 'hidden';
methodInput.name = '_method';
methodInput.value = 'DELETE';
form.appendChild(methodInput);
document.body.appendChild(form);
form.submit();
});
});
});
</script>
@endsection

View File

@@ -0,0 +1,14 @@
<div class="row g-2 justify-content-center categories-block">
@foreach ($categories as $category)
<div class="col-6 col-md-4 col-lg-3">
<a class="text-decoration-none" href="{{ $category->url }}">
<div class="card category-card"
style="background-image: url('{{ $category->image_url }}'); background-size: cover; background-position: center; height: 200px;">
<div class="card-body">
<h2>{{ $category->name }}</h2>
</div>
</div>
</a>
</div>
@endforeach
</div>

View File

@@ -0,0 +1,94 @@
<div id="{{ $field }}-step" data-step-{{ $field }}="{{ $step }}" class="row g-2 mb-2">
<div class="col-6">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white border-end-0">от</span>
<input
data-min-{{ $field }}={{ $min }}
type="number"
class="form-control form-control-sm border-start-0"
id="{{ $field }}-min"
name="{{ $field }}-min"
value="{{ $currentMin ?? $min }}"
min="{{ $min }}"
max="{{ $max }}">
</div>
</div>
<div class="col-6">
<div class="input-group input-group-sm">
<span class="input-group-text bg-white border-end-0">до</span>
<input
data-max-{{ $field }}={{ $max }}
type="number"
class="form-control form-control-sm border-start-0"
id="{{ $field }}-max"
name="{{ $field }}-max"
value="{{ $currentMax ?? $max }}"
min="{{ $min }}"
max="{{ $max }}">
</div>
</div>
</div>
<div id="{{ $field }}-slider" class="ui-slider"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const sliderId = '{{ $field }}-slider';
const minId = '{{ $field }}-min';
const maxId = '{{ $field }}-max';
const stepId = '{{ $field }}-step';
const slider = document.getElementById(sliderId);
const fieldMin = document.getElementById(minId);
const fieldMax = document.getElementById(maxId);
const fieldStep = document.getElementById(stepId);
const dataAttrMin = 'data-min-{{ $field }}';
const dataAttrMax = 'data-max-{{ $field }}';
const dataAttrStep = 'data-step-{{ $field }}';
const minPossible = parseFloat(fieldMin.getAttribute(dataAttrMin));
const maxPossible = parseFloat(fieldMax.getAttribute(dataAttrMax));
const step = parseFloat(fieldStep.getAttribute(dataAttrStep));
const currentMin = parseFloat(fieldMin.value);
const currentMax = parseFloat(fieldMax.value);
noUiSlider.create(slider, {
start: [currentMin, currentMax],
connect: true,
range: {
'min': minPossible,
'max': maxPossible
},
step: step,
format: {
to: value => Math.round(value),
from: value => Math.round(value)
}
});
slider.noUiSlider.on('update', function(values, handle) {
if (handle === 0) {
fieldMin.value = values[0];
} else {
fieldMax.value = values[1];
}
});
fieldMin.addEventListener('change', function() {
let val = parseFloat(this.value);
if (val < minPossible) val = minPossible;
if (val > parseFloat(fieldMax.value)) val = parseFloat(fieldMax.value);
this.value = val;
slider.noUiSlider.set([val, null]);
});
fieldMax.addEventListener('change', function() {
let val = parseFloat(this.value);
if (val > maxPossible) val = maxPossible;
if (val < parseFloat(fieldMin.value)) val = parseFloat(fieldMin.value);
this.value = val;
slider.noUiSlider.set([null, val]);
});
});
</script>

View File

@@ -0,0 +1,191 @@
@extends('layouts.app')
@section('title', $category->name)
@php $style = 'category'; @endphp
@section('content')
<div class="container">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
@foreach($category->breadcrumbs as $crumb)
@if($loop->last)
<li class="breadcrumb-item active">{{ $crumb['name'] }}</li>
@else
<li class="breadcrumb-item">
<a href="{{ $crumb['url'] }}">{{ $crumb['name'] }}</a>
</li>
@endif
@endforeach
</ol>
</nav>
<div class="d-flex flex-column gap-3 p-5 mb-4 tw-bg-dark-green tw-text-light-gray align-items-center">
<h1 class="fs-2">{{ $category->name }}</h1>
@if($category->description)
<small class="lead text-center d-none d-lg-block mt-3">{{ $category->description }}</small>
@endif
</div>
{{-- Подкатегории --}}
@if($children->count())
<div class="row mb-5 d-none d-lg-block">
<div class="col-12">
<h3 class="fs-4 mb-4 text-center">Подкатегории</h3>
@include('categories.partials.categories-block', ['categories' => $children])
</div>
</div>
@endif
{{-- Продукты --}}
<div class="row">
<div class="col-12 col-md-4 col-lg-3" class="h-100">
<div class="d-md-none col-12 mb-3">
<button class="btn-dark-green w-100 p-3" type="button" onclick="toggleFilters()">
<span id="btn-text">Показать фильтры</span>
</button>
</div>
<div id="mobile-filters" class="d-none d-md-block filters tw-bg-orange tw-text-light-gray p-3">
<button type="button" class="d-md-none close-btn" onclick="toggleFilters()">&times;</button>
{{-- ФОРМА ФИЛЬТРОВ --}}
<form method="GET" action="{{ route('category.show', $category->full_slug) }}" class="filters tw-bg-orange tw-text-light-gray p-3" id="filter-form">
{{-- Бренды --}}
@if($brands->count())
<div class="row align-items-start">
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<p class="form-label fw-semibold mb-3">Бренды</p>
<div class="brands-list" style="max-height: 50%; overflow-y: auto;">
@foreach($brands as $brand)
<div class="form-check mb-2">
<input class="form-check-input"
type="checkbox"
name="brands[]"
value="{{ $brand->id }}"
id="brand_{{ $brand->id }}"
{{ in_array($brand->id, request('brands', [])) ? 'checked' : '' }}>
<label class="form-check-label" for="brand_{{ $brand->id }}">
{{ $brand->name }}
</label>
</div>
@endforeach
</div>
</div>
</div>
@endif
{{-- Цена --}}
<div class="row align-items-start">
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<p class="form-label fw-semibold mb-2">Цена</p>
@include('categories.partials.ui-slider', [
'field' => 'price',
'max' => $maxPrice,
'min' => $minPrice,
'step' => 25,
'currentMin' => request('price-min', $minPrice),
'currentMax' => request('price-max', $maxPrice)
])
</div>
</div>
{{-- Кнопки --}}
<div class="d-flex flex-column justify-content-between col-12 p-3 mb-3 tw-text-light-gray">
<button type="submit" class="btn-dark-green" id="apply-filters-btn">Применить фильтры</button>
<a href="{{ route('category.show', $category->full_slug) }}" class="btn btn-danger mt-2 text-center d-block" id="reset-filters-btn">Сбросить</a>
</div>
</form>
</div>
</div>
{{-- БЛОК С КАРТОЧКАМИ --}}
<div class="col-12 col-md-8 col-lg-9" id="products-container">
@if($products->count())
<div class="row g-2 g-lg-4" id="products-grid">
@foreach($products as $product)
@include('products.partials.product-card', compact('product'))
@endforeach
</div>
{{ $products->links() }}
@else
<div class="alert alert-dark-green">
В этой категории пока нет товаров.
</div>
@endif
</div>
</div>
</div>
@endsection
@section('script')
<script>
const filters = document.getElementById('mobile-filters');
const btnText = document.getElementById('btn-text');
const body = document.body;
function toggleFilters() {
if (filters.classList.contains('d-none')) {
filters.classList.remove('d-none');
filters.classList.add('mobile-open');
body.classList.add('filters-open');
btnText.textContent = 'Скрыть фильтры';
} else {
filters.classList.add('d-none');
filters.classList.remove('mobile-open');
body.classList.remove('filters-open');
btnText.textContent = 'Показать фильтры';
}
}
function scrollToProducts() {
const productsContainer = document.getElementById('products-container');
if (productsContainer) {
const offset = 80;
const elementPosition = productsContainer.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
}
document.addEventListener('DOMContentLoaded', function() {
const filterForm = document.getElementById('filter-form');
const resetBtn = document.getElementById('reset-filters-btn');
if (filterForm) {
filterForm.addEventListener('submit', function(e) {
localStorage.setItem('scrollToProducts', 'true');
});
}
if (resetBtn) {
resetBtn.addEventListener('click', function(e) {
localStorage.setItem('scrollToProducts', 'true');
});
}
if (localStorage.getItem('scrollToProducts') === 'true') {
setTimeout(() => {
scrollToProducts();
localStorage.removeItem('scrollToProducts');
}, 300);
}
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.toString().length > 0) {
setTimeout(() => {
scrollToProducts();
}, 300);
}
});
</script>
@endsection

View File

@@ -0,0 +1,102 @@
@extends('layouts.app')
@section('title', 'Контакты')
@section('content')
<div class="container py-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
<li class="breadcrumb-item active">Контакты</li>
</ol>
</nav>
<div class="row">
<div class="col-md-5 mb-4 pt-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-4">
<h3 class="mb-4">Контактная информация</h3>
<div class="mb-4">
<div class="d-flex align-items-center mb-3">
<i class="bi bi-geo-alt fs-4 tw-text-dark-green me-3"></i>
<div>
<strong>Адрес</strong><br>
{{ $menuContacts->address ?? 'г. Челябинск, ул. Дружбы, д. 15' }}
</div>
</div>
<div class="d-flex align-items-center mb-3">
<i class="bi bi-telephone fs-4 tw-text-dark-green me-3"></i>
<div>
<strong>Телефон</strong><br>
<a href="tel:{{ $menuContacts->phone ?? '+7(985)-070-56-33' }}" class="text-decoration-none">
{{ $menuContacts->phone ?? '+7(985)-070-56-33' }}
</a>
</div>
</div>
<div class="d-flex align-items-center mb-3">
<i class="bi bi-envelope fs-4 tw-text-dark-green me-3"></i>
<div>
<strong>Email</strong><br>
<a href="mailto:{{ $menuContacts->email ?? 'tailandpaws_info@gmail.com' }}" class="text-decoration-none">
{{ $menuContacts->email ?? 'tailandpaws_info@gmail.com' }}
</a>
</div>
</div>
<div class="d-flex align-items-center">
<i class="bi bi-clock fs-4 tw-text-dark-green me-3"></i>
<div>
<strong>Режим работы</strong><br>
{{ $menuContacts->work_hours ?? 'Пн-Пт: 10:00 - 18:00, Сб-Вс: выходной' }}
</div>
</div>
</div>
<hr>
<div class="mt-3">
<p class="mb-2"><strong>Социальные сети:</strong></p>
<div class="d-flex gap-3">
<a href="{{ $menuContacts->telegram }}" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/telegram-svgrepo-com.svg') }}" alt="telegram" class="header-icon">
</a>
<a href="{{ $menuContacts->vk }}" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/whatsapp-svgrepo-com.svg') }}" alt="whatsapp" class="header-icon">
</a>
<a href="{{ $menuContacts->vk }}" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/vk-svgrepo-com.svg') }}" alt="vk" class="header-icon">
</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-7 mb-4">
<div class="card border-0 shadow-sm h-100 overflow-hidden">
<div class="card-body p-0">
<iframe
src="https://yandex.com/map-widget/v1/?ll=61.445115%2C55.142641&mode=search&ol=geo&ouri=ymapsbm1%3A%2F%2Fgeo%3Fdata%3DCgg1NjAyNDI0ORJA0KDQvtGB0YHQuNGPLCDQp9C10LvRj9Cx0LjQvdGB0LosINGD0LvQuNGG0LAg0JPQsNCz0LDRgNC40L3QsCwgNyIKDUDHdUIVYpJcQg%2C%2C&z=17.99"
width="100%"
height="400"
frameborder="0"
allowfullscreen="true"
style="border:0;">
</iframe>
</div>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="mailto:{{ $menuContacts->email ?? 'tailandpaws_info@gmail.com' }}" class="btn-dark-green px-5 py-3">
Написать нам
</a>
</div>
</div>
@endsection

View File

@@ -0,0 +1,18 @@
@extends('layouts.app')
@php $style = 'index'; @endphp
@section('content')
<div class="container">
@include('partials.index.carousel-discount', [ 'carousel_id' => 'carouselDiscount', 'items' => $discountedProducts, 'interval' => 7000 ])
</div>
<div class="container">
@include('categories.partials.categories-block', compact('categories'))
</div>
<div class="container">
@include('partials.index.product-carousel', [ 'title' => 'Сухие корма для кошек', 'carousel_id' => 'carouselDryCatFood', 'items' => $dryCatFood, 'interval' => 7000 ])
</div>
<div class="container">
@include('partials.index.product-carousel', [ 'title' => 'Сухие корма для собак', 'carousel_id' => 'carouselDryDogFood', 'items' => $dryDogFood, 'interval' => 7000 ])
</div>
@endsection

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
@if(isset($product))
<meta name="product-id" content="{{ $product->id }}">
@endif
<title>@yield('title', 'Админ-панель')</title>
<link rel="icon" href="{{ asset('assets/images/logo/' . $menuContacts->favicon) }}" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="{{ asset('assets/css/menu.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/profile.css') }}">
</head>
<body>
<div class="p-0 h-100">
<div class="row g-0 h-100">
<div class="d-none d-lg-block col-lg-3 col-xl-2">
@include('admin.partials.sidebar')
</div>
<div class="col-12 col-lg-9 col-xl-10">
<div class="d-block d-lg-none">
@include('admin.partials.mobile-nav')
</div>
<main class="p-4 h-100" style="background-color: #f8f9fa; min-height: 100vh;">
@include('admin.partials.header')
@yield('content')
</main>
</div>
</div>
</div>
<script src="{{ asset('assets/js/jquery-3.7.1.min.js') }}"></script>
<script src="{{ asset('assets/js/jquery.maskedinput.min.js') }}"></script>
@yield('script')
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title> @yield('title', 'Хвостики и лапки')</title>
<link rel="icon" href="{{ asset('assets/images/logo/' . $menuContacts->favicon) }}" type="image/x-icon">
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="{{ asset('assets/css/nouislider.min.css') }}">
<link rel="stylesheet" href="{{ asset('assets/css/menu.css') }}">
@if (isset($style))
<link rel="stylesheet" href="{{ asset('assets/css/' . $style . '.css') }}">
@endif
</head>
<body>
@include('partials.menu.menu')
<main class="mt-4 mb-4 mt-lg-5 mb-lg-5">
@yield('content')
</main>
<footer class="tw-bg-dark-green pt-4 pb-4">
<div class="container d-flex flex-column gap-5">
<div class="row g-4 justify-content-center">
{{-- О компании --}}
<section class="footer-section col-12 col-lg-6 col-xl-4">
<h3 class="pb-2">О компании</h3>
<div class="footer-links">
<a class="footer-link" href="{{ route('about') }}">О нас</a>
<a class="footer-link" href="{{ route('contacts') }}">Контакты</a>
@auth
@can('admin_access')
<a class="footer-link" href="{{ route('admin.dashboard') }}">Админ-панель</a>
@endcan
@can('manage_cart')
<a class="footer-link" href="{{ route('cart.index') }}">Корзина</a>
@endcan
@can('cabinet_access')
<a class="footer-link" href="{{ route('profile.index') }}">Личный кабинет</a>
@endcan
@endauth
</div>
</section>
{{-- Категории (0 уровень) --}}
<section class="footer-section col-12 col-lg-6 col-xl-4">
<h3 class="pb-2">Категории</h3>
<div class="footer-links">
@foreach($footerCategories as $category)
<a class="footer-link" href="{{ $category->url }}">{{ $category->name }}</a>
@endforeach
</div>
</section>
{{-- Контакты --}}
<section class="footer-section col-12 col-lg-6 col-xl-4 tw-text-light-gray">
<h3 class="pb-2">Связаться с нами</h3>
<p>Контактный номер телефона: {{ $menuContacts->phone }}</p>
<p class="mt-2">Адрес электронной почты: {{ $menuContacts->email }}</p>
<p class="mt-2">Адрес: {{ $menuContacts->address }}</p>
<p class="mt-2">Часы работы: {{ $menuContacts->work_hours }}</p>
<div class="social-media d-flex gap-3 align-items-center mt-3">
@if($menuContacts->telegram)
<a href="https://t.me/{{ $menuContacts->telegram }}" target="_blank" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/telegram-svgrepo-com.svg') }}" alt="telegram" class="header-icon">
</a>
@endif
@if($menuContacts->vkontakte)
<a href="https://vk.com/{{ $menuContacts->vkontakte }}" target="_blank" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/vk-svgrepo-com.svg') }}" alt="vk" class="header-icon">
</a>
@endif
@if($menuContacts->whatsapp)
<a href="https://wa.me/{{ $menuContacts->whatsapp }}" target="_blank" class="btn btn-orange p-0 action-btn">
<img src="{{ asset('assets/images/icons/social_media/whatsapp-svgrepo-com.svg') }}" alt="whatsapp" class="header-icon">
</a>
@endif
</div>
</section>
</div>
<div class="d-flex justify-content-between gap-2">
<p class="p-0 tw-text-light-gray">&copy; <span class="current-year"></span> {{ $menuContacts->name }}. Все права защищены.</p>
<a href="{{ route('privacy-policy') }}" class="tw-text-light-gray text-decoration-underline"> Политика обработки персональных данных</a>
</div>
</div>
</footer>
<script src="{{ asset('assets/js/current-year.js') }}"></script>
<script src="{{ asset('assets/js/jquery-3.7.1.min.js') }}"></script>
<script src="{{ asset('assets/js/jquery.maskedinput.min.js') }}"></script>
<script src="{{ asset('assets/js/nouislider.min.js') }}"></script>
<script src="{{ asset('assets/js/ajax-cart.js') }}"></script>
<script src="{{ asset('assets/js/ajax-search.js') }}"></script>
@yield('script')
<script>
document.addEventListener('DOMContentLoaded', function() {
if (window.innerWidth <= 991) {
const catalogItem = document.querySelector('.dropdown-megamenu > .nav-link');
const megamenu = document.querySelector('.megamenu');
if (catalogItem && megamenu) {
catalogItem.addEventListener('click', function(e) {
e.preventDefault();
megamenu.style.display = megamenu.style.display === 'block' ? 'none' : 'block';
});
}
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown-megamenu')) {
const megamenu = document.querySelector('.megamenu');
if (megamenu && window.innerWidth <= 991) {
megamenu.style.display = 'none';
}
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
@extends('layouts.app')

View File

@@ -0,0 +1,47 @@
<div id="{{ $carousel_id }}" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-inner p-3">
@foreach ($items as $key => $productVariation)
<div class="carousel-item {{ $key == 0 ? 'active' : '' }}">
<div class="row w-100 h-100 mx-auto g-2 g-lg-5">
<div class="carousel-discount-img col-12 col-lg-6">
<a href="{{ route('product.show', $productVariation->product->slug) }}">
@if (isset($productVariation->main_image_url))
<img src="{{ asset($productVariation->main_image_url) }}" alt="{{ $productVariation->name }}">
@else
<div class="bg-light rounded d-flex align-items-center justify-content-center">
<i class="bi bi-image text-secondary"></i>
</div>
@endif
</a>
</div>
<div class="col-12 col-lg-6 d-flex flex-column gap-3">
<div class="row align-items-start g-3 carousel-discount-header">
<h1 class="col-10 fs-3">{{ $productVariation->name }}</h1>
<div class="col-2">
<div class="discount-badge">
-{{ $productVariation->discount_percent }}%
</div>
</div>
</div>
<p class="d-none d-lg-block">{{ Str::words($productVariation->product->description, 30, '...') }}</p>
<div class="carousel-price">
<div class="w-25 d-flex flex-column mx-auto">
<span class="original-price fs-4">{{ number_format($productVariation->old_price, 0, ',', ' ') }} </span>
<span class="discounted-price fs-2">{{ number_format($productVariation->price, 0, ',', ' ') }} </span>
</div>
</div>
<button class="btn btn-dark-green w-100 p-3 mt-auto">Подробнее... </button>
</div>
</div>
</div>
@endforeach
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#{{ $carousel_id }}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#{{ $carousel_id }}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>

View File

@@ -0,0 +1,58 @@
@php
$title = $title ?? 'Товары';
$carousel_id = $carousel_id ?? 'productCarousel';
$interval = $interval ?? 5000;
@endphp
@if($items->count())
<div class="container">
<h1 class="text-center mb-4 fw-medium fs-3">
<a class="text-decoration-none tw-text-dark-blue hover:tw-text-opacity-90" href="#">
{{ $title }}
</a>
</h1>
<div id="{{ $carousel_id }}" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
@foreach($items as $key => $chunk)
<div class="carousel-item {{ $key === 0 ? 'active' : '' }}" data-bs-interval="{{ $interval }}">
<div class="row g-2 justify-content-center">
@foreach($chunk as $product)
@include('products.partials.product-card', compact('product'))
@endforeach
</div>
</div>
@endforeach
</div>
<!-- Индикаторы -->
@if($items->count() > 1)
<div class="carousel-indicators">
@foreach($items as $key => $chunk)
<button type="button"
data-bs-target="#{{ $carousel_id }}"
data-bs-slide-to="{{ $key }}"
class="{{ $key === 0 ? 'active' : '' }}"
aria-current="{{ $key === 0 ? 'true' : 'false' }}"
aria-label="Slide {{ $key + 1 }}">
</button>
@endforeach
</div>
@endif
<!-- Кнопки навигации -->
<button class="carousel-control-prev" type="button" data-bs-target="#{{ $carousel_id }}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#{{ $carousel_id }}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
</div>
@else
<div class="container text-center">
<p class="text-muted">Товары не найдены</p>
</div>
@endif

View File

@@ -0,0 +1,58 @@
<div class="megamenu">
<div class="megamenu-grid">
@foreach($menuCategories as $category)
<div class="category-column">
<h5 class="tw-text-orange border-bottom pb-2 d-flex gap-2">
<img src="{{ $category->iconUrl }}" alt="{{ $category->name }}" class="object-fit-scale" height="20" width="20">
<a href="{{ $category->url }}" class="text-decoration-none tw-text-orange">
{{ $category->name }}
</a>
</h5>
<ul>
{{-- Подкатегории 1 уровня --}}
@foreach($category->children as $child)
@if($child->children->count() > 0)
{{-- Категория с подкатегориями --}}
<li class="fw-bold mt-2">
<a href="{{ $child->url }}" class="tw-text-orange text-decoration-none">
{{ $child->name }}
</a>
</li>
@foreach($child->children as $subChild)
<li>
<a href="{{ $subChild->url }}" class="text-decoration-none">
{{ $subChild->name }}
</a>
</li>
@endforeach
@else
{{-- Обычная категория --}}
<li>
<a href="{{ $child->url }}" class="text-decoration-none">
{{ $child->name }}
</a>
</li>
@endif
@endforeach
</ul>
</div>
@endforeach
</div>
{{-- Популярные бренды --}}
@if(isset($menuBrands) && $menuBrands->count() > 0)
<div class="row mt-4 pt-3 border-top">
<div class="col-12">
<p class="mb-2 fw-bold tw-text-light-gray">Популярные бренды:</p>
<div class="d-flex flex-wrap gap-3">
@foreach($menuBrands as $brand)
<a href=""
class="tw-text-light-gray text-decoration-none hover:text-orange">
{{ $brand->name }}
</a>
@endforeach
</div>
</div>
</div>
@endif
</div>

View File

@@ -0,0 +1,125 @@
<header class="tw-bg-dark-green pt-3 pb-3">
<div class="container">
<nav class="navbar navbar-expand-xl navbar-dark">
<div class="container-fluid px-0 align-items-center">
<a class="navbar-brand d-flex align-items-center gap-2 me-4 flex-shrink-0" href="/">
<img src="{{ asset('assets/images/logo/' . $menuContacts->logo) }}" alt="{{ $menuContacts->name }}" width="70" height="70">
<h3 class="fs-2 tw-text-light-gray p-0 m-0 text-uppercase">{{ $menuContacts->name }}</h3>
</a>
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="collapse"
data-bs-target="#navbarTogglerHeader" aria-controls="navbarTogglerHeader" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse w-100" id="navbarTogglerHeader">
<div class="d-xl-flex w-100 align-items-center gap-3">
<ul class="navbar-nav flex-wrap flex-grow-1 mb-2 mb-xl-0 header-main-menu">
<li class="nav-item">
<a class="nav-link {{ request()->routeIs('index') ? 'active' : '' }}" href="{{ route('index') }}">Главная</a>
</li>
<li class="nav-item dropdown-megamenu">
<a class="nav-link d-flex align-items-center justify-content-center {{ request()->routeIs('category.show') ? 'active' : '' }}"
href=""
id="catalogDropdown" role="button">
Каталог
<span class="dropdown-icon ms-1">
<img src="{{ asset('assets/images/icons/dropdown-arrow-svgrepo-com.svg') }}" alt="dropdown">
</span>
</a>
@include('partials.menu.catalog-megamenu')
</li>
<li class="nav-item">
<a class="nav-link {{ request()->routeIs('about') ? 'active' : '' }}" href="{{ route('about') }}">О нас</a>
</li>
<li class="nav-item">
<a class="nav-link {{ request()->routeIs('contacts') ? 'active' : '' }}" href="{{ route('contacts') }}">Контакты</a>
</li>
</ul>
<div class="header-right d-flex align-items-center justify-content-between gap-2">
<form class="header-search d-flex position-relative" role="search" action="{{ route('search') }}" method="GET" onsubmit="return validateSearch()">
<input id="input_search" class="form-control" type="search"
name="q" placeholder="Поиск" value="{{ request('q') }}" required>
<button class="btn btn-orange p-0 action-btn" type="submit">
<img src="{{ asset('assets/images/icons/search-svgrepo-com.svg') }}" alt="Найти" class="header-icon">
</button>
</form>
@guest
<a href="{{ route('login') }}" class="btn btn-orange p-0 action-btn" title="Войти">
<img src="{{ asset('assets/images/icons/enter-svgrepo-com.svg') }}" alt="Войти" class="header-icon">
</a>
@endguest
@auth
@can('manage_cart')
<a href="{{ route('cart.index') }}" class="btn btn-orange p-0 action-btn position-relative" title="Корзина">
<img src="{{ asset('assets/images/icons/cart-svgrepo-com.svg') }}" alt="Корзина" class="header-icon">
<span class="cart-count position-absolute translate-middle badge rounded-pill bg-danger" style="font-size: 10px;">
{{ \App\Models\CartItem::getCount() }}
</span>
</a>
@endcan
<div class="d-none d-xl-block">
<div class="dropdown auth-dropdown">
<button class="btn btn-orange p-0 action-btn" type="button">
<img src="{{ asset('assets/images/icons/menu-svgrepo-com.svg') }}" alt="Меню" class="header-icon">
</button>
<div class="auth-dropdown-menu">
@can('cabinet_access')
<a href="" class="btn btn-orange p-0 action-btn" title="Личный кабинет">
<img src="{{ asset('assets/images/icons/user-svgrepo-com.svg') }}" alt="Кабинет" class="header-icon">
</a>
@endcan
@can('admin_access')
<a href="{{ route('admin.dashboard') }}" class="btn btn-orange p-0 action-btn" title="Панель управления">
<img src="{{ asset('assets/images/icons/settings-svgrepo-com.svg') }}" alt="Админка" class="header-icon">
</a>
@endcan
<form method="POST" action="{{ route('logout') }}" onsubmit="return confirm('Вы действительно хотите выйти?');">
@csrf
<button type="submit" class="btn btn-orange p-0 action-btn" title="Выйти">
<img src="{{ asset('assets/images/icons/logout-svgrepo-com.svg') }}" alt="Выйти" class="header-icon">
</button>
</form>
</div>
</div>
</div>
<div class="d-xl-none user-mobile-menu">
@can('cabinet_access')
<a href="" class="btn btn-orange p-0 action-btn" title="Личный кабинет">
<img src="{{ asset('assets/images/icons/user-svgrepo-com.svg') }}" alt="Кабинет" class="header-icon">
</a>
@endcan
@can('admin_access')
<a href="{{ route('admin.dashboard') }}" class="btn btn-orange p-0 action-btn" title="Панель управления">
<img src="{{ asset('assets/images/icons/settings-svgrepo-com.svg') }}" alt="Админка" class="header-icon">
</a>
@endcan
<form method="POST" action="{{ route('logout') }}" onsubmit="return confirm('Вы действительно хотите выйти?');">
@csrf
<button type="submit" class="btn btn-orange p-0 action-btn" title="Выйти">
<img src="{{ asset('assets/images/icons/logout-svgrepo-com.svg') }}" alt="Выйти" class="header-icon">
</button>
</form>
</div>
@endauth
</div>
</div>
</div>
</div>
</nav>
</div>
</header>

View File

@@ -0,0 +1,207 @@
@extends('layouts.app')
@section('title', 'Политика обработки персональных данных')
@section('content')
<div class="container py-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
<li class="breadcrumb-item active">Политика конфиденциальности</li>
</ol>
</nav>
<div class="row">
<div class="col-lg-10 mx-auto">
<h1 class="h2 mb-4 text-center mt-4">Политика обработки персональных данных</h1>
<div class="text-muted text-center mb-4">
<small>Актуальная версия: 1.0 от 06.04.2026</small>
</div>
{{-- 1. Общие положения --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">1. Общие положения</h2>
<p>Настоящая политика обработки персональных данных составлена в соответствии с требованиями Федерального закона от 27.07.2006. 152-ФЗ «О персональных данных» (далее Закон о персональных данных) и определяет порядок обработки персональных данных и меры по обеспечению безопасности персональных данных, предпринимаемые «Хвостики и Лапки» (далее Оператор).</p>
<p class="mt-2">Оператор ставит своей важнейшей целью и условием осуществления своей деятельности соблюдение прав и свобод человека и гражданина при обработке его персональных данных, в том числе защиты прав на неприкосновенность частной жизни, личную и семейную тайну.</p>
<p class="mt-2">Настоящая политика Оператора в отношении обработки персональных данных (далее Политика) применяется ко всей информации, которую Оператор может получить о посетителях веб-сайта <a href="http://tailandpuws.ru/">http://tailandpuws.ru/</a>.</p>
</div>
{{-- 2. Основные понятия --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">2. Основные понятия</h2>
<div class="row g-3">
<div class="col-md-6">
<div class="bg-light p-3 rounded">
<strong>Автоматизированная обработка</strong>
<p class="mb-0 small text-muted">Обработка персональных данных с помощью средств вычислительной техники.</p>
</div>
</div>
<div class="col-md-6">
<div class="bg-light p-3 rounded">
<strong>Блокирование персональных данных</strong>
<p class="mb-0 small text-muted">Временное прекращение обработки персональных данных.</p>
</div>
</div>
<div class="col-md-6">
<div class="bg-light p-3 rounded">
<strong>Веб-сайт</strong>
<p class="mb-0 small text-muted">Совокупность графических и информационных материалов по адресу <a href="http://tailandpuws.ru/">http://tailandpuws.ru/</a>.</p>
</div>
</div>
<div class="col-md-6">
<div class="bg-light p-3 rounded">
<strong>Персональные данные</strong>
<p class="mb-0 small text-muted">Любая информация, относящаяся прямо или косвенно к Пользователю.</p>
</div>
</div>
</div>
</div>
{{-- 3. Права и обязанности Оператора --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">3. Права и обязанности Оператора</h2>
<div class="row">
<div class="col-md-6">
<div class="border-start border-success border-4 p-4 mb-3">
<h5 class="text-success mb-2">Оператор имеет право:</h5>
<ul class="small">
<li>Получать достоверные информацию и/или документы, содержащие персональные данные</li>
<li>Продолжить обработку персональных данных без согласия субъекта при наличии оснований</li>
<li>Самостоятельно определять состав мер для обеспечения безопасности персональных данных</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="border-start border-primary border-4 p-4 mb-3">
<h5 class="text-primary mb-2">Оператор обязан:</h5>
<ul class="small">
<li>Предоставлять информацию о обработке персональных данных по запросу</li>
<li>Организовывать обработку в соответствии с законодательством РФ</li>
<li>Публиковать Политику в свободном доступе</li>
<li>Принимать меры для защиты персональных данных</li>
</ul>
</div>
</div>
</div>
</div>
{{-- 4. Права субъектов персональных данных --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">4. Права субъектов персональных данных</h2>
<div class="bg-light p-3 rounded">
<p class="mb-2">Вы имеете право:</p>
<ul class="mb-0">
<li>Получать информацию, касающуюся обработки ваших персональных данных</li>
<li>Требовать уточнения, блокирования или уничтожения ваших персональных данных</li>
<li>Отозвать согласие на обработку персональных данных</li>
<li>Обжаловать неправомерные действия Оператора в уполномоченный орган или суд</li>
</ul>
</div>
</div>
{{-- 5. Принципы обработки --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">5. Принципы обработки персональных данных</h2>
<div class="row g-2">
<div class="col-md-4">
<div class="text-center p-3 border rounded h-100">
<i class="bi bi-shield-check fs-1 text-success"></i>
<p class="mt-2 mb-0 small">Законность и справедливость</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 border rounded h-100">
<i class="bi bi-bullseye fs-1 text-success"></i>
<p class="mt-2 mb-0 small">Ограничение целями обработки</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center p-3 border rounded h-100">
<i class="bi bi-database fs-1 text-success"></i>
<p class="mt-2 mb-0 small">Достоверность и достаточность</p>
</div>
</div>
</div>
</div>
{{-- 6. Цели обработки --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">6. Цели обработки персональных данных</h2>
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Цель обработки</th>
<th>Персональные данные</th>
<th>Правовые основания</th>
</tr>
</thead>
<tbody>
<tr>
<td>Предоставление доступа к сервисам, информации и материалам сайта</td>
<td>
<ul class="mb-0 small">
<li>Фамилия, имя, отчество</li>
<li>Электронный адрес</li>
<li>Номера телефонов</li>
</ul>
</td>
<td>Федеральный закон 149-ФЗ «Об информации, информационных технологиях и о защите информации»</td>
</tr>
</tbody>
</table>
</div>
{{-- 7. Условия обработки --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">7. Условия обработки персональных данных</h2>
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
Обработка персональных данных осуществляется <strong>с согласия субъекта</strong> персональных данных.
</div>
</div>
{{-- 8. Порядок сбора, хранения, передачи --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">8. Порядок сбора, хранения, передачи</h2>
<p>Оператор обеспечивает сохранность персональных данных и принимает все возможные меры, исключающие доступ к персональным данным неуполномоченных лиц.</p>
<div class="alert alert-warning mt-3">
<i class="bi bi-exclamation-triangle me-2"></i>
<strong>Важно:</strong> Персональные данные Пользователя никогда, ни при каких условиях не будут переданы третьим лицам, за исключением случаев, предусмотренных законодательством.
</div>
<p class="mt-3">Для актуализации или отзыва согласия на обработку персональных данных направьте уведомление на электронную почту: <a href="mailto:tailandpaws_info@gmail.com">tailandpaws_info@gmail.com</a>.</p>
</div>
{{-- 9. Конфиденциальность --}}
<div class="policy-section mb-5">
<h2 class="h4 mb-3">9. Конфиденциальность персональных данных</h2>
<p>Оператор и иные лица, получившие доступ к персональным данным, обязаны не раскрывать третьим лицам и не распространять персональные данные без согласия субъекта персональных данных, если иное не предусмотрено федеральным законом.</p>
</div>
{{-- 10. Контактная информация --}}
<div class="policy-section">
<h2 class="h4 mb-3">10. Контактная информация</h2>
<div class="bg-light p-4 rounded">
<p class="mb-2"><strong>По всем вопросам, касающимся обработки персональных данных, вы можете обратиться:</strong></p>
<ul class="mb-0">
<li>По электронной почте: <a href="mailto:tailandpaws_info@gmail.com">tailandpaws_info@gmail.com</a></li>
<li>По телефону: {{ $contacts->phone ?? '+7(985)-070-56-33' }}</li>
<li>По адресу: {{ $contacts->address ?? 'г. Челябинск, ул. Дружбы, д. 15' }}</li>
</ul>
</div>
</div>
<hr class="my-4">
<div class="text-center text-muted small">
<p>Данная Политика действует бессрочно до замены ее новой версией.</p>
<p>Актуальная версия Политики в свободном доступе расположена по адресу: <a href="{{ route('privacy-policy') }}">{{ route('privacy-policy') }}</a></p>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,35 @@
@php
$defaultVariation = $product->default_variation;
$price = $defaultVariation ? $defaultVariation->price : $product->min_price;
$hasStock = $defaultVariation ? $defaultVariation->stock > 0 : false;
$rating = $product->rating;
@endphp
<div class="col-md-4 col-sm-6 mb-4">
<div class="card product-card h-100">
<a href="{{ route('product.show', $product->slug) }}" class="text-decoration-none product-link">
<img src="{{ $product->main_image }}" class="card-img-top" alt="{{ $product->name }}">
<div class="card-body">
<div class="card-title d-flex justify-content-between align-items-start">
<h5 class="fs-4 mb-0">{{ number_format($price, 0, '.', ' ') }} </h5>
<div class="d-flex align-items-center text-orange">
<img src="{{ asset('assets/images/icons/star-alt-4-svgrepo-com.svg') }}" width="20"
height="20" alt="rating">
<p class="card-text mb-0 ms-1">{{ $rating['score'] ?? '4.5' }} ({{ $rating['count'] ?? '0' }} отзывов)</p>
</div>
</div>
<p class="card-text mt-2">{{ Str::limit($product->description, 100) }}</p>
</div>
</a>
<div class="mt-auto p-3 pt-0">
<form action="{{ route('cart.add') }}" method="POST" class="w-100">
@csrf
<input type="hidden" name="variation_id" value="{{ $defaultVariation->id ?? '' }}">
<input type="hidden" name="quantity" value="1">
<button type="submit" class="btn-dark-green w-100 p-3" {{ !$hasStock ? 'disabled' : '' }}>
{{ $hasStock ? 'В корзину' : 'Нет в наличии' }}
</button>
</form>
</div>
</div>
</div>

View File

@@ -0,0 +1,461 @@
@extends('layouts.app')
@section('title', $product->name)
@php $style = 'product'; @endphp
@section('content')
<div class="container py-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb" id="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
@if($product->category)
@foreach($product->category->breadcrumbs as $crumb)
<li class="breadcrumb-item">
<a href="{{ $crumb['url'] }}">{{ $crumb['name'] }}</a>
</li>
@endforeach
@endif
<li class="breadcrumb-item active" id="breadcrumb-product">{{ $product->name }}</li>
</ol>
</nav>
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="product-gallery">
<div class="main-image mb-3">
<img id="main-product-image"
src="{{ $defaultVariation ? $defaultVariation->main_image_url : $product->main_image }}"
alt="{{ $product->name }}"
class="img-fluid rounded w-100"
style="height: 400px; object-fit: contain;">
</div>
@php
$images = $defaultVariation ? $defaultVariation->images : collect();
@endphp
@if($images->count() > 1)
<div class="thumbnail-list d-flex gap-2 overflow-auto" id="thumbnail-list">
@foreach($images as $index => $image)
<div class="thumbnail-item {{ $index === 0 ? 'active' : '' }}"
onclick="changeMainImage('{{ $image->url }}', this)">
<img src="{{ $image->url }}"
alt="{{ $product->name }}"
class="img-fluid rounded"
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;">
</div>
@endforeach
</div>
@endif
</div>
</div>
<div class="col-12 col-md-6">
<div class="product-info">
<div class="d-flex justify-content-between align-items-center mb-2">
@if($defaultVariation && $defaultVariation->sku)
<small class="text-muted" id="product-sku">Артикул: {{ $defaultVariation->sku }}</small>
@endif
@if($product->brand)
<a href="{{ route('brands.show', $product->brand->slug) }}">
<small class="text-muted text-decoration-underline">{{ $product->brand->name }}</small>
</a>
@endif
</div>
<h1 id="product-name" class="h2 mb-3">{{ $product->name }}</h1>
<div class="d-flex align-items-center gap-2 mb-3">
<div class="d-flex align-items-center text-orange">
<img src="{{ asset('assets/images/icons/star-alt-4-svgrepo-com.svg') }}" width="25"
height="25" alt="rating">
<span class="ms-1 fw-bold">{{ $product->rating['score'] ?? '4.9' }}</span>
</div>
<a href="#reviews" class="text-decoration-none">
<small>{{ $product->rating['count'] ?? '0' }} отзывов</small>
</a>
</div>
@php
$defaultVariation = $product->default_variation;
$price = $defaultVariation ? $defaultVariation->price : $product->min_price;
$oldPrice = $defaultVariation ? $defaultVariation->old_price : null;
$hasStock = $defaultVariation ? $defaultVariation->stock > 0 : false;
@endphp
<div class="price-block mb-3" id="price-block">
@if($oldPrice && $oldPrice > $price)
<span class="text-muted text-decoration-line-through fs-5 me-2" id="old-price">
{{ number_format($oldPrice, 0, '.', ' ') }}
</span>
<span class="text-danger fs-1 fw-bold" id="current-price">
{{ number_format($price, 0, '.', ' ') }}
</span>
<span class="badge bg-danger ms-2" id="discount-badge">-{{ $defaultVariation->discount_percent }}%</span>
@else
<span class="fs-1 fw-bold text-success" id="current-price">
{{ number_format($price, 0, '.', ' ') }}
</span>
@endif
</div>
<div class="stock-block mb-3" id="stock-block">
@if($hasStock)
<span class="text-success">
<i class="bi bi-check-circle-fill"></i> В наличии
</span>
@if($defaultVariation && $defaultVariation->stock < 10)
<small class="text-muted ms-2" id="stock-count">(осталось {{ $defaultVariation->stock }} шт.)</small>
@endif
@else
<span class="text-danger">
<i class="bi bi-x-circle-fill"></i> Нет в наличии
</span>
@endif
</div>
@if($product->variations->count() > 1)
<div class="variations-block mb-3">
<label class="fw-semibold mb-2">Варианты:</label>
<div class="d-flex flex-wrap gap-2" id="variations-list">
@foreach($product->variations as $variation)
@php
$imageUrls = $variation->images->map(function($image) {
return $image->url;
})->values()->toArray();
@endphp
<button type="button"
class="btn btn-outline-secondary variation-btn {{ $defaultVariation && $defaultVariation->id == $variation->id ? 'active' : '' }}"
data-variation-id="{{ $variation->id }}"
data-price="{{ $variation->price }}"
data-old-price="{{ $variation->old_price }}"
data-sku="{{ $variation->sku }}"
data-stock="{{ $variation->stock }}"
data-full-name="{{ $variation->name ?? $product->name }}"
data-discount-percent="{{ $variation->discount_percent }}"
data-images='{{ json_encode($imageUrls) }}'
{{ !$variation->stock ? 'disabled' : '' }}>
{{ $variation->name ?? $variation->sku }}
</button>
@endforeach
</div>
</div>
@endif
<div class="cart-block mb-4">
<form action="{{ route('cart.add') }}" method="POST" class="d-flex gap-2" id="cart-form">
@csrf
<input type="hidden" name="variation_id" id="variation-id" value="{{ $defaultVariation->id ?? '' }}">
<div class="quantity-selector d-flex border rounded" style="width: 120px;">
<button type="button" class="btn btn-link px-2" onclick="updateQuantity(-1)">-</button>
<input type="number" name="quantity" id="quantity-input" value="1" min="1" max="{{ $defaultVariation->stock ?? 0 }}"
class="form-control border-0 text-center" style="width: 50px;">
<button type="button" class="btn btn-link px-2" onclick="updateQuantity(1)">+</button>
</div>
<button type="submit" class="btn-dark-green flex-grow-1" id="add-to-cart-btn" {{ !$hasStock ? 'disabled' : '' }}>
{{ $hasStock ? 'В корзину' : 'Нет в наличии' }}
</button>
</form>
</div>
<div class="delivery-block border-top pt-3">
<div class="row g-2">
<div class="col-12 col-sm-4">
<div class="delivery-method p-2 rounded text-center">
<i class="bi bi-lightning-charge fs-4"></i>
<small class="d-block">Экспресс</small>
<small class="text-muted">Платно, за 1 час</small>
</div>
</div>
<div class="col-12 col-sm-4">
<div class="delivery-method p-2 rounded text-center">
<i class="bi bi-truck fs-4"></i>
<small class="d-block">Доставка</small>
<small class="text-muted">Бесплатно</small>
</div>
</div>
<div class="col-12 col-sm-4">
<div class="delivery-method p-2 rounded text-center">
<i class="bi bi-shop fs-4"></i>
<small class="d-block">Самовывоз</small>
<small class="text-muted">Бесплатно</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- Описание и характеристики товара --}}
<div class="row mt-5">
<div class="col-12">
@php
$hasAttributes = $product->attributes->count() > 0;
$hasDescription = !empty($product->description);
@endphp
@if($hasDescription || $hasAttributes)
<ul class="nav nav-tabs" id="productTabs" role="tablist">
@if($hasDescription)
<li class="nav-item" role="presentation">
<button class="nav-link {{ $hasDescription ? 'active' : '' }}"
id="description-tab"
data-bs-toggle="tab"
data-bs-target="#description"
type="button"
role="tab">
Описание
</button>
</li>
@endif
@if($hasAttributes)
<li class="nav-item" role="presentation">
<button class="nav-link {{ !$hasDescription ? 'active' : '' }}"
id="characteristics-tab"
data-bs-toggle="tab"
data-bs-target="#characteristics"
type="button"
role="tab">
Характеристики
</button>
</li>
@endif
<li class="nav-item" role="presentation">
<button class="nav-link"
id="reviews-tab"
data-bs-toggle="tab"
data-bs-target="#reviews"
type="button"
role="tab">
Отзывы ({{ $product->rating['count'] ?? '0' }})
</button>
</li>
</ul>
<div class="tab-content p-3 border border-top-0 rounded-bottom" id="productTabsContent">
@if($hasDescription)
<div class="tab-pane fade {{ $hasDescription ? 'show active' : '' }}"
id="description"
role="tabpanel"
aria-labelledby="description-tab">
<p>{{ $product->description }}</p>
</div>
@endif
@if($hasAttributes)
<div class="tab-pane fade {{ !$hasDescription ? 'show active' : '' }}"
id="characteristics"
role="tabpanel"
aria-labelledby="characteristics-tab">
<table class="table table-bordered">
@foreach($product->attributes as $attribute)
<tr>
<th class="bg-light" style="width: 30%;">{{ $attribute->key }}</th>
<td>{{ $attribute->value }}</td>
</tr>
@endforeach
</table>
</div>
@endif
<div class="tab-pane fade"
id="reviews"
role="tabpanel"
aria-labelledby="reviews-tab">
<p>Отзывы будут здесь</p>
</div>
</div>
@else
<div class="border rounded p-3">
<h4 class="mb-3">Отзывы ({{ $product->rating['count'] ?? '0' }})</h4>
<p>Отзывы будут здесь</p>
</div>
@endif
</div>
</div>
</div>
@endsection
@section('script')
<script>
// Переключение главного изображения
function changeMainImage(url, element) {
const mainImage = document.getElementById('main-product-image');
if (mainImage) {
mainImage.src = url;
}
document.querySelectorAll('.thumbnail-item').forEach(item => {
item.classList.remove('active');
});
if (element) {
element.classList.add('active');
}
}
// Обновление количества
function updateQuantity(delta) {
const quantityInput = document.getElementById('quantity-input');
if (!quantityInput) return;
let newValue = parseInt(quantityInput.value) + delta;
const max = parseInt(quantityInput.getAttribute('max')) || 999;
if (newValue < 1) newValue = 1;
if (newValue > max) newValue = max;
quantityInput.value = newValue;
}
// Переключение вариаций
document.addEventListener('DOMContentLoaded', function() {
const variationBtns = document.querySelectorAll('.variation-btn');
variationBtns.forEach(btn => {
btn.addEventListener('click', function() {
const variationId = this.dataset.variationId;
const price = parseFloat(this.dataset.price);
const oldPrice = parseFloat(this.dataset.oldPrice);
const sku = this.dataset.sku;
const stock = parseInt(this.dataset.stock);
const fullName = this.dataset.fullName;
const discountPercent = this.dataset.discountPercent;
// Получаем массив URL изображений из data-images атрибута
let images = [];
const imagesData = this.dataset.images;
if (imagesData) {
try {
images = JSON.parse(imagesData);
} catch (e) {
console.error('Error parsing images:', e);
images = [];
}
}
// 1. Обновляем название товара
const productName = document.getElementById('product-name');
if (productName && fullName) {
productName.textContent = fullName;
}
// 2. Обновляем хлебные крошки
const breadcrumbProduct = document.getElementById('breadcrumb-product');
if (breadcrumbProduct && fullName) {
breadcrumbProduct.textContent = fullName;
}
// 3. Обновляем скрытое поле с variation_id
const variationInput = document.getElementById('variation-id');
if (variationInput) {
variationInput.value = variationId;
}
// 4. Обновляем цену
const priceBlock = document.getElementById('price-block');
if (priceBlock) {
if (oldPrice && oldPrice > price) {
priceBlock.innerHTML = `
<span class="text-muted text-decoration-line-through fs-5 me-2" id="old-price">
${oldPrice.toLocaleString()}
</span>
<span class="text-danger fs-1 fw-bold" id="current-price">
${price.toLocaleString()}
</span>
<span class="badge bg-danger ms-2" id="discount-badge">-${discountPercent}%</span>
`;
} else {
priceBlock.innerHTML = `
<span class="fs-1 fw-bold text-success" id="current-price">
${price.toLocaleString()}
</span>
`;
}
}
// 5. Обновляем артикул
const skuElement = document.getElementById('product-sku');
if (skuElement && sku) {
skuElement.innerHTML = `Артикул: ${sku}`;
}
// 6. Обновляем наличие
const stockBlock = document.getElementById('stock-block');
const cartBtn = document.getElementById('add-to-cart-btn');
const quantityInput = document.getElementById('quantity-input');
if (stock > 0) {
if (stockBlock) {
stockBlock.innerHTML = `
<span class="text-success">
<i class="bi bi-check-circle-fill"></i> В наличии
${stock < 10 ? `<small class="text-muted ms-2" id="stock-count">(осталось ${stock} шт.)</small>` : ''}
</span>
`;
}
if (cartBtn) cartBtn.disabled = false;
if (quantityInput) quantityInput.max = stock;
cartBtn.textContent = 'В корзину';
} else {
if (stockBlock) {
stockBlock.innerHTML = `
<span class="text-danger">
<i class="bi bi-x-circle-fill"></i> Нет в наличии
</span>
`;
}
if (cartBtn) {
cartBtn.disabled = true;
cartBtn.textContent = 'Нет в наличии';
}
if (quantityInput) quantityInput.max = 0;
}
// 7. Обновляем активный класс кнопок
variationBtns.forEach(b => {
b.classList.remove('active');
});
this.classList.add('active');
// 8. ОБНОВЛЯЕМ ИЗОБРАЖЕНИЯ
const mainImage = document.getElementById('main-product-image');
const thumbnailList = document.getElementById('thumbnail-list');
if (images && images.length > 0) {
// Обновляем главное изображение
mainImage.src = images[0];
// Обновляем миниатюры
if (thumbnailList) {
if (images.length > 1) {
thumbnailList.innerHTML = images.map((imageUrl, index) => `
<div class="thumbnail-item ${index === 0 ? 'active' : ''}"
onclick="changeMainImage('${imageUrl}', this)">
<img src="${imageUrl}"
alt="Изображение товара"
class="img-fluid rounded"
style="width: 80px; height: 80px; object-fit: cover; cursor: pointer;">
</div>
`).join('');
} else {
thumbnailList.innerHTML = '';
}
}
} else {
// Нет изображений - показываем дефолтное
mainImage.src = '/assets/images/products/default.svg';
if (thumbnailList) {
thumbnailList.innerHTML = '';
}
}
});
});
});
</script>
@endsection

View File

@@ -0,0 +1,86 @@
@extends('layouts.app')
@section('title', 'Настройки профиля')
@php $style = 'profile'; @endphp
@section('content')
<div class="container py-4">
@include('profile.partials.profile-sidebar', compact('user'))
<div class="col-md-9">
<div class="card border-0">
<div class="card-body p-4">
<h4 class="mb-4">Настройки профиля</h4>
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show">
{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
<form action="{{ route('profile.update') }}" method="POST">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label">Имя <span class="text-danger">*</span></label>
<input type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name', $user->name) }}">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Email <span class="text-danger">*</span></label>
<input type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email', $user->email) }}">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Телефон</label>
<input type="text" class="form-control @error('phone') is-invalid @enderror" name="phone" value="{{ old('phone', $user->phone) }}">
@error('phone')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<hr class="my-4">
<h5 class="mb-3">Смена пароля</h5>
<div class="mb-3">
<label class="form-label">Текущий пароль</label>
<input type="password" class="form-control @error('current_password') is-invalid @enderror" name="current_password">
@error('current_password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Новый пароль</label>
<input type="password" class="form-control @error('password') is-invalid @enderror" name="password">
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label">Подтверждение пароля</label>
<input type="password" class="form-control" name="password_confirmation">
</div>
<div class="mt-4">
<button type="submit" class="btn btn-dark-green px-4">Сохранить изменения</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,97 @@
@extends('layouts.app')
@section('title', 'Заказ ' . $order->order_number)
@php $style = 'profile'; @endphp
@section('content')
<div class="container py-4">
@include('profile.partials.profile-sidebar', compact('user'))
<div class="col-md-9">
<div class="card border-0">
<div class="card-body p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0">Заказ #{{ $order->order_number }}</h4>
<a href="{{ route('profile.index') }}" class="btn btn-outline-secondary btn-sm">
Назад
</a>
</div>
<div class="row mb-4">
<div class="col-md-6">
<p><strong>Дата заказа:</strong> {{ $order->created_at->format('d.m.Y H:i') }}</p>
<p><strong>Способ доставки:</strong>
@switch($order->delivery_method)
@case('courier') Курьером @break
@case('pickup') Самовывоз @break
@case('express') Экспресс @break
@endswitch
</p>
<p><strong>Статус доставки:</strong>
<span class="badge bg-{{ $order->delivery_status_color }}">
{{ $order->delivery_status_name }}
</span>
</p>
</div>
<div class="col-md-6">
<p><strong>Способ оплаты:</strong>
@switch($order->payment_method)
@case('cash') Наличными при получении @break
@case('card') Картой при получении @break
@case('online') Онлайн-оплата @break
@endswitch
</p>
<p><strong>Статус оплаты:</strong>
<span class="badge bg-{{ $order->payment_status === 'paid' ? 'success' : 'warning' }}">
{{ $order->payment_status_name }}
</span>
</p>
@if($order->shipping_address)
<p><strong>Адрес доставки:</strong> {{ $order->shipping_address }}</p>
@endif
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th>Товар</th>
<th>Вариация</th>
<th>Кол-во</th>
<th>Цена</th>
<th>Сумма</th>
</tr>
</thead>
<tbody>
@foreach($order->items as $item)
<tr>
<td>{{ $item->product_name }}</td>
<td>{{ $item->variation_name ?? '—' }}</td>
<td>{{ $item->quantity }}</td>
<td>{{ number_format($item->price, 0, '.', ' ') }} </td>
<td>{{ number_format($item->total, 0, '.', ' ') }} </td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr class="table-light">
<td colspan="4" class="text-end fw-bold">Итого:</td>
<td class="fw-bold">{{ number_format($order->total, 0, '.', ' ') }} </td>
</tr>
</tfoot>
</table>
</div>
@if($order->comment)
<div class="mt-4">
<p><strong>Комментарий к заказу:</strong></p>
<p class="text-muted">{{ $order->comment }}</p>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,28 @@
<div class="row">
<div class="col-md-3 mb-4">
<div class="card border-0 tw-bg-orange tw-text-light-gray">
<div class="card-body p-4 text-center">
<div class="mb-3">
<i class="bi bi-person-circle fs-1"></i>
</div>
<h5>{{ $user->name }}</h5>
<p class="small">{{ $user->email }}</p>
<hr>
<nav class="nav flex-column mt-2 mb-2">
<a href="{{ route('profile.index') }}" class="nav-link py-2 px-0 {{ request()->routeIs('profile.index') ? 'active' : '' }}">
<i class="bi bi-speedometer2 me-2"></i> Обзор
</a>
<a href="{{ route('profile.edit') }}" class="nav-link py-2 px-0 {{ request()->routeIs('profile.edit') ? 'active' : '' }}">
<i class="bi bi-gear me-2"></i> Настройки профиля
</a>
<hr class="my-2">
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-link nav-link py-2 px-0 text-start">
<i class="bi bi-box-arrow-right me-2"></i> Выйти
</button>
</form>
</nav>
</div>
</div>
</div>

View File

@@ -0,0 +1,101 @@
@extends('layouts.app')
@section('title', 'Личный кабинет')
@php $style = 'profile'; @endphp
@section('content')
<div class="container py-4">
@include('profile.partials.profile-sidebar', compact('user'))
<div class="col-md-9">
<div class="card border-0">
<div class="card-body p-4">
<h4 class="mb-4">Мои заказы</h4>
@if($activeOrders->count())
<h5 class="mb-3">Активные заказы</h5>
<div class="table-responsive mb-4">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th> заказа</th>
<th>Дата</th>
<th>Сумма</th>
<th>Статус</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach($activeOrders as $order)
<tr>
<td>{{ $order->order_number }}</td>
<td>{{ $order->created_at->format('d.m.Y H:i') }}</td>
<td>{{ number_format($order->total, 0, '.', ' ') }} </td>
<td>
<span class="badge bg-{{ $order->delivery_status_color }}">
{{ $order->delivery_status_name }}
</span>
</td>
<td>
<a href="{{ route('profile.order.show', $order->id) }}" class="btn btn-sm btn-outline-secondary">
Детали
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if($archiveOrders->count())
<h5 class="mb-3">Архив заказов</h5>
<div class="table-responsive">
<table class="table table-bordered">
<thead class="table-light">
<tr>
<th> заказа</th>
<th>Дата</th>
<th>Сумма</th>
<th>Статус</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach($archiveOrders as $order)
<tr>
<td>{{ $order->order_number }}</td>
<td>{{ $order->created_at->format('d.m.Y H:i') }}</td>
<td>{{ number_format($order->total, 0, '.', ' ') }} </td>
<td>
<span class="badge bg-{{ $order->delivery_status_color }}">
{{ $order->delivery_status_name }}
</span>
</td>
<td>
<a href="{{ route('profile.order.show', $order->id) }}" class="btn btn-sm btn-outline-secondary">
Детали
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if($activeOrders->isEmpty() && $archiveOrders->isEmpty())
<div class="text-center py-5">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted mt-2">У вас пока нет заказов</p>
<a href="{{ route('index') }}" class="btn btn-dark-green mt-3">Перейти к покупкам</a>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,81 @@
@extends('layouts.app')
@section('title', 'Поиск: ' . $query)
@section('content')
<div class="container py-4">
{{-- Хлебные крошки --}}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{{ route('index') }}">Главная</a>
</li>
<li class="breadcrumb-item active">Поиск</li>
</ol>
</nav>
<h1 class="h2 mb-4">Результаты поиска: "{{ $query }}"</h1>
{{-- Бренды --}}
@if($brands->count())
<div class="card border-0 shadow-sm mb-5">
<div class="card-header bg-white">
<h3 class="h5 mb-0">Бренды</h3>
</div>
<div class="card-body">
<div class="row g-3">
@foreach($brands as $brand)
<div class="col-md-3 col-sm-4 col-6">
<a href="{{ route('brands.show', $brand->slug) }}" class="text-decoration-none">
<div class="text-center p-3 border rounded hover-shadow">
@if($brand->logo)
<img src="{{ asset('assets/images/brands/' . $brand->logo) }}"
alt="{{ $brand->name }}"
class="img-fluid mb-2"
style="max-height: 60px;">
@else
<i class="bi bi-building fs-1 text-secondary"></i>
@endif
<p class="mb-0 fw-semibold">{{ $brand->name }}</p>
</div>
</a>
</div>
@endforeach
</div>
</div>
</div>
@endif
{{-- Товары --}}
@if($products->count())
<div class="card border-0 shadow-sm">
<div class="card-header bg-white">
<h3 class="h5 mb-0">Товары ({{ $products->total() }})</h3>
</div>
<div class="card-body">
<div class="row g-4">
@foreach($products as $product)
<div class="col-md-4 col-sm-6">
@include('products.partials.product-card', ['product' => $product])
</div>
@endforeach
</div>
<div class="mt-4">
{{ $products->withQueryString()->links() }}
</div>
</div>
</div>
@endif
{{-- Ничего не найдено --}}
@if($products->isEmpty() && $brands->isEmpty())
<div class="text-center py-5">
<i class="bi bi-search fs-1 text-muted"></i>
<h3 class="mt-3">Ничего не найдено</h3>
<p class="text-muted">Попробуйте изменить поисковый запрос или проверьте орфографию</p>
<a href="{{ route('index') }}" class="btn btn-dark-green mt-3">Вернуться на главную</a>
</div>
@endif
</div>
@endsection