Merge pull request #18 from jitendra-webkul/jitendra
Product Image Upload Added
This commit is contained in:
commit
ebea698c72
|
|
@ -13,7 +13,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DRIVER', 'local'),
|
||||
'default' => env('FILESYSTEM_DRIVER', 'public'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
@ -51,7 +51,7 @@ return [
|
|||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => env('APP_URL').'/storage',
|
||||
'url' => env('APP_URL').'/public/storage',
|
||||
'visibility' => 'public',
|
||||
],
|
||||
|
||||
|
|
|
|||
|
|
@ -131,13 +131,13 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<!--<div class="control-group">
|
||||
<div class="control-group">
|
||||
<label for="value_per_channel">{{ __('admin::app.catalog.attributes.value_per_channel') }}</label>
|
||||
<select class="control" id="value_per_channel" name="value_per_channel">
|
||||
<option value="0">{{ __('admin::app.catalog.attributes.no') }}</option>
|
||||
<option value="1">{{ __('admin::app.catalog.attributes.yes') }}</option>
|
||||
</select>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="is_filterable">{{ __('admin::app.catalog.attributes.is_filterable') }}</label>
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@
|
|||
<input type="hidden" name="value_per_locale" value="{{ $attribute->value_per_locale }}"/>
|
||||
</div>
|
||||
|
||||
<!--<div class="control-group">
|
||||
<div class="control-group">
|
||||
<label for="value_per_channel">{{ __('admin::app.catalog.attributes.value_per_channel') }}</label>
|
||||
<select class="control" id="value_per_channel" name="value_per_channel" disabled>
|
||||
<option value="0" {{ $attribute->value_per_channel ? '' : 'selected' }}>
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
</option>
|
||||
</select>
|
||||
<input type="hidden" name="value_per_channel" value="{{ $attribute->value_per_channel }}"/>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="is_filterable">{{ __('admin::app.catalog.attributes.is_filterable') }}</label>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<accordian :title="'{{ __($accordian['name']) }}'" :active="true">
|
||||
<div slot="body">
|
||||
|
||||
<image-wrapper :button-label="'{{ __('admin::app.catalog.products.add-image-btn-title') }}'" input-name="images" multiple="true"></image-wrapper>
|
||||
|
||||
<image-wrapper :button-label="'{{ __('admin::app.catalog.products.add-image-btn-title') }}'" input-name="images" :images='@json($product->images)'></image-wrapper>
|
||||
|
||||
</div>
|
||||
</accordian>
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<?php $locale = request()->get('locale') ?: app()->getLocale(); ?>
|
||||
<?php $channel = request()->get('channel') ?: channel()->getChannel(); ?>
|
||||
|
||||
<form method="POST" action="" @submit.prevent="onSubmit">
|
||||
<form method="POST" action="" @submit.prevent="onSubmit" enctype="multipart/form-data">
|
||||
|
||||
<div class="page-header">
|
||||
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
|
||||
@section('javascript')
|
||||
<script>
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#channel-switcher, #locale-switcher').on('change', function (e) {
|
||||
$('#channel-switcher').val()
|
||||
|
|
|
|||
|
|
@ -18,4 +18,8 @@ class Channel
|
|||
|
||||
return $channel->code;
|
||||
}
|
||||
|
||||
public function getChannelModel() {
|
||||
return ChannelModel::first();
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,9 @@ class CreateProductImagesTable extends Migration
|
|||
{
|
||||
Schema::create('product_images', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->string('type');
|
||||
$table->string('type')->nullable();
|
||||
$table->string('path');
|
||||
$table->integer('product_id')->unsigned()->nullable();
|
||||
$table->integer('product_id')->unsigned();
|
||||
$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,8 +161,6 @@ class ProductController extends Controller
|
|||
*/
|
||||
public function update(ProductForm $request, $id)
|
||||
{
|
||||
dd(request()->all());
|
||||
|
||||
$this->product->update(request()->all(), $id);
|
||||
|
||||
session()->flash('success', 'Product updated successfully.');
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ namespace Webkul\Product\Models;
|
|||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Webkul\Product\Models\Product;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ProductImage extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [];
|
||||
protected $fillable = ['path', 'product_id'];
|
||||
|
||||
/**
|
||||
* Get the product that owns the image.
|
||||
|
|
@ -18,4 +19,42 @@ class ProductImage extends Model
|
|||
{
|
||||
return $this->belongsTo(Product::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image url for the product image.
|
||||
*/
|
||||
public function url()
|
||||
{
|
||||
return Storage::url($this->path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image url for the product image.
|
||||
*/
|
||||
public function getUrlAttribute()
|
||||
{
|
||||
return $this->url();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCustomAttribute($attribute)
|
||||
{
|
||||
return $this->attribute_family->custom_attributes->pluck('code')->contains($attribute);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function toArray()
|
||||
{
|
||||
$array = parent::toArray();
|
||||
|
||||
$array['url'] = $this->url;
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace Webkul\Product\Repositories;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Webkul\Core\Eloquent\Repository;
|
||||
|
||||
/**
|
||||
* Product Image Reposotory
|
||||
*
|
||||
* @author Jitendra Singh <jitendra@webkul.com>
|
||||
* @copyright 2018 Webkul Software Pvt Ltd (http://www.webkul.com)
|
||||
*/
|
||||
class ProductImageRepository extends Repository
|
||||
{
|
||||
/**
|
||||
* Specify Model class name
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
function model()
|
||||
{
|
||||
return 'Webkul\Product\Models\ProductImage';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $data
|
||||
* @param mixed $product
|
||||
* @return mixed
|
||||
*/
|
||||
public function uploadImages($data, $product)
|
||||
{
|
||||
$previousImageIds = $product->images()->pluck('id');
|
||||
|
||||
if(isset($data['images'])) {
|
||||
foreach ($data['images'] as $imageId => $image) {
|
||||
$file = 'images.' . $imageId;
|
||||
$dir = 'product/' . $product->id;
|
||||
|
||||
if (str_contains($imageId, 'image_')) {
|
||||
$this->create([
|
||||
'path' => request()->file($file)->store($dir),
|
||||
'product_id' => $product->id
|
||||
]);
|
||||
} else {
|
||||
if(is_numeric($index = $previousImageIds->search($imageId))) {
|
||||
$previousImageIds->forget($index);
|
||||
}
|
||||
|
||||
if(request()->hasFile($file)) {
|
||||
if($imageModel = $this->find($imageId)) {
|
||||
Storage::delete($imageModel->path);
|
||||
}
|
||||
|
||||
$this->update([
|
||||
'path' => request()->file($file)->store($dir)
|
||||
], $imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($previousImageIds as $imageId) {
|
||||
if($imageModel = $this->find($imageId)) {
|
||||
Storage::delete($imageModel->path);
|
||||
|
||||
$this->delete($imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ use Webkul\Attribute\Repositories\AttributeRepository;
|
|||
use Webkul\Attribute\Repositories\AttributeOptionRepository;
|
||||
use Webkul\Product\Repositories\ProductAttributeValueRepository;
|
||||
use Webkul\Product\Repositories\ProductInventoryRepository;
|
||||
use Webkul\Product\Repositories\ProductImageRepository;
|
||||
use Webkul\Product\Models\ProductAttributeValue;
|
||||
|
||||
/**
|
||||
|
|
@ -46,6 +47,13 @@ class ProductRepository extends Repository
|
|||
*/
|
||||
protected $productInventory;
|
||||
|
||||
/**
|
||||
* ProductImageRepository object
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $productImage;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
|
|
@ -53,6 +61,7 @@ class ProductRepository extends Repository
|
|||
* @param Webkul\Attribute\Repositories\AttributeOptionRepository $attributeOption
|
||||
* @param Webkul\Product\Repositories\ProductAttributeValueRepository $attributeValue
|
||||
* @param Webkul\Product\Repositories\ProductInventoryRepository $productInventory
|
||||
* @param Webkul\Product\Repositories\ProductImageRepository $productImage
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(
|
||||
|
|
@ -60,6 +69,7 @@ class ProductRepository extends Repository
|
|||
AttributeOptionRepository $attributeOption,
|
||||
ProductAttributeValueRepository $attributeValue,
|
||||
ProductInventoryRepository $productInventory,
|
||||
ProductImageRepository $productImage,
|
||||
App $app)
|
||||
{
|
||||
$this->attribute = $attribute;
|
||||
|
|
@ -70,6 +80,8 @@ class ProductRepository extends Repository
|
|||
|
||||
$this->productInventory = $productInventory;
|
||||
|
||||
$this->productImage = $productImage;
|
||||
|
||||
parent::__construct($app);
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +195,8 @@ class ProductRepository extends Repository
|
|||
}
|
||||
|
||||
$this->productInventory->saveInventories($data, $product);
|
||||
|
||||
$this->productImage->uploadImages($data, $product);
|
||||
|
||||
return $product;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,55 @@
|
|||
@extends('shop::store.layouts.master')
|
||||
|
||||
@section('content-wrapper')
|
||||
<div class="content">
|
||||
<div class="sign-up-text">
|
||||
Already have an account - <a href="{{ route('customer.session.index') }}">Sign In</a>
|
||||
</div>
|
||||
<form method="post" action="{{ route('customer.register.create') }}">
|
||||
{{ csrf_field() }}
|
||||
<div class="login-form">
|
||||
<div class="login-text">Sign Up</div>
|
||||
<div class="control-group">
|
||||
<label for="email">First Name</label>
|
||||
<input type="text" class="control" name="first_name">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="email">Last Name</label>
|
||||
<input type="text" class="control" name="last_name">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" class="control" name="email">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="email">Password</label>
|
||||
<input type="password" class="control" name="password">
|
||||
</div>
|
||||
<div class="control-group">
|
||||
<label for="email">Confirm Password</label>
|
||||
<input type="password" class="control" name="confirm_password">
|
||||
</div>
|
||||
<div class="signup-confirm">
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" id="checkbox2" name="agreement" required>
|
||||
<label class="checkbox-view" for="checkbox2"></label>
|
||||
<span>Agree <a href="">Terms</a> & <a href="">Conditions</a> by using this website.</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
<input class="btn btn-primary btn-lg" type="submit" value="sign in">
|
||||
<div class="content">
|
||||
<div class="sign-up-text">
|
||||
Already have an account - <a href="{{ route('customer.session.index') }}">Sign In</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<form method="post" action="{{ route('customer.register.create') }}">
|
||||
{{ csrf_field() }}
|
||||
|
||||
<div class="login-form">
|
||||
<div class="login-text">Sign Up</div>
|
||||
|
||||
<div class="control-group" :class="[errors.has('first_name') ? 'has-error' : '']">
|
||||
<label for="email">First Name</label>
|
||||
<input type="text" v-validate="'required'" class="control" name="first_name">
|
||||
<span class="control-error" v-if="errors.has('first_name')">@{{ errors.first('first_name') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="email">Last Name</label>
|
||||
<input type="text" class="control" name="last_name">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" class="control" name="email">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="email">Password</label>
|
||||
<input type="password" class="control" name="password">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label for="email">Confirm Password</label>
|
||||
<input type="password" class="control" name="confirm_password">
|
||||
</div>
|
||||
|
||||
<div class="signup-confirm">
|
||||
<span class="checkbox">
|
||||
<input type="checkbox" id="checkbox2" name="agreement" required>
|
||||
<label class="checkbox-view" for="checkbox2"></label>
|
||||
<span>Agree <a href="">Terms</a> & <a href="">Conditions</a> by using this website.</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-lg" type="submit" value="sign in">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="160px" height="114px" viewBox="0 0 160 114" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 50 (54983) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>placeholder-icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Catalog-Product-New-Variable-Product" transform="translate(-144.000000, -1457.000000)" stroke="#C7C7C7" stroke-width="2">
|
||||
<g id="2" transform="translate(104.000000, 860.000000)">
|
||||
<g id="Image" transform="translate(20.000000, 528.000000)">
|
||||
<g id="Images" transform="translate(0.000000, 26.000000)">
|
||||
<g id="Block">
|
||||
<g id="Image-Icon">
|
||||
<g id="placeholder-icon" transform="translate(20.000000, 43.333333)">
|
||||
<rect id="Rectangle-3" x="1" y="1" width="158" height="111.333333"></rect>
|
||||
<rect id="Rectangle-3" x="17.6666667" y="17.6666667" width="121.333333" height="78"></rect>
|
||||
<polyline id="Path-2" points="17.9238281 78.6588542 47.7115392 48.4681532 97.1757812 95.7462565"></polyline>
|
||||
<polyline id="Path-3" points="73.7011719 72.7286606 110.238697 37.3952908 139.06543 66.2701823"></polyline>
|
||||
<circle id="Oval-3" cx="75" cy="41.6666667" r="8.33333333"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<input type="file" :name="finalInputName" ref="imageInput"/>
|
||||
</div>
|
||||
<label class="image-item" :for="_uid" v-bind:class="{ 'has-image': imageData.length > 0 }">
|
||||
<input type="hidden" :name="finalInputName"/>
|
||||
|
||||
<input type="file" accept="image/*" :name="finalInputName" ref="imageInput" :id="_uid" @change="addImageView($event)"/>
|
||||
|
||||
<img class="preview" :src="imageData" v-if="imageData.length > 0">
|
||||
|
||||
<label class="remove-image" @click="removeImage()">{{ removeButtonLabel }}</label>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -13,10 +19,8 @@
|
|||
default: 'attachments'
|
||||
},
|
||||
|
||||
multiple: {
|
||||
type: [Boolean, String],
|
||||
required: false,
|
||||
default: true
|
||||
removeButtonLabel: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
image: {
|
||||
|
|
@ -26,22 +30,42 @@
|
|||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
imageData: ''
|
||||
}
|
||||
},
|
||||
|
||||
mounted () {
|
||||
if(!this.image.id) {
|
||||
var element = this.$refs.imageInput;
|
||||
element.dispatchEvent(new Event("click"));
|
||||
element.click();
|
||||
if(this.image.id && this.image.url) {
|
||||
this.imageData = this.image.url;
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
finalInputName () {
|
||||
if(this.multiple)
|
||||
return this.inputName + '[]';
|
||||
|
||||
return this.inputName;
|
||||
return this.inputName + '[' + this.image.id + ']';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
addImageView () {
|
||||
var imageInput = this.$refs.imageInput;
|
||||
|
||||
if (imageInput.files && imageInput.files[0]) {
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.onload = (e) => {
|
||||
this.imageData = e.target.result;
|
||||
}
|
||||
|
||||
reader.readAsDataURL(imageInput.files[0]);
|
||||
}
|
||||
},
|
||||
|
||||
removeImage () {
|
||||
this.$emit('onRemoveImage', this.image)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="image-wrapper">
|
||||
<image-item v-for='(image, index) in images' :key='image.uid' :image="image" :input-name="inputName" :multiple="multiple" @onRemoveImage="removImage($event)"></image-item>
|
||||
<image-item
|
||||
v-for='(image, index) in items'
|
||||
:key='image.id'
|
||||
:image="image"
|
||||
:input-name="inputName"
|
||||
:remove-button-label="removeButtonLabel"
|
||||
@onRemoveImage="removeImage($event)"
|
||||
></image-item>
|
||||
</div>
|
||||
|
||||
<label class="btn btn-lg btn-primary" style="display: inline-block" @click="createFileType">{{ buttonLabel }}</label>
|
||||
|
|
@ -17,18 +24,18 @@
|
|||
default: 'Add Image'
|
||||
},
|
||||
|
||||
removeButtonLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'Remove Image'
|
||||
},
|
||||
|
||||
inputName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'attachments'
|
||||
},
|
||||
|
||||
multiple: {
|
||||
type: [Boolean, String],
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
|
||||
images: {
|
||||
type: Array,
|
||||
required: false,
|
||||
|
|
@ -36,9 +43,33 @@
|
|||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
imageCount: 0,
|
||||
items: []
|
||||
}
|
||||
},
|
||||
|
||||
created () {
|
||||
var this_this = this;
|
||||
|
||||
this.images.forEach(function(image) {
|
||||
this_this.items.push(image)
|
||||
this_this.imageCount++;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
createFileType () {
|
||||
this.images.push({});
|
||||
this.imageCount++;
|
||||
|
||||
this.items.push({'id': 'image_' + this.imageCount});
|
||||
},
|
||||
|
||||
removeImage (image) {
|
||||
let index = this.items.indexOf(image)
|
||||
|
||||
Vue.delete(this.items, index);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -954,7 +954,7 @@ h2 {
|
|||
|
||||
.label {
|
||||
background: #E7E7E7;
|
||||
border-radius: 2px;
|
||||
@include border-radius(2px);
|
||||
padding: 8px;
|
||||
color: #000311;
|
||||
display: inline-block;
|
||||
|
|
@ -974,4 +974,55 @@ h2 {
|
|||
&.label-xl {
|
||||
padding: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
margin-bottom: 20px;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
||||
.image-item {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin-right: 20px;
|
||||
background: #F8F9FA;
|
||||
@include border-radius(3px);
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background-image: url("../images/placeholder-icon.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
|
||||
img.preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.remove-image {
|
||||
// display: none;
|
||||
background-image: linear-gradient(-180deg, rgba(0,0,0,0.08) 0%, rgba(0,0,0,0.24) 100%);
|
||||
@include border-radius(0 0 4px 4px);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.24);
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover .remove-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.has-image {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue