forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1<script setup lang="ts">
2import { useAtproto } from '~/composables/atproto/useAtproto'
3import { authRedirect } from '~/utils/atproto/helpers'
4import { isAtIdentifierString } from '@atproto/lex'
5
6const handleInput = shallowRef('')
7const errorMessage = shallowRef('')
8const route = useRoute()
9const { user, logout } = useAtproto()
10
11// https://atproto.com supports 4 locales as of 2026-02-07
12const { locale } = useI18n()
13const currentLang = locale.value.split('-')[0] ?? 'en'
14const localeSubPath = ['ko', 'pt', 'ja'].includes(currentLang) ? currentLang : ''
15const atprotoLink = `https://atproto.com/${localeSubPath}`
16
17async function handleBlueskySignIn() {
18 await authRedirect('https://bsky.social', { redirectTo: route.fullPath, locale: locale.value })
19}
20
21async function handleCreateAccount() {
22 await authRedirect('https://npmx.social', {
23 create: true,
24 redirectTo: route.fullPath,
25 locale: locale.value,
26 })
27}
28
29async function handleLogin() {
30 if (handleInput.value) {
31 // URLS to PDSs are valid for initiating oauth flows
32 if (handleInput.value.startsWith('https://') || isAtIdentifierString(handleInput.value)) {
33 await authRedirect(handleInput.value, {
34 redirectTo: route.fullPath,
35 locale: locale.value,
36 })
37 } else {
38 errorMessage.value = $t('auth.modal.default_input_error')
39 }
40 }
41}
42
43watch(handleInput, newHandleInput => {
44 errorMessage.value = ''
45 if (!newHandleInput) return
46
47 const normalized = newHandleInput.trim().toLowerCase().replace(/@/g, '')
48
49 if (normalized !== newHandleInput) {
50 handleInput.value = normalized
51 }
52})
53</script>
54
55<template>
56 <!-- Modal -->
57 <Modal :modalTitle="$t('auth.modal.title')" class="max-w-lg" id="auth-modal">
58 <div v-if="user?.handle" class="space-y-4">
59 <div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
60 <span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
61 <div>
62 <p class="font-mono text-xs text-fg-muted">
63 {{ $t('auth.modal.connected_as', { handle: user.handle }) }}
64 </p>
65 </div>
66 </div>
67 <ButtonBase class="w-full" @click="logout">
68 {{ $t('auth.modal.disconnect') }}
69 </ButtonBase>
70 </div>
71
72 <!-- Disconnected state -->
73 <form v-else class="space-y-4" @submit.prevent="handleLogin">
74 <p class="text-sm text-fg-muted">{{ $t('auth.modal.connect_prompt') }}</p>
75
76 <div class="space-y-3">
77 <div>
78 <label
79 for="handle-input"
80 class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
81 >
82 {{ $t('auth.modal.handle_label') }}
83 </label>
84 <InputBase
85 id="handle-input"
86 v-model="handleInput"
87 type="text"
88 name="handle"
89 :placeholder="$t('auth.modal.handle_placeholder')"
90 no-correct
91 class="w-full"
92 size="medium"
93 />
94 <p v-if="errorMessage" class="text-red-500 text-xs mt-1" role="alert">
95 {{ errorMessage }}
96 </p>
97 </div>
98
99 <details class="text-sm">
100 <summary
101 class="text-fg-subtle hover:text-fg-muted transition-colors duration-200 focus-visible:(outline-2 outline-accent/70)"
102 >
103 {{ $t('auth.modal.what_is_atmosphere') }}
104 </summary>
105 <div class="mt-3">
106 <i18n-t keypath="auth.modal.atmosphere_explanation" tag="p" scope="global">
107 <template #npmx>
108 <span class="font-bold">npmx.dev</span>
109 </template>
110 <template #atproto>
111 <LinkBase :to="atprotoLink"> AT Protocol </LinkBase>
112 </template>
113 <template #bluesky>
114 <LinkBase to="https://bsky.app"> Bluesky </LinkBase>
115 </template>
116 <template #tangled>
117 <LinkBase to="https://tangled.org"> Tangled </LinkBase>
118 </template>
119 </i18n-t>
120 </div>
121 </details>
122 </div>
123
124 <ButtonBase type="submit" variant="primary" :disabled="!handleInput.trim()" class="w-full">
125 {{ $t('auth.modal.connect') }}
126 </ButtonBase>
127 <ButtonBase type="button" class="w-full" @click="handleCreateAccount">
128 {{ $t('auth.modal.create_account') }}
129 </ButtonBase>
130 <hr class="color-border" />
131 <ButtonBase
132 type="button"
133 class="w-full"
134 @click="handleBlueskySignIn"
135 classicon="i-simple-icons:bluesky"
136 >
137 {{ $t('auth.modal.connect_bluesky') }}
138 </ButtonBase>
139 </form>
140 </Modal>
141</template>