mirror of
https://github.com/reconurge/flowsint.git
synced 2026-07-10 16:13:05 -05:00
feat: shadcn
This commit is contained in:
21
components.json
Normal file
21
components.json
Normal 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
6
lib/utils.ts
Normal 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))
|
||||
}
|
||||
23
package.json
23
package.json
@@ -14,7 +14,22 @@
|
||||
"@dagrejs/dagre": "^1.1.4",
|
||||
"@internationalized/date": "^3.7.0",
|
||||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-collapsible": "^1.1.3",
|
||||
"@radix-ui/react-context-menu": "^2.2.6",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-scroll-area": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@radix-ui/themes": "^3.2.0",
|
||||
"@react-aria/ssr": "3.9.7",
|
||||
"@react-aria/visually-hidden": "3.8.18",
|
||||
@@ -25,16 +40,18 @@
|
||||
"@tailwindcss/postcss": "^4.0.3",
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"ai": "^4.1.34",
|
||||
"clsx": "2.1.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-quadtree": "^3.0.1",
|
||||
"framer-motion": "^12.3.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lucide-react": "^0.474.0",
|
||||
"lucide-react": "^0.475.0",
|
||||
"next": "15.1.6",
|
||||
"next-themes": "^0.4.4",
|
||||
"nextjs-toploader": "^3.7.15",
|
||||
"popover": "^2.4.1",
|
||||
"postcss": "^8.5.1",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
@@ -42,7 +59,9 @@
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"swr": "^2.3.0",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.3",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"usehooks-ts": "^3.1.1",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.1"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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'à 10 clés API et les supprimer lorsque vous n'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'expiration</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell className="w-[50px]"></Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>clé d'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'expiration</Table.ColumnHeaderCell>
|
||||
<Table.ColumnHeaderCell className="w-[50px]"></Table.ColumnHeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.Row>
|
||||
<Table.RowHeaderCell>clé d'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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
175
src/components/app-sidebar.tsx
Normal file
175
src/components/app-sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
@@ -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 >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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't have an account?{" "}
|
||||
<Button variant="ghost" formAction={signup}>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
73
src/components/nav-main.tsx
Normal file
73
src/components/nav-main.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
89
src/components/nav-projects.tsx
Normal file
89
src/components/nav-projects.tsx
Normal 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
114
src/components/nav-user.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
89
src/components/team-switcher.tsx
Normal file
89
src/components/team-switcher.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
141
src/components/ui/alert-dialog.tsx
Normal file
141
src/components/ui/alert-dialog.tsx
Normal 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,
|
||||
}
|
||||
59
src/components/ui/alert.tsx
Normal file
59
src/components/ui/alert.tsx
Normal 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 }
|
||||
50
src/components/ui/avatar.tsx
Normal file
50
src/components/ui/avatar.tsx
Normal 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 }
|
||||
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal 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 }
|
||||
115
src/components/ui/breadcrumb.tsx
Normal file
115
src/components/ui/breadcrumb.tsx
Normal 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,
|
||||
}
|
||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal 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 }
|
||||
76
src/components/ui/card.tsx
Normal file
76
src/components/ui/card.tsx
Normal 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 }
|
||||
11
src/components/ui/collapsible.tsx
Normal file
11
src/components/ui/collapsible.tsx
Normal 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 }
|
||||
200
src/components/ui/context-menu.tsx
Normal file
200
src/components/ui/context-menu.tsx
Normal 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,
|
||||
}
|
||||
122
src/components/ui/dialog.tsx
Normal file
122
src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
201
src/components/ui/dropdown-menu.tsx
Normal file
201
src/components/ui/dropdown-menu.tsx
Normal 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,
|
||||
}
|
||||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal 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 }
|
||||
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal 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 }
|
||||
33
src/components/ui/popover.tsx
Normal file
33
src/components/ui/popover.tsx
Normal 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 }
|
||||
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal 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 }
|
||||
159
src/components/ui/select.tsx
Normal file
159
src/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal 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
140
src/components/ui/sheet.tsx
Normal 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,
|
||||
}
|
||||
763
src/components/ui/sidebar.tsx
Normal file
763
src/components/ui/sidebar.tsx
Normal 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,
|
||||
}
|
||||
15
src/components/ui/skeleton.tsx
Normal file
15
src/components/ui/skeleton.tsx
Normal 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 }
|
||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal 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 }
|
||||
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal 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 }
|
||||
32
src/components/ui/tooltip.tsx
Normal file
32
src/components/ui/tooltip.tsx
Normal 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 }
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
19
src/hooks/use-mobile.tsx
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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[],
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
81
yarn.lock
81
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user