mirror of https://github.com/4xmen/xshop.git
added single product route
added aria product theme part added tooltip to compare add to card fixed has discount bug fixed toggle card bugs [quantity] improved ui different color for secondary color fixed tag urlpull/49/head
parent
52c0f48bc1
commit
1d4622f270
@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div id="quantities-add-to-card">
|
||||
<template v-for="(q,i) in qz">
|
||||
<div :class="`q `+(selected == i?'selected-q':'')" v-if="q.count > 0" @click="select(i)">
|
||||
<div class="row">
|
||||
|
||||
<template v-for="(v,k) in data2object(q.data)">
|
||||
<div :class="calcClass(props[k])">
|
||||
<div v-if="props[k].type == 'color'">
|
||||
<span class="q-color float-start" :style="`background-color:${v}`"></span>
|
||||
</div>
|
||||
<div
|
||||
v-if="props[k].type == 'select' || props[k].type == 'singlemulti' || props[k].type == 'multi'">
|
||||
|
||||
|
||||
<span>
|
||||
{{ props[k].label }}:
|
||||
</span>
|
||||
<b>
|
||||
{{ props[k].data[v] }}
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="col-md" v-if="discount != null">
|
||||
|
||||
<strong>
|
||||
{{ calcDiscount(q.price) }}
|
||||
</strong>
|
||||
|
||||
<del class="text-muted">
|
||||
{{ commafy(q.price.toString()) }} {{ currency }}
|
||||
</del>
|
||||
</div>
|
||||
<div class="col-md" v-else>
|
||||
{{ commafy(q.price.toString()) }} {{ currency }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a
|
||||
class="btn btn-outline-primary btn-lg" @click="add2card">
|
||||
<i class="ri-shopping-bag-3-line"></i>
|
||||
Add to card
|
||||
<!-- WIP translate-->
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
function commafy(num) {
|
||||
if (typeof num !== 'string') {
|
||||
return '';
|
||||
}
|
||||
let str = uncommafy(num.toString()).split('.');
|
||||
if (str[0].length >= 4) {
|
||||
str[0] = str[0].replace(/(\d)(?=(\d{3})+$)/g, '$1,');
|
||||
}
|
||||
if (str[1] && str[1].length >= 4) {
|
||||
str[1] = str[1].replace(/(\d{3})/g, '$1 ');
|
||||
}
|
||||
return str.join('.');
|
||||
}
|
||||
|
||||
function uncommafy(txt) {
|
||||
return txt.split(',').join('');
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "quantities-add-to-card",
|
||||
components: {},
|
||||
data: () => {
|
||||
return {
|
||||
selected: null,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
qz: {
|
||||
default: []
|
||||
},
|
||||
props: {
|
||||
default: {},
|
||||
},
|
||||
currency: {
|
||||
default: '$',
|
||||
},
|
||||
cardLink: {
|
||||
default: '',
|
||||
},
|
||||
discount: {
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
select(i) {
|
||||
document.querySelector('#price').innerText = commafy(this.qz[i].price.toString()) + ' ' + this.currency;
|
||||
this.selected = i;
|
||||
},
|
||||
async add2card() {
|
||||
if (this.selected == null) {
|
||||
window.$toast.warning('You need to select one quantity');
|
||||
return;
|
||||
}
|
||||
|
||||
let resp = await axios.get(this.cardLink + '?quantity=' + this.qz[this.selected].id);
|
||||
if (resp.data.success) {
|
||||
window.$toast.success(resp.data.message);
|
||||
document.querySelectorAll('.card-count')?.forEach(function (el2) {
|
||||
el2.innerText = resp.data.data.count;
|
||||
});
|
||||
} else {
|
||||
window.$toast.error("Error!");
|
||||
}
|
||||
},
|
||||
calcDiscount(price) {
|
||||
if (this.discount == null) {
|
||||
return '-';
|
||||
}
|
||||
if (this.discount.type == 'PERCENT') {
|
||||
return commafy(
|
||||
parseInt(((100 - this.discount.amount) * price) / 100).toString()
|
||||
) + ' ' + this.currency;
|
||||
} else {
|
||||
return commafy((price - this.discount.amount).toString()) + ' ' + this.currency;
|
||||
}
|
||||
},
|
||||
calcClass(prop) {
|
||||
let cls = '';
|
||||
if (prop.type == 'color') {
|
||||
cls = 'col-md-1';
|
||||
} else {
|
||||
cls = 'col-md';
|
||||
}
|
||||
cls += ' ' + prop.type;
|
||||
return cls;
|
||||
},
|
||||
data2object(data) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
commafy: commafy,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#quantities-add-to-card {
|
||||
|
||||
}
|
||||
|
||||
.q {
|
||||
border: 1px solid var(--xshop-primary);
|
||||
border-radius: var(--xshop-border-radius);
|
||||
margin-bottom: .5rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: 300ms;
|
||||
padding: 7px;
|
||||
|
||||
|
||||
&:hover {
|
||||
background: var(--xshop-primary);
|
||||
color: var(--xshop-diff);
|
||||
}
|
||||
}
|
||||
|
||||
.q-color {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.selected-q {
|
||||
background: var(--xshop-secondary);
|
||||
color: var(--xshop-diff2);
|
||||
}
|
||||
|
||||
</style>
|
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div id="quantity">
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "quantity",
|
||||
components: {},
|
||||
data: () => {
|
||||
return {
|
||||
modelValue: 1,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
min:{
|
||||
default: 1,
|
||||
type: Number,
|
||||
},
|
||||
max: {
|
||||
default: 99,
|
||||
type: Number,
|
||||
},
|
||||
xname:{
|
||||
default: null,
|
||||
type: null|String,
|
||||
},
|
||||
|
||||
|
||||
},
|
||||
mounted() {
|
||||
},
|
||||
computed: {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#quantity {
|
||||
direction: ltr;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,223 @@
|
||||
<section id='ProductAria' class="content">
|
||||
|
||||
<div class="{{gfx()['container']}}">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{homeUrl()}}">
|
||||
{{config('app.name')}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{{$product->category->webUrl()}}">
|
||||
{{$product->category->name}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
{{$product->name}}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-lg-5">
|
||||
<a href="{{$product->originalImageUrl()}}" id="aria-main-img" class="light-box"
|
||||
data-gallery="aria-products">
|
||||
<img src="{{$product->originalImageUrl()}}" alt="{{$product->name}}">
|
||||
</a>
|
||||
<div id="aria-img-slider">
|
||||
@foreach($product->getMedia() as $media)
|
||||
<div class="item">
|
||||
<a href="{{$media->getUrl('product-image')}}">
|
||||
<img src="{{$media->getUrl('product-image')}}" alt="{{$product->name}}">
|
||||
</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-7" id="aria-product-detail">
|
||||
<a class="fav-btn" data-slug="{{$product->slug}}" data-is-fav="{{$product->isFav()}}"
|
||||
data-bs-custom-class="custom-tooltip"
|
||||
data-bs-toggle="tooltip" data-bs-placement="auto" title="{{__("Add to / Remove from favorites")}}"
|
||||
>
|
||||
<i class="ri-heart-line"></i>
|
||||
<i class="ri-heart-fill"></i>
|
||||
</a>
|
||||
|
||||
<a class="compare-btn" data-slug="{{$product->slug}}"
|
||||
data-bs-custom-class="custom-tooltip"
|
||||
data-bs-toggle="tooltip" data-bs-placement="auto" title="{{__("Add to/ Remove from compare list")}}">
|
||||
<i class="ri-scales-3-line"></i>
|
||||
</a>
|
||||
|
||||
<h1>
|
||||
{{$product->name}}
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div id="price" class="col">
|
||||
{{$product->getPrice()}}
|
||||
</div>
|
||||
|
||||
@if($product->hasDiscount())
|
||||
<div id="price-old" class="col">
|
||||
{{$product->oldPrice()}}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="description border-end-0 ps-3">
|
||||
<p>
|
||||
{{$product->excerpt}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4"> </div>
|
||||
@if($product->quantities()->count()>0)
|
||||
<quantities-add-to-card
|
||||
:qz='@json($product->quantities)'
|
||||
:props='@json(usableProp($product->category->props))'
|
||||
currency="{{config('app.currency.symbol')}}"
|
||||
card-link="{{ route('client.product-card-toggle',$product->slug) }}"
|
||||
|
||||
@if($product->hasDiscount())
|
||||
:discount='@json($product->activeDiscounts()->first())'
|
||||
@endif
|
||||
></quantities-add-to-card>
|
||||
@else
|
||||
<a href="{{ route('client.product-card-toggle',$product->slug) }}"
|
||||
class="btn btn-outline-primary add-to-card btn-lg">
|
||||
<i class="ri-shopping-bag-3-line"></i>
|
||||
{{__("Add to card")}}
|
||||
</a>
|
||||
@endif
|
||||
<div class="mt-4"> </div>
|
||||
@if($product->sku != null && $product->sku != '')
|
||||
<div class="aria-product-data">
|
||||
|
||||
<span>
|
||||
{{__("SKU")}}:
|
||||
</span>
|
||||
<b>
|
||||
{{$product->sku}}
|
||||
</b>
|
||||
</div>
|
||||
@endif
|
||||
@if($product->categories()->count() > 0)
|
||||
<div class="aria-product-data">
|
||||
<span>
|
||||
{{__("Categories")}}:
|
||||
</span>
|
||||
@foreach($product->categories()->where('id','<>',$product->category->id)->get() as $cat)
|
||||
<a href="{{$cat->webUrl()}}">
|
||||
{{$cat->name}},
|
||||
</a>
|
||||
@endforeach
|
||||
<a href="{{$product->category->webUrl()}}">
|
||||
{{$product->category->name}}
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@if($product->tags()->count() > 0)
|
||||
<div class="aria-product-data">
|
||||
<span>
|
||||
{{__("Tags")}}:
|
||||
</span>
|
||||
@foreach($product->tags as $tag)
|
||||
<a href="{{tagUrl($tag->slug)}}" class="tag me-2">
|
||||
<i class="ri-price-tag-line"></i>
|
||||
{{$tag->name}}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion mt-4" id="product-detail">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#desc" aria-expanded="true"
|
||||
aria-controls="desc">
|
||||
{{__("Description")}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="desc" class="accordion-collapse collapse show" data-bs-parent="#product-detail">
|
||||
<div class="accordion-body">
|
||||
{!! $product->description !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($product->table != null || trim($product->table) != '')
|
||||
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#table"
|
||||
aria-expanded="false" aria-controls="table">
|
||||
{{__("Product table")}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="table" class="accordion-collapse collapse" data-bs-parent="#product-detail">
|
||||
<div class="accordion-body">
|
||||
{!! $product->table !!}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#info" aria-expanded="false" aria-controls="info">
|
||||
{{__("Information")}}
|
||||
</button>
|
||||
</h2>
|
||||
<div id="info" class="accordion-collapse collapse" data-bs-parent="#product-detail">
|
||||
<div class="accordion-body">
|
||||
<table class="table table-striped table-bordered table-striped">
|
||||
<tr>
|
||||
<th class="w-50">
|
||||
{{__("Item")}}
|
||||
</th>
|
||||
<th>
|
||||
{{__("Value")}}
|
||||
</th>
|
||||
</tr>
|
||||
@foreach($product->fullMeta() as $meta)
|
||||
<tr>
|
||||
<td>
|
||||
<i class="{{$meta['data']->icon}}"></i>
|
||||
|
||||
{{$meta['data']->label}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{!! $meta['human_value'] !!}
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="mt-4">
|
||||
{{__("Related products")}}
|
||||
</h3>
|
||||
<div id="rel-products">
|
||||
@foreach($product->category->products()->where('status',1)->limit(10)->get() as $p)
|
||||
<div class="item">
|
||||
<div class="aria-product-list">
|
||||
<a href="{{$p->imgUrl()}}">
|
||||
<img src="{{$p->imgUrl()}}" alt="{{$p->name}}">
|
||||
<h5>
|
||||
{{$p->name}}
|
||||
</h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<div id="hidden-images">
|
||||
@foreach($product->getMedia() as $k => $media)
|
||||
<a href="{{$media->getUrl()}}" class="light-box"
|
||||
data-gallery="aria-products">
|
||||
<img src="{{$media->getUrl('product-image')}}" id="hidden-img-{{$k}}"
|
||||
alt="{{$product->name}}">
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</section>
|
@ -0,0 +1,62 @@
|
||||
import Lightbox from 'bs5-lightbox' ;
|
||||
import {tns} from "tiny-slider/src/tiny-slider";
|
||||
|
||||
var ariaImgSlider, ariaRelativeSlider;
|
||||
document.addEventListener('DOMContentLoaded',function () {
|
||||
|
||||
for (const el of document.querySelectorAll('.light-box')) {
|
||||
el.addEventListener('click', Lightbox.initialize);
|
||||
}
|
||||
ariaImgSlider = tns({
|
||||
container: '#aria-img-slider',
|
||||
items: 3,
|
||||
autoplay: true,
|
||||
autoplayButton: false,
|
||||
// nextButton: false,
|
||||
controls: false,
|
||||
autoplayHoverPause: true,
|
||||
mouseDrag: true,
|
||||
gutter: 5,
|
||||
slideBy: 1,
|
||||
autoplayTimeout: 5000,
|
||||
// speed:10000,
|
||||
});
|
||||
ariaRelativeSlider = tns({
|
||||
container: '#rel-products',
|
||||
items: 3,
|
||||
autoplay: true,
|
||||
autoplayButton: false,
|
||||
// nextButton: false,
|
||||
controls: false,
|
||||
autoplayHoverPause: true,
|
||||
mouseDrag: true,
|
||||
gutter: 5,
|
||||
slideBy: 1,
|
||||
autoplayTimeout: 5000,
|
||||
responsive:{
|
||||
560:{
|
||||
items: 1,
|
||||
},
|
||||
768:{
|
||||
items: 2,
|
||||
},
|
||||
1000:{
|
||||
items: 4,
|
||||
},
|
||||
1400:{
|
||||
items: 5,
|
||||
},
|
||||
|
||||
}
|
||||
// speed:10000,
|
||||
});
|
||||
|
||||
document.querySelectorAll('#aria-img-slider a')?.forEach(function (el) {
|
||||
el.addEventListener('click',function (e) {
|
||||
e.preventDefault();
|
||||
document.querySelector('#aria-main-img').setAttribute('href',el.getAttribute('href'));
|
||||
document.querySelector('#aria-main-img img').setAttribute('src',el.querySelector('img').getAttribute('src'));
|
||||
})
|
||||
});
|
||||
|
||||
});
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "ProductAria",
|
||||
"version": "1.0",
|
||||
"author": "xStack",
|
||||
"email": "xshop@xstack.ir",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"url": "https:\/\/xstack.ir",
|
||||
"author_url": "https:\/\/4xmen.ir",
|
||||
"packages": []
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Resources\Views\Segments;
|
||||
|
||||
use App\Models\Part;
|
||||
|
||||
class ProductAria
|
||||
{
|
||||
public static function onAdd(Part $part = null)
|
||||
{
|
||||
|
||||
}
|
||||
public static function onRemove(Part $part = null)
|
||||
{
|
||||
|
||||
}
|
||||
public static function onMount(Part $part = null)
|
||||
{
|
||||
return $part;
|
||||
}
|
||||
}
|
@ -0,0 +1,116 @@
|
||||
#ProductAria {
|
||||
padding: 1rem 0;
|
||||
|
||||
|
||||
#aria-product-detail{
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.description{
|
||||
border: solid 2px var(--xshop-primary);
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
h1{
|
||||
font-size: 32px;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
#price{
|
||||
color: var(--xshop-primary);
|
||||
font-weight: 800;
|
||||
font-size: 27px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
#price-old{
|
||||
text-decoration: line-through red;
|
||||
font-size: 27px;
|
||||
color: gray;
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.fav-btn, .compare-btn {
|
||||
position: absolute;
|
||||
inset-inline-end: 3%;
|
||||
top: 3%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #ffffff55;
|
||||
font-size: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--xshop-border-radius);
|
||||
z-index: 4;
|
||||
cursor: pointer;
|
||||
transition: .4s;
|
||||
|
||||
&:hover {
|
||||
background: var(--xshop-primary);
|
||||
color: var(--xshop-diff);
|
||||
}
|
||||
}
|
||||
.fav-btn {
|
||||
top: calc(3% + 50px);
|
||||
|
||||
&[data-is-fav="-1"]{
|
||||
display: none;
|
||||
}
|
||||
&[data-is-fav="1"]{
|
||||
.ri-heart-line{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&[data-is-fav="0"]{
|
||||
.ri-heart-fill{
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p{
|
||||
text-align: justify;
|
||||
color: gray;
|
||||
line-height: 2em;
|
||||
}
|
||||
|
||||
|
||||
#aria-main-img{
|
||||
img{
|
||||
height: 50vh;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
border-radius: var(--xshop-border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
#aria-img-slider{
|
||||
margin-top: 5px;
|
||||
img{
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
object-fit: cover;
|
||||
border-radius: var(--xshop-border-radius);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.aria-product-data{
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.aria-product-list{
|
||||
padding: 5px;
|
||||
img{
|
||||
height: 20vh;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: var(--xshop-border-radius);
|
||||
}
|
||||
h5{
|
||||
text-align: center;
|
||||
padding: .5rem 0;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 513 KiB |
Loading…
Reference in New Issue