Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3bf1cde
add profile lexicon + scope, add profile button to AuthModal, add pro…
zeucapua Feb 6, 2026
09c99c1
Like endpoint
fatfingers23 Feb 7, 2026
ecdc8cf
Merge pull request #4 from fatfingers23/feat/profile-page
zeucapua Feb 7, 2026
9591998
init useProfileLikes composable, add likes grid to profile page
zeucapua Feb 10, 2026
c467c3e
create and implement BasicCard for likes grid
zeucapua Feb 10, 2026
dfc4d7d
rename BasicCard to LikeCard, copy paste like button to card
zeucapua Feb 12, 2026
58a3b0c
implement like update
zeucapua Feb 12, 2026
0215014
add website to profile
zeucapua Feb 12, 2026
30741b8
working update profile endpoint, util, and cache update
zeucapua Feb 26, 2026
6e5be0e
style edit buttons
zeucapua Feb 26, 2026
45d35ef
Merge branch 'main' into feat/profile-page
zeucapua Feb 26, 2026
c012a45
fix merge, change likes cols to 2
zeucapua Feb 27, 2026
b81eda7
add TODOs, use LinkBase for profile website
zeucapua Feb 27, 2026
f732b14
fix: use resolved handle from profile in OAuth callback
danielroe Feb 27, 2026
73ce1df
fix: remove duplicate and unused imports in oauth.ts
danielroe Feb 27, 2026
78db755
fix(ui): check isEditing.value instead of ref object in watchEffect
danielroe Feb 27, 2026
d72a756
fix(ui): pass handle.value instead of ComputedRef to updateProfile
danielroe Feb 27, 2026
2e1bbd7
fix(ui): handle non-matching URLs in extractPackageFromRef
danielroe Feb 27, 2026
6e41ecc
fix: use DID consistently for profile cache keys
danielroe Feb 27, 2026
7f0c987
fix: validate identifier param in likes endpoint
danielroe Feb 27, 2026
6c49480
fix(ui): remove undefined prefetch reference in AuthModal
danielroe Feb 27, 2026
fe73fea
fix(ui): remove unused imports, duplicate type, and fix title binding
danielroe Feb 27, 2026
75db4ae
fix(i18n): extract hardcoded strings to locale file
danielroe Feb 27, 2026
8dfc2d9
Merge remote-tracking branch 'origin/main' into feat/profile-page
danielroe Feb 27, 2026
5f3874c
fix(a11y): remove nested button inside NuxtLink in AuthModal
danielroe Feb 27, 2026
b5bf1f2
fix(ui): remove invalid second argument from format() call
danielroe Feb 27, 2026
a3b6cc0
fix(ui): fix useRoute argument, useFetch types, and null safety
danielroe Feb 27, 2026
1bb55b8
fix: replace unlisted @atproto/syntax import and fix typo in identity.ts
danielroe Feb 27, 2026
e2e027d
fix: add response.ok check in slingshotMiniDoc fetch
danielroe Feb 27, 2026
7bde6b7
fix: add validation constraints to ProfileEditBodySchema
danielroe Feb 27, 2026
b2ecd2c
fix: add fetch timeout and encode URI in getNpmxProfile
danielroe Feb 27, 2026
0804679
fix(a11y): add LikeCard a11y test and exclude lexicons from knip
danielroe Feb 27, 2026
2cbf1d9
fix(ui): fix likesData type errors and only close editor on success
danielroe Feb 27, 2026
c4ab87d
fix(ui): add website label/placeholder, fix empty string validation, …
danielroe Feb 27, 2026
cdb891b
fix(ui): handle 404 for non-existing profiles
danielroe Feb 27, 2026
91b24a9
fix: use linkbase/buttonbase
danielroe Feb 27, 2026
e5d1638
fix(ui): SSR likes data and add skeleton placeholders to prevent layo…
danielroe Feb 27, 2026
8f19abe
fix: ssr likes
danielroe Feb 27, 2026
0bf4443
fix(ui): reserve space for edit button to prevent layout shift
danielroe Feb 27, 2026
14556af
fix(ui): remove duplicate group class from LikeCard
danielroe Feb 27, 2026
8bf70eb
Merge remote-tracking branch 'origin/main' into feat/profile-page
danielroe Feb 27, 2026
981b70b
moved some fetches to use the xrpc client
fatfingers23 Feb 27, 2026
81402ee
should be the best to tell if an identity is there or not
fatfingers23 Feb 27, 2026
a834d7a
tell the homies about npmx
fatfingers23 Feb 27, 2026
a114bf0
Update i18n/locales/en.json
zeucapua Feb 27, 2026
5676cf6
Update lunaria/files/en-US.json
zeucapua Feb 27, 2026
e5b9bbd
Merge pull request #5 from fatfingers23/feat/profile-page
zeucapua Feb 27, 2026
c89de62
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions app/components/Header/AuthModal.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { authRedirect } from '~/utils/atproto/helpers'
import { isAtIdentifierString } from '@atproto/lex'

const authModal = useModal('auth-modal')

const handleInput = shallowRef('')
const errorMessage = shallowRef('')
const route = useRoute()
Expand Down Expand Up @@ -72,9 +74,27 @@
</p>
</div>
</div>
<ButtonBase class="w-full" @click="logout">
{{ $t('auth.modal.disconnect') }}
</ButtonBase>

<div class="flex flex-col space-y-4">
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"

Check failure on line 81 in app/components/Header/AuthModal.client.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

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: <T>(url: string, options?: NitroFetchOptions<...> | undefined, ttl?: number | undefined) => Promise<...>; }'.
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's read the entire file to understand the component structure
wc -l app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 107


🏁 Script executed:

# Read the file to see the full context, focusing on the lines around 79-85
cat -n app/components/Header/AuthModal.client.vue | head -120 | tail -50

Repository: npmx-dev/npmx.dev

Length of output: 2297


🏁 Script executed:

# Search for prefetch definition in the file
rg -n "prefetch" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Search for NuxtLink and button elements to understand the structure
rg -n "NuxtLink|<button" app/components/Header/AuthModal.client.vue -B2 -A8

Repository: npmx-dev/npmx.dev

Length of output: 1126


🏁 Script executed:

# Read the script section to check if prefetch is defined
cat -n app/components/Header/AuthModal.client.vue | head -70

Repository: npmx-dev/npmx.dev

Length of output: 2672


🏁 Script executed:

# Search for all variable/prop definitions
rg -n "prefetch|defineProps|const prefetch|ref\(|computed\(" app/components/Header/AuthModal.client.vue

Repository: npmx-dev/npmx.dev

Length of output: 128


🏁 Script executed:

# Check if there are other uses of prefetch-on in the codebase to understand the pattern
rg -n "prefetch-on" app/ --type vue

Repository: npmx-dev/npmx.dev

Length of output: 88


🏁 Script executed:

# Check other NuxtLink usages to see if they apply classes and click handlers
rg -n "NuxtLink" app/components/ -A3 | head -60

Repository: npmx-dev/npmx.dev

Length of output: 4126


Remove undefined variable and fix invalid interactive element nesting.

The prefetch variable 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 to NuxtLink instead:

Proposed fix
-        <NuxtLink
-          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
-          :prefetch-on="prefetch ? 'visibility' : 'interaction'"
-        >
-          <button
-            class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
-            `@click`="authModal.close()"
-          >
-            Profile
-          </button>
-        </NuxtLink>
+        <NuxtLink
+          :to="{ name: 'profile-handle', params: { handle: user.handle } }"
+          class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
+          `@click`="authModal.close()"
+        >
+          Profile
+        </NuxtLink>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
:prefetch-on="prefetch ? 'visibility' : 'interaction'"
>
<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="authModal.close()"
<NuxtLink
:to="{ name: 'profile-handle', params: { handle: user.handle } }"
class="block w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
`@click`="authModal.close()"
>
Profile
</NuxtLink>
🧰 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<...>; }'.

>
Profile
</button>
</NuxtLink>

<button
class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50"
@click="logout"
>
{{ $t('auth.modal.disconnect') }}
</button>
</div>
</div>

<!-- Disconnected state -->
Expand Down
113 changes: 113 additions & 0 deletions app/components/Package/LikeCard.vue
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

View workflow job for this annotation

GitHub Actions / 💪 Type check

Object is possibly 'null'.

Check failure on line 11 in app/components/Package/LikeCard.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Property 'pkg' does not exist on type '{ [key: string]: string; } | undefined'.
return pkg
}
Comment thread
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 })

Check failure on line 104 in app/components/Package/LikeCard.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Expected 1 arguments, but got 2.
}}</span>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</button>
</TooltipApp>
Comment on lines +73 to +104
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid nesting a <button> inside NuxtLink.

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:
Expected 1 arguments, but got 2.

</ClientOnly>
<p class="transition-transform duration-150 group-hover:rotate-45 pb-1">↗</p>
</div>
</BaseCard>
</NuxtLink>
</template>
30 changes: 30 additions & 0 deletions app/composables/atproto/useProfileLikes.ts
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
}
198 changes: 198 additions & 0 deletions app/pages/profile/[handle]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<script setup lang="ts">
import { debounce } from 'perfect-debounce'

Check failure on line 2 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'debounce' is declared but its value is never read.
import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile'
import { normalizeSearchParam } from '#shared/utils/url'

Check failure on line 4 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'normalizeSearchParam' is declared but its value is never read.

type LikesResult = {

Check failure on line 6 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'LikesResult' is declared but never used.
records: {
value: {
subjectRef: string
}
}[]
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const route = useRoute('/profile/[handle]')

Check failure on line 14 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Argument of type '"/profile/[handle]"' is not assignable to parameter of type 'keyof RouteNamedMap | undefined'.
const router = useRouter()

Check failure on line 15 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'router' is declared but its value is never read.
const handle = computed(() => route.params.handle)

Check failure on line 16 in app/pages/profile/[handle]/index.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

Property 'handle' does not exist on type 'Record<never, never> | { path: ParamValueOneOrMore<false>; } | { org: string; } | { org?: string | undefined; name: string; } | { org?: string | undefined; packageName: string; versionRange: string; } | ... 4 more ... | { ...; }'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd app/pages/profile/[handle] && cat -n index.vue | head -30

Repository: npmx-dev/npmx.dev

Length of output: 1035


🌐 Web query:

Nuxt 4 useRoute typed routes API documentation

💡 Result:

Nuxt 4 “typed routes” are provided by the experimental typedPages feature (powered by unplugin-vue-router). Enable it in nuxt.config.ts, then you can get typed params by calling useRoute() with a route name.

// nuxt.config.ts
export default defineNuxtConfig({
  experimental: {
    typedPages: true,
  },
})

With typedPages: true, Nuxt documents that you can get typed params like:

const route = useRoute('route-name')

This enables typed usage across navigateTo, <NuxtLink>, router.push() and more. If you use pnpm without shamefully-hoist=true, Nuxt notes you may need to add unplugin-vue-router as a devDependency. [1]

API reference for useRoute (Nuxt’s wrapper around vue-router’s useRoute, with Nuxt-specific route-update timing behavior) is here: [2]

Sources: Experimental typedPages docs [1], useRoute composable docs [2].


🏁 Script executed:

rg -n "export.*normalizeSearchParam" --type ts --type js

Repository: npmx-dev/npmx.dev

Length of output: 181


🏁 Script executed:

cat -n $(fd normalizeSearchParam -t f | head -5) 2>/dev/null | head -40

Repository: npmx-dev/npmx.dev

Length of output: 43


🏁 Script executed:

sed -n '30,45p' shared/utils/url.ts

Repository: npmx-dev/npmx.dev

Length of output: 272


Fix typed-route usage and safely normalise the handle param.

Lines 14-17 currently fail type-check and can yield non-string params at runtime. The argument to useRoute() should be omitted for file-based dynamic routes, and route.params.handle (which can be string | string[] | undefined) must be safely normalised.

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:
Property 'handle' does not exist on type 'Record<never, never> | { path: ParamValueOneOrMore; } | { org: string; } | { org?: string | undefined; name: string; } | { org?: string | undefined; packageName: string; versionRange: string; } | ... 4 more ... | { ...; }'.


[failure] 15-15:
'router' is declared but its value is never read.


[failure] 14-14:
Argument of type '"/profile/[handle]"' is not assignable to parameter of type 'keyof RouteNamedMap | undefined'.

const { data: profile }: { data?: NPMXProfile } = useFetch(
() => `/api/social/profile/${handle.value}`,
{
default: () => ({ profile: { displayName: handle.value } }),
server: false,
},
Comment thread
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard should check profile.value, not profile.

profile is a Ref object which is always truthy. The condition should check profile.value to properly guard against null/undefined profile data before accessing its properties.

🐛 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
watchEffect(() => {
if (isEditing.value) {
if (profile) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})
watchEffect(() => {
if (isEditing.value) {
if (profile.value) {
displayNameInput.value = profile.value.displayName
descriptionInput.value = profile.value.description
websiteInput.value = profile.value.website
}
}
})


async function updateProfile() {
if (!user.value.handle || !displayNameInput.value) {
Comment thread
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,
})
Comment thread
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
}
Comment thread
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"
Comment thread
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"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
/>
</div>
</section>
</main>
</template>
Loading
Loading