tsett
This commit is contained in:
16
resources/css/app.css
Normal file
16
resources/css/app.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@import "../../node_modules/bootstrap/dist/css/bootstrap.min.css";
|
||||
@import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.btn-orange {
|
||||
@apply tw-bg-orange tw-text-light-gray tw-px-4 tw-py-2 tw-rounded-lg hover:tw-bg-dark-orange hover:tw-text-light-gray focus:tw-bg-dark-orange focus:tw-text-light-gray active:tw-bg-dark-orange active:tw-text-light-gray active:tw-scale-95 tw-transition-all;
|
||||
}
|
||||
|
||||
.btn-dark-green {
|
||||
@apply tw-bg-dark-green tw-text-light-gray tw-px-4 tw-py-2 tw-rounded-lg hover:tw-bg-dark-green-hover focus:tw-bg-dark-green-hover active:tw-bg-dark-green-hover active:tw-scale-95 tw-transition-all;
|
||||
}
|
||||
}
|
||||
7
resources/js/app.js
Normal file
7
resources/js/app.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import '../css/app.css';
|
||||
import * as bootstrap from 'bootstrap';
|
||||
|
||||
import Sortable from 'sortablejs';
|
||||
window.Sortable = Sortable;
|
||||
|
||||
window.bootstrap = bootstrap;
|
||||
32
resources/js/bootstrap.js
vendored
Normal file
32
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
* CSRF token as a header based on the value of the "XSRF" token cookie.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
/**
|
||||
* Echo exposes an expressive API for subscribing to channels and listening
|
||||
* for events that are broadcast by Laravel. Echo and event broadcasting
|
||||
* allows your team to easily build robust real-time web applications.
|
||||
*/
|
||||
|
||||
// import Echo from 'laravel-echo';
|
||||
|
||||
// import Pusher from 'pusher-js';
|
||||
// window.Pusher = Pusher;
|
||||
|
||||
// window.Echo = new Echo({
|
||||
// broadcaster: 'pusher',
|
||||
// key: import.meta.env.VITE_PUSHER_APP_KEY,
|
||||
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
|
||||
// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
|
||||
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
|
||||
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
|
||||
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
|
||||
// enabledTransports: ['ws', 'wss'],
|
||||
// });
|
||||
167
resources/views/about.blade.php
Normal file
167
resources/views/about.blade.php
Normal 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
|
||||
160
resources/views/admin/brands/brands.blade.php
Normal file
160
resources/views/admin/brands/brands.blade.php
Normal 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
|
||||
101
resources/views/admin/brands/edit.blade.php
Normal file
101
resources/views/admin/brands/edit.blade.php
Normal 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
|
||||
245
resources/views/admin/categories/categories.blade.php
Normal file
245
resources/views/admin/categories/categories.blade.php
Normal 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
|
||||
132
resources/views/admin/categories/edit.blade.php
Normal file
132
resources/views/admin/categories/edit.blade.php
Normal 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
|
||||
347
resources/views/admin/contacts/edit.blade.php
Normal file
347
resources/views/admin/contacts/edit.blade.php
Normal 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
|
||||
113
resources/views/admin/dashboard.blade.php
Normal file
113
resources/views/admin/dashboard.blade.php
Normal 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
|
||||
82
resources/views/admin/orders/orders.blade.php
Normal file
82
resources/views/admin/orders/orders.blade.php
Normal 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
|
||||
109
resources/views/admin/orders/show.blade.php
Normal file
109
resources/views/admin/orders/show.blade.php
Normal 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
|
||||
5
resources/views/admin/partials/header.blade.php
Normal file
5
resources/views/admin/partials/header.blade.php
Normal 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>
|
||||
97
resources/views/admin/partials/mobile-nav.blade.php
Normal file
97
resources/views/admin/partials/mobile-nav.blade.php
Normal 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>
|
||||
105
resources/views/admin/partials/sidebar.blade.php
Normal file
105
resources/views/admin/partials/sidebar.blade.php
Normal 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>
|
||||
74
resources/views/admin/permissions/edit.blade.php
Normal file
74
resources/views/admin/permissions/edit.blade.php
Normal 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
|
||||
118
resources/views/admin/permissions/permissions.blade.php
Normal file
118
resources/views/admin/permissions/permissions.blade.php
Normal 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
|
||||
650
resources/views/admin/products/create.blade.php
Normal file
650
resources/views/admin/products/create.blade.php
Normal 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 '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
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
|
||||
832
resources/views/admin/products/edit.blade.php
Normal file
832
resources/views/admin/products/edit.blade.php
Normal 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 '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
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
|
||||
180
resources/views/admin/products/partials/variation-form.blade.php
Normal file
180
resources/views/admin/products/partials/variation-form.blade.php
Normal 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>
|
||||
164
resources/views/admin/products/products.blade.php
Normal file
164
resources/views/admin/products/products.blade.php
Normal 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
|
||||
79
resources/views/admin/roles/create.blade.php
Normal file
79
resources/views/admin/roles/create.blade.php
Normal 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
|
||||
91
resources/views/admin/roles/edit.blade.php
Normal file
91
resources/views/admin/roles/edit.blade.php
Normal 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
|
||||
90
resources/views/admin/roles/roles.blade.php
Normal file
90
resources/views/admin/roles/roles.blade.php
Normal 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
|
||||
100
resources/views/admin/users/create.blade.php
Normal file
100
resources/views/admin/users/create.blade.php
Normal 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
|
||||
98
resources/views/admin/users/edit.blade.php
Normal file
98
resources/views/admin/users/edit.blade.php
Normal 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
|
||||
104
resources/views/admin/users/users.blade.php
Normal file
104
resources/views/admin/users/users.blade.php
Normal 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
|
||||
51
resources/views/auth/login.blade.php
Normal file
51
resources/views/auth/login.blade.php
Normal 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
|
||||
56
resources/views/auth/register.blade.php
Normal file
56
resources/views/auth/register.blade.php
Normal 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
|
||||
34
resources/views/brands/brands.blade.php
Normal file
34
resources/views/brands/brands.blade.php
Normal 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
|
||||
180
resources/views/brands/show.blade.php
Normal file
180
resources/views/brands/show.blade.php
Normal 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()">×</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
|
||||
270
resources/views/cart/checkout.blade.php
Normal file
270
resources/views/cart/checkout.blade.php
Normal 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
|
||||
299
resources/views/cart/show.blade.php
Normal file
299
resources/views/cart/show.blade.php
Normal 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
|
||||
@@ -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>
|
||||
94
resources/views/categories/partials/ui-slider.blade.php
Normal file
94
resources/views/categories/partials/ui-slider.blade.php
Normal 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>
|
||||
191
resources/views/categories/show.blade.php
Normal file
191
resources/views/categories/show.blade.php
Normal 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()">×</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
|
||||
102
resources/views/contacts.blade.php
Normal file
102
resources/views/contacts.blade.php
Normal 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
|
||||
18
resources/views/index.blade.php
Normal file
18
resources/views/index.blade.php
Normal 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
|
||||
44
resources/views/layouts/admin.blade.php
Normal file
44
resources/views/layouts/admin.blade.php
Normal 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>
|
||||
125
resources/views/layouts/app.blade.php
Normal file
125
resources/views/layouts/app.blade.php
Normal 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">© <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>
|
||||
1
resources/views/maket.blade.php
Normal file
1
resources/views/maket.blade.php
Normal file
@@ -0,0 +1 @@
|
||||
@extends('layouts.app')
|
||||
47
resources/views/partials/index/carousel-discount.blade.php
Normal file
47
resources/views/partials/index/carousel-discount.blade.php
Normal 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>
|
||||
58
resources/views/partials/index/product-carousel.blade.php
Normal file
58
resources/views/partials/index/product-carousel.blade.php
Normal 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
|
||||
58
resources/views/partials/menu/catalog-megamenu.blade.php
Normal file
58
resources/views/partials/menu/catalog-megamenu.blade.php
Normal 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>
|
||||
125
resources/views/partials/menu/menu.blade.php
Normal file
125
resources/views/partials/menu/menu.blade.php
Normal 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>
|
||||
207
resources/views/privacy-policy.blade.php
Normal file
207
resources/views/privacy-policy.blade.php
Normal 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
|
||||
35
resources/views/products/partials/product-card.blade.php
Normal file
35
resources/views/products/partials/product-card.blade.php
Normal 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>
|
||||
461
resources/views/products/show.blade.php
Normal file
461
resources/views/products/show.blade.php
Normal 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
|
||||
86
resources/views/profile/edit.blade.php
Normal file
86
resources/views/profile/edit.blade.php
Normal 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
|
||||
97
resources/views/profile/order-show.blade.php
Normal file
97
resources/views/profile/order-show.blade.php
Normal 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
|
||||
28
resources/views/profile/partials/profile-sidebar.blade.php
Normal file
28
resources/views/profile/partials/profile-sidebar.blade.php
Normal 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>
|
||||
101
resources/views/profile/profile.blade.php
Normal file
101
resources/views/profile/profile.blade.php
Normal 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
|
||||
81
resources/views/search/results.blade.php
Normal file
81
resources/views/search/results.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user