Browse Source

Implement Role Reset on Logout and Enhance AreaTabs Logic

- Added a resetRoles function in useActiveRole to clear user roles upon logout, ensuring a clean state.
- Updated UserMenu.vue to call resetRoles during logout for improved role management.
- Enhanced AreaTabs.vue to support multiple newly added tabs with staggered confetti animations, improving user engagement.
- Refactored logic to track newly added areas and their visibility, ensuring a smoother navigation experience.
main
Bastian Masanek 1 month ago
parent
commit
344c3d6644
  1. 3
      app/components/UserMenu.vue
  2. 46
      app/components/navigation/AreaTabs.vue
  3. 22
      app/composables/useActiveRole.ts
  4. 4
      app/composables/useAuth.ts

3
app/components/UserMenu.vue

@ -15,6 +15,7 @@ import {
} from './ui/dropdown-menu' } from './ui/dropdown-menu'
const { user, loggedIn, logout } = useAuth() const { user, loggedIn, logout } = useAuth()
const { resetRoles } = useActiveRole()
// Extended user type with all profile fields // Extended user type with all profile fields
type ExtendedUser = typeof user.value & { type ExtendedUser = typeof user.value & {
@ -45,6 +46,8 @@ const userInitials = computed(() => {
async function handleLogout() { async function handleLogout() {
try { try {
await logout() await logout()
// Reset role state to default (private, no roles)
resetRoles()
} catch (error) { } catch (error) {
console.error('Logout error:', error) console.error('Logout error:', error)
} }

46
app/components/navigation/AreaTabs.vue

@ -56,12 +56,21 @@ const areas: ProductArea[] = [
route: '/experimenta', route: '/experimenta',
roleVisibility: 'all', roleVisibility: 'all',
}, },
{
id: 'pedagogical-offers',
label: 'Pädagogische Angebote',
icon: GraduationCap,
enabled: true,
visible: true,
route: '/experimenta',
roleVisibility: ['educator'],
},
{ {
id: 'labs', id: 'labs',
label: 'Labore', label: 'Labore',
icon: FlaskConical, icon: FlaskConical,
enabled: false, enabled: false,
visible: false, visible: true,
badge: 'Demnächst', badge: 'Demnächst',
route: '/labs', route: '/labs',
roleVisibility: ['educator', 'company'], roleVisibility: ['educator', 'company'],
@ -107,7 +116,7 @@ const currentArea = computed(() => {
// Track previous area IDs for animation detection // Track previous area IDs for animation detection
const previousAreaIds = ref<string[]>([]) const previousAreaIds = ref<string[]>([])
const newlyAddedAreaId = ref<string | null>(null) const newlyAddedAreaIds = ref<string[]>([])
const tabRefs = ref<Record<string, HTMLElement>>({}) const tabRefs = ref<Record<string, HTMLElement>>({})
// Watch for changes in visible areas to trigger animations // Watch for changes in visible areas to trigger animations
@ -115,25 +124,40 @@ watch(visibleAreas, (newAreas, oldAreas) => {
const newAreaIds = newAreas.map(a => a.id) const newAreaIds = newAreas.map(a => a.id)
const oldAreaIds = oldAreas?.map(a => a.id) || [] const oldAreaIds = oldAreas?.map(a => a.id) || []
// Check if current route is still accessible with new visible areas
const currentPath = route.path
if (currentPath !== '/') {
const isCurrentRouteStillVisible = newAreas.some(area =>
area.route !== '/' && currentPath.startsWith(area.route)
)
if (!isCurrentRouteStillVisible) {
// Delay navigation until fade-out animation completes (300ms + 50ms buffer)
setTimeout(() => {
navigateTo('/')
}, 350)
}
}
// Find newly added areas // Find newly added areas
const addedIds = newAreaIds.filter(id => !oldAreaIds.includes(id)) const addedIds = newAreaIds.filter(id => !oldAreaIds.includes(id))
if (addedIds.length > 0) { if (addedIds.length > 0) {
// Mark as newly added for highlight animation // Mark ALL newly added tabs for highlight animation
newlyAddedAreaId.value = addedIds[0] newlyAddedAreaIds.value = addedIds
// Trigger confetti after a small delay (so element is rendered) // Trigger confetti for each new tab with staggered delay (wave effect)
addedIds.forEach((areaId, index) => {
setTimeout(() => { setTimeout(() => {
const areaId = addedIds[0]
const element = tabRefs.value[areaId] const element = tabRefs.value[areaId]
if (element) { if (element) {
triggerConfetti(element) triggerConfetti(element)
} }
}, 300) }, 300 + (index * 150)) // Stagger: 300ms, 450ms, 600ms, ...
})
// Clear highlight after animation // Clear all highlights after animation
setTimeout(() => { setTimeout(() => {
newlyAddedAreaId.value = null newlyAddedAreaIds.value = []
}, 2000) }, 2000)
} }
@ -189,7 +213,7 @@ function setTabRef(areaId: string, el: any) {
:value="area.id" :disabled="!area.enabled" :class="[ :value="area.id" :disabled="!area.enabled" :class="[
'gap-2 py-3 md:py-4 data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md transition-all duration-300', 'gap-2 py-3 md:py-4 data-[state=active]:bg-accent data-[state=active]:text-white data-[state=active]:shadow-md transition-all duration-300',
!area.enabled && 'opacity-50 cursor-not-allowed', !area.enabled && 'opacity-50 cursor-not-allowed',
newlyAddedAreaId === area.id && 'tab-highlight', newlyAddedAreaIds.includes(area.id) && 'tab-highlight',
]" @click="navigateToArea(area)"> ]" @click="navigateToArea(area)">
<component :is="area.icon" class="h-4 w-4" /> <component :is="area.icon" class="h-4 w-4" />
<span>{{ area.label }}</span> <span>{{ area.label }}</span>
@ -217,7 +241,7 @@ function setTabRef(areaId: string, el: any) {
? 'bg-accent text-white shadow-md' ? 'bg-accent text-white shadow-md'
: 'text-white/70 hover:text-white', : 'text-white/70 hover:text-white',
!area.enabled && 'opacity-50 cursor-not-allowed', !area.enabled && 'opacity-50 cursor-not-allowed',
newlyAddedAreaId === area.id && 'tab-highlight', newlyAddedAreaIds.includes(area.id) && 'tab-highlight',
]" @click="navigateToArea(area)"> ]" @click="navigateToArea(area)">
<component :is="area.icon" class="h-4 w-4" /> <component :is="area.icon" class="h-4 w-4" />
<span>{{ area.label }}</span> <span>{{ area.label }}</span>

22
app/composables/useActiveRole.ts

@ -105,6 +105,17 @@ export function useActiveRole() {
} }
} }
/**
* Reset all role state (called on logout)
*/
function resetRoles() {
activeRole.value = 'private'
roles.value = []
roleChangedByAdmin.value = false
loading.value = false
error.value = null
}
/** /**
* Get roles that user actually has (approved only) * Get roles that user actually has (approved only)
*/ */
@ -118,13 +129,21 @@ export function useActiveRole() {
/** /**
* Auto-fetch roles when user logs in * Auto-fetch roles when user logs in
* This ensures the role button shows the correct role immediately after login * This ensures the role button shows the correct role immediately after login
* Uses callOnce to prevent redundant API calls
*/ */
const { loggedIn } = useUserSession() const { loggedIn } = useUserSession()
// Initial fetch if already logged in
if (loggedIn.value) { if (loggedIn.value) {
callOnce('init-roles', () => fetchRoleStatus()) callOnce('init-roles', () => fetchRoleStatus())
} }
// Watch for login changes - fetch roles when user logs in
watch(loggedIn, (newLoggedIn) => {
if (newLoggedIn) {
fetchRoleStatus()
}
})
return { return {
// State (useState already returns writable refs) // State (useState already returns writable refs)
activeRole, activeRole,
@ -138,5 +157,6 @@ export function useActiveRole() {
// Actions // Actions
fetchRoleStatus, fetchRoleStatus,
switchRole, switchRole,
resetRoles,
} }
} }

4
app/composables/useAuth.ts

@ -11,6 +11,7 @@
export function useAuth() { export function useAuth() {
const { loggedIn, user, clear, fetch } = useUserSession() const { loggedIn, user, clear, fetch } = useUserSession()
const { fetchRoleStatus } = useActiveRole()
/** /**
* Login with email and password * Login with email and password
@ -37,6 +38,9 @@ export function useAuth() {
// Refresh user session // Refresh user session
await fetch() await fetch()
// Fetch user roles immediately after login
await fetchRoleStatus()
// Redirect to products page or saved destination // Redirect to products page or saved destination
const redirectAfterLogin = useCookie('redirect_after_login') const redirectAfterLogin = useCookie('redirect_after_login')
const destination = redirectAfterLogin.value || '/' const destination = redirectAfterLogin.value || '/'

Loading…
Cancel
Save