-
-
Notifications
You must be signed in to change notification settings - Fork 283
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
base: main
Are you sure you want to change the base?
feat: profile page #1113
Changes from all 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,110 @@ | ||
| <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) { | ||
| return /https:\/\/npmx.dev\/package\/(?<pkg>.*)/.exec(ref)?.groups?.pkg ?? ref | ||
| } | ||
|
|
||
| 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="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) }}</span> | ||
| </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,13 @@ | ||
| export type LikesResult = { | ||
| records: { | ||
| value: { | ||
| subjectRef: string | ||
| } | ||
| }[] | ||
| } | ||
|
|
||
| export function useProfileLikes(handle: MaybeRefOrGetter<string>) { | ||
| const asyncData = useLazyFetch<LikesResult>(() => `/api/social/profile/${toValue(handle)}/likes`) | ||
|
|
||
| return asyncData | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,215 @@ | ||||||||||||||||||||||||||||||||||||||
| <script setup lang="ts"> | ||||||||||||||||||||||||||||||||||||||
| import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile' | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const route = useRoute('profile-handle') | ||||||||||||||||||||||||||||||||||||||
| const handle = computed(() => route.params.handle) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { data: profile, error: profileError } = await useFetch<NPMXProfile>( | ||||||||||||||||||||||||||||||||||||||
| () => `/api/social/profile/${handle.value}`, | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| default: () => ({ | ||||||||||||||||||||||||||||||||||||||
| displayName: handle.value, | ||||||||||||||||||||||||||||||||||||||
| description: '', | ||||||||||||||||||||||||||||||||||||||
| website: '', | ||||||||||||||||||||||||||||||||||||||
| recordExists: false, | ||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| if (!profile.value || profileError.value?.statusCode === 404) { | ||||||||||||||||||||||||||||||||||||||
| throw createError({ | ||||||||||||||||||||||||||||||||||||||
| statusCode: 404, | ||||||||||||||||||||||||||||||||||||||
| statusMessage: $t('profile.not_found'), | ||||||||||||||||||||||||||||||||||||||
| message: $t('profile.not_found_message', { handle: handle.value }), | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { user } = useAtproto() | ||||||||||||||||||||||||||||||||||||||
| const isEditing = ref(false) | ||||||||||||||||||||||||||||||||||||||
| const displayNameInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const descriptionInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const websiteInput = ref() | ||||||||||||||||||||||||||||||||||||||
| const isUpdateProfileActionPending = ref(false) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| watchEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| if (isEditing.value) { | ||||||||||||||||||||||||||||||||||||||
| 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) { | ||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = true | ||||||||||||||||||||||||||||||||||||||
| const currentProfile = profile.value | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // optimistic update | ||||||||||||||||||||||||||||||||||||||
| profile.value = { | ||||||||||||||||||||||||||||||||||||||
| displayName: displayNameInput.value, | ||||||||||||||||||||||||||||||||||||||
| description: descriptionInput.value || undefined, | ||||||||||||||||||||||||||||||||||||||
| website: websiteInput.value || undefined, | ||||||||||||||||||||||||||||||||||||||
| recordExists: true, | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const result = await updateProfileUtil(handle.value, { | ||||||||||||||||||||||||||||||||||||||
| displayName: displayNameInput.value, | ||||||||||||||||||||||||||||||||||||||
| description: descriptionInput.value || undefined, | ||||||||||||||||||||||||||||||||||||||
| website: websiteInput.value || undefined, | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (result.success) { | ||||||||||||||||||||||||||||||||||||||
| isEditing.value = false | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| profile.value = currentProfile | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = false | ||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||
| profile.value = currentProfile | ||||||||||||||||||||||||||||||||||||||
| isUpdateProfileActionPending.value = false | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const { data: likes, status } = useProfileLikes(handle) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const showInviteSection = computed(() => { | ||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
| profile.value.recordExists === false && | ||||||||||||||||||||||||||||||||||||||
| status.value === 'success' && | ||||||||||||||||||||||||||||||||||||||
| !likes.value?.records?.length && | ||||||||||||||||||||||||||||||||||||||
| user.value?.handle !== handle.value | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const inviteUrl = computed(() => { | ||||||||||||||||||||||||||||||||||||||
| const text = $t('profile.invite.compose_text', { handle: handle.value }) | ||||||||||||||||||||||||||||||||||||||
| return `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}` | ||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useSeoMeta({ | ||||||||||||||||||||||||||||||||||||||
| title: () => $t('profile.seo_title', { handle: handle.value }), | ||||||||||||||||||||||||||||||||||||||
| description: () => $t('profile.seo_description', { handle: 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"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.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"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.description') }} | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| name="description" | ||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||
| :placeholder="$t('profile.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> | ||||||||||||||||||||||||||||||||||||||
| <label for="website" class="text-sm flex flex-col gap-2"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.website') }} | ||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||
| name="website" | ||||||||||||||||||||||||||||||||||||||
| type="url" | ||||||||||||||||||||||||||||||||||||||
| :placeholder="$t('profile.website_placeholder')" | ||||||||||||||||||||||||||||||||||||||
| 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)" | ||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||
| <div class="flex gap-4 items-center font-mono text-sm"> | ||||||||||||||||||||||||||||||||||||||
| <h2>@{{ handle }}</h2> | ||||||||||||||||||||||||||||||||||||||
| <ButtonBase @click="isEditing = false"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('common.cancel') }} | ||||||||||||||||||||||||||||||||||||||
| </ButtonBase> | ||||||||||||||||||||||||||||||||||||||
| <ButtonBase | ||||||||||||||||||||||||||||||||||||||
| @click="updateProfile" | ||||||||||||||||||||||||||||||||||||||
| variant="primary" | ||||||||||||||||||||||||||||||||||||||
| :disabled="isUpdateProfileActionPending" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {{ $t('common.save') }} | ||||||||||||||||||||||||||||||||||||||
| </ButtonBase> | ||||||||||||||||||||||||||||||||||||||
| </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> | ||||||||||||||||||||||||||||||||||||||
| <LinkBase v-if="profile.website" :to="profile.website" classicon="i-lucide:link"> | ||||||||||||||||||||||||||||||||||||||
| {{ profile.website }} | ||||||||||||||||||||||||||||||||||||||
| </LinkBase> | ||||||||||||||||||||||||||||||||||||||
| <ButtonBase | ||||||||||||||||||||||||||||||||||||||
| @click="isEditing = true" | ||||||||||||||||||||||||||||||||||||||
| :class="user?.handle === handle ? '' : 'invisible'" | ||||||||||||||||||||||||||||||||||||||
| class="hidden sm:inline-flex" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {{ $t('common.edit') }} | ||||||||||||||||||||||||||||||||||||||
| </ButtonBase> | ||||||||||||||||||||||||||||||||||||||
| </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="$t('profile.likes')" | ||||||||||||||||||||||||||||||||||||||
| dir="ltr" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.likes') }} | ||||||||||||||||||||||||||||||||||||||
| <span v-if="likes">({{ likes.records?.length ?? 0 }})</span> | ||||||||||||||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||||||||||||||
| <div v-if="status === 'pending'" class="grid grid-cols-1 lg:grid-cols-2 gap-4"> | ||||||||||||||||||||||||||||||||||||||
| <SkeletonBlock v-for="i in 4" :key="i" class="h-16 rounded-lg" /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div v-else-if="status === 'error'"> | ||||||||||||||||||||||||||||||||||||||
| <p>{{ $t('common.error') }}</p> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div v-else-if="likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4"> | ||||||||||||||||||||||||||||||||||||||
| <PackageLikeCard v-for="like in likes.records" :packageUrl="like.value.subjectRef" /> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| <!-- Invite section: shown when user does not have npmx profile or any like lexicons --> | ||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||
| v-if="showInviteSection" | ||||||||||||||||||||||||||||||||||||||
| class="flex flex-col items-start gap-4 p-6 bg-bg-subtle border border-border rounded-lg" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <p class="text-fg-muted"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.invite.message') }} | ||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||
| <LinkBase variant="button-secondary" classicon="i-simple-icons:bluesky" :to="inviteUrl"> | ||||||||||||||||||||||||||||||||||||||
| {{ $t('profile.invite.share_button') }} | ||||||||||||||||||||||||||||||||||||||
| </LinkBase> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||
| </template> | ||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.