Skip to content

Commit f08f2dc

Browse files
committed
add github connection
1 parent c9e9821 commit f08f2dc

File tree

24 files changed

+716
-31
lines changed

24 files changed

+716
-31
lines changed

app/components/Header/AccountMenu.client.vue

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,26 @@ const {
1212
} = useConnector()
1313
1414
const { user: atprotoUser } = useAtproto()
15+
const { isConnected: isGitHubConnected, user: githubUser } = useGitHub()
1516
1617
const isOpen = shallowRef(false)
1718
1819
/** Check if connected to at least one service */
19-
const hasAnyConnection = computed(() => isNpmConnected.value || !!atprotoUser.value)
20+
const hasAnyConnection = computed(
21+
() => isNpmConnected.value || !!atprotoUser.value || isGitHubConnected.value,
22+
)
2023
21-
/** Check if connected to both services */
22-
const hasBothConnections = computed(() => isNpmConnected.value && !!atprotoUser.value)
24+
/** Count of connected services for avatar stacking */
25+
const connectedCount = computed(() => {
26+
let count = 0
27+
if (isNpmConnected.value) count++
28+
if (atprotoUser.value) count++
29+
if (isGitHubConnected.value) count++
30+
return count
31+
})
32+
33+
/** Check if connected to more than one service */
34+
const hasMultipleConnections = computed(() => connectedCount.value > 1)
2335
2436
/** Only show count of active (pending/approved/running) operations */
2537
const operationCount = computed(() => activeOperations.value.length)
@@ -45,12 +57,21 @@ function openConnectorModal() {
4557
}
4658
}
4759
48-
const authModal = useModal('auth-modal')
60+
const atprotoModal = useModal('atproto-modal')
4961
50-
function openAuthModal() {
51-
if (authModal) {
62+
function openAtprotoModal() {
63+
if (atprotoModal) {
5264
isOpen.value = false
53-
authModal.open()
65+
atprotoModal.open()
66+
}
67+
}
68+
69+
const githubModal = useModal('github-modal')
70+
71+
function openGitHubModal() {
72+
if (githubModal) {
73+
isOpen.value = false
74+
githubModal.open()
5475
}
5576
}
5677
</script>
@@ -68,7 +89,7 @@ function openAuthModal() {
6889
<span
6990
v-if="hasAnyConnection"
7091
class="flex items-center"
71-
:class="hasBothConnections ? '-space-x-2' : ''"
92+
:class="hasMultipleConnections ? '-space-x-2' : ''"
7293
>
7394
<!-- npm avatar (first/back) -->
7495
<img
@@ -94,15 +115,24 @@ function openAuthModal() {
94115
width="24"
95116
height="24"
96117
class="w-6 h-6 rounded-full ring-2 ring-bg object-cover"
97-
:class="hasBothConnections ? 'relative z-10' : ''"
118+
:class="hasMultipleConnections ? 'relative z-10' : ''"
98119
/>
99120
<span
100121
v-else-if="atprotoUser"
101122
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
102-
:class="hasBothConnections ? 'relative z-10' : ''"
123+
:class="hasMultipleConnections ? 'relative z-10' : ''"
103124
>
104125
<span class="i-lucide:at-sign w-3 h-3 text-fg-muted" aria-hidden="true" />
105126
</span>
127+
128+
<!-- GitHub avatar (overlapping) -->
129+
<span
130+
v-if="isGitHubConnected"
131+
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
132+
:class="hasMultipleConnections ? 'relative z-20' : ''"
133+
>
134+
<span class="i-simple-icons:github w-3 h-3 text-fg-muted" aria-hidden="true" />
135+
</span>
106136
</span>
107137

108138
<!-- "connect" text when not connected -->
@@ -189,7 +219,7 @@ function openAuthModal() {
189219
v-if="atprotoUser"
190220
role="menuitem"
191221
class="w-full text-start gap-x-3 border-none"
192-
@click="openAuthModal"
222+
@click="openAtprotoModal"
193223
>
194224
<img
195225
v-if="atprotoUser.avatar"
@@ -212,16 +242,34 @@ function openAuthModal() {
212242
<span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere') }}</span>
213243
</span>
214244
</ButtonBase>
245+
246+
<!-- GitHub connection -->
247+
<ButtonBase
248+
v-if="isGitHubConnected && githubUser"
249+
role="menuitem"
250+
class="w-full text-start gap-x-3 border-none"
251+
@click="openGitHubModal"
252+
>
253+
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
254+
<span class="i-simple-icons:github w-4 h-4 text-fg-muted" aria-hidden="true" />
255+
</span>
256+
<span class="flex-1 min-w-0">
257+
<span class="font-mono text-sm text-fg truncate block">{{
258+
githubUser.username
259+
}}</span>
260+
<span class="text-xs text-fg-subtle">{{ $t('account_menu.github') }}</span>
261+
</span>
262+
</ButtonBase>
215263
</div>
216264

217265
<!-- Divider (only if we have connections AND options to connect) -->
218266
<div
219-
v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser)"
267+
v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser || !isGitHubConnected)"
220268
class="border-t border-border"
221269
/>
222270

223271
<!-- Connect options -->
224-
<div v-if="!isNpmConnected || !atprotoUser" class="py-1">
272+
<div v-if="!isNpmConnected || !atprotoUser || !isGitHubConnected" class="py-1">
225273
<ButtonBase
226274
v-if="!isNpmConnected"
227275
role="menuitem"
@@ -252,7 +300,7 @@ function openAuthModal() {
252300
v-if="!atprotoUser"
253301
role="menuitem"
254302
class="w-full text-start gap-x-3 border-none"
255-
@click="openAuthModal"
303+
@click="openAtprotoModal"
256304
>
257305
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
258306
<span class="i-lucide:at-sign w-4 h-4 text-fg-muted" aria-hidden="true" />
@@ -264,11 +312,29 @@ function openAuthModal() {
264312
<span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere_desc') }}</span>
265313
</span>
266314
</ButtonBase>
315+
316+
<ButtonBase
317+
v-if="!isGitHubConnected"
318+
role="menuitem"
319+
class="w-full text-start gap-x-3 border-none"
320+
@click="openGitHubModal"
321+
>
322+
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
323+
<span class="i-simple-icons:github w-4 h-4 text-fg-muted" aria-hidden="true" />
324+
</span>
325+
<span class="flex-1 min-w-0">
326+
<span class="font-mono text-sm text-fg block">
327+
{{ $t('account_menu.connect_github') }}
328+
</span>
329+
<span class="text-xs text-fg-subtle">{{ $t('account_menu.github_desc') }}</span>
330+
</span>
331+
</ButtonBase>
267332
</div>
268333
</div>
269334
</div>
270335
</Transition>
271336
</div>
272337
<HeaderConnectorModal />
273-
<HeaderAuthModal />
338+
<HeaderAtprotoModal />
339+
<HeaderGitHubModal />
274340
</template>

app/components/Header/AuthModal.client.vue renamed to app/components/Header/AtprotoModal.client.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ watch(handleInput, newHandleInput => {
5454

5555
<template>
5656
<!-- Modal -->
57-
<Modal :modalTitle="$t('auth.modal.title')" class="max-w-lg" id="auth-modal">
57+
<Modal :modalTitle="$t('auth.modal.title')" class="max-w-lg" id="atproto-modal">
5858
<div v-if="user?.handle" class="space-y-4">
5959
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
6060
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script setup lang="ts">
2+
const route = useRoute()
3+
const { isConnected, user, login, logout } = useGitHub()
4+
5+
function handleConnect() {
6+
login(route.fullPath)
7+
}
8+
</script>
9+
10+
<template>
11+
<Modal :modalTitle="$t('auth.github.title')" class="max-w-lg" id="github-modal">
12+
<!-- Connected state -->
13+
<div v-if="isConnected && user" class="space-y-4">
14+
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
15+
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
16+
<div>
17+
<p class="font-mono text-xs text-fg-muted">
18+
{{ $t('auth.github.connected_as', { username: user.username }) }}
19+
</p>
20+
</div>
21+
</div>
22+
<ButtonBase class="w-full" @click="logout">
23+
{{ $t('auth.github.disconnect') }}
24+
</ButtonBase>
25+
</div>
26+
27+
<!-- Disconnected state -->
28+
<div v-else class="space-y-4">
29+
<p class="text-sm text-fg-muted">{{ $t('auth.github.connect_prompt') }}</p>
30+
<ButtonBase
31+
variant="primary"
32+
class="w-full"
33+
classicon="i-simple-icons:github"
34+
@click="handleConnect"
35+
>
36+
{{ $t('auth.github.connect') }}
37+
</ButtonBase>
38+
</div>
39+
</Modal>
40+
</template>

app/composables/atproto/useAtproto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ export const useAtproto = createSharedComposable(function useAtproto() {
33
data: user,
44
pending,
55
clear,
6-
} = useFetch('/api/auth/session', {
6+
} = useFetch('/api/auth/atproto/session', {
77
server: false,
88
immediate: !import.meta.test,
99
})
1010

1111
async function logout() {
12-
await $fetch('/api/auth/session', {
12+
await $fetch('/api/auth/atproto/session', {
1313
method: 'delete',
1414
})
1515

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
function login(redirectTo?: string) {
2+
const query: Record<string, string> = {}
3+
if (redirectTo) {
4+
query.returnTo = redirectTo
5+
}
6+
navigateTo(
7+
{
8+
path: '/api/auth/github',
9+
query,
10+
},
11+
{ external: true },
12+
)
13+
}
14+
15+
export const useGitHub = createSharedComposable(function useGitHub() {
16+
const {
17+
data: user,
18+
pending,
19+
clear,
20+
refresh,
21+
} = useFetch<{ username: string } | null>('/api/auth/github/session', {
22+
server: false,
23+
immediate: !import.meta.test,
24+
})
25+
26+
const isConnected = computed(() => !!user.value?.username)
27+
28+
async function logout() {
29+
await $fetch('/api/auth/github/session', {
30+
method: 'delete',
31+
})
32+
33+
clear()
34+
}
35+
36+
return { user, isConnected, pending, logout, login, refresh }
37+
})
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { RepoRef } from '#shared/utils/git-providers'
2+
3+
type StarStatus = {
4+
starred: boolean
5+
connected: boolean
6+
}
7+
8+
export function useGitHubStar(repoRef: Ref<RepoRef | null>) {
9+
const { isConnected } = useGitHub()
10+
11+
const isGitHubRepo = computed(() => repoRef.value?.provider === 'github')
12+
const owner = computed(() => repoRef.value?.owner ?? '')
13+
const repo = computed(() => repoRef.value?.repo ?? '')
14+
15+
const shouldFetch = computed(
16+
() => isConnected.value && isGitHubRepo.value && !!owner.value && !!repo.value,
17+
)
18+
19+
const { data: starStatus, refresh } = useFetch<StarStatus>(
20+
() => `/api/github/starred?owner=${owner.value}&repo=${repo.value}`,
21+
{
22+
server: false,
23+
immediate: false,
24+
default: () => ({ starred: false, connected: false }),
25+
watch: false,
26+
},
27+
)
28+
29+
watch(
30+
shouldFetch,
31+
async value => {
32+
if (value) {
33+
await refresh()
34+
}
35+
},
36+
{ immediate: true },
37+
)
38+
39+
const isStarred = computed(() => starStatus.value?.starred ?? false)
40+
const isStarActionPending = shallowRef(false)
41+
42+
async function toggleStar() {
43+
if (!shouldFetch.value || isStarActionPending.value) return
44+
45+
const currentlyStarred = isStarred.value
46+
47+
// Optimistic update
48+
starStatus.value = {
49+
starred: !currentlyStarred,
50+
connected: true,
51+
}
52+
53+
isStarActionPending.value = true
54+
55+
try {
56+
const result = await $fetch<{ starred: boolean }>('/api/github/star', {
57+
method: currentlyStarred ? 'DELETE' : 'PUT',
58+
body: { owner: owner.value, repo: repo.value },
59+
})
60+
61+
starStatus.value = { starred: result.starred, connected: true }
62+
} catch {
63+
// Revert on error
64+
starStatus.value = {
65+
starred: currentlyStarred,
66+
connected: true,
67+
}
68+
} finally {
69+
isStarActionPending.value = false
70+
}
71+
}
72+
73+
return {
74+
isStarred,
75+
isStarActionPending,
76+
isGitHubRepo,
77+
toggleStar,
78+
}
79+
}

0 commit comments

Comments
 (0)