product page
This commit is contained in:
445
components/ProductCardDetails.vue
Normal file
445
components/ProductCardDetails.vue
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
<template>
|
||||||
|
<div class="productcard_container">
|
||||||
|
<div class="row productcard_container-basic">
|
||||||
|
<div class="image_container col-md-5">
|
||||||
|
<img :src="getProductImg(product)" class="image" alt="" />
|
||||||
|
</div>
|
||||||
|
<div class="info_container col-md-5">
|
||||||
|
<h2 variant="primary">
|
||||||
|
{{ product?.name }}
|
||||||
|
</h2>
|
||||||
|
<span v-if="product?.price" class="price">{{
|
||||||
|
`${product?.price}€`
|
||||||
|
}}</span>
|
||||||
|
<span v-else class="price">Precio a consultar</span>
|
||||||
|
<span v-if="Number(product?.shipping_cost)"
|
||||||
|
>| {{ `Gastos de envío ${product?.shipping_cost}€` }}</span
|
||||||
|
>
|
||||||
|
<span v-else>| Sin gastos de envío</span>
|
||||||
|
|
||||||
|
<span v-if="product?.stock">| {{ `Stock ${product?.stock}€` }} </span>
|
||||||
|
<p
|
||||||
|
v-if="product?.description"
|
||||||
|
class="description"
|
||||||
|
v-html="sanitize(product?.description)"
|
||||||
|
></p>
|
||||||
|
<span v-if="product?.shipping_terms">{{ product?.shipping_terms }}</span>
|
||||||
|
<BCollapse visible accordion="my-accordion">
|
||||||
|
<div class="tags_container">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="n in product?.tags"
|
||||||
|
:key="n"
|
||||||
|
:to="tagRoute(n)"
|
||||||
|
class="tag_container"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="tag_img"
|
||||||
|
alt="tag image"
|
||||||
|
src="@/assets/img/latienda-tag.svg"
|
||||||
|
/>
|
||||||
|
<span>{{ n }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
<div class="smlogos_container">
|
||||||
|
<p class="share-text">Comparte:</p>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<a @click="shareFacebook">
|
||||||
|
<img
|
||||||
|
alt="facebook logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-facebook.svg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a :href="shareTwitter()">
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="twitter logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-twitter.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="shareWhatsApp()"
|
||||||
|
data-action="share/whatsapp/share"
|
||||||
|
target="_blank"
|
||||||
|
title="latiendacoop"
|
||||||
|
>
|
||||||
|
<div class="smlogo_container">
|
||||||
|
<img
|
||||||
|
alt="whatsapp logo"
|
||||||
|
class="smlogo_img"
|
||||||
|
src="@/assets/img/latienda-smlogo-whatsapp.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="coop_info">
|
||||||
|
<NuxtLink :to="`/c/${company?.id}`">
|
||||||
|
<h2>{{ company?.company_name }}</h2>
|
||||||
|
</NuxtLink>
|
||||||
|
<p class="description">{{ company?.description }}</p>
|
||||||
|
<a href="#">{{ company?.web_link }}</a>
|
||||||
|
</div>
|
||||||
|
</BCollapse>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 button_container-detail" align="center">
|
||||||
|
<button class="button_buy-simple" @click="buyIntent">
|
||||||
|
<img
|
||||||
|
class="button_cart_img"
|
||||||
|
alt="cart"
|
||||||
|
src="@/assets/img/latienda-carrito.svg"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="product?.discount && product?.discount > 0"
|
||||||
|
class="discount-tag"
|
||||||
|
>
|
||||||
|
{{ `Descuento ${product?.discount}%` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="related_products">
|
||||||
|
<h2 v-if="related">Productos relacionados</h2>
|
||||||
|
<h2 v-else>Otros productos</h2>
|
||||||
|
<ProductsRelated :related-products="relatedProducts" />
|
||||||
|
</div>
|
||||||
|
<ProductModal v-if="modal" :product="product" @close-modal="closeModal" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
import socialShare from '~/utils/socialShare'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
product: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
company: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
relatedProducts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
related: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modal: true,
|
||||||
|
productUrl: null,
|
||||||
|
geolocation: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.productUrl = window.location.href
|
||||||
|
this.sendLog('view')
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getProductImg(product) {
|
||||||
|
if (product && product.image)
|
||||||
|
return product.image
|
||||||
|
return `@/assets/img/latienda-product-default.svg`
|
||||||
|
},
|
||||||
|
tagRoute(tag) {
|
||||||
|
return `/busqueda?tags=${tag}`
|
||||||
|
},
|
||||||
|
openUrl(url) {
|
||||||
|
window.open(url)
|
||||||
|
},
|
||||||
|
closeModal(value) {
|
||||||
|
this.modal = false
|
||||||
|
if (value === 200) {
|
||||||
|
this.$bvToast.toast(`Email enviado correctamente`, {
|
||||||
|
title: 'latienda.coop',
|
||||||
|
autoHideDelay: 5000,
|
||||||
|
appendToast: true,
|
||||||
|
})
|
||||||
|
} else if (value) {
|
||||||
|
this.$bvToast.toast(`Se ha producido un error en el envío`, {
|
||||||
|
title: 'latienda.coop',
|
||||||
|
autoHideDelay: 5000,
|
||||||
|
appendToast: true,
|
||||||
|
variant: 'danger',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buyIntent() {
|
||||||
|
this.sendLog('shop')
|
||||||
|
return this.product.url
|
||||||
|
? this.openUrl(this.product.url)
|
||||||
|
: (this.modal = true)
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPosition() {
|
||||||
|
const geoLocation = () =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(posData) => {
|
||||||
|
resolve(posData)
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const position = await geoLocation()
|
||||||
|
const geo = {
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
}
|
||||||
|
return geo
|
||||||
|
} catch {
|
||||||
|
const geo = null
|
||||||
|
return geo
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendLog(action) {
|
||||||
|
const geo = await this.getPosition()
|
||||||
|
try {
|
||||||
|
const { data } = await this.$axios.get(
|
||||||
|
'https://api.ipify.org?format=json'
|
||||||
|
)
|
||||||
|
const ip = data.ip
|
||||||
|
const object = {
|
||||||
|
action: action,
|
||||||
|
action_object: {
|
||||||
|
model: 'product',
|
||||||
|
id: this.product.id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if (ip) object.ip = ip
|
||||||
|
if (geo) object.geo = geo
|
||||||
|
await this.$api.post(`/stats/me/`, object)
|
||||||
|
} catch {}
|
||||||
|
},
|
||||||
|
|
||||||
|
shareFacebook() {
|
||||||
|
const url = socialShare.facebook(this.productUrl)
|
||||||
|
window.open(url, '_blank')
|
||||||
|
},
|
||||||
|
shareTwitter() {
|
||||||
|
return socialShare.twitter(this.productUrl)
|
||||||
|
},
|
||||||
|
shareWhatsApp() {
|
||||||
|
return socialShare.whatsApp(this.productUrl)
|
||||||
|
},
|
||||||
|
|
||||||
|
sanitize(dirtyHtml) {
|
||||||
|
return DOMPurify.sanitize(dirtyHtml, {
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
'b',
|
||||||
|
'i',
|
||||||
|
'em',
|
||||||
|
'strong',
|
||||||
|
'a',
|
||||||
|
'p',
|
||||||
|
'br',
|
||||||
|
'ul',
|
||||||
|
'li',
|
||||||
|
'ol',
|
||||||
|
'span',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: ['href', 'target', 'rel', 'class'],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.productcard_container {
|
||||||
|
border: 3px solid $color-grey-nav;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.productcard_container-basic {
|
||||||
|
padding: 25px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image_container {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info_container {
|
||||||
|
h2 {
|
||||||
|
outline: none;
|
||||||
|
font-weight: medium;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
.price {
|
||||||
|
font-weight: bold;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $m;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin-top: 15px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: medium;
|
||||||
|
font-size: $m;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: $color-light;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $color-orange;
|
||||||
|
font-weight: $bold;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_buy-simple {
|
||||||
|
border: 3px solid $color-orange;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $color-light;
|
||||||
|
padding: 10px 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(99, 99, 99, 0.2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag_container {
|
||||||
|
margin: 25px 6px 0 0;
|
||||||
|
border: 2px solid $color-greylayout;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
|
||||||
|
.tag_img {
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-text {
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-bottom: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smlogo_container {
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
|
.smlogo_img {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
fill: $color-greylayout;
|
||||||
|
}
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button_cart_img {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop_info {
|
||||||
|
margin-top: 25px;
|
||||||
|
p,
|
||||||
|
a {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $s;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_products {
|
||||||
|
background-color: $color-lighter-green;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 35px auto;
|
||||||
|
font-weight: medium;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.discount-tag {
|
||||||
|
margin: 5px;
|
||||||
|
border: none;
|
||||||
|
background-color: $color-green;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
display: inline-block;
|
||||||
|
font-family: $font-secondary;
|
||||||
|
font-size: $xs;
|
||||||
|
color: $color-greytext;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content > h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
310
components/ProductModal.vue
Normal file
310
components/ProductModal.vue
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
<template>
|
||||||
|
<transition name="modal">
|
||||||
|
<div v-if="product" class="mask">
|
||||||
|
<div class="wrapper" @click.self="handleEmit">
|
||||||
|
<div class="container">
|
||||||
|
<div class="element header">
|
||||||
|
<h2>Interesado en comprar el producto</h2>
|
||||||
|
<img src="@/assets/img/latienda-lineapuntos-2.svg" alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<form @submit.prevent="sendForm">
|
||||||
|
<div>
|
||||||
|
<BFormGroup class="element">
|
||||||
|
<BFormInput
|
||||||
|
v-model="form.email"
|
||||||
|
required
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
placeholder="Email"
|
||||||
|
/>
|
||||||
|
</BFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<BFormGroup class="element">
|
||||||
|
<BFormInput
|
||||||
|
v-model="form.telephone"
|
||||||
|
required
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
placeholder="Teléfono"
|
||||||
|
/>
|
||||||
|
</BFormGroup>
|
||||||
|
</div>
|
||||||
|
<div v-if="!isAuthenticated" class="element">
|
||||||
|
<BButton
|
||||||
|
class="input"
|
||||||
|
size="lg"
|
||||||
|
variant="outline-primary w-100"
|
||||||
|
@click="redirectToLogin"
|
||||||
|
>Login</BButton
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="element coop">
|
||||||
|
<!-- <BCard
|
||||||
|
img-src="https://placekitten.com/1000/300"
|
||||||
|
img-alt="Card image"
|
||||||
|
img-left
|
||||||
|
>
|
||||||
|
<BCardText>
|
||||||
|
<h3>Cooperativa</h3>
|
||||||
|
<p>Dirección</p>
|
||||||
|
</BCardText>
|
||||||
|
</BCard> -->
|
||||||
|
<div class="content">
|
||||||
|
<img :src="getImgUrl(product.company.logo)" alt="" />
|
||||||
|
<div class="text">
|
||||||
|
<h3>{{ product?.company?.company_name }}</h3>
|
||||||
|
<p>{{ product?.company?.address }}</p>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="element coop">
|
||||||
|
<!-- <BCard
|
||||||
|
img-src="https://placekitten.com/1000/300"
|
||||||
|
img-alt="Card image"
|
||||||
|
img-left
|
||||||
|
>
|
||||||
|
<BCardText>
|
||||||
|
<h3>Cooperativa</h3>
|
||||||
|
<p>Dirección</p>
|
||||||
|
</BCardText>
|
||||||
|
</BCard> -->
|
||||||
|
<div class="content">
|
||||||
|
<img :src="getImgUrl(product.image)" alt="" />
|
||||||
|
<div class="text">
|
||||||
|
<h3>{{ product?.name }}</h3>
|
||||||
|
<p>{{ product?.price }} €</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ product?.shipping_cost || 'Sin gastos de envío' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="element">
|
||||||
|
<BFormTextarea
|
||||||
|
id="textarea-no-resize"
|
||||||
|
v-model="form.comment"
|
||||||
|
class="input"
|
||||||
|
placeholder="Comentarios"
|
||||||
|
rows="3"
|
||||||
|
no-resize
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<BButton type="submit" class="enviar-button">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="loading"
|
||||||
|
:size="15"
|
||||||
|
:width="2"
|
||||||
|
indeterminate
|
||||||
|
/>
|
||||||
|
<span v-else>Enviar</span>
|
||||||
|
</BButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
product: { type: Object, default: () => ({}) },
|
||||||
|
},
|
||||||
|
emits: ['closeModal'],
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return {
|
||||||
|
authStore
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isAuthenticated: true,
|
||||||
|
form: {
|
||||||
|
email: undefined,
|
||||||
|
telephone: undefined,
|
||||||
|
company: undefined,
|
||||||
|
product: undefined,
|
||||||
|
comment: '',
|
||||||
|
},
|
||||||
|
loading: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.isAuthenticated = this.authStore.isAuthenticated
|
||||||
|
if (this.isAuthenticated) {
|
||||||
|
const email = this.authStore.email
|
||||||
|
this.form.email = email
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async sendForm() {
|
||||||
|
this.form.company = this.product.company.id
|
||||||
|
this.form.product = this.product.id
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
let response
|
||||||
|
let status
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
try { //TODO: review if its working
|
||||||
|
response = await $fetch(`/purchase_email/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: { ...this.form }
|
||||||
|
})
|
||||||
|
status = response.status
|
||||||
|
} catch {
|
||||||
|
status = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false
|
||||||
|
this.$emit('closeModal', status)
|
||||||
|
},
|
||||||
|
getImgUrl(image) {
|
||||||
|
if (image) return image
|
||||||
|
return `@/assets/img/latienda-product-default.svg`
|
||||||
|
},
|
||||||
|
redirectToLogin() {
|
||||||
|
this.$router.push({
|
||||||
|
name: 'login',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleEmit() {
|
||||||
|
this.$emit('closeModal')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.mask {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9998;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: table;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 500px;
|
||||||
|
margin: 0px auto;
|
||||||
|
padding: 20px 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 7px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.element {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header > h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374493;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header > img {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
width: 48px;
|
||||||
|
border-right: none;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop {
|
||||||
|
border: 1px lightgray solid;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coop > .content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin: 5px 10px;
|
||||||
|
}
|
||||||
|
.coop > .content > img {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
.coop > .content > .text > h3,
|
||||||
|
p {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enviar-button {
|
||||||
|
width: 100%;
|
||||||
|
background: #fd6871;
|
||||||
|
border-color: #fd6871;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The following styles are auto-applied to elements with
|
||||||
|
* transition="modal" when their visibility is toggled
|
||||||
|
* by Vue.js.
|
||||||
|
*
|
||||||
|
* You can easily play with the modal transition by editing
|
||||||
|
* these styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.modal-enter {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-enter .modal-container,
|
||||||
|
.modal-leave-active .modal-container {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
86
components/ProductsRelated.vue
Normal file
86
components/ProductsRelated.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row related_products-cards">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="(product, key) in relatedProducts"
|
||||||
|
:key="`related-${key}`"
|
||||||
|
:to="`/productos/${product.id}`"
|
||||||
|
class="col mx-2 related_product-card"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="product.image"
|
||||||
|
:src="product.image"
|
||||||
|
alt=""
|
||||||
|
class="related_product-image"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="image-default"
|
||||||
|
src="@/assets/img/latienda-product-default.svg"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<h3>{{ product.name }}</h3>
|
||||||
|
<span v-if="product.price" class="price">{{ `${product.price}€` }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
relatedProducts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.related_products {
|
||||||
|
background-color: $color-lighter-green;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 15px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-default {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star {
|
||||||
|
width: 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.related_product-card {
|
||||||
|
border: 3px solid $color-grey-nav;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 35px;
|
||||||
|
|
||||||
|
.related_product-image {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-weight: $regular;
|
||||||
|
color: $color-navy;
|
||||||
|
font-size: $m;
|
||||||
|
display: block;
|
||||||
|
font-weight: bolder;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -12,6 +12,7 @@
|
|||||||
"@nuxt/eslint": "^1.8.0",
|
"@nuxt/eslint": "^1.8.0",
|
||||||
"@nuxtjs/sitemap": "^7.4.3",
|
"@nuxtjs/sitemap": "^7.4.3",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"nuxt": "^3.17.7",
|
"nuxt": "^3.17.7",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
@@ -4321,6 +4322,13 @@
|
|||||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/yauzl": {
|
"node_modules/@types/yauzl": {
|
||||||
"version": "2.10.3",
|
"version": "2.10.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
|
||||||
@@ -7027,6 +7035,15 @@
|
|||||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/domutils": {
|
"node_modules/domutils": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@nuxt/eslint": "^1.8.0",
|
"@nuxt/eslint": "^1.8.0",
|
||||||
"@nuxtjs/sitemap": "^7.4.3",
|
"@nuxtjs/sitemap": "^7.4.3",
|
||||||
"@pinia/nuxt": "^0.11.2",
|
"@pinia/nuxt": "^0.11.2",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"nuxt": "^3.17.7",
|
"nuxt": "^3.17.7",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
<SubmitButton text="Actualizar" image-url="" />
|
<SubmitButton text="Actualizar" image-url="" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{{ form }}
|
|
||||||
</BCol>
|
</BCol>
|
||||||
</BRow>
|
</BRow>
|
||||||
</BContainer>
|
</BContainer>
|
||||||
|
|||||||
@@ -1,15 +1,92 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="container">
|
||||||
Producto 1
|
<h1 class="title">Detalles del producto</h1>
|
||||||
|
<ProductCardDetails
|
||||||
|
:product="product"
|
||||||
|
:related="related"
|
||||||
|
:related-products="relatedProducts"
|
||||||
|
:company="company"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
import { useRoute } from 'vue-router'
|
||||||
|
export default {
|
||||||
}
|
setup() {
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'mainbanner'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
product: null,
|
||||||
|
company: null,
|
||||||
|
relatedProducts: null,
|
||||||
|
related: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const $route = useRoute()
|
||||||
|
this.product = await $fetch(`/products/${$route.params.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.product.company) {
|
||||||
|
this.company = await $fetch(`/companies/${this.product.company.id}/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.relatedProducts = await $fetch(`/products/${this.product.id}/related/`, {
|
||||||
|
baseURL: config.public.baseURL,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
this.related = this.relatedProducts.length > 0
|
||||||
|
|
||||||
|
//return { product, company, relatedProducts, related }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching product details:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//TODO: meta data
|
||||||
|
// mounted() {
|
||||||
|
// useHead({
|
||||||
|
// title: `latienda.coop | ${this.product.id}`,
|
||||||
|
// meta: [
|
||||||
|
// {
|
||||||
|
// hid: 'description',
|
||||||
|
// name: 'description',
|
||||||
|
// content: this.product.description,
|
||||||
|
// },
|
||||||
|
// { property: 'og:title', content: this.product.id },
|
||||||
|
// { property: 'og:description', content: this.product.description },
|
||||||
|
// { property: 'og:image', content: this.product.image },
|
||||||
|
// { property: 'og:url', content: this.product.url },
|
||||||
|
// { name: 'twitter:card', content: 'summary_large_image' },
|
||||||
|
// ],
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 80px;
|
||||||
|
@include mobile {
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
.title {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: $xl;
|
||||||
|
color: $color-navy;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user