Initial commit
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
ssww23
2026-03-10 00:55:37 +03:00
parent fc0f28d830
commit 93a655235a
155 changed files with 24768 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
@php
$currentProduct = $product ?? null;
$selectedCategoryId = (int) old('category_id', $currentProduct->category_id ?? 0);
$specValues = old('specs', $currentProduct->specs ?? []);
$selectedGalleryPathsForRemoval = collect(old('remove_gallery_paths', []))
->filter(fn ($path) => is_string($path) && trim($path) !== '')
->map(fn (string $path) => trim($path))
->unique()
->values()
->all();
$galleryItems = collect((array) ($currentProduct->gallery_paths ?? []))
->filter(fn ($path) => is_string($path) && trim($path) !== '')
->map(fn (string $path) => trim($path))
->unique()
->filter(fn (string $path) => $path !== ($currentProduct->image_path ?? null))
->map(function (string $path) {
$url = $path;
if (!\Illuminate\Support\Str::startsWith($path, ['http://', 'https://', '/'])) {
$url = \Illuminate\Support\Str::startsWith($path, 'uploads/')
? asset($path)
: '/storage/' . ltrim($path, '/');
}
return [
'path' => $path,
'url' => $url,
];
})
->values()
->all();
@endphp
<label>
Категория
<select id="product-category-select" name="category_id" required>
<option value="">Выберите категорию</option>
@foreach ($categories as $categoryOption)
<option value="{{ $categoryOption->id }}" @selected((int) old('category_id', $currentProduct->category_id ?? 0) === $categoryOption->id)>
{{ $categoryOption->name }}
</option>
@endforeach
</select>
</label>
<label>
Название
<input type="text" name="name" value="{{ old('name', $currentProduct->name ?? '') }}" required>
</label>
<label>
Slug
<input type="text" name="slug" value="{{ old('slug', $currentProduct->slug ?? '') }}" placeholder="Можно оставить пустым">
</label>
<label>
Артикул
<input type="text" name="sku" value="{{ old('sku', $currentProduct->sku ?? '') }}">
</label>
<label>
Изображение товара (jpg, png, webp)
<input class="pc-file-input" type="file" name="image" accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp">
</label>
<label>
Дополнительные изображения (до 12 файлов)
<input
class="pc-file-input"
type="file"
name="gallery_images[]"
accept=".jpg,.jpeg,.png,.webp,image/jpeg,image/png,image/webp"
multiple
>
</label>
<div class="pc-grid pc-grid-2">
<label>
Цена
<input type="number" step="0.01" min="0" name="price" value="{{ old('price', $currentProduct->price ?? '') }}" required>
</label>
<label>
Старая цена
<input type="number" step="0.01" min="0" name="old_price" value="{{ old('old_price', $currentProduct->old_price ?? '') }}">
</label>
</div>
<label>
Остаток
<input type="number" min="0" name="stock" value="{{ old('stock', $currentProduct->stock ?? 0) }}" required>
</label>
<label>
Короткое описание
<input type="text" name="short_description" value="{{ old('short_description', $currentProduct->short_description ?? '') }}">
</label>
<label>
Описание
<textarea name="description">{{ old('description', $currentProduct->description ?? '') }}</textarea>
</label>
@if (!empty($currentProduct?->image_url))
<div class="pc-admin-product-image-current">
<img class="pc-admin-product-image-preview" src="{{ $currentProduct->image_url }}" alt="{{ $currentProduct->name }}" loading="lazy">
<span class="pc-muted">Текущее изображение. Загрузите новое, чтобы заменить.</span>
<label class="pc-checkbox">
<input type="checkbox" name="remove_image" value="1" @checked(old('remove_image'))>
<span>Удалить текущее изображение</span>
</label>
</div>
@endif
@if ($galleryItems !== [])
<div class="pc-admin-product-gallery-current">
<span class="pc-muted">Отметьте изображения, которые нужно удалить. При новой загрузке текущая галерея будет заменена.</span>
<div class="pc-admin-product-gallery-grid">
@foreach ($galleryItems as $galleryItem)
<label class="pc-admin-product-gallery-item">
<img class="pc-admin-product-gallery-preview" src="{{ $galleryItem['url'] }}" alt="Дополнительное изображение товара" loading="lazy">
<span class="pc-checkbox">
<input type="checkbox" name="remove_gallery_paths[]" value="{{ $galleryItem['path'] }}" @checked(in_array($galleryItem['path'], $selectedGalleryPathsForRemoval, true))>
<span>Удалить</span>
</span>
</label>
@endforeach
</div>
</div>
@endif
<div class="pc-spec-fields-wrap">
<h3 class="pc-spec-fields-title">Характеристики категории</h3>
<div id="pc-spec-fields" class="pc-grid pc-grid-2"></div>
</div>
<label class="pc-checkbox">
<input type="checkbox" name="is_active" value="1" @checked(old('is_active', $currentProduct->is_active ?? true))>
<span>Товар активен</span>
</label>
<button class="pc-btn primary" type="submit">{{ $submitLabel ?? 'Сохранить' }}</button>
<script>
(() => {
const categorySelect = document.getElementById('product-category-select');
const specsContainer = document.getElementById('pc-spec-fields');
if (!categorySelect || !specsContainer) {
return;
}
const specsByCategory = @json($categorySpecsById ?? []);
const specValues = @json($specValues);
const renderSpecFields = () => {
const selectedCategoryId = categorySelect.value;
const fields = specsByCategory[selectedCategoryId] || [];
specsContainer.innerHTML = '';
if (fields.length === 0) {
const muted = document.createElement('p');
muted.className = 'pc-muted';
muted.textContent = 'Для выбранной категории нет дополнительных полей.';
specsContainer.appendChild(muted);
return;
}
fields.forEach((field) => {
const label = document.createElement('label');
const title = document.createElement('span');
title.textContent = field.label;
label.appendChild(title);
const input = document.createElement('input');
input.type = 'text';
input.name = `specs[${field.key}]`;
input.value = typeof specValues[field.key] === 'string' ? specValues[field.key] : '';
const options = Array.isArray(field.options)
? [...new Set(field.options.map((value) => String(value).trim()).filter(Boolean))]
: [];
if (options.length > 0) {
const listId = `pc-spec-options-${field.key}`;
input.setAttribute('list', listId);
input.setAttribute('autocomplete', 'off');
const datalist = document.createElement('datalist');
datalist.id = listId;
options.forEach((optionValue) => {
const option = document.createElement('option');
option.value = optionValue;
datalist.appendChild(option);
});
const hint = document.createElement('small');
hint.className = 'pc-field-hint';
hint.textContent = 'Выберите из списка или введите свое значение';
label.appendChild(input);
label.appendChild(datalist);
label.appendChild(hint);
} else {
label.appendChild(input);
}
specsContainer.appendChild(label);
});
};
categorySelect.addEventListener('change', renderSpecFields);
if (categorySelect.value === '' && '{{ $selectedCategoryId }}' !== '0') {
categorySelect.value = '{{ $selectedCategoryId }}';
}
renderSpecFields();
})();
</script>

View File

@@ -0,0 +1,22 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Товары', 'url' => route('admin.products.index')],
['label' => 'Новый товар', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Новый товар</h2>
</div>
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.store') }}" enctype="multipart/form-data">
@csrf
@include('admin.products._form', ['submitLabel' => 'Создать'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,23 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Товары', 'url' => route('admin.products.index')],
['label' => 'Редактирование', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-section-title">
<h2>Редактирование товара</h2>
</div>
<form class="pc-card pc-form" method="post" action="{{ route('admin.products.update', $product) }}" enctype="multipart/form-data">
@csrf
@method('put')
@include('admin.products._form', ['product' => $product, 'submitLabel' => 'Сохранить'])
</form>
</section>
@endsection

View File

@@ -0,0 +1,58 @@
@extends('layouts.shop')
@section('content')
@include('partials.breadcrumbs', [
'items' => [
['label' => 'Админка', 'url' => route('admin.dashboard')],
['label' => 'Товары', 'url' => null],
],
])
<section class="pc-section">
<div class="pc-category-toolbar">
<div class="pc-section-title">
<h2>Товары</h2>
</div>
<div class="pc-product-actions">
<a class="pc-btn primary" href="{{ route('admin.products.create') }}">Добавить товар</a>
<a class="pc-btn ghost" href="{{ route('admin.products.export') }}">Экспорт CSV</a>
</div>
</div>
<div class="pc-card">
<h3>Импорт из CSV</h3>
<p class="pc-muted">Загрузите CSV файл (UTF-8). Поддерживаются колонки category_slug/category_name, name, price, stock и дополнительные характеристики.</p>
<form class="pc-product-actions" method="post" action="{{ route('admin.products.import') }}" enctype="multipart/form-data">
@csrf
<input class="pc-file-input" type="file" name="csv_file" accept=".csv,text/csv" required>
<button class="pc-btn ghost" type="submit">Импорт CSV</button>
</form>
</div>
<div class="pc-card">
@if ($products->isEmpty())
<p>Товары пока не созданы.</p>
@else
<div class="pc-account-orders">
@foreach ($products as $product)
<div class="pc-account-order">
<span>{{ $product->name }} ({{ $product->category?->name ?? 'Без категории' }})</span>
<div class="pc-product-actions">
<a class="pc-btn ghost" href="{{ route('admin.products.edit', $product) }}">Редактировать</a>
<form method="post" action="{{ route('admin.products.destroy', $product) }}">
@csrf
@method('delete')
<button class="pc-btn ghost" type="submit">Удалить</button>
</form>
</div>
</div>
@endforeach
</div>
@endif
</div>
<div class="pc-pagination">
{{ $products->links() }}
</div>
</section>
@endsection