Files
BRETT/app/components/CommentReactions.vue
FliesentischKingandDevin fab086d17e Discord-style reaction tooltips
Tooltips now show a large emoji on the left with reactor names
stacked vertically, instead of a comma-separated name list.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-04 03:05:41 +02:00

131 lines
3.6 KiB
Vue

<script setup lang="ts">
import { toast } from 'vue-sonner'
import { cn } from '@/lib/utils'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
const props = defineProps<{ commentId: number }>()
const { user } = useAuth()
const { reactionsFor, toggle } = useCommentReactions()
const { t } = useI18n()
const busy = ref(false)
const pending = ref<string | null>(null)
const reactions = computed(() => reactionsFor(props.commentId))
const BURST_LINES = Array.from({ length: 12 }, (_, i) => ({
angle: i * 30,
long: i % 2 === 0,
}))
const burstEmoji = ref<string | null>(null)
const burstNonce = ref(0)
async function pick(emoji: string) {
if (!user.value) {
toast.error(t('comments.loginToReact'))
return
}
if (busy.value) return
busy.value = true
pending.value = emoji
burstEmoji.value = emoji
burstNonce.value++
try {
await toggle(props.commentId, emoji)
}
catch {
toast.error(t('comments.reactionFailed'))
}
finally {
busy.value = false
pending.value = null
}
}
</script>
<template>
<div v-if="reactions.length" class="mt-3.5 flex flex-wrap items-center gap-2">
<Tooltip v-for="r in reactions" :key="r.emoji">
<TooltipTrigger as-child>
<button
type="button"
class="relative flex cursor-pointer items-center gap-2 rounded-md border px-2.5 py-1 transition-colors"
:class="cn(
r.me
? 'border-foreground/40 bg-secondary hover:border-foreground/60'
: 'border-border bg-muted/40 hover:border-foreground/30',
pending === r.emoji && 'animate-pulse',
)"
@click="pick(r.emoji)"
>
<span v-if="burstEmoji === r.emoji" :key="burstNonce" class="pointer-events-none absolute inset-0" aria-hidden="true">
<i
v-for="l in BURST_LINES"
:key="l.angle"
class="burst-line"
:style="{ '--a': `${l.angle}deg`, '--h': l.long ? '9px' : '6px', '--d': l.long ? '-26px' : '-19px' }"
/>
</span>
<span :key="burstNonce" class="text-xl leading-none" :class="burstEmoji === r.emoji && 'emoji-pop'">{{ r.emoji }}</span>
<span class="text-sm font-medium tabular-nums" :class="r.me ? 'text-foreground' : 'text-muted-foreground'">{{ r.count }}</span>
</button>
</TooltipTrigger>
<TooltipContent class="flex items-center gap-2.5 px-3 py-2">
<span class="text-5xl leading-none">{{ r.emoji }}</span>
<span class="flex flex-col leading-snug">
<span v-for="n in r.names" :key="n">{{ n }}</span>
</span>
</TooltipContent>
</Tooltip>
</div>
</template>
<style scoped>
.burst-line {
position: absolute;
left: 50%;
top: 50%;
width: 3px;
height: var(--h);
margin-left: -1.5px;
margin-top: -3px;
border-radius: 1.5px;
background: var(--color-primary);
opacity: 0;
}
.emoji-pop {
display: inline-block;
}
@media (prefers-reduced-motion: no-preference) {
.burst-line {
animation: burst-line 0.55s cubic-bezier(0.15, 0.85, 0.35, 1) forwards;
}
.emoji-pop {
animation: emoji-pop 0.3s ease-out;
}
}
@keyframes burst-line {
0% {
opacity: 1;
transform: rotate(var(--a)) translateY(-4px) scaleY(0.3);
}
30% {
opacity: 1;
transform: rotate(var(--a)) translateY(calc(var(--d) * 0.7)) scaleY(1.15);
}
100% {
opacity: 0;
transform: rotate(var(--a)) translateY(var(--d)) scaleY(0.5);
}
}
@keyframes emoji-pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
100% { transform: scale(1); }
}
</style>