This commit is contained in:
2026-02-04 01:05:00 +00:00
parent f9222627ef
commit d02d95e680
30 changed files with 2449 additions and 326 deletions

View File

@@ -6,9 +6,7 @@ import { usePathname } from "next/navigation"
import {
Command,
Frame,
HelpCircle,
Settings,
Settings2,
Terminal,
Target,
Plus,
@@ -66,11 +64,16 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const [isSubmittingProject, setIsSubmittingProject] = React.useState(false);
const createProject = useMutation(api.projects.createProject);
const updateProject = useMutation(api.projects.updateProject);
const deleteProject = useMutation(api.projects.deleteProject);
const [isEditingProject, setIsEditingProject] = React.useState(false);
const [editingProjectId, setEditingProjectId] = React.useState<string | null>(null);
const [editingProjectName, setEditingProjectName] = React.useState("");
const [editingProjectDefault, setEditingProjectDefault] = React.useState(false);
const [editingProjectError, setEditingProjectError] = React.useState<string | null>(null);
const [deleteConfirmName, setDeleteConfirmName] = React.useState("");
const [deleteProjectError, setDeleteProjectError] = React.useState<string | null>(null);
const [isDeletingProject, setIsDeletingProject] = React.useState(false);
const [isDeleteConfirmOpen, setIsDeleteConfirmOpen] = React.useState(false);
const [isSubmittingEdit, setIsSubmittingEdit] = React.useState(false);
const [sourceUrl, setSourceUrl] = React.useState("");
const [sourceName, setSourceName] = React.useState("");
@@ -107,6 +110,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const toggleConfig = useMutation(api.projects.toggleDataSourceConfig);
const selectedProject = projects?.find(p => p._id === selectedProjectId);
const editingProject = projects?.find((project) => project._id === editingProjectId);
const canDeleteProject = (projects?.length ?? 0) > 1;
const selectedSourceIds = selectedProject?.dorkingConfig?.selectedSourceIds || [];
const selectedProjectName = selectedProject?.name || "Select Project";
@@ -291,9 +296,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton size="lg">
<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">{selectedProjectName}</span>
<span className="truncate text-xs text-muted-foreground">Projects</span>
@@ -332,6 +334,9 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
setEditingProjectName(project.name);
setEditingProjectDefault(project.isDefault);
setEditingProjectError(null);
setDeleteConfirmName("");
setDeleteProjectError(null);
setIsDeleteConfirmOpen(false);
setIsEditingProject(true);
}}
aria-label={`Project settings for ${project.name}`}
@@ -353,54 +358,41 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<SidebarContent>
{/* Platform Nav */}
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarGroupLabel>Main</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Dashboard"
tooltip="Overview"
isActive={pathname === "/dashboard"}
>
<Link href="/dashboard">
<Terminal />
<span>Dashboard</span>
<span>Overview</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Opportunities"
tooltip="Search"
isActive={pathname === "/opportunities"}
>
<Link href="/opportunities">
<Target />
<span>Opportunities</span>
<span>Search</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
asChild
tooltip="Settings"
isActive={pathname === "/settings"}
tooltip="Inbox"
isActive={pathname === "/leads"}
>
<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 href="/leads" className="pl-8 text-sm text-muted-foreground hover:text-foreground">
<span>Inbox</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -412,7 +404,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
{selectedProjectId && (
<SidebarGroup>
<SidebarGroupLabel>
Active Data Sources
Selected Sources
<span className="ml-2 text-xs font-normal text-muted-foreground">({selectedProject?.name})</span>
</SidebarGroupLabel>
<SidebarGroupContent>
@@ -624,6 +616,33 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
{editingProjectError && (
<div className="text-sm text-destructive">{editingProjectError}</div>
)}
<div className="border-t border-border pt-4 space-y-2">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-semibold text-destructive">Delete project</div>
<p className="text-xs text-muted-foreground">
This removes the project and all related data sources, analyses, and opportunities.
</p>
</div>
<Button
variant="destructive"
size="sm"
disabled={!canDeleteProject}
onClick={() => {
setDeleteConfirmName("");
setDeleteProjectError(null);
setIsDeleteConfirmOpen(true);
}}
>
Delete
</Button>
</div>
{!canDeleteProject && (
<div className="text-xs text-muted-foreground">
You must keep at least one project.
</div>
)}
</div>
<div className="flex justify-end gap-2">
<Button
variant="outline"
@@ -662,6 +681,77 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</div>
</DialogContent>
</Dialog>
<Dialog open={isDeleteConfirmOpen} onOpenChange={setIsDeleteConfirmOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete project</DialogTitle>
<DialogDescription>
This action is permanent. You are deleting{" "}
<span className="font-semibold text-foreground">
{editingProject?.name || "this project"}
</span>
. Type the project name to confirm deletion.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="deleteProjectConfirm">Project name</Label>
<Input
id="deleteProjectConfirm"
value={deleteConfirmName}
onChange={(event) => setDeleteConfirmName(event.target.value)}
disabled={isDeletingProject || !canDeleteProject}
/>
</div>
{deleteProjectError && (
<div className="text-sm text-destructive">{deleteProjectError}</div>
)}
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => setIsDeleteConfirmOpen(false)}
disabled={isDeletingProject}
>
Cancel
</Button>
<Button
variant="destructive"
disabled={
isDeletingProject ||
!canDeleteProject ||
!editingProject ||
deleteConfirmName.trim() !== editingProject.name
}
onClick={async () => {
if (!editingProjectId || !editingProject) return;
if (deleteConfirmName.trim() !== editingProject.name) {
setDeleteProjectError("Project name does not match.");
return;
}
setDeleteProjectError(null);
setIsDeletingProject(true);
try {
const result = await deleteProject({
projectId: editingProjectId as any,
});
if (selectedProjectId === editingProjectId && result?.newDefaultProjectId) {
setSelectedProjectId(result.newDefaultProjectId);
}
setIsDeleteConfirmOpen(false);
setIsEditingProject(false);
} catch (err: any) {
setDeleteProjectError(err?.message || "Failed to delete project.");
} finally {
setIsDeletingProject(false);
}
}}
>
{isDeletingProject ? "Deleting..." : "Delete Project"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={isCreatingProject} onOpenChange={setIsCreatingProject}>
<DialogContent>
<DialogHeader>