-
-
Notifications
You must be signed in to change notification settings - Fork 271
feat: package likes #712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: package likes #712
Changes from all commits
06d854c
1b5dfbe
4dc224c
81548b7
20a521b
d209cd1
d6f58dd
b39dc33
703935b
798dc71
94b6cb3
15a2d7a
21a99a4
2ee102b
135ca56
6045cb7
e401ab5
f6ed29d
f913b30
449aed7
40488fd
988c5c7
2ba4075
0f5887c
b8792f9
181d044
4c70696
f42a24d
60211db
d01eddb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,9 @@ import { areUrlsEquivalent } from '#shared/utils/url' | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { isEditableElement } from '~/utils/input' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { formatBytes } from '~/utils/formatters' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { NuxtLink } from '#components' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useModal } from '~/composables/useModal' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAtproto } from '~/composables/atproto/useAtproto' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { togglePackageLike } from '~/utils/atproto/likes' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| definePageMeta({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: 'package', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -356,6 +359,54 @@ const canonicalUrl = computed(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return requestedVersion.value ? `${base}/v/${requestedVersion.value}` : base | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //atproto | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Maybe set this where it's not loaded here every load? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { user } = useAtproto() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const authModal = useModal('auth-modal') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: likesData } = useFetch(() => `/api/social/likes/${packageName.value}`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: () => ({ totalLikes: 0, userHasLiked: false }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| server: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isLikeActionPending = ref(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const likeAction = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (user.value?.handle == null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authModal.open() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isLikeActionPending.value) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentlyLiked = likesData.value?.userHasLiked ?? false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentLikes = likesData.value?.totalLikes ?? 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Optimistic update | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| likesData.value = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userHasLiked: !currentlyLiked, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLikeActionPending.value = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await togglePackageLike(packageName.value, currentlyLiked, user.value?.handle) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isLikeActionPending.value = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Update with server response | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| likesData.value = result.data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Revert on error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| likesData.value = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| totalLikes: currentLikes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userHasLiked: currentlyLiked, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+375
to
+407
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure pending state resets even when the request throws. If 🛡️ Proposed fix // Optimistic update
likesData.value = {
totalLikes: currentlyLiked ? currentLikes - 1 : currentLikes + 1,
userHasLiked: !currentlyLiked,
}
isLikeActionPending.value = true
- const result = await togglePackageLike(packageName.value, currentlyLiked, user.value?.handle)
-
- isLikeActionPending.value = false
-
- if (result.success) {
- // Update with server response
- likesData.value = result.data
- } else {
- // Revert on error
- likesData.value = {
- totalLikes: currentLikes,
- userHasLiked: currentlyLiked,
- }
- }
+ try {
+ const result = await togglePackageLike(packageName.value, currentlyLiked, user.value?.handle)
+ if (result.success) {
+ // Update with server response
+ likesData.value = result.data
+ } else {
+ // Revert on error
+ likesData.value = {
+ totalLikes: currentLikes,
+ userHasLiked: currentlyLiked,
+ }
+ }
+ } catch {
+ likesData.value = {
+ totalLikes: currentLikes,
+ userHasLiked: currentlyLiked,
+ }
+ } finally {
+ isLikeActionPending.value = false
+ }📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useHead({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| link: [{ rel: 'canonical', href: canonicalUrl }], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -497,10 +548,31 @@ defineOgImageComponent('Package', { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :is-binary="isBinaryOnly" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="self-baseline ms-1 sm:ms-2" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Package likes --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @click="likeAction" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="inline-flex items-center gap-1.5 font-mono text-sm text-fg hover:text-fg-muted transition-colors duration-200" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :title="$t('package.links.like')" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| :class=" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| likesData?.userHasLiked | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'i-lucide-heart-minus text-red-500' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'i-lucide-heart-plus' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| " | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class="w-4 h-4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-hidden="true" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span>{{ formatCompactNumber(likesData?.totalLikes ?? 0, { decimals: 1 }) }}</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <template #fallback> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div class="flex items-center gap-1.5 self-baseline ms-1 sm:ms-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SkeletonBlock class="w-8 h-5 rounded" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SkeletonBlock class="w-12 h-5 rounded" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SkeletonBlock class="w-5 h-5 rounded" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ClientOnly> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import type { FetchError } from 'ofetch' | ||
| import type { LocationQueryRaw } from 'vue-router' | ||
|
|
||
| /** | ||
| * Redirect user to ATProto authentication | ||
| */ | ||
| export async function authRedirect(identifier: string, create: boolean = false) { | ||
| let query: LocationQueryRaw = { handle: identifier } | ||
| if (create) { | ||
| query = { ...query, create: 'true' } | ||
| } | ||
| await navigateTo( | ||
| { | ||
| path: '/api/auth/atproto', | ||
| query, | ||
| }, | ||
| { external: true }, | ||
| ) | ||
| } | ||
|
|
||
| export async function handleAuthError( | ||
| fetchError: FetchError, | ||
| userHandle?: string | null, | ||
| ): Promise<never> { | ||
| const errorMessage = fetchError?.data?.message | ||
| if (errorMessage === ERROR_NEED_REAUTH && userHandle) { | ||
| await authRedirect(userHandle) | ||
| } | ||
| throw fetchError | ||
| } | ||
|
Comment on lines
+21
to
+30
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "=== Checking for nuxt.config files ==="
fd -t f -e 'ts' -e 'js' 'nuxt.config' .
echo -e "\n=== Searching for ERROR_NEED_REAUTH usage across codebase ==="
rg "ERROR_NEED_REAUTH" -n --type ts --type js
echo -e "\n=== Checking if ERROR_NEED_REAUTH is defined in constants ==="
fd -t f 'constants' | head -20 | xargs cat 2>/dev/null | grep -B 2 -A 2 "ERROR_NEED_REAUTH" || echo "No direct matches found"
echo -e "\n=== Checking app/utils/atproto/helpers.ts for all imports ==="
cat app/utils/atproto/helpers.tsRepository: npmx-dev/npmx.dev Length of output: 1492 🏁 Script executed: #!/bin/bash
echo "=== Checking nuxt.config.ts for auto-import settings ==="
cat nuxt.config.ts | grep -A 30 "imports" || echo "No imports section found"
echo -e "\n=== Checking server/utils/atproto/oauth.ts for ERROR_NEED_REAUTH import ==="
head -40 server/utils/atproto/oauth.tsRepository: npmx-dev/npmx.dev Length of output: 2915 🏁 Script executed: #!/bin/bash
echo "=== Checking server/utils/atproto/oauth.ts around line 76 where ERROR_NEED_REAUTH is used ==="
sed -n '1,90p' server/utils/atproto/oauth.ts | tail -50Repository: npmx-dev/npmx.dev Length of output: 1873 🏁 Script executed: #!/bin/bash
echo "=== Checking complete imports in server/utils/atproto/oauth.ts ==="
head -20 server/utils/atproto/oauth.tsRepository: npmx-dev/npmx.dev Length of output: 1019 Import
import { ERROR_NEED_REAUTH } from '#shared/utils/constants'This constant is defined in |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { FetchError } from 'ofetch' | ||
| import { handleAuthError } from '~/utils/atproto/helpers' | ||
| import type { PackageLikes } from '#shared/types/social' | ||
|
|
||
| export type LikeResult = { success: true; data: PackageLikes } | { success: false; error: Error } | ||
|
|
||
| /** | ||
| * Like a package via the API | ||
| */ | ||
| export async function likePackage( | ||
| packageName: string, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| try { | ||
| const result = await $fetch<PackageLikes>('/api/social/like', { | ||
| method: 'POST', | ||
| body: { packageName }, | ||
| }) | ||
| return { success: true, data: result } | ||
| } catch (e) { | ||
| if (e instanceof FetchError) { | ||
| await handleAuthError(e, userHandle) | ||
| } | ||
| return { success: false, error: e as Error } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Unlike a package via the API | ||
| */ | ||
| export async function unlikePackage( | ||
| packageName: string, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| try { | ||
| const result = await $fetch<PackageLikes>('/api/social/like', { | ||
| method: 'DELETE', | ||
| body: { packageName }, | ||
| }) | ||
| return { success: true, data: result } | ||
| } catch (e) { | ||
| if (e instanceof FetchError) { | ||
| await handleAuthError(e, userHandle) | ||
| } | ||
| return { success: false, error: e as Error } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Toggle like status for a package | ||
| */ | ||
| export async function togglePackageLike( | ||
| packageName: string, | ||
| currentlyLiked: boolean, | ||
| userHandle?: string | null, | ||
| ): Promise<LikeResult> { | ||
| return currentlyLiked | ||
| ? unlikePackage(packageName, userHandle) | ||
| : likePackage(packageName, userHandle) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { Agent } from '@atproto/api' | ||
| import { NodeOAuthClient } from '@atproto/oauth-client-node' | ||
| import { createError, getQuery, sendRedirect } from 'h3' | ||
| import { getOAuthLock } from '#server/utils/atproto/lock' | ||
| import { useOAuthStorage } from '#server/utils/atproto/storage' | ||
| import { SLINGSHOT_HOST } from '#shared/utils/constants' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing import for The constant is used on line 14 but not imported. This will cause a 🐛 Proposed fix to add the missing import-import { SLINGSHOT_HOST } from '#shared/utils/constants'
+import { SLINGSHOT_HOST, UNSET_NUXT_SESSION_PASSWORD } from '#shared/utils/constants'Also applies to: 14-14 |
||
| import { useServerSession } from '#server/utils/server-session' | ||
|
|
@@ -11,7 +12,7 @@ export default defineEventHandler(async event => { | |
| if (!config.sessionPassword) { | ||
| throw createError({ | ||
| status: 500, | ||
| message: 'NUXT_SESSION_PASSWORD not set', | ||
| message: UNSET_NUXT_SESSION_PASSWORD, | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -24,6 +25,7 @@ export default defineEventHandler(async event => { | |
| stateStore, | ||
| sessionStore, | ||
| clientMetadata, | ||
| requestLock: getOAuthLock(), | ||
| }) | ||
|
|
||
| if (!query.code) { | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about making xrpc routes (sth like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now we're just using the the server side oauth client so if we made this endpoint an XRPC it wouldn't be quite correct since it uses the cookie for authentication. But there is a plan for some XRPC endpoints for other things just need to make a middleware for it to authenticate the service auth jwt
43081j marked this conversation as resolved.
Show resolved
Hide resolved
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as v from 'valibot' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Client } from '@atproto/lex' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as dev from '#shared/types/lexicons/dev' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PackageLikeBodySchema } from '#shared/schemas/social' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { throwOnMissingOAuthScope } from '#server/utils/atproto/oauth' | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check nuxt.config for auto-import configuration
echo "=== Checking nuxt.config for auto-import settings ==="
find . -name 'nuxt.config.*' -type f | head -5 | xargs -I {} sh -c 'echo "File: {}"; cat {}'
echo -e "\n=== Checking like.post.ts for LIKES_SCOPE import pattern ==="
fd -t f 'like.post.ts' | head -1 | xargs -I {} sh -c 'echo "File: {}"; cat {}'
echo -e "\n=== Checking like.delete.ts full content ==="
cat server/api/social/like.delete.ts
echo -e "\n=== Verifying LIKES_SCOPE definition in constants ==="
grep -n "LIKES_SCOPE" '#shared/utils/constants' 2>/dev/null || find . -path '*/shared/utils/constants*' -type f | xargs grep -l "LIKES_SCOPE" 2>/dev/null | head -1 | xargs grep -n "LIKES_SCOPE"Repository: npmx-dev/npmx.dev Length of output: 10575 Add missing import for
🔧 Proposed fix import * as v from 'valibot'
import { Client } from '@atproto/lex'
import * as dev from '#shared/types/lexicons/dev'
import { PackageLikeBodySchema } from '#shared/schemas/social'
import { throwOnMissingOAuthScope } from '#server/utils/atproto/oauth'
+import { LIKES_SCOPE } from '#shared/utils/constants'📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export default eventHandlerWithOAuthSession(async (event, oAuthSession) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const loggedInUsersDid = oAuthSession?.did.toString() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!oAuthSession || !loggedInUsersDid) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| throw createError({ statusCode: 401, statusMessage: 'Unauthorized' }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| //Checks if the user has a scope to like packages | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await throwOnMissingOAuthScope(oAuthSession, LIKES_SCOPE) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const body = v.parse(PackageLikeBodySchema, await readBody(event)) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const likesUtil = new PackageLikesUtils() | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const getTheUsersLikedRecord = await likesUtil.getTheUsersLikedRecord( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| body.packageName, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| loggedInUsersDid, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (getTheUsersLikedRecord) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
43081j marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const client = new Client(oAuthSession) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| await client.delete(dev.npmx.feed.like, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| rkey: getTheUsersLikedRecord.rkey, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await likesUtil.unlikeAPackageAndReturnLikes(body.packageName, loggedInUsersDid) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+26
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for the AT Protocol delete operation. The Consider wrapping the operation in a try-catch to handle failures gracefully: 🛡️ Proposed fix if (getTheUsersLikedRecord) {
const client = new Client(oAuthSession)
- await client.delete(dev.npmx.feed.like, {
- rkey: getTheUsersLikedRecord.rkey,
- })
- const result = await likesUtil.unlikeAPackageAndReturnLikes(body.packageName, loggedInUsersDid)
- return result
+ try {
+ await client.delete(dev.npmx.feed.like, {
+ rkey: getTheUsersLikedRecord.rkey,
+ })
+ const result = await likesUtil.unlikeAPackageAndReturnLikes(body.packageName, loggedInUsersDid)
+ return result
+ }
+ catch (error) {
+ console.error(`Failed to delete like for package ${body.packageName}:`, error)
+ throw createError({ statusCode: 500, statusMessage: 'Failed to unlike package' })
+ }
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `User ${loggedInUsersDid} tried to unlike a package ${body.packageName} but it was not liked by them.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return await likesUtil.getLikes(body.packageName, loggedInUsersDid) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clamp optimistic counts to avoid negatives.
If
currentLikesis stale (e.g., 0 whileuserHasLikedis true), the optimistic decrement can go negative.🧮 Suggested clamp
📝 Committable suggestion