195 lines
6.3 KiB
Vue
195 lines
6.3 KiB
Vue
<script setup lang="ts">
|
|
/* Both ways in — into an existing account or a new one — in a single popup,
|
|
drawn and closed in the same shape as the settings popup: esc, the backdrop,
|
|
the ✕, or moving to another page. Whoever opens it chooses a tab and may leave
|
|
a line of explanation above the form (see useSignInPopup); the editor uses
|
|
that when a save comes back saying the session has gone.
|
|
|
|
Signing in is enough on its own: the editor pages show their form as soon as
|
|
there is a session, so the page behind this popup is already ready when it
|
|
closes — whatever was being written is still there. */
|
|
const { open, mode, notice, show, hide } = useSignInPopup()
|
|
const { hide: hideSearch } = useSearch()
|
|
const { hide: hideSettings } = useSettings()
|
|
const { hide: hideProfile } = useProfileMenu()
|
|
const { signIn, signUp } = useAuth()
|
|
const { applyAccount } = useAppLocale()
|
|
const { t } = useI18n()
|
|
const route = useRoute()
|
|
|
|
const username = ref('')
|
|
const password = ref('')
|
|
const errorMsg = ref('')
|
|
const busy = ref(false)
|
|
const panelEl = ref<HTMLElement | null>(null)
|
|
const usernameEl = ref<HTMLInputElement | null>(null)
|
|
|
|
const isSignUp = computed(() => mode.value === 'signup')
|
|
const heading = computed(() => (isSignUp.value ? t('auth.signup') : t('auth.signin')))
|
|
const submitLabel = computed(() =>
|
|
busy.value ? t('auth.busy') : isSignUp.value ? t('auth.signupSubmit') : t('auth.signin')
|
|
)
|
|
|
|
watch(
|
|
open,
|
|
async (isOpen) => {
|
|
if (!isOpen) return
|
|
// opening this shouldn't leave another popup stacked behind it
|
|
hideSearch()
|
|
hideSettings()
|
|
hideProfile()
|
|
errorMsg.value = ''
|
|
await nextTick()
|
|
usernameEl.value?.focus()
|
|
},
|
|
{ flush: 'post' }
|
|
)
|
|
|
|
/* Switching tabs keeps the username — the same person either way — but not the
|
|
password, which is a different secret in each direction. */
|
|
function setMode(next: SignInMode) {
|
|
if (mode.value === next) return
|
|
show({ mode: next })
|
|
password.value = ''
|
|
errorMsg.value = ''
|
|
}
|
|
|
|
async function submit() {
|
|
if (busy.value) return
|
|
errorMsg.value = ''
|
|
busy.value = true
|
|
try {
|
|
// the account decides the interface language — let it overrule the device
|
|
const user = isSignUp.value ? await signUp(username.value, password.value) : await signIn(username.value, password.value)
|
|
applyAccount(user?.locale)
|
|
password.value = ''
|
|
hide()
|
|
} catch (err: any) {
|
|
errorMsg.value = err?.data?.message || t('common.apiUnreachable')
|
|
} finally {
|
|
busy.value = false
|
|
}
|
|
}
|
|
|
|
/* --- esc closes, navigation closes --- */
|
|
function onGlobalKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && open.value) hide()
|
|
}
|
|
onMounted(() => window.addEventListener('keydown', onGlobalKeydown))
|
|
onBeforeUnmount(() => window.removeEventListener('keydown', onGlobalKeydown))
|
|
|
|
watch(
|
|
() => route.fullPath,
|
|
() => {
|
|
if (open.value) hide()
|
|
}
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<Teleport to="body">
|
|
<Transition name="auth-popup">
|
|
<div v-if="open" class="auth-overlay" @click.self="hide()">
|
|
<div
|
|
ref="panelEl"
|
|
class="auth-modal"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
:aria-label="heading"
|
|
tabindex="-1"
|
|
>
|
|
<div class="auth-modal-head">
|
|
<h2 class="auth-title">{{ heading }}</h2>
|
|
<button
|
|
type="button"
|
|
class="panel-icon-btn"
|
|
:aria-label="t('common.close')"
|
|
:title="t('common.close')"
|
|
@click="hide()"
|
|
>
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
|
|
<path d="M6 6l12 12M18 6L6 18" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="auth-modal-body">
|
|
<div class="auth-tabs" role="tablist" :aria-label="t('auth.tabsAria')">
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="auth-tab"
|
|
:class="{ active: !isSignUp }"
|
|
:aria-selected="!isSignUp"
|
|
@click="setMode('signin')"
|
|
>
|
|
{{ t('auth.signin') }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
role="tab"
|
|
class="auth-tab"
|
|
:class="{ active: isSignUp }"
|
|
:aria-selected="isSignUp"
|
|
@click="setMode('signup')"
|
|
>
|
|
{{ t('auth.signupTab') }}
|
|
</button>
|
|
</div>
|
|
|
|
<p v-if="notice" class="auth-notice">{{ notice }}</p>
|
|
|
|
<form @submit.prevent="submit">
|
|
<div class="field">
|
|
<label for="auth-username">{{ t('auth.username') }}</label>
|
|
<input
|
|
id="auth-username"
|
|
ref="usernameEl"
|
|
v-model="username"
|
|
type="text"
|
|
autocomplete="username"
|
|
autocapitalize="none"
|
|
spellcheck="false"
|
|
:placeholder="t('auth.usernamePlaceholder')"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="field">
|
|
<label for="auth-password">
|
|
{{ t('auth.password') }} <span class="hint">{{ t('auth.passwordHint') }}</span>
|
|
</label>
|
|
<input
|
|
id="auth-password"
|
|
v-model="password"
|
|
type="password"
|
|
:autocomplete="isSignUp ? 'new-password' : 'current-password'"
|
|
minlength="8"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<p v-if="errorMsg" class="form-error">{{ errorMsg }}</p>
|
|
|
|
<button type="submit" class="btn btn-primary auth-submit" :disabled="busy">
|
|
{{ submitLabel }}
|
|
</button>
|
|
</form>
|
|
|
|
<p class="auth-switch">
|
|
<template v-if="isSignUp">
|
|
{{ t('auth.haveAccount') }}
|
|
<button type="button" @click="setMode('signin')">{{ t('auth.signin') }}</button>
|
|
</template>
|
|
<template v-else>
|
|
{{ t('auth.newToMathew') }}
|
|
<button type="button" @click="setMode('signup')">{{ t('auth.signup') }}</button>
|
|
</template>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</template>
|