feat: shadcn

This commit is contained in:
dextmorgn
2025-02-13 17:01:08 +01:00
parent 00464a6f17
commit 99877eb816
91 changed files with 5144 additions and 1815 deletions

21
components.json Normal file
View File

@@ -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"
}

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 (
<div className="max-w-6xl mx-auto p-6 space-y-24">
<div>
<div className="flex flex-col justify-between items-start mb-4">
<div className="flex justify-between w-full items-center">
<div>
<h1 className="text-3xl font-semibold mb-2">Vos clés API</h1>
<p className="mb-6 opacity-70">
Gérez vos clés API personnelles pour votre espace de travail.
</p>
</div>
<Button
>
Créer une nouvelle clé
</Button>
</div>
<Separator className='w-full' size={"4"} />
<p className="mb-8 mt-4 opacity-70">
Vous pouvez créer jusqu&apos;à 10 clés API et les supprimer lorsque vous n&apos;en avez plus besoin.
</p>
</div>
<Tabs.Root defaultValue="active" className="mb-3">
<Tabs.List>
<Tabs.Trigger
value="active"
>
Actif
</Tabs.Trigger>
<Tabs.Trigger
value="expired"
>
Expiré
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<Card>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell className="w-[300px]">Nom</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Clé</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell className="text-right">Date d&apos;expiration</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell className="w-[50px]"></Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.RowHeaderCell>clé d&apos;essai</Table.RowHeaderCell>
<Table.Cell className="font-mono">************************pnA5</Table.Cell>
<Table.Cell className="text-right">15 février 2025</Table.Cell>
<Table.Cell>
<IconButton variant="ghost">
<Trash2 className="h-4 w-4" />
</IconButton>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Card>
</div>
<div>
<div className="flex flex-col justify-between items-start mb-4">
<div className="flex justify-between w-full items-center">
<div>
<h1 className="text-3xl font-semibold mb-2">Vos clés de services tiers</h1>
<p className="mb-6 opacity-70">
Gérez vos clés API de services tiers pour votre espace de travail.
</p>
</div>
<Button
>
Créer une nouvelle clé
</Button>
</div>
</div>
<Tabs.Root defaultValue="active" className="mb-3">
<Tabs.List>
<Tabs.Trigger
value="active"
>
Actif
</Tabs.Trigger>
<Tabs.Trigger
value="expired"
>
Expiré
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<Card>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeaderCell className="w-[300px]">Nom</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell>Clé</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell className="text-right">Date d&apos;expiration</Table.ColumnHeaderCell>
<Table.ColumnHeaderCell className="w-[50px]"></Table.ColumnHeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.RowHeaderCell>clé d&apos;essai</Table.RowHeaderCell>
<Table.Cell className="font-mono">************************pnA5</Table.Cell>
<Table.Cell className="text-right">15 février 2025</Table.Cell>
<Table.Cell>
<IconButton variant="ghost">
<Trash2 className="h-4 w-4" />
</IconButton>
</Table.Cell>
</Table.Row>
</Table.Body>
</Table.Root>
</Card>
</div>
</div>
)
const page = () => {
return (
<div>keys</div>
)
}
export default page

View File

@@ -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 (
<Flex style={{ height: "100vh", overflow: "hidden" }}>
{/* Left Panel */}
<Flex direction="column" style={{ width: "300px", borderRight: "1px solid rgba(128, 128, 128, 0.2)" }}>
{/* Left Panel Header */}
<Box
style={{
height: "48px",
borderBottom: "1px solid rgba(128, 128, 128, 0.2)",
padding: "8px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Logo />
<NewCase>
<IconButton color="gray" size="2" variant="soft">
<PlusIcon className="h-5" />
</IconButton>
</NewCase>
</Box>
{/* Left Panel Content */}
<ScrollArea type="auto" scrollbars="vertical" style={{ flex: 1, overflow: "auto" }}>
<Flex direction="column" gap="4" p="4">
{navigation.map((nav, i) => (
<Flex direction="column" gap="1" key={i}>
<Text style={{ opacity: 0.6 }} weight="light">
{nav.name}
</Text>
<Flex direction="column" gap="1">
{nav?.children?.map((child, j) => (
<Link key={j} href={child.href} style={{ color: "inherit", textDecoration: "none" }}>
<Text
className="hover:text-sky-400 hover:underline"
weight="medium"
style={{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{child.name}
</Text>
</Link>
))}
</Flex>
</Flex>
))}
</Flex>
</ScrollArea>
</Flex>
{/* Main Content */}
<Flex direction="column" style={{ flex: 1, overflow: "hidden" }}>
{/* Top Navigation */}
<Box
style={{
height: "48px",
borderBottom: "1px solid rgba(128, 128, 128, 0.2)",
padding: "8px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<TextField.Root placeholder="Search an investigation..." style={{ width: "100%", maxWidth: "240px", height: "32px" }}>
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root>
<User user={data?.user} />
</Box>
{/* Main Content Area */}
<ScrollArea type="auto" scrollbars="vertical" style={{ flex: 1, overflowY: "auto" }}>
<Box p="24px">{children}</Box>
</ScrollArea>
</Flex>
</Flex>
)
}
export default DashboardLayout

View File

@@ -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 <div>An error occured.</div>
return (
<div className='space-y-6 max-w-6xl mx-auto p-6'>
<div>
<h1 className="text-3xl font-semibold mb-2">Vos investigations</h1>
<p className="mb-6 opacity-70">
La liste de vos investigations. Vous pouvez également en créer d'autres.
</p>
</div>
<Flex gap="3">
<NewCase>
<Button><PlusIcon className='h-4 w-4' />Create</Button>
</NewCase>
<Button color="gray" variant='soft'><DownloadIcon className='h-4 w-4' />Upload</Button>
<Button color="gray" variant='soft'><FolderIcon className='h-4 w-4' />Create organization</Button>
</Flex>
<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-5'>{investigations.map((investigation) => (
<Investigation key={investigation.id} investigation={investigation} />
))}
</div>
export default function Page() {
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
<div className="aspect-video rounded-xl bg-muted/50" />
</div>
<div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min" />
</div>
)
</SidebarInset>
</SidebarProvider>
)
}
export default DashboardPage

View File

@@ -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 (
<div className='flex flex-col gap-2 p-1'>
<Flex justify={"between"} align={"center"}><Text size={"2"}>Profiles</Text>
<IconButton className='!p-1 !mr-2' radius='medium' disabled={isLoading} onClick={() => refetch()} size="1" variant="ghost" color="gray">
<div className='flex item-center justify-between'><div>Profiles</div>
<Button size="icon" className='!p-1 !mr-2' disabled={isLoading} onClick={() => refetch()} variant="ghost" >
<RotateCwIcon width="15" height="15" />
</IconButton>
</Flex>
<Flex direction={"column"} gap="2">
</Button>
</div>
<div className='flex flex-col gap-2'>
{isLoading && <>
<Skeleton height={"48px"} />
<Skeleton height={"48px"} />
<Skeleton height={"48px"} />
<Skeleton className='w-full h-[30px]' />
<Skeleton className='w-full h-[30px]' />
<Skeleton className='w-full h-[30px]' />
</>
}
{individuals?.map((individual: any) => (
<Box key={individual.id}>
<Card size={"1"} className={cn('!p-2 relative group cursor-pointer border border-transparent hover:border-sky-400', currentNode === individual.id && 'border-sky-400')} onClick={() => setCurrentNode(individual.id)}>
<Flex gap="3" align="center">
<Avatar
size="1"
src={individual?.image_url}
radius="full"
color="gray"
fallback={individual?.full_name[0]}
/>
<Box>
<Text as="div" size="2" weight="bold">
<div key={individual.id}>
<Card className={cn('!p-2 relative group cursor-pointer border border-transparent hover:border-sky-400', currentNode === individual.id && 'border-sky-400')} onClick={() => setCurrentNode(individual.id)}>
<div className='flex gap-3 items-center'>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<div>
<div>
{individual.full_name}
</Text>
<Text as="div" size="2" color="gray">
</div>
<div>
{individual.notes}
</Text>
</Box>
</Flex>
<IconButton
</div>
</div>
</div>
<Button
className='!absolute !top-2 !right-2 !hidden group-hover:!block'
type="button"
variant="ghost"
size={"1"}
size={"icon"}
onClick={(e) => { e.stopPropagation(); handleOpenIndividualModal(individual.id) }}
aria-label={"Edit profile"}
>
<Pencil1Icon />
</IconButton>
</Button>
</Card>
</Box>
</div>
))}
</Flex>
</div >
</div>
</div>
)
}

View File

@@ -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,

View File

@@ -1,9 +1,8 @@
import { Spinner } from '@radix-ui/themes'
import React from 'react'
const loading = () => {
return (
<div className='h-[90vh] w-full flex items-center justify-center gap-2'><Spinner /> Loading nodes and edges...</div>
<div className='h-[90vh] w-full flex items-center justify-center gap-2'>Loading nodes and edges...</div>
)
}

View File

@@ -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,

View File

@@ -1,9 +1,8 @@
import { Spinner } from '@radix-ui/themes'
import React from 'react'
const loading = () => {
return (
<div className='h-screen w-screen flex items-center justify-center gap-2'><Spinner /> Loading investigation...</div>
<div className='h-screen w-screen flex items-center justify-center gap-2'>Loading investigation...</div>
)
}

View File

@@ -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({
<head />
<body
className={clsx(
"min-h-screen dark:bg-[#1C1C1C] font-sans antialiased",
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
)}
>
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
<Theme>
<main>
<NextTopLoader />
<main>
<NextTopLoader />
<TooltipProvider>
<ConfirmContextProvider>
{children}
</ConfirmContextProvider>
</main>
</Theme>
</TooltipProvider>
</main>
</Providers>
</body>
</html>

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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<typeof Sidebar>) {
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
<TeamSwitcher teams={data.teams} />
</SidebarHeader>
<SidebarContent>
<NavMain items={data.navMain} />
<NavProjects projects={data.projects} />
</SidebarContent>
<SidebarFooter>
<NavUser user={data.user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -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<ChatContextType | undefined>(undefined);
const ChatContext = createContext<ChatContextType | undefined>(undefined)
interface ChatProviderProps {
children: ReactNode;
children: ReactNode
}
export const ChatProvider: React.FC<ChatProviderProps> = ({ 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<any>(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 (
<ChatContext.Provider value={{ open, handleOpenChat }}>
{children}
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content maxWidth={messages?.length > 0 ? "730px" : '480px'} className="w-full p-4">
<Dialog.Title>Ask AI</Dialog.Title>
<Dialog.Description size="2" mb="4">
Ask AI chat bot to help you understant patterns.</Dialog.Description>
{/* <div>
context: {content?.title || content?.full_name}
</div> */}
<Flex direction={"column"} gap="3" className=" overflow-y-auto w-full">
{messages.map(m => (
<Flex key={m.id} className='grow'>
<Flex gap="1" align="start" justify={m.role === 'user' ? "end" : "start"} direction={m.role === 'user' ? "row-reverse" : "row"} className='w-full'>
<Avatar
size="3"
color={m.role === 'user' ? 'blue' : 'orange'}
radius="full"
fallback={m.role === 'user' ? 'U' : isLoading ? <Spinner /> : <BotIcon className='h-4' />}
/>
<Flex direction={"column"} align={m.role === 'user' ? 'end' : 'start'} gap="1" className='w-full'>
<Text size="2" as='div' weight="bold" className={cn(m.role === 'user' ? 'text-right' : 'text-left')}>
{isLoading && m.role !== 'user' ? <LoadingDots /> : m.role === 'user' ? 'User' : 'Chatbot'}
</Text>
<Card className='max-w-[80%]'>
<Box>
<Text as="div" size="2" color="gray">
<ReactMarkdown>{m.content}</ReactMarkdown>
</Text>
</Box>
</Card>
</Flex>
</Flex>
</Flex>
))}
{error && <Flex className='grow'>
<Flex gap="1" align="start" justify={"start"} direction={"row"} className='w-full'>
<Avatar
size="3"
color={'red'}
radius="full"
fallback={<BotIcon className='h-4' />}
/>
<Flex direction={"column"} align={'start'} gap="1" className='w-full'>
<Text size="2" as='div' weight="bold" color='red' className={cn('text-left')}>
{'Error'}
</Text>
<Card className='max-w-[70%]'>
<Box>
<Text as="div" size="2" color="red">
{"Oops, an error occured. Make sure you provided a valid Mistral API key."}
</Text>
</Box>
</Card>
</Flex>
</Flex>
</Flex>}
</Flex>
<form onSubmit={handleSubmit} className="mt-4">
<Box width={"100%"}>
<TextField.Root
value={input}
onChange={handleInputChange}
size="2" placeholder="'What pattern can you extract from those relations ?'" />
</Box>
</form>
</Dialog.Content>
</Dialog.Root>
</ChatContext.Provider>
);
return (
<ChatContext.Provider value={{ open, handleOpenChat }}>
{children}
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className={cn("w-full p-4", messages?.length > 0 ? "sm:max-w-[730px]" : "sm:max-w-[480px]")}>
<DialogHeader>
<DialogTitle>Ask AI</DialogTitle>
<DialogDescription>Ask AI chat bot to help you understand patterns.</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-3 overflow-y-auto w-full max-h-[60vh]">
{messages.map((m) => (
<div key={m.id} className="flex grow">
<div
className={cn(
"flex gap-1 items-start w-full",
m.role === "user" ? "flex-row-reverse justify-end" : "flex-row justify-start",
)}
>
<Avatar className={cn("h-8 w-8", m.role === "user" ? "bg-blue-500" : "bg-orange-500")}>
<AvatarFallback>
{m.role === "user" ? (
<User className="h-4 w-4" />
) : isLoading ? (
<LoadingDots />
) : (
<Bot className="h-4 w-4" />
)}
</AvatarFallback>
</Avatar>
<div className={cn("flex flex-col gap-1 w-full", m.role === "user" ? "items-end" : "items-start")}>
<span className={cn("text-sm font-bold", m.role === "user" ? "text-right" : "text-left")}>
{isLoading && m.role !== "user" ? <LoadingDots /> : m.role === "user" ? "User" : "Chatbot"}
</span>
<Card className="max-w-[80%]">
<CardContent className="p-3">
<ReactMarkdown>{m.content}</ReactMarkdown>
</CardContent>
</Card>
</div>
</div>
</div>
))}
{error && (
<div className="flex grow">
<div className="flex gap-1 items-start justify-start w-full">
<Avatar className="h-8 w-8 bg-red-500">
<AvatarFallback>
<Bot className="h-4 w-4" />
</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1 items-start w-full">
<span className="text-sm font-bold text-red-500">Error</span>
<Card className="max-w-[80%]">
<CardContent className="p-3 text-red-500">
Oops, an error occurred. Make sure you provided a valid Mistral API key.
</CardContent>
</Card>
</div>
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="mt-4">
<Input
value={input}
onChange={handleInputChange}
placeholder="'What pattern can you extract from those relations ?'"
className="w-full"
/>
<Button type="submit" className="mt-2">
Send
</Button>
</form>
</DialogContent>
</Dialog>
</ChatContext.Provider>
)
}
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<LoadingDotsProps> = ({ 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 (
<div className="flex items-center">
<span>{text}</span>
<span className="w-8 text-left">{dots}</span>
</div>
)
}
return (
<div className="flex items-center">
<span>{text}</span>
<span className="w-8 text-left">{dots}</span>
</div>
)
}

View File

@@ -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<InvestigationProviderProps> = ({ ch
return (
<InvestigationContext.Provider value={{ filters, setFilters, settings, setSettings, setOpenSettingsModal, investigation, isLoadingInvestigation, handleOpenIndividualModal, handleDeleteInvestigation, currentNode, setCurrentNode, panelOpen, setPanelOpen }}>
{children}
<Dialog.Root open={openSettingsModal}>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Description size="2" mb="4">
<Dialog open={openSettingsModal}>
<DialogContent>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>
Make changes to your settings.
</Dialog.Description>
<Flex direction="column" gap="3">
</DialogDescription>
<div className="flex flex-col gap-3">
<SettingSwitch setting={"showNodeLabel"} value={settings.showNodeLabel} title={"Show labels on nodes"} description={"Displays the labels on the nodes, like username or avatar."} />
<SettingSwitch setting={"showEdgeLabel"} value={settings.showEdgeLabel} title={"Show labels on edges"} description={"Displays the labels on the edges, like relation type."} />
<SettingSwitch setting={"showMiniMap"} value={settings.showMiniMap} title={"Show minimap on the canva"} description={"Displays the minimap on canva."} />
@@ -109,16 +117,16 @@ export const InvestigationProvider: React.FC<InvestigationProviderProps> = ({ ch
</div>
<ThemeSwitch />
</div>
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close onClick={() => setOpenSettingsModal(false)}>
<Button variant="soft" color="gray">
</div>
<div className="flex items-center gap-2 justify-end">
<DialogClose asChild onClick={() => setOpenSettingsModal(false)}>
<Button variant="outline">
Cancel
</Button>
</Dialog.Close>
</Flex>
</Dialog.Content>
</Dialog.Root>
</DialogClose>
</div>
</DialogContent>
</Dialog>
</InvestigationContext.Provider >
);
};

View File

@@ -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<NodeProviderProps> = (props: any) => {
return (
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading }}>
{props.children}
<Dialog.Root open={openAddNodeModal && nodeType} onOpenChange={setOpenNodeModal}>
<Dialog.Content maxWidth="450px">
<Dialog.Title>New {nodeType?.type}</Dialog.Title>
<Dialog.Description size="2" mb="4">
<Dialog open={openAddNodeModal && nodeType} onOpenChange={setOpenNodeModal}>
<DialogContent>
<DialogTitle>New {nodeType?.type}</DialogTitle>
<DialogDescription>
Add a new related {nodeType?.type}.
</Dialog.Description>
</DialogDescription>
<form onSubmit={onSubmitNewNodeModal}>
<Flex direction="column" gap="3">
<div className="flex flex-col ga-3">
{nodeType?.fields.map((field: any, i: number) => {
const [key, value] = field.split(":")
return (
<label key={i}>
<Text as="div" size="2" mb="1" weight="bold">
<p>
{key}
</Text>
<TextField.Root
</p>
<Input
defaultValue={value || ""}
// disabled={Boolean(value)}
name={key}
@@ -169,26 +178,26 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
</label>
)
})}
</Flex>
{error && <Callout.Root className="mt-4" color="red">
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
{error}
</Callout.Text>
</Callout.Root>}
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
</div>
{error &&
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error}
</AlertDescription>
</Alert>}
<div className="flex items-center gap-2 justify-end mt-4">
<DialogClose asChild>
<Button variant="outline">
Cancel
</Button>
</Dialog.Close>
<Button loading={loading} type="submit">Save</Button>
</Flex>
</DialogClose>
<Button disabled={loading} type="submit">Save</Button>
</div>
</form>
</Dialog.Content>
</Dialog.Root>
</DialogContent>
</Dialog>
</NodeContext.Provider>
);
};

View File

@@ -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<SearchContextType | undefined>(undefined);
const SearchContext = createContext<SearchContextType | undefined>(undefined)
interface SearchProviderProps {
children: ReactNode;
children: ReactNode
}
export const SearchProvider: React.FC<SearchProviderProps> = (props: any) => {
export const SearchProvider: React.FC<SearchProviderProps> = ({ children }) => {
const [openSearchModal, setOpenSearchModal] = useState(false)
const { investigation_id } = useParams()
const [value, setValue] = useState<string | null>(null)
@@ -31,10 +42,10 @@ export const SearchProvider: React.FC<SearchProviderProps> = (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<SearchProviderProps> = (props: any) => {
}
return (
<SearchContext.Provider {...props} value={{ openSearchModal, handleOpenSearchModal }}>
{props.children}
<Dialog.Root open={openSearchModal} onOpenChange={handleCloseModal}>
<Dialog.Content maxWidth={results ? "950px" : "450px"}>
<Dialog.Title>New search</Dialog.Title>
<Dialog.Description size="2" mb="4">
Make a new keyword associated research.
</Dialog.Description>
<SearchContext.Provider value={{ openSearchModal, handleOpenSearchModal }}>
{children}
<Dialog open={openSearchModal} onOpenChange={handleCloseModal}>
<DialogContent className={results ? "sm:max-w-[950px]" : "sm:max-w-[450px]"}>
<DialogHeader>
<DialogTitle>New search</DialogTitle>
<DialogDescription>Make a new keyword associated research.</DialogDescription>
</DialogHeader>
<form onSubmit={onSubmitNewSearch}>
<Flex direction="column" gap="3">
<Card><Text weight={"regular"}>value: </Text><Text weight={"medium"}>"{value}"</Text></Card>
{/* <Callout.Root size="1">
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
This query may return some (a lot) or no results. Make sure you filter your results manually afterwards.
</Callout.Text>
</Callout.Root> */}
{error &&
<Callout.Root color="red" size="1">
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
{JSON.stringify(error)}
</Callout.Text>
</Callout.Root>}
{results && Array.isArray(results) ?
<div className="flex flex-col gap-3">
<Card className="p-4">
<p>
value: <span className="font-medium">"{value}"</span>
</p>
</Card>
{error && (
<Alert variant="destructive">
<InfoIcon className="h-4 w-4" />
<AlertDescription>{JSON.stringify(error)}</AlertDescription>
</Alert>
)}
{results && Array.isArray(results) ? (
<>
<Callout.Root color="orange" size="1">
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
<Alert variant="destructive">
<InfoIcon className="h-4 w-4" />
<AlertDescription>
"{value}" has appeared in {results.length} data breach(es). Here are the infos we have:
</Callout.Text>
</Callout.Root>
</AlertDescription>
</Alert>
<Breaches breaches={results} />
</> : results && <Callout.Root color="green" size="1">
<Callout.Icon>
<InfoCircledIcon />
</Callout.Icon>
<Callout.Text>
{JSON.stringify(results)}
</Callout.Text>
</Callout.Root>
}
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Close
</>
) : (
results && (
<Alert variant="default">
<InfoIcon className="h-4 w-4" />
<AlertDescription>{JSON.stringify(results)}</AlertDescription>
</Alert>
)
)}
</div>
<DialogFooter className="mt-4">
<Button variant="outline" onClick={handleCloseModal}>
Close
</Button>
{!results && (
<Button disabled={isLoading} type="submit">
{isLoading ? "Saving..." : "Save"}
</Button>
</Dialog.Close>
{!results &&
<Button autoFocus loading={isLoading} type="submit">Save</Button>
}
</Flex>
)}
</DialogFooter>
</form>
</Dialog.Content>
</Dialog.Root>
</DialogContent>
</Dialog>
</SearchContext.Provider>
);
};
)
}
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;
};
return context
}

View File

@@ -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 (
<Tooltip open={isCopied} content="Copied !">
<IconButton size={"1"} color="gray" radius="large" variant="ghost" onClick={handleCopy} aria-label="Copy content">
<Copy className="h-3 w-3 opacity-50" />
</IconButton>
<Tooltip open={isCopied}>
<TooltipTrigger asChild>
<Button className={className} size={"icon"} variant="ghost" onClick={handleCopy} aria-label="Copy content">
<Copy className="h-3 w-3 opacity-50" />
</Button>
</TooltipTrigger>
<TooltipContent>
Copied !
</TooltipContent>
</Tooltip>
)
}

View File

@@ -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 (
<Link href={`/investigations/${investigation.id}`} >
<Box className="w-full h-full">
<div className="w-full h-full">
<Card className='h-full hover:border-sky-400 border border-transparent'>
<Inset>
<div className="group relative h-full overflow-hidden rounded-lg">
<div className="aspect-[4/3] overflow-hidden">
<Image
src={"https://c6p3q0oludlschf8w.lite.vusercontent.net/placeholder.svg"}
alt={investigation.title}
width={400}
height={300}
className="h-full w-full object-cover transition-transform group-hover:scale-115 dark:invert"
/>
<div className="group relative h-full overflow-hidden rounded-lg">
<div className="aspect-[4/3] overflow-hidden">
<Image
src={"https://c6p3q0oludlschf8w.lite.vusercontent.net/placeholder.svg"}
alt={investigation.title}
width={400}
height={300}
className="h-full w-full object-cover transition-transform group-hover:scale-115 dark:invert"
/>
</div>
<div className="p-4 h-full">
<div>
{investigation.title}
</div>
<div className="p-4 h-full">
<Text as="div" size="2" weight="bold">
{investigation.title}
</Text>
<Text as="div" size="2" color="gray">
{investigation.description || <span className='italic'>No description provided.</span>}
</Text>
<div>
{investigation.description || <span className='italic'>No description provided.</span>}
</div>
</div>
</Inset>
</div>
</Card>
</Box>
</div>
</Link>
)
}

View File

@@ -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<HTMLFormElement>) {
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 (
<>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
{children}
</DropdownMenu.Trigger>
<DropdownMenu.Content size="2">
<DropdownMenu.Item onClick={() => setOpen(true)} shortcut="⌘ E">New case</DropdownMenu.Item>
<DropdownMenu.Item disabled shortcut="⌘ D">New organization <Badge radius="full" color="orange" size={"1"}>Soon</Badge></DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Content maxWidth="450px">
<DropdownMenu>
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onSelect={() => setOpen(true)}>
New case
<span className="ml-auto text-xs text-muted-foreground"> E</span>
</DropdownMenuItem>
<DropdownMenuItem disabled>
New organization
<Badge variant="outline" className="ml-2">
Soon
</Badge>
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>New case</DialogTitle>
<DialogDescription>Create a new blank case.</DialogDescription>
</DialogHeader>
<form onSubmit={handleNewCase}>
<Box>
<Dialog.Title>New case</Dialog.Title>
<Dialog.Description size="2" mb="4">
Create a new blank case.
</Dialog.Description>
<Flex direction="column" gap="3">
<label>
<Text as="div" size="2" mb="1" weight="bold">
Investigation name
</Text>
<TextField.Root
required
name="title"
placeholder="Suspicion de fraude"
/>
</label>
<label>
<Text as="div" size="2" mb="1" weight="bold">
Description
</Text>
<TextField.Root
name="description"
placeholder="Investigation sur une campagne de phishing via LinkedIn."
/>
</label>
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
<Button type="submit">Save</Button>
</Flex>
</Box>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="title">Investigation name</Label>
<Input id="title" name="title" placeholder="Suspicion de fraude" required />
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
name="description"
placeholder="Investigation sur une campagne de phishing via LinkedIn."
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</Dialog.Content>
</Dialog.Root >
</DialogContent>
</Dialog>
</>
);
)
}

View File

@@ -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<string>; }; }) => {
setSearch(event.target.value);
refetch && refetch();
};
const debouncedChangeHandler = useCallback(debounce(changeHandler, 300), []);
const changeHandler = (event: { target: { value: SetStateAction<string> } }) => {
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) =>
(<li onClick={() => setOpen(false)}>
<Card className='cursor-pointer hover:border-sky-400 border border-transparent' onClick={() => setCurrentNode(item.id)}>
<span className='flex flex-col gap-1 text-left'>
<span className='text-xs opacity-50'>Individual</span>
<span className='flex items-center gap-1'>
<span className='truncate text-ellipsis'>
<Highlighter
searchWords={search.split(" ")}
autoEscape={true}
textToHighlight={item?.full_name}
/>
</span>
<span className='truncate text-ellipsis text-sm opacity-75'>
<Highlighter
searchWords={search.split(" ")}
autoEscape={true}
textToHighlight={item?.notes}
/>
</span>
</span>
{/* <span className='text-sm opacity-75'>
<Highlighter
searchWords={search.split(" ")}
autoEscape={true}
textToHighlight={item.netname}
/>
</span> */}
</span>
</Card>
</li>
const SearchItem = ({ item }: any) => (
<li onClick={() => setOpen(false)}>
<Card className='cursor-pointer hover:border-sky-400 border border-transparent' onClick={() => setCurrentNode(item.id)}>
<CardContent className='p-3'>
<div className='flex flex-col gap-1 text-left'>
<span className='text-xs text-muted-foreground'>Individual</span>
<div className='flex items-center gap-1'>
<span className='truncate'>
<Highlighter
searchWords={search.split(" ")}
autoEscape={true}
textToHighlight={item?.full_name}
/>
</span>
<span className='truncate text-sm text-muted-foreground'>
<Highlighter
searchWords={search.split(" ")}
autoEscape={true}
textToHighlight={item?.notes}
/>
</span>
</div>
</div>
</CardContent>
</Card>
</li>
)
return (
<>
<Dialog.Root open={open} onOpenChange={handleClose}>
<IconButton onClick={() => setOpen(true)} color="gray" size="2" variant="soft">
<SearchIcon className="h-4" />
</IconButton>
<Dialog.Content maxWidth="450px">
<Dialog.Title>Search</Dialog.Title>
<Dialog.Description size="2" mb="4">
Find the profile you're looking for.
</Dialog.Description>
<TextField.Root
defaultValue={search}
onChange={debouncedChangeHandler}
placeholder="Your search here…">
<TextField.Slot>
<MagnifyingGlassIcon height="16" width="16" />
</TextField.Slot>
</TextField.Root>
<div className='w-full relative text-center flex flex-col items-center justify-center gap-2'>
{error && "An error occured."}
{isLoading && <Spinner />}
{results?.length === 0 && `No results found for "${search}".`}
<Flex direction={"column"}>
<ScrollArea className='max-h-[60vh] p-2'>
<ul className='w-full h-full flex flex-col gap-1 mt-2'>{!error && !isLoading && Array.isArray(results) && results?.map((item) => (
<SearchItem key={item.id} item={item} />
))}
<Dialog open={open} onOpenChange={handleClose}>
<Button variant="outline" size="icon" onClick={() => setOpen(true)}>
<Search className="h-4 w-4" />
</Button>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Search</DialogTitle>
<DialogDescription>
Find the profile you're looking for.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<Input
defaultValue={search}
onChange={debouncedChangeHandler}
placeholder="Your search here…"
/>
<div className='w-full relative text-center flex flex-col items-center justify-center gap-2'>
{error && <p className="text-red-500">An error occurred.</p>}
{isLoading && <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>}
{results?.length === 0 && <p>No results found for "{search}".</p>}
<ScrollArea className="h-[60vh] w-full rounded-md border p-4">
<ul className='space-y-2'>
{!error && !isLoading && Array.isArray(results) && results?.map((item) => (
<SearchItem key={item.id} item={item} />
))}
</ul>
</ScrollArea>
</Flex>
{/* {search === '' && (
<div className="px-6 py-14 text-center flex items-center justify-center flex-col text-sm sm:px-14">
<SearchIcon
className="h-12 w-12 opacity-40"
aria-hidden="true"
/>
<p className="mt-4 font-semibold">Search for ip_addresses, domain names and other</p>
<p className="mt-2 opacity-70">
Put you search between quotes for an exact match search.
</p>
</div>
)} */}
</div>
</div>
</Dialog.Content>
</Dialog.Root>
</DialogContent>
</Dialog>
</>
)
}
export default SearchModal
export default SearchModal

View File

@@ -1,6 +1,6 @@
import * as React from "react";
import { IconSvgProps } from "@/src/types";
import { IconSvgProps } from "@/types";
export const Logo: React.FC<IconSvgProps> = ({
size = 36,

View File

@@ -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 (
<div className="ml-2 flex items-center">
<Spinner loading={isLoading || isLoadingInvestigation}>
<Select.Root onValueChange={handleSelectionChange} defaultValue={investigation?.id}>
<Select.Trigger className="min-w-none w-full text-ellipsis truncate" variant="ghost" />
<Select.Content>
{isLoading || isLoadingInvestigation ? <Skeleton className="h-8 w-40" /> :
<Select onValueChange={handleSelectionChange} defaultValue={investigation?.id}>
<SelectTrigger className="min-w-none w-full text-ellipsis truncate">
<SelectValue defaultValue={investigation?.title || ""} placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
{investigations?.map((investigation) => (
<Select.Item className="text-ellipsis truncate" key={investigation.id} value={investigation.id}>{investigation.title}</Select.Item>
<SelectItem className="text-ellipsis truncate" key={investigation.id} value={investigation.id}>{investigation.title}</SelectItem>
))}
</Select.Content>
</Select.Root >
</Spinner>
</SelectContent>
</Select >}
</div>
);
}

View File

@@ -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) {
<BaseEdge id={id} path={edgePath} style={style} />
<EdgeLabelRenderer>
{settings.showEdgeLabel &&
<Badge size={"1"} color={label === "relation" ? 'orange' : "blue"} style={{
<Badge color={label === "relation" ? 'orange' : "blue"} style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',

View File

@@ -1,100 +0,0 @@
"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { Check } from 'lucide-react'
export default function DisplaySelector({ values }: { values: string[] }) {
const [selected, setSelected] = useState<string[]>([])
const toggleSelect = (val: string) => {
setSelected((prev) =>
prev.includes(val) ? prev.filter((c) => c !== val) : [...prev, val]
)
}
return (
<div className="">
<motion.div
className="flex flex-wrap gap-1 overflow-visible"
layout
transition={{
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.5,
}}
>
{values.map((value: string, i: number) => {
const isSelected = selected.includes(value)
return (
<motion.button
key={i}
onClick={() => 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)]"}
`}
>
<motion.div
className="relative flex items-center"
animate={{
width: isSelected ? "auto" : "100%",
paddingRight: isSelected ? "1rem" : "0",
}}
transition={{
ease: [0.175, 0.885, 0.32, 1.275],
duration: 0.3,
}}
>
<span>{value}</span>
<AnimatePresence>
{isSelected && (
<motion.span
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
transition={{
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.5
}}
className="absolute right-0"
>
<div className="w-3 h-3 rounded-full bg-[#ff9066] flex items-center justify-center">
<Check className="w-2 h-2 text-[#2a1711]" strokeWidth={1.5} />
</div>
</motion.span>
)}
</AnimatePresence>
</motion.div>
</motion.button>
)
})}
</motion.div>
</div>
)
}

View File

@@ -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,

View File

@@ -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) {
/>
<EdgeLabelRenderer>
{settings.showEdgeLabel &&
<Badge size={"1"} color={label === "relation" ? 'orange' : "blue"} style={{
<Badge color={label === "relation" ? 'orange' : "blue"} style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',

View File

@@ -15,10 +15,10 @@ import {
Node,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { supabase } from '@/src/lib/supabase/client';
import { supabase } from '@/lib/supabase/client';
import IndividualNode from './nodes/individual';
import PhoneNode from './nodes/phone';
import CustomEdge from './custom-edge';
// import CustomEdge from './custom-edge';
import IpNode from './nodes/ip_address';
import EmailNode from './nodes/email';
import SocialNode from './nodes/social'
@@ -26,13 +26,16 @@ import AddressNode from './nodes/physical_address'
import { AlignCenterHorizontal, AlignCenterVertical, LockOpenIcon, MaximizeIcon, RotateCcwIcon, ZoomInIcon, ZoomOutIcon, LockIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import NewActions from './new-actions';
import { IconButton, Tooltip, Spinner, Card, Flex, SegmentedControl } from '@radix-ui/themes';
import { getIncomers, getOutgoers } from "@xyflow/react";
import { EdgeBase } from '@xyflow/system';
import { useInvestigationContext } from '../contexts/investigation-provider';
import { useInvestigationContext } from '@/components/contexts/investigation-provider';
import FloatingEdge from './floating-edge';
import FloatingConnectionLine from './floating-connection';
import { useParams } from 'next/navigation';
import { Tooltip, TooltipContent } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import { TooltipTrigger } from '@radix-ui/react-tooltip';
import { Card } from '@/components/ui/card';
const edgeTypes = {
"custom": FloatingEdge
@@ -294,71 +297,81 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
edgeTypes={edgeTypes}
>
<Panel position="top-left" className='flex items-center gap-1'>
<Tooltip content="Auto layout (vertical)">
<IconButton color="gray" variant="soft" onClick={() => onLayout('TB')}>
<AlignCenterVertical className='h-4 w-4' />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => onLayout('TB')}>
<AlignCenterVertical className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Auto layout (vertical)
</TooltipContent>
</Tooltip>
<Tooltip content="Auto layout (horizontal)">
<IconButton color="gray" variant="soft" onClick={() => onLayout('LR')}>
<AlignCenterHorizontal className='h-4 w-4' />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="outline" onClick={() => onLayout('LR')}>
<AlignCenterHorizontal className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Auto layout (horizontal)
</TooltipContent>
</Tooltip>
</Panel>
<Panel position="top-center" className='flex items-center gap-1'>
<SegmentedControl.Root defaultValue="graph">
<SegmentedControl.Item value="graph">Graph</SegmentedControl.Item>
<SegmentedControl.Item value="timeline">Timeline</SegmentedControl.Item>
<SegmentedControl.Item value="map">Map</SegmentedControl.Item>
</SegmentedControl.Root>
</Panel>
<Panel position="top-right" className='flex items-center gap-1'>
<Flex direction={"column"} align={"end"} gap={"1"}>
<Flex gap="1">
<Tooltip content="Reload schema">
<IconButton onClick={() => window.location.reload()} variant="soft">
<RotateCcwIcon className='h-4 w-4' />
</IconButton>
</Tooltip>
<div className='flex flex-col items-end gap-2'>
<div className='flex gap-1 items-center'>
<Button size="icon" variant="outline" onClick={() => window.location.reload()}>
<RotateCcwIcon className='h-4 w-4' />
</Button>
<NewActions addNodes={addNodes} />
</Flex>
</div>
{currentNode &&
<Card>
{/* @ts-ignore */}
{getNode(currentNode)?.data?.label}
</Card>}
</Flex>
</div>
</Panel>
<Panel position="bottom-left" className='flex flex-col items-center gap-1'>
<Tooltip content="Center view">
{/* @ts-ignore */}
<IconButton color="gray" variant="soft" onClick={fitView}>
<MaximizeIcon className='h-4 w-4' />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
{/* @ts-ignore */}
<Button size="icon" variant="outline" onClick={fitView}>
<MaximizeIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Center view
</TooltipContent>
</Tooltip>
<Tooltip content="Zoom in">
{/* @ts-ignore */}
<IconButton color="gray" variant="soft" onClick={zoomIn}>
<ZoomInIcon className='h-4 w-4' />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
{/* @ts-ignore */}
<Button size="icon" variant="outline" onClick={zoomIn}>
<ZoomInIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
<TooltipContent>
Zoom in
</TooltipContent>
</Tooltip>
<Tooltip content="Zoom out">
{/* @ts-ignore */}
<IconButton color="gray" variant="soft" onClick={zoomOut}>
<ZoomOutIcon className='h-4 w-4' />
</IconButton>
</Tooltip>
<Tooltip content="Lock">
{/* @ts-ignore */}
<IconButton color="gray" variant="soft" onClick={() => setIsLocked((prev) => !prev)}>
{isLocked ? <LockIcon className='h-4 w-4' /> : <LockOpenIcon className='h-4 w-4' />}
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
{/* @ts-ignore */}
<Button size="icon" variant="outline" onClick={zoomOut}>
<ZoomOutIcon className='h-4 w-4' />
</Button>
</TooltipTrigger>
Zoom out
<TooltipContent>
</TooltipContent>
</Tooltip>
</Panel>
<Background />
{settings.showMiniMap && <MiniMap pannable />}
</ReactFlow>
</div >
</div>
);
};
@@ -370,7 +383,7 @@ export default function (props: any) {
}, [])
if (!mounted) {
return <div className='h-[calc(100vh_-_48px)] w-full flex items-center justify-center'><Spinner size="3" />
return <div className='h-[calc(100vh_-_48px)] w-full flex items-center justify-center'>Loading...
</div>
}
return (

View File

@@ -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<string | null>('')
const [image, setImage] = useState<string | null>("")
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 (
<Dialog.Root open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
<Dialog.Content>
<Dialog.Title>No data</Dialog.Title>
<Dialog.Description size="2" mb="4">
No data found for this individual.
</Dialog.Description>
<Dialog.Close>
<Button variant="soft" color="gray">
<Dialog open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
<DialogContent>
<DialogTitle>No data</DialogTitle>
<DialogDescription>No data found for this individual.</DialogDescription>
<DialogFooter>
<Button variant="outline" onClick={handleCloseModal}>
Close
</Button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Root>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
return (
<Dialog.Root open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
<Dialog.Content style={{ maxWidth: "1200px", width: "90vw" }} minHeight={"80vh"}>
<Skeleton loading={isLoading}>
<form className="flex flex-col gap-3 justify-between h-full" onSubmit={handleSave}>
<Flex direction="column" gap="4" flexGrow="0">
<Flex justify="between" align="center">
<Dialog.Title>User Profile</Dialog.Title>
<IconButton
type="button"
variant="ghost"
onClick={() => setEditMode(!editMode)}
aria-label={editMode ? "Cancel edit" : "Edit profile"}
>
{editMode ? <Cross2Icon /> : <Pencil1Icon />}
</IconButton>
</Flex>
<Flex gap="6">
<Flex direction={"column"}>
<Avatar
size="9"
src={image || undefined}
fallback={individual?.full_name?.[0] || "?"}
radius="full"
/>
{/* {editMode && (
<Button type="button" size="1" variant="soft" style={{ marginTop: "8px" }}>
Change Photo
</Button>
)} */}
</Flex>
<Box style={{ flexGrow: 1 }}>
<Tabs.Root defaultValue="overview">
<Tabs.List className="!overflow-x-auto">
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
<Tabs.Trigger value="social_account">Social accounts<Spinner className="ml-1" loading={isLoadingRelations}><Badge color={accounts?.length > 0 ? "indigo" : "gray"} radius="full" className="ml-1">{accounts?.length}</Badge></Spinner></Tabs.Trigger>
<Tabs.Trigger value="emails">Emails<Spinner className="ml-1" loading={isLoadingEmails}><Badge color={emails?.length > 0 ? "indigo" : "gray"} radius="full" className="ml-1">{emails?.length}</Badge></Spinner></Tabs.Trigger>
<Tabs.Trigger value="phone_numbers">Phone numbers<Spinner className="ml-1" loading={isLoadingRelations}><Badge color={phones?.length > 0 ? "indigo" : "gray"} radius="full" className="ml-1">{phones?.length}</Badge></Spinner></Tabs.Trigger>
<Tabs.Trigger value="ip_addresses">IP addresses<Spinner className="ml-1" loading={isLoadingRelations}><Badge color={ips?.length > 0 ? "indigo" : "gray"} radius="full" className="ml-1">{ips?.length}</Badge></Spinner></Tabs.Trigger>
<Tabs.Trigger value="relations">Relations<Spinner className="ml-1" loading={isLoadingRelations}><Badge color={relations?.length > 0 ? "indigo" : "gray"} radius="full" className="ml-1">{relations?.length}</Badge></Spinner></Tabs.Trigger>
</Tabs.List>
<Box pt="3">
<Tabs.Content value="overview">
<Flex direction="column" gap="3">
{individual && Object.keys(individual).filter((key) => typeof individual[key] !== "object" || !Array.isArray(individual[key])).map((key) => (
<Flex key={key} direction="column" gap="1">
<label className="capitalize-first">{key}</label>
<TextField.Root
type={key === "birth_date" ? "date" : "text"}
defaultValue={individual[key]}
placeholder={key}
name={key}
disabled={key === "id" || key === "investigation_id" || !editMode}
/>
</Flex>
))}
</Flex>
</Tabs.Content>
<Tabs.Content value="emails">
<Flex direction="column" gap="3">
{emails.length === 0 && <Text className="italic opacity-70 text-sm">No email registered. Click on edit to add one.</Text>}
{emails.map((email: any, index: number) => (
<Flex direction={"column"} key={index}>
<Callout.Root color={email.breaches.length > 0 ? "orange" : "green"} size="1">
<Callout.Text>
<Text weight={"bold"}>{email.email}</Text> {email.breaches.length === 0 ? " is not yet involved in a data breach." : ` was involved in ${email.breaches.length} data breach(es).`}
</Callout.Text>
</Callout.Root>
<Breaches breaches={email.breaches} />
</Flex>
))}
</Flex>
</Tabs.Content>
<Tabs.Content value="social_account">
<Grid columns="3" gap="3" width="auto">
{accounts.length === 0 && <Text className="italic opacity-70 text-sm">No account registered. Click on edit to add one.</Text>}
{accounts.map((account: any, index) => (
<Badge className="!p-3 cursor-pointer" color="gray" radius="large" key={index} asChild size="1">
<Flex gap="3" align="center" direction={"row"} className="w-full">
<Dialog open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
<DialogContent className="max-w-[90vw] w-[1200px] min-h-[80vh]">
<form className="flex flex-col gap-3 justify-between h-full" onSubmit={handleSave}>
<div className="flex flex-col gap-4 flex-grow-0">
<div className="flex justify-between items-center">
<DialogTitle>User Profile</DialogTitle>
<Button
variant="ghost"
size="icon"
onClick={() => setEditMode(!editMode)}
aria-label={editMode ? "Cancel edit" : "Edit profile"}
>
{editMode ? <X className="h-4 w-4" /> : <Pencil className="h-4 w-4" />}
</Button>
</div>
<div className="flex gap-6">
<div className="flex flex-col">
<Avatar className="h-20 w-20">
<AvatarImage src={image || undefined} alt={individual?.full_name} />
<AvatarFallback>{individual?.full_name?.[0] || "?"}</AvatarFallback>
</Avatar>
</div>
<div className="flex-grow">
<Tabs defaultValue="overview">
<TabsList className="overflow-x-auto">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="social_account">
Social accounts
{isLoadingRelations ? (
<span className="ml-1">Loading...</span>
) : (
<Badge variant={accounts?.length > 0 ? "default" : "outline"} className="ml-1">
{accounts?.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="emails">
Emails
{isLoadingEmails ? (
<span className="ml-1">Loading...</span>
) : (
<Badge variant={emails?.length > 0 ? "default" : "outline"} className="ml-1">
{emails?.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="phone_numbers">
Phone numbers
{isLoadingRelations ? (
<span className="ml-1">Loading...</span>
) : (
<Badge variant={phones?.length > 0 ? "default" : "outline"} className="ml-1">
{phones?.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="ip_addresses">
IP addresses
{isLoadingRelations ? (
<span className="ml-1">Loading...</span>
) : (
<Badge variant={ips?.length > 0 ? "default" : "outline"} className="ml-1">
{ips?.length}
</Badge>
)}
</TabsTrigger>
<TabsTrigger value="relations">
Relations
{isLoadingRelations ? (
<span className="ml-1">Loading...</span>
) : (
<Badge variant={relations?.length > 0 ? "default" : "outline"} className="ml-1">
{relations?.length}
</Badge>
)}
</TabsTrigger>
</TabsList>
<div className="pt-3">
<TabsContent value="overview">
<div className="flex flex-col gap-3">
{individual &&
Object.keys(individual)
.filter((key) => typeof individual[key] !== "object" || !Array.isArray(individual[key]))
.map((key) => (
<div key={key} className="flex flex-col gap-1">
<label className="capitalize-first">{key}</label>
<Input
type={key === "birth_date" ? "date" : "text"}
defaultValue={individual[key]}
placeholder={key}
name={key}
disabled={key === "id" || key === "investigation_id" || !editMode}
/>
</div>
))}
</div>
</TabsContent>
<TabsContent value="emails">
<div className="flex flex-col gap-3">
{emails.length === 0 && (
<p className="italic text-muted-foreground text-sm">
No email registered. Click on edit to add one.
</p>
)}
{emails.map((email: any, index: number) => (
<div key={index} className="flex flex-col">
<Alert variant={email.breaches.length > 0 ? "destructive" : "default"}>
<AlertDescription>
<span className="font-bold">{email.email}</span>{" "}
{email.breaches.length === 0
? " is not yet involved in a data breach."
: ` was involved in ${email.breaches.length} data breach(es).`}
</AlertDescription>
</Alert>
<Breaches breaches={email.breaches} />
</div>
))}
</div>
</TabsContent>
<TabsContent value="social_account">
<div className="grid grid-cols-3 gap-3">
{accounts.length === 0 && (
<p className="italic text-muted-foreground text-sm">
No account registered. Click on edit to add one.
</p>
)}
{accounts.map((account: any, index) => (
<Card key={index} className="p-3 cursor-pointer">
<CardContent className="flex items-center gap-3 p-0">
<Avatar className="h-8 w-8">
{/* @ts-ignore */}
<Avatar size="3" radius="full" color={platformsIcons[account?.platform]?.color || "amber"} fallback={platformsIcons[account?.platform]?.icon || "?"} />
<Box width={"80%"}>
<Text as="div" size="1" weight="bold">
{account?.platform}
</Text>
<Text as="div" size="2" weight="bold">
{account?.username || <span className="italic font-light">No username</span>}
</Text>
<Link href={account?.profile_url} size="2" className="!max-w-full" color="indigo">
<Text as="div" truncate>
{account?.profile_url}
</Text>
</Link>
</Box>
</Flex>
</Badge>
))}
{editMode && (
<Button type="button" onClick={() => handleAddField(setAccounts)} variant="soft">
<PlusIcon /> Add account
</Button>
)}
</Grid>
</Tabs.Content>
<Tabs.Content value="phone_numbers">
<Flex direction="column" gap="3" maxWidth={"420px"}>
{phones.length === 0 && <Text className="italic opacity-70 text-sm">No phone number registered. Click on edit to add one.</Text>}
{phones.map((phone, index) => (
<Flex key={index} gap="2" align="center">
<TextField.Root
value={phone}
onChange={(e) => handleFieldChange(index, e.target.value, setPhones)}
placeholder="Phone Number"
type="tel"
disabled={!editMode}
style={{ flexGrow: 1 }}
/>
{editMode && (
<IconButton
type="button"
variant="ghost"
onClick={() => handleRemoveField(index, setPhones)}
aria-label="Remove phone"
<AvatarImage src={platformsIcons[account?.platform]?.icon} />
<AvatarFallback>{account?.platform?.[0] || "?"}</AvatarFallback>
</Avatar>
<div className="overflow-hidden">
<p className="text-sm font-semibold">{account?.platform}</p>
<p className="text-sm font-bold">
{account?.username || <span className="italic font-light">No username</span>}
</p>
<a
href={account?.profile_url}
className="text-sm text-blue-600 hover:underline truncate block"
>
<TrashIcon />
</IconButton>
)}
</Flex>
))}
{editMode && (
<Button type="button" onClick={() => handleAddField(setPhones)} variant="soft">
<PlusIcon /> Add Phone
</Button>
)}
</Flex>
</Tabs.Content>
<Tabs.Content value="ip_addresses">
<Flex direction="column" gap="3">
{ips.length === 0 && <Text className="italic opacity-70 text-sm">No IP address registered. Click on edit to add one.</Text>}
{ips.map((ip, index) => (
<Flex key={index} gap="2" align="center">
<TextField.Root
value={ip}
onChange={(e) => handleFieldChange(index, e.target.value, setIps)}
placeholder="IP address"
type="text"
disabled={!editMode}
style={{ flexGrow: 1 }}
/>
{editMode && (
<IconButton
type="button"
{account?.profile_url}
</a>
</div>
</CardContent>
</Card>
))}
{editMode && (
<Button onClick={() => handleAddField(setAccounts)} variant="outline">
<Plus className="mr-2 h-4 w-4" /> Add account
</Button>
)}
</div>
</TabsContent>
<TabsContent value="phone_numbers">
<div className="flex flex-col gap-3 max-w-md">
{phones.length === 0 && (
<p className="italic text-muted-foreground text-sm">
No phone number registered. Click on edit to add one.
</p>
)}
{phones.map((phone, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={phone}
onChange={(e) => handleFieldChange(index, e.target.value, setPhones)}
placeholder="Phone Number"
type="tel"
disabled={!editMode}
className="flex-grow"
/>
{editMode && (
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveField(index, setPhones)}
aria-label="Remove phone"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
))}
{editMode && (
<Button onClick={() => handleAddField(setPhones)} variant="outline">
<Plus className="mr-2 h-4 w-4" /> Add Phone
</Button>
)}
</div>
</TabsContent>
<TabsContent value="ip_addresses">
<div className="flex flex-col gap-3">
{ips.length === 0 && (
<p className="italic text-muted-foreground text-sm">
No IP address registered. Click on edit to add one.
</p>
)}
{ips.map((ip, index) => (
<div key={index} className="flex items-center gap-2">
<Input
value={ip}
onChange={(e) => handleFieldChange(index, e.target.value, setIps)}
placeholder="IP address"
type="text"
disabled={!editMode}
className="flex-grow"
/>
{editMode &&
(
<Button
variant="ghost"
size="icon"
onClick={() => handleRemoveField(index, setIps)}
aria-label="Remove ip"
aria-label="Remove IP"
>
<TrashIcon />
</IconButton>
<Trash2 className="h-4 w-4" />
</Button>
)}
</Flex>
))}
{editMode && (
<Button type="button" onClick={() => handleAddField(setIps)} variant="soft">
<PlusIcon /> Add IP address
</Button>
)}
</Flex>
</Tabs.Content>
<Tabs.Content value="relations">
<Grid columns="3" gap="3" width="auto">
{relations.length === 0 && <Text className="italic opacity-70 text-sm">No relation registered. Click on edit to add one.</Text>}
{relations.map((relation) => (
<Badge className="!p-3 cursor-pointer" color="gray" radius="large" key={relation.id} onClick={() => handleOpenIndividualModal(relation.id)} asChild size="1">
<Flex gap="3" align="center" direction={"row"} className="w-full">
<Avatar src={relation?.image_url} size="3" radius="full" fallback={relation.full_name[0]} color="indigo" />
<Box>
<Text as="div" size="2" weight="bold">
{relation.full_name}
</Text>
<Text as="div" size="2" color="gray">
{relation.relation_type}
</Text>
</Box>
</Flex>
</Badge>
))}
</Grid>
</Tabs.Content>
</Box>
</Tabs.Root>
</Box>
</Flex>
</Flex>
<Flex gap="3" justify="end">
<Dialog.Close>
<Button variant="soft" color="gray">
Cancel
</Button>
</Dialog.Close>
{editMode && <Button type="submit">Save Changes</Button>}
</Flex>
</form>
</Skeleton>
</Dialog.Content >
</Dialog.Root >
</div>
))}
{editMode && (
<Button onClick={() => handleAddField(setIps)} variant="outline">
<Plus className="mr-2 h-4 w-4" /> Add IP address
</Button>
)}
</div>
</TabsContent>
<TabsContent value="relations">
<div className="grid grid-cols-3 gap-3">
{relations.length === 0 && (
<p className="italic text-muted-foreground text-sm">
No relation registered. Click on edit to add one.
</p>
)}
{relations.map((relation) => (
<Card
key={relation.id}
className="p-3 cursor-pointer"
onClick={() => handleOpenIndividualModal(relation.id)}
>
<CardContent className="flex items-center gap-3 p-0">
<Avatar className="h-8 w-8">
<AvatarImage src={relation?.image_url} alt={relation.full_name} />
<AvatarFallback>{relation.full_name[0]}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-semibold">{relation.full_name}</p>
<p className="text-sm text-muted-foreground">{relation.relation_type}</p>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
</div>
</Tabs>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleCloseModal}>
Cancel
</Button>
{editMode && <Button type="submit">Save Changes</Button>}
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}
export default IndividualModal
export default IndividualModal

View File

@@ -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 (
<PanelGroup autoSaveId="conditional" className='h-screen w-screen flex' direction="horizontal">
{panelOpen && <Panel id="left" order={1} className='h-screen' defaultSize={20} minSize={15}>
<div className='flex flex-col w-full h-full rounded-none shadow-none border-r border-gray-400/20'>
<div className='w-full rounded-none shadow-none h-12 border-b border-gray-400/20 flex items-center gap-1 flex-row justify-between p-2'>
<div className='flex flex-col w-full h-full rounded-none shadow-none border-r'>
<div className='w-full rounded-none shadow-none h-12 border-b flex items-center gap-1 flex-row justify-between p-2'>
<Logo />
<Flex gap={"1"}>
<IconButton onClick={() => handleOpenChat(investigation)} color="gray" size="2" variant="soft"><BotMessageSquareIcon className="h-4" /></IconButton>
<div className='flex gap-1'>
<Button variant="outline" size="icon" onClick={() => handleOpenChat(investigation)} color="gray"><BotMessageSquareIcon className="h-4" /></Button>
<SearchModal investigation_id={investigation_id} />
<NewCase>
<IconButton color="gray" size="2" variant="soft">
<Button variant="outline" size="icon">
<PlusIcon className="h-5" />
</IconButton>
</Button>
</NewCase>
</Flex>
</div>
</div>
<ScrollArea type="auto" scrollbars="vertical" className='p-3 h-full grow overflow-y-auto'>
<ScrollArea type="auto" className='p-3 h-full grow overflow-y-auto'>
<div className="flex flex-col">
{left}
</div>
</ScrollArea>
<Flex justify={"end"} className='p-2'>
<User user={user} />
</Flex>
<div className="flex justify-end p-2">
<NavUser user={user} />
</div>
</div>
</Panel>}
<PanelResizeHandle />
<Panel id="right" order={2} defaultSize={80} minSize={50} className='grow flex flex-col'>
<div>
<div className='w-full rounded-none shadow-none h-12 justify-between border-b border-gray-400/20 flex flex-row items-center p-2'>
<Flex gap={"1"}>
<IconButton onClick={() => setPanelOpen(!panelOpen)} variant='soft' color='gray'>
<div className='w-full rounded-none shadow-none h-12 justify-between border-b flex flex-row items-center p-2'>
<div className='flex gap-1'>
<Button onClick={() => setPanelOpen(!panelOpen)} size="icon" variant="outline">
<PanelRightIcon className='h-4 w-4' />
</IconButton>
</Button>
<CaseSelector />
</Flex>
</div>
<MoreMenu />
</div>
{children}
</div>
</Panel>
</PanelGroup>
</PanelGroup >
)
}

View File

@@ -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 (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton color="gray" variant="soft" size="2">
<Ellipsis className="h-4" />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content size="2">
<DropdownMenu.Item onClick={setOpenSettingsModal} shortcut="⌘ E">Settings</DropdownMenu.Item>
<DropdownMenu.Item onClick={handleDeleteInvestigation} shortcut="⌘ ⌫" color="red">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Ellipsis className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={setOpenSettingsModal}>Settings</DropdownMenuItem>
<DropdownMenuItem onClick={handleDeleteInvestigation} className="text-red-500">
Delete
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}

View File

@@ -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<HTMLFormElement>) => {
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 (
<Popover.Root>
<Popover.Trigger>
<Button variant="soft">
<PlusIcon width="16" height="16" />
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="outline" className="gap-2">
<Plus className="h-4 w-4" />
Individual
</Button>
</Popover.Trigger>
<Popover.Content width="260px">
<form onSubmit={onSubmit}>
<Flex gap="3">
<Avatar
size="3"
fallback="A"
radius="full"
/>
<Box flexGrow="1">
<TextField.Root
required
defaultValue={""}
name={"full_name"}
placeholder={`Name of the individual`}
/>
<Flex justify={"end"} className="mt-2">
<Button type="submit" size="2">Add</Button>
</Flex>
</Box>
</Flex>
</PopoverTrigger>
<PopoverContent className="w-80">
<form onSubmit={onSubmit} className="flex items-start space-x-3">
<Avatar>
<AvatarFallback>A</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-2">
<Input required name="full_name" placeholder="Name of the individual" />
<div className="flex justify-end">
<Button type="submit" size="sm">
Add
</Button>
</div>
</div>
</form>
</Popover.Content>
</Popover.Root>
);
</PopoverContent>
</Popover>
)
}

View File

@@ -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 (
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel && showContent ?
<Card data-radius='full' className={cn('!pr-4 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Inset>
<Flex className='items-center p-0'>
<Badge color='green' className='!h-[24px] !rounded-r-none'>
<AtSignIcon className='h-3 w-3' />
</Badge>
<Flex align={"center"} className='p-1 px-1.5' gap="2">
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</Flex>
</Flex>
</Inset>
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"pr-4 border border-transparent hover:border-sky-400 rounded-full",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex items-center">
<Badge variant="secondary" className="h-6 rounded-r-none">
<AtSign className="h-3 w-3" />
</Badge>
<div className="flex items-center p-1 px-1.5 gap-2">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</div>
</div>
</Card>
:
<Tooltip content={data.label}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<AtSignIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<AtSign className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500 hidden", settings.floatingEdges && "hidden")}
className={cn("w-16 bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenSearchModal(data.email)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.email)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root >
);
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
<NodeProvider>
<EmailNode {...props} />
</NodeProvider>
);
)
export default memo(MemoizedNode)
export default memo(MemoizedNode);

View File

@@ -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 &&
<NodeToolbar
isVisible={data.forceToolbarVisible || undefined}
position={Position.Top}
>
<Card data-radius='full' className='!p-1'>
<Flex gap={"1"}>
<Button variant='soft' color='gray' onClick={() => handleOpenIndividualModal(data.id)}><EditIcon className='h-4 w-4' />Edit</Button>
<Button variant='soft' color='gray' onClick={() => handleOpenChat(data)}><ZapIcon color='orange' className='h-4 w-4' />Ask AI</Button>
</Flex>
{settings.showNodeToolbar && (
<NodeToolbar isVisible={data.forceToolbarVisible || undefined} position={Position.Top}>
<Card className="p-1 rounded-full shadow-none">
<div className="flex gap-1">
<Button variant="outline" className="rounded-full" size="sm" onClick={() => handleOpenIndividualModal(data.id)}>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<Button variant="outline" className="rounded-full" size="sm" onClick={() => handleOpenChat(data)}>
<Zap className="h-4 w-4 mr-2 text-orange-500" />
Ask AI
</Button>
</div>
</Card>
</NodeToolbar>}
<ContextMenu.Root>
<ContextMenu.Trigger onContextMenu={(e) => { e.stopPropagation() }}>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100", "overflow-hidden group")}>{settings.showNodeLabel && showContent ?
<Card data-radius='full' onDoubleClick={() => handleOpenIndividualModal(data.id)} className={cn('!p-1 !pr-2 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Flex gap="2" align="center" className='rounded-full'>
<Avatar
color='gray'
size="1"
src={data?.image_url}
radius="full"
fallback={loading ? <Spinner /> : data.full_name[0]}
/>
{settings.showNodeLabel && showContent &&
<Flex align={"center"} gap="2">
<Text as="div" size="1" weight="regular">
{data.full_name}
</Text>
{settings.showCopyIcon && <CopyButton content={data.full_name} />}
</Flex>}
</Flex>
</Card> :
<Tooltip content={data.full_name}>
<button onDoubleClick={() => handleOpenIndividualModal(data.id)} className={cn('rounded-full border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Avatar
color='gray'
size="3"
src={data?.image_url}
radius="full"
fallback={<UserIcon className='h-4 w-4' />}
/>
</button>
</Tooltip>}
</NodeToolbar>
)}
<ContextMenu>
<ContextMenuTrigger
onContextMenu={(e) => {
e.stopPropagation()
}}
>
<div className={cn(loading ? "opacity-40" : "opacity-100", "overflow-hidden group")}>
{settings.showNodeLabel && showContent ? (
<Card
onDoubleClick={() => handleOpenIndividualModal(data.id)}
className={cn(
"p-1 border border-border hover:border-sky-400 rounded-full shadow-none",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex gap-2 items-center rounded-full">
<Avatar className="h-9 w-9">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>{loading ? "..." : data.full_name[0]}</AvatarFallback>
</Avatar>
{settings.showNodeLabel && showContent && (
<div className="flex items-center gap-2">
<span className="text-sm">{data.full_name}</span>
{settings.showCopyIcon && <CopyButton className="rounded-full" content={data.full_name} />}
</div>
)}
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onDoubleClick={() => handleOpenIndividualModal(data.id)}
className={cn(
"rounded-full border border-transparent hover:border-sky-400",
currentNode === data.id && "border-sky-400",
)}
>
<Avatar className="h-12 w-12">
<AvatarImage src={data?.image_url} alt={data.full_name} />
<AvatarFallback>
<User className="h-4 w-4" />
</AvatarFallback>
</Avatar>
</button>
</TooltipTrigger>
<TooltipContent>{data.full_name}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
<Handle
type="source"
position={Position.Bottom}
className={cn("w-16 !bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
className={cn("w-16 bg-teal-500", showContent ? "group-hover:opacity-100 opacity-0" : "opacity-0")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenSearchModal(data.full_name)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenChat(data)}>Ask AI<BotIcon color='#12A594' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger >New</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "individuals")}><UserIcon className='h-4 w-4 opacity-70' /> New relation</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "phone_numbers", data.id)}><PhoneIcon className='h-4 w-4 opacity-70' />Phone number</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "physical_addresses", data.id)}><MapPinHouseIcon className='h-4 w-4 opacity-70' />Physical address</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "emails", data.id)}><AtSignIcon className='h-4 w-4 opacity-70' />Email address</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "ip_addresses", data.id)}><LocateIcon className='h-4 w-4 opacity-70' />IP address</ContextMenu.Item>
<ContextMenu.Sub>
<ContextMenu.SubTrigger >Social account</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_facebook", data.id)}><FacebookIcon className='h-4 w-4 opacity-70' />Facebook</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_instagram", data.id)}><InstagramIcon className='h-4 w-4 opacity-70' />Instagram</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_telegram", data.id)}><SendIcon className='h-4 w-4 opacity-70' />Telegram</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_signal", data.id)}><MessageCircleDashedIcon className='h-4 w-4 opacity-70' />Signal</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_snapchat", data.id)}><CameraIcon className='h-4 w-4 opacity-70' />Snapchat</ContextMenu.Item>
<ContextMenu.Item onClick={(e) => setOpenAddNodeModal(e, "social_accounts_github", data.id)}><GithubIcon className='h-4 w-4 opacity-70' />Github</ContextMenu.Item>
<ContextMenu.Item disabled onClick={(e) => setOpenAddNodeModal(e, "social_accounts_coco", data.id)}>Coco <Badge radius='full'>soon</Badge></ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.SubContent>
</ContextMenu.Sub>
<ContextMenu.Item onClick={() => handleOpenIndividualModal(data.id)}>View and edit</ContextMenu.Item>
<ContextMenu.Item onClick={handleDuplicateNode}>Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.full_name)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem onClick={() => handleOpenChat(data)}>
Ask AI
<Bot className="ml-2 h-4 w-4 text-teal-600" />
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>New</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "individuals")}>
<User className="mr-2 h-4 w-4 opacity-70" /> New relation
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "phone_numbers", data.id)}>
<Phone className="mr-2 h-4 w-4 opacity-70" />
Phone number
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "physical_addresses", data.id)}>
<MapPin className="mr-2 h-4 w-4 opacity-70" />
Physical address
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "emails", data.id)}>
<AtSign className="mr-2 h-4 w-4 opacity-70" />
Email address
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "ip_addresses", data.id)}>
<Locate className="mr-2 h-4 w-4 opacity-70" />
IP address
</ContextMenuItem>
<ContextMenuSub>
<ContextMenuSubTrigger>Social account</ContextMenuSubTrigger>
<ContextMenuSubContent>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_facebook", data.id)}>
<Facebook className="mr-2 h-4 w-4 opacity-70" />
Facebook
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_instagram", data.id)}>
<Instagram className="mr-2 h-4 w-4 opacity-70" />
Instagram
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_telegram", data.id)}>
<Send className="mr-2 h-4 w-4 opacity-70" />
Telegram
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_signal", data.id)}>
<MessageCircleDashed className="mr-2 h-4 w-4 opacity-70" />
Signal
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_snapchat", data.id)}>
<Camera className="mr-2 h-4 w-4 opacity-70" />
Snapchat
</ContextMenuItem>
<ContextMenuItem onClick={(e) => setOpenAddNodeModal(e, "social_accounts_github", data.id)}>
<GithubIcon className="mr-2 h-4 w-4 opacity-70" />
Github
</ContextMenuItem>
<ContextMenuItem disabled onClick={(e) => setOpenAddNodeModal(e, "social_accounts_coco", data.id)}>
Coco{" "}
<Badge variant="outline" className="ml-2">
soon
</Badge>
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuItem onClick={() => handleOpenIndividualModal(data.id)}>View and edit</ContextMenuItem>
<ContextMenuItem onClick={handleDuplicateNode}>Duplicate</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root >
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</>
);
)
}
const IndividualNode = (props: any) => (
<NodeProvider>
<Custom {...props} />
</NodeProvider>
);
)
export default memo(IndividualNode)
export default memo(IndividualNode);

View File

@@ -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 (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel && showContent ?
<Card data-radius='full' className={cn('!pr-4 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Inset>
<Flex className='items-center p-0'>
<Badge color='orange' className='!h-[24px] !rounded-r-none'>
<LocateIcon className='h-3 w-3' />
</Badge>
<Flex align={"center"} className='p-1 px-1.5' gap="2">
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</Flex>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.label}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<LocateIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenSearchModal(data.ip_address)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
</>
);
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"pr-4 border border-transparent hover:border-sky-400 rounded-full",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex items-center">
<Badge variant="secondary" className="h-6 rounded-r-none">
<LocateIcon className="h-3 w-3" />
</Badge>
<div className="flex items-center p-1 px-1.5 gap-2">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<LocateIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.email)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const IpNode = (props: any) => (
const MemoizedNode = (props: any) => (
<NodeProvider>
<Custom {...props} />
<EmailNode {...props} />
</NodeProvider>
);
)
export default memo(MemoizedNode)
export default memo(IpNode);

View File

@@ -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 (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel && showContent ?
<Card data-radius='full' className={cn('!pr-4 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Inset>
<Flex className='items-center p-0'>
<Badge color='violet' className='!h-[24px] !rounded-r-none'>
<PhoneIcon className='h-3 w-3' />
</Badge>
<Flex align={"center"} className='p-1 px-1.5' gap="2">
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</Flex>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.label}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<PhoneIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenSearchModal(data.phone_number)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
</>
);
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"pr-4 border border-transparent hover:border-sky-400 rounded-full",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex items-center">
<Badge variant="secondary" className="h-6 rounded-r-none">
<PhoneIcon className="h-3 w-3" />
</Badge>
<div className="flex items-center p-1 px-1.5 gap-2">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<PhoneIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.email)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const PhoneNode = (props: any) => (
const MemoizedNode = (props: any) => (
<NodeProvider>
<Custom {...props} />
<EmailNode {...props} />
</NodeProvider>
);
)
export default memo(MemoizedNode)
export default memo(PhoneNode);

View File

@@ -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 (
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel && showContent ?
<Card data-radius='full' className={cn('!pr-4 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Inset>
<Flex className='items-center p-0'>
<Badge color='ruby' className='!h-[24px] !rounded-r-none'>
<MapPinHouseIcon className='h-3 w-3' />
</Badge>
<Flex align={"center"} className='p-1 px-1.5' gap="2">
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</Flex>
</Flex>
</Inset>
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"pr-4 border border-transparent hover:border-sky-400 rounded-full",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex items-center">
<Badge variant="secondary" className="h-6 rounded-r-none">
<MapPinHouseIcon className="h-3 w-3" />
</Badge>
<div className="flex items-center p-1 px-1.5 gap-2">
<span className="text-sm">{data.label}</span>
{settings.showCopyIcon && <CopyButton content={data.label} />}
</div>
</div>
</Card>
:
<Tooltip content={data.label}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<MapPinHouseIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="rounded-full p-0">
<Avatar className="h-6 w-6">
<AvatarFallback>
<MapPinHouseIcon className="h-3 w-3" />
</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.label}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500 hidden", settings.floatingEdges && "hidden")}
className={cn("w-16 bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item disabled className='font-bold' onClick={() => handleOpenSearchModal(data.address)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.email)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
);
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const MemoizedNode = (props: any) => (
<NodeProvider>
<AddressNode {...props} />
<EmailNode {...props} />
</NodeProvider>
);
)
export default memo(MemoizedNode)
export default memo(MemoizedNode);

View File

@@ -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 (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel && showContent ?
<Card data-radius='full' className={cn('!pr-4 border border-transparent hover:border-sky-400', currentNode === data.id && "border-sky-400")}>
<Inset>
<Flex className='items-center p-0'>
<ContextMenu>
<ContextMenuTrigger>
<div className={cn(loading ? "opacity-40" : "opacity-100")}>
{settings.showNodeLabel && showContent ? (
<Card
className={cn(
"pr-4 border border-transparent hover:border-sky-400 rounded-full",
currentNode === data.id && "border-sky-400",
)}
>
<div className="flex items-center">
<Badge
variant="secondary"
/* @ts-ignore */
className={cn("h-6 rounded-r-none", `bg-${platformsIcons?.[data?.platform]?.color}-100`)}
>
{/* @ts-ignore */}
{platformsIcons?.[data?.platform]?.icon || "?"}
</Badge>
<div className="flex items-center p-1 px-1.5 gap-2">
<span className="text-sm">{data.username || data.profile_url}</span>
{settings.showCopyIcon && <CopyButton content={data.username || data.profile_url} />}
</div>
</div>
</Card>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" className="p-0 h-auto">
{/* @ts-ignore */}
<Badge color={platformsIcons?.[data?.platform]?.color as any || "amber"}
className='!h-[24px] !rounded-r-none'>
<Avatar className={cn("h-6 w-6", `bg-${platformsIcons?.[data?.platform]?.color}-100`)}>
<AvatarImage src={data?.image_url} alt={data.username || data.profile_url} />
{/* @ts-ignore */}
{platformsIcons?.[data?.platform]?.icon || "?"}
</Badge>
<Flex align={"center"} className='p-1 px-1.5' gap="2">
<Text as="div" size="1" weight="regular">
{data.username || data.profile_url}
</Text>
{settings.showCopyIcon && <CopyButton content={data.username || data.profile_url} />}
</Flex>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.username || data.profile_url}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
/* @ts-ignore */
color={platformsIcons?.[data?.platform]?.color || "amber"}
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={platformsIcons?.[data?.platform]?.icon || "?"}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 !bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item className='font-bold' onClick={() => handleOpenSearchModal(data.username || data.profile_url)}>Launch search<ZapIcon color='orange' className='h-4 w-4' /></ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root >
</>
);
<AvatarFallback>{platformsIcons?.[data?.platform]?.icon || "?"}</AvatarFallback>
</Avatar>
</Button>
</TooltipTrigger>
<TooltipContent>{data.username || data.profile_url}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<Handle
type="target"
position={Position.Top}
className={cn("w-16 bg-teal-500 hidden", settings.floatingEdges && "hidden")}
/>
</div>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={() => handleOpenSearchModal(data.username || data.profile_url)}>
Launch search
<Zap className="ml-2 h-4 w-4 text-orange-500" />
</ContextMenuItem>
<ContextMenuItem>
Copy content
<span className="ml-auto text-xs text-muted-foreground"> C</span>
</ContextMenuItem>
<ContextMenuItem>
Duplicate
<span className="ml-auto text-xs text-muted-foreground"> D</span>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={handleDeleteNode} className="text-red-600">
Delete
<span className="ml-auto text-xs text-muted-foreground"> </span>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
)
}
const SocialNode = (props: any) => (
<NodeProvider>
<Custom {...props} />
</NodeProvider>
);
)
export default memo(SocialNode)
export default memo(SocialNode);

View File

@@ -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 (
<Card className="mx-auto w-full max-w-md !p-6">
<Flex direction={"column"} gap="2">
<Text as="div" size="6" weight="bold">
<div className="flex flex-cl gap-2">
<div>
Login
</Text>
<Text as="div" size="2" color="gray">
</div>
<div>
Login into your account or create a new one.
</Text>
</Flex>
<Flex direction={"column"} gap="3">
</div>
</div>
<div className="flex flex-cl gap-3">
<form>
<Flex direction={"column"} gap="3">
<div className="flex flex-cl gap-2">
<label htmlFor="email">
<Text as="div" size="2" mb="1" weight="bold">
<div>
Email
</Text>
<TextField.Root
</div>
<Input
id="email"
type="email"
name="email"
@@ -30,10 +31,8 @@ export function LoginForm() {
/>
</label>
<label htmlFor="email">
<Text as="div" size="2" mb="1" weight="bold">
Password
</Text>
<TextField.Root id="password" name="password" placeholder="Your password" type="password" required />
Password
<Input id="password" name="password" placeholder="Your password" type="password" required />
</label>
<Link href="#" className="ml-auto inline-block text-sm underline">
Forgot your password?
@@ -41,16 +40,16 @@ export function LoginForm() {
<Button formAction={login} type="submit" className="w-full mt-4">
Login
</Button>
</Flex>
</div>
</form>
<form>
<Flex direction={"column"} gap="3">
<div className="flex flex-cl gap-3">
<Button formAction={signInWithGithub} variant="outline" className="w-full">
Login with Github
</Button>
</Flex>
</div>
</form>
</Flex>
</div>
<div className="mt-4 text-center text-sm">
Don&apos;t have an account?{" "}
<Button variant="ghost" formAction={signup}>

View File

@@ -1,14 +1,13 @@
import React from 'react'
import { CableIcon, HomeIcon } from 'lucide-react'
import { HomeIcon } from 'lucide-react'
import Link from 'next/link'
import { IconButton } from '@radix-ui/themes'
import { Button } from './ui/button'
const Logo = () => {
return (
<Link href="/dashboard">
<IconButton color="gray" size="2" variant="soft">
<Button variant="outline" size="icon">
<HomeIcon className="h-5" />
</IconButton>
</Button>
</Link>
)
}

View File

@@ -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 (
<SidebarGroup>
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => (
<Collapsible
key={item.title}
asChild
defaultOpen={item.isActive}
className="group/collapsible"
>
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={item.title}>
{item.icon && <item.icon />}
<span>{item.title}</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.items?.map((subItem) => (
<SidebarMenuSubItem key={subItem.title}>
<SidebarMenuSubButton asChild>
<a href={subItem.url}>
<span>{subItem.title}</span>
</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
))}
</SidebarMenu>
</SidebarGroup>
)
}

View File

@@ -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 (
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
<SidebarGroupLabel>Projects</SidebarGroupLabel>
<SidebarMenu>
{projects.map((item) => (
<SidebarMenuItem key={item.name}>
<SidebarMenuButton asChild>
<a href={item.url}>
<item.icon />
<span>{item.name}</span>
</a>
</SidebarMenuButton>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuAction showOnHover>
<MoreHorizontal />
<span className="sr-only">More</span>
</SidebarMenuAction>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-48 rounded-lg"
side={isMobile ? "bottom" : "right"}
align={isMobile ? "end" : "start"}
>
<DropdownMenuItem>
<Folder className="text-muted-foreground" />
<span>View Project</span>
</DropdownMenuItem>
<DropdownMenuItem>
<Forward className="text-muted-foreground" />
<span>Share Project</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trash2 className="text-muted-foreground" />
<span>Delete Project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<SidebarMenuButton className="text-sidebar-foreground/70">
<MoreHorizontal className="text-sidebar-foreground/70" />
<span>More</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup>
)
}

114
src/components/nav-user.tsx Normal file
View File

@@ -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 (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -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 (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<activeTeam.logo className="size-4" />
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">
{activeTeam.name}
</span>
<span className="truncate text-xs">{activeTeam.plan}</span>
</div>
<ChevronsUpDown className="ml-auto" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="start"
side={isMobile ? "bottom" : "right"}
sideOffset={4}
>
<DropdownMenuLabel className="text-xs text-muted-foreground">
Teams
</DropdownMenuLabel>
{teams.map((team, index) => (
<DropdownMenuItem
key={team.name}
onClick={() => setActiveTeam(team)}
className="gap-2 p-2"
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<team.logo className="size-4 shrink-0" />
</div>
{team.name}
<DropdownMenuShortcut>{index + 1}</DropdownMenuShortcut>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-2 p-2">
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
<Plus className="size-4" />
</div>
<div className="font-medium text-muted-foreground">Add team</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}

View File

@@ -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 (
<div>
{!isSRR && <Switch defaultChecked suppressHydrationWarning onCheckedChange={onChange} checked={theme === "light"} />}
</div>
);
};
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -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<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View File

@@ -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<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -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<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@@ -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<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -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) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
Breadcrumb.displayName = "Breadcrumb"
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
))
BreadcrumbList.displayName = "BreadcrumbList"
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
))
BreadcrumbItem.displayName = "BreadcrumbItem"
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
})
BreadcrumbLink.displayName = "BreadcrumbLink"
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
))
BreadcrumbPage.displayName = "BreadcrumbPage"
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
)
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@@ -0,0 +1,57 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline:
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,76 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-xl border bg-card text-card-foreground shadow",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("font-semibold leading-none tracking-tight", className)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,11 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
const Collapsible = CollapsiblePrimitive.Root
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const ContextMenu = ContextMenuPrimitive.Root
const ContextMenuTrigger = ContextMenuPrimitive.Trigger
const ContextMenuGroup = ContextMenuPrimitive.Group
const ContextMenuPortal = ContextMenuPrimitive.Portal
const ContextMenuSub = ContextMenuPrimitive.Sub
const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
const ContextMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuPrimitive.SubTrigger>
))
ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
const ContextMenuSubContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
const ContextMenuContent = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Portal>
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</ContextMenuPrimitive.Portal>
))
ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
const ContextMenuItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
const ContextMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.CheckboxItem>
))
ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
const ContextMenuRadioItem = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<ContextMenuPrimitive.ItemIndicator>
<Circle className="h-4 w-4 fill-current" />
</ContextMenuPrimitive.ItemIndicator>
</span>
{children}
</ContextMenuPrimitive.RadioItem>
))
ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
const ContextMenuLabel = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
{...props}
/>
))
ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
const ContextMenuSeparator = React.forwardRef<
React.ElementRef<typeof ContextMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
const ContextMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
)
}
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuItem,
ContextMenuCheckboxItem,
ContextMenuRadioItem,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuShortcut,
ContextMenuGroup,
ContextMenuPortal,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuRadioGroup,
}

View File

@@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View File

@@ -0,0 +1,201 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

View File

@@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

140
src/components/ui/sheet.tsx Normal file
View File

@@ -0,0 +1,140 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
{children}
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View File

@@ -0,0 +1,763 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { PanelLeft } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Separator } from "@/components/ui/separator"
import { Sheet, SheetContent } from "@/components/ui/sheet"
import { Skeleton } from "@/components/ui/skeleton"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "16rem"
const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContext = {
state: "expanded" | "collapsed"
open: boolean
setOpen: (open: boolean) => void
openMobile: boolean
setOpenMobile: (open: boolean) => void
isMobile: boolean
toggleSidebar: () => void
}
const SidebarContext = React.createContext<SidebarContext | null>(null)
function useSidebar() {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebar must be used within a SidebarProvider.")
}
return context
}
const SidebarProvider = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
defaultOpen?: boolean
open?: boolean
onOpenChange?: (open: boolean) => void
}
>(
(
{
defaultOpen = true,
open: openProp,
onOpenChange: setOpenProp,
className,
style,
children,
...props
},
ref
) => {
const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false)
// This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open
const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value
if (setOpenProp) {
setOpenProp(openState)
} else {
_setOpen(openState)
}
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
},
[setOpenProp, open]
)
// Helper to toggle the sidebar.
const toggleSidebar = React.useCallback(() => {
return isMobile
? setOpenMobile((open) => !open)
: setOpen((open) => !open)
}, [isMobile, setOpen, setOpenMobile])
// Adds a keyboard shortcut to toggle the sidebar.
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
(event.metaKey || event.ctrlKey)
) {
event.preventDefault()
toggleSidebar()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [toggleSidebar])
// We add a state so that we can do data-state="expanded" or "collapsed".
// This makes it easier to style the sidebar with Tailwind classes.
const state = open ? "expanded" : "collapsed"
const contextValue = React.useMemo<SidebarContext>(
() => ({
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
}),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
)
return (
<SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}>
<div
style={
{
"--sidebar-width": SIDEBAR_WIDTH,
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
...style,
} as React.CSSProperties
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
className
)}
ref={ref}
{...props}
>
{children}
</div>
</TooltipProvider>
</SidebarContext.Provider>
)
}
)
SidebarProvider.displayName = "SidebarProvider"
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
side?: "left" | "right"
variant?: "sidebar" | "floating" | "inset"
collapsible?: "offcanvas" | "icon" | "none"
}
>(
(
{
side = "left",
variant = "sidebar",
collapsible = "offcanvas",
className,
children,
...props
},
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
if (collapsible === "none") {
return (
<div
className={cn(
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
className
)}
ref={ref}
{...props}
>
{children}
</div>
)
}
if (isMobile) {
return (
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
<SheetContent
data-sidebar="sidebar"
data-mobile="true"
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
style={
{
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
} as React.CSSProperties
}
side={side}
>
<div className="flex h-full w-full flex-col">{children}</div>
</SheetContent>
</Sheet>
)
}
return (
<div
ref={ref}
className="group peer hidden text-sidebar-foreground md:block"
data-state={state}
data-collapsible={state === "collapsed" ? collapsible : ""}
data-variant={variant}
data-side={side}
>
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
"relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
"group-data-[collapsible=offcanvas]:w-0",
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]"
)}
/>
<div
className={cn(
"fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
side === "left"
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
>
{children}
</div>
</div>
</div>
)
}
)
Sidebar.displayName = "Sidebar"
const SidebarTrigger = React.forwardRef<
React.ElementRef<typeof Button>,
React.ComponentProps<typeof Button>
>(({ className, onClick, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<Button
ref={ref}
data-sidebar="trigger"
variant="ghost"
size="icon"
className={cn("h-7 w-7", className)}
onClick={(event) => {
onClick?.(event)
toggleSidebar()
}}
{...props}
>
<PanelLeft />
<span className="sr-only">Toggle Sidebar</span>
</Button>
)
})
SidebarTrigger.displayName = "SidebarTrigger"
const SidebarRail = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button">
>(({ className, ...props }, ref) => {
const { toggleSidebar } = useSidebar()
return (
<button
ref={ref}
data-sidebar="rail"
aria-label="Toggle Sidebar"
tabIndex={-1}
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className
)}
{...props}
/>
)
})
SidebarRail.displayName = "SidebarRail"
const SidebarInset = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"main">
>(({ className, ...props }, ref) => {
return (
<main
ref={ref}
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-theme(spacing.4))] md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
className
)}
{...props}
/>
)
})
SidebarInset.displayName = "SidebarInset"
const SidebarInput = React.forwardRef<
React.ElementRef<typeof Input>,
React.ComponentProps<typeof Input>
>(({ className, ...props }, ref) => {
return (
<Input
ref={ref}
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className
)}
{...props}
/>
)
})
SidebarInput.displayName = "SidebarInput"
const SidebarHeader = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarHeader.displayName = "SidebarHeader"
const SidebarFooter = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
{...props}
/>
)
})
SidebarFooter.displayName = "SidebarFooter"
const SidebarSeparator = React.forwardRef<
React.ElementRef<typeof Separator>,
React.ComponentProps<typeof Separator>
>(({ className, ...props }, ref) => {
return (
<Separator
ref={ref}
data-sidebar="separator"
className={cn("mx-2 w-auto bg-sidebar-border", className)}
{...props}
/>
)
})
SidebarSeparator.displayName = "SidebarSeparator"
const SidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className
)}
{...props}
/>
)
})
SidebarContent.displayName = "SidebarContent"
const SidebarGroup = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => {
return (
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
{...props}
/>
)
})
SidebarGroup.displayName = "SidebarGroup"
const SidebarGroupLabel = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "div"
return (
<Comp
ref={ref}
data-sidebar="group-label"
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className
)}
{...props}
/>
)
})
SidebarGroupLabel.displayName = "SidebarGroupLabel"
const SidebarGroupAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & { asChild?: boolean }
>(({ className, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="group-action"
className={cn(
"absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarGroupAction.displayName = "SidebarGroupAction"
const SidebarGroupContent = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="group-content"
className={cn("w-full text-sm", className)}
{...props}
/>
))
SidebarGroupContent.displayName = "SidebarGroupContent"
const SidebarMenu = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
{...props}
/>
))
SidebarMenu.displayName = "SidebarMenu"
const SidebarMenuItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
data-sidebar="menu-item"
className={cn("group/menu-item relative", className)}
{...props}
/>
))
SidebarMenuItem.displayName = "SidebarMenuItem"
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const SidebarMenuButton = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
{
asChild = false,
isActive = false,
variant = "default",
size = "default",
tooltip,
className,
...props
},
ref
) => {
const Comp = asChild ? Slot : "button"
const { isMobile, state } = useSidebar()
const button = (
<Comp
ref={ref}
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
{...props}
/>
)
if (!tooltip) {
return button
}
if (typeof tooltip === "string") {
tooltip = {
children: tooltip,
}
}
return (
<Tooltip>
<TooltipTrigger asChild>{button}</TooltipTrigger>
<TooltipContent
side="right"
align="center"
hidden={state !== "collapsed" || isMobile}
{...tooltip}
/>
</Tooltip>
)
}
)
SidebarMenuButton.displayName = "SidebarMenuButton"
const SidebarMenuAction = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<"button"> & {
asChild?: boolean
showOnHover?: boolean
}
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 after:md:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className
)}
{...props}
/>
)
})
SidebarMenuAction.displayName = "SidebarMenuAction"
const SidebarMenuBadge = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div">
>(({ className, ...props }, ref) => (
<div
ref={ref}
data-sidebar="menu-badge"
className={cn(
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground",
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuBadge.displayName = "SidebarMenuBadge"
const SidebarMenuSkeleton = React.forwardRef<
HTMLDivElement,
React.ComponentProps<"div"> & {
showIcon?: boolean
}
>(({ className, showIcon = false, ...props }, ref) => {
// Random width between 50 to 90%.
const width = React.useMemo(() => {
return `${Math.floor(Math.random() * 40) + 50}%`
}, [])
return (
<div
ref={ref}
data-sidebar="menu-skeleton"
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
{...props}
>
{showIcon && (
<Skeleton
className="size-4 rounded-md"
data-sidebar="menu-skeleton-icon"
/>
)}
<Skeleton
className="h-4 max-w-[--skeleton-width] flex-1"
data-sidebar="menu-skeleton-text"
style={
{
"--skeleton-width": width,
} as React.CSSProperties
}
/>
</div>
)
})
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
const SidebarMenuSub = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
))
SidebarMenuSub.displayName = "SidebarMenuSub"
const SidebarMenuSubItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ ...props }, ref) => <li ref={ref} {...props} />)
SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
const SidebarMenuSubButton = React.forwardRef<
HTMLAnchorElement,
React.ComponentProps<"a"> & {
asChild?: boolean
size?: "sm" | "md"
isActive?: boolean
}
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a"
return (
<Comp
ref={ref}
data-sidebar="menu-sub-button"
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className
)}
{...props}
/>
)
})
SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
export {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupAction,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInput,
SidebarInset,
SidebarMenu,
SidebarMenuAction,
SidebarMenuBadge,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSkeleton,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarProvider,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
useSidebar,
}

View File

@@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-primary/10", className)}
{...props}
/>
)
}
export { Skeleton }

View File

@@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -1,7 +1,15 @@
"use client";
import { AlertDialog, Button, Flex } from "@radix-ui/themes";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button";
import React, {
createContext,
ReactNode,
@@ -110,25 +118,25 @@ const ConfirmDialog = ({
message: string;
}) => {
return (
<AlertDialog.Root open={open} onOpenChange={onCancel}>
<AlertDialog.Content maxWidth="450px">
<AlertDialog.Title>{title}</AlertDialog.Title>
<AlertDialog.Description size="2">
<AlertDialog open={open} onOpenChange={onCancel}>
<AlertDialogContent>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>
{message}
</AlertDialog.Description>
<Flex gap="3" mt="4" justify="end">
<AlertDialog.Cancel>
<Button onClick={onCancel} variant="soft" color="gray">
</AlertDialogDescription>
<div className="flex items-center gap-3 mt-4 justify-end">
<AlertDialogCancel>
<Button onClick={onCancel} variant="ghost" color="gray">
Cancel
</Button>
</AlertDialog.Cancel>
<AlertDialog.Action>
<Button onClick={onConfirm} variant="solid" color="red">
</AlertDialogCancel>
<AlertDialogAction>
<Button onClick={onConfirm} variant={"destructive"}>
Continue
</Button>
</AlertDialog.Action>
</Flex>
</AlertDialog.Content>
</AlertDialog.Root>
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -1,26 +1,103 @@
import { Avatar, Button, DropdownMenu, IconButton, Inset, Text } from '@radix-ui/themes'
import React from 'react'
"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 { Button } from "./ui/button"
export function NavUser({
user,
}: {
user: {
name?: string
email: string
avatar?: string
}
}) {
const User = ({ user }: any) => {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton radius='full' className='!mr-1' size={"1"} variant='ghost'>
<Inset>
<Avatar size={"2"} radius='full' src={user?.user_metadata?.avatar_url} fallback={user?.user_metadata?.user_name?.[0] || "?"} variant="soft" />
</Inset>
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content className='p-2'>
<Text weight={"medium"}>{user?.user_metadata?.user_name}</Text>
<Text weight={"light"} size={"2"} className='opacity-70'>{user?.user_metadata?.email}</Text>
<DropdownMenu.Item>Profile</DropdownMenu.Item>
<DropdownMenu.Item>Settings</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item>API keys</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="lg"
variant="outline"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Sparkles />
Upgrade to Pro
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<BadgeCheck />
Account
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard />
Billing
</DropdownMenuItem>
<DropdownMenuItem>
<Bell />
Notifications
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut />
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
export default User

19
src/hooks/use-mobile.tsx Normal file
View File

@@ -0,0 +1,19 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
const onChange = () => {
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
}, [])
return !!isMobile
}

View File

@@ -2,7 +2,7 @@
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/src/lib/supabase/server'
import { createClient } from '@/lib/supabase/server'
export async function signInWithGithub() {
const supabase = await createClient()

View File

@@ -1,7 +1,7 @@
"use server"
import { createClient } from "../supabase/server"
import { Investigation } from "@/src/types/investigation"
import { NodeData, EdgeData } from "@/src/types"
import { Investigation } from "@/types/investigation"
import { NodeData, EdgeData } from "@/types"
import { notFound } from "next/navigation"
interface ReturnTypeGetInvestigations {
investigations: Investigation[],

View File

@@ -1,6 +1,6 @@
'use server'
import { getInvestigation } from "@/src/lib/actions/investigations"
import { getInvestigation } from "@/lib/actions/investigations"
import { notFound } from "next/navigation"
import { createClient } from "../supabase/server";

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client"
import { supabase } from "@/lib/supabase/client"
import { useQuery } from "@supabase-cache-helpers/postgrest-swr"
export function useEmailsAndBreaches(individualId: string | null | undefined) {

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
export function useIndividual(individualId: string | null | undefined) {

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
export function useIndividuals(investigationId: string | null | undefined) {

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
export function useRelations(individualId: string | null | undefined) {

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
import { notFound } from "next/navigation";

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
export function useSearchResults(search: string | undefined, investigationId: string | string[] | undefined) {

View File

@@ -1,4 +1,4 @@
import { supabase } from "@/src/lib/supabase/client";
import { supabase } from "@/lib/supabase/client";
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
export function useUsernames(investigationId: string | string[] | undefined) {

View File

@@ -1,7 +1,131 @@
@import "tailwindcss";
@plugin "tailwindcss-animate";
@custom-variant dark (&:is(.dark *));
[data-radius='full'] {
--radius-factor: 3 !important;
--radius-full: 999999px !important;
--radius-thumb: 999999px !important;
}
:root {
--background: hsl(0 0% 100%);
--foreground: hsl(222.2 84% 4.9%);
--card: hsl(0 0% 100%);
--card-foreground: hsl(222.2 84% 4.9%);
--popover: hsl(0 0% 100%);
--popover-foreground: hsl(222.2 84% 4.9%);
--primary: hsl(222.2 47.4% 11.2%);
--primary-foreground: hsl(210 40% 98%);
--secondary: hsl(210 40% 96.1%);
--secondary-foreground: hsl(222.2 47.4% 11.2%);
--muted: hsl(210 40% 96.1%);
--muted-foreground: hsl(215.4 16.3% 46.9%);
--accent: hsl(210 40% 96.1%);
--accent-foreground: hsl(222.2 47.4% 11.2%);
--destructive: hsl(0 84.2% 60.2%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(214.3 31.8% 91.4%);
--input: hsl(214.3 31.8% 91.4%);
--ring: hsl(222.2 84% 4.9%);
--chart-1: hsl(12 76% 61%);
--chart-2: hsl(173 58% 39%);
--chart-3: hsl(197 37% 24%);
--chart-4: hsl(43 74% 66%);
--chart-5: hsl(27 87% 67%);
--radius: 0.6rem;
}
.dark {
--background: hsl(0 0 11);
--foreground: hsl(210 40% 98%);
--card: hsla(0, 0%, 11%, 0.848);
--card-foreground: hsl(210 40% 98%);
--popover: hsl(0 0 11);
--popover-foreground: hsl(210 40% 98%);
--primary: hsl(210 40% 98%);
--primary-foreground: hsl(0 0 11);
--secondary: hsl(217.2 32.6% 17.5%);
--secondary-foreground: hsl(210 40% 98%);
--muted: hsl(217.2 32.6% 17.5%);
--muted-foreground: hsl(215 20.2% 65.1%);
--accent: hsl(217.2 32.6% 17.5%);
--accent-foreground: hsl(210 40% 98%);
--destructive: hsl(0 62.8% 30.6%);
--destructive-foreground: hsl(210 40% 98%);
--border: hsl(0, 0%, 24%);
--input: hsl(0, 0%, 24%);
--ring: hsl(212.7 26.8% 83.9%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@layer base {
:root {
--sidebar-background: hsl(0, 0%, 98%);
--sidebar-foreground: hsl(240, 5.3%, 26.1%);
--sidebar-primary: hsl(240, 5.9%, 10%);
--sidebar-primary-foreground: hsl(0, 0%, 98%);
--sidebar-accent: hsl(240, 4.8%, 95.9%);
--sidebar-accent-foreground: hsl(240, 5.9%, 10%);
--sidebar-border: hsl(220, 13%, 91%);
--sidebar-ring: hsl(217.2, 91.2%, 59.8%);
}
.dark {
--sidebar-background: hsl(240, 5.9%, 10%);
--sidebar-foreground: hsl(240, 4.8%, 95.9%);
--sidebar-primary: hsl(224.3, 76.3%, 48%);
--sidebar-primary-foreground: hsl(0, 0%, 100%);
--sidebar-accent: hsl(240, 3.7%, 15.9%);
--sidebar-accent-foreground: hsl(240, 4.8%, 95.9%);
--sidebar-border: hsl(240, 3.7%, 15.9%);
--sidebar-ring: hsl(217.2, 91.2%, 59.8%);
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -7,12 +7,28 @@ module.exports = {
"./node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-sans)"],
mono: ["var(--font-mono)"],
},
},
extend: {
fontFamily: {
sans: [
'var(--font-sans)'
],
mono: [
'var(--font-mono)'
]
},
colors: {
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))'
}
}
}
},
darkMode: "class",
darkMode: ["class", 'class'],
}

View File

@@ -20,7 +20,7 @@
}
],
"paths": {
"@/*": ["./*"]
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../ReconOps/src/lib/utils.ts", "src/components/investigations/collide.js", "src/lib/utils/flow.js"],

View File

@@ -546,7 +546,7 @@
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-alert-dialog@1.1.6":
"@radix-ui/react-alert-dialog@1.1.6", "@radix-ui/react-alert-dialog@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz#52187fdaa5110ed6749e75974e90c3505f788c5d"
integrity sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==
@@ -572,7 +572,7 @@
dependencies:
"@radix-ui/react-primitive" "2.0.2"
"@radix-ui/react-avatar@1.1.3":
"@radix-ui/react-avatar@1.1.3", "@radix-ui/react-avatar@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz#8de2bcebdc38fbe14e952ccacf05ba2cb426bd83"
integrity sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==
@@ -596,7 +596,7 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-collapsible@1.1.3":
"@radix-ui/react-collapsible@1.1.3", "@radix-ui/react-collapsible@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.3.tgz#522a5749646b7a393c9e9049165a9a6dfa924e91"
integrity sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==
@@ -625,7 +625,7 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec"
integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==
"@radix-ui/react-context-menu@2.2.6":
"@radix-ui/react-context-menu@2.2.6", "@radix-ui/react-context-menu@^2.2.6":
version "2.2.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz#752fd1d91f92bba287ef2b558770f4ca7d74523e"
integrity sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==
@@ -642,7 +642,7 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-dialog@1.1.6", "@radix-ui/react-dialog@^1.1.1":
"@radix-ui/react-dialog@1.1.6", "@radix-ui/react-dialog@^1.1.1", "@radix-ui/react-dialog@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz#65b4465e99ad900f28a98eed9a94bb21ec644bf7"
integrity sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==
@@ -678,7 +678,7 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-dropdown-menu@2.1.6":
"@radix-ui/react-dropdown-menu@2.1.6", "@radix-ui/react-dropdown-menu@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz#b66b62648b378370aa3c38e5727fd3bc5b8792a3"
integrity sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==
@@ -744,7 +744,7 @@
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-label@2.1.2":
"@radix-ui/react-label@2.1.2", "@radix-ui/react-label@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.2.tgz#994a5d815c2ff46e151410ae4e301f1b639f9971"
integrity sha512-zo1uGMTaNlHehDyFQcDZXRJhUPDuukcnHz0/jnrup0JA6qL+AFpAnty+7VKa9esuU5xTblAZzTGYJKSKaBxBhw==
@@ -811,7 +811,7 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-visually-hidden" "1.1.2"
"@radix-ui/react-popover@1.1.6":
"@radix-ui/react-popover@1.1.6", "@radix-ui/react-popover@^1.1.6":
version "1.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.1.6.tgz#699634dbc7899429f657bb590d71fb3ca0904087"
integrity sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==
@@ -910,7 +910,7 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-scroll-area@1.2.3":
"@radix-ui/react-scroll-area@1.2.3", "@radix-ui/react-scroll-area@^1.2.3":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz#6a9a7897add739ce84b517796ee345d495893d3f"
integrity sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==
@@ -925,7 +925,7 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-select@2.1.6":
"@radix-ui/react-select@2.1.6", "@radix-ui/react-select@^2.1.6":
version "2.1.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.6.tgz#79c07cac4de0188e6f7afb2720a87a0405d88849"
integrity sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==
@@ -952,7 +952,7 @@
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-separator@1.1.2":
"@radix-ui/react-separator@1.1.2", "@radix-ui/react-separator@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-separator/-/react-separator-1.1.2.tgz#24c5450db20f341f2b743ed4b07b907e18579216"
integrity sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==
@@ -976,14 +976,14 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-slot@1.1.2":
"@radix-ui/react-slot@1.1.2", "@radix-ui/react-slot@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.2.tgz#daffff7b2bfe99ade63b5168407680b93c00e1c6"
integrity sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==
dependencies:
"@radix-ui/react-compose-refs" "1.1.1"
"@radix-ui/react-switch@1.1.3":
"@radix-ui/react-switch@1.1.3", "@radix-ui/react-switch@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0"
integrity sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==
@@ -996,7 +996,7 @@
"@radix-ui/react-use-previous" "1.1.0"
"@radix-ui/react-use-size" "1.1.0"
"@radix-ui/react-tabs@1.1.3":
"@radix-ui/react-tabs@1.1.3", "@radix-ui/react-tabs@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz#c47c8202dc676dea47676215863d2ef9b141c17a"
integrity sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==
@@ -1063,7 +1063,7 @@
"@radix-ui/react-separator" "1.1.2"
"@radix-ui/react-toggle-group" "1.1.2"
"@radix-ui/react-tooltip@1.1.8":
"@radix-ui/react-tooltip@1.1.8", "@radix-ui/react-tooltip@^1.1.8":
version "1.1.8"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz#1aa2a575630fca2b2845b62f85056bb826bec456"
integrity sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==
@@ -2052,6 +2052,13 @@ character-reference-invalid@^2.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9"
integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
class-variance-authority@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz#4008a798a0e4553a781a57ac5177c9fb5d043787"
integrity sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==
dependencies:
clsx "^2.1.1"
classcat@^5.0.3:
version "5.0.5"
resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
@@ -2067,7 +2074,7 @@ client-only@0.0.1:
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
clsx@2.1.1, clsx@^2.0.0:
clsx@^2.0.0, clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
@@ -2242,6 +2249,11 @@ data-view-byte-offset@^1.0.1:
es-errors "^1.3.0"
is-data-view "^1.0.1"
debounce@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.0.2.tgz#503cc674d8d7f737099664fb75ddbd36b9626dc6"
integrity sha512-AW9SsfJ9CYtgGys1gzmNV21U8OR6xkJX7XZPSWRhzkwwJbfCBiHMLRCJFCpty2ZC1LxXZqRBT8VX7hZY01v2/w==
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
@@ -2337,6 +2349,11 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
domify@~1.4.0:
version "1.4.2"
resolved "https://registry.yarnpkg.com/domify/-/domify-1.4.2.tgz#2d3e8ac49cae41206cc84be31ca401050ae780a6"
integrity sha512-m4yreHcUWHBncGVV7U+yQzc12vIlq0jMrtHZ5mW6dQMiL/7skSYNVX9wqKwOtyO9SGCgevrAFEgOCAHmamHTUA==
dunder-proto@^1.0.0, dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -2351,6 +2368,11 @@ electron-to-chromium@^1.5.73:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.93.tgz#6fae820ac8bb772a841aa67fce5389e6c6da7ba2"
integrity sha512-M+29jTcfNNoR9NV7la4SwUqzWAxEwnc7ThA5e1m6LRSotmpfpCpLcIfgtSCVL+MllNLgAyM/5ru86iMRemPzDQ==
emitter-component@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.2.tgz#d65af5833dc7c682fd0ade35f902d16bc4bad772"
integrity sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
@@ -3598,10 +3620,10 @@ loose-envify@^1.4.0:
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lucide-react@^0.474.0:
version "0.474.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.474.0.tgz#9fcaa96250fa2de0b3e2803d4ad744eaea572247"
integrity sha512-CmghgHkh0OJNmxGKWc0qfPJCYHASPMVSyGY8fj3xgk4v84ItqDg64JNKFZn5hC6E0vHi6gxnbCgwhyVB09wQtA==
lucide-react@^0.475.0:
version "0.475.0"
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.475.0.tgz#4b7b62c024f153ee4b52a6a0f33f9e72f07156f0"
integrity sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==
math-intrinsics@^1.1.0:
version "1.1.0"
@@ -4196,6 +4218,15 @@ picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
popover@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/popover/-/popover-2.4.1.tgz#aeec30d9628b9399ebf13e10f8aafcc154e57a9e"
integrity sha512-YiiOLTvNUJfuhuMm6ug9Uk80nTYUnWjrNRlS8ufWD2W9uzchKG+xpHlid++TTZ30uzEPHUF1qr9vumAFXGdqlw==
dependencies:
debounce "~1.0.0"
domify "~1.4.0"
emitter-component "~1.1.0"
possible-typed-array-names@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f"
@@ -4830,6 +4861,11 @@ tailwind-merge@^1.14.0:
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.14.0.tgz#e677f55d864edc6794562c63f5001f45093cdb8b"
integrity sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==
tailwind-merge@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-3.0.1.tgz#0f0189966511ebcd63ef98d9eaf5607beb0d59d3"
integrity sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==
tailwind-variants@0.1.20:
version "0.1.20"
resolved "https://registry.yarnpkg.com/tailwind-variants/-/tailwind-variants-0.1.20.tgz#8aaed9094be0379a438641a42d588943e44c5fcd"
@@ -4837,6 +4873,11 @@ tailwind-variants@0.1.20:
dependencies:
tailwind-merge "^1.14.0"
tailwindcss-animate@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4"
integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==
tailwindcss@4.0.3, tailwindcss@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.0.3.tgz#8f34c24cd23e021662113bd4e13df1e700cb52f8"