@@ -12,6 +12,9 @@ import { areUrlsEquivalent } from '#shared/utils/url'
1212import { isEditableElement } from ' ~/utils/input'
1313import { formatBytes } from ' ~/utils/formatters'
1414import { NuxtLink } from ' #components'
15+ import { useModal } from ' ~/composables/useModal'
16+ import { useAtproto } from ' ~/composables/atproto/useAtproto'
17+ import { togglePackageLike } from ' ~/utils/atproto/likes'
1518
1619definePageMeta ({
1720 name: ' package' ,
@@ -356,6 +359,54 @@ const canonicalUrl = computed(() => {
356359 return requestedVersion .value ? ` ${base }/v/${requestedVersion .value } ` : base
357360})
358361
362+ // atproto
363+ // TODO: Maybe set this where it's not loaded here every load?
364+ const { user } = useAtproto ()
365+
366+ const authModal = useModal (' auth-modal' )
367+
368+ const { data : likesData } = useFetch (() => ` /api/social/likes/${packageName .value } ` , {
369+ default : () => ({ totalLikes: 0 , userHasLiked: false }),
370+ server: false ,
371+ })
372+
373+ const isLikeActionPending = ref (false )
374+
375+ const likeAction = async () => {
376+ if (user .value ?.handle == null ) {
377+ authModal .open ()
378+ return
379+ }
380+
381+ if (isLikeActionPending .value ) return
382+
383+ const currentlyLiked = likesData .value ?.userHasLiked ?? false
384+ const currentLikes = likesData .value ?.totalLikes ?? 0
385+
386+ // Optimistic update
387+ likesData .value = {
388+ totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1 ,
389+ userHasLiked: ! currentlyLiked ,
390+ }
391+
392+ isLikeActionPending .value = true
393+
394+ const result = await togglePackageLike (packageName .value , currentlyLiked , user .value ?.handle )
395+
396+ isLikeActionPending .value = false
397+
398+ if (result .success ) {
399+ // Update with server response
400+ likesData .value = result .data
401+ } else {
402+ // Revert on error
403+ likesData .value = {
404+ totalLikes: currentLikes ,
405+ userHasLiked: currentlyLiked ,
406+ }
407+ }
408+ }
409+
359410useHead ({
360411 link: [{ rel: ' canonical' , href: canonicalUrl }],
361412})
@@ -497,10 +548,31 @@ defineOgImageComponent('Package', {
497548 :is-binary =" isBinaryOnly"
498549 class =" self-baseline ms-1 sm:ms-2"
499550 />
551+
552+ <!-- Package likes -->
553+ <button
554+ @click =" likeAction"
555+ type =" button"
556+ class =" inline-flex items-center gap-1.5 font-mono text-sm text-fg hover:text-fg-muted transition-colors duration-200"
557+ :title =" $t('package.links.like')"
558+ >
559+ <span
560+ :class ="
561+ likesData?.userHasLiked
562+ ? 'i-lucide-heart-minus text-red-500'
563+ : 'i-lucide-heart-plus'
564+ "
565+ class =" w-4 h-4"
566+ aria-hidden =" true"
567+ />
568+ <span >{{ formatCompactNumber(likesData?.totalLikes ?? 0, { decimals: 1 }) }}</span >
569+ </button >
570+
500571 <template #fallback >
501572 <div class =" flex items-center gap-1.5 self-baseline ms-1 sm:ms-2" >
502573 <SkeletonBlock class =" w-8 h-5 rounded" />
503574 <SkeletonBlock class =" w-12 h-5 rounded" />
575+ <SkeletonBlock class =" w-5 h-5 rounded" />
504576 </div >
505577 </template >
506578 </ClientOnly >
0 commit comments