a
This commit is contained in:
326
components/analysis-section-editor.tsx
Normal file
326
components/analysis-section-editor.tsx
Normal file
@@ -0,0 +1,326 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { useMutation } from "convex/react"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
||||
type SectionKey =
|
||||
| "profile"
|
||||
| "features"
|
||||
| "competitors"
|
||||
| "keywords"
|
||||
| "problems"
|
||||
| "personas"
|
||||
| "useCases"
|
||||
| "dorkQueries"
|
||||
|
||||
function summarizeItem(item: any) {
|
||||
if (typeof item === "string") return item
|
||||
if (!item || typeof item !== "object") return String(item)
|
||||
if (item.name && item.role) return `${item.name} · ${item.role}`
|
||||
if (item.name) return item.name
|
||||
if (item.term) return item.term
|
||||
if (item.problem) return item.problem
|
||||
if (item.scenario) return item.scenario
|
||||
if (item.query) return item.query
|
||||
return JSON.stringify(item)
|
||||
}
|
||||
|
||||
export function SectionEditor({
|
||||
analysisId,
|
||||
sectionKey,
|
||||
title,
|
||||
items,
|
||||
}: {
|
||||
analysisId: string
|
||||
sectionKey: SectionKey
|
||||
title: string
|
||||
items: any[]
|
||||
}) {
|
||||
const addItem = useMutation(api.analysisSections.addItem)
|
||||
const removeItem = useMutation(api.analysisSections.removeItem)
|
||||
|
||||
const [isRepromptOpen, setIsRepromptOpen] = React.useState(false)
|
||||
const [repromptText, setRepromptText] = React.useState("")
|
||||
const [isAddOpen, setIsAddOpen] = React.useState(false)
|
||||
const [addText, setAddText] = React.useState("")
|
||||
const [isBusy, setIsBusy] = React.useState(false)
|
||||
|
||||
const handleAdd = async () => {
|
||||
setIsBusy(true)
|
||||
try {
|
||||
let parsed: any = addText
|
||||
if (addText.trim().startsWith("{") || addText.trim().startsWith("[")) {
|
||||
parsed = JSON.parse(addText)
|
||||
}
|
||||
await addItem({ analysisId: analysisId as any, sectionKey, item: parsed })
|
||||
setAddText("")
|
||||
setIsAddOpen(false)
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReprompt = async () => {
|
||||
setIsBusy(true)
|
||||
try {
|
||||
const response = await fetch("/api/analysis/reprompt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
analysisId,
|
||||
sectionKey,
|
||||
prompt: repromptText.trim() || undefined,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || "Reprompt failed")
|
||||
}
|
||||
setRepromptText("")
|
||||
setIsRepromptOpen(false)
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-3">
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setIsAddOpen(true)}>
|
||||
Add
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setIsRepromptOpen(true)}>
|
||||
Reprompt
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{items.length === 0 ? (
|
||||
<div className="text-muted-foreground">No items yet.</div>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<div
|
||||
key={`${sectionKey}-${index}`}
|
||||
className="flex flex-wrap items-start justify-between gap-2 rounded-md border border-border/60 p-3"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">{summarizeItem(item)}</div>
|
||||
<Badge variant="outline">#{index + 1}</Badge>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeItem({ analysisId: analysisId as any, sectionKey, index })}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<Dialog open={isAddOpen} onOpenChange={setIsAddOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add to {title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste JSON for an item, or plain text for a string entry.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={addText}
|
||||
onChange={(event) => setAddText(event.target.value)}
|
||||
placeholder='{"name":"...", "description":"..."}'
|
||||
className="min-h-[160px]"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsAddOpen(false)} disabled={isBusy}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleAdd} disabled={isBusy || !addText.trim()}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isRepromptOpen} onOpenChange={setIsRepromptOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reprompt {title}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide guidance to regenerate just this section.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={repromptText}
|
||||
onChange={(event) => setRepromptText(event.target.value)}
|
||||
placeholder="Focus on B2B teams in healthcare..."
|
||||
className="min-h-[140px]"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsRepromptOpen(false)} disabled={isBusy}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleReprompt} disabled={isBusy}>
|
||||
Reprompt
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export function ProfileSectionEditor({
|
||||
analysisId,
|
||||
items,
|
||||
}: {
|
||||
analysisId: string
|
||||
items: Record<string, any>
|
||||
}) {
|
||||
const replaceSection = useMutation(api.analysisSections.replaceSection)
|
||||
const [isRepromptOpen, setIsRepromptOpen] = React.useState(false)
|
||||
const [isEditOpen, setIsEditOpen] = React.useState(false)
|
||||
const [repromptText, setRepromptText] = React.useState("")
|
||||
const [editText, setEditText] = React.useState(JSON.stringify(items, null, 2))
|
||||
const [isBusy, setIsBusy] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
setEditText(JSON.stringify(items, null, 2))
|
||||
}, [items])
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsBusy(true)
|
||||
try {
|
||||
const parsed = JSON.parse(editText)
|
||||
await replaceSection({
|
||||
analysisId: analysisId as any,
|
||||
sectionKey: "profile",
|
||||
items: parsed,
|
||||
source: "mixed",
|
||||
})
|
||||
setIsEditOpen(false)
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleReprompt = async () => {
|
||||
setIsBusy(true)
|
||||
try {
|
||||
const response = await fetch("/api/analysis/reprompt", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
analysisId,
|
||||
sectionKey: "profile",
|
||||
prompt: repromptText.trim() || undefined,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.error || "Reprompt failed")
|
||||
}
|
||||
setRepromptText("")
|
||||
setIsRepromptOpen(false)
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between gap-3">
|
||||
<CardTitle className="text-base">Product Profile</CardTitle>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setIsEditOpen(true)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="secondary" size="sm" onClick={() => setIsRepromptOpen(true)}>
|
||||
Reprompt
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Product:</span>{" "}
|
||||
{items.productName || "Not set"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Tagline:</span>{" "}
|
||||
{items.tagline || "Not set"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Category:</span>{" "}
|
||||
{items.category || "Not set"}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-foreground">Positioning:</span>{" "}
|
||||
{items.positioning || "Not set"}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
<DialogDescription>Update the profile JSON.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={editText}
|
||||
onChange={(event) => setEditText(event.target.value)}
|
||||
className="min-h-[200px]"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsEditOpen(false)} disabled={isBusy}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isBusy}>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isRepromptOpen} onOpenChange={setIsRepromptOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Reprompt Profile</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide guidance to regenerate the profile.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Textarea
|
||||
value={repromptText}
|
||||
onChange={(event) => setRepromptText(event.target.value)}
|
||||
className="min-h-[140px]"
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsRepromptOpen(false)} disabled={isBusy}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleReprompt} disabled={isBusy}>
|
||||
Reprompt
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -4,9 +4,9 @@ import * as React from "react"
|
||||
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
} from "lucide-react"
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { useAuthActions } from "@convex-dev/auth/react"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
export function NavUser({
|
||||
user,
|
||||
@@ -44,6 +45,7 @@ export function NavUser({
|
||||
}) {
|
||||
const { isMobile } = useSidebar()
|
||||
const { signOut } = useAuthActions()
|
||||
const router = useRouter()
|
||||
const seed = React.useMemo(() => {
|
||||
const base = user.email || user.name || "";
|
||||
return base.trim() || "user";
|
||||
@@ -98,27 +100,28 @@ export function NavUser({
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => router.push("/settings?tab=upgrade")}>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => router.push("/settings?tab=account")}>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={() => router.push("/settings?tab=billing")}>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={() => router.push("/help")}>
|
||||
<HelpCircle />
|
||||
Support
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => signOut()}>
|
||||
<LogOut />
|
||||
Log out
|
||||
|
||||
@@ -8,9 +8,6 @@ import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Search,
|
||||
Settings,
|
||||
HelpCircle,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
Target
|
||||
@@ -25,30 +22,11 @@ export function Sidebar({ productName }: SidebarProps) {
|
||||
|
||||
const routes = [
|
||||
{
|
||||
label: 'Dashboard',
|
||||
label: 'Overview',
|
||||
icon: LayoutDashboard,
|
||||
href: '/dashboard',
|
||||
active: pathname === '/dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Opportunities',
|
||||
icon: Target,
|
||||
href: '/opportunities',
|
||||
active: pathname === '/opportunities',
|
||||
},
|
||||
]
|
||||
|
||||
const bottomRoutes = [
|
||||
{
|
||||
label: 'Settings',
|
||||
icon: Settings,
|
||||
href: '/settings',
|
||||
},
|
||||
{
|
||||
label: 'Help',
|
||||
icon: HelpCircle,
|
||||
href: '/help',
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -89,22 +67,32 @@ export function Sidebar({ productName }: SidebarProps) {
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
<Link
|
||||
href="/opportunities"
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
|
||||
pathname === '/opportunities'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
<Target className="h-4 w-4" />
|
||||
Search
|
||||
</Link>
|
||||
<Link
|
||||
href="/leads"
|
||||
className={cn(
|
||||
'flex items-center rounded-md px-3 py-2 pl-9 text-sm font-medium transition-colors',
|
||||
pathname === '/leads'
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
)}
|
||||
>
|
||||
Inbox
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<nav className="space-y-1 px-2">
|
||||
{bottomRoutes.map((route) => (
|
||||
<Link
|
||||
key={route.href}
|
||||
href={route.href}
|
||||
className="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||
>
|
||||
<route.icon className="h-4 w-4" />
|
||||
{route.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</ScrollArea>
|
||||
|
||||
{/* Bottom */}
|
||||
|
||||
Reference in New Issue
Block a user