Verifying…
@@ -155,9 +155,8 @@ import {
USER_NOT_FOUND,
ALREADY_VERIFIED,
} from '@/common/errorCodes'
-import '@/assets/actions-shared.css'
-import '@/assets/button-shared.css'
import { parseErrorResponse } from '@/common/api'
+import '@/assets/styles.css'
const router = useRouter()
const route = useRoute()
@@ -299,8 +298,13 @@ function goToLogin() {
+
diff --git a/frontend/vue-app/src/components/child/ChildView.vue b/frontend/vue-app/src/components/child/ChildView.vue
index 0faef21..805624c 100644
--- a/frontend/vue-app/src/components/child/ChildView.vue
+++ b/frontend/vue-app/src/components/child/ChildView.vue
@@ -6,7 +6,7 @@ import ChildDetailCard from './ChildDetailCard.vue'
import ScrollingList from '../shared/ScrollingList.vue'
import StatusMessage from '../shared/StatusMessage.vue'
import { eventBus } from '@/common/eventBus'
-import '@/assets/view-shared.css'
+//import '@/assets/view-shared.css'
import '@/assets/styles.css'
import type {
Child,
@@ -438,6 +438,35 @@ onUnmounted(() => {
+
diff --git a/frontend/vue-app/src/components/reward/RewardView.vue b/frontend/vue-app/src/components/reward/RewardView.vue
index 27ff83f..e0f3b2b 100644
--- a/frontend/vue-app/src/components/reward/RewardView.vue
+++ b/frontend/vue-app/src/components/reward/RewardView.vue
@@ -40,7 +40,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import ItemList from '../shared/ItemList.vue'
import MessageBlock from '@/components/shared/MessageBlock.vue'
-import '@/assets/button-shared.css'
+//import '@/assets/button-shared.css'
import FloatingActionButton from '../shared/FloatingActionButton.vue'
import DeleteModal from '../shared/DeleteModal.vue'
import type { Reward } from '@/common/models'
diff --git a/frontend/vue-app/src/components/shared/ChildrenListView.vue b/frontend/vue-app/src/components/shared/ChildrenListView.vue
index 0b4a8d7..31757bf 100644
--- a/frontend/vue-app/src/components/shared/ChildrenListView.vue
+++ b/frontend/vue-app/src/components/shared/ChildrenListView.vue
@@ -12,7 +12,7 @@ import type {
Event,
} from '@/common/models'
import MessageBlock from '@/components/shared/MessageBlock.vue'
-import '@/assets/button-shared.css'
+//import '@/assets/button-shared.css'
import FloatingActionButton from '../shared/FloatingActionButton.vue'
import DeleteModal from '../shared/DeleteModal.vue'
diff --git a/frontend/vue-app/src/components/shared/DeleteModal.vue b/frontend/vue-app/src/components/shared/DeleteModal.vue
index 76683e5..a0b6691 100644
--- a/frontend/vue-app/src/components/shared/DeleteModal.vue
+++ b/frontend/vue-app/src/components/shared/DeleteModal.vue
@@ -15,7 +15,6 @@
diff --git a/frontend/vue-app/src/components/shared/LoginButton.vue b/frontend/vue-app/src/components/shared/LoginButton.vue
index a6a65ec..c592faa 100644
--- a/frontend/vue-app/src/components/shared/LoginButton.vue
+++ b/frontend/vue-app/src/components/shared/LoginButton.vue
@@ -8,8 +8,8 @@ import {
logoutParent,
logoutUser,
} from '../../stores/auth'
+import { getCachedImageUrl, getCachedImageBlob } from '@/common/imageCache'
import '@/assets/styles.css'
-import '@/assets/colors.css'
import ModalDialog from './ModalDialog.vue'
const router = useRouter()
@@ -19,6 +19,63 @@ const error = ref('')
const pinInput = ref
(null)
const dropdownOpen = ref(false)
const dropdownRef = ref(null)
+const avatarButtonRef = ref(null)
+const focusedMenuIndex = ref(0)
+
+// User profile data
+const userImageId = ref(null)
+const userFirstName = ref('')
+const userEmail = ref('')
+const profileLoading = ref(true)
+const avatarImageUrl = ref(null)
+const dropdownAvatarImageUrl = ref(null)
+
+// Compute avatar initial
+const avatarInitial = ref('?')
+
+// Fetch user profile
+async function fetchUserProfile() {
+ try {
+ console.log('Fetching user profile')
+ const res = await fetch('/api/user/profile', { credentials: 'include' })
+ if (!res.ok) {
+ console.error('Failed to fetch user profile')
+ profileLoading.value = false
+ return
+ }
+ const data = await res.json()
+ userImageId.value = data.image_id || null
+ userFirstName.value = data.first_name || ''
+ userEmail.value = data.email || ''
+
+ // Update avatar initial
+ avatarInitial.value = userFirstName.value ? userFirstName.value.charAt(0).toUpperCase() : '?'
+
+ profileLoading.value = false
+
+ // Load cached image if available
+ if (userImageId.value) {
+ await loadAvatarImages(userImageId.value)
+ }
+ } catch (e) {
+ console.error('Error fetching user profile:', e)
+ profileLoading.value = false
+ }
+}
+
+async function loadAvatarImages(imageId: string) {
+ try {
+ const blob = await getCachedImageBlob(imageId)
+ // Clean up previous URLs
+ if (avatarImageUrl.value) URL.revokeObjectURL(avatarImageUrl.value)
+ if (dropdownAvatarImageUrl.value) URL.revokeObjectURL(dropdownAvatarImageUrl.value)
+ avatarImageUrl.value = URL.createObjectURL(blob)
+ dropdownAvatarImageUrl.value = URL.createObjectURL(blob)
+ } catch (e) {
+ avatarImageUrl.value = null
+ dropdownAvatarImageUrl.value = null
+ }
+}
const open = async () => {
// Check if user has a pin
@@ -87,10 +144,71 @@ const handleLogout = () => {
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
+ if (dropdownOpen.value) {
+ focusedMenuIndex.value = 0
+ }
}
function closeDropdown() {
dropdownOpen.value = false
+ focusedMenuIndex.value = 0
+ avatarButtonRef.value?.focus()
+}
+
+function handleKeyDown(event: KeyboardEvent) {
+ if (!dropdownOpen.value) {
+ // Handle avatar button keyboard
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ if (isParentAuthenticated.value) {
+ toggleDropdown()
+ } else {
+ open()
+ }
+ }
+ return
+ }
+
+ // Handle dropdown keyboard navigation
+ const menuItems = 3 // Profile, Child Mode, Sign Out
+
+ switch (event.key) {
+ case 'ArrowDown':
+ event.preventDefault()
+ focusedMenuIndex.value = (focusedMenuIndex.value + 1) % menuItems
+ break
+ case 'ArrowUp':
+ event.preventDefault()
+ focusedMenuIndex.value = (focusedMenuIndex.value - 1 + menuItems) % menuItems
+ break
+ case 'Enter':
+ case ' ':
+ event.preventDefault()
+ executeMenuItem(focusedMenuIndex.value)
+ break
+ case 'Escape':
+ event.preventDefault()
+ closeDropdown()
+ break
+ case 'Tab':
+ closeDropdown()
+ break
+ }
+}
+
+function executeMenuItem(index: number) {
+ switch (index) {
+ case 0:
+ goToProfile()
+ break
+ case 1:
+ handleLogout()
+ closeDropdown()
+ break
+ case 2:
+ signOut()
+ break
+ }
}
async function signOut() {
@@ -122,62 +240,115 @@ function handleClickOutside(event: MouseEvent) {
onMounted(() => {
eventBus.on('open-login', open)
document.addEventListener('mousedown', handleClickOutside)
+ fetchUserProfile()
})
+
onUnmounted(() => {
eventBus.off('open-login', open)
document.removeEventListener('mousedown', handleClickOutside)
+
+ // Revoke object URL to free memory
+ if (avatarImageUrl.value) URL.revokeObjectURL(avatarImageUrl.value)
+ if (dropdownAvatarImageUrl.value) URL.revokeObjectURL(dropdownAvatarImageUrl.value)
})
-
-
+
+
-
+