332 lines
7.4 KiB
Vue
332 lines
7.4 KiB
Vue
<template>
|
|
<div class="c-card">
|
|
<div class="image-container">
|
|
<img v-if="product.image" class="image" :src="product.image" alt="" />
|
|
<img v-else class="image" :src="defaultImage" alt="" />
|
|
</div>
|
|
<div class="details-container">
|
|
<p>{{ product.name }}</p>
|
|
<p class="company">{{ product.company.company_name }}</p>
|
|
<p v-if="product.price" class="price">{{ product.price }}€</p>
|
|
</div>
|
|
<div class="links-btns">
|
|
<NuxtLink :to="`/productos/${product.id}`" class="div-action show-link">
|
|
<img class="div-action-img" src="@/assets/img/eye.svg" />
|
|
VER
|
|
</NuxtLink>
|
|
<NuxtLink v-if="product.url" :to="product.url" class="div-action buy-link">
|
|
<img class="div-action-img" src="@/assets/img/shopping-cart.svg" />
|
|
COMPRAR
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import DOMPurify from 'dompurify'
|
|
import { mapState } from 'pinia'
|
|
import socialShare from '~/utils/socialShare'
|
|
import defaultImage from '@/assets/img/producto-default.png'
|
|
export default {
|
|
props: {
|
|
product: {
|
|
type: Object,
|
|
default: () => ({}),
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
expanded: false,
|
|
modal: false,
|
|
productUrl: null,
|
|
relatedProducts: null,
|
|
active: false,
|
|
modalText: '',
|
|
modalColor: 'info',
|
|
defaultImage: defaultImage,
|
|
}
|
|
},
|
|
computed: {
|
|
...mapState(useAuthStore, ['access']),
|
|
sanitizedDescription() {
|
|
return DOMPurify.sanitize(this.product.description, {
|
|
ALLOWED_TAGS: ['p'],
|
|
ALLOWED_ATTR: [],
|
|
})
|
|
}
|
|
},
|
|
mounted() {
|
|
this.productUrl = window.location.origin + `/productos/${this.product.id}`
|
|
this.getRelatedProducts()
|
|
},
|
|
|
|
methods: {
|
|
async onOpen() {
|
|
this.expanded = true
|
|
await this.getRelatedProducts()
|
|
await this.sendLog('view')
|
|
},
|
|
onClose() {
|
|
this.expanded = false
|
|
},
|
|
tagRoute(tag) {
|
|
return `/busqueda?tags=${tag}`
|
|
},
|
|
async getRelatedProducts() {
|
|
try {
|
|
const config = useRuntimeConfig()
|
|
const data = await $fetch(`/products/${this.product.id}/related/`, {
|
|
baseURL: config.public.baseURL,
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: '/',
|
|
},
|
|
})
|
|
this.relatedProducts = data
|
|
} catch {
|
|
this.relatedProducts = null
|
|
}
|
|
},
|
|
|
|
openUrl(url) {
|
|
window.open(url)
|
|
},
|
|
|
|
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 config = useRuntimeConfig()
|
|
const accessToken = this.access
|
|
const data = await $fetch('https://api.ipify.org?format=json', {
|
|
baseURL: config.public.baseURL,
|
|
method: 'GET'
|
|
})
|
|
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
|
|
//TODO: review problems with 406 error backend
|
|
await $fetch(`/stats/me/`, {
|
|
baseURL: config.public.baseURL,
|
|
method: 'POST',
|
|
body: object,
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`
|
|
}
|
|
})
|
|
} catch (error) {
|
|
console.error('Error sending log:', error)
|
|
}
|
|
},
|
|
|
|
closeModal(value) {
|
|
console.log(value)
|
|
this.modal = false
|
|
this.active = true
|
|
if (value === 200 || value === 201) {
|
|
this.modalText = 'Actualizado correctamente'
|
|
this.modalColor = 'success'
|
|
} else if (value) {
|
|
this.modalText = 'Se ha producido un error en el envío'
|
|
this.modalColor = 'danger'
|
|
|
|
}
|
|
},
|
|
|
|
shareFacebook() {
|
|
const url = socialShare.facebook(this.productUrl)
|
|
window.open(url, '_blank')
|
|
},
|
|
shareTwitter() {
|
|
return socialShare.twitter(this.productUrl)
|
|
},
|
|
shareWhatsApp() {
|
|
return socialShare.whatsApp(this.productUrl)
|
|
},
|
|
},
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.image-container {
|
|
width: 100%;
|
|
height: 11rem;
|
|
flex-shrink: 0;
|
|
align-self: stretch;
|
|
border-radius: 24px 24px 0 0;
|
|
@include mobile {
|
|
max-height: 10rem;
|
|
max-width: 10rem;
|
|
}
|
|
.image {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
border-radius: 24px 24px 0 0;
|
|
}
|
|
}
|
|
.c-card {
|
|
width: 237px;
|
|
height: 316px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: stretch;
|
|
background-color: #FDFCFB;
|
|
border: 5px solid white;
|
|
border-radius: 24px;
|
|
box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.15);
|
|
text-decoration: none;
|
|
position: relative; // necesario para controlar hijos absolutos
|
|
overflow: hidden;
|
|
|
|
p {
|
|
text-align: center;
|
|
display: -webkit-box;
|
|
--webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
width: 100%;
|
|
font-weight: $medium;
|
|
font-size: $m;
|
|
}
|
|
|
|
// detalles visibles por defecto
|
|
.details-container {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transition: all 0.3s ease;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 4px;
|
|
height: 100%;
|
|
p {
|
|
margin: 0;
|
|
}
|
|
.company {
|
|
font-weight: $regular;
|
|
font-size: $s;
|
|
margin: 0;
|
|
}
|
|
.price {
|
|
font-weight: $bold;
|
|
font-size: $m;
|
|
margin-top: auto;
|
|
margin-bottom: 2px;
|
|
}
|
|
}
|
|
|
|
// botones ocultos por defecto
|
|
.links-btns {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
position: absolute;
|
|
bottom: 1rem;
|
|
left: 0;
|
|
width: 100%;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
// en hover se intercambian
|
|
&:hover {
|
|
.details-container {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transform: translateY(10px); // opcional efecto
|
|
}
|
|
|
|
.links-btns {
|
|
opacity: 1;
|
|
visibility: visible;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
.links-btns {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0 0.5rem;
|
|
.div-action {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
font-weight: $bold;
|
|
font-size: $s;
|
|
text-decoration: none;
|
|
|
|
&.show-link {
|
|
border: 1px solid $color-button;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 12px;
|
|
background-color: white;
|
|
&:hover {
|
|
background-color: $color-button;
|
|
color: white;
|
|
}
|
|
}
|
|
|
|
&.buy-link {
|
|
color: white;
|
|
border: 1px solid $color-button;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 12px;
|
|
background-color: $color-button;
|
|
&:hover {
|
|
background-color: white;
|
|
color: $color-button;
|
|
}
|
|
}
|
|
|
|
.div-action-img {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
}
|
|
}
|
|
}
|
|
|
|
</style>
|