Skip to content
Merged
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
39 changes: 22 additions & 17 deletions app/pages/collections/new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const githubLoginHref = computed(() => loginUrl({ returnTo: route.fullPath }))
>
<h1
id="new-collection-heading"
class="font-mono text-2xl font-medium tracking-tight"
class="text-2xl font-semibold tracking-tight"
>
Publish a collection
</h1>
Expand All @@ -76,16 +76,15 @@ const githubLoginHref = computed(() => loginUrl({ returnTo: route.fullPath }))

<div
v-if="!isAuthenticated"
class="mt-6 rounded-lg border border-default p-6 text-center"
class="mt-6 rounded-lg border border-default p-4"
>
<UIcon name="i-lucide-github" class="mx-auto size-6 text-muted" aria-hidden="true" />
<p class="mt-3 text-sm">
<p class="text-sm">
Sign in with GitHub to publish.
</p>
<UButton
:to="githubLoginHref"
external
class="mt-4"
class="mt-4 min-h-11"
size="sm"
label="Continue with GitHub"
icon="i-lucide-github"
Expand All @@ -98,84 +97,88 @@ const githubLoginHref = computed(() => loginUrl({ returnTo: route.fullPath }))
@submit.prevent="submit"
>
<div>
<label for="name" class="text-xs uppercase tracking-wide text-muted">Name</label>
<label for="name" class="font-mono text-sm text-muted">Name</label>
<input
id="name"
v-model="name"
required
maxlength="120"
class="mt-1 w-full rounded border border-default bg-default px-3 py-2 font-mono text-sm"
class="mt-2 min-h-11 w-full rounded-lg border border-default bg-default px-3 py-2 font-mono text-sm"
placeholder="My Stack"
>
</div>

<div>
<label for="slug" class="text-xs uppercase tracking-wide text-muted">Slug</label>
<label for="slug" class="font-mono text-sm text-muted">Slug</label>
<div class="mt-1 flex items-center gap-2">
<span class="font-mono text-xs text-muted">/@{{ user?.login }}/</span>
<input
id="slug"
v-model="slug"
required
pattern="[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?"
class="flex-1 rounded border border-default bg-default px-3 py-2 font-mono text-sm"
class="min-h-11 min-w-0 flex-1 rounded-lg border border-default bg-default px-3 py-2 font-mono text-sm"
placeholder="my-stack"
>
</div>
</div>

<div>
<label for="preamble" class="text-xs uppercase tracking-wide text-muted">Preamble (optional)</label>
<label for="preamble" class="font-mono text-sm text-muted">Preamble (optional)</label>
<textarea
id="preamble"
v-model="preamble"
rows="4"
maxlength="4000"
class="mt-1 w-full rounded border border-default bg-default px-3 py-2 text-sm"
class="mt-2 w-full rounded-lg border border-default bg-default px-3 py-2 text-sm"
placeholder="A few sentences on what this collection is for."
/>
</div>

<div>
<div class="flex items-center justify-between">
<span class="text-xs uppercase tracking-wide text-muted">Skills</span>
<span class="font-mono text-sm text-muted">Skills</span>
<UButton
size="xs"
color="neutral"
variant="outline"
icon="i-lucide-plus"
label="Add"
class="min-h-11"
@click="addSkill"
/>
</div>
<ul class="mt-2 space-y-2 list-none p-0">
<li
v-for="(s, i) in skills"
:key="i"
class="flex items-center gap-2"
class="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] items-center gap-2 sm:grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.5fr)_auto]"
>
<input
v-model="s.owner"
placeholder="owner"
class="w-32 rounded border border-default bg-default px-2 py-1 font-mono text-xs"
aria-label="Owner"
class="min-h-11 min-w-0 rounded-lg border border-default bg-default px-3 py-2 font-mono text-sm"
>
<span class="font-mono text-xs text-muted">/</span>
<input
v-model="s.repo"
placeholder="repo"
class="w-40 rounded border border-default bg-default px-2 py-1 font-mono text-xs"
aria-label="Repository"
class="min-h-11 min-w-0 rounded-lg border border-default bg-default px-3 py-2 font-mono text-sm"
>
<input
v-model="s.reason"
placeholder="Why this skill? (optional)"
class="flex-1 rounded border border-default bg-default px-2 py-1 text-xs"
aria-label="Reason (optional)"
class="col-span-3 row-start-2 min-h-11 min-w-0 rounded-lg border border-default bg-default px-3 py-2 text-sm sm:col-span-1 sm:row-auto"
>
<UButton
size="xs"
color="neutral"
variant="ghost"
icon="i-lucide-x"
aria-label="Remove skill"
class="min-h-11 min-w-11 justify-center"
@click="removeSkill(i)"
/>
</li>
Expand All @@ -197,12 +200,14 @@ const githubLoginHref = computed(() => loginUrl({ returnTo: route.fullPath }))
variant="ghost"
size="sm"
label="Cancel"
class="min-h-11"
/>
<UButton
:loading="submitting"
type="submit"
size="sm"
label="Publish"
class="min-h-11"
trailing-icon="i-lucide-arrow-right"
/>
</div>
Expand Down
6 changes: 4 additions & 2 deletions app/pages/me/cli-tokens/new.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ useSeoMeta({ title: 'New token', robots: 'noindex' })

<template>
<section class="mx-auto max-w-xl px-4 pt-12 pb-16 sm:px-6 md:pt-16">
<UButton to="/me/devices" label="Devices and tokens" icon="i-lucide-arrow-left" color="neutral" variant="link" class="mb-4 min-h-11 px-0" />
<header class="border-b border-default pb-6">
<h1 class="text-3xl font-semibold tracking-tight sm:text-4xl">
<h1 class="text-2xl font-semibold tracking-tight sm:text-3xl">
New token
</h1>
<p class="mt-3 text-base leading-relaxed text-muted">
Expand All @@ -38,7 +39,7 @@ useSeoMeta({ title: 'New token', robots: 'noindex' })

<UForm v-if="state._tag !== 'created'" :schema="schema" :state="form" class="mt-6 space-y-5" @submit="createToken">
<UFormField label="Label" name="label" description="Choose a name you can recognise when you revoke it." required>
<UInput v-model="form.label" placeholder="API script" maxlength="80" autocomplete="off" :disabled="state._tag === 'creating'" class="w-full" />
<UInput v-model="form.label" placeholder="API script" maxlength="80" autocomplete="off" :disabled="state._tag === 'creating'" :ui="{ base: 'min-h-11' }" class="w-full" />
</UFormField>
<UFormField label="Expiry" name="ttl">
<USelect
Expand All @@ -49,6 +50,7 @@ useSeoMeta({ title: 'New token', robots: 'noindex' })
{ label: '365 days', value: '365' },
]"
:disabled="state._tag === 'creating'"
:ui="{ base: 'min-h-11' }"
class="w-full"
/>
</UFormField>
Expand Down
5 changes: 3 additions & 2 deletions app/pages/me/devices.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ useSeoMeta({ title: 'Devices and tokens', robots: 'noindex' })
</script>

<template>
<section class="mx-auto max-w-3xl px-4 pt-10 pb-16 sm:px-6 md:pt-14">
<section class="mx-auto max-w-5xl px-4 pt-10 pb-16 sm:px-6 md:pt-14">
<UButton to="/me" label="Your skills" icon="i-lucide-arrow-left" color="neutral" variant="link" class="mb-4 min-h-11 px-0" />
<header class="flex flex-col gap-5 border-b border-default pb-7 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-balance sm:text-4xl">
<h1 class="text-2xl font-semibold tracking-tight text-balance sm:text-3xl">
Devices and tokens
</h1>
<p class="mt-3 max-w-xl text-base leading-relaxed text-muted text-pretty">
Expand Down
33 changes: 22 additions & 11 deletions layers/identity/app/components/HeaderAccount.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ const { variant = 'bar' } = defineProps<{
variant?: 'bar' | 'menu'
}>()

const { state, logout } = useAuth()
const { state, logout, isLoading } = useAuth()

const route = useRoute()
const signInTo = computed(() => route.path === '/login' ? route.fullPath : { path: '/login', query: { return_to: route.fullPath } })

const accountItems = computed<DropdownMenuItem[]>(() => {
if (state.value._tag !== 'signed-in')
return []
return [
{ label: `@${state.value.user.login}`, icon: 'i-lucide-user', to: '/me' },
{ label: 'Sign out', icon: 'i-lucide-log-out', onSelect: () => { void logout() } },
{ label: 'Sign out', disabled: isLoading.value, icon: 'i-lucide-log-out', onSelect: () => { void logout() } },
]
})
</script>
Expand All @@ -39,7 +42,7 @@ const accountItems = computed<DropdownMenuItem[]>(() => {
/>
<UButton
v-else-if="state._tag === 'anonymous'"
to="/login"
:to="signInTo"
label="Sign in"
icon="i-lucide-github"
color="neutral"
Expand All @@ -60,13 +63,20 @@ const accountItems = computed<DropdownMenuItem[]>(() => {
class="size-11 justify-center p-0"
:aria-label="`Signed in as @${state.user.login}`"
>
<img
:src="githubAvatarProxyUrl(state.user.login, 64)"
alt=""
width="28"
height="28"
class="size-7 rounded-full"
>
<!-- Mount after hydration so an early image failure reaches the fallback. -->
<ClientOnly>
<UAvatar
:src="githubAvatarProxyUrl(state.user.login, 64)"
:alt="state.user.login"
size="xs"
class="size-7"
/>
<template #fallback>
<span class="inline-flex size-7 items-center justify-center rounded-full bg-elevated text-xs">
{{ state.user.login.charAt(0) }}
</span>
</template>
</ClientOnly>
</UButton>
</LazyUDropdownMenu>
</div>
Expand All @@ -82,7 +92,7 @@ const accountItems = computed<DropdownMenuItem[]>(() => {
/>
<UButton
v-else-if="state._tag === 'anonymous'"
to="/login"
:to="signInTo"
label="Sign in"
icon="i-lucide-github"
color="neutral"
Expand All @@ -107,6 +117,7 @@ const accountItems = computed<DropdownMenuItem[]>(() => {
variant="ghost"
block
class="min-h-11 justify-start"
:loading="isLoading"
@click="() => { void logout() }"
/>
</template>
Expand Down
23 changes: 18 additions & 5 deletions layers/identity/app/composables/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,31 @@ export type AuthState

// Backed by nuxt-auth-utils useUserSession().
export function useAuth() {
const { ready, loggedIn, user, clear, fetch: fetchSession } = useUserSession()
const { ready, loggedIn, user, session, fetch: fetchSession } = useUserSession()
const state = computed<AuthState>(() => {
if (!ready.value)
return { _tag: 'pending' }
return user.value ? { _tag: 'signed-in', user: user.value } : { _tag: 'anonymous' }
})
const isAuthenticated = computed(() => loggedIn.value)
const isLoading = ref(false)
const isLoading = useState('auth:logging-out', () => false)
const toast = useToast()
async function logout() {
await $fetch('/api/auth/logout', { method: 'POST' })
await clear()
await navigateTo('/')
if (isLoading.value)
return
isLoading.value = true
const result = await $fetch('/api/auth/logout', { method: 'POST', retry: false })
.then(() => ({ _tag: 'ok' as const }))
.catch((cause: unknown) => ({ _tag: 'err' as const, cause }))
isLoading.value = false
if (result._tag === 'err') {
console.warn('[logout]', result.cause)
toast.add({ title: 'Could not sign out', description: 'Check your connection and try again.', color: 'error' })
return
}
session.value = null
removeNuxtQueries()
await navigateTo('/', { external: true })
}
function loginUrl(opts: { returnTo?: string, action?: string } = {}): string {
const params = new URLSearchParams()
Expand Down
13 changes: 7 additions & 6 deletions layers/identity/app/pages/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ useSeoMeta({
</script>

<template>
<section class="mx-auto max-w-md px-4 sm:px-6 pt-16 pb-12 md:pt-24">
<div class="rounded-lg border border-default p-6 sm:p-8 text-center">
<UIcon name="i-lucide-github" class="mx-auto size-8 text-muted" aria-hidden="true" />
<h1 class="mt-3 font-mono text-lg font-medium">
<section class="mx-auto max-w-md px-4 sm:px-6 pt-10 pb-12 md:pt-16">
<div class="rounded-lg border border-default p-4 sm:p-5">
<h1 class="text-2xl font-semibold tracking-tight">
Sign in to skilld
</h1>
<p class="mt-2 text-sm text-muted leading-relaxed">
Expand All @@ -42,13 +41,15 @@ useSeoMeta({
label="Continue with GitHub"
icon="i-lucide-github"
trailing-icon="i-lucide-arrow-right"
size="sm"
size="md"
class="min-h-11"
block
/>
</div>
<p
v-if="error"
class="mt-3 text-xs text-error"
class="mt-4 text-sm text-error"
role="alert"
>
Couldn't complete sign-in. Try again.
</p>
Expand Down
Loading