elk/components/user/UserSignIn.vue

156 wiersze
4.3 KiB
Vue

<script setup lang="ts">
import Fuse from 'fuse.js'
import { $fetch } from 'ofetch'
import { DEFAULT_SERVER } from '~/constants'
const input = $ref<HTMLInputElement>()
let server = $ref<string>('')
let busy = $ref<boolean>(false)
let error = $ref<boolean>(false)
let displayError = $ref<boolean>(false)
let knownServers = $ref<string[]>([])
let autocompleteIndex = $ref(0)
let autocompleteShow = $ref(false)
async function oauth() {
if (busy)
return
busy = true
error = false
displayError = false
await nextTick()
if (server)
server = server.split('/')[0]
try {
location.href = await $fetch<string>(`/api/${server || DEFAULT_SERVER}/login`)
}
catch {
displayError = true
error = true
await nextTick()
input?.focus()
await nextTick()
setTimeout(() => {
busy = false
error = false
}, 512)
}
}
async function handleInput() {
if (server.startsWith('https://'))
server = server.replace('https://', '')
if (server?.length)
displayError = false
}
let fuse = $shallowRef(new Fuse([] as string[]))
const filteredServers = $computed(() => {
if (!server)
return []
const results = fuse.search(server, { limit: 6 }).map(result => result.item)
if (results[0] === server)
return []
return results
})
function move(delta: number) {
autocompleteIndex = ((autocompleteIndex + delta) + filteredServers.length) % filteredServers.length
}
function onEnter(e: KeyboardEvent) {
if (autocompleteShow === true && filteredServers[autocompleteIndex]) {
server = filteredServers[autocompleteIndex]
e.preventDefault()
autocompleteShow = false
}
}
onMounted(async () => {
input?.focus()
knownServers = await $fetch('/api/list-servers')
fuse = new Fuse(knownServers, { shouldSort: true })
})
</script>
<template>
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
<div flex="~ center" mb2>
<img src="/logo.svg" w-12 h-12 mxa height="48" width="48" alt="logo">
<div text-3xl>
{{ $t('action.sign_in') }}
</div>
</div>
<div>
{{ $t('user.server_address_label') }}
</div>
<div :class="error ? 'animate animate-shake-x animate-delay-100' : null">
<div
flex bg-gray:10 px4 py2 mxa rounded
border="~ base" items-center font-mono
focus:outline-none focus:ring="2 primary inset"
relative
:class="displayError ? 'border-red-600 dark:border-red-400' : null"
>
<span text-secondary-light mr1>https://</span>
<input
ref="input"
v-model="server"
outline-none bg-transparent w-full max-w-50
@input="handleInput"
@keydown.down="move(1)"
@keydown.up="move(-1)"
@keydown.enter="onEnter"
@keydown.esc.prevent="autocompleteShow = false"
@blur="autocompleteShow = false"
@focus="autocompleteShow = true"
>
<div
v-if="autocompleteShow && filteredServers.length"
absolute left-6em right-0 top="100%"
bg-base rounded border="~ base"
text-left z-10 shadow of-auto
>
<div
v-for="server, idx in filteredServers"
:key="server"
:value="server"
px-2 py1 font-mono
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
>
{{ server }}
</div>
</div>
</div>
<div min-h-4>
<Transition css enter-active-class="animate animate-fade-in">
<p v-if="displayError" role="alert" p-0 m-0 text-xs text-red-600 dark:text-red-400>
{{ $t('error.sign_in_error') }}
</p>
</Transition>
</div>
</div>
<div text-secondary text-sm flex>
<div i-ri:lightbulb-line mr-1 />
<span>
<i18n-t keypath="user.tip_no_account">
<a href="https://joinmastodon.org/servers" target="_blank" hover="underline text-primary">{{ $t('user.tip_register_account') }}</a>
</i18n-t>
</span>
</div>
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
<span aria-hidden="true" inline-block :class="busy ? 'i-ri:loader-2-fill animate animate-spin' : 'i-ri:login-circle-line'" />
{{ $t('action.sign_in') }}
</button>
</form>
</template>