Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
172 changes: 172 additions & 0 deletions app/pages/pds.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<script setup lang="ts">
import type { AtprotoProfile } from '#shared/types/atproto'

const router = useRouter()
const canGoBack = useCanGoBack()

useSeoMeta({
title: () => `${$t('pds.title')} - npmx`,
ogTitle: () => `${$t('pds.title')} - npmx`,
twitterTitle: () => `${$t('pds.title')} - npmx`,
description: () => $t('pds.meta_description'),
ogDescription: () => $t('pds.meta_description'),
twitterDescription: () => $t('pds.meta_description'),
})

defineOgImageComponent('Default', {
primaryColor: '#60a5fa',
title: 'npmx.social',
description: 'The official **PDS** for the npmx community.',
})

const brokenImages = ref(new Set<string>())

const handleImageError = (handle: string) => {
brokenImages.value.add(handle)
}

const { data: pdsUsers, status: pdsStatus } = useLazyFetch<AtprotoProfile[]>(
'/api/atproto/pds-users',
{
default: () => [],
},
)

const usersWithAvatars = computed(() => {
return pdsUsers.value.filter(user => user.avatar && !brokenImages.value.has(user.handle))
})
</script>

<template>
<main class="container flex-1 py-12 sm:py-16 overflow-x-hidden">
<article class="max-w-2xl mx-auto">
<header class="mb-12">
<div class="flex items-baseline justify-between gap-4 mb-4">
<h1 class="font-mono text-3xl sm:text-4xl font-medium">
{{ $t('pds.title') }}
</h1>
<button
type="button"
class="cursor-pointer inline-flex items-center gap-2 p-1.5 -mx-1.5 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
@click="router.back()"
v-if="canGoBack"
>
<span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="hidden sm:inline">{{ $t('nav.back') }}</span>
</button>
</div>
<p class="text-fg-muted text-lg">
{{ $t('pds.meta_description') }}
</p>
</header>

<section class="max-w-none space-y-12">
<div>
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('pds.join.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-4">
{{ $t('pds.join.description') }}
</p>
<div class="mt-6">
<LinkBase
to="https://pdsmoover.com/moover/npmx.social"
class="gap-2 px-4 py-2 text-sm font-medium rounded-md border border-border hover:border-border-hover bg-bg-muted hover:bg-bg"
no-underline
>
<span class="i-lucide:arrow-right-left w-4 h-4 text-fg-muted" aria-hidden="true" />
{{ $t('pds.join.migrate') }}
</LinkBase>
</div>
</div>

<div>
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('pds.server.title') }}
</h2>
<ul class="space-y-3 text-fg-muted list-none p-0">
<li class="flex items-start gap-3">
<span
class="i-lucide:map-pin shrink-0 mt-1 w-4 h-4 text-fg-subtle"
aria-hidden="true"
/>
<span>
<strong class="text-fg">{{ $t('pds.server.location_label') }}</strong>
{{ $t('pds.server.location_value') }}
</span>
</li>
<li class="flex items-start gap-3">
<span
class="i-lucide:server shrink-0 mt-1 w-4 h-4 text-fg-subtle"
aria-hidden="true"
/>
<span>
<strong class="text-fg">{{ $t('pds.server.infrastructure_label') }}</strong>
{{ $t('pds.server.infrastructure_value') }}
</span>
</li>
<li class="flex items-start gap-3">
<span
class="i-lucide:shield-check shrink-0 mt-1 w-4 h-4 text-fg-subtle"
aria-hidden="true"
/>
<span>
<strong class="text-fg">{{ $t('pds.server.privacy_label') }}</strong>
{{ $t('pds.server.privacy_value') }}
</span>
</li>
</ul>
</div>
<div aria-labelledby="community-heading">
<h2 id="community-heading" class="text-lg text-fg uppercase tracking-wider mb-4">
{{ $t('pds.community.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-6">
Copy link
Contributor

Choose a reason for hiding this comment

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

Be nice to add a count like "Home to 192 accounts" or something and the count is only active accounts

{{ $t('pds.community.description') }}
</p>

<div v-if="pdsStatus === 'pending'" class="text-fg-subtle text-sm" role="status">
{{ $t('pds.community.loading') }}
</div>
<div v-else-if="pdsStatus === 'error'" class="text-fg-subtle text-sm" role="alert">
{{ $t('pds.community.error') }}
</div>
<div v-else-if="!usersWithAvatars.length" class="text-fg-subtle text-sm">
{{ $t('pds.community.empty') }}
</div>
<ul
v-else
class="grid grid-cols-[repeat(auto-fill,48px)] justify-center gap-2 list-none p-0"
>
<li v-for="user in usersWithAvatars" :key="user.handle" class="block group relative">
<a
:href="`https://bsky.app/profile/${user.handle}`"
target="_blank"
rel="noopener noreferrer"
:aria-label="$t('pds.community.view_profile', { handle: user.handle })"
class="block rounded-lg"
>
<img
:src="user.avatar"
:alt="`${user.handle}'s avatar`"
@error="handleImageError(user.handle)"
width="48"
height="48"
class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out group-hover:scale-125 will-change-transform"
loading="lazy"
/>
<span
class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100"
dir="ltr"
role="tooltip"
>
Comment on lines 158 to 162
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Tooltip semantics are incomplete for keyboard users.

role="tooltip" is declared, but the element is only shown on hover and is not wired to a trigger via aria-describedby. Either fully wire ARIA tooltip behaviour or treat this as visual-only text and support keyboard visibility.

Suggested patch
-                  class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100 z-10"
+                  class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100 group-focus-within:opacity-100 group-focus-within:scale-100 z-10"
                   dir="ltr"
-                  role="tooltip"
+                  aria-hidden="true"
📝 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
<span
class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100 z-10"
dir="ltr"
role="tooltip"
>
<span
class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100 group-focus-within:opacity-100 group-focus-within:scale-100 z-10"
dir="ltr"
aria-hidden="true"
>

@{{ user.handle }}
</span>
</a>
</li>
</ul>
</div>
</section>
</article>
</main>
</template>
26 changes: 26 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,32 @@
"close_files_panel": "Close files panel",
"filter_files_label": "Filter files by change type"
},
"pds": {
"title": "npmx.social",
"meta_description": "The official AT Protocol Personal Data Server (PDS) for the npmx community.",
"join": {
"title": "Join the Community",
"description": "Whether you are creating your first Bluesky account or migrating an existing one, you belong here. You can migrate your current account without losing your handle, your posts, or your followers.",
"migrate": "Migrate with PDS MOOver"
},
"server": {
"title": "Server Details",
"location_label": "Location:",
"location_value": "Nuremberg, Germany",
"infrastructure_label": "Infrastructure:",
"infrastructure_value": "Hosted on Hetzner",
"privacy_label": "Privacy:",
"privacy_value": "Subject to strict EU Data Protection laws"
},
"community": {
"title": "Who is here",
"description": "They are already calling npmx.social home.",
"loading": "Loading PDS community...",
"error": "Failed to load PDS community.",
"empty": "No community members to display.",
"view_profile": "View {handle}'s profile"
}
},
"privacy_policy": {
"title": "privacy policy",
"last_updated": "Last updated: {date}",
Expand Down
78 changes: 78 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3499,6 +3499,84 @@
},
"additionalProperties": false
},
"pds": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"meta_description": {
"type": "string"
},
"join": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"migrate": {
"type": "string"
}
},
"additionalProperties": false
},
"server": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"location_label": {
"type": "string"
},
"location_value": {
"type": "string"
},
"infrastructure_label": {
"type": "string"
},
"infrastructure_value": {
"type": "string"
},
"privacy_label": {
"type": "string"
},
"privacy_value": {
"type": "string"
}
},
"additionalProperties": false
},
"community": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"loading": {
"type": "string"
},
"error": {
"type": "string"
},
"empty": {
"type": "string"
},
"view_profile": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"privacy_policy": {
"type": "object",
"properties": {
Expand Down
26 changes: 26 additions & 0 deletions lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,32 @@
"close_files_panel": "Close files panel",
"filter_files_label": "Filter files by change type"
},
"pds": {
"title": "npmx.social",
"meta_description": "The official AT Protocol Personal Data Server (PDS) for the npmx community.",
"join": {
"title": "Join the Community",
"description": "Whether you are creating your first Bluesky account or migrating an existing one, you belong here. You can migrate your current account without losing your handle, your posts, or your followers.",
"migrate": "Migrate with PDS MOOver"
},
"server": {
"title": "Server Details",
"location_label": "Location:",
"location_value": "Nuremberg, Germany",
"infrastructure_label": "Infrastructure:",
"infrastructure_value": "Hosted on Hetzner",
"privacy_label": "Privacy:",
"privacy_value": "Subject to strict EU Data Protection laws"
},
"community": {
"title": "Who is here",
"description": "They are already calling npmx.social home.",
"loading": "Loading PDS community...",
"error": "Failed to load PDS community.",
"empty": "No community members to display.",
"view_profile": "View {handle}'s profile"
}
},
"privacy_policy": {
"title": "privacy policy",
"last_updated": "Last updated: {date}",
Expand Down
26 changes: 26 additions & 0 deletions lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,32 @@
"close_files_panel": "Close files panel",
"filter_files_label": "Filter files by change type"
},
"pds": {
"title": "npmx.social",
"meta_description": "The official AT Protocol Personal Data Server (PDS) for the npmx community.",
"join": {
"title": "Join the Community",
"description": "Whether you are creating your first Bluesky account or migrating an existing one, you belong here. You can migrate your current account without losing your handle, your posts, or your followers.",
"migrate": "Migrate with PDS MOOver"
},
"server": {
"title": "Server Details",
"location_label": "Location:",
"location_value": "Nuremberg, Germany",
"infrastructure_label": "Infrastructure:",
"infrastructure_value": "Hosted on Hetzner",
"privacy_label": "Privacy:",
"privacy_value": "Subject to strict EU Data Protection laws"
},
"community": {
"title": "Who is here",
"description": "They are already calling npmx.social home.",
"loading": "Loading PDS community...",
"error": "Failed to load PDS community.",
"empty": "No community members to display.",
"view_profile": "View {handle}'s profile"
}
},
"privacy_policy": {
"title": "privacy policy",
"last_updated": "Last updated: {date}",
Expand Down
1 change: 1 addition & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default defineNuxtConfig({
'/search': { isr: false, cache: false }, // never cache
'/settings': { prerender: true },
'/recharging': { prerender: true },
'/pds': { prerender: true },
// proxy for insights
'/blog/**': { prerender: true },
'/_v/script.js': {
Expand Down
Loading
Loading