Files
chore/frontend/vue-app/src/components/shared/LoginButton.vue
2026-01-28 16:42:06 -05:00

245 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, nextTick, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { eventBus } from '@/common/eventBus'
import { authenticateParent, isParentAuthenticated, logoutParent } from '../../stores/auth'
import '@/assets/styles.css'
import '@/assets/colors.css'
import ModalDialog from './ModalDialog.vue'
const router = useRouter()
const show = ref(false)
const pin = ref('')
const error = ref('')
const pinInput = ref<HTMLInputElement | null>(null)
const dropdownOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
const open = async () => {
// Check if user has a pin
try {
const res = await fetch('/api/user/has-pin', { credentials: 'include' })
const data = await res.json()
if (!res.ok) throw new Error(data.error || 'Error checking PIN')
if (!data.has_pin) {
console.log('No PIN set, redirecting to setup')
// Route to PIN setup view
router.push('/parent/pin-setup')
return
}
} catch (e) {
error.value = 'Network error'
return
}
pin.value = ''
error.value = ''
show.value = true
await nextTick()
pinInput.value?.focus()
}
const close = () => {
show.value = false
error.value = ''
}
const submit = async () => {
const isDigits = /^\d{4,6}$/.test(pin.value)
if (!isDigits) {
error.value = 'Enter 46 digits'
return
}
try {
const res = await fetch('/api/user/check-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pin: pin.value }),
credentials: 'include',
})
const data = await res.json()
if (!res.ok) {
error.value = data.error || 'Error validating PIN'
return
}
if (!data.valid) {
error.value = 'Incorrect PIN'
return
}
// Authenticate parent and navigate
authenticateParent()
close()
router.push('/parent')
} catch (e) {
error.value = 'Network error'
}
}
const handleLogout = () => {
logoutParent()
router.push('/child')
}
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
function closeDropdown() {
dropdownOpen.value = false
}
async function signOut() {
try {
await fetch('/api/logout', { method: 'POST' })
logoutParent()
router.push('/auth')
} catch {
// Optionally show error
}
closeDropdown()
}
function goToProfile() {
router.push('/parent/profile')
closeDropdown()
}
function handleClickOutside(event: MouseEvent) {
if (
dropdownOpen.value &&
dropdownRef.value &&
!dropdownRef.value.contains(event.target as Node)
) {
closeDropdown()
}
}
onMounted(() => {
eventBus.on('open-login', open)
document.addEventListener('mousedown', handleClickOutside)
})
onUnmounted(() => {
eventBus.off('open-login', open)
document.removeEventListener('mousedown', handleClickOutside)
})
</script>
<template>
<div style="position: relative">
<button v-if="!isParentAuthenticated" @click="open" aria-label="Parent login" class="login-btn">
Parent
</button>
<div v-else style="display: inline-block; position: relative" ref="dropdownRef">
<button
@click="toggleDropdown"
aria-label="Parent menu"
class="login-btn"
style="min-width: 80px"
>
Parent
</button>
<div
v-if="dropdownOpen"
class="dropdown-menu"
style="
position: absolute;
right: 0;
top: 100%;
background: #fff;
border: 1px solid #e6e6e6;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
min-width: 120px;
z-index: 10;
"
>
<button class="menu-item" @click="goToProfile" style="width: 100%; text-align: left">
Profile
</button>
<button
class="menu-item"
@click="
() => {
handleLogout()
closeDropdown()
}
"
style="width: 100%; text-align: left"
>
Log out
</button>
<button class="menu-item danger" @click="signOut" style="width: 100%; text-align: left">
Sign out
</button>
</div>
</div>
<ModalDialog v-if="show" title="Enter parent PIN" @click.self="close" @close="close">
<form @submit.prevent="submit">
<input
ref="pinInput"
v-model="pin"
inputmode="numeric"
pattern="\d*"
maxlength="6"
placeholder="46 digits"
class="pin-input"
/>
<div class="actions modal-actions">
<button type="button" class="btn btn-secondary" @click="close">Cancel</button>
<button type="submit" class="btn btn-primary">OK</button>
</div>
</form>
<div v-if="error" class="error modal-message">{{ error }}</div>
</ModalDialog>
</div>
</template>
<style>
/* modal */
.pin-input {
width: 100%;
padding: 0.5rem 0.6rem;
font-size: 1rem;
border-radius: 8px;
border: 1px solid #e6e6e6;
margin-bottom: 0.6rem;
box-sizing: border-box;
text-align: center;
}
.dropdown-menu {
padding: 0.5rem 0;
}
.menu-item {
padding: 1rem 0.9rem;
background: transparent;
border: 0;
text-align: left;
cursor: pointer;
font-weight: 600;
color: var(--menu-item-color, #333);
font-size: 0.9rem;
}
.menu-item + .menu-item {
margin-top: 0.5rem;
}
.menu-item:hover {
background: var(--menu-item-hover-bg, rgba(0, 0, 0, 0.04));
}
.menu-item.danger {
color: var(--menu-item-danger, #ff4d4f);
}
@media (max-width: 600px) {
.menu-item {
padding: 0.85rem 0.7rem;
font-size: 1rem;
}
.menu-item + .menu-item {
margin-top: 0.35rem;
}
}
</style>