Dynavera/site/src/App.vue
2026-02-26 01:32:04 +00:00

331 lines
10 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, type Component } from 'vue'
import { Layout, Menu, Button, Space, Typography, Select } from 'ant-design-vue'
import {
HomeOutlined,
InfoCircleOutlined,
RobotOutlined,
DashboardOutlined,
LoginOutlined,
UserAddOutlined,
BuildOutlined,
PayCircleOutlined,
} from '@ant-design/icons-vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from './stores/userStore'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
type NavItem = {
key: string
label: string
icon: Component
path?: string
manager?: boolean
children?: NavItem[]
}
const navItems: NavItem[] = [
{ key: '/', label: 'Home', icon: HomeOutlined, path: '/' },
{ key: '/about', label: 'About', icon: InfoCircleOutlined, path: '/about' },
{ key: '/pricing', label: 'Pricing', icon: PayCircleOutlined, path: '/pricing' },
{ key: '/agents', label: 'Agents', icon: RobotOutlined, path: '/agents', manager: true },
{ key: '/organization', label: 'Organizations', icon: BuildOutlined, path: '/organization' },
{ key: '/progress', label: 'Progress', icon: DashboardOutlined, path: '/progress' },
]
const visibleNavItems = computed<NavItem[]>(() =>
navItems.filter((item) => (item.manager ? userStore.user?.is_manager : true)),
)
const selectedKeys = computed(() => {
for (const item of visibleNavItems.value) {
if (item.key === '/' && route.path === '/') return [item.key]
if (route.path.startsWith(item.key)) return [item.key]
if (item.children) {
const childMatch = item.children.find((c) => route.path.startsWith(c.key))
if (childMatch) return [item.key]
}
}
return []
})
type SimpleMenuInfo = { key: string | number | Array<string | number> }
const onSelect = (info: SimpleMenuInfo) => {
const key = String(info.key)
let found: NavItem | undefined
for (const item of visibleNavItems.value) {
if (item.key === key) {
found = item
break
}
if (item.children) {
const child = item.children.find((c) => c.key === key)
if (child) {
found = child
break
}
}
}
if (found && found.path && route.path !== found.path) {
const selectedOrgUuid = userStore.userSelectedOrganization?.uuid
if (found.path === '/organization' && selectedOrgUuid) {
router.push(`/organization/${selectedOrgUuid}`)
} else {
router.push(found.path)
}
}
}
const handleLogout = async () => {
await userStore.logout()
router.push('/')
}
onMounted(() => {
userStore.fetchSession()
})
const user = userStore
</script>
<template>
<Layout class="shell">
<Layout.Header class="shell-header">
<div class="brand" @click="route.path !== '/' && router.push('/')">Dynavera</div>
<div style="margin-right: 1rem" v-if="user.isAuthenticated"></div>
<Menu
mode="horizontal"
theme="dark"
:selectedKeys="selectedKeys"
class="shell-menu"
@select="onSelect"
>
<template v-for="item in visibleNavItems" :key="item.key">
<Menu.SubMenu v-if="item.children" :key="`${item.key}-submenu`">
<template #title>
<span
@click.stop="
item.path && route.path !== item.path && router.push(item.path)
"
>
<Space size="small">
<component :is="item.icon" />
<span>{{ item.label }}</span>
</Space>
</span>
</template>
<Menu.Item
v-for="child in item.children"
:key="child.key"
@click="
child.path && route.path !== child.path && router.push(child.path)
"
>
<Space size="small">
<component :is="child.icon" />
<span>{{ child.label }}</span>
</Space>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item
v-else
:key="`${item.key}-item`"
@click="item.path && route.path !== item.path && router.push(item.path)"
>
<Space size="small">
<component :is="item.icon" />
<span>{{ item.label }}</span>
</Space>
</Menu.Item>
</template>
</Menu>
<Space>
<template v-if="user.isAuthenticated">
<Select
v-if="
user.userJoinedOrganizations && user.userJoinedOrganizations.length > 0
"
:value="user.userSelectedOrganization?.uuid ?? undefined"
@change="
(val) => {
const org = user.userJoinedOrganizations.find((o) => o.uuid === val)
user.setSelectedOrganization &&
user.setSelectedOrganization(org ?? null)
}
"
style="min-width: 220px; margin-right: 0.5rem"
placeholder="Select organization"
>
<Select.Option
v-for="o in user.userJoinedOrganizations"
:key="o.uuid"
:value="o.uuid"
>
{{ o.name }}
</Select.Option>
</Select>
<Typography.Text class="user-chip" strong>
{{ user.displayName || 'Account' }}
</Typography.Text>
<Button ghost :loading="user.loading" @click="handleLogout">Logout</Button>
</template>
<template v-else>
<Button ghost @click="router.push('/login')">
<LoginOutlined />
Login
</Button>
<Button type="primary" @click="router.push('/register')">
<UserAddOutlined />
Register
</Button>
</template>
</Space>
</Layout.Header>
<Layout class="shell-body">
<Layout.Content class="shell-content">
<router-view />
</Layout.Content>
<Layout.Footer class="shell-footer">
<Typography.Text type="secondary">
<strong>Project Disclaimer:</strong>
This is a proof-of-concept demo project for educational purposes. All
testimonials, statistics, and company names are fictional placeholders.
</Typography.Text>
</Layout.Footer>
</Layout>
</Layout>
</template>
<style scoped>
.shell {
min-height: 100vh;
background: #0b1220;
}
.shell-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0 1.25rem;
background: #0f172a;
}
.brand {
color: #e5e7eb;
font-weight: 700;
cursor: pointer;
font-size: 1.05rem;
}
.shell-menu {
flex: 1;
background: transparent;
border-bottom: none;
}
.shell-body {
background: #0b1220;
min-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
}
.shell-content {
padding: 24px;
flex: 1;
min-height: calc(100vh - 64px - 64px);
}
.shell-footer {
text-align: center;
background: #0f172a;
}
:deep(.ant-menu-dark) {
background: transparent;
}
:deep(.ant-menu-dark .ant-menu-item-selected) {
background: transparent !important;
}
:deep(.ant-typography),
:deep(.ant-typography p),
:deep(.ant-typography span),
:deep(.ant-list-item),
:deep(.ant-list-item-meta-title),
:deep(.ant-list-item-meta-description),
:deep(.ant-statistic-title),
:deep(.ant-statistic-content),
:deep(.ant-card-meta-title),
:deep(.ant-card-meta-description) {
color: #e5e7eb;
}
:deep(.ant-typography-secondary) {
color: #cbd5e1 !important;
}
:deep(.ant-form-item-label > label) {
color: #e5e7eb;
}
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-select-selection-item),
:deep(.ant-picker-input input) {
background: #111827;
color: #e5e7eb;
border-color: #334155;
}
:deep(.ant-input::placeholder),
:deep(.ant-select-selection-placeholder),
:deep(.ant-picker-input input::placeholder) {
color: #9ca3af;
}
:deep(.ant-card) {
background: #0f172a;
border-color: #1f2937;
}
:deep(.ant-btn:not(.ant-btn-primary)) {
color: #e5e7eb;
border-color: #334155;
background: #111827;
}
:deep(.ant-btn-primary) {
background: linear-gradient(90deg, #6366f1, #8b5cf6);
border: none;
}
.user-chip {
color: #e5e7eb;
}
:deep(.ant-typography-secondary) {
color: #cbd5e1 !important;
}
:deep(.ant-form-item-label > label) {
color: #e5e7eb;
}
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-select-selection-item),
:deep(.ant-picker-input input) {
background: #111827;
color: #e5e7eb;
border-color: #334155;
}
:deep(.ant-input::placeholder),
:deep(.ant-select-selection-placeholder),
:deep(.ant-picker-input input::placeholder) {
color: #9ca3af;
}
:deep(.ant-card) {
background: #0f172a;
border-color: #1f2937;
}
:deep(.ant-btn:not(.ant-btn-primary)) {
color: #e5e7eb;
border-color: #334155;
background: #111827;
}
:deep(.ant-btn-primary) {
background: linear-gradient(90deg, #6366f1, #8b5cf6);
border: none;
}
.user-chip {
color: #e5e7eb;
}
</style>