feat: Implement a new dashboard layout with sidebar, introduce project and data source management, and add various UI components.
This commit is contained in:
169
components/app-sidebar.tsx
Normal file
169
components/app-sidebar.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Command,
|
||||
Frame,
|
||||
Settings2,
|
||||
Terminal,
|
||||
Plus
|
||||
} from "lucide-react"
|
||||
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarGroup,
|
||||
SidebarGroupLabel,
|
||||
SidebarGroupContent,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { useQuery, useMutation } from "convex/react"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const projects = useQuery(api.projects.getProjects);
|
||||
const [selectedProjectId, setSelectedProjectId] = React.useState<string | null>(null);
|
||||
|
||||
// Set default selected project
|
||||
React.useEffect(() => {
|
||||
if (projects && projects.length > 0 && !selectedProjectId) {
|
||||
// Prefer default project, otherwise first
|
||||
const defaultProj = projects.find(p => p.isDefault);
|
||||
setSelectedProjectId(defaultProj ? defaultProj._id : projects[0]._id);
|
||||
}
|
||||
}, [projects, selectedProjectId]);
|
||||
|
||||
// Data Sources Query
|
||||
const dataSources = useQuery(
|
||||
api.dataSources.getProjectDataSources,
|
||||
selectedProjectId ? { projectId: selectedProjectId as any } : "skip"
|
||||
);
|
||||
|
||||
const toggleConfig = useMutation(api.projects.toggleDataSourceConfig);
|
||||
|
||||
const selectedProject = projects?.find(p => p._id === selectedProjectId);
|
||||
const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || [];
|
||||
|
||||
const handleToggle = async (sourceId: string, checked: boolean) => {
|
||||
if (!selectedProjectId) return;
|
||||
await toggleConfig({
|
||||
projectId: selectedProjectId as any,
|
||||
sourceId: sourceId as any,
|
||||
selected: checked
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar variant="inset" {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size="lg" asChild>
|
||||
<a href="#">
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
||||
<Command className="size-4" />
|
||||
</div>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-semibold uppercase">AutoDork</span>
|
||||
<span className="truncate text-xs">Pro</span>
|
||||
</div>
|
||||
</a>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{/* Platform Nav */}
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>Platform</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip="Dashboard" isActive>
|
||||
<Terminal />
|
||||
<span>Dashboard</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton tooltip="Settings">
|
||||
<Settings2 />
|
||||
<span>Settings</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
{/* Projects (Simple List for now, can be switcher) */}
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Projects</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{projects?.map((project) => (
|
||||
<SidebarMenuItem key={project._id}>
|
||||
<SidebarMenuButton
|
||||
onClick={() => setSelectedProjectId(project._id)}
|
||||
isActive={selectedProjectId === project._id}
|
||||
>
|
||||
<Frame className="text-muted-foreground" />
|
||||
<span>{project.name}</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton className="text-muted-foreground">
|
||||
<Plus />
|
||||
<span>Create Project</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
|
||||
{/* Data Sources Config */}
|
||||
{selectedProjectId && (
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
Active Data Sources
|
||||
<span className="ml-2 text-xs font-normal text-muted-foreground">({selectedProject?.name})</span>
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<div className="flex flex-col gap-2 p-2">
|
||||
{(!dataSources || dataSources.length === 0) && (
|
||||
<div className="text-sm text-muted-foreground pl-2">No data sources yet.</div>
|
||||
)}
|
||||
{dataSources?.map((source) => (
|
||||
<div key={source._id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={source._id}
|
||||
checked={selectedSourceIds.includes(source._id)}
|
||||
onCheckedChange={(checked) => handleToggle(source._id, checked === true)}
|
||||
/>
|
||||
<Label htmlFor={source._id} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 truncate cursor-pointer">
|
||||
{source.name || source.url}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<NavUser user={{
|
||||
name: "User",
|
||||
email: "user@example.com",
|
||||
avatar: ""
|
||||
}} />
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user