10-04-2026
This commit is contained in:
parent
70a7776da5
commit
761b1156c1
50 changed files with 11997 additions and 150 deletions
200
frontend/_src/components/GlowDot.vue
Normal file
200
frontend/_src/components/GlowDot.vue
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
<template>
|
||||
<div
|
||||
class="glow-dot"
|
||||
:class="{
|
||||
'glow-dot--ghost': isGhost,
|
||||
'glow-dot--selected': selected,
|
||||
'glow-dot--dimmed': isDimmed,
|
||||
'glow-dot--label-above': labelAbove
|
||||
}"
|
||||
:style="dotStyle"
|
||||
@click.stop="onSelect"
|
||||
>
|
||||
<div class="glow-dot__inner" :style="{ boxShadow: glowShadow }">
|
||||
<img
|
||||
v-if="imageSrc"
|
||||
:src="imageSrc"
|
||||
class="glow-dot__image"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isGhost && event.title" class="glow-dot__label" :style="labelStyle">
|
||||
<span class="glow-dot__title" :style="titleStyle">{{ event.title }}</span>
|
||||
<span class="glow-dot__date" :style="dateStyle">{{ formattedDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useEventsStore } from 'stores/events'
|
||||
import { useSettingsStore } from 'stores/settings'
|
||||
import { useImageCache } from 'composables/useImageCache'
|
||||
|
||||
const props = defineProps({
|
||||
event: { type: Object, required: true },
|
||||
x: { type: Number, default: 0 },
|
||||
isGhost: { type: Boolean, default: false },
|
||||
selected: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
const eventsStore = useEventsStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
// Resolve image: cached thumbnail from IndexedDB or fetch & cache
|
||||
const { resolvedSrc: imageSrc } = props.event.image
|
||||
? useImageCache(props.event.image, props.event.id)
|
||||
: { resolvedSrc: computed(() => null) }
|
||||
|
||||
const fl = computed(() => settingsStore.floatingLines)
|
||||
const glowColor = computed(() => eventsStore.getGlowColor(props.event))
|
||||
|
||||
// CSS diameter derived from settings (circleRadius is half-size in px)
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
const dotSize = computed(() => 2 * fl.value.circleRadius / dpr)
|
||||
|
||||
// Y position: emotion +1 → top (20%), 0 → middle (47%), -1 → bottom (74%)
|
||||
|
||||
const yPercent = computed(() => 48 - props.event.emotion * 30)
|
||||
|
||||
// Label above dot when dot is in lower half, below when in upper half
|
||||
const labelAbove = computed(() => yPercent.value > 48)
|
||||
|
||||
// Format date as "12. Feb 2024"
|
||||
const MONTH_SHORT = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']
|
||||
const formattedDate = computed(() => {
|
||||
const d = new Date(props.event.date)
|
||||
return `${d.getDate()}. ${MONTH_SHORT[d.getMonth()]} ${d.getFullYear()}`
|
||||
})
|
||||
|
||||
// Label font sizes per setting
|
||||
const LABEL_FONT = { small: { title: 10, date: 9 }, medium: { title: 12, date: 11 }, large: { title: 14, date: 13 } }
|
||||
const labelFont = computed(() => LABEL_FONT[fl.value.labelSize] ?? LABEL_FONT.small)
|
||||
const labelColor = computed(() => fl.value.labelColor ?? '#ffffff')
|
||||
|
||||
const labelStyle = computed(() => ({
|
||||
maxWidth: labelFont.value.title >= 14 ? '120px' : '90px'
|
||||
}))
|
||||
const titleStyle = computed(() => ({
|
||||
fontSize: `${labelFont.value.title}px`,
|
||||
color: labelColor.value,
|
||||
maxWidth: labelFont.value.title >= 14 ? '120px' : '90px'
|
||||
}))
|
||||
const dateStyle = computed(() => ({
|
||||
fontSize: `${labelFont.value.date}px`,
|
||||
color: labelColor.value
|
||||
}))
|
||||
|
||||
const dotStyle = computed(() => ({
|
||||
left: `${props.x}px`,
|
||||
top: `${yPercent.value}%`,
|
||||
width: `${dotSize.value}px`,
|
||||
height: `${dotSize.value}px`
|
||||
}))
|
||||
|
||||
// Two-layer box-shadow: tight bright core + wide soft halo
|
||||
const glowShadow = computed(() => {
|
||||
const size = fl.value.glowSize
|
||||
const strength = fl.value.glowStrength
|
||||
const color = glowColor.value
|
||||
const core = alphaHex(Math.min(strength / 3, 1))
|
||||
const halo = alphaHex(Math.min(strength / 7, 1))
|
||||
return `0 0 ${size}px 0px ${color}${core}, 0 0 ${size * 2.5}px ${size * 0.3}px ${color}${halo}`
|
||||
})
|
||||
|
||||
function alphaHex(a) {
|
||||
return Math.round(Math.min(1, Math.max(0, a)) * 255).toString(16).padStart(2, '0')
|
||||
}
|
||||
|
||||
const isDimmed = computed(() =>
|
||||
eventsStore.selectedEventId !== null && !props.selected && !props.isGhost
|
||||
)
|
||||
|
||||
function onSelect() {
|
||||
if (!props.isGhost) emit('select')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.glow-dot {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
z-index: 10;
|
||||
transition: opacity 0.3s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.glow-dot__inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.glow-dot__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* States */
|
||||
.glow-dot--ghost {
|
||||
opacity: 1;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.glow-dot--selected {
|
||||
transform: translate(-50%, -50%) scale(1.15);
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
.glow-dot--dimmed {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Event label (title + date) */
|
||||
.glow-dot__label {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: calc(100% + 6px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
max-width: 90px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* When dot is in lower half, show label above */
|
||||
.glow-dot--label-above .glow-dot__label {
|
||||
top: auto;
|
||||
bottom: calc(100% + 6px);
|
||||
}
|
||||
|
||||
.glow-dot__title {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 90px;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.glow-dot__date {
|
||||
font-size: 9px;
|
||||
font-weight: 400;
|
||||
opacity: 0.4;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue