diff --git a/components.json b/components.json new file mode 100644 index 0000000..329d3e5 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "styles/globals.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/package.json b/package.json index c58a454..0235806 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,22 @@ "@dagrejs/dagre": "^1.1.4", "@internationalized/date": "^3.7.0", "@mistralai/mistralai": "^1.5.0", + "@radix-ui/react-alert-dialog": "^1.1.6", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-context-menu": "^2.2.6", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/themes": "^3.2.0", "@react-aria/ssr": "3.9.7", "@react-aria/visually-hidden": "3.8.18", @@ -25,16 +40,18 @@ "@tailwindcss/postcss": "^4.0.3", "@xyflow/react": "^12.4.2", "ai": "^4.1.34", - "clsx": "2.1.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "d3-force": "^3.0.0", "d3-quadtree": "^3.0.1", "framer-motion": "^12.3.1", "intl-messageformat": "^10.5.0", "lodash.debounce": "^4.0.8", - "lucide-react": "^0.474.0", + "lucide-react": "^0.475.0", "next": "15.1.6", "next-themes": "^0.4.4", "nextjs-toploader": "^3.7.15", + "popover": "^2.4.1", "postcss": "^8.5.1", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -42,7 +59,9 @@ "react-markdown": "^9.0.3", "react-resizable-panels": "^2.1.7", "swr": "^2.3.0", + "tailwind-merge": "^3.0.1", "tailwindcss": "^4.0.3", + "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.1", "vaul": "^1.1.2", "zod": "^3.24.1" diff --git a/src/app/auth/callback/route.ts b/src/app/auth/callback/route.ts index 1dea95e..af11e00 100644 --- a/src/app/auth/callback/route.ts +++ b/src/app/auth/callback/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' // The client you created from the Server-Side Auth instructions -import { createClient } from '@/src/lib/supabase/server' +import { createClient } from '@/lib/supabase/server' export async function GET(request: Request) { const { searchParams, origin } = new URL(request.url) diff --git a/src/app/auth/confirm/route.ts b/src/app/auth/confirm/route.ts index db1c675..4a46dca 100644 --- a/src/app/auth/confirm/route.ts +++ b/src/app/auth/confirm/route.ts @@ -1,7 +1,7 @@ import { type EmailOtpType } from '@supabase/supabase-js' import { type NextRequest } from 'next/server' -import { createClient } from '@/src/lib/supabase/server' +import { createClient } from '@/lib/supabase/server' import { redirect } from 'next/navigation' export async function GET(request: NextRequest) { diff --git a/src/app/dashboard/keys/page.tsx b/src/app/dashboard/keys/page.tsx index 14d0ce3..fbac352 100644 --- a/src/app/dashboard/keys/page.tsx +++ b/src/app/dashboard/keys/page.tsx @@ -1,121 +1,9 @@ -import { Button, Card, IconButton, Separator, Table, Tabs } from "@radix-ui/themes" -import { Trash2 } from 'lucide-react' +import React from 'react' -export default function APIKeys() { - return ( -
-
-
-
-
-

Vos clés API

-

- Gérez vos clés API personnelles pour votre espace de travail. -

-
- -
- -

- Vous pouvez créer jusqu'à 10 clés API et les supprimer lorsque vous n'en avez plus besoin. -

-
- - - - Actif - - - Expiré - - - - - - - - Nom - Clé - Date d'expiration - - - - - - clé d'essai - ************************pnA5 - 15 février 2025 - - - - - - - - - -
-
-
-
-
-

Vos clés de services tiers

-

- Gérez vos clés API de services tiers pour votre espace de travail. -

-
- -
-
- - - - Actif - - - Expiré - - - - - - - - Nom - Clé - Date d'expiration - - - - - - clé d'essai - ************************pnA5 - 15 février 2025 - - - - - - - - - -
-
- ) +const page = () => { + return ( +
keys
+ ) } + +export default page \ No newline at end of file diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx deleted file mode 100644 index e565855..0000000 --- a/src/app/dashboard/layout.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import type React from "react" -import { createClient } from "@/src/lib/supabase/server" -import { redirect } from "next/navigation" -import Logo from "@/src/components/logo" -import { Box, Flex, ScrollArea, TextField, Text, IconButton } from "@radix-ui/themes" -import NewCase from "@/src/components/dashboard/new-case" -import { MagnifyingGlassIcon } from "@radix-ui/react-icons" -import User from "@/src/components/user" -import Link from "next/link" -import { PlusIcon } from 'lucide-react'; - -const navigation = [ - { - name: "Dashboard", - children: [ - { - name: "Your investigations", - href: "/dashboard", - }, - ], - }, - { - name: "Configuration", - children: [ - { - name: "Settings", - href: "/dashboard/settings", - }, - { - name: "API keys", - href: "/dashboard/keys", - }, - ], - }, -] - -const DashboardLayout = async ({ - children, -}: { - children: React.ReactNode -}) => { - const supabase = await createClient() - const { data, error } = await supabase.auth.getUser() - if (error || !data?.user) { - redirect("/login") - } - - return ( - - {/* Left Panel */} - - {/* Left Panel Header */} - - - - - - - - - - {/* Left Panel Content */} - - - {navigation.map((nav, i) => ( - - - {nav.name} - - - {nav?.children?.map((child, j) => ( - - - {child.name} - - - ))} - - - ))} - - - - - {/* Main Content */} - - {/* Top Navigation */} - - - - - - - - - - {/* Main Content Area */} - - {children} - - - - ) -} - -export default DashboardLayout \ No newline at end of file diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index f63a8aa..23034ab 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,33 +1,52 @@ -import React from 'react' -import { getInvestigations } from '@/src/lib/actions/investigations' -import Investigation from '@/src/components/dashboard/investigation' -import { Button, Flex } from '@radix-ui/themes' -import { DownloadIcon, FolderIcon, PlusIcon } from 'lucide-react' -import NewCase from '@/src/components/dashboard/new-case' +import { AppSidebar } from "@/components/app-sidebar" +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb" +import { Separator } from "@/components/ui/separator" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" -const DashboardPage = async () => { - const { investigations, error } = await getInvestigations() - if (error) return
An error occured.
- return ( -
-
-

Vos investigations

-

- La liste de vos investigations. Vous pouvez également en créer d'autres. -

-
- - - - - - - -
{investigations.map((investigation) => ( - - ))} -
+export default function Page() { + return ( + + + +
+
+ + + + + + + Building Your Application + + + + + Data Fetching + + + +
+
+
+
+
+
+
+
+
- ) + + + ) } -export default DashboardPage \ No newline at end of file diff --git a/src/app/investigations/[investigation_id]/individuals.tsx b/src/app/investigations/[investigation_id]/individuals.tsx index 1bc2c70..360ac83 100644 --- a/src/app/investigations/[investigation_id]/individuals.tsx +++ b/src/app/investigations/[investigation_id]/individuals.tsx @@ -1,11 +1,14 @@ "use client" import React from 'react' -import { Avatar, Box, Card, Flex, IconButton, Skeleton, Text } from '@radix-ui/themes'; -import { useIndividuals } from '@/src/lib/hooks/individuals/use-individuals'; -import { useInvestigationContext } from '@/src/components/contexts/investigation-provider'; -import { cn } from '@/src/lib/utils'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useIndividuals } from '@/lib/hooks/individuals/use-individuals'; +import { useInvestigationContext } from '@/components/contexts/investigation-provider'; +import { cn } from '@/lib/utils'; import { Pencil1Icon } from '@radix-ui/react-icons'; import { RotateCwIcon } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; const Filters = ({ investigation_id }: { investigation_id: string }) => { const { individuals, isLoading, refetch } = useIndividuals(investigation_id) @@ -13,53 +16,49 @@ const Filters = ({ investigation_id }: { investigation_id: string }) => { return (
- Profiles - refetch()} size="1" variant="ghost" color="gray"> +
Profiles
+ +
+
{isLoading && <> - - - + + + } {individuals?.map((individual: any) => ( - - setCurrentNode(individual.id)}> - - - - +
+ setCurrentNode(individual.id)}> +
+ + + CN + +
+
{individual.full_name} - - +
+
{individual.notes} - - - - +
+
+ - +
))} - -
+
+
) } diff --git a/src/app/investigations/[investigation_id]/layout.tsx b/src/app/investigations/[investigation_id]/layout.tsx index 7179816..54846cb 100644 --- a/src/app/investigations/[investigation_id]/layout.tsx +++ b/src/app/investigations/[investigation_id]/layout.tsx @@ -1,11 +1,11 @@ -import { InvestigationProvider } from '@/src/components/contexts/investigation-provider'; -import InvestigationLayout from '@/src/components/investigations/layout'; +import { InvestigationProvider } from '@/components/contexts/investigation-provider'; +import InvestigationLayout from '@/components/investigations/layout'; import Individuals from './individuals'; -import { SearchProvider } from '@/src/components/contexts/search-context'; -import { createClient } from "@/src/lib/supabase/server"; +import { SearchProvider } from '@/components/contexts/search-context'; +import { createClient } from "@/lib/supabase/server"; import { notFound, redirect } from "next/navigation"; -import { ChatProvider } from '@/src/components/contexts/chatbot-context'; -import { getInvestigation } from '@/src/lib/actions/investigations'; +import { ChatProvider } from '@/components/contexts/chatbot-context'; +import { getInvestigation } from '@/lib/actions/investigations'; const DashboardLayout = async ({ children, diff --git a/src/app/investigations/[investigation_id]/loading.tsx b/src/app/investigations/[investigation_id]/loading.tsx index a1452a1..48e8ea0 100644 --- a/src/app/investigations/[investigation_id]/loading.tsx +++ b/src/app/investigations/[investigation_id]/loading.tsx @@ -1,9 +1,8 @@ -import { Spinner } from '@radix-ui/themes' import React from 'react' const loading = () => { return ( -
Loading nodes and edges...
+
Loading nodes and edges...
) } diff --git a/src/app/investigations/[investigation_id]/page.tsx b/src/app/investigations/[investigation_id]/page.tsx index 5629a23..45b7a6b 100644 --- a/src/app/investigations/[investigation_id]/page.tsx +++ b/src/app/investigations/[investigation_id]/page.tsx @@ -1,6 +1,6 @@ -import { getInvestigationData } from '@/src/lib/actions/investigations' -import InvestigationGraph from '@/src/components/investigations/graph' -import IndividualModal from '@/src/components/investigations/individual-modal' +import { getInvestigationData } from '@/lib/actions/investigations' +import InvestigationGraph from '@/components/investigations/graph' +import IndividualModal from '@/components/investigations/individual-modal' const DashboardPage = async ({ params, diff --git a/src/app/investigations/loading.tsx b/src/app/investigations/loading.tsx index 79604ce..7a28b35 100644 --- a/src/app/investigations/loading.tsx +++ b/src/app/investigations/loading.tsx @@ -1,9 +1,8 @@ -import { Spinner } from '@radix-ui/themes' import React from 'react' const loading = () => { return ( -
Loading investigation...
+
Loading investigation...
) } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c4ae0a2..0eab430 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,14 +1,12 @@ -import "@/styles/globals.css"; +import "../../styles/globals.css"; import { Metadata, Viewport } from "next"; import clsx from "clsx"; -import "@/styles/globals.css" import { Providers } from "./providers"; -import "@radix-ui/themes/styles.css"; import { siteConfig } from "@/config/site"; import { fontSans } from "@/config/fonts"; -import { Theme } from "@radix-ui/themes"; -import { ConfirmContextProvider } from "@/src/components/use-confirm-dialog"; +import { ConfirmContextProvider } from "@/components/use-confirm-dialog"; import NextTopLoader from 'nextjs-toploader'; +import { TooltipProvider } from "@/components/ui/tooltip"; export const metadata: Metadata = { title: { @@ -38,19 +36,19 @@ export default function RootLayout({ - -
- +
+ + {children} -
- + +
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index f531122..67dcbde 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,5 @@ -import { LoginForm } from "@/src/components/login-form" -import { createClient } from "@/src/lib/supabase/server" +import { LoginForm } from "@/components/login-form" +import { createClient } from "@/lib/supabase/server" import { redirect } from "next/navigation" export default async function Page() { diff --git a/src/app/page.tsx b/src/app/page.tsx index 61de571..9096e66 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import { Button } from "@radix-ui/themes"; +import { Button } from '@/components/ui/button' import Link from "next/link"; export default function Home() { diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx new file mode 100644 index 0000000..94ad3a0 --- /dev/null +++ b/src/components/app-sidebar.tsx @@ -0,0 +1,175 @@ +"use client" + +import * as React from "react" +import { + AudioWaveform, + BookOpen, + Bot, + Command, + Frame, + GalleryVerticalEnd, + Map, + PieChart, + Settings2, + SquareTerminal, +} from "lucide-react" + +import { NavMain } from "@/components/nav-main" +import { NavProjects } from "@/components/nav-projects" +import { NavUser } from "@/components/nav-user" +import { TeamSwitcher } from "@/components/team-switcher" +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarHeader, + SidebarRail, +} from "@/components/ui/sidebar" + +// This is sample data. +const data = { + user: { + name: "shadcn", + email: "m@example.com", + avatar: "/avatars/shadcn.jpg", + }, + teams: [ + { + name: "Acme Inc", + logo: GalleryVerticalEnd, + plan: "Enterprise", + }, + { + name: "Acme Corp.", + logo: AudioWaveform, + plan: "Startup", + }, + { + name: "Evil Corp.", + logo: Command, + plan: "Free", + }, + ], + navMain: [ + { + title: "Playground", + url: "#", + icon: SquareTerminal, + isActive: true, + items: [ + { + title: "History", + url: "#", + }, + { + title: "Starred", + url: "#", + }, + { + title: "Settings", + url: "#", + }, + ], + }, + { + title: "Models", + url: "#", + icon: Bot, + items: [ + { + title: "Genesis", + url: "#", + }, + { + title: "Explorer", + url: "#", + }, + { + title: "Quantum", + url: "#", + }, + ], + }, + { + title: "Documentation", + url: "#", + icon: BookOpen, + items: [ + { + title: "Introduction", + url: "#", + }, + { + title: "Get Started", + url: "#", + }, + { + title: "Tutorials", + url: "#", + }, + { + title: "Changelog", + url: "#", + }, + ], + }, + { + title: "Settings", + url: "#", + icon: Settings2, + items: [ + { + title: "General", + url: "#", + }, + { + title: "Team", + url: "#", + }, + { + title: "Billing", + url: "#", + }, + { + title: "Limits", + url: "#", + }, + ], + }, + ], + projects: [ + { + name: "Design Engineering", + url: "#", + icon: Frame, + }, + { + name: "Sales & Marketing", + url: "#", + icon: PieChart, + }, + { + name: "Travel", + url: "#", + icon: Map, + }, + ], +} + +export function AppSidebar({ ...props }: React.ComponentProps) { + return ( + + + + + + + + + + + + + + ) +} diff --git a/src/components/contexts/chatbot-context.tsx b/src/components/contexts/chatbot-context.tsx index ccd94be..54adff8 100644 --- a/src/components/contexts/chatbot-context.tsx +++ b/src/components/contexts/chatbot-context.tsx @@ -1,142 +1,152 @@ -'use client'; - -import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; -import { Avatar, Box, Card, Dialog, Flex, Spinner, Text, TextArea, TextField } from '@radix-ui/themes'; -import { useChat } from '@ai-sdk/react'; -import { BotIcon } from 'lucide-react'; -import { cn } from '@/src/lib/utils'; -import ReactMarkdown from 'react-markdown' +"use client" +import type React from "react" +import { createContext, type ReactNode, useContext, useEffect, useState } from "react" +import { useChat } from "@ai-sdk/react" +import { Bot, User } from "lucide-react" +import { cn } from "@/lib/utils" +import ReactMarkdown from "react-markdown" +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Card, CardContent } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" interface ChatContextType { - open: boolean, - handleOpenChat: any + open: boolean + handleOpenChat: (content: any) => void } -const ChatContext = createContext(undefined); +const ChatContext = createContext(undefined) interface ChatProviderProps { - children: ReactNode; + children: ReactNode } export const ChatProvider: React.FC = ({ children }) => { - const [open, setOpen] = useState(false); - // content is the object passed to the chat, as a secondary object - const [context, setContext] = useState(false); - const { messages, input, handleInputChange, setInput, handleSubmit, error, isLoading } = useChat(); + const [open, setOpen] = useState(false) + const [context, setContext] = useState(null) + const { messages, input, handleInputChange, setInput, handleSubmit, error, isLoading } = useChat() - const handleOpenChat = (content: any) => { - setContext(content) - setInput(`What pattern can you extract from this person ?\n ${JSON.stringify(content)}`) - setOpen(true) - } + const handleOpenChat = (content: any) => { + setContext(content) + setInput(`What pattern can you extract from this person ?\n ${JSON.stringify(content)}`) + setOpen(true) + } - return ( - - {children} - - 0 ? "730px" : '480px'} className="w-full p-4"> - Ask AI - - Ask AI chat bot to help you understant patterns. - {/*
- context: {content?.title || content?.full_name} -
*/} - - {messages.map(m => ( - - - : } - /> - - - {isLoading && m.role !== 'user' ? : m.role === 'user' ? 'User' : 'Chatbot'} - - - - - {m.content} - - - - - - - ))} - {error && - - } - /> - - - {'Error'} - - - - - {"Oops, an error occured. Make sure you provided a valid Mistral API key."} - - - - - - } - -
- - - -
-
-
-
- ); + return ( + + {children} + + 0 ? "sm:max-w-[730px]" : "sm:max-w-[480px]")}> + + Ask AI + Ask AI chat bot to help you understand patterns. + +
+ {messages.map((m) => ( +
+
+ + + {m.role === "user" ? ( + + ) : isLoading ? ( + + ) : ( + + )} + + +
+ + {isLoading && m.role !== "user" ? : m.role === "user" ? "User" : "Chatbot"} + + + + {m.content} + + +
+
+
+ ))} + {error && ( +
+
+ + + + + +
+ Error + + + Oops, an error occurred. Make sure you provided a valid Mistral API key. + + +
+
+
+ )} +
+
+ + +
+
+
+
+ ) } export const useChatContext = (): ChatContextType => { - const context = useContext(ChatContext); - if (!context) { - throw new Error("useChatContext must be used within a ChatProvider"); - } - return context; -}; + const context = useContext(ChatContext) + if (!context) { + throw new Error("useChatContext must be used within a ChatProvider") + } + return context +} interface LoadingDotsProps { - speed?: number - text?: string + speed?: number + text?: string } const LoadingDots: React.FC = ({ speed = 200, text = "Thinking" }) => { - const [dots, setDots] = useState("") + const [dots, setDots] = useState("") - useEffect(() => { - const interval = setInterval(() => { - setDots((prevDots) => { - if (prevDots.length >= 3) { - return "" - } - return prevDots + "." - }) - }, speed) + useEffect(() => { + const interval = setInterval(() => { + setDots((prevDots) => { + if (prevDots.length >= 3) { + return "" + } + return prevDots + "." + }) + }, speed) - return () => clearInterval(interval) - }, [speed]) + return () => clearInterval(interval) + }, [speed]) + + return ( +
+ {text} + {dots} +
+ ) +} - return ( -
- {text} - {dots} -
- ) -} \ No newline at end of file diff --git a/src/components/contexts/investigation-provider.tsx b/src/components/contexts/investigation-provider.tsx index 479885d..61f95dc 100644 --- a/src/components/contexts/investigation-provider.tsx +++ b/src/components/contexts/investigation-provider.tsx @@ -1,15 +1,23 @@ "use client" -import useLocalStorage from "@/src/lib/hooks/use-local-storage"; +import useLocalStorage from "@/lib/hooks/use-local-storage"; import React, { createContext, useContext, ReactNode, useState, useCallback } from "react"; -import { Button, Dialog, Flex, Switch } from "@radix-ui/themes"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogClose, + DialogTitle, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; import { useParams, useRouter, usePathname, useSearchParams } from "next/navigation"; -import { Investigation } from "@/src/types/investigation"; -import { useInvestigation } from "@/src/lib/hooks/investigation/investigation"; -import { ThemeSwitch } from "../theme-switch"; -import { useConfirm } from "../use-confirm-dialog"; -import { supabase } from "@/src/lib/supabase/client"; -import FloatingEdge from "../investigations/floating-edge"; -import { cn } from "@/src/lib/utils"; +import { Investigation } from "@/types/investigation"; +import { useInvestigation } from "@/lib/hooks/investigation/investigation"; +import { ThemeSwitch } from "@/components/theme-switch"; +import { useConfirm } from "@/components/use-confirm-dialog"; +import { supabase } from "@/lib/supabase/client"; +// import FloatingEdge from "../investigations/floating-edge"; +import { cn } from "@/lib/utils"; interface InvestigationContextType { filters: any, @@ -87,13 +95,13 @@ export const InvestigationProvider: React.FC = ({ ch return ( {children} - - - Settings - + + + Settings + Make changes to your settings. - - + +
@@ -109,16 +117,16 @@ export const InvestigationProvider: React.FC = ({ ch
- - - setOpenSettingsModal(false)}> -
+
+ setOpenSettingsModal(false)}> + - - - - + +
+ + ); }; diff --git a/src/components/contexts/node-context.tsx b/src/components/contexts/node-context.tsx index 0a353b9..94860bf 100644 --- a/src/components/contexts/node-context.tsx +++ b/src/components/contexts/node-context.tsx @@ -1,11 +1,20 @@ "use client" import React, { createContext, useContext, ReactNode, useState, useCallback } from "react"; -import { Button, Callout, Dialog, Flex, Text, TextField } from "@radix-ui/themes"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogTitle, +} from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button"; import { useParams } from "next/navigation"; -import { supabase } from "@/src/lib/supabase/client"; +import { supabase } from "@/lib/supabase/client"; import { useNodeId, useReactFlow } from "@xyflow/react"; -import { InfoCircledIcon } from "@radix-ui/react-icons"; -import { useConfirm } from "../use-confirm-dialog"; +import { useConfirm } from "@/components/use-confirm-dialog"; +import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle } from "lucide-react"; interface NodeContextType { setOpenAddNodeModal: any, @@ -145,22 +154,22 @@ export const NodeProvider: React.FC = (props: any) => { return ( {props.children} - - - New {nodeType?.type} - + + + New {nodeType?.type} + Add a new related {nodeType?.type}. - +
- +
{nodeType?.fields.map((field: any, i: number) => { const [key, value] = field.split(":") return (
+ {error && + + + Error + + {error} + + } +
+ + - - - + + +
-
-
+ +
); }; diff --git a/src/components/contexts/search-context.tsx b/src/components/contexts/search-context.tsx index b1cf285..7caada5 100644 --- a/src/components/contexts/search-context.tsx +++ b/src/components/contexts/search-context.tsx @@ -1,23 +1,34 @@ "use client" -import React, { createContext, useContext, ReactNode, useState } from "react"; -import { Button, Callout, Card, Dialog, Flex, Text, TextField } from "@radix-ui/themes"; -import { InfoCircledIcon } from "@radix-ui/react-icons"; -import { investigateValue } from "@/src/lib/actions/search"; -import { useParams } from "next/navigation"; -import Breaches from "../breach"; +import type React from "react" +import { createContext, useContext, type ReactNode, useState } from "react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { InfoIcon } from "lucide-react" +import { investigateValue } from "@/lib/actions/search" +import { useParams } from "next/navigation" +import Breaches from "../breach" interface SearchContextType { - openSearchModal: boolean, - handleOpenSearchModal: any + openSearchModal: boolean + handleOpenSearchModal: (val: string) => void } -const SearchContext = createContext(undefined); +const SearchContext = createContext(undefined) interface SearchProviderProps { - children: ReactNode; + children: ReactNode } -export const SearchProvider: React.FC = (props: any) => { +export const SearchProvider: React.FC = ({ children }) => { const [openSearchModal, setOpenSearchModal] = useState(false) const { investigation_id } = useParams() const [value, setValue] = useState(null) @@ -31,10 +42,10 @@ export const SearchProvider: React.FC = (props: any) => { setIsLoading(true) setError("") try { - const data = await investigateValue(investigation_id, value) + const data = await investigateValue(investigation_id as string, value) setResults(data) } catch (err) { - setError(err instanceof Error ? err.message : "An error occured.") + setError(err instanceof Error ? err.message : "An error occurred.") } finally { setIsLoading(false) } @@ -53,76 +64,68 @@ export const SearchProvider: React.FC = (props: any) => { } return ( - - {props.children} - - - New search - - Make a new keyword associated research. - + + {children} + + + + New search + Make a new keyword associated research. +
- - value: "{value}" - {/* - - - - - This query may return some (a lot) or no results. Make sure you filter your results manually afterwards. - - */} - {error && - - - - - - {JSON.stringify(error)} - - } - {results && Array.isArray(results) ? +
+ +

+ value: "{value}" +

+
+ {error && ( + + + {JSON.stringify(error)} + + )} + {results && Array.isArray(results) ? ( <> - - - - - + + + "{value}" has appeared in {results.length} data breach(es). Here are the infos we have: - - + + - : results && - - - - - {JSON.stringify(results)} - - - } - - - -
+ + + {!results && ( + - - {!results && - - } -
+ )} +
-
-
+ +
- ); -}; + ) +} export const useSearchContext = (): SearchContextType => { - const context = useContext(SearchContext); + const context = useContext(SearchContext) if (!context) { - throw new Error("useSearchContext must be used within a SearchProvider"); + throw new Error("useSearchContext must be used within a SearchProvider") } - return context; -}; \ No newline at end of file + return context +} + diff --git a/src/components/copy.tsx b/src/components/copy.tsx index 56228e7..818e5f1 100644 --- a/src/components/copy.tsx +++ b/src/components/copy.tsx @@ -1,16 +1,18 @@ "use client" import { useState, useCallback } from "react" -import { IconButton, Tooltip } from "@radix-ui/themes" import { Copy } from "lucide-react" import { useTimeout } from "usehooks-ts" +import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip" +import { Button } from "./ui/button" interface CopyButtonProps { content: string delay?: number + className?: string } -export function CopyButton({ content, delay = 2000 }: CopyButtonProps) { +export function CopyButton({ content, className, delay = 2000 }: CopyButtonProps) { const [isCopied, setIsCopied] = useState(false) const handleCopy = useCallback((e: { stopPropagation: () => void }) => { @@ -30,10 +32,15 @@ export function CopyButton({ content, delay = 2000 }: CopyButtonProps) { ) return ( - - - - + + + + + + Copied ! + ) } diff --git a/src/components/dashboard/investigation.tsx b/src/components/dashboard/investigation.tsx index 7e8d644..e526fc4 100644 --- a/src/components/dashboard/investigation.tsx +++ b/src/components/dashboard/investigation.tsx @@ -1,37 +1,35 @@ "use client" import React from 'react' -import { Investigation } from '@/src/types/investigation'; +import { Investigation } from '@/types/investigation'; import Link from 'next/link'; -import { Box, Card, Inset, Text } from '@radix-ui/themes'; import Image from 'next/image'; +import { Card } from '../ui/card'; const investigation = ({ investigation }: { investigation: Investigation }) => { return ( - +
- -
-
- {investigation.title} +
+
+ {investigation.title} +
+
+
+ {investigation.title}
-
- - {investigation.title} - - - {investigation.description || No description provided.} - +
+ {investigation.description || No description provided.}
- +
- +
) } diff --git a/src/components/dashboard/new-case.tsx b/src/components/dashboard/new-case.tsx index 53b6011..6ee621b 100644 --- a/src/components/dashboard/new-case.tsx +++ b/src/components/dashboard/new-case.tsx @@ -1,79 +1,91 @@ "use client" -import { DropdownMenu, Button, Dialog, Flex, TextField, Text, Badge, Box } from "@radix-ui/themes"; -import { useState } from "react"; -import { supabase } from '@/src/lib/supabase/client'; -import { useRouter } from "next/navigation"; +import { useState } from "react" +import { useRouter } from "next/navigation" +import { supabase } from "@/lib/supabase/client" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Badge } from "@/components/ui/badge" export default function NewCase({ children }: { children: React.ReactNode }) { const [open, setOpen] = useState(false) const router = useRouter() - async function handleNewCase(e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.currentTarget)) - const investigation = await supabase.from("investigations").insert(data).select("id") - .single() - .then(({ data, error }) => { - if (error) - throw error - return data - }) - if (investigation) - router.push(`/investigations/${investigation.id}`) - setOpen(false) + async function handleNewCase(e: React.FormEvent) { + e.preventDefault() + const formData = new FormData(e.currentTarget) + const data = Object.fromEntries(formData) + + try { + const { data: investigation, error } = await supabase.from("investigations").insert(data).select("id").single() + + if (error) throw error + if (investigation) router.push(`/investigations/${investigation.id}`) + setOpen(false) + } catch (error) { + console.error("Error creating new case:", error) + // Handle error (e.g., show error message to user) + } } + return ( <> - - - {children} - - - setOpen(true)} shortcut="⌘ E">New case - New organization Soon - - - - + + {children} + + setOpen(true)}> + New case + ⌘ E + + + New organization + + Soon + + ⌘ D + + + + + + + New case + Create a new blank case. +
- - New case - - Create a new blank case. - - - - - - - - - - - - +
+
+ + +
+
+ + +
+
+ + + +
-
-
+ + - ); + ) } + diff --git a/src/components/dashboard/search-palette.tsx b/src/components/dashboard/search-palette.tsx index 458d289..1edace3 100644 --- a/src/components/dashboard/search-palette.tsx +++ b/src/components/dashboard/search-palette.tsx @@ -1,15 +1,18 @@ "use client" + import { useState, useCallback, useEffect, SetStateAction } from 'react' // @ts-ignore -import debounce from "lodash.debounce"; -import { SearchIcon } from "lucide-react" +import debounce from "lodash.debounce" +import { Search } from 'lucide-react' // @ts-ignore -import Highlighter from "react-highlight-words"; -import Link from 'next/link'; -import { useSearchResults } from '../../lib/hooks/investigation/use-search-results'; -import { Card, Dialog, Flex, IconButton, ScrollArea, Spinner, TextField } from '@radix-ui/themes'; -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { useInvestigationContext } from '../contexts/investigation-provider'; +import Highlighter from "react-highlight-words" +import { useSearchResults } from '../../lib/hooks/investigation/use-search-results' +import { useInvestigationContext } from '../contexts/investigation-provider' +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { ScrollArea } from "@/components/ui/scroll-area" const SearchModal = ({ investigation_id }: { investigation_id: string }) => { const [search, setSearch] = useState("") @@ -20,113 +23,95 @@ const SearchModal = ({ investigation_id }: { investigation_id: string }) => { error, isLoading, refetch, - } = useSearchResults(search, investigation_id); + } = useSearchResults(search, investigation_id) - const changeHandler = (event: { target: { value: SetStateAction; }; }) => { - setSearch(event.target.value); - refetch && refetch(); - }; - const debouncedChangeHandler = useCallback(debounce(changeHandler, 300), []); + const changeHandler = (event: { target: { value: SetStateAction } }) => { + setSearch(event.target.value) + refetch && refetch() + } + const debouncedChangeHandler = useCallback(debounce(changeHandler, 300), []) const handleClose = () => { - () => setSearch('') + setSearch('') setOpen(false) } useEffect(() => { - const handleKeyPress = (event: { ctrlKey: any; key: string; }) => { + const handleKeyPress = (event: KeyboardEvent) => { if (event.ctrlKey && event.key === 'k') { setOpen(true) } - }; - window.addEventListener('keydown', handleKeyPress); + } + window.addEventListener('keydown', handleKeyPress) return () => { - window.removeEventListener('keydown', handleKeyPress); - }; - }, []); + window.removeEventListener('keydown', handleKeyPress) + } + }, []) - const SearchItem = ({ item }: any) => - (
  • setOpen(false)}> - setCurrentNode(item.id)}> - - Individual - - - - - - - - - {/* - - */} - - -
  • + const SearchItem = ({ item }: any) => ( +
  • setOpen(false)}> + setCurrentNode(item.id)}> + +
    + Individual +
    + + + + + + +
    +
    +
    +
    +
  • ) + return ( <> - - - setOpen(true)} color="gray" size="2" variant="soft"> - - - - Search - - Find the profile you're looking for. - - - - - - - -
    - {error && "An error occured."} - {isLoading && } - {results?.length === 0 && `No results found for "${search}".`} - - -
      {!error && !isLoading && Array.isArray(results) && results?.map((item) => ( - - ))} + + + + + Search + + Find the profile you're looking for. + + +
      + +
      + {error &&

      An error occurred.

      } + {isLoading &&
      } + {results?.length === 0 &&

      No results found for "{search}".

      } + +
        + {!error && !isLoading && Array.isArray(results) && results?.map((item) => ( + + ))}
      - - {/* {search === '' && ( -
      -
      - )} */} +
      - - +
      +
      ) } -export default SearchModal \ No newline at end of file +export default SearchModal diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 09c0659..7978722 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { IconSvgProps } from "@/src/types"; +import { IconSvgProps } from "@/types"; export const Logo: React.FC = ({ size = 36, diff --git a/src/components/investigations/case-selector.tsx b/src/components/investigations/case-selector.tsx index 0e63cd3..eb038c4 100644 --- a/src/components/investigations/case-selector.tsx +++ b/src/components/investigations/case-selector.tsx @@ -1,7 +1,14 @@ -import { useInvestigations } from "@/src/lib/hooks/investigation/investigation"; +import { useInvestigations } from "@/lib/hooks/investigation/investigation"; import { useInvestigationContext } from "../contexts/investigation-provider"; import { useRouter } from "next/navigation"; -import { Select, Spinner } from "@radix-ui/themes"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Skeleton } from "@/components/ui/skeleton"; export default function CaseSelector() { const router = useRouter() @@ -12,16 +19,17 @@ export default function CaseSelector() { }; return (
      - - - - + {isLoading || isLoadingInvestigation ? : + }
      ); } diff --git a/src/components/investigations/custom-edge.tsx b/src/components/investigations/custom-edge.tsx index 6eecfa1..2a1e184 100644 --- a/src/components/investigations/custom-edge.tsx +++ b/src/components/investigations/custom-edge.tsx @@ -4,7 +4,7 @@ import { getBezierPath, } from '@xyflow/react'; import { useInvestigationContext } from '../contexts/investigation-provider'; -import { Badge } from '@radix-ui/themes'; +import { Badge } from '@/components/ui/badge'; export default function CustomEdge(props: any) { const { id, label, confidence_level, sourceX, sourceY, targetX, targetY, style } = props @@ -20,7 +20,7 @@ export default function CustomEdge(props: any) { {settings.showEdgeLabel && - ([]) - - const toggleSelect = (val: string) => { - setSelected((prev) => - prev.includes(val) ? prev.filter((c) => c !== val) : [...prev, val] - ) - } - - return ( -
      - - {values.map((value: string, i: number) => { - const isSelected = selected.includes(value) - return ( - toggleSelect(value)} - layout - initial={false} - animate={{ - backgroundColor: isSelected ? "#2a1711" : "rgba(39, 39, 42, 0.07)", - }} - whileHover={{ - backgroundColor: isSelected ? "#2a1711" : "rgba(39, 39, 42, 0.07)", - }} - whileTap={{ - backgroundColor: isSelected ? "#1f1209" : "rgba(39, 39, 42, 0.07)", - }} - transition={{ - type: "spring", - stiffness: 500, - damping: 30, - mass: 0.5, - backgroundColor: { duration: 0.1 }, - }} - className={` - inline-flex items-center px-2 py-1 rounded-full text-xs font-medium - whitespace-nowrap overflow-hidden ring-1 ring-inset - ${isSelected - ? "text-[#ff9066] ring-[hsla(0, 0.00%, 100.00%, 0.32)]" - : "text-zinc-400 ring-[hsla(0, 0.00%, 100.00%, 0.33)]"} - `} - > - - {value} - - {isSelected && ( - -
      - -
      -
      - )} -
      -
      -
      - ) - })} -
      -
      - ) -} - diff --git a/src/components/investigations/floating-connection.tsx b/src/components/investigations/floating-connection.tsx index bc5b1ab..d0656d8 100644 --- a/src/components/investigations/floating-connection.tsx +++ b/src/components/investigations/floating-connection.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { getBezierPath, Position } from '@xyflow/react'; -import { getEdgeParams } from '@/src/lib/utils'; +import { getEdgeParams } from '@/lib/utils'; const FloatingConnectionLine = ({ toX, diff --git a/src/components/investigations/floating-edge.tsx b/src/components/investigations/floating-edge.tsx index 4e6c0a2..5b13d28 100644 --- a/src/components/investigations/floating-edge.tsx +++ b/src/components/investigations/floating-edge.tsx @@ -1,11 +1,11 @@ -import { getEdgeParams } from '@/src/lib/utils'; +import { getEdgeParams } from '@/lib/utils'; import { EdgeLabelRenderer, getBezierPath, useInternalNode } from '@xyflow/react'; -import { useInvestigationContext } from '../contexts/investigation-provider'; -import { Badge } from '@radix-ui/themes'; +import { useInvestigationContext } from '@/components/contexts/investigation-provider'; +import { Badge } from '@/components/ui/badge'; function FloatingEdge(props: any) { const { id, source, target, markerEnd, label, confidence_level, sourceX, sourceY, targetX, targetY, style } = props const sourceNode = useInternalNode(source); @@ -41,7 +41,7 @@ function FloatingEdge(props: any) { /> {settings.showEdgeLabel && - - - onLayout('TB')}> - - + + + + + + Auto layout (vertical) + - - onLayout('LR')}> - - + + + + + + Auto layout (horizontal) + - - - Graph - Timeline - Map - - - - - - window.location.reload()} variant="soft"> - - - +
      +
      + - +
      {currentNode && {/* @ts-ignore */} {getNode(currentNode)?.data?.label} } - +
      - - {/* @ts-ignore */} - - - + + + {/* @ts-ignore */} + + + + Center view + - - {/* @ts-ignore */} - - - + + + {/* @ts-ignore */} + + + + Zoom in + - - {/* @ts-ignore */} - - - - - - {/* @ts-ignore */} - setIsLocked((prev) => !prev)}> - {isLocked ? : } - + + + {/* @ts-ignore */} + + + Zoom out + + {settings.showMiniMap && } -
    +
    ); }; @@ -370,7 +383,7 @@ export default function (props: any) { }, []) if (!mounted) { - return
    + return
    Loading...
    } return ( diff --git a/src/components/investigations/individual-modal.tsx b/src/components/investigations/individual-modal.tsx index 9ab8a0d..3ec77eb 100644 --- a/src/components/investigations/individual-modal.tsx +++ b/src/components/investigations/individual-modal.tsx @@ -1,32 +1,23 @@ "use client" import type React from "react" import { useEffect, useState } from "react" -import { - Flex, - Dialog, - TextField, - Button, - Box, - Skeleton, - Avatar, - Tabs, - IconButton, - Text, - Spinner, - Badge, - Grid, - Link, - Callout, -} from "@radix-ui/themes" import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useIndividual } from "@/src/lib/hooks/individuals/use-individual" -import { useEmailsAndBreaches } from "@/src/lib/hooks/individuals/use-emails-breaches" -import { Pencil1Icon, Cross2Icon, PlusIcon, TrashIcon } from "@radix-ui/react-icons" -import { useRelations } from "@/src/lib/hooks/individuals/use-relations" +import { useIndividual } from "@/lib/hooks/individuals/use-individual" +import { useEmailsAndBreaches } from "@/lib/hooks/individuals/use-emails-breaches" +import { useRelations } from "@/lib/hooks/individuals/use-relations" import { useInvestigationContext } from "../contexts/investigation-provider" -import { usePlatformIcons } from "@/src/lib/hooks/use-platform-icons" +import { usePlatformIcons } from "@/lib/hooks/use-platform-icons" +import { supabase } from "@/lib/supabase/client" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogTitle } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Badge } from "@/components/ui/badge" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { Card, CardContent } from "@/components/ui/card" +import { Pencil, X, Plus, Trash2 } from "lucide-react" import Breaches from "../breach" -import { supabase } from "@/src/lib/supabase/client" const IndividualModal = () => { const router = useRouter() @@ -39,7 +30,7 @@ const IndividualModal = () => { const platformsIcons = usePlatformIcons("medium") const { relations, isLoading: isLoadingRelations } = useRelations(individual_id) const [editMode, setEditMode] = useState(false) - const [image, setImage] = useState('') + const [image, setImage] = useState("") const [phones, setPhones] = useState([""]) const [accounts, setAccounts] = useState([""]) const [ips, setIps] = useState([""]) @@ -60,7 +51,12 @@ const IndividualModal = () => { event.preventDefault() const formData = new FormData(event.currentTarget) const formContent = Object.fromEntries(formData.entries()) - const { data } = await supabase.from("individuals").update({ ...formContent, birth_date: null }).eq("id", individual_id).select("image_url").single() + const { data } = await supabase + .from("individuals") + .update({ ...formContent, birth_date: null }) + .eq("id", individual_id) + .select("image_url") + .single() setImage(data?.image_url) setEditMode(false) } @@ -80,229 +76,292 @@ const IndividualModal = () => { if (!isLoading && !individual) { return ( - - - No data - - No data found for this individual. - - - - - - + + + ) } return ( - - - -
    - - - User Profile - setEditMode(!editMode)} - aria-label={editMode ? "Cancel edit" : "Edit profile"} - > - {editMode ? : } - - - - - - {/* {editMode && ( - - )} */} - - - - - Overview - Social accounts 0 ? "indigo" : "gray"} radius="full" className="ml-1">{accounts?.length} - Emails 0 ? "indigo" : "gray"} radius="full" className="ml-1">{emails?.length} - Phone numbers 0 ? "indigo" : "gray"} radius="full" className="ml-1">{phones?.length} - IP addresses 0 ? "indigo" : "gray"} radius="full" className="ml-1">{ips?.length} - Relations 0 ? "indigo" : "gray"} radius="full" className="ml-1">{relations?.length} - - - - - {individual && Object.keys(individual).filter((key) => typeof individual[key] !== "object" || !Array.isArray(individual[key])).map((key) => ( - - - - - ))} - - - - - {emails.length === 0 && No email registered. Click on edit to add one.} - {emails.map((email: any, index: number) => ( - - 0 ? "orange" : "green"} size="1"> - - {email.email} {email.breaches.length === 0 ? " is not yet involved in a data breach." : ` was involved in ${email.breaches.length} data breach(es).`} - - - - - ))} - - - - - {accounts.length === 0 && No account registered. Click on edit to add one.} - {accounts.map((account: any, index) => ( - - + + + +
    +
    + User Profile + +
    +
    +
    + + + {individual?.full_name?.[0] || "?"} + +
    +
    + + + Overview + + Social accounts + {isLoadingRelations ? ( + Loading... + ) : ( + 0 ? "default" : "outline"} className="ml-1"> + {accounts?.length} + + )} + + + Emails + {isLoadingEmails ? ( + Loading... + ) : ( + 0 ? "default" : "outline"} className="ml-1"> + {emails?.length} + + )} + + + Phone numbers + {isLoadingRelations ? ( + Loading... + ) : ( + 0 ? "default" : "outline"} className="ml-1"> + {phones?.length} + + )} + + + IP addresses + {isLoadingRelations ? ( + Loading... + ) : ( + 0 ? "default" : "outline"} className="ml-1"> + {ips?.length} + + )} + + + Relations + {isLoadingRelations ? ( + Loading... + ) : ( + 0 ? "default" : "outline"} className="ml-1"> + {relations?.length} + + )} + + +
    + +
    + {individual && + Object.keys(individual) + .filter((key) => typeof individual[key] !== "object" || !Array.isArray(individual[key])) + .map((key) => ( +
    + + +
    + ))} +
    +
    + +
    + {emails.length === 0 && ( +

    + No email registered. Click on edit to add one. +

    + )} + {emails.map((email: any, index: number) => ( +
    + 0 ? "destructive" : "default"}> + + {email.email}{" "} + {email.breaches.length === 0 + ? " is not yet involved in a data breach." + : ` was involved in ${email.breaches.length} data breach(es).`} + + + +
    + ))} +
    +
    + +
    + {accounts.length === 0 && ( +

    + No account registered. Click on edit to add one. +

    + )} + {accounts.map((account: any, index) => ( + + + {/* @ts-ignore */} - - - - {account?.platform} - - - {account?.username || No username} - - - - {account?.profile_url} - - - - - - ))} - {editMode && ( - - )} - - - - - {phones.length === 0 && No phone number registered. Click on edit to add one.} - {phones.map((phone, index) => ( - - handleFieldChange(index, e.target.value, setPhones)} - placeholder="Phone Number" - type="tel" - disabled={!editMode} - style={{ flexGrow: 1 }} - /> - {editMode && ( - handleRemoveField(index, setPhones)} - aria-label="Remove phone" + + {account?.platform?.[0] || "?"} + + + + + ))} + {editMode && ( + + )} +
    +
    + +
    + {phones.length === 0 && ( +

    + No phone number registered. Click on edit to add one. +

    + )} + {phones.map((phone, index) => ( +
    + handleFieldChange(index, e.target.value, setPhones)} + placeholder="Phone Number" + type="tel" + disabled={!editMode} + className="flex-grow" + /> + {editMode && ( + + )} +
    + ))} + {editMode && ( + + )} +
    +
    + +
    + {ips.length === 0 && ( +

    + No IP address registered. Click on edit to add one. +

    + )} + {ips.map((ip, index) => ( +
    + handleFieldChange(index, e.target.value, setIps)} + placeholder="IP address" + type="text" + disabled={!editMode} + className="flex-grow" + /> + {editMode && + ( + )} - - ))} - {editMode && ( - - )} - - - - - {relations.length === 0 && No relation registered. Click on edit to add one.} - {relations.map((relation) => ( - handleOpenIndividualModal(relation.id)} asChild size="1"> - - - - - {relation.full_name} - - - {relation.relation_type} - - - - - ))} - - - - - - - - - - - - {editMode && } - - - - - +
    + ))} + {editMode && ( + + )} +
    +
    + +
    + {relations.length === 0 && ( +

    + No relation registered. Click on edit to add one. +

    + )} + {relations.map((relation) => ( + handleOpenIndividualModal(relation.id)} + > + + + + {relation.full_name[0]} + +
    +

    {relation.full_name}

    +

    {relation.relation_type}

    +
    +
    +
    + ))} +
    +
    +
    +
    +
    +
    +
    + + + {editMode && } + + +
    +
    ) } -export default IndividualModal - +export default IndividualModal \ No newline at end of file diff --git a/src/components/investigations/layout.tsx b/src/components/investigations/layout.tsx index 94b3322..1adedc2 100644 --- a/src/components/investigations/layout.tsx +++ b/src/components/investigations/layout.tsx @@ -2,15 +2,16 @@ import React from 'react' import MoreMenu from './more-menu'; import CaseSelector from './case-selector'; -import NewCase from '../dashboard/new-case'; -import SearchModal from '../dashboard/search-palette'; -import { Flex, IconButton, ScrollArea } from '@radix-ui/themes'; +import NewCase from '@/components/dashboard/new-case'; +import SearchModal from '@/components/dashboard/search-palette'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { BotMessageSquareIcon, PanelRightIcon, PlusIcon } from 'lucide-react'; -import { useChatContext } from '../contexts/chatbot-context'; -import Logo from '../logo'; -import { useInvestigationContext } from '../contexts/investigation-provider'; -import User from '../user'; +import { useChatContext } from '@/components/contexts/chatbot-context'; +import Logo from '@/components/logo'; +import { useInvestigationContext } from '@/components/contexts/investigation-provider'; +import { NavUser } from '@/components/user'; +import { Button } from '../ui/button'; +import { ScrollArea } from '../ui/scroll-area'; const InvestigationLayout = ({ children, @@ -28,45 +29,45 @@ const InvestigationLayout = ({ return ( {panelOpen && -
    -
    +
    +
    - - handleOpenChat(investigation)} color="gray" size="2" variant="soft"> +
    + - + - +
    - +
    {left}
    - - - +
    + +
    }
    -
    - - setPanelOpen(!panelOpen)} variant='soft' color='gray'> +
    +
    + - +
    {children}
    - + ) } diff --git a/src/components/investigations/more-menu.tsx b/src/components/investigations/more-menu.tsx index 1a2f503..737b024 100644 --- a/src/components/investigations/more-menu.tsx +++ b/src/components/investigations/more-menu.tsx @@ -1,24 +1,28 @@ -import { Ellipsis } from 'lucide-react'; +import { Ellipsis } from "lucide-react"; import { useInvestigationContext } from "../contexts/investigation-provider"; -import { DropdownMenu, IconButton } from "@radix-ui/themes"; - - +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { Button } from "@/components/ui/button"; export default function MoreMenu() { - const { setOpenSettingsModal, handleDeleteInvestigation } = useInvestigationContext() + const { setOpenSettingsModal, handleDeleteInvestigation } = useInvestigationContext(); return ( - - - - - - - - Settings - + + + + + + Settings + Delete - - - + + + ); -} +} \ No newline at end of file diff --git a/src/components/investigations/new-actions.tsx b/src/components/investigations/new-actions.tsx index 3064198..02e72a1 100644 --- a/src/components/investigations/new-actions.tsx +++ b/src/components/investigations/new-actions.tsx @@ -1,59 +1,70 @@ -import { supabase } from "@/src/lib/supabase/client"; -import { DropdownMenu, Button, Popover, Flex, Box, TextField, Avatar } from "@radix-ui/themes"; -import { PlusIcon } from "lucide-react"; -import { useParams } from "next/navigation"; +"use client" +import { useState } from "react" +import { useParams } from "next/navigation" +import { supabase } from "@/lib/supabase/client" +import { Button } from "@/components/ui/button" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Input } from "@/components/ui/input" +import { Plus } from "lucide-react" +import type React from "react" // Added import for React export default function NewActions({ addNodes }: { addNodes: any }) { const { investigation_id } = useParams() - const onSubmit = async (e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) => { - e.preventDefault(); - const data = Object.fromEntries(new FormData(e.currentTarget)); - const node = await supabase.from("individuals").insert({ ...data, investigation_id: investigation_id?.toString() }).select("*") - .single() - .then(({ data, error }) => { - if (error) - console.error(error) - return data - }) - addNodes({ - id: node.id, - type: "individual", - data: { ...node, "label": data.full_name }, - position: { x: -100, y: -100 } - }); + const [open, setOpen] = useState(false) + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault() + const formData = new FormData(e.currentTarget) + const data = Object.fromEntries(formData) + + try { + const { data: node, error } = await supabase + .from("individuals") + .insert({ ...data, investigation_id: investigation_id?.toString() }) + .select("*") + .single() + + if (error) throw error + + addNodes({ + id: node.id, + type: "individual", + data: { ...node, label: data.full_name }, + position: { x: -100, y: -100 }, + }) + + setOpen(false) + } catch (error) { + console.error("Error adding individual:", error) + } } + return ( - - - - - -
    - - - - - - - - - + + + + + A + +
    + +
    + +
    +
    -
    -
    - ); + + + ) } + diff --git a/src/components/investigations/nodes/email.tsx b/src/components/investigations/nodes/email.tsx index 4101cf6..a505222 100644 --- a/src/components/investigations/nodes/email.tsx +++ b/src/components/investigations/nodes/email.tsx @@ -1,77 +1,104 @@ "use client" -import React, { memo } from 'react'; -import { Handle, Position, useStore } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Badge, Flex, Inset, Tooltip, Avatar } from '@radix-ui/themes'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { AtSignIcon, ZapIcon } from 'lucide-react'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { CopyButton } from '../../copy'; -import { useSearchContext } from '../../contexts/search-context'; + +import { memo } from "react" +import { Handle, Position, useStore } from "@xyflow/react" +import { NodeProvider, useNodeContext } from "../../contexts/node-context" +import { AtSign, Zap } from "lucide-react" +import { cn, zoomSelector } from "@/lib/utils" +import { useInvestigationContext } from "../../contexts/investigation-provider" +import { CopyButton } from "../../copy" +import { useSearchContext } from "../../contexts/search-context" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" function EmailNode({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() const { settings, currentNode } = useInvestigationContext() const { handleOpenSearchModal } = useSearchContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) return ( - - - - {settings.showNodeLabel && showContent ? - - - - - - - - - {data.label} - - {settings.showCopyIcon && } - - - + + +
    + {settings.showNodeLabel && showContent ? ( + +
    + + + +
    + {data.label} + {settings.showCopyIcon && } +
    +
    - : - - - } + ) : ( + + + + + + {data.label} + + + )} - - - - - handleOpenSearchModal(data.email)}>Launch search - Copy content - Duplicate - - +
    +
    + + handleOpenSearchModal(data.email)}> + Launch search + + + + Copy content + ⌘ C + + + Duplicate + ⌘ D + + + Delete - - -
    - ); + ⌘ ⌫ + + + + ) } const MemoizedNode = (props: any) => ( -); +) + +export default memo(MemoizedNode) -export default memo(MemoizedNode); \ No newline at end of file diff --git a/src/components/investigations/nodes/individual.tsx b/src/components/investigations/nodes/individual.tsx index 6284ef0..35d2244 100644 --- a/src/components/investigations/nodes/individual.tsx +++ b/src/components/investigations/nodes/individual.tsx @@ -1,124 +1,221 @@ "use client" -import React, { memo } from 'react'; -import { Handle, NodeToolbar, Position, useStore } from '@xyflow/react'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { Avatar, Card, Box, Flex, Text, ContextMenu, Spinner, Badge, Tooltip, IconButton, Button } from '@radix-ui/themes'; -import { AtSignIcon, CameraIcon, FacebookIcon, InstagramIcon, LocateIcon, MessageCircleDashedIcon, PhoneIcon, SendIcon, UserIcon, MapPinHouseIcon, ZapIcon, BotIcon, EditIcon } from 'lucide-react'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { useSearchContext } from '../../contexts/search-context'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { CopyButton } from '../../copy'; -import { useChatContext } from '../../contexts/chatbot-context'; -import { GithubIcon } from '../../icons'; +import { memo } from "react" +import { Handle, NodeToolbar, Position, useStore } from "@xyflow/react" +import { useInvestigationContext } from "@/components/contexts/investigation-provider" +import { NodeProvider, useNodeContext } from "@/components/contexts/node-context" +import { useSearchContext } from "@/components/contexts/search-context" +import { cn, zoomSelector } from "@/lib/utils" +import { CopyButton } from "@/components/copy" +import { useChatContext } from "@/components/contexts/chatbot-context" +import { GithubIcon } from "lucide-react" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { Badge } from "@/components/ui/badge" +import { + AtSign, + Bot, + Camera, + Edit, + Facebook, + Instagram, + Locate, + MapPin, + MessageCircleDashed, + Phone, + Send, + User, + Zap, +} from "lucide-react" function Custom(props: any) { const { settings, handleOpenIndividualModal, currentNode } = useInvestigationContext() const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading } = useNodeContext() const { handleOpenSearchModal } = useSearchContext() const { handleOpenChat } = useChatContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) const { data } = props + return ( <> - {settings.showNodeToolbar && - - - - - - + {settings.showNodeToolbar && ( + + +
    + + +
    -
    } - - { e.stopPropagation() }}> - {settings.showNodeLabel && showContent ? - handleOpenIndividualModal(data.id)} className={cn('!p-1 !pr-2 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}> - - : data.full_name[0]} - /> - {settings.showNodeLabel && showContent && - - - {data.full_name} - - {settings.showCopyIcon && } - } - - : - - - } +
    + )} + + { + e.stopPropagation() + }} + > +
    + {settings.showNodeLabel && showContent ? ( + handleOpenIndividualModal(data.id)} + className={cn( + "p-1 border border-border hover:border-sky-400 rounded-full shadow-none", + currentNode === data.id && "border-sky-400", + )} + > +
    + + + {loading ? "..." : data.full_name[0]} + + {settings.showNodeLabel && showContent && ( +
    + {data.full_name} + {settings.showCopyIcon && } +
    + )} +
    +
    + ) : ( + + + + + + {data.full_name} + + + )} - - - - handleOpenSearchModal(data.full_name)}>Launch search - handleOpenChat(data)}>Ask AI - - New - - setOpenAddNodeModal(e, "individuals")}> New relation - setOpenAddNodeModal(e, "phone_numbers", data.id)}>Phone number - setOpenAddNodeModal(e, "physical_addresses", data.id)}>Physical address - setOpenAddNodeModal(e, "emails", data.id)}>Email address - setOpenAddNodeModal(e, "ip_addresses", data.id)}>IP address - - Social account - - setOpenAddNodeModal(e, "social_accounts_facebook", data.id)}>Facebook - setOpenAddNodeModal(e, "social_accounts_instagram", data.id)}>Instagram - setOpenAddNodeModal(e, "social_accounts_telegram", data.id)}>Telegram - setOpenAddNodeModal(e, "social_accounts_signal", data.id)}>Signal - setOpenAddNodeModal(e, "social_accounts_snapchat", data.id)}>Snapchat - setOpenAddNodeModal(e, "social_accounts_github", data.id)}>Github - setOpenAddNodeModal(e, "social_accounts_coco", data.id)}>Coco soon - - - - - handleOpenIndividualModal(data.id)}>View and edit - Duplicate - - +
    +
    + + handleOpenSearchModal(data.full_name)}> + Launch search + + + handleOpenChat(data)}> + Ask AI + + + + New + + setOpenAddNodeModal(e, "individuals")}> + New relation + + setOpenAddNodeModal(e, "phone_numbers", data.id)}> + + Phone number + + setOpenAddNodeModal(e, "physical_addresses", data.id)}> + + Physical address + + setOpenAddNodeModal(e, "emails", data.id)}> + + Email address + + setOpenAddNodeModal(e, "ip_addresses", data.id)}> + + IP address + + + Social account + + setOpenAddNodeModal(e, "social_accounts_facebook", data.id)}> + + Facebook + + setOpenAddNodeModal(e, "social_accounts_instagram", data.id)}> + + Instagram + + setOpenAddNodeModal(e, "social_accounts_telegram", data.id)}> + + Telegram + + setOpenAddNodeModal(e, "social_accounts_signal", data.id)}> + + Signal + + setOpenAddNodeModal(e, "social_accounts_snapchat", data.id)}> + + Snapchat + + setOpenAddNodeModal(e, "social_accounts_github", data.id)}> + + Github + + setOpenAddNodeModal(e, "social_accounts_coco", data.id)}> + Coco{" "} + + soon + + + + + + + handleOpenIndividualModal(data.id)}>View and edit + Duplicate + + Delete - - - + ⌘ ⌫ + + +
    - ); + ) } const IndividualNode = (props: any) => ( -); +) + +export default memo(IndividualNode) -export default memo(IndividualNode); \ No newline at end of file diff --git a/src/components/investigations/nodes/ip_address.tsx b/src/components/investigations/nodes/ip_address.tsx index 56a8791..01f61a3 100644 --- a/src/components/investigations/nodes/ip_address.tsx +++ b/src/components/investigations/nodes/ip_address.tsx @@ -1,78 +1,104 @@ "use client" -import React, { memo } from 'react'; -import { Handle, Position, useStore } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { LocateIcon, ZapIcon } from 'lucide-react'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { CopyButton } from '../../copy'; -import { useSearchContext } from '../../contexts/search-context'; -function Custom({ data }: any) { +import { memo } from "react" +import { Handle, Position, useStore } from "@xyflow/react" +import { NodeProvider, useNodeContext } from "@/components/contexts/node-context" +import { LocateIcon, Zap } from "lucide-react" +import { cn, zoomSelector } from "@/lib/utils" +import { useInvestigationContext } from "@/components/contexts/investigation-provider" +import { CopyButton } from "@/components/copy" +import { useSearchContext } from "@/components/contexts/search-context" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" + +function EmailNode({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() const { settings, currentNode } = useInvestigationContext() const { handleOpenSearchModal } = useSearchContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) return ( - <> - - - - {settings.showNodeLabel && showContent ? - - - - - - - - - {data.label} - - {settings.showCopyIcon && } - - - - - : - - - } - - - - - handleOpenSearchModal(data.ip_address)}>Launch search - Copy content - Duplicate - - - Delete - - - - - ); + + +
    + {settings.showNodeLabel && showContent ? ( + +
    + + + +
    + {data.label} + {settings.showCopyIcon && } +
    +
    +
    + ) : ( + + + + + + {data.label} + + + )} + +
    +
    + + handleOpenSearchModal(data.email)}> + Launch search + + + + Copy content + ⌘ C + + + Duplicate + ⌘ D + + + + Delete + ⌘ ⌫ + + +
    + ) } -const IpNode = (props: any) => ( +const MemoizedNode = (props: any) => ( - + -); +) + +export default memo(MemoizedNode) -export default memo(IpNode); \ No newline at end of file diff --git a/src/components/investigations/nodes/phone.tsx b/src/components/investigations/nodes/phone.tsx index 245beb7..45ed4f4 100644 --- a/src/components/investigations/nodes/phone.tsx +++ b/src/components/investigations/nodes/phone.tsx @@ -1,79 +1,104 @@ "use client" -import React, { memo } from 'react'; -import { Handle, Position, useStore } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { PhoneIcon, ZapIcon } from 'lucide-react'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { CopyButton } from '../../copy'; -import { useSearchContext } from '../../contexts/search-context'; -function Custom({ data }: any) { +import { memo } from "react" +import { Handle, Position, useStore } from "@xyflow/react" +import { NodeProvider, useNodeContext } from "@/components/contexts/node-context" +import { PhoneIcon, Zap } from "lucide-react" +import { cn, zoomSelector } from "@/lib/utils" +import { useInvestigationContext } from "@/components/contexts/investigation-provider" +import { CopyButton } from "@/components/copy" +import { useSearchContext } from "@/components/contexts/search-context" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" + +function EmailNode({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() const { settings, currentNode } = useInvestigationContext() const { handleOpenSearchModal } = useSearchContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) return ( - <> - - - - {settings.showNodeLabel && showContent ? - - - - - - - - - {data.label} - - {settings.showCopyIcon && } - - - - - : - - - } - - - - - - handleOpenSearchModal(data.phone_number)}>Launch search - Copy content - Duplicate - - - Delete - - - - - ); + + +
    + {settings.showNodeLabel && showContent ? ( + +
    + + + +
    + {data.label} + {settings.showCopyIcon && } +
    +
    +
    + ) : ( + + + + + + {data.label} + + + )} + +
    +
    + + handleOpenSearchModal(data.email)}> + Launch search + + + + Copy content + ⌘ C + + + Duplicate + ⌘ D + + + + Delete + ⌘ ⌫ + + +
    + ) } -const PhoneNode = (props: any) => ( +const MemoizedNode = (props: any) => ( - + -); +) + +export default memo(MemoizedNode) -export default memo(PhoneNode); \ No newline at end of file diff --git a/src/components/investigations/nodes/physical_address.tsx b/src/components/investigations/nodes/physical_address.tsx index c93e5de..6c68c68 100644 --- a/src/components/investigations/nodes/physical_address.tsx +++ b/src/components/investigations/nodes/physical_address.tsx @@ -1,77 +1,104 @@ "use client" -import React, { memo } from 'react'; -import { Handle, Position, useStore } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Badge, Flex, Inset, Tooltip, Avatar } from '@radix-ui/themes'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { MapPinHouseIcon, ZapIcon } from 'lucide-react'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { CopyButton } from '../../copy'; -import { useSearchContext } from '../../contexts/search-context'; -function AddressNode({ data }: any) { +import { memo } from "react" +import { Handle, Position, useStore } from "@xyflow/react" +import { NodeProvider, useNodeContext } from "@/components/contexts/node-context" +import { MapPinHouseIcon, Zap } from "lucide-react" +import { cn, zoomSelector } from "@/lib/utils" +import { useInvestigationContext } from "@/components/contexts/investigation-provider" +import { CopyButton } from "@/components/copy" +import { useSearchContext } from "@/components/contexts/search-context" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" + +function EmailNode({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() const { settings, currentNode } = useInvestigationContext() const { handleOpenSearchModal } = useSearchContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) return ( - - - - {settings.showNodeLabel && showContent ? - - - - - - - - - {data.label} - - {settings.showCopyIcon && } - - - + + +
    + {settings.showNodeLabel && showContent ? ( + +
    + + + +
    + {data.label} + {settings.showCopyIcon && } +
    +
    - : - - - } + ) : ( + + + + + + {data.label} + + + )} - - - - - handleOpenSearchModal(data.address)}>Launch search - Copy content - Duplicate - - +
    +
    + + handleOpenSearchModal(data.email)}> + Launch search + + + + Copy content + ⌘ C + + + Duplicate + ⌘ D + + + Delete - - -
    - ); + ⌘ ⌫ + + + + ) } const MemoizedNode = (props: any) => ( - + -); +) + +export default memo(MemoizedNode) -export default memo(MemoizedNode); \ No newline at end of file diff --git a/src/components/investigations/nodes/social.tsx b/src/components/investigations/nodes/social.tsx index 70fc0a2..0e3ecf8 100644 --- a/src/components/investigations/nodes/social.tsx +++ b/src/components/investigations/nodes/social.tsx @@ -1,86 +1,112 @@ "use client" -import React, { memo } from 'react'; -import { Handle, Position, useStore } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes'; -import { NodeProvider, useNodeContext } from '../../contexts/node-context'; -import { cn, zoomSelector } from '@/src/lib/utils'; -import { useInvestigationContext } from '../../contexts/investigation-provider'; -import { usePlatformIcons } from '@/src/lib/hooks/use-platform-icons'; -import { CopyButton } from '../../copy'; -import { ZapIcon } from 'lucide-react'; -import { useSearchContext } from '../../contexts/search-context'; + +import { memo } from "react" +import { Handle, Position, useStore } from "@xyflow/react" +import { NodeProvider, useNodeContext } from "@/components/contexts/node-context" +import { cn, zoomSelector } from "@/lib/utils" +import { useInvestigationContext } from "@/components/contexts/investigation-provider" +import { usePlatformIcons } from "@/lib/hooks/use-platform-icons" +import { CopyButton } from "@/components/copy" +import { useSearchContext } from "@/components/contexts/search-context" +import { Card } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@/components/ui/context-menu" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" +import { Zap } from "lucide-react" function Custom({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() const { settings, currentNode } = useInvestigationContext() const platformsIcons = usePlatformIcons() const { handleOpenSearchModal } = useSearchContext() - const showContent = useStore(zoomSelector); + const showContent = useStore(zoomSelector) return ( - <> - - - - {settings.showNodeLabel && showContent ? - - - + + +
    + {settings.showNodeLabel && showContent ? ( + +
    + + {/* @ts-ignore */} + {platformsIcons?.[data?.platform]?.icon || "?"} + +
    + {data.username || data.profile_url} + {settings.showCopyIcon && } +
    +
    +
    + ) : ( + + + + - } - - - - - handleOpenSearchModal(data.username || data.profile_url)}>Launch search - Copy content - Duplicate - - - Delete - - - - - ); + {platformsIcons?.[data?.platform]?.icon || "?"} + + + + {data.username || data.profile_url} + + + )} + +
    +
    + + handleOpenSearchModal(data.username || data.profile_url)}> + Launch search + + + + Copy content + ⌘ C + + + Duplicate + ⌘ D + + + + Delete + ⌘ ⌫ + + +
    + ) } const SocialNode = (props: any) => ( -); +) + +export default memo(SocialNode) -export default memo(SocialNode); \ No newline at end of file diff --git a/src/components/login-form.tsx b/src/components/login-form.tsx index 12c5e38..6fdda56 100644 --- a/src/components/login-form.tsx +++ b/src/components/login-form.tsx @@ -1,27 +1,28 @@ import Link from "next/link" - -import { Button, Card, TextField, Box, Text, Flex } from "@radix-ui/themes" -import { login, signup, signInWithGithub } from '@/src/lib/actions/auth' +import { Button } from "@/components/ui/button" +import { login, signup, signInWithGithub } from '@/lib/actions/auth' +import { Card } from "./ui/card" +import { Input } from "./ui/input" export function LoginForm() { return ( - - +
    +
    Login - - +
    +
    Login into your account or create a new one. - - - +
    +
    +
    - +
    Don't have an account?{" "} ) } diff --git a/src/components/nav-main.tsx b/src/components/nav-main.tsx new file mode 100644 index 0000000..1d71af1 --- /dev/null +++ b/src/components/nav-main.tsx @@ -0,0 +1,73 @@ +"use client" + +import { ChevronRight, type LucideIcon } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, +} from "@/components/ui/sidebar" + +export function NavMain({ + items, +}: { + items: { + title: string + url: string + icon?: LucideIcon + isActive?: boolean + items?: { + title: string + url: string + }[] + }[] +}) { + return ( + + Platform + + {items.map((item) => ( + + + + + {item.icon && } + {item.title} + + + + + + {item.items?.map((subItem) => ( + + + + {subItem.title} + + + + ))} + + + + + ))} + + + ) +} diff --git a/src/components/nav-projects.tsx b/src/components/nav-projects.tsx new file mode 100644 index 0000000..f50b20d --- /dev/null +++ b/src/components/nav-projects.tsx @@ -0,0 +1,89 @@ +"use client" + +import { + Folder, + Forward, + MoreHorizontal, + Trash2, + type LucideIcon, +} from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarGroup, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuAction, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavProjects({ + projects, +}: { + projects: { + name: string + url: string + icon: LucideIcon + }[] +}) { + const { isMobile } = useSidebar() + + return ( + + Projects + + {projects.map((item) => ( + + + + + {item.name} + + + + + + + More + + + + + + View Project + + + + Share Project + + + + + Delete Project + + + + + ))} + + + + More + + + + + ) +} diff --git a/src/components/nav-user.tsx b/src/components/nav-user.tsx new file mode 100644 index 0000000..d12ef78 --- /dev/null +++ b/src/components/nav-user.tsx @@ -0,0 +1,114 @@ +"use client" + +import { + BadgeCheck, + Bell, + ChevronsUpDown, + CreditCard, + LogOut, + Sparkles, +} from "lucide-react" + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/components/ui/avatar" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function NavUser({ + user, +}: { + user: { + name: string + email: string + avatar: string + } +}) { + const { isMobile } = useSidebar() + + return ( + + + + + + + + CN + +
    + {user.name} + {user.email} +
    + +
    +
    + + +
    + + + CN + +
    + {user.name} + {user.email} +
    +
    +
    + + + + + Upgrade to Pro + + + + + + + Account + + + + Billing + + + + Notifications + + + + + + Log out + +
    +
    +
    +
    + ) +} diff --git a/src/components/team-switcher.tsx b/src/components/team-switcher.tsx new file mode 100644 index 0000000..2808e0a --- /dev/null +++ b/src/components/team-switcher.tsx @@ -0,0 +1,89 @@ +"use client" + +import * as React from "react" +import { ChevronsUpDown, Plus } from "lucide-react" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + useSidebar, +} from "@/components/ui/sidebar" + +export function TeamSwitcher({ + teams, +}: { + teams: { + name: string + logo: React.ElementType + plan: string + }[] +}) { + const { isMobile } = useSidebar() + const [activeTeam, setActiveTeam] = React.useState(teams[0]) + + return ( + + + + + +
    + +
    +
    + + {activeTeam.name} + + {activeTeam.plan} +
    + +
    +
    + + + Teams + + {teams.map((team, index) => ( + setActiveTeam(team)} + className="gap-2 p-2" + > +
    + +
    + {team.name} + ⌘{index + 1} +
    + ))} + + +
    + +
    +
    Add team
    +
    +
    +
    +
    +
    + ) +} diff --git a/src/components/theme-switch.tsx b/src/components/theme-switch.tsx index ffe351c..75912b0 100644 --- a/src/components/theme-switch.tsx +++ b/src/components/theme-switch.tsx @@ -1,27 +1,40 @@ -"use client"; +"use client" -import { FC } from "react"; -import { useTheme } from "next-themes"; -import { useIsSSR } from "@react-aria/ssr"; -import clsx from "clsx"; +import * as React from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" -import { SunIcon, MoonIcon } from "lucide-react"; -import { Switch } from "@radix-ui/themes"; +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" - -export const ThemeSwitch = ({ - -}) => { - const { theme, setTheme } = useTheme(); - const isSRR = useIsSSR() - - const onChange = () => { - theme === "light" ? setTheme("dark") : setTheme("light"); - }; +export function ThemeSwitch() { + const { setTheme } = useTheme() return ( -
    - {!isSRR && } -
    - ); -}; + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ) +} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..57760f2 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    +) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..5afd41d --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
    +)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
    +)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx new file mode 100644 index 0000000..51e507b --- /dev/null +++ b/src/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..60e6c96 --- /dev/null +++ b/src/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) =>