diff --git a/app/(main)/board/[id]/page.tsx b/app/(main)/board/[id]/page.tsx index cf1de9d..92f62fc 100644 --- a/app/(main)/board/[id]/page.tsx +++ b/app/(main)/board/[id]/page.tsx @@ -1,5 +1,10 @@ -const BoardPage = () => { - // TODO: Create the board here - return
BoardPage
; -}; -export default BoardPage; +import Whiteboard from "@/components/whiteboard" + +export default function Home() { + return ( +
+ +
+ ) +} + diff --git a/app/globals.css b/app/globals.css index d9bb630..4bfdbef 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,5 @@ @import "tailwindcss"; @import "tw-animate-css"; -@plugin "tailwindcss-animate"; @custom-variant dark (&:is(.dark *)); @@ -46,7 +45,7 @@ :root { --radius: 0.625rem; - --background: oklch(1 0 0); + --background: oklch(0.96 0.005 0); /* Light gray */ --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); @@ -61,9 +60,9 @@ --accent: oklch(0.97 0 0); --accent-foreground: oklch(0.205 0 0); --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); + --border: oklch(0.94 0 0); /* Lighter border */ + --input: oklch(0.94 0 0); /* Lighter input */ + --ring: oklch(0.85 0 0); /* Lighter ring */ --chart-1: oklch(0.646 0.222 41.116); --chart-2: oklch(0.6 0.118 184.704); --chart-3: oklch(0.398 0.07 227.392); diff --git a/components/canvas.tsx b/components/canvas.tsx new file mode 100644 index 0000000..e7e71b1 --- /dev/null +++ b/components/canvas.tsx @@ -0,0 +1,443 @@ +"use client" + +import type React from "react" +import { useRef, useState, useCallback } from "react" // Import useCallback +import type { ToolType } from "@/lib/types" +import StickyNoteElement from "./elements/sticky-note" +import ImageElement from "./elements/image-element" +import TextElement from "./elements/text-element" +import DrawingCanvas from "./elements/drawing-canvas" +import StampElement from "./elements/stamp-element" +import ShapeElement from "./elements/shape-element" +import ShapeHandler from "./shape-handler" +import { Trash2 } from "lucide-react" + +interface CanvasProps { + scale: number + position: { x: number; y: number } + activeTool: ToolType + isDragging: boolean // This prop might indicate panning/zooming, separate from element dragging + currentColor: string +} + +export interface CanvasElement { + id: string + type: "sticky" | "image" | "text" | "drawing" | "stamp" | "shape" + position: { x: number; y: number } + content: any + zIndex: number +} + +type Point = { x: number; y: number }; + +export default function Canvas({ scale, position, activeTool, isDragging, currentColor }: CanvasProps) { + const [elements, setElements] = useState([ + { + id: "sticky-1", + type: "sticky", + position: { x: 700, y: 200 }, + content: { + text: "I want to go somewhere with nice nature.... A lot of extreme activities", + color: "#FFC8F0", + author: "Sarah Kim", + }, + zIndex: 1, + }, + { + id: "image-1", + type: "image", + position: { x: 600, y: 400 }, + content: { + src: "/placeholder.svg?height=200&width=200", + alt: "Paragliding", + caption: "Paragliding", + }, + zIndex: 2, + }, + ]) + + const canvasRef = useRef(null) + const fileInputRef = useRef(null) + const [nextZIndex, setNextZIndex] = useState(3) + const [selectedElement, setSelectedElement] = useState(null) + const [currentShape, setCurrentShape] = useState<"square" | "circle" | "triangle" | "diamond">("square") + const [isDrawing, setIsDrawing] = useState(false) // For pencil tool + const [startShapePosition, setStartShapePosition] = useState(null) // For shape tool + const [currentShapeSize, setCurrentShapeSize] = useState(50) + const [isShapeDragging, setIsShapeDragging] = useState(false) // For shape tool preview + const [currentShapeType, setCurrentShapeType] = useState<"square" | "circle" | "triangle" | "diamond">("square") + const [shapePreview, setShapePreview] = useState(null) + const [cursorPosition, setCursorPosition] = useState(null) + const [isCursorPreviewVisible, setIsCursorPreviewVisible] = useState(false) + const [shapeStyle, setShapeStyle] = useState({ + width: 50, + height: 50, + border: `2px dashed ${currentColor}`, + position: "absolute", + left: -1000, + top: -1000, + pointerEvents: "none", + }); + + const [currentPath, setCurrentPath] = useState([]) + const [currentStrokeWidth, setCurrentStrokeWidth] = useState(2) + + // Helper to get coordinates from Mouse or Touch events relative to the canvas + const getCoordinatesFromEvent = useCallback((e: React.MouseEvent | React.TouchEvent): Point | null => { + if (!canvasRef.current) return null; + const rect = canvasRef.current.getBoundingClientRect(); + let clientX: number, clientY: number; + + if ('touches' in e) { // Touch event + if (e.touches.length === 0) return null; // No touch points + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { // Mouse event + clientX = e.clientX; + clientY = e.clientY; + } + + const x = (clientX - rect.left - position.x) / scale; + const y = (clientY - rect.top - position.y) / scale; + return { x, y }; + }, [scale, position.x, position.y]); + + + // --- Drawing Logic --- + const startDrawing = useCallback((point: Point) => { + setIsDrawing(true); + setCurrentPath([point]); // Start new path + }, []); + + const draw = useCallback((point: Point) => { + if (!isDrawing) return; + setCurrentPath((prevPath) => [...prevPath, point]); + }, [isDrawing]); + + const endDrawing = useCallback(() => { + if (!isDrawing) return; // Prevent ending if not drawing + + if (currentPath.length > 1) { + const newDrawing: CanvasElement = { + id: `drawing-${Date.now()}`, + type: "drawing", + position: { x: 0, y: 0 }, // Position handled by DrawingCanvas based on path + content: { + path: currentPath, + color: currentColor, + strokeWidth: currentStrokeWidth, + }, + zIndex: nextZIndex, + }; + setElements((prev) => [...prev, newDrawing]); + setNextZIndex((prev) => prev + 1); + } + setIsDrawing(false); + setCurrentPath([]); + }, [isDrawing, currentPath, currentColor, currentStrokeWidth, nextZIndex]); + + // --- Shape Logic --- + const startShape = useCallback((point: Point) => { + setStartShapePosition(point); + setIsShapeDragging(true); + // Initial style set here, updated in move + setShapeStyle({ + width: 0, // Start with zero size + height: 0, + border: `2px dashed ${currentColor}`, + position: "absolute", + left: point.x, + top: point.y, + pointerEvents: "none", + }); + }, [currentColor]); + + const updateShapePreview = useCallback((point: Point) => { + if (!isShapeDragging || !startShapePosition) return; + const size = Math.max(Math.abs(point.x - startShapePosition.x), Math.abs(point.y - startShapePosition.y)); + setCurrentShapeSize(size); // Store the size if needed for the final element + setShapeStyle({ + width: size, + height: size, + border: `2px dashed ${currentColor}`, + position: "absolute", + // Adjust left/top based on drag direction + left: Math.min(point.x, startShapePosition.x), + top: Math.min(point.y, startShapePosition.y), + pointerEvents: "none", + }); + }, [isShapeDragging, startShapePosition, currentColor]); + + const endShape = useCallback(() => { + if (!isShapeDragging || !startShapePosition) return; + + // Use the calculated size + if (currentShapeSize > 5) { // Add a threshold to avoid tiny shapes on click + const topLeft = { + x: Math.min(cursorPosition?.x ?? startShapePosition.x, startShapePosition.x), + y: Math.min(cursorPosition?.y ?? startShapePosition.y, startShapePosition.y) + }; + const newShape: CanvasElement = { + id: `shape-${Date.now()}`, + type: "shape", + position: topLeft, // Use calculated top-left corner + content: { + shape: currentShapeType, // Make sure this is updated based on toolbar selection + color: currentColor, + size: currentShapeSize, + }, + zIndex: nextZIndex, + }; + setElements((prev) => [...prev, newShape]); + setNextZIndex((prev) => prev + 1); + setSelectedElement(newShape.id); // Optionally select the new shape + } + + // Reset shape dragging state + setStartShapePosition(null); + setIsShapeDragging(false); + setCurrentShapeSize(50); // Reset default size + setShapeStyle({ // Hide preview + width: 50, height: 50, border: `2px dashed ${currentColor}`, + position: "absolute", left: -1000, top: -1000, pointerEvents: "none", + }); + }, [isShapeDragging, startShapePosition, cursorPosition, currentShapeType, currentColor, currentShapeSize, nextZIndex]); + + + // --- Event Handlers --- + const handlePointerDown = useCallback((e: React.MouseEvent | React.TouchEvent) => { + // Prevent default touch behavior like scrolling + if ('touches' in e) { + // Allow default for multi-touch (pinch/zoom) gestures if implemented elsewhere + if (e.touches.length === 1) { + e.preventDefault(); + } + } + const point = getCoordinatesFromEvent(e); + // Original check: Ignore if no point or if canvas is being dragged (panning/zooming) + if (!point || isDragging) return; + + if (activeTool === "pencil") { + startDrawing(point); + } else if (activeTool.startsWith("shape-")) { + startShape(point); + } + // Add other tool start logic here (e.g., grab) + }, [activeTool, getCoordinatesFromEvent, startDrawing, startShape, isDragging]); + + const handlePointerMove = useCallback((e: React.MouseEvent | React.TouchEvent) => { + const point = getCoordinatesFromEvent(e); + if (!point) return; + setCursorPosition(point); // Update general cursor position + + if (activeTool === "pencil") { + draw(point); + } else if (activeTool.startsWith("shape-")) { + updateShapePreview(point); + } + // Add other tool move logic here (e.g., grab) + }, [activeTool, getCoordinatesFromEvent, draw, updateShapePreview]); + + const handlePointerUp = useCallback(() => { + if (activeTool === "pencil") { + endDrawing(); + } else if (activeTool.startsWith("shape-")) { + endShape(); + } + // Add other tool end logic here (e.g., grab) + }, [activeTool, endDrawing, endShape]); + + // --- Element Specific Handlers --- + const handleCanvasClick = (e: React.MouseEvent) => { + // Keep for tools that activate on simple click (sticky, text, stamp) + // Prevent activating these if a drag/draw just finished + if (!canvasRef.current || isDragging || isDrawing || isShapeDragging || currentPath.length > 0 || startShapePosition) return; + + const point = getCoordinatesFromEvent(e); + if (!point) return; + + if (activeTool === "sticky") addStickyNote(point.x, point.y); + else if (activeTool === "camera") fileInputRef.current?.click(); + else if (activeTool === "text") addTextElement(point.x, point.y); + else if (activeTool === "stamp") addStampElement(point.x, point.y); + else if (activeTool === "select") setSelectedElement(null); // Deselect on canvas click + }; + + const handleElementPointerDown = (e: React.PointerEvent, id: string) => { + // Use PointerEvent for consistency + e.stopPropagation(); // Prevent canvas pointer down handler + + if (activeTool === "eraser") { + deleteElement(id); + } else if (activeTool === "select") { + setSelectedElement(id); + bringToFront(id); + // Add logic here to initiate element dragging if needed + } + // Add grab tool logic here if elements should be grabbable + }; + + // --- Add Element Functions --- + const addStickyNote = (x: number, y: number) => { + const newSticky: CanvasElement = { id: `sticky-${Date.now()}`, type: "sticky", position: { x, y }, content: { text: "New note...", color: currentColor, author: "You" }, zIndex: nextZIndex }; + setElements((prev) => [...prev, newSticky]); + setNextZIndex((prev) => prev + 1); + setSelectedElement(newSticky.id); + }; + + const addTextElement = (x: number, y: number) => { + const newText: CanvasElement = { id: `text-${Date.now()}`, type: "text", position: { x, y }, content: { text: "Click to edit text", fontSize: 16, color: "#000000" }, zIndex: nextZIndex }; + setElements((prev) => [...prev, newText]); + setNextZIndex((prev) => prev + 1); + setSelectedElement(newText.id); + }; + + const addStampElement = (x: number, y: number) => { + const stamps = ["❤️", "👍", "⭐", "✅", "🔥"]; + const randomStamp = stamps[Math.floor(Math.random() * stamps.length)]; + const newStamp: CanvasElement = { id: `stamp-${Date.now()}`, type: "stamp", position: { x, y }, content: { emoji: randomStamp, size: 32 }, zIndex: nextZIndex }; + setElements((prev) => [...prev, newStamp]); + setNextZIndex((prev) => prev + 1); + setSelectedElement(newStamp.id); + }; + + // Handle file input change + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !canvasRef.current) return; + const reader = new FileReader(); + reader.onload = (event) => { + if (event.target?.result) { + const rect = canvasRef.current!.getBoundingClientRect(); + const centerX = (window.innerWidth / 2 - rect.left - position.x) / scale; + const centerY = (window.innerHeight / 2 - position.y) / scale; + const newImage: CanvasElement = { id: `image-${Date.now()}`, type: "image", position: { x: centerX - 100, y: centerY - 100 }, content: { src: event.target.result as string, alt: file.name, file: file }, zIndex: nextZIndex }; + setElements((prev) => [...prev, newImage]); + setNextZIndex((prev) => prev + 1); + setSelectedElement(newImage.id); + } + }; + reader.readAsDataURL(file); + e.target.value = ""; // Reset file input + }; + + // --- Element Management --- + const bringToFront = (id: string) => { + setElements(elements.map((el) => (el.id === id ? { ...el, zIndex: nextZIndex } : el))); + setNextZIndex((prev) => prev + 1); + setSelectedElement(id); + }; + + const updateElementContent = (id: string, newContent: any) => { + setElements(elements.map((el) => (el.id === id ? { ...el, content: { ...el.content, ...newContent } } : el))); + }; + + const deleteElement = (id: string) => { + setElements(elements.filter((el) => el.id !== id)); + setSelectedElement(null); + }; + + const updateElementPosition = (id: string, newPosition: Point) => { + setElements(elements.map((el) => (el.id === id ? { ...el, position: newPosition } : el))); + }; + + + return ( +
+ {/* Shape Preview (Visible during shape drag) */} + {isShapeDragging && startShapePosition && ( +
+ )} + + {/* Hidden file input for image upload */} + + + {/* Current drawing path (Live preview) */} + {isDrawing && currentPath.length > 1 && ( + + `${p.x},${p.y}`).join(" L ")}`} + stroke={currentColor} + strokeWidth={currentStrokeWidth} + fill="none" + strokeLinecap="round" + strokeLinejoin="round" + /> + + )} + + {/* Render all finalized elements */} + {elements.map((element) => { + const isSelected = selectedElement === element.id + // Base style, position is handled differently for drawing + const baseStyle = { + position: "absolute" as const, + zIndex: element.zIndex, + } + // Specific style for non-drawing elements, applying position + const elementStyle = element.type !== 'drawing' ? { + ...baseStyle, + left: `${element.position.x}px`, + top: `${element.position.y}px`, + } : baseStyle; // DrawingCanvas calculates its own position + + // Delete button for selected element + const deleteButton = + isSelected && activeTool === "select" ? ( + + ) : null + + // Wrapper div for positioning and event handling + const wrapperStyle = element.type === 'drawing' ? baseStyle : elementStyle; + + return ( +
handleElementPointerDown(e, element.id)}> + {deleteButton} + {(() => { + switch (element.type) { + case "sticky": + return bringToFront(element.id)} onPositionChange={(newPos) => updateElementPosition(element.id, newPos)} onContentChange={(newContent) => updateElementContent(element.id, newContent)} canDrag={activeTool === "select"} />; + case "image": + return bringToFront(element.id)} onPositionChange={(newPos) => updateElementPosition(element.id, newPos)} canDrag={activeTool === "select"} />; + case "text": + return bringToFront(element.id)} onPositionChange={(newPos) => updateElementPosition(element.id, newPos)} onContentChange={(newContent) => updateElementContent(element.id, newContent)} canDrag={activeTool === "select"} />; + case "drawing": + // Pass baseStyle as DrawingCanvas handles its own positioning via SVG coords + return ; + case "stamp": + return bringToFront(element.id)} onPositionChange={(newPos) => updateElementPosition(element.id, newPos)} canDrag={activeTool === "select"} />; + case "shape": + return ; + default: + return null; + } + })()} +
+ ) + })} +
+ ) +} diff --git a/components/chat-panel.tsx b/components/chat-panel.tsx new file mode 100644 index 0000000..b1f049b --- /dev/null +++ b/components/chat-panel.tsx @@ -0,0 +1,73 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Send } from "lucide-react" + +export default function ChatPanel() { + const [message, setMessage] = useState("") + const [messages, setMessages] = useState<{ text: string; sender: string }[]>([]) + + const handleSendMessage = () => { + if (message.trim()) { + setMessages([...messages, { text: message, sender: "user" }]) + setMessage("") + } + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault() + handleSendMessage() + } + } + + return ( +
+
+

Chat

+
+ +
+ {messages.length === 0 ? ( +
+
+ No messages yet. Start a conversation about your travel plans. +
+
+ ) : ( +
+ {messages.map((msg, index) => ( +
+
+ {msg.text} +
+
+ ))} +
+ )} +
+ +
+
+ setMessage(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1" + /> + +
+
+
+ ) +} + diff --git a/components/color-picker.tsx b/components/color-picker.tsx new file mode 100644 index 0000000..91465a6 --- /dev/null +++ b/components/color-picker.tsx @@ -0,0 +1,34 @@ +"use client" + +interface ColorPickerProps { + color: string + onChange: (color: string) => void +} + +export function ColorPicker({ color, onChange }: ColorPickerProps) { + const colors = [ + "#FF9B9B", // Red + "#FFD699", // Orange + "#FDFF8F", // Yellow + "#91F48F", // Green + "#9EFFFF", // Cyan + "#B69CFF", // Purple + "#FFC8F0", // Pink + "#FFFFFF", // White + ] + + return ( +
+ {colors.map((c) => ( +
+ ) +} + diff --git a/components/elements/drawing-canvas.tsx b/components/elements/drawing-canvas.tsx new file mode 100644 index 0000000..bae8637 --- /dev/null +++ b/components/elements/drawing-canvas.tsx @@ -0,0 +1,63 @@ +"use client" + +import React, { useEffect } from "react" // Import useEffect + +interface DrawingCanvasProps { + id: string + style: React.CSSProperties + content: { + path: { x: number; y: number }[] + color: string + strokeWidth: number + } + isSelected: boolean +} + +export default function DrawingCanvas({ id, style, content, isSelected }: DrawingCanvasProps) { + useEffect(() => { + console.log(`DrawingCanvas ${id} rendered with path length: ${content.path.length}`); + }, [id, content.path]); // Log when component renders or path changes significantly + + const { path, color, strokeWidth } = content + + if (path.length < 2) return null + + // Calculate the bounding box of the path + const minX = Math.min(...path.map((p) => p.x)) + const minY = Math.min(...path.map((p) => p.y)) + const maxX = Math.max(...path.map((p) => p.x)) + const maxY = Math.max(...path.map((p) => p.y)) + + const width = maxX - minX + strokeWidth * 2 + const height = maxY - minY + strokeWidth * 2 + + // Adjust the path to be relative to the SVG + const adjustedPath = path.map((p) => ({ + x: p.x - minX + strokeWidth, + y: p.y - minY + strokeWidth, + })) + + return ( +
+ + `${p.x},${p.y}`).join(" L ")}`} + stroke={color} + strokeWidth={strokeWidth} + fill="none" + strokeLinecap="round" + strokeLinejoin="round" + /> + +
+ ) +} diff --git a/components/elements/image-element.tsx b/components/elements/image-element.tsx new file mode 100644 index 0000000..8bcdecb --- /dev/null +++ b/components/elements/image-element.tsx @@ -0,0 +1,88 @@ +"use client" + +import type React from "react" + +import { useState } from "react" +import Image from "next/image" +import { Card } from "@/components/ui/card" + +interface ImageElementProps { + id: string + style: React.CSSProperties + content: { + src: string + alt: string + caption?: string + } + isSelected: boolean + onBringToFront: () => void + onPositionChange: (position: { x: number; y: number }) => void + canDrag: boolean +} + +export default function ImageElement({ + id, + style, + content, + isSelected, + onBringToFront, + onPositionChange, + canDrag, +}: ImageElementProps) { + const [isDragging, setIsDragging] = useState(false) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation() + onBringToFront() + + if (!canDrag) return + + setIsDragging(true) + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }) + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging) return + e.stopPropagation() + + const x = e.clientX - dragOffset.x + const y = e.clientY - dragOffset.y + + onPositionChange({ x, y }) + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + return ( + +
+ {content.alt} + {content.caption &&
{content.caption}
} +
+
+ ) +} + diff --git a/components/elements/shape-element.tsx b/components/elements/shape-element.tsx new file mode 100644 index 0000000..c864fde --- /dev/null +++ b/components/elements/shape-element.tsx @@ -0,0 +1,79 @@ +"use client" + +import type React from "react" + +interface ShapeElementProps { + id: string + style: React.CSSProperties + content: { + shape: "square" | "circle" | "triangle" | "diamond" + color: string + size: number + } + isSelected: boolean +} + +export default function ShapeElement({ id, style, content, isSelected }: ShapeElementProps) { + const { shape, color, size } = content + + switch (shape) { + case "square": + return ( +
+ ) + case "circle": + return ( +
+ ) + case "triangle": + return ( +
+ ) + case "diamond": + return ( +
+ ) + default: + return null + } +} diff --git a/components/elements/stamp-element.tsx b/components/elements/stamp-element.tsx new file mode 100644 index 0000000..fb0690b --- /dev/null +++ b/components/elements/stamp-element.tsx @@ -0,0 +1,79 @@ +"use client" + +import type React from "react" + +import { useState } from "react" + +interface StampElementProps { + id: string + style: React.CSSProperties + content: { + emoji: string + size: number + } + isSelected: boolean + onBringToFront: () => void + onPositionChange: (position: { x: number; y: number }) => void + canDrag: boolean +} + +export default function StampElement({ + id, + style, + content, + isSelected, + onBringToFront, + onPositionChange, + canDrag, +}: StampElementProps) { + const [isDragging, setIsDragging] = useState(false) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation() + onBringToFront() + + if (!canDrag) return + + setIsDragging(true) + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }) + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging) return + e.stopPropagation() + + const x = e.clientX - dragOffset.x + const y = e.clientY - dragOffset.y + + onPositionChange({ x, y }) + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + return ( +
+ {content.emoji} +
+ ) +} + diff --git a/components/elements/sticky-note.tsx b/components/elements/sticky-note.tsx new file mode 100644 index 0000000..a345b78 --- /dev/null +++ b/components/elements/sticky-note.tsx @@ -0,0 +1,115 @@ +"use client" + +import type React from "react" + +import { useState, useRef } from "react" +import { Card } from "@/components/ui/card" + +interface StickyNoteProps { + id: string + style: React.CSSProperties + content: { + text: string + color: string + author: string + } + isSelected: boolean + onBringToFront: () => void + onPositionChange: (position: { x: number; y: number }) => void + onContentChange: (content: { text?: string; color?: string }) => void + canDrag: boolean +} + +export default function StickyNoteElement({ + id, + style, + content, + isSelected, + onBringToFront, + onPositionChange, + onContentChange, + canDrag, +}: StickyNoteProps) { + const [isDragging, setIsDragging] = useState(false) + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) + const [isEditing, setIsEditing] = useState(false) + const textareaRef = useRef(null) + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation() + onBringToFront() + + if (isEditing || !canDrag) return + + setIsDragging(true) + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect() + setDragOffset({ + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }) + } + + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging) return + e.stopPropagation() + + const x = e.clientX - dragOffset.x + const y = e.clientY - dragOffset.y + + onPositionChange({ x, y }) + } + + const handleMouseUp = () => { + setIsDragging(false) + } + + const handleDoubleClick = (e: React.MouseEvent) => { + e.stopPropagation() + setIsEditing(true) + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.focus() + textareaRef.current.select() + } + }, 0) + } + + const handleBlur = () => { + setIsEditing(false) + } + + const handleTextChange = (e: React.ChangeEvent) => { + onContentChange({ text: e.target.value }) + } + + return ( + + {isEditing ? ( +