-
-
Notifications
You must be signed in to change notification settings - Fork 442
feat: profile page #1113
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: profile page #1113
Changes from 12 commits
3bf1cde
09c99c1
ecdc8cf
9591998
c467c3e
dfc4d7d
58a3b0c
0215014
30741b8
6e5be0e
45d35ef
c012a45
b81eda7
f732b14
73ce1df
78db755
d72a756
2e1bbd7
6e41ecc
7f0c987
6c49480
fe73fea
75db4ae
8dfc2d9
5f3874c
b5bf1f2
a3b6cc0
1bb55b8
e2e027d
7bde6b7
b2ecd2c
0804679
2cbf1d9
c4ab87d
cdb891b
91b24a9
e5d1638
8f19abe
0bf4443
14556af
8bf70eb
981b70b
81402ee
a834d7a
a114bf0
5676cf6
e5b9bbd
c89de62
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 |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| <script setup lang="ts"> | ||
| import { useAtproto } from '~/composables/atproto/useAtproto' | ||
| import { togglePackageLike } from '~/utils/atproto/likes' | ||
| const props = defineProps<{ | ||
| packageUrl: string | ||
| }>() | ||
|
|
||
| const compactNumberFormatter = useCompactNumberFormatter() | ||
|
|
||
| function extractPackageFromRef(ref: string) { | ||
| const { pkg } = /https:\/\/npmx.dev\/package\/(?<pkg>.*)/.exec(ref).groups | ||
|
Check failure on line 11 in app/components/Package/LikeCard.vue
|
||
| return pkg | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| const name = computed(() => extractPackageFromRef(props.packageUrl)) | ||
|
|
||
| const { user } = useAtproto() | ||
|
|
||
| const authModal = useModal('auth-modal') | ||
|
|
||
| const { data: likesData } = useFetch(() => `/api/social/likes/${name.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 | ||
|
|
||
| try { | ||
| const result = await togglePackageLike(name.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, | ||
| } | ||
| } | ||
| } catch (e) { | ||
| // Revert on error | ||
| likesData.value = { | ||
| totalLikes: currentLikes, | ||
| userHasLiked: currentlyLiked, | ||
| } | ||
| isLikeActionPending.value = false | ||
| } | ||
| } | ||
| </script> | ||
|
|
||
| <template> | ||
| <NuxtLink :to="packageRoute(name)"> | ||
| <BaseCard class="group font-mono flex justify-between"> | ||
| {{ name }} | ||
| <div class="flex items-center gap-4 justify-between"> | ||
| <ClientOnly> | ||
| <TooltipApp | ||
| :text="likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like')" | ||
| position="bottom" | ||
| > | ||
| <button | ||
| @click.prevent="likeAction" | ||
| type="button" | ||
| :title=" | ||
| likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.like') | ||
| " | ||
| class="inline-flex items-center gap-1.5 font-mono text-sm text-fg hover:text-fg-muted transition-colors duration-200" | ||
| :aria-label=" | ||
| likesData?.userHasLiked ? $t('package.likes.unlike') : $t('package.likes.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>{{ | ||
| compactNumberFormatter.format(likesData?.totalLikes ?? 0, { decimals: 1 }) | ||
| }}</span> | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| </button> | ||
| </TooltipApp> | ||
|
Comment on lines
+73
to
+104
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. Avoid nesting a This creates invalid interactive nesting and can break keyboard/screen-reader behaviour. Split the clickable card link and like button into sibling interactive elements. 🧰 Tools🪛 GitHub Check: 💪 Type check[failure] 103-103: |
||
| </ClientOnly> | ||
| <p class="transition-transform duration-150 group-hover:rotate-45 pb-1">↗</p> | ||
| </div> | ||
| </BaseCard> | ||
| </NuxtLink> | ||
| </template> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| export type LikesResult = { | ||
| records: { | ||
| value: { | ||
| subjectRef: string | ||
| } | ||
| }[] | ||
| } | ||
|
|
||
| export function useProfileLikes(handle: MaybeRefOrGetter<string>) { | ||
| const cachedFetch = useCachedFetch() | ||
| const asyncData = useLazyAsyncData( | ||
| `profile:${toValue(handle)}:likes`, | ||
| async (_nuxtApp, { signal }) => { | ||
| const { data: likes, isStale } = await cachedFetch<LikesResult>( | ||
| `/api/social/profile/${toValue(handle)}/likes`, | ||
| { signal }, | ||
| ) | ||
|
|
||
| return { likes, isStale } | ||
| }, | ||
| ) | ||
|
|
||
| if (import.meta.client) { | ||
| onMounted(() => { | ||
| asyncData.refresh() | ||
| }) | ||
| } | ||
|
|
||
| return asyncData | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,198 @@ | ||||||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||||||
| import { debounce } from 'perfect-debounce' | ||||||||||||||||||||||||||||||||||||||
| import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile' | ||||||||||||||||||||||||||||||||||||||
| import { normalizeSearchParam } from '#shared/utils/url' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| type LikesResult = { | ||||||||||||||||||||||||||||||||||||||
| records: { | ||||||||||||||||||||||||||||||||||||||
| value: { | ||||||||||||||||||||||||||||||||||||||
| subjectRef: string | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }[] | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const route = useRoute('/profile/[handle]') | ||||||||||||||||||||||||||||||||||||||
| const router = useRouter() | ||||||||||||||||||||||||||||||||||||||
| const handle = computed(() => route.params.handle) | ||||||||||||||||||||||||||||||||||||||
|
Check failure on line 16 in app/pages/profile/[handle]/index.vue
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
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: cd app/pages/profile/[handle] && cat -n index.vue | head -30Repository: npmx-dev/npmx.dev Length of output: 1035 🌐 Web query:
💡 Result: Nuxt 4 “typed routes” are provided by the experimental // nuxt.config.ts
export default defineNuxtConfig({
experimental: {
typedPages: true,
},
})With const route = useRoute('route-name')This enables typed usage across API reference for Sources: Experimental typedPages docs [1], 🏁 Script executed: rg -n "export.*normalizeSearchParam" --type ts --type jsRepository: npmx-dev/npmx.dev Length of output: 181 🏁 Script executed: cat -n $(fd normalizeSearchParam -t f | head -5) 2>/dev/null | head -40Repository: npmx-dev/npmx.dev Length of output: 43 🏁 Script executed: sed -n '30,45p' shared/utils/url.tsRepository: npmx-dev/npmx.dev Length of output: 272 Fix typed-route usage and safely normalise the Lines 14-17 currently fail type-check and can yield non-string params at runtime. The argument to Proposed fix-const route = useRoute('/profile/[handle]')
-const router = useRouter()
-const handle = computed(() => route.params.handle)
+const route = useRoute()
+const handle = computed(() => normalizeSearchParam(route.params.handle) ?? '')🧰 Tools🪛 GitHub Check: 💪 Type check[failure] 16-16: [failure] 15-15: [failure] 14-14: |
||||||||||||||||||||||||||||||||||||||
| const { data: profile }: { data?: NPMXProfile } = useFetch( | ||||||||||||||||||||||||||||||||||||||
| () => `/api/social/profile/${handle.value}`, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| default: () => ({ profile: { displayName: handle.value } }), | ||||||||||||||||||||||||||||||||||||||
| server: false, | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { user } = useAtproto() | ||||||||||||||||||||||||||||||||||||||
| const isEditing = ref(false) | ||||||||||||||||||||||||||||||||||||||
| const displayNameInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const descriptionInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const websiteInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const isUpdateProfileActionPending = ref(false) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| watchEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| if (isEditing) { | ||||||||||||||||||||||||||||||||||||||
| if (profile) { | ||||||||||||||||||||||||||||||||||||||
| displayNameInput.value = profile.value.displayName | ||||||||||||||||||||||||||||||||||||||
| descriptionInput.value = profile.value.description | ||||||||||||||||||||||||||||||||||||||
| websiteInput.value = profile.value.website | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+33
to
+41
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. Guard should check
🐛 Proposed fix watchEffect(() => {
if (isEditing.value) {
- if (profile) {
+ if (profile.value) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| async function updateProfile() { | ||||||||||||||||||||||||||||||||||||||
| if (!user.value.handle || !displayNameInput.value) { | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = true | ||||||||||||||||||||||||||||||||||||||
| const currentProfile = profile.value | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // optimistic update | ||||||||||||||||||||||||||||||||||||||
| profile.value = { | ||||||||||||||||||||||||||||||||||||||
| displayName: displayNameInput.value, | ||||||||||||||||||||||||||||||||||||||
| description: descriptionInput.value, | ||||||||||||||||||||||||||||||||||||||
| website: websiteInput.value, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const result = await updateProfileUtil(handle, { | ||||||||||||||||||||||||||||||||||||||
| displayName: displayNameInput.value, | ||||||||||||||||||||||||||||||||||||||
| description: descriptionInput.value, | ||||||||||||||||||||||||||||||||||||||
| website: websiteInput.value, | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (!result.success) { | ||||||||||||||||||||||||||||||||||||||
| profile.value = currentProfile | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = false | ||||||||||||||||||||||||||||||||||||||
| isEditing.value = false | ||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||
| profile.value = currentProfile | ||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { data: likesData, status } = await useProfileLikes(handle) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useSeoMeta({ | ||||||||||||||||||||||||||||||||||||||
| title: () => `${handle.value} - npmx`, | ||||||||||||||||||||||||||||||||||||||
| description: () => `npmx profile by ${handle.value}`, | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||
| defineOgImageComponent('Default', { | ||||||||||||||||||||||||||||||||||||||
| title: () => `~${username.value}`, | ||||||||||||||||||||||||||||||||||||||
| description: () => (results.value ? `${results.value.total} packages` : 'npm user profile'), | ||||||||||||||||||||||||||||||||||||||
| primaryColor: '#60a5fa', | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| **/ | ||||||||||||||||||||||||||||||||||||||
| </script> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <template> | ||||||||||||||||||||||||||||||||||||||
| <main class="container flex-1 flex flex-col py-8 sm:py-12 w-full"> | ||||||||||||||||||||||||||||||||||||||
| <!-- Header --> | ||||||||||||||||||||||||||||||||||||||
| <header class="mb-8 pb-8 border-b border-border"> | ||||||||||||||||||||||||||||||||||||||
| <!-- Editing Profile --> | ||||||||||||||||||||||||||||||||||||||
| <div v-if="isEditing" class="flex flex-col flex-wrap gap-4"> | ||||||||||||||||||||||||||||||||||||||
| <label for="displayName" class="text-sm flex flex-col gap-2"> | ||||||||||||||||||||||||||||||||||||||
| Display Name | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| required | ||||||||||||||||||||||||||||||||||||||
| name="displayName" | ||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||
| class="w-full min-w-25 bg-bg-subtle border border-border rounded-md ps-3 pe-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)" | ||||||||||||||||||||||||||||||||||||||
| v-model="displayNameInput" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||
| <label for="description" class="text-sm flex flex-col gap-2"> | ||||||||||||||||||||||||||||||||||||||
| Description | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| name="description" | ||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||
| placeholder="No description" | ||||||||||||||||||||||||||||||||||||||
| v-model="descriptionInput" | ||||||||||||||||||||||||||||||||||||||
| class="w-full min-w-25 bg-bg-subtle border border-border rounded-md ps-3 pe-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||
| <div class="flex gap-4 items-center font-mono text-sm"> | ||||||||||||||||||||||||||||||||||||||
| <h2>@{{ handle }}</h2> | ||||||||||||||||||||||||||||||||||||||
| <div class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"> | ||||||||||||||||||||||||||||||||||||||
| <span class="i-carbon:link w-4 h-4" aria-hidden="true" /> | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| name="website" | ||||||||||||||||||||||||||||||||||||||
| type="url" | ||||||||||||||||||||||||||||||||||||||
| v-model="websiteInput" | ||||||||||||||||||||||||||||||||||||||
| class="w-full min-w-25 bg-bg-subtle border border-border rounded-md ps-3 pe-3 py-1.5 font-mono text-sm text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| @click="isEditing = false" | ||||||||||||||||||||||||||||||||||||||
| class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| Cancel | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| @click.prevent="updateProfile" | ||||||||||||||||||||||||||||||||||||||
| :disabled="isUpdateProfileActionPending" | ||||||||||||||||||||||||||||||||||||||
| class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| Save | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <!-- Display Profile --> | ||||||||||||||||||||||||||||||||||||||
| <div v-else class="flex flex-col flex-wrap gap-4"> | ||||||||||||||||||||||||||||||||||||||
| <h1 v-if="profile.displayName" class="font-mono text-2xl sm:text-3xl font-medium"> | ||||||||||||||||||||||||||||||||||||||
| {{ profile.displayName }} | ||||||||||||||||||||||||||||||||||||||
| </h1> | ||||||||||||||||||||||||||||||||||||||
| <p v-if="profile.description">{{ profile.description }}</p> | ||||||||||||||||||||||||||||||||||||||
| <div class="flex gap-4 items-center font-mono text-sm"> | ||||||||||||||||||||||||||||||||||||||
| <h2>@{{ handle }}</h2> | ||||||||||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||||||||||
| v-if="profile.website" | ||||||||||||||||||||||||||||||||||||||
| :href="profile.website" | ||||||||||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||||||||||
| class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <span class="i-carbon:link w-4 h-4" aria-hidden="true" /> | ||||||||||||||||||||||||||||||||||||||
| {{ profile.website }} | ||||||||||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| v-if="user?.handle === handle" | ||||||||||||||||||||||||||||||||||||||
| @click="isEditing = true" | ||||||||||||||||||||||||||||||||||||||
| class="hidden sm:inline-flex link-subtle font-mono text-sm items-center gap-2 px-2 py-1.5 hover:bg-bg-subtle focus-visible:outline-accent/70 rounded" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| Edit | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </header> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <section class="flex flex-col gap-8"> | ||||||||||||||||||||||||||||||||||||||
| <h2 | ||||||||||||||||||||||||||||||||||||||
| class="font-mono text-2xl sm:text-3xl font-medium min-w-0 break-words" | ||||||||||||||||||||||||||||||||||||||
| :title="Likes" | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
| dir="ltr" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| Likes <span v-if="likesData">({{ likesData.likes.records.length ?? 0 }})</span> | ||||||||||||||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||||||||||||||
| <div v-if="status === 'pending'"> | ||||||||||||||||||||||||||||||||||||||
| <p>Loading...</p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div v-else-if="status === 'error'"> | ||||||||||||||||||||||||||||||||||||||
| <p>Error</p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div v-else-if="likesData.likes.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4"> | ||||||||||||||||||||||||||||||||||||||
| <PackageLikeCard | ||||||||||||||||||||||||||||||||||||||
| v-if="likesData.likes.records" | ||||||||||||||||||||||||||||||||||||||
| v-for="like in likesData.likes.records" | ||||||||||||||||||||||||||||||||||||||
| :packageUrl="like.value.subjectRef" | ||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||
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.
🧩 Analysis chain
🏁 Script executed:
# First, let's read the entire file to understand the component structure wc -l app/components/Header/AuthModal.client.vueRepository: npmx-dev/npmx.dev
Length of output: 107
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 2297
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 128
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 1126
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 2672
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 128
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 88
🏁 Script executed:
Repository: npmx-dev/npmx.dev
Length of output: 4126
Remove undefined variable and fix invalid interactive element nesting.
The
prefetchvariable is not defined in the component (lines 1–63 of the script section), causing a type-check failure. Additionally, nesting a<button>inside<NuxtLink>violates HTML semantics for interactive elements. Apply styles and event handlers directly toNuxtLinkinstead:Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 GitHub Check: 💪 Type check
[failure] 81-81:
Property 'prefetch' does not exist on type '{ user: Simplify<SerializeObject<{ did: string; handle: string; pds: string; avatar?: string | undefined; relogin?: boolean | undefined; }> | null> | undefined; ... 721 more ...; $npmApi: (url: string, options?: NitroFetchOptions<...> | undefined, ttl?: number | undefined) => Promise<...>; }'.