115 lines
3.1 KiB
Vue
115 lines
3.1 KiB
Vue
<template>
|
|
<div class="relative h-80 sm:h-96 overflow-hidden rounded-2xl border border-base-300 bg-base-200">
|
|
<!-- Map -->
|
|
<ClientOnly>
|
|
<MapboxGlobe
|
|
v-if="hasCoordinates"
|
|
:key="mapKey"
|
|
:map-id="`hero-${mapKey}`"
|
|
:locations="[mapLocation]"
|
|
height="100%"
|
|
:initial-center="mapCenter"
|
|
:initial-zoom="initialZoom"
|
|
/>
|
|
<div v-else class="w-full h-full bg-gradient-to-br from-primary/20 via-primary/10 to-base-200" />
|
|
</ClientOnly>
|
|
|
|
<!-- Overlay -->
|
|
<div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-6 sm:p-8">
|
|
<Stack gap="3">
|
|
<!-- Title -->
|
|
<h1 class="text-2xl sm:text-3xl font-bold text-white">{{ title }}</h1>
|
|
<p v-if="location?.country" class="text-white/80 flex items-center gap-2">
|
|
<span class="text-xl leading-none">{{ countryFlag }}</span>
|
|
<span>{{ location.country }}</span>
|
|
</p>
|
|
|
|
<!-- Badges -->
|
|
<div v-if="badges.length > 0" class="flex flex-wrap items-center gap-2">
|
|
<span
|
|
v-for="(badge, index) in badges"
|
|
:key="index"
|
|
class="badge badge-dash"
|
|
:class="badgeTone(badge, index)"
|
|
>
|
|
<Icon v-if="badge.icon" :name="badge.icon" size="14" />
|
|
{{ badge.text }}
|
|
</span>
|
|
</div>
|
|
</Stack>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Badge {
|
|
icon?: string
|
|
text: string
|
|
tone?: string
|
|
}
|
|
|
|
interface Location {
|
|
uuid?: string | null
|
|
name?: string | null
|
|
latitude?: number | null
|
|
longitude?: number | null
|
|
country?: string | null
|
|
countryCode?: string | null
|
|
}
|
|
|
|
const props = withDefaults(defineProps<{
|
|
title: string
|
|
badges?: Badge[]
|
|
location?: Location | null
|
|
initialZoom?: number
|
|
}>(), {
|
|
badges: () => [],
|
|
location: null,
|
|
initialZoom: 6
|
|
})
|
|
|
|
const hasCoordinates = computed(() =>
|
|
props.location?.latitude != null && props.location?.longitude != null
|
|
)
|
|
|
|
// Key to force map recreation when location changes
|
|
const mapKey = computed(() =>
|
|
`${props.location?.uuid}-${props.location?.latitude}-${props.location?.longitude}`
|
|
)
|
|
|
|
const mapCenter = computed<[number, number]>(() => {
|
|
if (hasCoordinates.value) {
|
|
return [props.location!.longitude!, props.location!.latitude!]
|
|
}
|
|
return [37.64, 55.76]
|
|
})
|
|
|
|
const mapLocation = computed(() => ({
|
|
uuid: props.location?.uuid,
|
|
name: props.location?.name,
|
|
latitude: props.location?.latitude,
|
|
longitude: props.location?.longitude,
|
|
country: props.location?.country
|
|
}))
|
|
|
|
const badgeTone = (badge: Badge, index: number) => {
|
|
if (badge.tone) return `badge-${badge.tone}`
|
|
const palette = ['badge-primary', 'badge-secondary', 'badge-accent', 'badge-info', 'badge-success']
|
|
return palette[index % palette.length]
|
|
}
|
|
|
|
const isoToEmoji = (code: string): string => {
|
|
return code.toUpperCase().split('').map(char => String.fromCodePoint(0x1F1E6 - 65 + char.charCodeAt(0))).join('')
|
|
}
|
|
|
|
const countryFlag = computed(() => {
|
|
if (props.location?.countryCode) {
|
|
return isoToEmoji(props.location.countryCode)
|
|
}
|
|
if (props.location?.country) {
|
|
return '🌍'
|
|
}
|
|
return ''
|
|
})
|
|
</script>
|