119 lines
3.3 KiB
Vue
119 lines
3.3 KiB
Vue
<template>
|
|
<component
|
|
:is="linkable ? NuxtLink : 'div'"
|
|
:to="linkable ? localePath(`/catalog/offers/${offer.uuid}`) : undefined"
|
|
class="block"
|
|
:class="{ 'cursor-pointer': selectable }"
|
|
@click="selectable && $emit('select')"
|
|
>
|
|
<Card
|
|
padding="small"
|
|
:interactive="linkable || selectable"
|
|
:class="[
|
|
isSelected && 'ring-2 ring-primary ring-offset-2'
|
|
]"
|
|
>
|
|
<div class="flex flex-col gap-1">
|
|
<!-- Product title -->
|
|
<Text size="base" weight="semibold" class="truncate">{{ offer.productName }}</Text>
|
|
<!-- Quantity -->
|
|
<div v-if="offer.quantity" class="flex">
|
|
<span class="badge badge-neutral badge-dash text-xs">
|
|
{{ t('catalogOfferCard.labels.quantity_with_unit', { quantity: offer.quantity, unit: displayUnit }) }}
|
|
</span>
|
|
</div>
|
|
<!-- Price -->
|
|
<div v-if="offer.pricePerUnit" class="font-semibold text-primary text-sm">
|
|
{{ formatPrice(offer.pricePerUnit, offer.currency) }}/{{ displayUnit }}
|
|
</div>
|
|
<!-- Country below -->
|
|
<Text v-if="!compact" tone="muted" size="sm">
|
|
{{ countryFlag }} {{ offer.locationCountry || offer.locationName || t('catalogOfferCard.labels.country_unknown') }}
|
|
</Text>
|
|
</div>
|
|
</Card>
|
|
</component>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { NuxtLink } from '#components'
|
|
|
|
interface Offer {
|
|
uuid?: string | null
|
|
// Product
|
|
productUuid?: string | null
|
|
productName?: string | null
|
|
categoryName?: string | null
|
|
// Location
|
|
locationUuid?: string | null
|
|
locationName?: string | null
|
|
locationCountry?: string | null
|
|
locationCountryCode?: string | null
|
|
// Price
|
|
quantity?: number | string | null
|
|
unit?: string | null
|
|
pricePerUnit?: number | string | null
|
|
currency?: string | null
|
|
// Misc
|
|
status?: string | null
|
|
validUntil?: string | null
|
|
}
|
|
|
|
const props = defineProps<{
|
|
offer: Offer
|
|
selectable?: boolean
|
|
isSelected?: boolean
|
|
compact?: boolean
|
|
}>()
|
|
|
|
defineEmits<{
|
|
(e: 'select'): void
|
|
}>()
|
|
|
|
const localePath = useLocalePath()
|
|
const { t } = useI18n()
|
|
|
|
const linkable = computed(() => !props.selectable && props.offer.uuid)
|
|
|
|
const formattedDate = computed(() => {
|
|
if (!props.offer.validUntil) return ''
|
|
try {
|
|
return new Intl.DateTimeFormat('ru', {
|
|
day: 'numeric',
|
|
month: 'short'
|
|
}).format(new Date(props.offer.validUntil))
|
|
} catch {
|
|
return props.offer.validUntil
|
|
}
|
|
})
|
|
|
|
const formatPrice = (price: number | string | null | undefined, currency: string | null | undefined) => {
|
|
if (!price) return ''
|
|
const num = typeof price === 'string' ? parseFloat(price) : price
|
|
const curr = currency || 'USD'
|
|
try {
|
|
return new Intl.NumberFormat('ru', {
|
|
style: 'currency',
|
|
currency: curr,
|
|
maximumFractionDigits: 0
|
|
}).format(num)
|
|
} catch {
|
|
return `${num} ${curr}`
|
|
}
|
|
}
|
|
|
|
// ISO code to emoji flag
|
|
const isoToEmoji = (code: string): string => {
|
|
return code.toUpperCase().split('').map(char => String.fromCodePoint(0x1F1E6 - 65 + char.charCodeAt(0))).join('')
|
|
}
|
|
|
|
const countryFlag = computed(() => {
|
|
if (props.offer.locationCountryCode) {
|
|
return isoToEmoji(props.offer.locationCountryCode)
|
|
}
|
|
return '🌍'
|
|
})
|
|
|
|
const displayUnit = computed(() => props.offer.unit || t('catalogOfferCard.labels.default_unit'))
|
|
</script>
|