FlowWeb / src /features /canvas /components /nodes /CommentNode.jsx
danylokhodus's picture
fix: implement useUpdateNodeInternals across all node types to resolve connection offset issues on size change
4555b70
Raw
History Blame Contribute Delete
6.98 kB
import React, { useRef } from 'react';
import { NodeResizer, Handle, Position, useUpdateNodeInternals } from '@xyflow/react';
import { Settings, StickyNote, Pin } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useTheme } from '../../../../context/ThemeContext';
const getInitials = (displayName, email) => {
if (displayName && displayName.trim()) {
const parts = displayName.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return parts[0].substring(0, 2).toUpperCase();
}
if (email && email.trim()) {
return email.substring(0, 2).toUpperCase();
}
return 'U';
};
const PASTEL_COLORS = {
yellow: { light: '#fef9c3', dark: '#2e2b14', borderLight: '#eab308', borderDark: '#ca8a04' },
blue: { light: '#dbeafe', dark: '#111827', borderLight: '#3b82f6', borderDark: '#2563eb' },
green: { light: '#dcfce7', dark: '#062f17', borderLight: '#22c55e', borderDark: '#16a34a' },
pink: { light: '#fce7f3', dark: '#37061d', borderLight: '#ec4899', borderDark: '#db2777' },
purple: { light: '#f3e8ff', dark: '#210638', borderLight: '#a855f7', borderDark: '#9333ea' },
orange: { light: '#ffedd5', dark: '#2e1208', borderLight: '#f97316', borderDark: '#ea580c' },
white: { light: '#f5f5f4', dark: '#1c1917', borderLight: '#d6d3d1', borderDark: '#44403c' }
};
const CommentNode = ({ id, data, selected }) => {
const { t } = useTranslation();
const { isDarkMode } = useTheme();
const updateNodeInternals = useUpdateNodeInternals();
// Re-calculate handles when node size might change
React.useEffect(() => {
updateNodeInternals(id);
}, [id, data.label, data.description, updateNodeInternals]);
// Create a slight premium dynamic rotation based on node ID to make it look like a physical sticky note
const rotateAngle = useRef(((parseInt(id.slice(0, 2), 16) % 3) - 1.5).toFixed(1));
const openConfigureModal = (e) => {
e.stopPropagation();
window.dispatchEvent(new CustomEvent('configure-sketch-node', { detail: { nodeId: id } }));
};
const togglePin = (e) => {
e.stopPropagation();
e.preventDefault();
if (data.updateNodeData) {
data.updateNodeData(id, { isPinned: !data.isPinned });
}
};
// Background and border style resolution based on selected color or default to yellow
const colorKey = data.color && data.color !== 'default' ? data.color : 'yellow';
const selectedColor = PASTEL_COLORS[colorKey];
const nodeBg = isDarkMode ? selectedColor.dark : selectedColor.light;
const nodeBorder = isDarkMode ? selectedColor.borderDark : selectedColor.borderLight;
return (
<div
onDoubleClick={openConfigureModal}
className={`px-5 py-4 w-full h-full min-w-[150px] min-h-[150px] rough-border transition-all group relative cursor-pointer select-none ${
selected ? 'shadow-[8px_8px_0px_0px_rgba(245,158,11,0.25)] -translate-y-1 border-amber-400 ring-4 ring-amber-400/20 scale-[1.01]' : 'shadow-[4px_4px_0px_0px_rgba(0,0,0,0.08)]'
}`}
style={{
transform: `rotate(${rotateAngle.current}deg)`,
backgroundColor: nodeBg,
borderColor: nodeBorder,
borderRadius: '2px 14px 4px 16px' // Hand-drawn sticker fold style
}}
>
{/*
Single Integrated Pin Handle
Styled as a sleek dark drawing pin to fit the sketchbook aesthetic.
*/}
<Handle
type="target"
position={Position.Top}
id="sticker-target"
className="!w-4 !h-4 !bg-primary !border-2 !border-primary !shadow-[2px_2px_0px_0px_rgba(0,0,0,0.15)] !z-50 hover:!scale-110 transition-transform !opacity-100 !rounded-full !absolute"
style={{
top: '-8px',
left: '50%',
transform: 'translateX(-50%)',
}}
>
{/* Metallic/Plastic highlight to give it volume */}
<div className="pointer-events-none absolute top-0.5 left-0.5 w-1 h-1 bg-white/20 rounded-full" />
</Handle>
{/* Invisible source handle sharing the same space */}
<Handle
type="source"
position={Position.Top}
id="sticker-source"
className="!w-4 !h-4 !opacity-0 !z-[51] !pointer-events-none"
style={{
top: '-8px',
left: '50%',
transform: 'translateX(-50%)'
}}
/>
{/* Resizer */}
<NodeResizer
minWidth={150}
minHeight={150}
isVisible={selected && !data.isPinned}
lineClassName="border-amber-400"
handleClassName="w-3 h-3 bg-white border-2 border-amber-400 rounded"
/>
{/* Folded Corner Accent */}
<div
className="absolute top-0 right-0 w-5 h-5 rounded-bl-lg border-l-2 border-b-2 opacity-30 pointer-events-none"
style={{ borderColor: isDarkMode ? '#ffffff' : '#000000' }}
/>
<div className="flex flex-col h-full gap-3">
<div>
{/* Header */}
<div className="flex justify-between items-start gap-2 border-b border-primary/10 pb-1 mb-2">
<div className="flex items-center gap-1.5 min-w-0">
<StickyNote size={14} className="text-primary/60 shrink-0" />
<span className="font-extrabold text-[10px] uppercase tracking-widest text-primary/60 truncate">
{data.label || t('canvas.comment_title', 'Стікер')}
</span>
</div>
<div className="flex items-center gap-1 shrink-0">
<button
onClick={togglePin}
className={`transition-all p-0.5 rounded-md cursor-pointer ${
data.isPinned
? 'text-amber-500 bg-amber-500/10 hover:bg-amber-500/20'
: 'opacity-0 group-hover:opacity-100 text-primary/60 hover:text-primary hover:bg-primary/5'
}`}
title={data.isPinned ? t('canvas.unpin', 'Відкріпити') : t('canvas.pin', 'Закріпити')}
>
<Pin size={13} className={data.isPinned ? 'rotate-45 fill-amber-500' : ''} />
</button>
<button
onClick={openConfigureModal}
className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 text-primary/60 hover:text-primary hover:bg-primary/5 rounded-md cursor-pointer"
title={t('canvas.configure', 'Configure')}
>
<Settings size={13} />
</button>
</div>
</div>
{/* Sticker content in a gorgeous handwriting font */}
<p className="text-base font-accent-note text-primary leading-tight break-words whitespace-pre-wrap italic">
{data.description || t('canvas.comment_placeholder', 'Двоклацніть, щоб додати коментар...')}
</p>
</div>
</div>
</div>
);
};
export default CommentNode;