UNPKG

sticky-horse

Version:

With StickyHorse allow your users to send feedback to your team.

466 lines (465 loc) 28.9 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UserFeedbackOverlay = void 0; var react_1 = __importStar(require("react")); var StickyHorseContext_1 = require("../contexts/StickyHorseContext"); var socket_1 = require("../utils/socket"); var emoji_picker_react_1 = __importDefault(require("emoji-picker-react")); // Import the emoji picker component var colorOptions = [ 'bg-yellow-200', 'bg-purple-200', 'bg-green-200', 'bg-pink-200', 'bg-blue-200', ]; var UserFeedbackOverlay = function () { var _a = (0, StickyHorseContext_1.useStickyHorse)(), comments = _a.comments, setComments = _a.setComments, stickyNotes = _a.stickyNotes, setStickyNotes = _a.setStickyNotes, addComment = _a.addComment, addStickyNote = _a.addStickyNote, removeStickyNote = _a.removeStickyNote, removeComment = _a.removeComment, trackingInfo = _a.trackingInfo, currentUserId = _a.currentUserId, isBeingTracked = _a.isBeingTracked, handleCommentMouseDown = _a.handleCommentMouseDown, isDraggingComment = _a.isDraggingComment; var _b = (0, react_1.useState)(colorOptions[0]), selectedColor = _b[0], setSelectedColor = _b[1]; var _c = (0, react_1.useState)(''), commentText = _c[0], setCommentText = _c[1]; // const [noteContent, setNoteContent] = useState<string>(''); // const updateTimeoutRef = useRef<NodeJS.Timeout>(); var _d = (0, react_1.useState)(false), isPlacing = _d[0], setIsPlacing = _d[1]; var _e = (0, react_1.useState)(false), isAddingNote = _e[0], setIsAddingNote = _e[1]; var _f = (0, react_1.useState)(null), replyToComment = _f[0], setReplyToComment = _f[1]; var _g = (0, react_1.useState)([]), tempNotes = _g[0], setTempNotes = _g[1]; var _h = (0, react_1.useState)(null), isDragging = _h[0], setIsDragging = _h[1]; var _j = (0, react_1.useState)({ x: 0, y: 0 }), dragOffset = _j[0], setDragOffset = _j[1]; var _k = (0, react_1.useState)(false), isDragStarted = _k[0], setIsDragStarted = _k[1]; var _l = (0, react_1.useState)(false), showEmojiPicker = _l[0], setShowEmojiPicker = _l[1]; var _m = (0, react_1.useState)(new Set()), minimizedComments = _m[0], setMinimizedComments = _m[1]; // Handle click to position comment var handlePageClick = (0, react_1.useCallback)(function (e) { if (!isPlacing && !isAddingNote) return; var target = e.target; if (target.closest('.comment-input-box')) return; if (isPlacing && commentText.trim() && trackingInfo) { var position = { x: e.clientX, y: e.clientY }; var newComment = { comment: commentText, position: (replyToComment === null || replyToComment === void 0 ? void 0 : replyToComment.position) || position, timestamp: new Date().toISOString(), fromUserId: currentUserId, targetUserId: trackingInfo.userId, page: window.location.pathname, parentId: replyToComment === null || replyToComment === void 0 ? void 0 : replyToComment.id }; addComment(newComment); setCommentText(''); setIsPlacing(false); setReplyToComment(null); document.body.style.cursor = 'default'; } if (isAddingNote && trackingInfo) { var position = { x: e.clientX, y: e.clientY }; var newNote = { id: "".concat(Date.now(), "-").concat(Math.random().toString(36).substr(2, 9)), content: commentText, position: position, color: selectedColor, userId: currentUserId, targetUserId: trackingInfo.userId, page: window.location.pathname }; addStickyNote(newNote); setCommentText(''); setIsAddingNote(false); document.body.style.cursor = 'default'; } }, [isPlacing, isAddingNote, commentText, trackingInfo, currentUserId, selectedColor, replyToComment]); // Add click event listener (0, react_1.useEffect)(function () { if (isPlacing || isAddingNote) { window.addEventListener('click', handlePageClick); document.body.style.cursor = 'crosshair'; } return function () { window.removeEventListener('click', handlePageClick); document.body.style.cursor = 'default'; }; }, [isPlacing, isAddingNote, handlePageClick]); var handleAddComment = function (comment) { if (!commentText.trim() || !trackingInfo) return; var newComment = { comment: commentText, position: { x: comment.position.x, y: comment.position.y + 40 // Offset the reply slightly below the parent }, timestamp: new Date().toISOString(), fromUserId: currentUserId, targetUserId: trackingInfo.userId, parentId: comment.id, page: window.location.pathname }; addComment(newComment); setCommentText(''); setReplyToComment(null); }; var handleAddStickyNote = function (e) { e.preventDefault(); if (isDragStarted || isDragging) return; if (!trackingInfo) return; var position = { x: e.clientX, y: e.clientY }; var newNote = { content: '', position: position, color: selectedColor, userId: currentUserId, targetUserId: trackingInfo.userId, page: window.location.pathname }; setTempNotes(function (prev) { return __spreadArray(__spreadArray([], prev, true), [newNote], false); }); }; // Filter comments based on the current user's role var relevantComments = comments.filter(function (comment) { return comment.fromUserId === currentUserId || comment.targetUserId === currentUserId || (trackingInfo && (comment.targetUserId === trackingInfo.userId || comment.fromUserId === trackingInfo.userId)); }); // Update the sticky notes section // const handleNoteChange = (note: StickyNote, content: string) => { // setNoteContent(content); // if (updateTimeoutRef.current) { // clearTimeout(updateTimeoutRef.current); // } // // Set a new timeout to update the note // updateTimeoutRef.current = setTimeout(() => { // const updatedNote = { // ...note, // content: content.trim() // }; // if (!content.trim()) { // removeStickyNote(note.id || ''); // } else { // addStickyNote(updatedNote); // } // }, 300); // }; // // Clean up timeout on unmount // useEffect(() => { // return () => { // if (updateTimeoutRef.current) { // clearTimeout(updateTimeoutRef.current); // } // }; // }, []); var handleStartPlacing = function (type) { if (!commentText.trim()) return; if (type === "comment") { setIsPlacing(true); document.body.style.cursor = 'crosshair'; } else if (type === "note") { setIsAddingNote(true); document.body.style.cursor = 'crosshair'; } }; // Group comments by parent ID var commentThreads = relevantComments.reduce(function (acc, comment) { if (comment.parentId) { if (!acc[comment.parentId]) { acc[comment.parentId] = []; } acc[comment.parentId].push(comment); } return acc; }, {}); var handleDeleteComment = function (commentId) { var socket = (0, socket_1.getSocket)(); if (!socket || !commentId) return; removeComment(commentId); socket.emit('remove_comment', { commentId: commentId }); }; // Add drag handlers var handleDragStart = function (e, noteId) { e.preventDefault(); var note = stickyNotes.find(function (n) { return n.id === noteId; }); if (!note) return; setIsDragging(noteId); setIsDragStarted(true); setDragOffset({ x: e.clientX - note.position.x, y: e.clientY - note.position.y }); }; var handleDragMove = (0, react_1.useCallback)(function (e) { if (!isDragging) return; var note = stickyNotes.find(function (n) { return n.id === isDragging; }); if (!note) return; var updatedNote = __assign(__assign({}, note), { position: { x: e.clientX - dragOffset.x, y: e.clientY - dragOffset.y } }); var updatedNotes = stickyNotes.map(function (n) { return n.id === isDragging ? __assign(__assign({}, note), { position: { x: e.clientX, y: e.clientY } }) : n; }); setStickyNotes(updatedNotes); }, [isDragging, dragOffset, stickyNotes]); var handleDragEnd = function () { if (!isDragging) return; var note = stickyNotes.find(function (n) { return n.id === isDragging; }); if (note) { addStickyNote(note); // Save position to server } setIsDragging(null); setTimeout(function () { setIsDragStarted(false); }, 100); }; // Add mouse move listener (0, react_1.useEffect)(function () { if (isDragging) { window.addEventListener('mousemove', handleDragMove); window.addEventListener('mouseup', handleDragEnd); } return function () { window.removeEventListener('mousemove', handleDragMove); window.removeEventListener('mouseup', handleDragEnd); }; }, [isDragging, handleDragMove]); var handleEmojiClick = function (emojiData) { setCommentText(function (prevComment) { return prevComment + emojiData.emoji; }); setShowEmojiPicker(false); // Close the emoji picker after selection }; // Update comment display section return (react_1.default.createElement(react_1.default.Fragment, null, (isPlacing || isAddingNote) && (react_1.default.createElement("div", { className: "fixed inset-0 bg-black bg-opacity-10 z-[51]" }, react_1.default.createElement("div", { className: "fixed top-[10%] left-1/2 -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg" }, "Click anywhere to place your ", isPlacing ? 'comment' : 'note'))), react_1.default.createElement("div", { onDragOver: function (e) { e.preventDefault(); if (isDragStarted) e.stopPropagation(); } }, stickyNotes.map(function (note) { return (react_1.default.createElement("div", { key: note.id, className: "group absolute", style: { left: note.position.x, top: note.position.y, transform: 'translate(-50%, -50%)', transition: isDragging === note.id ? 'none' : 'all 0.2s ease-out' } }, react_1.default.createElement("div", { className: "absolute left-1/2 -top-4 -translate-x-1/2 cursor-move z-10 shadow-lg", onMouseDown: function (e) { return handleDragStart(e, note.id || ''); }, draggable: false }, react_1.default.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: note.color === 'bg-yellow-200' ? '#FDE68A' : '#fff' }, react_1.default.createElement("path", { d: "M12 2C7.58 2 4 5.58 4 10c0 7 8 12 8 12s8-5 8-12c0-4.42-3.58-8-8-8zm0 10c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z", stroke: "currentColor", strokeWidth: "1.5", className: "text-gray-600" }))), react_1.default.createElement("div", { className: "\n ".concat(note.color, " p-4 rounded-lg shadow-lg \n min-w-[200px] min-h-[150px]\n opacity -0 group-hover:opacity-100\n transition-opacity duration-200\n ") }, react_1.default.createElement("textarea", { className: "w-full bg-transparent resize-none focus:outline-none min-h-[120px]", value: note === null || note === void 0 ? void 0 : note.content, // value={'content' in note ? note.content : ''} onChange: function (e) { var newContent = e.target.value; if ('id' in note) { var updatedNotes = stickyNotes.map(function (n) { return n.id === note.id ? __assign(__assign({}, n), { content: newContent }) : n; }); setStickyNotes(updatedNotes); } else { setTempNotes(function (prev) { return prev.map(function (n) { return n === note ? __assign(__assign({}, n), { content: newContent }) : n; }); }); } }, onBlur: function (e) { if ('id' in note) { // Update existing note var updatedNote = __assign(__assign({}, note), { content: e.target.value.trim() }); if (!e.target.value.trim()) { removeStickyNote(note.id || ''); } else { addStickyNote(updatedNote); } } else if (e.target.value.trim()) { // Save new note addStickyNote(__assign(__assign({}, note), { content: e.target.value.trim() })); } }, placeholder: "Write your feedback here..." }), react_1.default.createElement("button", { onClick: function () { return removeStickyNote(note.id || ''); }, className: "absolute top-2 right-2 text-gray-600 hover:text-gray-800", title: "Close" }, react_1.default.createElement("svg", { className: "w-4 h-4 text-gray-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", d: "M6 18L18 6M6 6l12 12" })))))); })), react_1.default.createElement("div", { className: "" }, "relevantComments", comments.filter(function (c) { return !c.parentId; }).map(function (comment) { var _a; return (react_1.default.createElement("div", { key: comment.id, className: "absolute ".concat(isDraggingComment === comment.id ? 'cursor-grabbing' : 'cursor-grab'), style: { left: comment.position.x, top: comment.position.y, transform: 'translate(-50%, -50%)', transition: isDraggingComment === comment.id ? 'none' : 'all 0.3s ease' }, onMouseDown: function (e) { return handleCommentMouseDown(e, comment.id || '', comment.position); } }, react_1.default.createElement("div", { className: "bg-white rounded-lg shadow-lg overflow-hidden ".concat(minimizedComments.has(comment.id || '') ? 'w-[200px]' : 'min-w-[300px]') }, react_1.default.createElement("div", { className: "flex items-center justify-between p-2 border-b" }, react_1.default.createElement("div", { className: "flex items-center gap-2" }, react_1.default.createElement("div", { className: "w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600" }, comment.fromUserId === currentUserId ? 'Y' : 'A'), !minimizedComments.has(comment.id || '') && (react_1.default.createElement("div", null, react_1.default.createElement("div", { className: "text-sm font-medium" }, comment.fromUserId === currentUserId ? 'You' : 'Admin'), react_1.default.createElement("div", { className: "text-xs text-gray-500" }, new Date(comment.timestamp).toLocaleTimeString())))), react_1.default.createElement("div", { className: "flex gap-2" }, react_1.default.createElement("button", { onClick: function () { setMinimizedComments(function (prev) { var next = new Set(prev); if (next.has(comment.id || '')) { next.delete(comment.id || ''); } else { next.add(comment.id || ''); } return next; }); }, className: "p-1 hover:bg-gray-100 rounded" }, minimizedComments.has(comment.id || '') ? (react_1.default.createElement("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor" }, react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" }))) : (react_1.default.createElement("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor" }, react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M20 12H4" })))), react_1.default.createElement("button", { onClick: function () { return handleDeleteComment(comment.id || ''); }, className: "p-1 hover:bg-gray-100 rounded" }, react_1.default.createElement("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor" }, react_1.default.createElement("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }))))), minimizedComments.has(comment.id || '') ? (react_1.default.createElement("div", { className: "p-2" }, react_1.default.createElement("p", { className: "text-sm text-gray-700 truncate" }, comment.comment))) : (react_1.default.createElement(react_1.default.Fragment, null, react_1.default.createElement("div", { className: "p-3" }, react_1.default.createElement("p", { className: "text-sm text-gray-700" }, comment.comment)), (_a = commentThreads[comment.id || '']) === null || _a === void 0 ? void 0 : _a.map(function (reply) { return (react_1.default.createElement("div", { key: reply.id, className: "px-3 py-2 bg-gray-50 border-t" }, react_1.default.createElement("div", { className: "flex items-center gap-2 mb-1" }, react_1.default.createElement("div", { className: "w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 text-xs" }, reply.fromUserId === currentUserId ? 'G' : 'A'), react_1.default.createElement("div", { className: "flex-1" }, react_1.default.createElement("div", { className: "text-sm text-gray-900" }, reply.fromUserId === currentUserId ? 'Admin' : 'Guest'), react_1.default.createElement("p", { className: "text-sm text-gray-700" }, reply.comment))))); }), (replyToComment === null || replyToComment === void 0 ? void 0 : replyToComment.id) === comment.id && (react_1.default.createElement("div", { className: "border-t" }, react_1.default.createElement("div", { className: "p-3" }, react_1.default.createElement("input", { type: "text", value: commentText, onChange: function (e) { return setCommentText(e.target.value); }, className: "w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-sm focus:outline-none", placeholder: "Write a reply...", autoFocus: true })), react_1.default.createElement("div", { className: "flex items-center justify-end gap-2 px-3 py-2 bg-gray-50" }, react_1.default.createElement("button", { onClick: function () { return setReplyToComment(null); }, className: "px-4 py-1 text-sm text-gray-600 hover:text-gray-800" }, "Cancel"), react_1.default.createElement("button", { onClick: function () { return handleAddComment(comment); }, className: "px-4 py-1 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600", disabled: !commentText.trim() }, "Send")))), !replyToComment && (react_1.default.createElement("div", { className: "px-3 py-2 border-t" }, react_1.default.createElement("button", { onClick: function () { setReplyToComment(comment); setCommentText(''); }, className: "text-sm text-blue-500 hover:text-blue-600 pointer-events-auto flex items-center gap-1" }, react_1.default.createElement("svg", { className: "w-4 h-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, react_1.default.createElement("path", { d: "M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" })), "Reply")))))))); })), !replyToComment && (react_1.default.createElement("div", { className: "comment-input-box fixed bottom-4 left-1/2 -translate-x-1/2 w-[500px] bg-white rounded-lg shadow-lg z-50" }, react_1.default.createElement("div", { className: "p-3" }, react_1.default.createElement("div", { className: "flex items-center gap-2 relative" }, react_1.default.createElement("textarea", { className: "flex-1 px-3 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900", placeholder: isPlacing ? "Click anywhere to place comment..." : replyToComment ? "Write your reply..." : "Write a comment...", maxLength: 1000, style: { maxHeight: '140px', overflowY: 'auto' }, value: commentText, onChange: function (e) { return setCommentText(e.target.value); }, onKeyDown: function (e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); if (commentText.trim()) { if (replyToComment) { handleAddComment(replyToComment); } else { handleStartPlacing("comment"); } } } if (e.key === 'Escape') { setIsPlacing(false); setIsAddingNote(false); setReplyToComment(null); document.body.style.cursor = 'default'; } } }), showEmojiPicker && (react_1.default.createElement("div", { className: "fixed bottom-[100%] left-1/2 -translate-x-1/2 z-50" }, react_1.default.createElement(emoji_picker_react_1.default, { onEmojiClick: handleEmojiClick }))), react_1.default.createElement("div", { className: "flex items-center gap-2" }, react_1.default.createElement("button", { className: "p-2 text-gray-500 hover:text-gray-700", onClick: function () { return setShowEmojiPicker(!showEmojiPicker); } }, react_1.default.createElement(EmojiIcon, null)), react_1.default.createElement("button", { onClick: function () { return handleStartPlacing("note"); }, className: "px-3 py-1 bg-yellow-400 text-gray-900 rounded-md hover:bg-yellow-500 disabled:opacity-50", disabled: !commentText.trim() || isAddingNote }, react_1.default.createElement(StickyNoteIcon, null)), react_1.default.createElement("button", { onClick: function () { return handleStartPlacing("comment"); }, className: "px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50", disabled: !commentText.trim() }, isPlacing ? 'Click to place' : '→')))))))); }; exports.UserFeedbackOverlay = UserFeedbackOverlay; // Add StickyNoteIcon component function StickyNoteIcon() { return (react_1.default.createElement("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" }, react_1.default.createElement("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }), react_1.default.createElement("polyline", { points: "14 2 14 8 20 8" }))); } // Icons components function EmojiIcon() { return (react_1.default.createElement("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }, react_1.default.createElement("circle", { cx: "12", cy: "12", r: "10" }), react_1.default.createElement("path", { d: "M8 14.5c0 0 1.5 2 4 2s4-2 4-2" }), react_1.default.createElement("circle", { cx: "9", cy: "9", r: "1.5", fill: "currentColor" }), react_1.default.createElement("circle", { cx: "15", cy: "9", r: "1.5", fill: "currentColor" }))); }