diff --git a/app/components/UserMenu.vue b/app/components/UserMenu.vue index 847d944..b86318a 100644 --- a/app/components/UserMenu.vue +++ b/app/components/UserMenu.vue @@ -15,6 +15,7 @@ import { } from './ui/dropdown-menu' const { user, loggedIn, logout } = useAuth() +const { resetRoles } = useActiveRole() // Extended user type with all profile fields type ExtendedUser = typeof user.value & { @@ -45,6 +46,8 @@ const userInitials = computed(() => { async function handleLogout() { try { await logout() + // Reset role state to default (private, no roles) + resetRoles() } catch (error) { console.error('Logout error:', error) } diff --git a/app/components/navigation/AreaTabs.vue b/app/components/navigation/AreaTabs.vue index 667c6c8..9f3ceea 100644 --- a/app/components/navigation/AreaTabs.vue +++ b/app/components/navigation/AreaTabs.vue @@ -56,12 +56,21 @@ const areas: ProductArea[] = [ route: '/experimenta', roleVisibility: 'all', }, + { + id: 'pedagogical-offers', + label: 'Pädagogische Angebote', + icon: GraduationCap, + enabled: true, + visible: true, + route: '/experimenta', + roleVisibility: ['educator'], + }, { id: 'labs', label: 'Labore', icon: FlaskConical, enabled: false, - visible: false, + visible: true, badge: 'Demnächst', route: '/labs', roleVisibility: ['educator', 'company'], @@ -107,7 +116,7 @@ const currentArea = computed(() => { // Track previous area IDs for animation detection const previousAreaIds = ref([]) -const newlyAddedAreaId = ref(null) +const newlyAddedAreaIds = ref([]) const tabRefs = ref>({}) // 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 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 const addedIds = newAreaIds.filter(id => !oldAreaIds.includes(id)) if (addedIds.length > 0) { - // Mark as newly added for highlight animation - newlyAddedAreaId.value = addedIds[0] - - // Trigger confetti after a small delay (so element is rendered) - setTimeout(() => { - const areaId = addedIds[0] - const element = tabRefs.value[areaId] - if (element) { - triggerConfetti(element) - } - }, 300) - - // Clear highlight after animation + // Mark ALL newly added tabs for highlight animation + newlyAddedAreaIds.value = addedIds + + // Trigger confetti for each new tab with staggered delay (wave effect) + addedIds.forEach((areaId, index) => { + setTimeout(() => { + const element = tabRefs.value[areaId] + if (element) { + triggerConfetti(element) + } + }, 300 + (index * 150)) // Stagger: 300ms, 450ms, 600ms, ... + }) + + // Clear all highlights after animation setTimeout(() => { - newlyAddedAreaId.value = null + newlyAddedAreaIds.value = [] }, 2000) } @@ -189,7 +213,7 @@ function setTabRef(areaId: string, el: any) { :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', !area.enabled && 'opacity-50 cursor-not-allowed', - newlyAddedAreaId === area.id && 'tab-highlight', + newlyAddedAreaIds.includes(area.id) && 'tab-highlight', ]" @click="navigateToArea(area)"> {{ area.label }} @@ -217,7 +241,7 @@ function setTabRef(areaId: string, el: any) { ? 'bg-accent text-white shadow-md' : 'text-white/70 hover:text-white', !area.enabled && 'opacity-50 cursor-not-allowed', - newlyAddedAreaId === area.id && 'tab-highlight', + newlyAddedAreaIds.includes(area.id) && 'tab-highlight', ]" @click="navigateToArea(area)"> {{ area.label }} diff --git a/app/composables/useActiveRole.ts b/app/composables/useActiveRole.ts index 0332a34..e26d2bf 100644 --- a/app/composables/useActiveRole.ts +++ b/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) */ @@ -118,13 +129,21 @@ export function useActiveRole() { /** * Auto-fetch roles when user logs in * This ensures the role button shows the correct role immediately after login - * Uses callOnce to prevent redundant API calls */ const { loggedIn } = useUserSession() + + // Initial fetch if already logged in if (loggedIn.value) { callOnce('init-roles', () => fetchRoleStatus()) } + // Watch for login changes - fetch roles when user logs in + watch(loggedIn, (newLoggedIn) => { + if (newLoggedIn) { + fetchRoleStatus() + } + }) + return { // State (useState already returns writable refs) activeRole, @@ -138,5 +157,6 @@ export function useActiveRole() { // Actions fetchRoleStatus, switchRole, + resetRoles, } } diff --git a/app/composables/useAuth.ts b/app/composables/useAuth.ts index e84b868..d77164a 100644 --- a/app/composables/useAuth.ts +++ b/app/composables/useAuth.ts @@ -11,6 +11,7 @@ export function useAuth() { const { loggedIn, user, clear, fetch } = useUserSession() + const { fetchRoleStatus } = useActiveRole() /** * Login with email and password @@ -37,6 +38,9 @@ export function useAuth() { // Refresh user session await fetch() + // Fetch user roles immediately after login + await fetchRoleStatus() + // Redirect to products page or saved destination const redirectAfterLogin = useCookie('redirect_after_login') const destination = redirectAfterLogin.value || '/'