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
96 changes: 81 additions & 15 deletions app/components/Header/AccountMenu.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,26 @@ const {
} = useConnector()

const { user: atprotoUser } = useAtproto()
const { isConnected: isGitHubConnected, user: githubUser } = useGitHub()

const isOpen = shallowRef(false)

/** Check if connected to at least one service */
const hasAnyConnection = computed(() => isNpmConnected.value || !!atprotoUser.value)
const hasAnyConnection = computed(
() => isNpmConnected.value || !!atprotoUser.value || isGitHubConnected.value,
)

/** Check if connected to both services */
const hasBothConnections = computed(() => isNpmConnected.value && !!atprotoUser.value)
/** Count of connected services for avatar stacking */
const connectedCount = computed(() => {
let count = 0
if (isNpmConnected.value) count++
if (atprotoUser.value) count++
if (isGitHubConnected.value) count++
return count
})

/** Check if connected to more than one service */
const hasMultipleConnections = computed(() => connectedCount.value > 1)

/** Only show count of active (pending/approved/running) operations */
const operationCount = computed(() => activeOperations.value.length)
Expand All @@ -45,12 +57,21 @@ function openConnectorModal() {
}
}

const authModal = useModal('auth-modal')
const atprotoModal = useModal('atproto-modal')

function openAuthModal() {
if (authModal) {
function openAtprotoModal() {
if (atprotoModal) {
isOpen.value = false
authModal.open()
atprotoModal.open()
}
}

const githubModal = useModal('github-modal')

function openGitHubModal() {
if (githubModal) {
isOpen.value = false
githubModal.open()
}
}
</script>
Expand All @@ -68,7 +89,7 @@ function openAuthModal() {
<span
v-if="hasAnyConnection"
class="flex items-center"
:class="hasBothConnections ? '-space-x-2' : ''"
:class="hasMultipleConnections ? '-space-x-2' : ''"
>
<!-- npm avatar (first/back) -->
<img
Expand All @@ -94,15 +115,24 @@ function openAuthModal() {
width="24"
height="24"
class="w-6 h-6 rounded-full ring-2 ring-bg object-cover"
:class="hasBothConnections ? 'relative z-10' : ''"
:class="hasMultipleConnections ? 'relative z-10' : ''"
/>
<span
v-else-if="atprotoUser"
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
:class="hasBothConnections ? 'relative z-10' : ''"
:class="hasMultipleConnections ? 'relative z-10' : ''"
>
<span class="i-lucide:at-sign w-3 h-3 text-fg-muted" aria-hidden="true" />
</span>

<!-- GitHub avatar (overlapping) -->
<span
v-if="isGitHubConnected"
class="w-6 h-6 rounded-full bg-bg-muted ring-2 ring-bg flex items-center justify-center"
:class="hasMultipleConnections ? 'relative z-20' : ''"
>
<span class="i-simple-icons:github w-3 h-3 text-fg-muted" aria-hidden="true" />
</span>
</span>

<!-- "connect" text when not connected -->
Expand Down Expand Up @@ -189,7 +219,7 @@ function openAuthModal() {
v-if="atprotoUser"
role="menuitem"
class="w-full text-start gap-x-3 border-none"
@click="openAuthModal"
@click="openAtprotoModal"
>
<img
v-if="atprotoUser.avatar"
Expand All @@ -212,16 +242,34 @@ function openAuthModal() {
<span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere') }}</span>
</span>
</ButtonBase>

<!-- GitHub connection -->
<ButtonBase
v-if="isGitHubConnected && githubUser"
role="menuitem"
class="w-full text-start gap-x-3 border-none"
@click="openGitHubModal"
>
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
<span class="i-simple-icons:github w-4 h-4 text-fg-muted" aria-hidden="true" />
</span>
<span class="flex-1 min-w-0">
<span class="font-mono text-sm text-fg truncate block">{{
githubUser.username
}}</span>
<span class="text-xs text-fg-subtle">{{ $t('account_menu.github') }}</span>
</span>
</ButtonBase>
</div>

<!-- Divider (only if we have connections AND options to connect) -->
<div
v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser)"
v-if="hasAnyConnection && (!isNpmConnected || !atprotoUser || !isGitHubConnected)"
class="border-t border-border"
/>

<!-- Connect options -->
<div v-if="!isNpmConnected || !atprotoUser" class="py-1">
<div v-if="!isNpmConnected || !atprotoUser || !isGitHubConnected" class="py-1">
<ButtonBase
v-if="!isNpmConnected"
role="menuitem"
Expand Down Expand Up @@ -252,7 +300,7 @@ function openAuthModal() {
v-if="!atprotoUser"
role="menuitem"
class="w-full text-start gap-x-3 border-none"
@click="openAuthModal"
@click="openAtprotoModal"
>
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
<span class="i-lucide:at-sign w-4 h-4 text-fg-muted" aria-hidden="true" />
Expand All @@ -264,11 +312,29 @@ function openAuthModal() {
<span class="text-xs text-fg-subtle">{{ $t('account_menu.atmosphere_desc') }}</span>
</span>
</ButtonBase>

<ButtonBase
v-if="!isGitHubConnected"
role="menuitem"
class="w-full text-start gap-x-3 border-none"
@click="openGitHubModal"
>
<span class="w-8 h-8 rounded-full bg-bg-muted flex items-center justify-center">
<span class="i-simple-icons:github w-4 h-4 text-fg-muted" aria-hidden="true" />
</span>
<span class="flex-1 min-w-0">
<span class="font-mono text-sm text-fg block">
{{ $t('account_menu.connect_github') }}
</span>
<span class="text-xs text-fg-subtle">{{ $t('account_menu.github_desc') }}</span>
</span>
</ButtonBase>
</div>
</div>
</div>
</Transition>
</div>
<HeaderConnectorModal />
<HeaderAuthModal />
<HeaderAtprotoModal />
<HeaderGitHubModal />
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function handleLogin() {
locale: locale.value,
})
} else {
errorMessage.value = $t('auth.modal.default_input_error')
errorMessage.value = $t('auth.modal.atmosphere.default_input_error')
}
}
}
Expand All @@ -62,13 +62,13 @@ watch(user, async newUser => {

<template>
<!-- Modal -->
<Modal :modalTitle="$t('auth.modal.title')" class="max-w-lg" id="auth-modal">
<Modal :modalTitle="$t('auth.modal.atmosphere.title')" class="max-w-lg" id="atproto-modal">
<div v-if="user?.handle" class="space-y-4">
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
<div>
<p class="font-mono text-xs text-fg-muted">
{{ $t('auth.modal.connected_as', { handle: user.handle }) }}
{{ $t('auth.modal.atmosphere.connected_as', { handle: user.handle }) }}
</p>
</div>
</div>
Expand All @@ -79,22 +79,22 @@ watch(user, async newUser => {

<!-- Disconnected state -->
<form v-else class="space-y-4" @submit.prevent="handleLogin">
<p class="text-sm text-fg-muted">{{ $t('auth.modal.connect_prompt') }}</p>
<p class="text-sm text-fg-muted">{{ $t('auth.modal.atmosphere.connect_prompt') }}</p>

<div class="space-y-3">
<div>
<label
for="handle-input"
class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5"
>
{{ $t('auth.modal.handle_label') }}
{{ $t('auth.modal.atmosphere.handle_label') }}
</label>
<InputBase
id="handle-input"
v-model="handleInput"
type="text"
name="handle"
:placeholder="$t('auth.modal.handle_placeholder')"
:placeholder="$t('auth.modal.atmosphere.handle_placeholder')"
no-correct
class="w-full"
size="medium"
Expand All @@ -108,10 +108,10 @@ watch(user, async newUser => {
<summary
class="text-fg-subtle hover:text-fg-muted transition-colors duration-200 focus-visible:(outline-2 outline-accent/70)"
>
{{ $t('auth.modal.what_is_atmosphere') }}
{{ $t('auth.modal.atmosphere.what_is_atmosphere') }}
</summary>
<div class="mt-3">
<i18n-t keypath="auth.modal.atmosphere_explanation" tag="p" scope="global">
<i18n-t keypath="auth.modal.atmosphere.explanation" tag="p" scope="global">
<template #npmx>
<span class="font-bold">npmx.dev</span>
</template>
Expand All @@ -133,7 +133,7 @@ watch(user, async newUser => {
{{ $t('auth.modal.connect') }}
</ButtonBase>
<ButtonBase type="button" class="w-full" @click="handleCreateAccount">
{{ $t('auth.modal.create_account') }}
{{ $t('auth.modal.atmosphere.create_account') }}
</ButtonBase>
<hr class="color-border" />
<ButtonBase
Expand All @@ -142,7 +142,7 @@ watch(user, async newUser => {
@click="handleBlueskySignIn"
classicon="i-simple-icons:bluesky"
>
{{ $t('auth.modal.connect_bluesky') }}
{{ $t('auth.modal.atmosphere.connect_bluesky') }}
</ButtonBase>
</form>
</Modal>
Expand Down
40 changes: 40 additions & 0 deletions app/components/Header/GitHubModal.client.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
const route = useRoute()
const { isConnected, user, login, logout } = useGitHub()

function handleConnect() {
login(route.fullPath)
}
</script>

<template>
<Modal :modalTitle="$t('auth.modal.github.title')" class="max-w-lg" id="github-modal">
<!-- Connected state -->
<div v-if="isConnected && user" class="space-y-4">
<div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg">
<span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" />
<div>
<p class="font-mono text-xs text-fg-muted">
{{ $t('auth.modal.github.connected_as', { username: user.username }) }}
</p>
</div>
</div>
<ButtonBase class="w-full" @click="logout">
{{ $t('auth.modal.disconnect') }}
</ButtonBase>
</div>

<!-- Disconnected state -->
<div v-else class="space-y-4">
<p class="text-sm text-fg-muted">{{ $t('auth.modal.github.connect_prompt') }}</p>
<ButtonBase
variant="primary"
class="w-full"
classicon="i-simple-icons:github"
@click="handleConnect"
>
{{ $t('auth.modal.connect') }}
</ButtonBase>
</div>
</Modal>
</template>
4 changes: 2 additions & 2 deletions app/composables/atproto/useAtproto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ export const useAtproto = createSharedComposable(function useAtproto() {
data: user,
pending,
clear,
} = useFetch('/api/auth/session', {
} = useFetch('/api/auth/atproto/session', {
server: false,
immediate: !import.meta.test,
})

async function logout() {
await $fetch('/api/auth/session', {
await $fetch('/api/auth/atproto/session', {
method: 'delete',
})

Expand Down
37 changes: 37 additions & 0 deletions app/composables/github/useGitHub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
function login(redirectTo?: string) {
const query: Record<string, string> = {}
if (redirectTo) {
query.returnTo = redirectTo
}
navigateTo(
{
path: '/api/auth/github',
query,
},
{ external: true },
)
}

export const useGitHub = createSharedComposable(function useGitHub() {
const {
data: user,
pending,
clear,
refresh,
} = useFetch<{ username: string } | null>('/api/auth/github/session', {
server: false,
immediate: !import.meta.test,
})

const isConnected = computed(() => !!user.value?.username)

async function logout() {
await $fetch('/api/auth/github/session', {
method: 'delete',
})

clear()
}

return { user, isConnected, pending, logout, login, refresh }
})
Loading
Loading