Skip to content

Commit b94aff5

Browse files
Kai-rospauliecodesdanielroeautofix-ci[bot]
authored
feat: add PDS landing page (#1750)
Co-authored-by: paula <pau9ale@gmail.com> Co-authored-by: Daniel Roe <daniel@roe.dev> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 8859e87 commit b94aff5

File tree

10 files changed

+528
-2
lines changed

10 files changed

+528
-2
lines changed

app/pages/pds.vue

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<script setup lang="ts">
2+
import type { AtprotoProfile } from '#shared/types/atproto'
3+
4+
const router = useRouter()
5+
const canGoBack = useCanGoBack()
6+
7+
useSeoMeta({
8+
title: () => `${$t('pds.title')} - npmx`,
9+
ogTitle: () => `${$t('pds.title')} - npmx`,
10+
twitterTitle: () => `${$t('pds.title')} - npmx`,
11+
description: () => $t('pds.meta_description'),
12+
ogDescription: () => $t('pds.meta_description'),
13+
twitterDescription: () => $t('pds.meta_description'),
14+
})
15+
16+
defineOgImageComponent('Default', {
17+
primaryColor: '#60a5fa',
18+
title: 'npmx.social',
19+
description: 'The official **PDS** for the npmx community.',
20+
})
21+
22+
const brokenImages = ref(new Set<string>())
23+
24+
const handleImageError = (handle: string) => {
25+
brokenImages.value.add(handle)
26+
}
27+
28+
const { data: pdsUsers, status: pdsStatus } = useLazyFetch<AtprotoProfile[]>(
29+
'/api/atproto/pds-users',
30+
{
31+
default: () => [],
32+
},
33+
)
34+
35+
const usersWithAvatars = computed(() => {
36+
return pdsUsers.value.filter(user => user.avatar && !brokenImages.value.has(user.handle))
37+
})
38+
39+
const usersWithoutAvatars = computed(() => {
40+
return pdsUsers.value.filter(user => !user.avatar || brokenImages.value.has(user.handle))
41+
})
42+
43+
const totalAccounts = computed(() => pdsUsers.value.length)
44+
</script>
45+
46+
<template>
47+
<main class="container flex-1 py-12 sm:py-16">
48+
<article class="max-w-2xl mx-auto">
49+
<header class="mb-12">
50+
<div class="flex items-baseline justify-between gap-4 mb-4">
51+
<h1 class="font-mono text-3xl sm:text-4xl font-medium">
52+
{{ $t('pds.title') }}
53+
</h1>
54+
<button
55+
type="button"
56+
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"
57+
@click="router.back()"
58+
v-if="canGoBack"
59+
>
60+
<span class="i-lucide:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
61+
<span class="hidden sm:inline">{{ $t('nav.back') }}</span>
62+
</button>
63+
</div>
64+
<p class="text-fg-muted text-lg">
65+
{{ $t('pds.meta_description') }}
66+
</p>
67+
</header>
68+
69+
<section class="max-w-none space-y-12">
70+
<div>
71+
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
72+
{{ $t('pds.join.title') }}
73+
</h2>
74+
<p class="text-fg-muted leading-relaxed mb-4">
75+
{{ $t('pds.join.description') }}
76+
</p>
77+
<div class="mt-6">
78+
<LinkBase
79+
to="https://pdsmoover.com/moover/npmx.social"
80+
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"
81+
no-underline
82+
>
83+
<span class="i-lucide:arrow-right-left w-4 h-4 text-fg-muted" aria-hidden="true" />
84+
{{ $t('pds.join.migrate') }}
85+
</LinkBase>
86+
</div>
87+
</div>
88+
89+
<div>
90+
<h2 class="text-lg text-fg uppercase tracking-wider mb-4">
91+
{{ $t('pds.server.title') }}
92+
</h2>
93+
<ul class="space-y-3 text-fg-muted list-none p-0">
94+
<li class="flex items-start gap-3">
95+
<span
96+
class="i-lucide:map-pin shrink-0 mt-1 w-4 h-4 text-fg-subtle"
97+
aria-hidden="true"
98+
/>
99+
<span>
100+
<strong class="text-fg">{{ $t('pds.server.location_label') }}</strong>
101+
{{ $t('pds.server.location_value') }}
102+
</span>
103+
</li>
104+
<li class="flex items-start gap-3">
105+
<span
106+
class="i-lucide:server shrink-0 mt-1 w-4 h-4 text-fg-subtle"
107+
aria-hidden="true"
108+
/>
109+
<span>
110+
<strong class="text-fg">{{ $t('pds.server.infrastructure_label') }}</strong>
111+
{{ $t('pds.server.infrastructure_value') }}
112+
</span>
113+
</li>
114+
<li class="flex items-start gap-3">
115+
<span
116+
class="i-lucide:shield-check shrink-0 mt-1 w-4 h-4 text-fg-subtle"
117+
aria-hidden="true"
118+
/>
119+
<span>
120+
<strong class="text-fg">{{ $t('pds.server.privacy_label') }}</strong>
121+
{{ $t('pds.server.privacy_value') }}
122+
</span>
123+
</li>
124+
</ul>
125+
<div class="mt-6">
126+
<LinkBase
127+
to="https://docs.npmx.dev/integrations/atmosphere"
128+
class="gap-2 text-sm text-fg-muted hover:text-fg"
129+
>
130+
<span class="i-lucide:book-open w-4 h-4" aria-hidden="true" />
131+
{{ $t('pds.server.learn_more') }}
132+
</LinkBase>
133+
</div>
134+
</div>
135+
<div aria-labelledby="community-heading">
136+
<h2 id="community-heading" class="text-lg text-fg uppercase tracking-wider mb-4">
137+
{{ $t('pds.community.title') }}
138+
</h2>
139+
<p class="text-fg-muted leading-relaxed mb-6">
140+
{{ $t('pds.community.description', { count: totalAccounts }) }}
141+
</p>
142+
143+
<div v-if="pdsStatus === 'pending'" class="text-fg-subtle text-sm" role="status">
144+
{{ $t('pds.community.loading') }}
145+
</div>
146+
<div v-else-if="pdsStatus === 'error'" class="text-fg-subtle text-sm" role="alert">
147+
{{ $t('pds.community.error') }}
148+
</div>
149+
<div v-else-if="!pdsUsers.length" class="text-fg-subtle text-sm">
150+
{{ $t('pds.community.empty') }}
151+
</div>
152+
<div v-else>
153+
<ul class="grid grid-cols-[repeat(auto-fill,48px)] justify-center gap-2 list-none p-0">
154+
<li
155+
v-for="user in usersWithAvatars"
156+
:key="user.handle"
157+
class="block group relative hover:z-10"
158+
>
159+
<a
160+
:href="`https://bsky.app/profile/${user.handle}`"
161+
target="_blank"
162+
rel="noopener noreferrer"
163+
:aria-label="$t('pds.community.view_profile', { handle: user.handle })"
164+
class="block rounded-lg"
165+
>
166+
<img
167+
:src="user.avatar"
168+
:alt="`${user.handle}'s avatar`"
169+
@error="handleImageError(user.handle)"
170+
width="48"
171+
height="48"
172+
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"
173+
loading="lazy"
174+
/>
175+
<span
176+
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"
177+
dir="ltr"
178+
role="tooltip"
179+
>
180+
@{{ user.handle }}
181+
</span>
182+
</a>
183+
</li>
184+
</ul>
185+
<p v-if="usersWithoutAvatars.length" class="text-center mt-4 text-fg-muted text-sm">
186+
{{ $t('pds.community.new_accounts', { count: usersWithoutAvatars.length }) }}
187+
</p>
188+
</div>
189+
</div>
190+
</section>
191+
</article>
192+
</main>
193+
</template>

docs/content/4.integrations/1.atmosphere.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Your PDS is your home on the network &ndash; it hosts your account, stores your
4141
4242
Want to migrate to npmx's self-hosted PDS? The easiest way is [PDS MOOver](https://pdsmoover.com/moover/npmx.social), built by [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev). If you need help, hop into our [Discord](https://chat.npmx.dev) and check out the `#pds` channel.
4343

44-
See the [atproto PDS docs](https://atproto.com/guides/the-at-stack#pds) for more.
44+
See who's already on npmx.social on the [PDS community page](https://npmx.dev/pds). For the protocol details, see the [atproto PDS docs](https://atproto.com/guides/the-at-stack#pds).
4545

4646
### OAuth
4747

i18n/locales/en.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,34 @@
11631163
"close_files_panel": "Close files panel",
11641164
"filter_files_label": "Filter files by change type"
11651165
},
1166+
"pds": {
1167+
"title": "npmx.social",
1168+
"meta_description": "The official AT Protocol Personal Data Server (PDS) for the npmx community.",
1169+
"join": {
1170+
"title": "Join the Community",
1171+
"description": "Whether you are creating your first account on the atmosphere or migrating an existing one, you belong here. You can migrate your current account without losing your handle, your posts, or your followers.",
1172+
"migrate": "Migrate with PDS MOOver"
1173+
},
1174+
"server": {
1175+
"title": "Server Details",
1176+
"location_label": "Location:",
1177+
"location_value": "Nuremberg, Germany",
1178+
"infrastructure_label": "Infrastructure:",
1179+
"infrastructure_value": "Hosted on Hetzner",
1180+
"privacy_label": "Privacy:",
1181+
"privacy_value": "Subject to strict EU Data Protection laws",
1182+
"learn_more": "Learn how npmx uses the Atmosphere"
1183+
},
1184+
"community": {
1185+
"title": "Who is here",
1186+
"description": "Some of the {count} accounts that are already calling npmx.social home:",
1187+
"loading": "Loading PDS community...",
1188+
"error": "Failed to load PDS community.",
1189+
"empty": "No community members to display.",
1190+
"view_profile": "View {handle}'s profile",
1191+
"new_accounts": "...plus {count} more that are new to the atmosphere"
1192+
}
1193+
},
11661194
"privacy_policy": {
11671195
"title": "privacy policy",
11681196
"last_updated": "Last updated: {date}",

i18n/schema.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3493,6 +3493,90 @@
34933493
},
34943494
"additionalProperties": false
34953495
},
3496+
"pds": {
3497+
"type": "object",
3498+
"properties": {
3499+
"title": {
3500+
"type": "string"
3501+
},
3502+
"meta_description": {
3503+
"type": "string"
3504+
},
3505+
"join": {
3506+
"type": "object",
3507+
"properties": {
3508+
"title": {
3509+
"type": "string"
3510+
},
3511+
"description": {
3512+
"type": "string"
3513+
},
3514+
"migrate": {
3515+
"type": "string"
3516+
}
3517+
},
3518+
"additionalProperties": false
3519+
},
3520+
"server": {
3521+
"type": "object",
3522+
"properties": {
3523+
"title": {
3524+
"type": "string"
3525+
},
3526+
"location_label": {
3527+
"type": "string"
3528+
},
3529+
"location_value": {
3530+
"type": "string"
3531+
},
3532+
"infrastructure_label": {
3533+
"type": "string"
3534+
},
3535+
"infrastructure_value": {
3536+
"type": "string"
3537+
},
3538+
"privacy_label": {
3539+
"type": "string"
3540+
},
3541+
"privacy_value": {
3542+
"type": "string"
3543+
},
3544+
"learn_more": {
3545+
"type": "string"
3546+
}
3547+
},
3548+
"additionalProperties": false
3549+
},
3550+
"community": {
3551+
"type": "object",
3552+
"properties": {
3553+
"title": {
3554+
"type": "string"
3555+
},
3556+
"description": {
3557+
"type": "string"
3558+
},
3559+
"loading": {
3560+
"type": "string"
3561+
},
3562+
"error": {
3563+
"type": "string"
3564+
},
3565+
"empty": {
3566+
"type": "string"
3567+
},
3568+
"view_profile": {
3569+
"type": "string"
3570+
},
3571+
"new_accounts": {
3572+
"type": "string"
3573+
}
3574+
},
3575+
"additionalProperties": false
3576+
}
3577+
},
3578+
"additionalProperties": false
3579+
},
34963580
"privacy_policy": {
34973581
"type": "object",
34983582
"properties": {

nuxt.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default defineNuxtConfig({
5555
},
5656
},
5757

58-
devtools: { enabled: true },
58+
devtools: { enabled: !true },
5959

6060
devServer: {
6161
// Used with atproto oauth
@@ -161,6 +161,7 @@ export default defineNuxtConfig({
161161
'/search': { isr: false, cache: false }, // never cache
162162
'/settings': { prerender: true },
163163
'/recharging': { prerender: true },
164+
'/pds': { isr: 86400 }, // revalidate daily
164165
// proxy for insights
165166
'/blog/**': { prerender: true },
166167
'/_v/script.js': {

0 commit comments

Comments
 (0)