tangled
alpha
login
or
join now
vt3e.cat
/
bbell
7
fork
atom
wip bsky client for the web & android
bbell.vt3e.cat
7
fork
atom
overview
issues
pulls
pipelines
feat: login page v3
vt3e.cat
1 month ago
cbf5059a
d5fa73ce
verified
This commit was signed with the committer's
known signature
.
vt3e.cat
SSH Key Fingerprint:
SHA256:MaVgF6bXxDdD131G4rXizPh+sttp3IVsdPrj48HV0X0=
+527
-96
12 changed files
expand all
collapse all
unified
split
src
components
Dialogs
CreateAccount.vue
Navigation
TabStack.vue
PdsSelector.vue
UI
BaseButton.vue
BaseCheckbox.vue
BaseModal.vue
ListItemContent.vue
stores
auth.ts
utils
pds.ts
views
Auth
LoginPage.vue
OAuthCallback.vue
Profile
FollowsView.vue
+221
src/components/Dialogs/CreateAccount.vue
···
1
1
+
<script lang="ts" setup>
2
2
+
import { computed, onMounted, ref } from 'vue'
3
3
+
4
4
+
import { useAuthStore } from '@/stores/auth'
5
5
+
import { getProviderList, type HydratedProvider } from '@/utils/pds'
6
6
+
7
7
+
import Button from '../UI/BaseButton.vue'
8
8
+
import Toggle from '../UI/BaseCheckbox.vue'
9
9
+
import Modal from '../UI/BaseModal.vue'
10
10
+
import PdsList from '../PdsSelector.vue'
11
11
+
12
12
+
const auth = useAuthStore()
13
13
+
const isOpen = defineModel<boolean>('open', { required: true })
14
14
+
function closeModal() {
15
15
+
isOpen.value = false
16
16
+
}
17
17
+
18
18
+
const providers = ref<HydratedProvider[]>([])
19
19
+
const selectedProvider = ref<HydratedProvider>()
20
20
+
21
21
+
const selectedHasHandlePolicy = computed(() => !!selectedProvider.value?.handlePolicy)
22
22
+
const hasAgreedToHandlePolicy = ref(false)
23
23
+
24
24
+
const canProgress = computed(() => {
25
25
+
const selected = selectedProvider.value
26
26
+
if (!selected) return false
27
27
+
if (selectedHasHandlePolicy.value && hasAgreedToHandlePolicy.value) return true
28
28
+
return !selected.handlePolicy
29
29
+
})
30
30
+
31
31
+
const pdsError = ref('')
32
32
+
const pdsInput = ref('')
33
33
+
34
34
+
async function submitPds() {
35
35
+
const val =
36
36
+
pdsInput.value.trim() === '' ? selectedProvider.value?.url.toString() : pdsInput.value.trim()
37
37
+
if (!val) {
38
38
+
pdsError.value = 'Enter a PDS URL or handle'
39
39
+
return
40
40
+
}
41
41
+
await auth.login(val)
42
42
+
}
43
43
+
44
44
+
onMounted(async () => {
45
45
+
providers.value = await getProviderList()
46
46
+
if (providers.value.length) {
47
47
+
selectedProvider.value = providers.value[0]
48
48
+
} else {
49
49
+
pdsError.value = 'No providers available'
50
50
+
}
51
51
+
})
52
52
+
53
53
+
function selectProvider(provider: HydratedProvider) {
54
54
+
hasAgreedToHandlePolicy.value = false
55
55
+
selectedProvider.value = provider
56
56
+
}
57
57
+
</script>
58
58
+
59
59
+
<template>
60
60
+
<Modal title="Create an account" v-model:open="isOpen" width="600px">
61
61
+
<div class="modal-body">
62
62
+
<p>
63
63
+
Choose a provider to host your account. You can migrate to a different provider later if
64
64
+
needed.
65
65
+
</p>
66
66
+
67
67
+
<PdsList @select="selectProvider" />
68
68
+
69
69
+
<div v-if="selectedProvider" class="provider-profile">
70
70
+
<div class="profile-main">
71
71
+
<div class="pds-info">
72
72
+
<p class="pds-name">{{ selectedProvider.name }}</p>
73
73
+
<p class="pds-location">{{ selectedProvider.location }}</p>
74
74
+
</div>
75
75
+
</div>
76
76
+
77
77
+
<div
78
78
+
class="policy-transition-wrapper"
79
79
+
:class="{ open: selectedHasHandlePolicy }"
80
80
+
:inert="!selectedHasHandlePolicy"
81
81
+
>
82
82
+
<div class="policy-inner">
83
83
+
<div class="policy-section">
84
84
+
<div class="policy-notice">
85
85
+
<p>
86
86
+
This provider has restrictions on who can use what handles.
87
87
+
<a :href="selectedProvider.handlePolicy?.toString() || '#'" class="policy-link">
88
88
+
View terms here
89
89
+
</a>
90
90
+
</p>
91
91
+
</div>
92
92
+
<label class="policy-checkbox" :class="{ checked: hasAgreedToHandlePolicy }">
93
93
+
<Toggle v-model="hasAgreedToHandlePolicy" id="policy-agree" />
94
94
+
<span>I've read and accept these terms</span>
95
95
+
</label>
96
96
+
</div>
97
97
+
</div>
98
98
+
</div>
99
99
+
</div>
100
100
+
</div>
101
101
+
102
102
+
<template #footer>
103
103
+
<Button variant="ghost" type="button" @click="closeModal">Cancel</Button>
104
104
+
<Button
105
105
+
variant="primary"
106
106
+
:loading="auth.isLoading"
107
107
+
@click="submitPds"
108
108
+
type="button"
109
109
+
:disabled="!canProgress"
110
110
+
>
111
111
+
Create account
112
112
+
</Button>
113
113
+
</template>
114
114
+
</Modal>
115
115
+
</template>
116
116
+
117
117
+
<style lang="scss" scoped>
118
118
+
.modal-body {
119
119
+
display: flex;
120
120
+
flex-direction: column;
121
121
+
gap: 1.5rem;
122
122
+
}
123
123
+
124
124
+
.provider-list {
125
125
+
margin-top: -0.75rem;
126
126
+
}
127
127
+
128
128
+
.provider-profile {
129
129
+
background: hsla(var(--surface0) / 0.4);
130
130
+
margin-left: -0.75rem;
131
131
+
width: calc(100% + 1.5rem);
132
132
+
border-radius: 1rem;
133
133
+
padding: 1.25rem;
134
134
+
display: flex;
135
135
+
flex-direction: column;
136
136
+
gap: 0;
137
137
+
138
138
+
.profile-main {
139
139
+
display: flex;
140
140
+
align-items: center;
141
141
+
gap: 1rem;
142
142
+
143
143
+
.pds-info {
144
144
+
display: flex;
145
145
+
flex-direction: row;
146
146
+
align-items: baseline;
147
147
+
gap: 0.5rem;
148
148
+
149
149
+
.pds-name {
150
150
+
font-weight: 600;
151
151
+
font-size: 1rem;
152
152
+
color: hsla(var(--text));
153
153
+
display: block;
154
154
+
}
155
155
+
156
156
+
.pds-location {
157
157
+
font-size: 0.8rem;
158
158
+
color: hsla(var(--text) / 0.6);
159
159
+
display: block;
160
160
+
}
161
161
+
}
162
162
+
}
163
163
+
164
164
+
.policy-transition-wrapper {
165
165
+
display: grid;
166
166
+
grid-template-rows: 0fr;
167
167
+
opacity: 0;
168
168
+
169
169
+
&.open {
170
170
+
grid-template-rows: 1fr;
171
171
+
opacity: 1;
172
172
+
}
173
173
+
}
174
174
+
175
175
+
.policy-inner {
176
176
+
overflow: hidden;
177
177
+
min-height: 0;
178
178
+
}
179
179
+
180
180
+
.policy-section {
181
181
+
border-top: 1px dashed hsla(var(--accent) / 0.5);
182
182
+
margin-top: 0.5rem;
183
183
+
padding-top: 0.5rem;
184
184
+
185
185
+
display: flex;
186
186
+
flex-direction: column;
187
187
+
gap: 0.25rem;
188
188
+
189
189
+
.policy-notice {
190
190
+
font-size: 0.85rem;
191
191
+
color: hsl(var(--subtext1));
192
192
+
193
193
+
.policy-link {
194
194
+
color: hsla(var(--accent));
195
195
+
text-decoration: none;
196
196
+
font-weight: 700;
197
197
+
198
198
+
&:hover {
199
199
+
text-decoration: underline;
200
200
+
}
201
201
+
}
202
202
+
}
203
203
+
204
204
+
.policy-checkbox {
205
205
+
display: flex;
206
206
+
align-items: center;
207
207
+
gap: 0.75rem;
208
208
+
209
209
+
span {
210
210
+
font-size: 0.85rem;
211
211
+
user-select: none;
212
212
+
}
213
213
+
}
214
214
+
}
215
215
+
}
216
216
+
217
217
+
.fade-enter-from,
218
218
+
.fade-leave-to {
219
219
+
opacity: 0;
220
220
+
}
221
221
+
</style>
-1
src/components/Navigation/TabStack.vue
···
20
20
})
21
21
: comp
22
22
23
23
-
console.log(acc)
24
23
return acc
25
24
},
26
25
{} as Record<string, Component>,
+152
src/components/PdsSelector.vue
···
1
1
+
<script lang="ts" setup>
2
2
+
import { onMounted, ref } from 'vue'
3
3
+
import { IconCheckRounded } from '@iconify-prerendered/vue-material-symbols'
4
4
+
5
5
+
import { getProviderList, type HydratedProvider } from '@/utils/pds'
6
6
+
7
7
+
const loadedProviders = ref(false)
8
8
+
const providers = ref<HydratedProvider[]>([])
9
9
+
10
10
+
const selectedProvider = ref<HydratedProvider>()
11
11
+
12
12
+
const emit = defineEmits<{
13
13
+
(e: 'select', provider: HydratedProvider): void
14
14
+
}>()
15
15
+
16
16
+
onMounted(async () => {
17
17
+
providers.value = await getProviderList()
18
18
+
19
19
+
loadedProviders.value = true
20
20
+
if (!providers.value.length) {
21
21
+
return
22
22
+
}
23
23
+
24
24
+
selectedProvider.value = providers.value[0]
25
25
+
if (!selectedProvider.value) return
26
26
+
27
27
+
emit('select', selectedProvider.value)
28
28
+
})
29
29
+
30
30
+
function selectProvider(provider: HydratedProvider) {
31
31
+
selectedProvider.value = provider
32
32
+
emit('select', provider)
33
33
+
}
34
34
+
</script>
35
35
+
36
36
+
<template>
37
37
+
<div class="provider-list">
38
38
+
<template v-if="loadedProviders">
39
39
+
<div
40
40
+
v-for="(provider, i) in providers"
41
41
+
:key="provider.url?.href || provider.name + i"
42
42
+
class="provider"
43
43
+
:class="{ selected: provider === selectedProvider, pinned: provider.pin }"
44
44
+
role="option"
45
45
+
:aria-selected="provider === selectedProvider"
46
46
+
tabindex="0"
47
47
+
@click="selectProvider(provider)"
48
48
+
@keydown.enter.prevent="selectProvider(provider)"
49
49
+
@keydown.space.prevent="selectProvider(provider)"
50
50
+
>
51
51
+
<div class="meta">
52
52
+
<h3>{{ provider.name }}</h3>
53
53
+
<p>{{ provider.subtitle || provider.url.hostname }}</p>
54
54
+
</div>
55
55
+
<div class="hint">
56
56
+
<IconCheckRounded />
57
57
+
</div>
58
58
+
</div>
59
59
+
</template>
60
60
+
<template v-else>
61
61
+
<div class="provider skeleton" v-for="i in 4" :key="i">
62
62
+
<div class="meta">
63
63
+
<h3>...</h3>
64
64
+
<p>...</p>
65
65
+
</div>
66
66
+
<div class="hint">
67
67
+
<IconCheckRounded />
68
68
+
</div>
69
69
+
</div>
70
70
+
</template>
71
71
+
</div>
72
72
+
</template>
73
73
+
74
74
+
<style lang="scss" scoped>
75
75
+
.provider-list {
76
76
+
display: flex;
77
77
+
flex-direction: column;
78
78
+
gap: 0.25rem;
79
79
+
80
80
+
--offset: 0.75rem;
81
81
+
margin-left: calc(var(--offset) * -1);
82
82
+
width: calc(100% + var(--offset) * 2);
83
83
+
}
84
84
+
85
85
+
.provider {
86
86
+
display: flex;
87
87
+
--colour: var(--surface0);
88
88
+
gap: 0.5rem;
89
89
+
align-items: center;
90
90
+
padding: 1rem;
91
91
+
background-color: hsla(var(--colour) / 0.5);
92
92
+
/* border-radius: 0.25rem; */
93
93
+
border-radius: 0.5rem;
94
94
+
cursor: pointer;
95
95
+
96
96
+
&.skeleton {
97
97
+
.skeleton-text {
98
98
+
height: 1rem;
99
99
+
width: 100%;
100
100
+
border-radius: 0.25rem;
101
101
+
background-color: hsla(var(--surface0) / 0.5);
102
102
+
}
103
103
+
}
104
104
+
105
105
+
&:hover,
106
106
+
&:focus-visible {
107
107
+
background-color: hsla(var(--colour) / 0.7);
108
108
+
}
109
109
+
&:active {
110
110
+
background-color: hsla(var(--colour) / 0.4);
111
111
+
}
112
112
+
113
113
+
.hint {
114
114
+
opacity: 0;
115
115
+
height: 100%;
116
116
+
aspect-ratio: 1/1;
117
117
+
width: 1.75rem;
118
118
+
119
119
+
svg {
120
120
+
width: 100%;
121
121
+
height: 100%;
122
122
+
color: hsla(var(--accent) / 1);
123
123
+
}
124
124
+
}
125
125
+
126
126
+
.meta {
127
127
+
flex: 1;
128
128
+
129
129
+
h3 {
130
130
+
font-weight: 600;
131
131
+
font-size: 0.95rem;
132
132
+
color: hsl(var(--text));
133
133
+
white-space: nowrap;
134
134
+
}
135
135
+
136
136
+
p {
137
137
+
font-size: 0.8rem;
138
138
+
color: hsl(var(--subtext0));
139
139
+
line-height: 1.4;
140
140
+
}
141
141
+
}
142
142
+
143
143
+
&.selected {
144
144
+
border-radius: 2.5rem;
145
145
+
--colour: var(--surface1);
146
146
+
147
147
+
.hint {
148
148
+
opacity: 1;
149
149
+
}
150
150
+
}
151
151
+
}
152
152
+
</style>
+7
-7
src/components/UI/BaseButton.vue
···
126
126
}
127
127
}
128
128
129
129
-
.variant-primary {
129
129
+
.th-btn.variant-primary {
130
130
--bg-colour: var(--accent);
131
131
--text-colour: var(--base);
132
132
--border-colour: var(--accent);
133
133
}
134
134
135
135
-
.variant-secondary {
135
135
+
.th-btn.variant-secondary {
136
136
--bg-colour: var(--surface0);
137
137
--text-colour: var(--text);
138
138
--border-colour: var(--surface2);
139
139
}
140
140
141
141
-
.variant-subtle {
141
141
+
.th-btn.variant-subtle {
142
142
--bg-colour: var(--accent);
143
143
--text-colour: var(--accent);
144
144
--border-colour: var(--accent);
···
156
156
}
157
157
}
158
158
159
159
-
.variant-subtle-alt {
159
159
+
.th-btn.variant-subtle-alt {
160
160
border-color: transparent;
161
161
-
background-color: hsla(var(--subtext0) / 0.04);
161
161
+
background-color: hsla(var(--overlay0) / 0.1);
162
162
color: hsl(var(--subtext0));
163
163
164
164
&:hover:not(:disabled) {
165
165
-
background-color: hsla(var(--subtext0) / 0.06);
165
165
+
background-color: hsla(var(--overlay0) / 0.15);
166
166
border-color: transparent;
167
167
}
168
168
&:active:not(:disabled) {
169
169
-
background-color: hsla(var(--subtext0) / 0.02);
169
169
+
background-color: hsla(var(--overlay0) / 0.075);
170
170
border-color: transparent;
171
171
}
172
172
}
+7
-3
src/components/UI/BaseCheckbox.vue
···
45
45
width: 1.5rem;
46
46
height: 1.5rem;
47
47
border-radius: 0.5rem;
48
48
-
border: 2px solid hsl(var(--subtext0));
49
49
-
background-color: hsl(var(--surface0));
48
48
+
border: 2px solid hsla(var(--subtext0) / 0.75);
49
49
+
background-color: hsla(var(--surface0) / 0.5);
50
50
display: flex;
51
51
align-items: center;
52
52
justify-content: center;
53
53
-
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
54
53
color: hsl(var(--base));
54
54
+
55
55
+
&:hover {
56
56
+
background-color: hsla(var(--surface0) / 1);
57
57
+
border-color: hsla(var(--subtext0) / 0.75);
58
58
+
}
55
59
}
56
60
57
61
.checkbox-box.is-checked {
+2
src/components/UI/BaseModal.vue
···
6
6
defineProps<{
7
7
title?: string
8
8
width?: string
9
9
+
id?: string
9
10
}>()
10
11
11
12
const emit = defineEmits<{
···
160
161
161
162
<Transition :name="isMobile ? 'slide-up' : 'zoom'">
162
163
<div
164
164
+
:id="id"
163
165
v-if="isOpen"
164
166
ref="modalContainerRef"
165
167
class="modal-container"
+1
src/components/UI/ListItemContent.vue
···
1
1
<script lang="ts" setup>
2
2
import type { PageNames, RouteParams } from '@/router'
3
3
+
import { IconChevronRightRounded } from '@iconify-prerendered/vue-material-symbols'
3
4
4
5
defineProps<{
5
6
title?: string
+9
-4
src/stores/auth.ts
···
111
111
authUrl = await createAuthorizationUrl({
112
112
target: { type: 'pds', serviceUrl: input },
113
113
scope,
114
114
-
// @ts-expect-error: craete will be available soon:tm:
115
115
-
prompt: createAccount ? 'create' : undefined,
114
114
+
// prompt: createAccount ? 'create' : undefined,
116
115
})
117
116
} else {
118
117
try {
···
148
147
}
149
148
}
150
149
151
151
-
async function attemptFallbackPdsLogin(input: string, scope: string): Promise<URL> {
150
150
+
async function attemptFallbackPdsLogin(
151
151
+
input: string,
152
152
+
scope: string,
153
153
+
isRetry = false,
154
154
+
): Promise<URL> {
152
155
const serviceUrl = `https://${input}`
153
156
154
157
const handler = simpleFetchHandler({ service: serviceUrl })
155
158
const client = new Client({ handler })
156
159
const { ok } = await client.get('com.atproto.server.describeServer')
157
160
158
158
-
if (!ok) throw new Error('Failed to resolve handle and server is not a valid PDS')
161
161
+
if (!ok) {
162
162
+
throw new Error('Failed to resolve handle and server is not a valid PDS')
163
163
+
}
159
164
160
165
return createAuthorizationUrl({
161
166
target: { type: 'pds', serviceUrl },
+87
src/utils/pds.ts
···
1
1
+
import type { ComAtprotoServerDescribeServer } from '@atcute/atproto'
2
2
+
import { Client, simpleFetchHandler } from '@atcute/client'
3
3
+
4
4
+
export type BaseProvider = {
5
5
+
name: string
6
6
+
url: URL
7
7
+
location: string
8
8
+
subtitle?: string
9
9
+
/** whether this provider should be pinned when displayed */
10
10
+
pin?: boolean
11
11
+
/** if the PDS has a policy on who can use what handles, blacksky, for example. */
12
12
+
handlePolicy?: URL
13
13
+
}
14
14
+
15
15
+
export type HydratedProvider = BaseProvider & {
16
16
+
server: ComAtprotoServerDescribeServer.$output
17
17
+
}
18
18
+
19
19
+
export const PROVIDERS: BaseProvider[] = [
20
20
+
{
21
21
+
name: 'selfhosted.social',
22
22
+
url: new URL('https://selfhosted.social/'),
23
23
+
subtitle: 'A popular community-run PDS',
24
24
+
location: 'US',
25
25
+
pin: true,
26
26
+
},
27
27
+
{
28
28
+
name: 'Bluesky',
29
29
+
url: new URL('https://bsky.social/'),
30
30
+
location: 'US',
31
31
+
},
32
32
+
{
33
33
+
name: 'Blacksky',
34
34
+
url: new URL('https://blacksky.app/'),
35
35
+
subtitle: 'A PDS for the black community and allies.',
36
36
+
location: 'US',
37
37
+
handlePolicy: new URL(
38
38
+
'https://docs.blacksky.community/migrating-to-blacksky-pds-complete-guide#who-can-use-blacksky-services',
39
39
+
),
40
40
+
},
41
41
+
{
42
42
+
name: 'Tophhie Social',
43
43
+
url: new URL('https://pds.tophhie.cloud'),
44
44
+
location: 'GB',
45
45
+
},
46
46
+
]
47
47
+
48
48
+
export async function getHydratedProviders(): Promise<HydratedProvider[]> {
49
49
+
return (
50
50
+
await Promise.all(
51
51
+
PROVIDERS.map(async (provider) => {
52
52
+
const xrpc = new Client({
53
53
+
handler: simpleFetchHandler({ service: provider.url }),
54
54
+
})
55
55
+
56
56
+
const { data, ok } = await xrpc.get('com.atproto.server.describeServer', {})
57
57
+
if (!ok) return undefined
58
58
+
59
59
+
return {
60
60
+
...provider,
61
61
+
server: data,
62
62
+
}
63
63
+
}),
64
64
+
)
65
65
+
).filter(Boolean) as HydratedProvider[]
66
66
+
}
67
67
+
68
68
+
function shuffle<T>(array: T[]): T[] {
69
69
+
const shuffled = [...array]
70
70
+
for (let i = shuffled.length - 1; i > 0; i--) {
71
71
+
const j = Math.floor(Math.random() * (i + 1))
72
72
+
const temp = shuffled[i]!
73
73
+
shuffled[i] = shuffled[j]!
74
74
+
shuffled[j] = temp
75
75
+
}
76
76
+
77
77
+
return shuffled
78
78
+
}
79
79
+
80
80
+
export async function getProviderList(shuffled = true): Promise<HydratedProvider[]> {
81
81
+
const hydratedProviders = await getHydratedProviders()
82
82
+
const pinnedProviders = hydratedProviders.filter((provider) => provider.pin)
83
83
+
const unpinnedProviders = hydratedProviders.filter((provider) => !provider.pin)
84
84
+
85
85
+
if (shuffled) return [...pinnedProviders, ...shuffle(unpinnedProviders)]
86
86
+
return [...pinnedProviders, ...unpinnedProviders]
87
87
+
}
+39
-77
src/views/Auth/LoginPage.vue
···
1
1
<script setup lang="ts">
2
2
-
import { ref, computed } from 'vue'
3
3
-
import { IconOpenInNewRounded } from '@iconify-prerendered/vue-material-symbols'
2
2
+
import { ref, computed, onMounted } from 'vue'
4
3
5
4
import { useAuthStore } from '@/stores/auth'
6
5
import PageLayout from '@/components/Navigation/PageLayout.vue'
7
6
import Button from '@/components/UI/BaseButton.vue'
8
7
import Modal from '@/components/UI/BaseModal.vue'
9
8
import TextInput from '@/components/UI/TextInput.vue'
9
9
+
import { getProviderList, type HydratedProvider } from '@/utils/pds'
10
10
11
11
-
const auth = useAuthStore()
11
11
+
import CreateAccount from '@/components/Dialogs/CreateAccount.vue'
12
12
13
13
-
type Provider = {
14
14
-
name: string
15
15
-
url: string
16
16
-
subtitle?: string
17
17
-
location: string
18
18
-
}
19
19
-
const providerList: Provider[] = [
20
20
-
{ name: 'Bluesky PBC', subtitle: 'bsky.social', url: 'https://bsky.social/', location: 'US' },
21
21
-
{
22
22
-
name: 'selfhosted.social',
23
23
-
url: 'https://selfhosted.social/',
24
24
-
subtitle:
25
25
-
'A collection of hackers, designers, developers, ATProto enthusiasts, scrobblers, tinkerers, friends, and curious minds. A shared space where everyone is welcome to be themselves.',
26
26
-
location: 'US',
27
27
-
},
28
28
-
{
29
29
-
name: 'Tophhie Social',
30
30
-
subtitle: 'pds.tophhie.cloud',
31
31
-
url: 'https://pds.tophhie.cloud',
32
32
-
location: 'GB',
33
33
-
},
34
34
-
// {
35
35
-
// name: 'Zio',
36
36
-
// subtitle: 'zio.blue',
37
37
-
// url: 'https://zio.blue',
38
38
-
// location: 'Finland',
39
39
-
// },
40
40
-
{
41
41
-
name: 'peedee.es',
42
42
-
url: 'https://peedee.es',
43
43
-
location: 'Germany',
44
44
-
},
45
45
-
{
46
46
-
name: 'Blacksky',
47
47
-
subtitle: 'A PDS for the black community.',
48
48
-
url: 'https://blacksky.app/',
49
49
-
location: 'US',
50
50
-
},
51
51
-
{ name: 'Spark', subtitle: 'pds.sprk.so', url: 'https://pds.sprk.so', location: 'US' },
52
52
-
]
13
13
+
const auth = useAuthStore()
53
14
54
15
const isLoading = computed(() => auth.isLoading)
16
16
+
const isLoadingProviders = ref(true)
55
17
const showPdsModal = ref(false)
56
18
const pdsInput = ref('')
57
19
const pdsError = ref('')
58
20
21
21
+
const showCreateAccountModal = ref(false)
22
22
+
23
23
+
const providers = ref<HydratedProvider[]>([])
24
24
+
59
25
async function pdsSubmit(pds: string, providerList = false) {
60
26
if (!pds || isLoading.value) return
61
27
await auth.login(pds, providerList)
···
67
33
showPdsModal.value = true
68
34
}
69
35
36
36
+
function openCreateModal() {
37
37
+
showCreateAccountModal.value = true
38
38
+
}
39
39
+
70
40
async function submitPds() {
71
41
const val = pdsInput.value.trim()
72
42
if (!val) {
73
43
pdsError.value = 'Enter a PDS URL or handle (e.g. https://pds.example or awesome.cat)'
74
44
return
75
45
}
76
76
-
showPdsModal.value = false
77
46
await auth.login(val)
78
47
}
48
48
+
49
49
+
onMounted(async () => {
50
50
+
providers.value = await getProviderList()
51
51
+
isLoadingProviders.value = false
52
52
+
})
79
53
</script>
80
54
81
55
<template>
···
91
65
variant="primary"
92
66
size="lg"
93
67
:block="true"
94
94
-
:loading="isLoading"
68
68
+
:loading="isLoading && !showPdsModal"
95
69
@click="pdsSubmit('https://bsky.social')"
96
70
>
97
71
<span class="btn-inner">
···
100
74
</Button>
101
75
102
76
<div class="secondary-row">
103
103
-
<Button variant="text" @click="openPdsModal" :disabled="isLoading">
104
104
-
Sign in with AT Protocol
77
77
+
<Button variant="subtle-alt" @click="openPdsModal" :disabled="isLoading && !showPdsModal">
78
78
+
Enter your handle
79
79
+
</Button>
80
80
+
<Button
81
81
+
variant="subtle-alt"
82
82
+
@click="openCreateModal"
83
83
+
:disabled="isLoading && !showPdsModal"
84
84
+
>
85
85
+
Create an account
105
86
</Button>
106
87
</div>
107
88
</div>
108
89
109
109
-
<div class="provider-list">
110
110
-
<button
111
111
-
v-for="provider in providerList"
112
112
-
:key="provider.name"
113
113
-
class="list-item"
114
114
-
:disabled="isLoading"
115
115
-
@click="() => pdsSubmit(provider.url, true)"
116
116
-
>
117
117
-
<div class="content-grid">
118
118
-
<div class="top-row">
119
119
-
<span class="name">{{ provider.name }}</span>
120
120
-
<span class="location" v-if="provider.location">{{ provider.location }}</span>
121
121
-
</div>
122
122
-
123
123
-
<p class="subtitle">
124
124
-
{{ provider.subtitle || provider.url.replace('https://', '') }}
125
125
-
</p>
126
126
-
</div>
127
127
-
128
128
-
<div class="action">
129
129
-
<IconOpenInNewRounded />
130
130
-
</div>
131
131
-
</button>
132
132
-
</div>
133
133
-
134
90
<p v-if="auth.error" class="error-text">{{ auth.error }}</p>
135
91
</div>
136
92
137
137
-
<Modal v-model:open="showPdsModal" title="Sign in with PDS">
93
93
+
<Modal v-model:open="showPdsModal" title="Enter your handle" @close="showPdsModal = false">
138
94
<div class="modal-body">
139
95
<label for="pds-input" class="sr-only">PDS URL or handle</label>
140
96
···
154
110
155
111
<template #footer>
156
112
<Button variant="ghost" type="button" @click="showPdsModal = false">Cancel</Button>
157
157
-
<Button variant="primary" :loading="auth.isLoading" @click="submitPds" type="button"
158
158
-
>Sign in</Button
159
159
-
>
113
113
+
<Button variant="primary" :loading="auth.isLoading" @click="submitPds" type="button">
114
114
+
Sign in
115
115
+
</Button>
160
116
</template>
161
117
</Modal>
118
118
+
119
119
+
<CreateAccount v-model:open="showCreateAccountModal" @close="showCreateAccountModal = false" />
162
120
</PageLayout>
163
121
</template>
164
122
···
201
159
}
202
160
.secondary-row {
203
161
display: flex;
204
204
-
justify-content: center;
162
162
+
gap: 0.5rem;
163
163
+
width: 100%;
164
164
+
button {
165
165
+
flex: 1;
166
166
+
}
205
167
}
206
168
207
169
.provider-list {
+2
-1
src/views/Auth/OAuthCallback.vue
···
37
37
38
38
onMounted(async () => {
39
39
try {
40
40
-
if (!auth.session) return
41
40
const style = loadingBar.value?.style
42
41
43
42
loadingMessage.value = 'verifying... '
···
50
49
style?.setProperty('--scale', (2 / 3).toString())
51
50
52
51
const rpc = auth.getRpc()
52
52
+
if (!auth.session) return
53
53
+
53
54
const { data, ok } = await rpc.get('app.bsky.actor.getProfile', {
54
55
params: { actor: auth.session?.info.sub },
55
56
})
-3
src/views/Profile/FollowsView.vue
···
37
37
38
38
const fetcher = async (c?: string | null) => {
39
39
const rpc = auth.getRpc()
40
40
-
console.log(mode.value)
41
40
const endpoint =
42
41
mode.value === 'followers' ? 'app.bsky.graph.getFollowers' : 'app.bsky.graph.getFollows'
43
42
···
46
45
headers: { 'atproto-proxy': BSKY_APPVIEW },
47
46
})
48
47
if (!ok) throw new Error((data && data.error) || 'Failed')
49
49
-
50
50
-
console.log(props)
51
48
52
49
return {
53
50
// @ts-expect-error: its ok typescript. ..