archy/neode-ui/src/views/Dashboard.vue

1171 lines
40 KiB
Vue
Raw Normal View History

2026-01-24 22:59:20 +00:00
<template>
<div class="min-h-screen flex relative dashboard-view">
<!-- Background container with 3D perspective -->
<div class="bg-perspective-container">
<!-- Background - default -->
<div
ref="bgDefault"
class="bg-layer"
:class="{ 'bg-transitioning-out': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
:style="{ backgroundImage: `url(/assets/img/${currentBackgroundImage})` }"
/>
<!-- Background - alternate for app details and Web5 -->
<div
ref="bgAlt"
class="bg-layer"
:class="{ 'bg-transitioning-in': showAltBackground || showWeb5Background || showNetworkBackground || showSettingsBackground || showMyAppsBackground || showAppStoreBackground || showCloudBackground || showHomeBackground }"
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
/>
<!-- Glitch overlays - trigger on background change -->
<div
class="bg-glitch-layer-1"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
/>
<div
class="bg-glitch-layer-2"
:class="{ 'glitch-active': isGlitching }"
:style="{ backgroundImage: `url(/assets/img/${altBackgroundImage})` }"
/>
<div
class="bg-glitch-scan"
:class="{ 'glitch-active': isGlitching }"
/>
</div>
<!-- Background overlay - 0.3 opacity default, 0.8 opacity for Web5, Network, and Settings -->
<div
class="fixed inset-0 transition-opacity duration-500 pointer-events-none"
:class="showDarkOverlay ? 'bg-black/80' : 'bg-black/30'"
style="z-index: -5;"
/>
<!-- Sidebar - Desktop Only -->
<aside class="hidden md:flex w-[256px] border-r border-glass-border shadow-glass-sm flex-shrink-0 relative flex-col" style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);">
<div class="p-6 flex-1">
<div class="flex items-center gap-3 mb-8">
<AnimatedLogo />
<div class="min-w-0 flex-1">
<h2 class="text-lg font-semibold text-white truncate">{{ serverName }}</h2>
2026-01-24 22:59:20 +00:00
<p class="text-xs text-white/60">v{{ version }}</p>
</div>
</div>
<nav class="space-y-2">
<RouterLink
v-for="item in desktopNavItems"
:key="item.path"
:to="item.path"
class="flex items-center gap-3 px-4 py-3 rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition-colors"
exact-active-class="nav-tab-active"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in getIconPath(item.icon)"
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
/>
</svg>
<span>{{ item.label }}</span>
</RouterLink>
</nav>
</div>
<!-- Controller indicator - Desktop sidebar -->
<div class="px-6 pb-2">
<ControllerIndicator />
</div>
2026-01-24 22:59:20 +00:00
<!-- User Section - Desktop Only -->
<div class="p-6">
<button
@click="handleLogout"
class="w-full flex items-center gap-3 px-4 py-3 rounded-lg text-white/80 hover:bg-white/10 hover:text-white transition-colors"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
<span>Logout</span>
</button>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 overflow-hidden relative pb-20 md:pb-0">
<!-- Connection Status Banner -->
<div v-if="isOffline && !store.isReconnecting && store.isAuthenticated" class="path-option-card mx-6 mt-6 px-6 py-3 border-l-4 border-yellow-500">
<div class="flex items-center gap-2 text-yellow-200">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span class="font-medium">
{{ isRestarting ? 'Server is restarting...' : isShuttingDown ? 'Server is shutting down...' : 'Connection lost' }}
</span>
</div>
</div>
<!-- Reconnecting Banner -->
<div v-if="store.isReconnecting && store.isAuthenticated" class="path-option-card mx-6 mt-6 px-6 py-3 border-l-4 border-blue-500">
<div class="flex items-center gap-2 text-blue-200">
<svg class="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span class="font-medium">Reconnecting...</span>
</div>
</div>
<!-- New message toast (top right, glassmorphic) -->
<Teleport to="body">
<Transition name="toast">
<div
v-if="toastMessage.show"
@click="messageToast.dismissToastAndOpenMessages"
class="fixed top-20 right-4 left-4 z-[100] w-auto max-w-md cursor-pointer rounded-xl p-4 transition-all hover:border-white/30 hover:shadow-2xl md:top-6 md:right-6 md:left-auto md:max-w-md toast-glass"
>
<div class="flex items-start gap-3">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-orange-500/20">
<svg class="h-5 w-5 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-white">New message</p>
<p class="mt-0.5 text-sm text-white/70 line-clamp-2">{{ toastMessage.text }}</p>
<p class="mt-1 text-xs text-orange-400">Click to view</p>
</div>
</div>
</div>
</Transition>
</Teleport>
2026-01-24 22:59:20 +00:00
<!-- Persistent Mobile Tabs for Apps/Marketplace -->
<div
v-if="showAppsTabs"
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${appsTabIndicatorLeft}px`,
width: `${appsTabIndicatorWidth}px`,
}"
></div>
<RouterLink
ref="appsTabRef"
to="/dashboard/apps"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/'))
}"
>
My Apps
</RouterLink>
<RouterLink
ref="marketplaceTabRef"
to="/dashboard/marketplace"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/'))
}"
>
App Store
</RouterLink>
</div>
</div>
<!-- Persistent Mobile Tabs for Network/Cloud -->
<div
v-if="showNetworkTabs"
class="md:hidden fixed top-0 left-0 right-0 z-40 px-4 pt-4 pb-2"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); transform: translateZ(0);"
:style="{ top: showAppsTabs ? '80px' : '0' }"
>
<div class="glass-card p-2 rounded-lg flex gap-2 relative">
<!-- Animated Active Indicator -->
<div
class="absolute top-2 bottom-2 rounded-lg bg-white/20 transition-all duration-300 ease-out"
:style="{
left: `${networkTabIndicatorLeft}px`,
width: `${networkTabIndicatorWidth}px`,
}"
></div>
<RouterLink
ref="cloudTabRef"
to="/dashboard/cloud"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/'))
}"
>
Cloud
</RouterLink>
<RouterLink
ref="serverTabRef"
to="/dashboard/server"
class="flex-1 px-4 py-2 rounded-lg font-medium transition-all duration-300 text-center relative z-10"
:class="{
'bg-white/20 text-white': route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'),
'text-white/60 hover:text-white/80': !(route.path === '/dashboard/server' || route.path.startsWith('/dashboard/server/'))
}"
>
Network
</RouterLink>
</div>
</div>
<div class="perspective-container-wrapper" :class="{ 'pt-40': showAppsTabs && showNetworkTabs, 'pt-20': showAppsTabs !== showNetworkTabs }">
<div class="perspective-container">
<RouterView v-slot="{ Component, route }">
<Transition :name="getTransitionName(route)">
<div :key="route.path" class="view-wrapper">
<div
:class="[
'px-4 pt-4 pb-4 md:px-8 md:pt-8 md:pb-8 overflow-y-auto h-full',
2026-01-24 22:59:20 +00:00
needsMobileBackButtonSpace
? 'pb-[calc(var(--mobile-tab-bar-height,_72px)+96px)] md:pb-8'
2026-01-24 22:59:20 +00:00
: undefined
]"
>
<component :is="Component" class="view-container" />
</div>
</div>
</Transition>
</RouterView>
</div>
</div>
</main>
<!-- Mobile Bottom Tab Bar -->
<nav
ref="mobileTabBar"
data-mobile-tab-bar
class="md:hidden fixed bottom-0 left-0 right-0 border-t border-glass-border shadow-glass z-50"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);"
>
<div class="flex justify-around items-center px-2 py-3 relative">
<RouterLink
v-for="item in mobileNavItems"
:key="item.path"
:to="item.path"
class="flex items-center justify-center w-full py-3 rounded-lg text-white/70 transition-all duration-300 relative z-10"
:class="{
'nav-tab-active': item.isCombined
? (item.path === '/dashboard/apps'
? (route.path.includes('/apps') || route.path.includes('/marketplace'))
: (route.path.includes('/cloud') || route.path.includes('/server')))
: undefined
}"
:exact-active-class="item.isCombined ? undefined : 'nav-tab-active'"
>
<svg class="w-7 h-7 transition-all duration-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
v-for="(path, index) in getIconPath(item.icon)"
:key="index"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
:d="path"
/>
</svg>
</RouterLink>
</div>
</nav>
</div>
</template>
<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { RouterLink, RouterView, useRouter, useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useMessageToast } from '@/composables/useMessageToast'
import AnimatedLogo from '@/components/AnimatedLogo.vue'
import ControllerIndicator from '@/components/ControllerIndicator.vue'
2026-01-24 22:59:20 +00:00
const router = useRouter()
const messageToast = useMessageToast()
const toastMessage = messageToast.toastMessage
2026-01-24 22:59:20 +00:00
const route = useRoute()
const store = useAppStore()
// Background swap for app details, Web5, Network, Settings, My Apps, App Store, Cloud, and Home
const showAltBackground = ref(false)
const showHomeBackground = ref(route.path === '/dashboard' || route.path === '/dashboard/')
const showWeb5Background = ref(route.path.includes('/dashboard/web5'))
const showNetworkBackground = ref(route.path.includes('/dashboard/server'))
const showSettingsBackground = ref(route.path.includes('/dashboard/settings'))
const showMyAppsBackground = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
const showAppStoreBackground = ref(route.path.includes('/dashboard/marketplace'))
const showCloudBackground = ref(route.path.includes('/dashboard/cloud'))
const showWeb5Overlay = ref(route.path.includes('/dashboard/web5')) // Separate ref for overlay to handle transition delay
const showNetworkOverlay = ref(route.path.includes('/dashboard/server'))
const showSettingsOverlay = ref(route.path.includes('/dashboard/settings'))
const showMyAppsOverlay = ref(route.path.includes('/dashboard/apps') && !route.path.includes('/dashboard/apps/'))
const showAppStoreOverlay = ref(route.path.includes('/dashboard/marketplace'))
const showCloudOverlay = ref(route.path.includes('/dashboard/cloud'))
const isGlitching = ref(false)
// Background images
const currentBackgroundImage = computed(() => {
if (showWeb5Background.value) return 'bg-web5.jpg'
if (showNetworkBackground.value) return 'bg-network.jpg'
if (showSettingsBackground.value) return 'bg-settings.jpg'
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
if (showCloudBackground.value) return 'bg-cloud.jpg'
if (showHomeBackground.value) return 'bg-home.jpg'
return 'bg-4.jpg'
})
const altBackgroundImage = computed(() => {
if (showWeb5Background.value) return 'bg-web5.jpg'
if (showNetworkBackground.value) return 'bg-network.jpg'
if (showSettingsBackground.value) return 'bg-settings.jpg'
if (showMyAppsBackground.value) return 'bg-myapps.jpg'
if (showAppStoreBackground.value) return 'bg-appstore.jpg'
if (showCloudBackground.value) return 'bg-cloud.jpg'
if (showHomeBackground.value) return 'bg-home.jpg'
return 'bg-3.jpg'
})
// Check if overlay should be dark (0.8 opacity)
const showDarkOverlay = computed(() => {
return showWeb5Overlay.value || showNetworkOverlay.value || showSettingsOverlay.value || showMyAppsOverlay.value || showAppStoreOverlay.value || showCloudOverlay.value
})
const mobileTabBar = ref<HTMLElement | null>(null)
const appsTabRef = ref<HTMLElement | null>(null)
const marketplaceTabRef = ref<HTMLElement | null>(null)
const appsTabIndicatorLeft = ref(0)
const appsTabIndicatorWidth = ref(0)
const serverTabRef = ref<HTMLElement | null>(null)
const cloudTabRef = ref<HTMLElement | null>(null)
const networkTabIndicatorLeft = ref(0)
const networkTabIndicatorWidth = ref(0)
function isDetailRoute(path: string) {
return (path.includes('/apps/') && !path.endsWith('/apps')) ||
(path.includes('/marketplace/') && !path.endsWith('/marketplace'))
}
watch(() => route.path, (newPath) => {
// Check if we're on app details OR marketplace app details
const isAppDetails = isDetailRoute(newPath)
const wasAppDetails = showAltBackground.value
// Check if we're on special background routes
const isHome = newPath === '/dashboard' || newPath === '/dashboard/'
const isWeb5 = newPath.includes('/dashboard/web5')
const wasWeb5 = showWeb5Background.value
const isNetwork = newPath.includes('/dashboard/server')
const wasNetwork = showNetworkBackground.value
const isSettings = newPath.includes('/dashboard/settings')
const wasSettings = showSettingsBackground.value
const isMyApps = newPath.includes('/dashboard/apps') && !newPath.includes('/dashboard/apps/')
const wasMyApps = showMyAppsBackground.value
const isAppStore = newPath.includes('/dashboard/marketplace')
const wasAppStore = showAppStoreBackground.value
const isCloud = newPath.includes('/dashboard/cloud')
const wasCloud = showCloudBackground.value
// Change background immediately
showAltBackground.value = isAppDetails
showHomeBackground.value = isHome
showWeb5Background.value = isWeb5
showNetworkBackground.value = isNetwork
showSettingsBackground.value = isSettings
showMyAppsBackground.value = isMyApps
showAppStoreBackground.value = isAppStore
showCloudBackground.value = isCloud
// Handle overlay transitions with delay when leaving special backgrounds
// Web5 overlay
if (isWeb5) {
showWeb5Overlay.value = true
} else if (wasWeb5 && !isWeb5) {
setTimeout(() => {
showWeb5Overlay.value = false
}, 450)
} else {
showWeb5Overlay.value = isWeb5
}
// Network overlay
if (isNetwork) {
showNetworkOverlay.value = true
} else if (wasNetwork && !isNetwork) {
setTimeout(() => {
showNetworkOverlay.value = false
}, 450)
} else {
showNetworkOverlay.value = isNetwork
}
// Settings overlay
if (isSettings) {
showSettingsOverlay.value = true
} else if (wasSettings && !isSettings) {
setTimeout(() => {
showSettingsOverlay.value = false
}, 450)
} else {
showSettingsOverlay.value = isSettings
}
// My Apps overlay
if (isMyApps) {
showMyAppsOverlay.value = true
} else if (wasMyApps && !isMyApps) {
setTimeout(() => {
showMyAppsOverlay.value = false
}, 450)
} else {
showMyAppsOverlay.value = isMyApps
}
// App Store overlay
if (isAppStore) {
showAppStoreOverlay.value = true
} else if (wasAppStore && !isAppStore) {
setTimeout(() => {
showAppStoreOverlay.value = false
}, 450)
} else {
showAppStoreOverlay.value = isAppStore
}
// Cloud overlay
if (isCloud) {
showCloudOverlay.value = true
} else if (wasCloud && !isCloud) {
setTimeout(() => {
showCloudOverlay.value = false
}, 450)
} else {
showCloudOverlay.value = isCloud
}
// Trigger glitch ONLY when going forward (to app details), not back
if (isAppDetails && !wasAppDetails) {
setTimeout(() => {
isGlitching.value = true
setTimeout(() => {
isGlitching.value = false
}, 375) // Glitch duration - 25% faster
}, 500) // Wait for background 3D transition to complete
}
})
const needsMobileBackButtonSpace = computed(() => isDetailRoute(route.path))
// Show persistent tabs for Apps/Marketplace on mobile
const showAppsTabs = computed(() => {
if (typeof window === 'undefined') return false
if (window.innerWidth >= 768) return false
return route.path.includes('/apps') || route.path.includes('/marketplace')
})
// Show persistent tabs for Network/Cloud on mobile
const showNetworkTabs = computed(() => {
if (typeof window === 'undefined') return false
if (window.innerWidth >= 768) return false
return route.path.includes('/server') || route.path.includes('/cloud')
})
function updateTabBarHeight() {
if (typeof window === 'undefined') return
if (mobileTabBar.value) {
const height = mobileTabBar.value.offsetHeight
document.documentElement.style.setProperty('--mobile-tab-bar-height', `${height}px`)
}
}
function updateAppsTabIndicator() {
if (typeof window === 'undefined' || window.innerWidth >= 768) return
const isAppsActive = route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')
const activeTab = isAppsActive ? appsTabRef.value : marketplaceTabRef.value
if (!activeTab) return
const container = activeTab.parentElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const tabRect = activeTab.getBoundingClientRect()
appsTabIndicatorLeft.value = tabRect.left - containerRect.left
appsTabIndicatorWidth.value = tabRect.width
}
function updateNetworkTabIndicator() {
if (typeof window === 'undefined' || window.innerWidth >= 768) return
const isCloudActive = route.path === '/dashboard/cloud' || route.path.startsWith('/dashboard/cloud/')
const activeTab = isCloudActive ? cloudTabRef.value : serverTabRef.value
if (!activeTab) return
const container = activeTab.parentElement
if (!container) return
const containerRect = container.getBoundingClientRect()
const tabRect = activeTab.getBoundingClientRect()
networkTabIndicatorLeft.value = tabRect.left - containerRect.left
networkTabIndicatorWidth.value = tabRect.width
}
onMounted(() => {
updateTabBarHeight()
messageToast.startPolling()
2026-01-24 22:59:20 +00:00
updateAppsTabIndicator()
updateNetworkTabIndicator()
window.addEventListener('resize', () => {
updateTabBarHeight()
updateAppsTabIndicator()
updateNetworkTabIndicator()
})
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateTabBarHeight)
messageToast.stopPolling()
2026-01-24 22:59:20 +00:00
})
// Watch route changes to update indicator position
watch(() => route.path, () => {
nextTick(() => {
updateAppsTabIndicator()
updateNetworkTabIndicator()
})
})
const serverName = computed(() => store.serverName)
const version = computed(() => store.serverInfo?.version || '0.0.0')
const isOffline = computed(() => store.isOffline)
const isRestarting = computed(() => store.isRestarting)
const isShuttingDown = computed(() => store.isShuttingDown)
// Desktop navigation items
const desktopNavItems = [
{
path: '/dashboard',
label: 'Home',
icon: 'home',
},
{
path: '/dashboard/apps',
label: 'My Apps',
icon: 'apps',
},
{
path: '/dashboard/marketplace',
label: 'App Store',
icon: 'marketplace',
},
{
path: '/dashboard/cloud',
label: 'Cloud',
icon: 'cloud',
},
{
path: '/dashboard/server',
label: 'Network',
icon: 'server',
},
{
path: '/dashboard/web5',
label: 'Web5',
icon: 'web5',
},
{
path: '/dashboard/settings',
label: 'Settings',
icon: 'settings',
},
]
// Mobile navigation items (Apps and App Store combined)
const mobileNavItems = [
{
path: '/dashboard',
label: 'Home',
icon: 'home',
},
{
path: '/dashboard/apps',
label: 'Apps',
icon: 'apps',
isCombined: true, // This combines apps and marketplace
},
{
path: '/dashboard/cloud',
label: 'Network',
icon: 'server',
isCombined: true, // This combines server and cloud
},
{
path: '/dashboard/web5',
label: 'Web5',
icon: 'web5',
},
{
path: '/dashboard/settings',
label: 'Settings',
icon: 'settings',
},
]
// Use appropriate nav items based on screen size
// @ts-ignore - Computed kept for future use
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const navItems = computed(() => {
if (typeof window === 'undefined') return desktopNavItems
return window.innerWidth >= 768 ? desktopNavItems : mobileNavItems
})
function getIconPath(iconName: string): string[] {
const icons: Record<string, string[]> = {
home: ['M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6'],
apps: ['M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z'],
marketplace: ['M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z'],
cloud: ['M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'],
server: ['M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01'],
web5: ['M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'],
settings: [
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
'M15 12a3 3 0 11-6 0 3 3 0 016 0z',
],
}
return icons[iconName] || []
}
async function handleLogout() {
await store.logout()
router.push('/login')
}
// Track previous route for transition logic
let previousPath = ''
// Tab order for vertical transitions
const tabOrder = [
'/dashboard',
'/dashboard/apps',
'/dashboard/marketplace',
'/dashboard/cloud',
'/dashboard/server',
'/dashboard/web5',
'/dashboard/settings'
]
// Determine transition direction based on route depth
function getTransitionName(currentRoute: any) {
const currentPath = currentRoute.path
// If no previous path, use fade transition for initial load
if (!previousPath) {
previousPath = currentPath
return 'fade'
}
const isAppDetails = currentPath.includes('/apps/') && !currentPath.endsWith('/apps')
const isAppsList = currentPath === '/dashboard/apps'
const wasAppDetails = previousPath.includes('/apps/') && !previousPath.endsWith('/apps')
const wasAppsList = previousPath === '/dashboard/apps'
// Marketplace detail transitions
const isMarketplaceDetails = currentPath.includes('/marketplace/') && !currentPath.endsWith('/marketplace')
const isMarketplaceList = currentPath === '/dashboard/marketplace'
const wasMarketplaceDetails = previousPath.includes('/marketplace/') && !previousPath.endsWith('/marketplace')
const wasMarketplaceList = previousPath === '/dashboard/marketplace'
// Cloud folder transitions
const isCloudFolder = currentPath.includes('/cloud/') && !currentPath.endsWith('/cloud')
const isCloudList = currentPath === '/dashboard/cloud'
const wasCloudFolder = previousPath.includes('/cloud/') && !previousPath.endsWith('/cloud')
const wasCloudList = previousPath === '/dashboard/cloud'
let transitionName = 'fade'
// Mobile: Horizontal slide transitions between Apps and Marketplace (check first on mobile)
if (typeof window !== 'undefined' && window.innerWidth < 768) {
// From Marketplace to Apps: slide right
if (wasMarketplaceList && isAppsList) {
transitionName = 'slide-right'
}
// From Apps to Marketplace: slide left
else if (wasAppsList && isMarketplaceList) {
transitionName = 'slide-left'
}
// From Network to Cloud: slide right
else if (previousPath === '/dashboard/server' && isCloudList) {
transitionName = 'slide-right'
}
// From Cloud to Network: slide left
else if (wasCloudList && currentPath === '/dashboard/server') {
transitionName = 'slide-left'
}
// Vertical transition: between main tabs (mobile fallback)
else {
const currentIndex = tabOrder.indexOf(currentPath)
const previousIndex = tabOrder.indexOf(previousPath)
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
// Moving down the menu (visual down)
if (currentIndex > previousIndex) {
transitionName = 'slide-down'
}
// Moving up the menu (visual up)
else {
transitionName = 'slide-up'
}
}
}
}
// Horizontal depth transition: apps list <-> app details
else if (wasAppsList && isAppDetails) {
transitionName = 'depth-forward'
} else if (wasAppDetails && isAppsList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: marketplace list <-> marketplace details
else if (wasMarketplaceList && isMarketplaceDetails) {
transitionName = 'depth-forward'
} else if (wasMarketplaceDetails && isMarketplaceList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: cloud list <-> cloud folder
else if (wasCloudList && isCloudFolder) {
transitionName = 'depth-forward'
} else if (wasCloudFolder && isCloudList) {
transitionName = 'depth-back'
}
// Horizontal depth transition: marketplace list <-> installed app details
// (when clicking installed app from marketplace)
else if (wasMarketplaceList && isAppDetails) {
transitionName = 'depth-forward'
} else if (wasAppDetails && isMarketplaceList) {
transitionName = 'depth-back'
}
// Vertical transition: between main tabs (desktop)
else {
const currentIndex = tabOrder.indexOf(currentPath)
const previousIndex = tabOrder.indexOf(previousPath)
if (currentIndex !== -1 && previousIndex !== -1 && currentIndex !== previousIndex) {
// Moving down the menu (visual down)
if (currentIndex > previousIndex) {
transitionName = 'slide-down'
}
// Moving up the menu (visual up)
else {
transitionName = 'slide-up'
}
}
}
// Update previous path for next transition
previousPath = currentPath
return transitionName
}
</script>
<style>
/* Wrapper to contain perspective without clipping */
.perspective-container-wrapper {
position: relative;
overflow: hidden;
height: 100%;
}
/* Perspective container for 3D depth effect */
.perspective-container {
perspective: 2000px;
perspective-origin: 50% 50%;
position: relative;
height: 100%;
overflow: hidden;
}
/* View wrapper - allows smooth transitions with absolute positioning */
.view-wrapper {
position: absolute;
inset: 0;
transform-style: preserve-3d;
backface-visibility: hidden;
will-change: transform, opacity;
opacity: 1;
}
.view-container {
height: 100%;
min-height: 100%;
}
/* Forward transition: Current screen pulls forward, new screen emerges from back */
.depth-forward-enter-active.view-wrapper,
.depth-forward-leave-active.view-wrapper {
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.depth-forward-enter-from.view-wrapper {
opacity: 0;
transform: translateZ(-800px) scale(0.75);
filter: blur(4px);
}
.depth-forward-enter-to.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-from.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-forward-leave-to.view-wrapper {
opacity: 0;
transform: translateZ(400px) scale(1.2);
filter: blur(8px);
}
/* Back transition: Current screen pulls back, previous screen comes forward */
.depth-back-enter-active.view-wrapper,
.depth-back-leave-active.view-wrapper {
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.depth-back-enter-from.view-wrapper {
opacity: 0;
transform: translateZ(400px) scale(1.2);
filter: blur(8px);
}
.depth-back-enter-to.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-back-leave-from.view-wrapper {
opacity: 1;
transform: translateZ(0) scale(1);
filter: blur(0px);
}
.depth-back-leave-to.view-wrapper {
opacity: 0;
transform: translateZ(-800px) scale(0.75);
filter: blur(4px);
}
/* Enhanced effect with rotation for more console-like feel */
@media (min-width: 768px) {
.depth-forward-enter-from.view-wrapper {
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
}
.depth-forward-leave-to.view-wrapper {
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
}
.depth-back-enter-from.view-wrapper {
transform: translateZ(400px) scale(1.2) rotateX(-5deg);
}
.depth-back-leave-to.view-wrapper {
transform: translateZ(-800px) scale(0.75) rotateX(8deg);
}
}
/* Fade transition for initial loads and default cases */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
}
/* Mobile: Slide left transition (Apps -> Marketplace) */
.slide-left-enter-active.view-wrapper,
.slide-left-leave-active.view-wrapper {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-left-enter-from.view-wrapper {
transform: translateX(100%);
}
.slide-left-enter-to.view-wrapper {
transform: translateX(0);
}
.slide-left-leave-from.view-wrapper {
transform: translateX(0);
}
.slide-left-leave-to.view-wrapper {
transform: translateX(-100%);
}
/* Mobile: Slide right transition (Marketplace -> Apps) */
.slide-right-enter-active.view-wrapper,
.slide-right-leave-active.view-wrapper {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-right-enter-from.view-wrapper {
transform: translateX(-100%);
}
.slide-right-enter-to.view-wrapper {
transform: translateX(0);
}
.slide-right-leave-from.view-wrapper {
transform: translateX(0);
}
.slide-right-leave-to.view-wrapper {
transform: translateX(100%);
}
/* Slide down: Moving down the menu (content slides up like a scroll) */
.slide-down-enter-active.view-wrapper {
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide-down-leave-active.view-wrapper {
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-down-enter-from.view-wrapper {
opacity: 0;
transform: translateY(40vh);
}
.slide-down-enter-to.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-down-leave-from.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-down-leave-to.view-wrapper {
opacity: 0;
transform: translateY(-30vh);
}
/* Slide up: Moving up the menu (content slides down like a scroll) */
.slide-up-enter-active.view-wrapper {
transition: all 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide-up-leave-active.view-wrapper {
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94),
opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-up-enter-from.view-wrapper {
opacity: 0;
transform: translateY(-40vh);
}
.slide-up-enter-to.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-up-leave-from.view-wrapper {
opacity: 1;
transform: translateY(0);
}
.slide-up-leave-to.view-wrapper {
opacity: 0;
transform: translateY(30vh);
}
/* Background 3D container */
.bg-perspective-container {
position: fixed;
inset: 0;
z-index: -10;
perspective: 1000px;
perspective-origin: 50% 50%;
overflow: hidden;
}
/* Background layers with 3D transitions */
.bg-layer {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.265, 1.55);
transform-style: preserve-3d;
will-change: transform, opacity;
}
/* Default state - bg-4 visible, bg-3 hidden back */
.bg-layer:first-child {
opacity: 1;
transform: translateZ(0) scale(1);
}
.bg-layer:nth-child(2) {
opacity: 0;
transform: translateZ(-200px) scale(0.9) rotateY(-15deg);
}
/* Transitioning out - current background moves away with zoom */
.bg-layer.bg-transitioning-out {
opacity: 0;
transform: translateZ(200px) scale(1.15) rotateY(15deg) !important;
}
/* Transitioning in - new background comes forward with zoom */
.bg-layer.bg-transitioning-in {
opacity: 1;
transform: translateZ(0) scale(1.05) rotateY(0deg) !important;
}
/* Background glitch effect layers - World Fair style */
.bg-glitch-layer-1,
.bg-glitch-layer-2,
.bg-glitch-scan {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 10;
opacity: 0;
}
.bg-glitch-layer-1 {
background-size: cover;
background-position: center;
mix-blend-mode: lighten;
filter: brightness(1.8) contrast(2) saturate(1.5) hue-rotate(180deg);
will-change: transform, clip-path, opacity;
}
.bg-glitch-layer-2 {
background-size: cover;
background-position: center;
mix-blend-mode: color-dodge;
filter: brightness(2) contrast(2) saturate(2) hue-rotate(90deg);
will-change: transform, clip-path, opacity;
}
.bg-glitch-scan {
background:
linear-gradient(90deg,
rgba(255,0,255,0.2) 0%,
rgba(0,255,255,0.2) 25%,
rgba(255,255,0,0.2) 50%,
rgba(0,255,255,0.2) 75%,
rgba(255,0,255,0.2) 100%
),
repeating-linear-gradient(0deg,
rgba(255,255,255,0.05) 0px,
rgba(255,255,255,0.05) 2px,
transparent 2px,
transparent 4px
);
will-change: transform, opacity;
}
/* Trigger glitch animation when active */
.bg-glitch-layer-1.glitch-active {
animation: bg-glitch-shift 0.375s steps(15, end) forwards;
}
.bg-glitch-layer-2.glitch-active {
animation: bg-glitch-shift-2 0.375s steps(12, end) forwards;
}
.bg-glitch-scan.glitch-active {
animation: bg-glitch-scan 0.375s linear forwards;
}
/* World Fair style - visible but tasteful glitch */
@keyframes bg-glitch-shift {
0% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
5% { opacity: 0.5; }
12% { transform: translate(15px,-8px); clip-path: inset(12% 0 70% 0); }
20% { transform: translate(-20px,10px); clip-path: inset(45% 0 35% 0); }
28% { transform: translate(18px,-5px); clip-path: inset(68% 0 15% 0); }
36% { transform: translate(-15px,12px); clip-path: inset(20% 0 60% 0); }
44% { transform: translate(22px,-10px); clip-path: inset(52% 0 28% 0); }
52% { transform: translate(-18px,8px); clip-path: inset(10% 0 75% 0); }
60% { transform: translate(12px,-6px); clip-path: inset(58% 0 22% 0); }
68% { transform: translate(-10px,15px); clip-path: inset(32% 0 48% 0); }
76% { transform: translate(16px,-4px); clip-path: inset(72% 0 12% 0); }
84% { transform: translate(-12px,7px); clip-path: inset(18% 0 65% 0); }
92% { transform: translate(8px,-3px); clip-path: inset(42% 0 40% 0); }
96% { opacity: 0.4; }
100% { transform: translate(0,0); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes bg-glitch-shift-2 {
0% { transform: translate(0,0) skewX(0deg); clip-path: inset(0% 0 0 0); opacity: 0; }
8% { opacity: 0.5; }
15% { transform: translate(-18px,10px) skewX(4deg); clip-path: inset(25% 0 55% 0); }
23% { transform: translate(22px,-12px) skewX(-5deg); clip-path: inset(50% 0 30% 0); }
31% { transform: translate(-16px,8px) skewX(3deg); clip-path: inset(72% 0 12% 0); }
39% { transform: translate(20px,-15px) skewX(-4deg); clip-path: inset(18% 0 65% 0); }
47% { transform: translate(-22px,12px) skewX(5deg); clip-path: inset(42% 0 38% 0); }
55% { transform: translate(18px,-8px) skewX(-3deg); clip-path: inset(62% 0 20% 0); }
63% { transform: translate(-14px,14px) skewX(4deg); clip-path: inset(30% 0 52% 0); }
71% { transform: translate(16px,-6px) skewX(-2deg); clip-path: inset(8% 0 78% 0); }
79% { transform: translate(-12px,10px) skewX(3deg); clip-path: inset(55% 0 28% 0); }
87% { transform: translate(10px,-4px) skewX(-2deg); clip-path: inset(35% 0 45% 0); }
95% { opacity: 0.4; }
100% { transform: translate(0,0) skewX(0deg); clip-path: inset(0% 0 0 0); opacity: 0; }
}
@keyframes bg-glitch-scan {
0% { opacity: 0; transform: translateX(-120%); }
5% { opacity: 0.5; }
15% { opacity: 0.55; transform: translateX(-80%); }
30% { opacity: 0.6; transform: translateX(-40%); }
50% { opacity: 0.6; transform: translateX(0%); }
70% { opacity: 0.55; transform: translateX(40%); }
85% { opacity: 0.5; transform: translateX(80%); }
95% { opacity: 0.45; }
100% { opacity: 0; transform: translateX(120%); }
}
</style>