sticky-horse
Version:
With StickyHorse allow your users to send feedback to your team.
466 lines (465 loc) • 28.9 kB
JavaScript
"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" })));
}