Init Repo
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
:disabled="isLoading"
|
||||
>
|
||||
<!-- Loading State -->
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
||||
</div>
|
||||
<!-- Error State -->
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-destructive text-destructive-foreground"
|
||||
>
|
||||
<AlertCircle class="size-4" />
|
||||
</div>
|
||||
<!-- Normal State -->
|
||||
<div
|
||||
v-else
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<component :is="activeProject.icon" class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight">
|
||||
<span class="truncate font-semibold">
|
||||
{{ isLoading ? 'Loading...' : error ? 'Error' : activeProject.name }}
|
||||
</span>
|
||||
<span class="truncate text-xs">
|
||||
{{ isLoading ? 'Fetching projects' : error ? 'Failed to load' : activeProject.status }}
|
||||
</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</SidebarMenuButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
class="w-[--reka-dropdown-menu-trigger-width] min-w-56 rounded-lg"
|
||||
align="start"
|
||||
:side="isMobile ? 'bottom' : 'right'"
|
||||
:side-offset="4"
|
||||
>
|
||||
<DropdownMenuLabel class="text-xs text-muted-foreground">
|
||||
Projects
|
||||
</DropdownMenuLabel>
|
||||
|
||||
<!-- Loading State in Dropdown -->
|
||||
<DropdownMenuItem v-if="isLoading" class="gap-2 p-2" disabled>
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-current"></div>
|
||||
<span class="text-muted-foreground">Loading projects...</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- Error State in Dropdown -->
|
||||
<template v-else-if="error">
|
||||
<DropdownMenuItem class="gap-2 p-2" disabled>
|
||||
<AlertCircle class="size-4 text-destructive" />
|
||||
<span class="text-destructive text-sm">{{ error }}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="gap-2 p-2" @click="refreshProjects">
|
||||
<RefreshCw class="size-4" />
|
||||
<span>Retry</span>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
|
||||
<!-- Projects List -->
|
||||
<template v-else>
|
||||
<DropdownMenuItem
|
||||
v-for="(project, index) in projects"
|
||||
:key="project.id"
|
||||
class="gap-2 p-2"
|
||||
@click="setActiveProject(project)"
|
||||
>
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-sm border"
|
||||
>
|
||||
<component :is="project.icon" class="size-4 shrink-0" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ project.name }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{
|
||||
project.status
|
||||
}}</span>
|
||||
</div>
|
||||
<DropdownMenuShortcut>⌘{{ index + 1 }}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem class="gap-2 p-2" @click="refreshProjects">
|
||||
<RefreshCw class="size-4" />
|
||||
<div class="font-medium text-muted-foreground">Refresh projects</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="canCreateProjects"
|
||||
class="gap-2 p-2"
|
||||
@click="handleCreateProject"
|
||||
>
|
||||
<div
|
||||
class="flex size-6 items-center justify-center rounded-md border bg-background"
|
||||
>
|
||||
<Plus class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium text-muted-foreground">Create project</div>
|
||||
</DropdownMenuItem>
|
||||
</template>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ChevronsUpDown, Plus, AlertCircle, RefreshCw } from "lucide-vue-next";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useProjectsStore } from "@/stores/projects";
|
||||
|
||||
const router = useRouter();
|
||||
const { isMobile } = useSidebar();
|
||||
const authStore = useAuthStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
// Get projects and active project from store
|
||||
const projects = computed(() => projectsStore.availableProjects);
|
||||
const activeProject = computed(() => projectsStore.currentProject);
|
||||
const isLoading = computed(() => projectsStore.isLoading);
|
||||
const error = computed(() => projectsStore.error);
|
||||
|
||||
// Check if user can create projects
|
||||
const canCreateProjects = computed(() => {
|
||||
const user = authStore.user;
|
||||
return user?.is_admin || user?.role === "coordinator";
|
||||
});
|
||||
|
||||
const setActiveProject = (project: any) => {
|
||||
projectsStore.setActiveProject(project);
|
||||
|
||||
// Navigate to project-specific view
|
||||
if (project.id === 0) {
|
||||
// "All Projects" view
|
||||
router.push("/projects");
|
||||
} else {
|
||||
// Specific project view - navigate to overview tab
|
||||
router.push(`/projects/${project.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateProject = () => {
|
||||
if (canCreateProjects.value) {
|
||||
router.push("/projects/new");
|
||||
}
|
||||
};
|
||||
|
||||
const refreshProjects = async () => {
|
||||
try {
|
||||
await projectsStore.fetchProjects();
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh projects:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set active project based on current route
|
||||
const updateActiveProjectFromRoute = () => {
|
||||
const currentPath = router.currentRoute.value.path;
|
||||
|
||||
if (currentPath.startsWith("/projects/")) {
|
||||
const pathSegments = currentPath.split("/");
|
||||
if (pathSegments[2] === "new") {
|
||||
// Creating new project, keep current active project
|
||||
return;
|
||||
}
|
||||
|
||||
const projectId = parseInt(pathSegments[2]);
|
||||
if (!isNaN(projectId)) {
|
||||
const project = projectsStore.getProjectById(projectId);
|
||||
if (project) {
|
||||
projectsStore.setActiveProject(project);
|
||||
}
|
||||
}
|
||||
} else if (currentPath === "/projects") {
|
||||
projectsStore.setActiveProject(projectsStore.allProjectsView);
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for route changes
|
||||
watch(
|
||||
() => router.currentRoute.value.path,
|
||||
() => {
|
||||
updateActiveProjectFromRoute();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Watch for authentication changes to fetch projects
|
||||
watch(
|
||||
() => authStore.isAuthenticated,
|
||||
(isAuthenticated) => {
|
||||
if (isAuthenticated && projectsStore.projects.length === 0 && !projectsStore.isLoading) {
|
||||
refreshProjects();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// Ensure projects are loaded on component mount
|
||||
onMounted(() => {
|
||||
if (authStore.isAuthenticated && projectsStore.projects.length === 0 && !projectsStore.isLoading) {
|
||||
refreshProjects();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user