diff --git a/app/components/ui/Toolbar/Toolbar.spec.tsx b/app/components/ui/Toolbar/Toolbar.spec.tsx index 7b81e31..18f32c6 100755 --- a/app/components/ui/Toolbar/Toolbar.spec.tsx +++ b/app/components/ui/Toolbar/Toolbar.spec.tsx @@ -15,3 +15,16 @@ describe('Toolbar', () => { expect(screen.getByTestId('toolbar')).toHaveClass(customClass); }); }); + +describe('ToolbarItem', () => { + it('renders children correctly', () => { + render(Hello World); + expect(screen.getByTestId('toolbar-item')).toBeInTheDocument(); + expect(screen.getByTestId('toolbar-item')).toHaveTextContent('Hello World'); + }); + + it('applies custom className', () => { + render(Content); + expect(screen.getByTestId('toolbar-item')).toHaveClass('custom-class'); + }); +}); diff --git a/app/components/ui/Toolbar/Toolbar.stories.tsx b/app/components/ui/Toolbar/Toolbar.stories.tsx index 4e950fe..afba883 100755 --- a/app/components/ui/Toolbar/Toolbar.stories.tsx +++ b/app/components/ui/Toolbar/Toolbar.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Toolbar } from './Toolbar'; +import IconCircle from '~/icons/icon-circle.svg?react'; +import IconTrash from '~/icons/icon-trash.svg?react'; const meta = { title: 'UI/Toolbar', @@ -23,4 +25,22 @@ export const Default: Story = { args: { children: 'Default Toolbar', }, -}; \ No newline at end of file +}; + +export const WithItems: Story = { + args: { + children: ( + <> + + + + + + + + + + + ), + }, +}; diff --git a/app/components/ui/Toolbar/Toolbar.tsx b/app/components/ui/Toolbar/Toolbar.tsx index 7ca4ffa..8988594 100755 --- a/app/components/ui/Toolbar/Toolbar.tsx +++ b/app/components/ui/Toolbar/Toolbar.tsx @@ -14,7 +14,7 @@ export const Toolbar = ({ className, children }: ToolbarProps) => { }); const classNames = clsx( - 'flex flex-col cursor-auto relative w-[120px] min-h-[200px] bg-white border-gray-100 border p-4 rounded-lg drop-shadow-sm transition-opacity duration-200 translate-x-4 translate-y-4 z-50', + 'flex flex-col cursor-auto relative w-[120px] min-h-[200px] bg-white border-gray-100 border p-2 rounded-lg drop-shadow-sm transition-opacity duration-200 translate-x-4 translate-y-4 z-50', className, { 'opacity-50': isDragging, @@ -36,7 +36,7 @@ export const Toolbar = ({ className, children }: ToolbarProps) => { data-testid="toolbar" tabIndex={-1} > - {children} + + ); +}; + +Toolbar.Item = ToolbarItem; diff --git a/app/components/ui/WhiteBoard/WhiteBoard.tsx b/app/components/ui/WhiteBoard/WhiteBoard.tsx index d3a5bdb..3c9e5ba 100755 --- a/app/components/ui/WhiteBoard/WhiteBoard.tsx +++ b/app/components/ui/WhiteBoard/WhiteBoard.tsx @@ -1,14 +1,11 @@ import clsx from 'clsx'; -import { useCanvasDrawing } from '~/hooks/useCanvasDrawing'; type WhiteBoardProps = { className?: string; - parentRef: React.RefObject; + canvasRef: React.RefObject; }; -export const WhiteBoard = ({ className, parentRef }: WhiteBoardProps) => { - const [canvasRef] = useCanvasDrawing(parentRef); - +export const WhiteBoard = ({ className, canvasRef }: WhiteBoardProps) => { return ( ) => { const canvasRef = useRef(null); - const [isDrawing, setIsDrawing] = useState(false); - const [lines, setLines] = useState([]); + const isDrawingRef = useRef(false); + const linesRef = useRef([]); - useEffect(() => { - const canvas = canvasRef.current; + const clearCanvas = () => { + linesRef.current = []; + redrawLines(); + }; + const drawLine = useCallback((line: Line) => { + const canvas = canvasRef.current; if (!canvas) { return; } + const ctx = canvas.getContext('2d'); + + if (!ctx || line.length < 2) { + return; + } + const [start, ...rest] = line; + ctx.beginPath(); + ctx.moveTo(start.x, start.y); + rest.forEach((point) => { + ctx.lineTo(point.x, point.y); + }); + ctx.stroke(); + }, []); + + const redrawLines = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas) { + return; + } const ctx = canvas.getContext('2d'); - if (!ctx) { + if (!canvas || !ctx) { return; } - const redrawLines = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - lines.forEach((line) => { - ctx.beginPath(); - line.forEach((point, index) => { - if (index === 0) { - ctx.moveTo(point.x, point.y); - } else { - ctx.lineTo(point.x, point.y); - ctx.stroke(); - } - }); - ctx.closePath(); - }); - }; + ctx.clearRect(0, 0, canvas.width, canvas.height); + linesRef.current.forEach(drawLine); + }, [drawLine]); - const resizeCanvas = () => { - canvas.width = parentRef?.current?.clientWidth ?? window.innerWidth; - canvas.height = parentRef?.current?.clientHeight ?? window.innerHeight; - redrawLines(); - }; + // Gestion du redimensionnement avec debounce pour optimiser les performances + const resizeCanvas = useCallback(() => { + const canvas = canvasRef.current; + if (!canvas || !parentRef?.current) return; - window.addEventListener('resize', resizeCanvas); - resizeCanvas(); + canvas.width = parentRef.current.clientWidth; + canvas.height = parentRef.current.clientHeight; + redrawLines(); + }, [redrawLines, parentRef]); - const startDrawing = (e: MouseEvent) => { - setIsDrawing(true); + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + // Ajout des écouteurs d'événements + const startDrawing = (e: MouseEvent) => { + isDrawingRef.current = true; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; - setLines((prevLines) => [...prevLines, [{ x, y }]]); + linesRef.current.push([{ x, y }]); }; const draw = (e: MouseEvent) => { - if (!isDrawing) return; - const currentLine = lines[lines.length - 1]; + if (!isDrawingRef.current) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; - const newLine = [...currentLine, { x, y }]; - - setLines((prevLines) => [...prevLines.slice(0, -1), newLine]); - redrawLines(); + const currentLine = linesRef.current[linesRef.current.length - 1]; + currentLine.push({ x, y }); + drawLine(currentLine); }; const stopDrawing = () => { - setIsDrawing(false); + isDrawingRef.current = false; + redrawLines(); // Redessine pour s'assurer que tout est cohérent }; canvas.addEventListener('mousedown', startDrawing); @@ -76,14 +91,18 @@ export const useCanvasDrawing = ( canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseleave', stopDrawing); + window.addEventListener('resize', resizeCanvas); + resizeCanvas(); // Applique initialement la taille du canvas + + // Nettoyage des écouteurs d'événements return () => { - window.removeEventListener('resize', resizeCanvas); canvas.removeEventListener('mousedown', startDrawing); canvas.removeEventListener('mousemove', draw); canvas.removeEventListener('mouseup', stopDrawing); canvas.removeEventListener('mouseleave', stopDrawing); + window.removeEventListener('resize', resizeCanvas); }; - }, [parentRef, lines, isDrawing]); + }, [resizeCanvas, drawLine, redrawLines]); - return [canvasRef, lines] as const; + return { canvasRef, clearCanvas }; }; diff --git a/app/icons/icon-circle.svg b/app/icons/icon-circle.svg new file mode 100644 index 0000000..bb404f7 --- /dev/null +++ b/app/icons/icon-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/icon-trash.svg b/app/icons/icon-trash.svg new file mode 100644 index 0000000..2e2cebf --- /dev/null +++ b/app/icons/icon-trash.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index ad06902..b8ae2d8 100755 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,14 +1,18 @@ -import { Toolbar } from '~/components/ui/Toolbar'; -import { WhiteBoard } from '~/components/ui/WhiteBoard'; +import { useRef } from 'react'; import { ClientRect, DndContext, Modifier } from '@dnd-kit/core'; +import { restrictToParentElement } from '@dnd-kit/modifiers'; import MainLayout from '~/layouts/_main'; -import { useRef } from 'react'; -import { restrictToParentElement } from '@dnd-kit/modifiers'; +import { Toolbar } from '~/components/ui/Toolbar'; +import { WhiteBoard } from '~/components/ui/WhiteBoard'; import { snapBottomToCursor } from '~/utils/dndkit'; +import IconTrash from '~/icons/icon-trash.svg?react'; +import { useCanvasDrawing } from '~/hooks/useCanvasDrawing'; + export default function Index() { const parentRef = useRef(null); + const { canvasRef, clearCanvas } = useCanvasDrawing(parentRef); const modifiers = [ snapBottomToCursor, (e: Parameters[0]) => @@ -23,10 +27,12 @@ export default function Index() {
- WIP + clearCanvas()}> + + - +