Files
SanatiLeads/components/app-sidebar.tsx

337 lines
15 KiB
TypeScript

"use client"
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import {
Command,
Frame,
HelpCircle,
Settings2,
Terminal,
Target,
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 { useProject } from "@/components/project-context"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname()
const projects = useQuery(api.projects.getProjects);
const { selectedProjectId, setSelectedProjectId } = useProject();
const addDataSource = useMutation(api.dataSources.addDataSource);
const updateDataSourceStatus = useMutation(api.dataSources.updateDataSourceStatus);
const createAnalysis = useMutation(api.analyses.createAnalysis);
const [isAdding, setIsAdding] = React.useState(false);
const [sourceUrl, setSourceUrl] = React.useState("");
const [sourceName, setSourceName] = React.useState("");
const [sourceError, setSourceError] = React.useState<string | null>(null);
const [isSubmittingSource, setIsSubmittingSource] = React.useState(false);
// 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
});
};
const handleAddSource = async () => {
if (!sourceUrl) {
setSourceError("Please enter a URL.");
return;
}
setSourceError(null);
setIsSubmittingSource(true);
try {
const { sourceId, projectId } = await addDataSource({
projectId: selectedProjectId as any,
url: sourceUrl,
name: sourceName || sourceUrl,
type: "website",
});
await updateDataSourceStatus({
dataSourceId: sourceId,
analysisStatus: "pending",
lastError: undefined,
lastAnalyzedAt: undefined,
});
const response = await fetch("/api/analyze", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: sourceUrl }),
});
const data = await response.json();
if (!response.ok) {
await updateDataSourceStatus({
dataSourceId: sourceId,
analysisStatus: "failed",
lastError: data.error || "Analysis failed",
lastAnalyzedAt: Date.now(),
});
throw new Error(data.error || "Analysis failed");
}
await createAnalysis({
projectId,
dataSourceId: sourceId,
analysis: data.data,
});
await updateDataSourceStatus({
dataSourceId: sourceId,
analysisStatus: "completed",
lastError: undefined,
lastAnalyzedAt: Date.now(),
});
setSourceUrl("");
setSourceName("");
setIsAdding(false);
} catch (err: any) {
setSourceError(err?.message || "Failed to add source.");
} finally {
setIsSubmittingSource(false);
}
};
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">Sanati</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
asChild
tooltip="Dashboard"
isActive={pathname === "/dashboard"}
>
<Link href="/dashboard">
<Terminal />
<span>Dashboard</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Opportunities"
isActive={pathname === "/opportunities"}
>
<Link href="/opportunities">
<Target />
<span>Opportunities</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Settings"
isActive={pathname === "/settings"}
>
<Link href="/settings">
<Settings2 />
<span>Settings</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Help"
isActive={pathname === "/help"}
>
<Link href="/help">
<HelpCircle />
<span>Help</span>
</Link>
</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>
))}
<Button
variant="outline"
size="sm"
onClick={() => setIsAdding(true)}
>
Add Data Source
</Button>
</div>
</SidebarGroupContent>
</SidebarGroup>
)}
</SidebarContent>
<SidebarFooter>
<NavUser user={{
name: "User",
email: "user@example.com",
avatar: ""
}} />
</SidebarFooter>
<Dialog open={isAdding} onOpenChange={setIsAdding}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Data Source</DialogTitle>
<DialogDescription>
Add a website to analyze for this project.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="sourceUrl">Website URL</Label>
<Input
id="sourceUrl"
placeholder="https://example.com"
value={sourceUrl}
onChange={(event) => setSourceUrl(event.target.value)}
disabled={isSubmittingSource}
/>
</div>
<div className="space-y-2">
<Label htmlFor="sourceName">Name (optional)</Label>
<Input
id="sourceName"
placeholder="Product name"
value={sourceName}
onChange={(event) => setSourceName(event.target.value)}
disabled={isSubmittingSource}
/>
</div>
{sourceError && (
<div className="text-sm text-destructive">{sourceError}</div>
)}
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAdding(false)} disabled={isSubmittingSource}>
Cancel
</Button>
<Button onClick={handleAddSource} disabled={isSubmittingSource}>
{isSubmittingSource ? "Analyzing..." : "Add Source"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</Sidebar>
)
}