Commit d5a7844e authored by Vasko Mitevski's avatar Vasko Mitevski
Browse files

Merge branch 'product_handle' into 'dev'

in the middle of product creating, request done without error messages,...

See merge request !5
2 merge requests!14in the middle of product creating, request done without error messages,...,!5in the middle of product creating, request done without error messages,...
Showing with 421 additions and 39 deletions
+421 -39
Добредојде во мојот проект Админ панел за продавницата Игралиште!
- Кратко објаснување и патоказ за различните функционалности низ проектот -Админ панел Игралиште -
--- Кратко објаснување и патоказ за различните функционалности низ проектот -Админ панел Игралиште ---
- Прво би те замолил да ги игнорираш дел од коментарите низ кодот. Голем дел од нив се оставани од мене за мене.
- Можеби во иднина би било подобро при внесување на нов продукт, ако количината би била повеќе од 1 парче, овде во овој момент да има админот можност да одбере боја и величина соодветно за сите парчиња, затоа што вака како што е креиран FrontEnd Админ панелот, админот ќе мора засебно да ги внесува продуктите (величина и боја)
Пр:
треба да внесе 3 нови парчиња од истиот продукт
(2 x L (црвено и плаво) и 1 x S (розево))
за овие 3 продукти, вака како што е поставен FrontEnd-от админот ќе треба 3 пати да ја пополнува истата форма. (чисто како размислување за модификација во иднина) (можев да го направам, не сакав да го менам дизајнот)
за овие 3 продукти, вака како што е поставен FrontEnd-от админот ќе треба 3 пати да ја пополнува истата форма. (чисто како размислување за модификација во иднина) (можев да го направам, но не сакав да го менам дизајнот)
- Кај внесување односно одбирање на боја, намерно ги направив така хард-кодирани бои, пошто очигледно ќе има одредени бои од кои ќе бира админот која ќе ја има на лагер.
......
......@@ -6,7 +6,10 @@
use App\Models\Product;
use App\Models\Category;
use App\Models\Discount;
use App\Models\ProductColor;
use App\Models\ProductImage;
use Illuminate\Http\Request;
use App\Http\Requests\ProductRequest;
class ProductController extends Controller
{
......@@ -34,9 +37,33 @@ public function create()
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
public function store(ProductRequest $request)
{
//
// Validate the incoming request using the ProductRequest
$validated = $request->validated();
// Create a new product with the validated data
$product = Product::create($validated);
// Create product colors
foreach ($validated['colors'] as $color) {
ProductColor::create([
'product_id' => $product->id,
'color' => $color,
]);
}
// Create product images
foreach ($request->file('images') as $image) {
// Store the image in the storage/app/public directory
$path = $image->store('public/images/products');
// Create a new product image record
ProductImage::create([
'product_id' => $product->id,
'image' => $path,
]);
}
}
/**
......
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ProductRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255|unique:products',
'description' => 'required|string',
'price' => 'required|numeric|min:0',
'stock_quantity' => 'required|numeric|min:1',
'size' => 'required',
'size_advice' => 'required|string',
'colors' => 'required|array|min:1',
'colors.*' => 'required|string',
'maintenance' => 'required|string',
'tags' => 'required|string|starts_with:#',
'file.*' => 'image|mimes:jpeg,png,jpg,gif,svg|max:2048',
'category_id' => 'required',
'brand_id' => 'required',
'discount_id' => 'nullable',
];
}
public function messages()
{
return [
'name.required' => 'The name field is required.',
'name.string' => 'The name must be a string.',
'name.max' => 'The name may not be greater than :max characters.',
'name.unique' => 'The name has already been taken.',
'description.required' => 'The description field is required.',
'description.string' => 'The description must be a string.',
'price.required' => 'The price field is required.',
'price.numeric' => 'The price must be a number.',
'price.min' => 'The price must be at least :min.',
'stock_quantity.required' => 'The stock quantity field is required.',
'stock_quantity.numeric' => 'The stock quantity must be a number.',
'stock_quantity.min' => 'The stock quantity must be at least :min.',
'size.required' => 'The size field is required.',
'size_advice.required' => 'The size advice field is required.',
'colors.required' => 'At least one color must be selected.',
'maintenance.required' => 'The maintenance field is required.',
'tags.required' => 'The tags field is required and must start with #.',
'file.*.image' => 'The file must be an image.',
'file.*.mimes' => 'The file must be a file of type: :values.',
'file.*.max' => 'The file must not be greater than :max kilobytes.',
'category_id.required' => 'The category field is required.',
'brand_id.required' => 'The brand field is required.',
];
}
}
......@@ -10,5 +10,19 @@ class Brand extends Model
use HasFactory;
protected $fillable = ['name', 'description', 'tags', 'status'];
/**
* Get the images associated with the brand.
*/
public function images()
{
return $this->hasMany(BrandImage::class);
}
/**
* Get the products associated with the brand.
*/
public function products()
{
return $this->hasMany(Product::class);
}
}
......@@ -10,4 +10,12 @@ class BrandImage extends Model
use HasFactory;
protected $fillable = ['image', 'brand_id'];
/**
* Get the brand that owns the image.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
}
......@@ -15,4 +15,28 @@ class Product extends Model
'size', 'size_advice', 'maintenance', 'tags',
'discount_id', 'category_id', 'brand_id'
];
/**
* Get the images associated with the product.
*/
public function images()
{
return $this->hasMany(ProductImage::class);
}
/**
* Get the brand that owns the product.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
/**
* Get the colors associated with the product.
*/
public function colors()
{
return $this->belongsToMany(Color::class, 'product_colors');
}
}
......@@ -10,4 +10,12 @@ class ProductColor extends Model
use HasFactory;
protected $fillable = ['color_id', 'product_id'];
/**
* Get the product that owns the color.
*/
public function product()
{
return $this->belongsTo(Product::class);
}
}
......@@ -10,4 +10,12 @@ class ProductImage extends Model
use HasFactory;
protected $fillable = ['image', 'product_id'];
/**
* Get the product that owns the image.
*/
public function product()
{
return $this->belongsTo(Product::class);
}
}
......@@ -13,7 +13,7 @@
"laravel/ui": "^4.5"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
"fakerphp/faker": "^1.23",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
......
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "a5e587af66df05d6a57013c70a6458f4",
"content-hash": "5047c9cd4d192894f0bf89c9ce834079",
"packages": [
{
"name": "brick/math",
......
......@@ -21,7 +21,7 @@ public function up(): void
$table->string('size_advice');
$table->text('maintenance');
$table->string('tags');
$table->foreignId('discount_id')->constrained();
$table->foreignId('discount_id')->nullable()->constrained();
$table->foreignId('category_id')->constrained();
$table->foreignId('brand_id')->constrained();
$table->softDeletes();
......
<?php
namespace Database\Seeders;
use App\Models\Brand;
use App\Models\BrandImage;
use Faker\Factory as Faker;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\File;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
class BrandsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run()
{
$faker = Faker::create();
// Define the directory for storing images
$imageDirectory = storage_path('app/public/images/brands');
// Ensure the directory exists and is writable
if (!File::exists($imageDirectory)) {
File::makeDirectory($imageDirectory, 0755, true);
}
// Create 5 clothes brands
for ($i = 0; $i < 5; $i++) {
// Create a brand
$brand = Brand::create([
'name' => $faker->company,
'description' => $faker->paragraph,
'tags' => 'clothes',
'status' => 'active'
]);
// Create 2 fake images for the brand
for ($j = 0; $j < 2; $j++) {
$imageFilename = $faker->image($imageDirectory, 400, 300, null, false);
// Store the fake image file
$imagePath = 'public/images/brands/' . $imageFilename;
$image = BrandImage::create([
'image' => $imagePath,
'brand_id' => $brand->id
]);
}
}
}
}
......@@ -21,5 +21,6 @@ public function run(): void
$this->call(UsersSeeder::class);
$this->call(CategorySeeder::class);
$this->call(BrandsTableSeeder::class);
}
}
......@@ -4,7 +4,7 @@ span.product {
}
.input-group .btn {
width: 40px; /* Adjust the width as needed */
width: 40px;
}
input.stock-quantity {
......@@ -35,7 +35,6 @@ input[type="checkbox"] {
display: none;
}
/* Style the custom checkbox */
.color-checkbox {
width: 20px;
height: 20px;
......@@ -66,4 +65,62 @@ label.pink {
background-color: pink;
}
.color-checkbox.checked {
border-color: aqua;
border: 5px double;
}
/* делот со сликите */
.image-grid {
display: flex;
gap: 10px;
}
.image-upload-box {
position: relative;
width: calc(25% - 10px);
padding-top: calc(25% - 1px);
padding-bottom: 14px;
border: 2px solid #ccc;
cursor: pointer;
overflow: hidden;
}
.image-upload-box input[type="file"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
z-index: 1;
}
.image-upload-box i {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 24px;
}
.image-container {
position: relative;
width: 100%;
padding-top: 100%;
overflow: hidden;
background-color: #f0f0f0;
margin-top: -100%;
z-index: 0;
}
.uploaded-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
......@@ -3,9 +3,7 @@ document.addEventListener("DOMContentLoaded", function () {
document.getElementById("name").focus();
document.getElementById("minus-btn").addEventListener("click", function () {
console.log("Minus button clicked");
var input = document.getElementById("stock_quantity");
console.log("Input value before decrement:", input.value);
var value = parseInt(input.value);
if (!isNaN(value) && value > 0) {
input.value = value - 1;
......@@ -13,18 +11,103 @@ document.addEventListener("DOMContentLoaded", function () {
});
document.getElementById("plus-btn").addEventListener("click", function () {
console.log("Plus button clicked");
var input = document.getElementById("stock_quantity");
console.log("Input value before increment:", input.value);
var value = parseInt(input.value);
if (!isNaN(value)) {
input.value = value + 1;
}
});
//чек боксовите за боја
const colorCheckboxes = document.querySelectorAll(".color-checkbox-input");
colorCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener("change", function () {
const label = this.nextElementSibling;
if (this.checked) {
label.classList.add("checked");
} else {
label.classList.remove("checked");
}
});
});
// споредба на тоа колку бои ќе одбереме со колку парчиња на лагер сме одбрале
// не може админот да одбере повеќе врсти на бои од количината на лагер.
const stockQuantityInput = document.getElementById("stock_quantity");
stockQuantityInput.addEventListener("input", function () {
const maxColors = parseInt(this.value);
updateColorCheckboxValidity(maxColors);
});
colorCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener("change", function () {
updateColorCheckboxValidity(parseInt(stockQuantityInput.value));
});
});
function updateColorCheckboxValidity(maxColors) {
const checkedColorCheckboxes = document.querySelectorAll(
".color-checkbox-input:checked"
);
const uncheckedColorCheckboxes = document.querySelectorAll(
'.color-checkbox-input[type="checkbox"]:not(:checked)'
);
if (checkedColorCheckboxes.length > maxColors) {
alert("Не може да се селектират повеќе бои отколку што има парчиња на магацин");
uncheckedColorCheckboxes.forEach(function (checkbox) {
if (confirm) {
const lastCheckedCheckbox =
checkedColorCheckboxes[
checkedColorCheckboxes.length - 1
];
lastCheckedCheckbox.checked = false;
const label = lastCheckedCheckbox.nextElementSibling;
label.classList.remove("checked");
}
checkbox.disabled = true;
});
} else {
uncheckedColorCheckboxes.forEach(function (checkbox) {
checkbox.disabled = false;
});
}
}
// делот со 4-те слики
const imageInputs = document.querySelectorAll(
'.image-upload-box input[type="file"]'
);
imageInputs.forEach(function (input) {
input.addEventListener("change", function (event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function (e) {
const img =
input.parentElement.querySelector(".uploaded-image");
img.src = e.target.result;
input.parentElement.querySelector(
".image-container + i"
).style.display = "none";
};
reader.readAsDataURL(file);
});
});
//копчето -откажи- на крајо од формата
document.getElementById("cancel").addEventListener("click", function () {
document.getElementById("create_product").reset();
const colorCheckboxes = document.querySelectorAll(
".color-checkbox-input"
);
colorCheckboxes.forEach(function (checkbox) {
checkbox.checked = false;
const label = checkbox.nextElementSibling;
label.classList.remove("checked");
});
window.scrollTo({ top: 0, behavior: "smooth" });
document.getElementById("name").focus();
});
......
......@@ -3,17 +3,13 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
@vite('resources/sass/app.scss')
<link rel="stylesheet" href="{{ asset('css/admin-login.css') }}?v=1">
......@@ -27,7 +23,6 @@
</main>
</div>
<!-- Scripts -->
@stack('scripts')
</body>
</html>
\ No newline at end of file
......@@ -23,12 +23,19 @@
</div>
<div class="mb-3">
<label for="name" class="form-label">Име на продукт</label>
<input type="text" class="form-control" id="name" name="name">
<input type="text" class="form-control" id="name" name="name" value="{{ old('name') }}">
@if ($errors->has('name'))
<span class="text-danger">{{ $errors->first('name') }}</span>
@endif
</div>
<div class="mb-3">
<label for="description" class="form-label">Опис</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
<textarea class="form-control" id="description" name="description" rows="3">
</textarea>
@if ($errors->has('description'))
<span class="text-danger">{{ $errors->first('description') }}</span>
@endif
</div>
<div class="mb-3 row">
<label for="price" class="form-label col-md-3">Цена</label>
......@@ -78,33 +85,33 @@
</div>
<div class="mb-3">
<label class="form-label">Colors</label><br>
<label class="form-label">Боја</label><br>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_black" name="colors[]" value="black">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_black" name="colors[]" value="black">
<label class="black color-checkbox" for="color_black"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_white" name="colors[]" value="white">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_white" name="colors[]" value="white">
<label class="white color-checkbox" for="color_white"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_yellow" name="colors[]" value="yellow">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_yellow" name="colors[]" value="yellow">
<label class="yellow color-checkbox" for="color_yellow"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_blue" name="colors[]" value="blue">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_blue" name="colors[]" value="blue">
<label class="blue color-checkbox" for="color_blue"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_green" name="colors[]" value="green">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_green" name="colors[]" value="green">
<label class="green color-checkbox" for="color_green"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_red" name="colors[]" value="red">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_red" name="colors[]" value="red">
<label class="red color-checkbox" for="color_red"></label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="color_pink" name="colors[]" value="pink">
<input class="form-check-input color-checkbox-input" type="checkbox" id="color_pink" name="colors[]" value="pink">
<label class="pink color-checkbox" for="color_pink"></label>
</div>
</div>
......@@ -121,21 +128,33 @@
<div class="mb-3">
<label for="images" class="form-label">Слики (Максимум 4)</label>
<div id="image-selection">
<div class="image-selection-item">
<input type="file" class="form-control" accept="image/*" style="display: none;">
<div class="image-grid">
<div class="image-upload-box">
<input type="file" class="form-control" accept="image/*">
<div class="image-container">
<img src="" class="uploaded-image">
</div>
<i class="fas fa-plus"></i>
</div>
<div class="image-selection-item">
<input type="file" class="form-control" accept="image/*" style="display: none;">
<div class="image-upload-box">
<input type="file" class="form-control" accept="image/*">
<div class="image-container">
<img src="" class="uploaded-image">
</div>
<i class="fas fa-plus"></i>
</div>
<div class="image-selection-item">
<input type="file" class="form-control" accept="image/*" style="display: none;">
<div class="image-upload-box">
<input type="file" class="form-control" accept="image/*">
<div class="image-container">
<img src="" class="uploaded-image">
</div>
<i class="fas fa-plus"></i>
</div>
<div class="image-selection-item">
<input type="file" class="form-control" accept="image/*" style="display: none;">
<div class="image-upload-box">
<input type="file" class="form-control" accept="image/*">
<div class="image-container">
<img src="" class="uploaded-image">
</div>
<i class="fas fa-plus"></i>
</div>
</div>
......@@ -175,7 +194,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="discountModalLabel">Select Discount</h5>
<h5 class="modal-title" id="discountModalLabel">Одбери Попуст</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment