diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue index db6ed1183..417ee8eae 100644 --- a/app/components/Package/Header.vue +++ b/app/components/Package/Header.vue @@ -184,6 +184,22 @@ onKeyStroke( // TODO: Maybe set this where it's not loaded here every load? const { user } = useAtproto() +const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)') + +const likeAnimKey = shallowRef(0) +const showLikeFloat = shallowRef(false) +const likeFloatKey = shallowRef(0) +let likeFloatTimer: ReturnType | null = null + +const heartAnimStyle = computed(() => { + if (likeAnimKey.value === 0 || prefersReducedMotion.value) return {} + return { + animation: likesData.value?.userHasLiked + ? 'heart-spring 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards' + : 'heart-unlike 0.3s ease forwards', + } +}) + const authModal = useModal('auth-modal') const { data: likesData, status: likeStatus } = useFetch( @@ -211,6 +227,21 @@ const likeAction = async () => { const currentlyLiked = likesData.value?.userHasLiked ?? false const currentLikes = likesData.value?.totalLikes ?? 0 + likeAnimKey.value++ + + if (!currentlyLiked && !prefersReducedMotion.value) { + if (likeFloatTimer !== null) { + clearTimeout(likeFloatTimer) + likeFloatTimer = null + } + likeFloatKey.value++ + showLikeFloat.value = true + likeFloatTimer = setTimeout(() => { + showLikeFloat.value = false + likeFloatTimer = null + }, 850) + } + // Optimistic update likesData.value = { totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1, @@ -293,26 +324,43 @@ const likeAction = async () => { class="items-center" strategy="fixed" > - +
+ + +
@@ -458,4 +506,99 @@ const likeAction = async () => { display: none; } } + +.likeWrapper { + position: relative; + display: inline-flex; +} + +.likeFloat { + position: absolute; + top: 0; + left: 50%; + font-size: 12px; + font-weight: 600; + color: var(--color-red-500, #ef4444); + pointer-events: none; + white-space: nowrap; + animation: float-up 0.75s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; +} + +@media (prefers-reduced-motion: reduce) { + .likeFloat { + display: none; + } +} + +@keyframes float-up { + 0% { + opacity: 0; + transform: translateX(-50%) translateY(0); + } + 15% { + opacity: 1; + transform: translateX(-50%) translateY(-4px); + } + 80% { + opacity: 1; + transform: translateX(-50%) translateY(-20px); + } + 100% { + opacity: 0; + transform: translateX(-50%) translateY(-28px); + } +} + + +