diff --git a/app/form/admin/client.tsx b/app/form/admin/client.tsx index 364e7e9..82f9d5f 100644 --- a/app/form/admin/client.tsx +++ b/app/form/admin/client.tsx @@ -20,6 +20,7 @@ import { } from "@/app/actions/questions"; import { uploadImage } from "@/app/actions/upload"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Save, Image as ImageIcon, X } from "lucide-react"; +import Image from 'next/image'; interface Question { id: string; @@ -297,7 +298,8 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
{q.imageUrl ? (
- {q.label}; } +interface ContextMenuState { + visible: boolean; + x: number; + y: number; + questionId: string | null; +} + export default function SubmissionDetailClient({ submission, questions, answers, }: SubmissionDetailClientProps) { const [copiedId, setCopiedId] = useState(null); + const [contextMenu, setContextMenu] = useState({ + visible: false, + x: 0, + y: 0, + questionId: null, + }); + const menuRef = useRef(null); const handleCopy = (text: string, id: string) => { if (!text) return; @@ -41,12 +56,48 @@ export default function SubmissionDetailClient({ setTimeout(() => setCopiedId(null), 2000); }; + const handleContextMenu = (e: React.MouseEvent, questionId: string) => { + e.preventDefault(); + setContextMenu({ visible: true, x: e.clientX, y: e.clientY, questionId }); + }; + + const closeMenu = () => { + setContextMenu((prev) => ({ ...prev, visible: false, questionId: null })); + }; + + // Close menu on click outside or scroll + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { + closeMenu(); + } + }; + document.addEventListener("mousedown", handleClick); + document.addEventListener("scroll", closeMenu, true); + return () => { + document.removeEventListener("mousedown", handleClick); + document.removeEventListener("scroll", closeMenu, true); + }; + }, []); + const userLabel = submission.userName ? `${submission.userName} (${submission.userDiscordId || "N/A"})` : submission.userDiscordId || "Anonymous"; const editHistory = Array.isArray(submission.editHistory) ? submission.editHistory : []; + // Build context menu data for the right-clicked question + const ctxQuestion = questions.find((q) => q.id === contextMenu.questionId); + const ctxAnswerHistory: { editedAt: string; answer: string }[] = []; + if (contextMenu.questionId) { + for (const edit of editHistory) { + ctxAnswerHistory.push({ + editedAt: edit.editedAt, + answer: edit.oldAnswers[contextMenu.questionId] ?? "", + }); + } + } + return (
{/* Submission Metadata Card */} @@ -83,41 +134,32 @@ export default function SubmissionDetailClient({
{questions.map((q) => { const ansValue = answers[q.id] || ""; - - // Check if this specific question was edited by comparing current answer with old ones - let wasEdited = false; - let oldAnswerForQ = ""; - let lastEditedTime = ""; - if (editHistory.length > 0) { - // Find the most recent edit where the answer was different - for (let i = editHistory.length - 1; i >= 0; i--) { - const oldAns = editHistory[i].oldAnswers[q.id] || ""; - if (oldAns !== ansValue) { - wasEdited = true; - oldAnswerForQ = oldAns; - lastEditedTime = new Date(editHistory[i].editedAt).toLocaleString(); - break; - } - } - } + const wasEdited = editHistory.some( + (edit) => (edit.oldAnswers[q.id] ?? "") !== ansValue + ); return ( - + handleContextMenu(e, q.id)} + className={wasEdited ? "cursor-context-menu" : ""} + >
{wasEdited && ( -

- This section has been edited (last edit: {lastEditedTime}) +

+ edited

)}
{q.imageUrl && (
- {q.label} - {/* Current Answer */} {ansValue ? (
- {wasEdited && Current Answer:} + {wasEdited && ( + + Current Answer: + + )} {ansValue} @@ -153,23 +198,84 @@ export default function SubmissionDetailClient({ No response provided )} - - {/* Previous Answer if edited */} - {wasEdited && ( -
-
- Previous Answer: - - {oldAnswerForQ || No previous response} - -
-
- )} ); })}
+ + {/* Context Menu */} + {contextMenu.visible && ctxQuestion && ( +
+ {/* Header */} +
+

+ History +

+

+ {ctxQuestion.label} +

+
+ + {/* Edit History timestamps */} + {editHistory.length > 0 && ( +
+

+ Edit History: +

+
    + {editHistory.map((edit, idx) => ( +
  • + + {new Date(edit.editedAt).toLocaleString()} +
  • + ))} +
+
+ )} + + {/* All Previous Answers */} +
+

+ Previous Answers: +

+ {ctxAnswerHistory.length === 0 ? ( +

No edit history for this question.

+ ) : ( +
+ {ctxAnswerHistory.map((entry, idx) => ( +
+
+ + {new Date(entry.editedAt).toLocaleString()} +
+ + {entry.answer || No response} + +
+ ))} +
+ )} +
+ + {/* Footer */} +
+ +
+
+ )}
); } diff --git a/components/delete-form-button.tsx b/components/delete-form-button.tsx index 56a271b..6cdcdb1 100644 --- a/components/delete-form-button.tsx +++ b/components/delete-form-button.tsx @@ -39,7 +39,7 @@ export default function DeleteFormButton({ formId }: DeleteFormButtonProps) { return ( - @@ -50,7 +50,7 @@ export default function DeleteFormButton({ formId }: DeleteFormButtonProps) { This action cannot be undone. This will permanently delete the form, all of its questions, and all submissions/answers. - + diff --git a/components/ui/context-menu.tsx b/components/ui/context-menu.tsx new file mode 100644 index 0000000..ef28518 --- /dev/null +++ b/components/ui/context-menu.tsx @@ -0,0 +1,263 @@ +"use client" + +import * as React from "react" +import { ContextMenu as ContextMenuPrimitive } from "radix-ui" + +import { cn } from "@/lib/utils" +import { ChevronRightIcon, CheckIcon } from "lucide-react" + +function ContextMenu({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function ContextMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuContent({ + className, + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + ) +} + +function ContextMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function ContextMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function ContextMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuCheckboxItem({ + className, + children, + checked, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuRadioItem({ + className, + children, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + + + + + + {children} + + ) +} + +function ContextMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function ContextMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function ContextMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +}