Skip to content

Commit

Permalink
refactor: extract clipboard and format logic into reusable hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
boiln committed Jan 25, 2025
1 parent b0ea750 commit 7261627
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 236 deletions.
158 changes: 20 additions & 138 deletions src/components/session/SessionViewer/ContentPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"use client";

import { useEffect, useState, useRef } from "react";

import { useEffect, useRef } from "react";
import { WrapText, Code2 } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";

import { ScrollArea } from "@/components/ui/scroll-area";
import { Toggle } from "@/components/ui/toggle";
import { useMessageFormatter } from "@/hooks/session/useMessageFormatter";
import { useToast } from "@/hooks/use-toast";
import { useClipboard } from "@/hooks/useClipboard";
import { decodeBase64 } from "@/lib/burpParser";
import { toCurl } from "@/lib/toCurl";
import type { ContentPanelProps } from "@/types/session";
Expand All @@ -24,166 +22,50 @@ export function ContentPanel({
prettify,
setPrettify,
}: ContentPanelProps) {
const { toast } = useToast();
const [isMounted, setIsMounted] = useState(false);
const { copyToClipboard, isMounted } = useClipboard();
const content = item?.[type] || { value: "", base64: false };
const { formatMessage } = useMessageFormatter();
const decodedContent = content.base64 ? decodeBase64(content.value) : content.value;
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
setIsMounted(true);
}, []);

useEffect(() => {
if (contentRef.current) {
contentRef.current.scrollTop = 0;
}
}, [item]);

const copyTextToClipboard = async (text: string): Promise<void> => {
if (!isMounted) return;

try {
// First try the modern clipboard API
if (window?.navigator?.clipboard) {
await window.navigator.clipboard.writeText(text);
return;
}

// Fallback to execCommand
const textArea = document.createElement("textarea");
textArea.value = text;

// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
textArea.style.opacity = "0";

document.body.appendChild(textArea);
textArea.select();

const successful = document.execCommand("copy");
document.body.removeChild(textArea);

if (!successful) {
throw new Error("Failed to copy text");
}
} catch (err) {
console.error("Copy failed:", err);
throw err;
}
};

const handleCopy = {
raw: async () => {
try {
await copyTextToClipboard(decodedContent);
toast({
description: "Copied raw content to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy raw content:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
await copyToClipboard(decodedContent, "raw content");
},
headers: async () => {
try {
const { headers } = formatMessage(decodedContent, {
wrap: false,
prettify: false,
});
const headerLines = headers.split("\n");
const justHeaders = headerLines.slice(1).join("\n").trim();
await copyTextToClipboard(justHeaders);
toast({
description: "Copied headers to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy headers:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
},
cookies: async () => {
try {
const { headers } = formatMessage(decodedContent, {
wrap: false,
prettify: false,
});
const cookieLines = headers
.split("\n")
.filter((line) => line.toLowerCase().startsWith("cookie:"))
.join("\n");
await copyTextToClipboard(cookieLines);
toast({
description: "Copied cookies to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy cookies:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
const { headers } = formatMessage(decodedContent, {
wrap: false,
prettify: false,
});
const headerLines = headers.split("\n");
const justHeaders = headerLines.slice(1).join("\n").trim();
await copyToClipboard(justHeaders, "headers");
},
payload: async () => {
try {
const { body } = formatMessage(decodedContent, {
wrap: false,
prettify,
});
await copyTextToClipboard(body);
toast({
description: "Copied payload to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy payload:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
body: async () => {
const { body } = formatMessage(decodedContent, {
wrap: false,
prettify: false,
});
await copyToClipboard(body, "body");
},
curl:
type === "request" && item
? async () => {
try {
const curl = toCurl(item);
await copyTextToClipboard(curl);
toast({
description: "Copied curl command to clipboard",
duration: 2000,
});
} catch (err) {
console.error("Failed to copy curl command:", err);
toast({
description: "Failed to copy to clipboard",
variant: "destructive",
duration: 2000,
});
}
const curl = toCurl(item);
await copyToClipboard(curl, "curl command");
}
: undefined,
};

// Don't attempt to render until mounted
if (!isMounted) {
return null; // or a loading state
return null;
}

return (
Expand Down
112 changes: 48 additions & 64 deletions src/components/session/SessionViewer/TableContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { Copy, MessageCircle, Paintbrush2, Link, Globe, Clock, Terminal } from "lucide-react";
import { useToast } from "@/hooks/use-toast";
import { useClipboard } from "@/hooks/useClipboard";
import type { BurpItem, HighlightColor } from "@/types/burp";
import { toCurl } from "@/lib/toCurl";

interface TableContextMenuProps {
children: React.ReactNode;
item: BurpItem;
items?: BurpItem[]; // Optional array of items for bulk operations
onHighlight: (color: HighlightColor) => void;
onHighlight: (color: HighlightColor | null) => void;
onUpdateComment: (comment: string) => void;
}

Expand All @@ -47,7 +47,7 @@ export function TableContextMenu({
}: TableContextMenuProps) {
const [showCommentDialog, setShowCommentDialog] = useState(false);
const [comment, setComment] = useState(item.comment);
const { toast } = useToast();
const { copyToClipboard, isMounted } = useClipboard();

const isBulkOperation = items && items.length > 1;

Expand All @@ -62,72 +62,61 @@ export function TableContextMenu({
setShowCommentDialog(false);
};

const copyToClipboard = (text: string, description: string) => {
navigator.clipboard.writeText(text);
toast({
description: `Copied ${description} to clipboard`,
duration: 2000,
});
};

const handleCopyUrl = () => {
const handleCopyUrl = async () => {
const fullUrl = `${item.host.value}${item.url}`;
navigator.clipboard.writeText(fullUrl);
await copyToClipboard(fullUrl, "URL");
};

const handleCopyCurl = () => {
const handleCopyCurl = async () => {
const curl = toCurl(item);
copyToClipboard(curl, "curl command");
await copyToClipboard(curl, "curl command");
};

const handleCopyHost = async () => {
await copyToClipboard(item.host.value, "host");
};

const handleCopyTime = async () => {
await copyToClipboard(item.time, "timestamp");
};

if (!isMounted) return null;

return (
<>
<ContextMenu>
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
<ContextMenuContent className="min-w-[180px] p-1">
{/* Copy Options - Only show in single item mode */}
{!isBulkOperation && (
<>
<ContextMenuSub>
<ContextMenuSubTrigger className="h-7 px-2">
<Copy className="mr-2 h-4 w-4" />
Copy
</ContextMenuSubTrigger>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={handleCopyUrl}>
<Link className="mr-2 h-4 w-4" />
URL
</ContextMenuItem>
<ContextMenuItem
className="h-7 px-2"
onClick={() => copyToClipboard(item.host.ip, "IP")}
>
<Globe className="mr-2 h-4 w-4" />
IP
</ContextMenuItem>
<ContextMenuItem
className="h-7 px-2"
onClick={() => copyToClipboard(item.time, "Time")}
>
<Clock className="mr-2 h-4 w-4" />
Time
</ContextMenuItem>
<ContextMenuSeparator className="my-0.5" />
<ContextMenuItem className="h-7 px-2" onClick={handleCopyCurl}>
<Terminal className="mr-2 h-4 w-4" />
cURL (bash)
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSub>
<ContextMenuSubTrigger className="h-7 px-2">
<Copy className="mr-2 h-4 w-4" />
<span>Copy</span>
</ContextMenuSubTrigger>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={handleCopyUrl}>
<Link className="mr-2 h-4 w-4" />
URL
</ContextMenuItem>
<ContextMenuItem className="h-7 px-2" onClick={handleCopyHost}>
<Globe className="mr-2 h-4 w-4" />
Host
</ContextMenuItem>
<ContextMenuItem className="h-7 px-2" onClick={handleCopyTime}>
<Clock className="mr-2 h-4 w-4" />
Time
</ContextMenuItem>
<ContextMenuSeparator className="my-0.5" />
</>
)}

{/* Highlight Options */}
<ContextMenuItem className="h-7 px-2" onClick={handleCopyCurl}>
<Terminal className="mr-2 h-4 w-4" />
cURL (bash)
</ContextMenuItem>
</ContextMenuSubContent>
</ContextMenuSub>
<ContextMenuSeparator className="my-0.5" />
<ContextMenuSub>
<ContextMenuSubTrigger className="h-7 px-2">
<Paintbrush2 className="mr-2 h-4 w-4" />
{isBulkOperation ? "Highlight" : "Highlight"}
<span>Highlight {isBulkOperation ? "All" : ""}</span>
</ContextMenuSubTrigger>
<ContextMenuSubContent className="min-w-[160px] p-1">
<ContextMenuItem className="h-7 px-2" onClick={() => onHighlight(null)}>
Expand All @@ -154,29 +143,24 @@ export function TableContextMenu({
onClick={() => setShowCommentDialog(true)}
>
<MessageCircle className="mr-2 h-4 w-4" />
{isBulkOperation
? "Add Comment"
: item.comment
? "Edit Comment"
: "Add Comment"}
{isBulkOperation ? "Comment All" : "Comment"}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

<Dialog open={showCommentDialog} onOpenChange={setShowCommentDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>{isBulkOperation ? "Add Comment" : "Add Comment"}</DialogTitle>
<DialogTitle>
{isBulkOperation ? "Add Comment to All" : "Add Comment"}
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<Input
placeholder={
isBulkOperation ? "Enter bulk comment..." : "Enter comment..."
}
value={comment}
onChange={(e) => setComment(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
placeholder="Enter comment..."
/>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setShowCommentDialog(false)}>
Expand Down
Loading

0 comments on commit 7261627

Please sign in to comment.