Init Repo

This commit is contained in:
2026-02-28 03:22:04 +08:00
commit de59b57ee7
883 changed files with 156857 additions and 0 deletions
@@ -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>