@@ -13,6 +13,9 @@ import { isEditableElement } from '~/utils/input'
1313import { formatBytes } from ' ~/utils/formatters'
1414import { getDependencyCount } from ' ~/utils/npm/dependency-count'
1515import { NuxtLink } from ' #components'
16+ import { useModal } from ' ~/composables/useModal'
17+ import { useAtproto } from ' ~/composables/atproto/useAtproto'
18+ import { togglePackageLike } from ' ~/utils/atproto/likes'
1619
1720definePageMeta ({
1821 name: ' package' ,
@@ -352,6 +355,54 @@ const canonicalUrl = computed(() => {
352355 return requestedVersion .value ? ` ${base }/v/${requestedVersion .value } ` : base
353356})
354357
358+ // atproto
359+ // TODO: Maybe set this where it's not loaded here every load?
360+ const { user } = useAtproto ()
361+
362+ const authModal = useModal (' auth-modal' )
363+
364+ const { data : likesData } = useFetch (() => ` /api/social/likes/${packageName .value } ` , {
365+ default : () => ({ totalLikes: 0 , userHasLiked: false }),
366+ server: false ,
367+ })
368+
369+ const isLikeActionPending = ref (false )
370+
371+ const likeAction = async () => {
372+ if (user .value ?.handle == null ) {
373+ authModal .open ()
374+ return
375+ }
376+
377+ if (isLikeActionPending .value ) return
378+
379+ const currentlyLiked = likesData .value ?.userHasLiked ?? false
380+ const currentLikes = likesData .value ?.totalLikes ?? 0
381+
382+ // Optimistic update
383+ likesData .value = {
384+ totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1 ,
385+ userHasLiked: ! currentlyLiked ,
386+ }
387+
388+ isLikeActionPending .value = true
389+
390+ const result = await togglePackageLike (packageName .value , currentlyLiked , user .value ?.handle )
391+
392+ isLikeActionPending .value = false
393+
394+ if (result .success ) {
395+ // Update with server response
396+ likesData .value = result .data
397+ } else {
398+ // Revert on error
399+ likesData .value = {
400+ totalLikes: currentLikes ,
401+ userHasLiked: currentlyLiked ,
402+ }
403+ }
404+ }
405+
355406useHead ({
356407 link: [{ rel: ' canonical' , href: canonicalUrl }],
357408})
@@ -493,10 +544,31 @@ defineOgImageComponent('Package', {
493544 :is-binary =" isBinaryOnly"
494545 class =" self-baseline ms-1 sm:ms-2"
495546 />
547+
548+ <!-- Package likes -->
549+ <button
550+ @click =" likeAction"
551+ type =" button"
552+ class =" inline-flex items-center gap-1.5 font-mono text-sm text-fg hover:text-fg-muted transition-colors duration-200"
553+ :title =" $t('package.links.like')"
554+ >
555+ <span
556+ :class ="
557+ likesData?.userHasLiked
558+ ? 'i-lucide-heart-minus text-red-500'
559+ : 'i-lucide-heart-plus'
560+ "
561+ class =" w-4 h-4"
562+ aria-hidden =" true"
563+ />
564+ <span >{{ formatCompactNumber(likesData?.totalLikes ?? 0, { decimals: 1 }) }}</span >
565+ </button >
566+
496567 <template #fallback >
497568 <div class =" flex items-center gap-1.5 self-baseline ms-1 sm:ms-2" >
498569 <SkeletonBlock class =" w-8 h-5 rounded" />
499570 <SkeletonBlock class =" w-12 h-5 rounded" />
571+ <SkeletonBlock class =" w-5 h-5 rounded" />
500572 </div >
501573 </template >
502574 </ClientOnly >
0 commit comments