Enhance Role-Based Visibility and Navigation Logic

- Introduced role visibility checks in AreaTabs.vue to filter displayed areas based on the user's active role, improving user experience and accessibility.
- Updated RoleSwitcher.vue to enhance accessibility with a high-contrast checkmark for better visibility.
- Modified useActiveRole.ts to streamline role initialization and refresh logic for role-based product visibility.
- Added explicit keys for role-based data fetching in product-related pages to ensure accurate data refresh.
- Enhanced API endpoint for product retrieval to return 404 if a product is not accessible based on the user's role, ensuring security and clarity.
This commit is contained in:
Bastian Masanek
2025-11-05 01:33:46 +01:00
parent f9125e744b
commit dcd96ffb68
8 changed files with 102 additions and 22 deletions

View File

@@ -3,6 +3,8 @@ import { Wrench, FlaskConical, Ticket, Sparkles, GraduationCap, Home } from 'luc
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Badge } from '@/components/ui/badge'
type RoleCode = 'private' | 'educator' | 'company'
interface ProductArea {
id: string
label: string
@@ -11,6 +13,7 @@ interface ProductArea {
visible: boolean
badge?: string
route: string
roleVisibility?: 'all' | RoleCode[]
}
const areas: ProductArea[] = [
@@ -21,6 +24,7 @@ const areas: ProductArea[] = [
enabled: true,
visible: true,
route: '/',
roleVisibility: 'all',
},
{
id: 'makerspace',
@@ -29,6 +33,7 @@ const areas: ProductArea[] = [
enabled: true,
visible: true,
route: '/products',
roleVisibility: 'all',
},
{
id: 'educator',
@@ -38,6 +43,7 @@ const areas: ProductArea[] = [
visible: true,
badge: 'Demnächst',
route: '/educator',
roleVisibility: ['educator'],
},
{
id: 'experimenta',
@@ -47,6 +53,7 @@ const areas: ProductArea[] = [
visible: true,
badge: 'Demnächst',
route: '/experimenta',
roleVisibility: 'all',
},
{
id: 'labs',
@@ -56,22 +63,41 @@ const areas: ProductArea[] = [
visible: false,
badge: 'Demnächst',
route: '/labs',
roleVisibility: ['educator', 'company'],
},
]
const route = useRoute()
const { activeRole } = useActiveRole()
// Filter areas by role visibility
const visibleAreas = computed(() => {
return areas.filter(area => {
// Legacy visible flag check
if (!area.visible) return false
// No role requirement = visible to all (backward compatible)
if (!area.roleVisibility) return true
// Explicitly set to 'all'
if (area.roleVisibility === 'all') return true
// Check if user's active role matches
return area.roleVisibility.includes(activeRole.value as RoleCode)
})
})
const currentArea = computed(() => {
// Determine current area based on route - check areas array dynamically
// Determine current area based on route - check visibleAreas array dynamically
const currentPath = route.path
// Exact match for root path
if (currentPath === '/') {
return areas.find(area => area.route === '/')?.id || ''
return visibleAreas.value.find(area => area.route === '/')?.id || ''
}
// Find area where route path starts with area.route
const matchedArea = areas.find(area =>
const matchedArea = visibleAreas.value.find(area =>
area.route !== '/' && currentPath.startsWith(area.route)
)
@@ -90,7 +116,7 @@ function navigateToArea(area: ProductArea) {
<!-- Desktop: Tabs -->
<Tabs :model-value="currentArea" class="hidden md:block">
<TabsList class="h-auto p-2 bg-white/5">
<TabsTrigger v-for="area in areas.filter(area => area.visible)" :key="area.id" :value="area.id"
<TabsTrigger v-for="area in visibleAreas" :key="area.id" :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',
!area.enabled && 'opacity-50 cursor-not-allowed',
@@ -112,7 +138,7 @@ function navigateToArea(area: ProductArea) {
<!-- Mobile: Horizontal scroll with cards (matching desktop styling) -->
<div class="md:hidden overflow-x-auto scrollbar-hide">
<div class="inline-flex h-auto items-center justify-center rounded-[35px] bg-white/5 p-2 min-w-max">
<button v-for="area in areas.filter(area => area.visible)" :key="area.id" :disabled="!area.enabled" :class="[
<button v-for="area in visibleAreas" :key="area.id" :disabled="!area.enabled" :class="[
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-[25px] px-4 py-3 text-lg font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-0',
currentArea === area.id
? 'bg-accent text-white shadow-md'