|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import type { RouteLocationRaw } from 'vue-router' |
3 | 3 | import { SCROLL_TO_TOP_THRESHOLD } from '~/composables/useScrollToTop' |
4 | | -import { useModal } from '~/composables/useModal' |
5 | | -import { useAtproto } from '~/composables/atproto/useAtproto' |
6 | | -import { togglePackageLike } from '~/utils/atproto/likes' |
7 | 4 | import { isEditableElement } from '~/utils/input' |
8 | 5 |
|
9 | 6 | const props = defineProps<{ |
@@ -64,7 +61,6 @@ const { y: scrollY } = useScroll(window) |
64 | 61 | const showScrollToTop = computed(() => scrollY.value > SCROLL_TO_TOP_THRESHOLD) |
65 | 62 |
|
66 | 63 | const packageName = computed(() => props.pkg?.name ?? '') |
67 | | -const compactNumberFormatter = useCompactNumberFormatter() |
68 | 64 |
|
69 | 65 | const { copied: copiedPkgName, copy: copyPkgName } = useClipboard({ |
70 | 66 | source: packageName, |
@@ -178,101 +174,6 @@ onKeyStroke( |
178 | 174 | { dedupe: true }, |
179 | 175 | ) |
180 | 176 |
|
181 | | -//atproto |
182 | | -// TODO: Maybe set this where it's not loaded here every load? |
183 | | -const { user } = useAtproto() |
184 | | -
|
185 | | -const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)') |
186 | | -
|
187 | | -const likeAnimKey = shallowRef(0) |
188 | | -const showLikeFloat = shallowRef(false) |
189 | | -const likeFloatKey = shallowRef(0) |
190 | | -let likeFloatTimer: ReturnType<typeof setTimeout> | null = null |
191 | | -
|
192 | | -const heartAnimStyle = computed(() => { |
193 | | - if (likeAnimKey.value === 0 || prefersReducedMotion.value) return {} |
194 | | - return { |
195 | | - animation: likesData.value?.userHasLiked |
196 | | - ? 'heart-spring 0.55s cubic-bezier(0.34,1.56,0.64,1) forwards' |
197 | | - : 'heart-unlike 0.3s ease forwards', |
198 | | - } |
199 | | -}) |
200 | | -
|
201 | | -const authModal = useModal('auth-modal') |
202 | | -
|
203 | | -const { data: likesData, status: likeStatus } = useFetch( |
204 | | - () => `/api/social/likes/${packageName.value}`, |
205 | | - { |
206 | | - default: () => ({ totalLikes: 0, userHasLiked: false }), |
207 | | - server: false, |
208 | | - }, |
209 | | -) |
210 | | -
|
211 | | -const isLoadingLikeData = computed( |
212 | | - () => likeStatus.value === 'pending' || likeStatus.value === 'idle', |
213 | | -) |
214 | | -
|
215 | | -const isLikeActionPending = shallowRef(false) |
216 | | -
|
217 | | -const likeAction = async () => { |
218 | | - if (user.value?.handle == null) { |
219 | | - authModal.open() |
220 | | - return |
221 | | - } |
222 | | -
|
223 | | - if (isLikeActionPending.value) return |
224 | | -
|
225 | | - const currentlyLiked = likesData.value?.userHasLiked ?? false |
226 | | - const currentLikes = likesData.value?.totalLikes ?? 0 |
227 | | -
|
228 | | - likeAnimKey.value++ |
229 | | -
|
230 | | - if (!currentlyLiked && !prefersReducedMotion.value) { |
231 | | - if (likeFloatTimer !== null) { |
232 | | - clearTimeout(likeFloatTimer) |
233 | | - likeFloatTimer = null |
234 | | - } |
235 | | - likeFloatKey.value++ |
236 | | - showLikeFloat.value = true |
237 | | - likeFloatTimer = setTimeout(() => { |
238 | | - showLikeFloat.value = false |
239 | | - likeFloatTimer = null |
240 | | - }, 850) |
241 | | - } |
242 | | -
|
243 | | - // Optimistic update |
244 | | - likesData.value = { |
245 | | - totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1, |
246 | | - userHasLiked: !currentlyLiked, |
247 | | - } |
248 | | -
|
249 | | - isLikeActionPending.value = true |
250 | | -
|
251 | | - try { |
252 | | - const result = await togglePackageLike(packageName.value, currentlyLiked, user.value?.handle) |
253 | | -
|
254 | | - isLikeActionPending.value = false |
255 | | -
|
256 | | - if (result.success) { |
257 | | - // Update with server response |
258 | | - likesData.value = result.data |
259 | | - } else { |
260 | | - // Revert on error |
261 | | - likesData.value = { |
262 | | - totalLikes: currentLikes, |
263 | | - userHasLiked: currentlyLiked, |
264 | | - } |
265 | | - } |
266 | | - } catch { |
267 | | - // Revert on error |
268 | | - likesData.value = { |
269 | | - totalLikes: currentLikes, |
270 | | - userHasLiked: currentlyLiked, |
271 | | - } |
272 | | - isLikeActionPending.value = false |
273 | | - } |
274 | | -} |
275 | | -
|
276 | 177 | const fundingUrl = computed(() => { |
277 | 178 | let funding = props.displayVersion?.funding |
278 | 179 | if (Array.isArray(funding)) funding = funding[0] |
@@ -318,57 +219,7 @@ const fundingUrl = computed(() => { |
318 | 219 | > |
319 | 220 | <span class="max-sm:sr-only">{{ $t('package.links.compare_this_package') }}</span> |
320 | 221 | </LinkBase> |
321 | | - <!-- Package likes --> |
322 | | - <TooltipApp |
323 | | - :text=" |
324 | | - isLoadingLikeData |
325 | | - ? $t('common.loading') |
326 | | - : likesData?.userHasLiked |
327 | | - ? $t('package.likes.unlike') |
328 | | - : $t('package.likes.like') |
329 | | - " |
330 | | - position="bottom" |
331 | | - class="items-center" |
332 | | - strategy="fixed" |
333 | | - > |
334 | | - <div :class="$style.likeWrapper"> |
335 | | - <span |
336 | | - v-if="showLikeFloat" |
337 | | - :key="likeFloatKey" |
338 | | - aria-hidden="true" |
339 | | - :class="$style.likeFloat" |
340 | | - >+1</span |
341 | | - > |
342 | | - <ButtonBase |
343 | | - @click="likeAction" |
344 | | - size="medium" |
345 | | - :aria-label=" |
346 | | - likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like') |
347 | | - " |
348 | | - :aria-pressed="likesData?.userHasLiked" |
349 | | - > |
350 | | - <span |
351 | | - :key="likeAnimKey" |
352 | | - :class=" |
353 | | - likesData?.userHasLiked |
354 | | - ? 'i-lucide:heart-minus fill-red-500 text-red-500' |
355 | | - : 'i-lucide:heart-plus' |
356 | | - " |
357 | | - :style="heartAnimStyle" |
358 | | - aria-hidden="true" |
359 | | - class="inline-block w-4 h-4" |
360 | | - /> |
361 | | - <span |
362 | | - v-if="isLoadingLikeData" |
363 | | - class="i-svg-spinners:ring-resize w-3 h-3 my-0.5" |
364 | | - aria-hidden="true" |
365 | | - /> |
366 | | - <span v-else> |
367 | | - {{ compactNumberFormatter.format(likesData?.totalLikes ?? 0) }} |
368 | | - </span> |
369 | | - </ButtonBase> |
370 | | - </div> |
371 | | - </TooltipApp> |
| 222 | + <PackageLikes :packageName /> |
372 | 223 |
|
373 | 224 | <LinkBase |
374 | 225 | variant="button-secondary" |
@@ -522,99 +373,4 @@ const fundingUrl = computed(() => { |
522 | 373 | display: none; |
523 | 374 | } |
524 | 375 | } |
525 | | -
|
526 | | -.likeWrapper { |
527 | | - position: relative; |
528 | | - display: inline-flex; |
529 | | -} |
530 | | -
|
531 | | -.likeFloat { |
532 | | - position: absolute; |
533 | | - top: 0; |
534 | | - left: 50%; |
535 | | - font-size: 12px; |
536 | | - font-weight: 600; |
537 | | - color: var(--color-red-500, #ef4444); |
538 | | - pointer-events: none; |
539 | | - white-space: nowrap; |
540 | | - animation: float-up 0.75s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; |
541 | | -} |
542 | | -
|
543 | | -@media (prefers-reduced-motion: reduce) { |
544 | | - .likeFloat { |
545 | | - display: none; |
546 | | - } |
547 | | -} |
548 | | -
|
549 | | -@keyframes float-up { |
550 | | - 0% { |
551 | | - opacity: 0; |
552 | | - transform: translateX(-50%) translateY(0); |
553 | | - } |
554 | | - 15% { |
555 | | - opacity: 1; |
556 | | - transform: translateX(-50%) translateY(-4px); |
557 | | - } |
558 | | - 80% { |
559 | | - opacity: 1; |
560 | | - transform: translateX(-50%) translateY(-20px); |
561 | | - } |
562 | | - 100% { |
563 | | - opacity: 0; |
564 | | - transform: translateX(-50%) translateY(-28px); |
565 | | - } |
566 | | -} |
567 | | -</style> |
568 | | - |
569 | | -<style> |
570 | | -@keyframes heart-spring { |
571 | | - 0% { |
572 | | - transform: scale(1); |
573 | | - } |
574 | | - 15% { |
575 | | - transform: scale(0.78); |
576 | | - } |
577 | | - 45% { |
578 | | - transform: scale(1.55); |
579 | | - } |
580 | | - 65% { |
581 | | - transform: scale(0.93); |
582 | | - } |
583 | | - 80% { |
584 | | - transform: scale(1.1); |
585 | | - } |
586 | | - 100% { |
587 | | - transform: scale(1); |
588 | | - } |
589 | | -} |
590 | | -
|
591 | | -@keyframes heart-unlike { |
592 | | - 0% { |
593 | | - transform: scale(1); |
594 | | - } |
595 | | - 30% { |
596 | | - transform: scale(0.85); |
597 | | - } |
598 | | - 60% { |
599 | | - transform: scale(1.05); |
600 | | - } |
601 | | - 100% { |
602 | | - transform: scale(1); |
603 | | - } |
604 | | -} |
605 | | -
|
606 | | -@media (prefers-reduced-motion: reduce) { |
607 | | - @keyframes heart-spring { |
608 | | - from, |
609 | | - to { |
610 | | - transform: scale(1); |
611 | | - } |
612 | | - } |
613 | | - @keyframes heart-unlike { |
614 | | - from, |
615 | | - to { |
616 | | - transform: scale(1); |
617 | | - } |
618 | | - } |
619 | | -} |
620 | 376 | </style> |
0 commit comments