elk/components/user/UserSignIn.vue

180 lines
5.3 KiB
Vue
Raw Normal View History

2022-11-23 03:06:56 +00:00
<script setup lang="ts">
2022-12-24 00:40:51 +00:00
import Fuse from 'fuse.js'
2022-11-23 03:06:56 +00:00
const input = ref<HTMLInputElement | undefined>()
2022-12-24 00:40:51 +00:00
let knownServers = $ref<string[]>([])
2022-12-24 11:42:54 +00:00
let autocompleteIndex = $ref(0)
let autocompleteShow = $ref(false)
2022-11-23 03:06:56 +00:00
const { busy, error, displayError, server, oauth } = useSignIn(input)
2022-11-23 09:06:32 +00:00
2022-12-24 00:40:51 +00:00
let fuse = $shallowRef(new Fuse([] as string[]))
const filteredServers = $computed(() => {
if (!server.value)
2022-12-24 00:40:51 +00:00
return []
const results = fuse.search(server.value, { limit: 6 }).map(result => result.item)
if (results[0] === server.value)
2022-12-24 00:40:51 +00:00
return []
return results
})
function isValidUrl(str: string) {
try {
// eslint-disable-next-line no-new
new URL(str)
return true
}
catch (err) {
return false
}
}
async function handleInput() {
const input = server.value.trim()
2023-01-12 18:18:21 +00:00
if (input.startsWith('https://'))
server.value = input.replace('https://', '')
2023-01-12 18:18:21 +00:00
if (input.length)
displayError.value = false
if (
2023-01-12 18:18:21 +00:00
isValidUrl(`https://${input}`)
&& input.match(/^[a-z0-9-]+(\.[a-z0-9-]+)+(:[0-9]+)?$/i)
// Do not hide the autocomplete if a result has an exact substring match on the input
2023-01-12 18:18:21 +00:00
&& !filteredServers.some(s => s.includes(input))
)
autocompleteShow = false
else
autocompleteShow = true
}
function toSelector(server: string) {
return server.replace(/[^\w-]/g, '-')
}
2022-12-24 00:40:51 +00:00
function move(delta: number) {
if (filteredServers.length === 0) {
autocompleteIndex = 0
return
}
2022-12-24 11:42:54 +00:00
autocompleteIndex = ((autocompleteIndex + delta) + filteredServers.length) % filteredServers.length
document.querySelector(`#${toSelector(filteredServers[autocompleteIndex])}`)?.scrollIntoView(false)
2022-12-24 00:40:51 +00:00
}
2022-12-24 11:42:54 +00:00
function onEnter(e: KeyboardEvent) {
if (autocompleteShow === true && filteredServers[autocompleteIndex]) {
server.value = filteredServers[autocompleteIndex]
2022-12-24 11:42:54 +00:00
e.preventDefault()
autocompleteShow = false
}
2022-12-24 00:40:51 +00:00
}
function escapeAutocomplete(evt: KeyboardEvent) {
if (!autocompleteShow)
return
autocompleteShow = false
evt.stopPropagation()
}
function select(index: number) {
server.value = filteredServers[index]
}
2022-12-24 00:40:51 +00:00
onMounted(async () => {
input?.value?.focus()
2023-01-12 22:58:41 +00:00
knownServers = await (globalThis.$fetch as any)('/api/list-servers')
2022-12-24 00:40:51 +00:00
fuse = new Fuse(knownServers, { shouldSort: true })
2022-11-29 12:04:16 +00:00
})
onClickOutside(input, () => {
autocompleteShow = false
})
2022-11-23 03:06:56 +00:00
</script>
2022-11-23 02:53:22 +00:00
<template>
2022-12-02 07:02:44 +00:00
<form text-center justify-center items-center max-w-150 py6 flex="~ col gap-3" @submit.prevent="oauth">
<div flex="~ center" items-end mb2 gap-x-2>
2023-02-12 11:56:29 +00:00
<img :src="`/${''}logo.svg`" w-12 h-12 mxa height="48" width="48" :alt="$t('app_logo')" class="rtl-flip">
2022-11-30 01:45:52 +00:00
<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
dir="ltr"
flex bg-gray:10 px4 py2 mxa rounded
border="~ base" items-center font-mono
focus:outline-none focus:ring="2 primary inset"
2022-12-24 00:40:51 +00:00
relative
:class="displayError ? 'border-red-600 dark:border-red-400' : null"
>
<span text-secondary-light me1>https://</span>
2022-12-24 00:40:51 +00:00
<input
ref="input"
v-model="server"
autocapitalize="off"
inputmode="url"
outline-none bg-transparent w-full max-w-50
spellcheck="false"
autocorrect="off"
autocomplete="off"
@input="handleInput"
2022-12-24 01:06:46 +00:00
@keydown.down="move(1)"
@keydown.up="move(-1)"
2022-12-24 00:40:51 +00:00
@keydown.enter="onEnter"
@keydown.esc.prevent="escapeAutocomplete"
2022-12-24 11:42:54 +00:00
@focus="autocompleteShow = true"
2022-12-24 00:40:51 +00:00
>
<div
2022-12-24 11:42:54 +00:00
v-if="autocompleteShow && filteredServers.length"
2022-12-24 00:40:51 +00:00
absolute left-6em right-0 top="100%"
bg-base rounded border="~ base"
z-10 shadow of-auto
overflow-y-auto
class="max-h-[8rem]"
>
<button
v-for="(name, idx) in filteredServers"
:id="toSelector(name)"
:key="name"
:value="name"
px-2 py1 font-mono w-full text-left
2022-12-24 11:42:54 +00:00
:class="autocompleteIndex === idx ? 'text-primary font-bold' : null"
@click="select(idx)"
2022-12-24 00:40:51 +00:00
>
{{ name }}
</button>
2022-12-24 00:40:51 +00:00
</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>
2022-11-23 02:53:22 +00:00
</div>
2022-11-30 01:45:52 +00:00
<div text-secondary text-sm flex>
<div i-ri:lightbulb-line me-1 />
2022-11-30 01:45:52 +00:00
<span>
<i18n-t keypath="user.tip_no_account">
<NuxtLink href="https://joinmastodon.org/servers" target="_blank" external class="text-primary" hover="underline">{{ $t('user.tip_register_account') }}</NuxtLink>
2022-11-30 01:45:52 +00:00
</i18n-t>
</span>
2022-11-23 03:06:56 +00:00
</div>
<button flex="~ row" gap-x-2 items-center btn-solid mt2 :disabled="!server || busy">
<span v-if="busy" aria-hidden="true" block animate animate-spin preserve-3d class="rtl-flip">
<span block i-ri:loader-2-fill aria-hidden="true" />
</span>
<span v-else aria-hidden="true" block i-ri:login-circle-line class="rtl-flip" />
{{ $t('action.sign_in') }}
2022-11-23 03:06:56 +00:00
</button>
</form>
2022-11-23 02:53:22 +00:00
</template>