First commit
This commit is contained in:
194
frontend/app/components/AuthModal.vue
Normal file
194
frontend/app/components/AuthModal.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user