From 5bee9a612a84a4249bbc4a1f0dc959db5090470f Mon Sep 17 00:00:00 2001 From: dextmorgn Date: Tue, 4 Feb 2025 16:58:09 +0100 Subject: [PATCH] feat: user modal --- package.json | 2 + .../[investigation_id]/page.tsx | 3 + src/components/dashboard/new-case.tsx | 2 +- .../investigations/individual-modal.tsx | 285 ++++++++++++++++++ src/components/investigations/new-actions.tsx | 1 + .../investigations/nodes/individual.tsx | 79 ++--- .../investigations/nodes/ip_address.tsx | 45 ++- src/lib/actions/investigations.ts | 6 +- src/lib/hooks/use-individual.ts | 17 ++ tsconfig.json | 2 +- yarn.lock | 14 + 11 files changed, 383 insertions(+), 73 deletions(-) create mode 100644 src/components/investigations/individual-modal.tsx create mode 100644 src/lib/hooks/use-individual.ts diff --git a/package.json b/package.json index a2c09cb..ea0b2f3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "@tailwindcss/postcss": "^4.0.3", "@xyflow/react": "^12.4.2", "clsx": "2.1.1", + "d3-force": "^3.0.0", + "d3-quadtree": "^3.0.1", "framer-motion": "11.13.1", "intl-messageformat": "^10.5.0", "lucide-react": "^0.474.0", diff --git a/src/app/investigations/[investigation_id]/page.tsx b/src/app/investigations/[investigation_id]/page.tsx index 1d9caf4..5629a23 100644 --- a/src/app/investigations/[investigation_id]/page.tsx +++ b/src/app/investigations/[investigation_id]/page.tsx @@ -1,5 +1,6 @@ import { getInvestigationData } from '@/src/lib/actions/investigations' import InvestigationGraph from '@/src/components/investigations/graph' +import IndividualModal from '@/src/components/investigations/individual-modal' const DashboardPage = async ({ params, @@ -8,9 +9,11 @@ const DashboardPage = async ({ }) => { const { investigation_id } = await (params) const { nodes, edges } = await getInvestigationData(investigation_id) + return (
+
) } diff --git a/src/components/dashboard/new-case.tsx b/src/components/dashboard/new-case.tsx index 68e1375..2c76b57 100644 --- a/src/components/dashboard/new-case.tsx +++ b/src/components/dashboard/new-case.tsx @@ -61,7 +61,7 @@ export default function NewCase() { diff --git a/src/components/investigations/individual-modal.tsx b/src/components/investigations/individual-modal.tsx new file mode 100644 index 0000000..1c79072 --- /dev/null +++ b/src/components/investigations/individual-modal.tsx @@ -0,0 +1,285 @@ +"use client" +import type React from "react" +import { useEffect, useState } from "react" +import { + Flex, + Dialog, + TextField, + Button, + Box, + Skeleton, + Card, + Inset, + Separator, + Avatar, + Tabs, + IconButton, +} from "@radix-ui/themes" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { useIndividual } from "@/src/lib/hooks/use-individual" +import { Pencil1Icon, Cross2Icon, PlusIcon, TrashIcon } from "@radix-ui/react-icons" +import social from "./nodes/social" + +const IndividualModal = () => { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const individual_id = searchParams.get("individual_id") + const { individual, isLoading } = useIndividual(individual_id) + const [editMode, setEditMode] = useState(false) + const [emails, setEmails] = useState([""]) + const [phones, setPhones] = useState([""]) + const [accounts, setAccounts] = useState([""]) + const [ipAddresses, setIpAddresses] = useState([""]) + + console.log(individual) + useEffect(() => { + setEmails(individual?.emails.map(({ email }: any) => email) || [""]) + setPhones(individual?.phone_numbers.map(({ phone_number }: any) => phone_number) || [""]) + setIpAddresses(individual?.ip_addresses.map(({ ip_address }: any) => ip_address) || [""]) + setAccounts(individual?.social_accounts.map(({ username }: any) => username) || [""]) + }, [individual]) + + const handleCloseModal = () => { + router.push(pathname) + } + + const handleSave = (event: React.FormEvent) => { + event.preventDefault() + const formData = new FormData(event.currentTarget) + const formContent = Object.fromEntries(formData.entries()) + formContent.emails = emails as any + formContent.phones = phones as any + formContent.ip_addresses = ipAddresses as any + alert(JSON.stringify(formContent, null, 2)) + setEditMode(false) + } + + const handleAddField = (setter: React.Dispatch>) => { + setter((prev) => [...prev, ""]) + } + + const handleRemoveField = (index: number, setter: React.Dispatch>) => { + setter((prev) => prev.filter((_, i) => i !== index)) + } + + const handleFieldChange = (index: number, value: string, setter: React.Dispatch>) => { + setter((prev) => prev.map((item, i) => (i === index ? value : item))) + } + + if (!isLoading && !individual) { + return ( + + + No data + + No data found for this individual. + + + + + + + ) + } + + return ( + + + +
+ + + User Profile + setEditMode(!editMode)} + aria-label={editMode ? "Cancel edit" : "Edit profile"} + > + {editMode ? : } + + + + + + {editMode && ( + + )} + + + + + Overview + Social accounts + Emails + Phone numbers + IP addresses + + + + + + {emails.map((email: string | number | undefined, index: React.Key | null | undefined) => ( + + handleFieldChange(index, e.target.value, setEmails)} + placeholder="Email" + type="email" + disabled={!editMode} + style={{ flexGrow: 1 }} + /> + {editMode && ( + handleRemoveField(index, setEmails)} + aria-label="Remove email" + > + + + )} + + ))} + {editMode && ( + + )} + {phones.map((phone, index) => ( + + handleFieldChange(index, e.target.value, setPhones)} + placeholder="Phone Number" + type="tel" + disabled={!editMode} + style={{ flexGrow: 1 }} + /> + {editMode && ( + handleRemoveField(index, setPhones)} + aria-label="Remove phone" + > + + + )} + + ))} + {editMode && ( + + )} + + + + + + + {editMode && ( + + )} + {accounts.map((account, index) => ( + + handleFieldChange(index, e.target.value, setAccounts)} + placeholder="Social account" + disabled={!editMode} + style={{ flexGrow: 1 }} + /> + {editMode && ( + handleRemoveField(index, setAccounts)} + aria-label="Remove social account" + > + + + )} + + ))} + {editMode && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + {editMode && } + +
+
+
+
+ ) +} + +export default IndividualModal + diff --git a/src/components/investigations/new-actions.tsx b/src/components/investigations/new-actions.tsx index 2f07462..dd412c2 100644 --- a/src/components/investigations/new-actions.tsx +++ b/src/components/investigations/new-actions.tsx @@ -42,6 +42,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) { /> { + const params = new URLSearchParams(searchParams.toString()) + params.set(name, value) + return params.toString() + }, + [searchParams] + ) + const handleOpenIndividualModal = () => router.push(pathname + '?' + createQueryString('individual_id', data.id)) return ( <> - {settings.showNodeLabel ? - setOpen(true)}> + {settings.showNodeLabel ? + : - } - setOpen(true)}>View and edit + View and edit Duplicate @@ -87,47 +99,6 @@ function Custom({ data }: any) { - - - {data.full_name} - - {data.notes} - - - - - - - - - - - - - - - - - ); } diff --git a/src/components/investigations/nodes/ip_address.tsx b/src/components/investigations/nodes/ip_address.tsx index a3072f4..b9094fd 100644 --- a/src/components/investigations/nodes/ip_address.tsx +++ b/src/components/investigations/nodes/ip_address.tsx @@ -1,33 +1,48 @@ "use client" import React, { memo } from 'react'; import { Handle, Position } from '@xyflow/react'; -import { Card, Box, Text, ContextMenu, Flex, Inset, Badge } from '@radix-ui/themes'; +import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes'; import { NodeProvider, useNodeContext } from './node-context'; import { LocateIcon } from 'lucide-react'; import { cn } from '@/utils'; +import { useInvestigationContext } from '../investigation-provider'; function Custom({ data }: any) { const { handleDeleteNode, loading } = useNodeContext() + const { settings } = useInvestigationContext() return ( <> - - - - - - - - - {data.label} - - - - - + {settings.showNodeLabel ? + + + + + + + + + {data.label} + + + + + + : + + + }