From 44d837496f35c0e052074a7deafec623bb2c0833 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 28 Jun 2025 17:27:51 +0200 Subject: [PATCH] Improve a lot the tablo page --- ui/src/components/ClickOutside.md | 101 +++ ui/src/components/ClickOutside.tsx | 33 + ui/src/components/CreateTabloModal.tsx | 121 ++++ ui/src/components/ImageColorPicker.tsx | 150 ++++ ui/src/components/StatusPicker.tsx | 52 ++ ui/src/components/TabloModal.tsx | 128 ++++ ui/src/hooks/useClickOutside.ts | 47 ++ ui/src/pages/tablo.tsx | 945 ++++++++----------------- 8 files changed, 921 insertions(+), 656 deletions(-) create mode 100644 ui/src/components/ClickOutside.md create mode 100644 ui/src/components/ClickOutside.tsx create mode 100644 ui/src/components/CreateTabloModal.tsx create mode 100644 ui/src/components/ImageColorPicker.tsx create mode 100644 ui/src/components/StatusPicker.tsx create mode 100644 ui/src/components/TabloModal.tsx create mode 100644 ui/src/hooks/useClickOutside.ts diff --git a/ui/src/components/ClickOutside.md b/ui/src/components/ClickOutside.md new file mode 100644 index 0000000..60dd036 --- /dev/null +++ b/ui/src/components/ClickOutside.md @@ -0,0 +1,101 @@ +# Click Outside Components + +This module provides two ways to detect clicks outside of elements: a hook and a wrapper component. + +## `useClickOutside` Hook + +A React hook that detects clicks outside of a referenced element. + +### Usage + +```tsx +import { useClickOutside } from "../hooks/useClickOutside"; + +function MyComponent() { + const [isOpen, setIsOpen] = useState(false); + const ref = useClickOutside(() => setIsOpen(false)); + + return ( +
{/* Content that will close when clicking outside */}
+ ); +} +``` + +### Parameters + +- `callback: () => void` - Function to call when clicking outside + +### Returns + +- `ref` - React ref to attach to the element you want to monitor + +## `ClickOutside` Component + +A wrapper component that handles click outside detection for its children. + +### Usage + +```tsx +import { ClickOutside } from "./ClickOutside"; + +function MyComponent() { + const [isOpen, setIsOpen] = useState(false); + + return ( + setIsOpen(false)}> +
{/* Content that will close when clicking outside */}
+
+ ); +} +``` + +### Props + +- `children: React.ReactNode` - The content to wrap +- `onClickOutside: () => void` - Function to call when clicking outside +- `className?: string` - Optional className for the wrapper div +- `disabled?: boolean` - Disable click outside detection (default: false) + +## Common Use Cases + +### Modal Dialog + +```tsx +
+ +
Modal content
+
+
+``` + +### Dropdown Menu + +```tsx +
+ + {isOpen && ( + +
Menu items
+
+ )} +
+``` + +### Popover/Tooltip + +```tsx + +
Popover content
+
+``` + +## Notes + +- The hook uses `mousedown` events for detection +- Event listeners are properly cleaned up when the component unmounts +- The wrapper component adds a single `div` element +- Use the `disabled` prop to temporarily disable click outside detection +- TypeScript generic support for proper ref typing diff --git a/ui/src/components/ClickOutside.tsx b/ui/src/components/ClickOutside.tsx new file mode 100644 index 0000000..be746aa --- /dev/null +++ b/ui/src/components/ClickOutside.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useClickOutside } from "../hooks/useClickOutside"; + +interface ClickOutsideProps { + children: React.ReactNode; + onClickOutside: () => void; + className?: string; + disabled?: boolean; +} + +/** + * Component that wraps children and detects clicks outside + * @param children - The content to wrap + * @param onClickOutside - Function to call when clicking outside + * @param className - Optional className for the wrapper + * @param disabled - Disable click outside detection + */ +export const ClickOutside: React.FC = ({ + children, + onClickOutside, + className, + disabled = false, +}) => { + const ref = useClickOutside( + disabled ? () => {} : onClickOutside + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/ui/src/components/CreateTabloModal.tsx b/ui/src/components/CreateTabloModal.tsx new file mode 100644 index 0000000..cfdc52c --- /dev/null +++ b/ui/src/components/CreateTabloModal.tsx @@ -0,0 +1,121 @@ +import { useState } from "react"; +import { ImageColorPicker } from "./ImageColorPicker"; +import { ClickOutside } from "./ClickOutside"; +import { StatusPicker } from "./StatusPicker"; + +interface Tablo { + id: number; + name: string; + image?: string; + color?: string; + status: "todo" | "in_progress" | "done"; +} + +interface CreateTabloModalProps { + onClose: () => void; + onCreate: (tabloData: Omit) => void; +} + +export const CreateTabloModal = ({ + onClose, + onCreate, +}: CreateTabloModalProps) => { + const [newTabloName, setNewTabloName] = useState(""); + const [creationMode, setCreationMode] = useState<"image" | "color">("color"); + const [selectedImage, setSelectedImage] = useState( + "https://images.unsplash.com/photo-1553877522-43269d4ea984?w=400&h=250&fit=crop&crop=center" + ); + const [selectedColor, setSelectedColor] = useState("bg-blue-500"); + const [selectedStatus, setSelectedStatus] = useState< + "todo" | "in_progress" | "done" + >("todo"); + + const resetForm = () => { + setNewTabloName(""); + setCreationMode("color"); + setSelectedImage( + "https://images.unsplash.com/photo-1553877522-43269d4ea984?w=400&h=250&fit=crop&crop=center" + ); + setSelectedColor("bg-blue-500"); + setSelectedStatus("todo"); + }; + + const handleClose = () => { + resetForm(); + onClose(); + }; + + const handleCreate = () => { + if (newTabloName.trim()) { + const tabloData = { + name: newTabloName.trim(), + status: selectedStatus, + ...(creationMode === "image" + ? { image: selectedImage } + : { color: selectedColor }), + }; + onCreate(tabloData); + resetForm(); + } + }; + + return ( +
+ +
+

+ Créer un nouveau tablo +

+ +
+ {/* Name Input */} +
+ + setNewTabloName(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" + placeholder="Entrez le nom du tablo" + autoFocus + /> +
+ + + + +
+ + {/* Modal Actions */} +
+ + +
+
+
+
+ ); +}; diff --git a/ui/src/components/ImageColorPicker.tsx b/ui/src/components/ImageColorPicker.tsx new file mode 100644 index 0000000..f23a000 --- /dev/null +++ b/ui/src/components/ImageColorPicker.tsx @@ -0,0 +1,150 @@ +interface ImageColorPickerProps { + creationMode: "image" | "color"; + setCreationMode: (mode: "image" | "color") => void; + selectedColor: string; + setSelectedColor: (color: string) => void; +} + +const AVAILABLE_COLORS = [ + "bg-blue-500", + "bg-green-500", + "bg-purple-500", + "bg-red-500", + "bg-yellow-500", + "bg-indigo-500", + "bg-pink-500", + "bg-teal-500", + "bg-orange-500", + "bg-cyan-500", +]; + +export const ImageColorPicker = ({ + creationMode, + setCreationMode, + selectedColor, + setSelectedColor, +}: ImageColorPickerProps) => { + return ( +
+ {/* Mode Toggle */} +
+ +
+ + +
+
+ + {/* Image Mode */} + {creationMode === "image" && ( +
+ {/* File Upload - Coming Soon */} +
+
+ + + +

+ Import d'images +

+

+ Bientôt disponible +

+
+
+ + {/* Commented out image selection */} + {/*
+ +
+ {availableImages.map((image) => ( + + ))} +
+
*/} +
+ )} + + {/* Color Mode */} + {creationMode === "color" && ( +
+ +
+ {AVAILABLE_COLORS.map((color) => ( + + ))} +
+
+ )} +
+ ); +}; diff --git a/ui/src/components/StatusPicker.tsx b/ui/src/components/StatusPicker.tsx new file mode 100644 index 0000000..c01599d --- /dev/null +++ b/ui/src/components/StatusPicker.tsx @@ -0,0 +1,52 @@ +interface StatusPickerProps { + selectedStatus: "todo" | "in_progress" | "done"; + setSelectedStatus: (status: "todo" | "in_progress" | "done") => void; +} + +export const StatusPicker = ({ + selectedStatus, + setSelectedStatus, +}: StatusPickerProps) => { + return ( +
+ +
+ + + +
+
+ ); +}; diff --git a/ui/src/components/TabloModal.tsx b/ui/src/components/TabloModal.tsx new file mode 100644 index 0000000..15cb02b --- /dev/null +++ b/ui/src/components/TabloModal.tsx @@ -0,0 +1,128 @@ +import { ClickOutside } from "./ClickOutside"; +import { useState } from "react"; +import { ImageColorPicker } from "./ImageColorPicker"; +import { StatusPicker } from "./StatusPicker"; + +interface Tablo { + id: number; + name: string; + image?: string; + color?: string; + status: "todo" | "in_progress" | "done"; +} + +interface TabloModalProps { + tablo: Tablo | null; + onClose: () => void; + onSave?: (updatedTablo: Tablo) => void; +} + +export const TabloModal = ({ tablo, onClose, onSave }: TabloModalProps) => { + const [editData, setEditData] = useState(tablo); + const [isEditingName, setIsEditingName] = useState(false); + + const [creationMode, setCreationMode] = useState<"image" | "color">("color"); + const [selectedColor, setSelectedColor] = useState("bg-blue-500"); + + const handleCancelEdit = () => { + setEditData(null); + }; + + const handleSaveEdit = () => { + if (editData && onSave) { + // Clear the unused field based on selection + const updatedTablo = { + ...editData, + image: creationMode === "image" ? editData.image : undefined, + color: creationMode === "color" ? editData.color : undefined, + }; + onSave(updatedTablo); + } + setEditData(null); + }; + + if (!tablo) return null; + + const currentData = editData || tablo; + + return ( +
+ +
+ {/* Header */} +
+ {isEditingName ? ( + setIsEditingName(false)}> + + setEditData((prev) => + prev ? { ...prev, name: e.target.value } : null + ) + } + className="text-2xl font-bold text-gray-900 dark:text-white bg-transparent border-b-2 border-blue-500 focus:outline-none focus:border-blue-600" + /> + + ) : ( +

setIsEditingName(true)} + > + {tablo.name} +

+ )} + +
+ + {/* Content - Scrollable */} +
+ + + {/* Details */} +
+
+ + setEditData((prev) => (prev ? { ...prev, status } : null)) + } + /> +
+
+
+ + {/* Footer */} +
+ <> + + + +
+
+
+
+ ); +}; diff --git a/ui/src/hooks/useClickOutside.ts b/ui/src/hooks/useClickOutside.ts new file mode 100644 index 0000000..11ff02c --- /dev/null +++ b/ui/src/hooks/useClickOutside.ts @@ -0,0 +1,47 @@ +import React, { useEffect, useRef } from "react"; + +/** + * Hook that detects clicks outside of a referenced element + * @param callback - Function to call when clicking outside + * @returns ref - Ref to attach to the element you want to detect clicks outside of + */ +export function useClickOutside( + callback: () => void +) { + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + }; + + // Add event listener + document.addEventListener("mousedown", handleClickOutside); + + // Cleanup + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [callback]); + + return ref; +} + +export const ClickOutside = ({ + children, + callback, +}: { + children: React.ReactNode; + callback: () => void; +}) => { + const ref = useClickOutside(callback); + return React.createElement( + "div", + { + ref, + }, + children + ); +}; diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx index 6cc3326..984ed22 100644 --- a/ui/src/pages/tablo.tsx +++ b/ui/src/pages/tablo.tsx @@ -1,738 +1,371 @@ import { SignOutButton } from "@ui/components/SignOutButton"; -import { useUser } from "@ui/providers/UserStoreProvider"; +import { CreateTabloModal } from "@ui/components/CreateTabloModal"; +import { TabloModal } from "@ui/components/TabloModal"; import { useState } from "react"; interface Tablo { id: number; name: string; - color: string; -} - -interface Folder { - id: string; - name: string; - tablos: Tablo[]; - isOpen: boolean; + image?: string; + color?: string; + status: "todo" | "in_progress" | "done"; } export const TabloPage = () => { - const user = useUser(); - console.log({ user }); - const [hoveredTablo, setHoveredTablo] = useState(null); - const [isModalOpen, setIsModalOpen] = useState(false); - const [newTabloName, setNewTabloName] = useState(""); - const [selectedColor, setSelectedColor] = useState("bg-blue-500"); - - // Drag and drop state - const [draggedItem, setDraggedItem] = useState<{ - type: "tablo" | "folder"; - id: string | number; - } | null>(null); - const [dragOverItem, setDragOverItem] = useState<{ - type: "tablo" | "folder"; - id: string | number; - } | null>(null); - const [dragOverPosition, setDragOverPosition] = useState< - "before" | "after" | "on" | null - >(null); - - // Folder editing state - const [editingFolderId, setEditingFolderId] = useState(null); - const [editingFolderName, setEditingFolderName] = useState(""); - - // Folder modal state - const [openFolderId, setOpenFolderId] = useState(null); + const [contextMenuTablo, setContextMenuTablo] = useState(null); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [viewingTablo, setViewingTablo] = useState(null); // Sample tablo data - in a real app this would come from an API const [tablos, setTablos] = useState([ - { id: 1, name: "Projet Alpha", color: "bg-blue-500" }, - { id: 2, name: "Marketing Q4", color: "bg-green-500" }, - { id: 3, name: "Équipe Dev", color: "bg-purple-500" }, - { id: 4, name: "Budget 2024", color: "bg-red-500" }, - { id: 5, name: "Roadmap", color: "bg-yellow-500" }, - { id: 6, name: "Support Client", color: "bg-indigo-500" }, + { + id: 1, + name: "Projet Alpha", + image: + "https://images.unsplash.com/photo-1553877522-43269d4ea984?w=400&h=250&fit=crop&crop=center", + status: "in_progress", + }, + { + id: 2, + name: "Marketing Q4", + image: + "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400&h=250&fit=crop&crop=center", + status: "done", + }, + { + id: 3, + name: "Équipe Dev", + image: + "https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=400&h=250&fit=crop&crop=center", + status: "todo", + }, + { + id: 4, + name: "Budget 2024", + image: + "https://images.unsplash.com/photo-1554224155-6726b3ff858f?w=400&h=250&fit=crop&crop=center", + status: "in_progress", + }, + { + id: 5, + name: "Roadmap", + image: + "https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=400&h=250&fit=crop&crop=center", + status: "todo", + }, + { + id: 6, + name: "Support Client", + image: + "https://images.unsplash.com/photo-1516321318423-f06f85e504b3?w=400&h=250&fit=crop&crop=center", + status: "done", + }, ]); - const [folders, setFolders] = useState([]); - const menuItems = [ - { name: "Conversations", icon: "💬" }, - { name: "Planning", icon: "📅" }, - { name: "Notes", icon: "📝" }, + { name: "Conversations" }, + { name: "Planning" }, + { name: "Notes" }, ]; - const availableColors = [ - "bg-blue-500", - "bg-green-500", - "bg-purple-500", - "bg-red-500", - "bg-yellow-500", - "bg-indigo-500", - "bg-pink-500", - "bg-teal-500", - "bg-orange-500", - "bg-cyan-500", - ]; - - const openModal = () => { - setIsModalOpen(true); - setNewTabloName(""); - setSelectedColor("bg-blue-500"); + const openCreateModal = () => { + setIsCreateModalOpen(true); }; - const closeModal = () => { - setIsModalOpen(false); - setNewTabloName(""); - setSelectedColor("bg-blue-500"); + const closeCreateModal = () => { + setIsCreateModalOpen(false); }; - const createNewTablo = () => { - if (newTabloName.trim()) { - const newId = Math.max(...tablos.map((t) => t.id), 0) + 1; - const newTablo = { - id: newId, - name: newTabloName.trim(), - color: selectedColor, - }; - setTablos([...tablos, newTablo]); - closeModal(); - } - }; - - // Drag and Drop Handlers - const handleDragStart = ( - e: React.DragEvent, - type: "tablo" | "folder", - id: string | number - ) => { - setDraggedItem({ type, id }); - e.dataTransfer.effectAllowed = "move"; - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - }; - - const handleDragEnter = ( - e: React.DragEvent, - type: "tablo" | "folder", - id: string | number - ) => { - e.preventDefault(); - if (draggedItem && (draggedItem.type !== type || draggedItem.id !== id)) { - const rect = e.currentTarget.getBoundingClientRect(); - const midY = rect.top + rect.height / 2; - const midX = rect.left + rect.width / 2; - - // Determine position based on mouse position - let position: "before" | "after" | "on" = "on"; - - if (draggedItem.type === "tablo" && type === "tablo") { - // For tablo-to-tablo, check if we want to reorder or create folder - const distanceFromCenter = Math.sqrt( - Math.pow(e.clientX - midX, 2) + Math.pow(e.clientY - midY, 2) - ); - - if (distanceFromCenter < 50) { - position = "on"; // Create folder - } else if (e.clientX < midX) { - position = "before"; // Reorder before - } else { - position = "after"; // Reorder after - } - } - - setDragOverItem({ type, id }); - setDragOverPosition(position); - } - }; - - const handleDragLeave = (e: React.DragEvent) => { - // Only clear drag over if we're leaving the actual target, not a child element - if (!e.currentTarget.contains(e.relatedTarget as Node)) { - setDragOverItem(null); - setDragOverPosition(null); - } - }; - - const handleDrop = ( - e: React.DragEvent, - targetType: "tablo" | "folder", - targetId: string | number - ) => { - e.preventDefault(); - - if ( - !draggedItem || - (draggedItem.type === targetType && draggedItem.id === targetId) - ) { - setDraggedItem(null); - setDragOverItem(null); - setDragOverPosition(null); - return; - } - - // Handle dropping tablo onto tablo - if (draggedItem.type === "tablo" && targetType === "tablo") { - const draggedTablo = tablos.find((t) => t.id === draggedItem.id); - const targetTablo = tablos.find((t) => t.id === targetId); - - if (draggedTablo && targetTablo) { - if (dragOverPosition === "on") { - // Create new folder - const newFolderId = `folder-${Date.now()}`; - const newFolder: Folder = { - id: newFolderId, - name: "Nouveau dossier", - tablos: [targetTablo, draggedTablo], - isOpen: false, - }; - - // Remove tablos from main list and add folder - setTablos( - tablos.filter((t) => t.id !== draggedItem.id && t.id !== targetId) - ); - setFolders([...folders, newFolder]); - } else { - // Reorder tablos - const newTablos = tablos.filter((t) => t.id !== draggedItem.id); - const targetIndex = newTablos.findIndex((t) => t.id === targetId); - const insertIndex = - dragOverPosition === "before" ? targetIndex : targetIndex + 1; - - newTablos.splice(insertIndex, 0, draggedTablo); - setTablos(newTablos); - } - } - } - - // Handle dropping tablo onto folder - else if (draggedItem.type === "tablo" && targetType === "folder") { - const draggedTablo = tablos.find((t) => t.id === draggedItem.id); - - if (draggedTablo) { - // Add tablo to folder - setFolders( - folders.map((folder) => - folder.id === targetId - ? { ...folder, tablos: [...folder.tablos, draggedTablo] } - : folder - ) - ); - - // Remove tablo from main list - setTablos(tablos.filter((t) => t.id !== draggedItem.id)); - } - } - - // Handle dropping folder to reorder folders - else if (draggedItem.type === "folder" && targetType === "folder") { - const draggedFolder = folders.find((f) => f.id === draggedItem.id); - const targetFolder = folders.find((f) => f.id === targetId); - - if (draggedFolder && targetFolder) { - const newFolders = folders.filter((f) => f.id !== draggedItem.id); - const targetIndex = newFolders.findIndex((f) => f.id === targetId); - const insertIndex = - dragOverPosition === "before" ? targetIndex : targetIndex + 1; - - newFolders.splice(insertIndex, 0, draggedFolder); - setFolders(newFolders); - } - } - - setDraggedItem(null); - setDragOverItem(null); - setDragOverPosition(null); - }; - - const handleDragEnd = () => { - setDraggedItem(null); - setDragOverItem(null); - setDragOverPosition(null); - }; - - const toggleFolder = (folderId: string) => { - setOpenFolderId(openFolderId === folderId ? null : folderId); - }; - - const removeTabloFromFolder = (folderId: string, tabloId: number) => { - const folder = folders.find((f) => f.id === folderId); - if (!folder) return; - - const tabloToRemove = folder.tablos.find((t) => t.id === tabloId); - if (!tabloToRemove) return; - - // Remove tablo from folder - const updatedFolder = { - ...folder, - tablos: folder.tablos.filter((t) => t.id !== tabloId), + const createNewTablo = (tabloData: Omit) => { + const newId = Math.max(...tablos.map((t) => t.id), 0) + 1; + const newTablo: Tablo = { + id: newId, + ...tabloData, }; + setTablos([...tablos, newTablo]); + setIsCreateModalOpen(false); + }; - // If folder becomes empty, just remove it - if (updatedFolder.tablos.length === 0) { - setFolders(folders.filter((f) => f.id !== folderId)); - setTablos([...tablos, tabloToRemove]); - // Close modal if this folder was open - if (openFolderId === folderId) { - setOpenFolderId(null); - } - } - // If folder has only one item left, dissolve it - else if (updatedFolder.tablos.length === 1) { - setFolders(folders.filter((f) => f.id !== folderId)); - setTablos([...tablos, tabloToRemove, ...updatedFolder.tablos]); - // Close modal if this folder was open (folder is being dissolved) - if (openFolderId === folderId) { - setOpenFolderId(null); - } - } - // Otherwise, keep the folder and just remove the tablo - else { - setFolders(folders.map((f) => (f.id === folderId ? updatedFolder : f))); - setTablos([...tablos, tabloToRemove]); + // Tablo movement functions + const moveTabloLeft = (tabloId: number) => { + const currentIndex = tablos.findIndex((t) => t.id === tabloId); + if (currentIndex > 0) { + const newTablos = [...tablos]; + [newTablos[currentIndex - 1], newTablos[currentIndex]] = [ + newTablos[currentIndex], + newTablos[currentIndex - 1], + ]; + setTablos(newTablos); } }; - // Folder editing handlers - const startEditingFolder = (folderId: string, currentName: string) => { - setEditingFolderId(folderId); - setEditingFolderName(currentName); - }; - - const saveEditingFolder = () => { - if (editingFolderId && editingFolderName.trim()) { - setFolders( - folders.map((folder) => - folder.id === editingFolderId - ? { ...folder, name: editingFolderName.trim() } - : folder - ) - ); + const moveTabloRight = (tabloId: number) => { + const currentIndex = tablos.findIndex((t) => t.id === tabloId); + if (currentIndex < tablos.length - 1) { + const newTablos = [...tablos]; + [newTablos[currentIndex], newTablos[currentIndex + 1]] = [ + newTablos[currentIndex + 1], + newTablos[currentIndex], + ]; + setTablos(newTablos); } - setEditingFolderId(null); - setEditingFolderName(""); }; - const cancelEditingFolder = () => { - setEditingFolderId(null); - setEditingFolderName(""); + const openTablo = (tabloId: number) => { + const tablo = tablos.find((t) => t.id === tabloId); + if (tablo) { + setViewingTablo(tablo); + } }; - const renderTablo = (tablo: Tablo, inFolder = false, folderId?: string) => { - const isDragOver = - dragOverItem?.type === "tablo" && dragOverItem?.id === tablo.id; - const canCreateFolder = - isDragOver && dragOverPosition === "on" && draggedItem?.type === "tablo"; + const closeTabloModal = () => { + setViewingTablo(null); + }; - const dragOverClass = isDragOver - ? dragOverPosition === "on" - ? "ring-2 ring-blue-400 ring-opacity-75 animate-pulse" - : "" - : ""; + const getStatusLabel = (status: Tablo["status"]) => { + switch (status) { + case "todo": + return "À faire"; + case "in_progress": + return "En cours"; + case "done": + return "Terminé"; + default: + return "À faire"; + } + }; + const getStatusBadgeColor = (status: Tablo["status"]) => { + switch (status) { + case "todo": + return "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"; + case "in_progress": + return "bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"; + case "done": + return "bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300"; + default: + return "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300"; + } + }; + + const changeTabloStatus = (tabloId: number, newStatus: Tablo["status"]) => { + setTablos( + tablos.map((tablo) => + tablo.id === tabloId ? { ...tablo, status: newStatus } : tablo + ) + ); + }; + + const renderTablo = (tablo: Tablo) => { return (
handleDragStart(e, "tablo", tablo.id)} - onDragOver={handleDragOver} - onDragEnter={(e) => handleDragEnter(e, "tablo", tablo.id)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, "tablo", tablo.id)} - onDragEnd={handleDragEnd} - onMouseEnter={() => setHoveredTablo(tablo.id)} - onMouseLeave={() => setHoveredTablo(null)} + className="relative" + onContextMenu={(e) => { + e.preventDefault(); + setContextMenuTablo(contextMenuTablo === tablo.id ? null : tablo.id); + }} > - {/* Drop indicators */} - {isDragOver && dragOverPosition === "before" && ( -
- )} - {isDragOver && dragOverPosition === "after" && ( -
- )} - - {/* Folder creation overlay */} - {canCreateFolder && ( -
-
- - - -
-
- )} -
console.log(`Open tablo: ${tablo.name}`)} + className="bg-white dark:bg-gray-800 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 cursor-pointer w-64 overflow-hidden border border-gray-200 dark:border-gray-700" + onClick={(e) => { + e.stopPropagation(); + openTablo(tablo.id); + }} > -
-

- {tablo.name} -

+ {/* Image or Color */} +
+ {tablo.image ? ( + {tablo.name} + ) : ( +
+

+ {tablo.name} +

+
+ )} +
+ + {/* Content */} +
+
+

+ {tablo.name} +

+ {/* Status badge */} +
+ {getStatusLabel(tablo.status)} +
+
{/* Contextual Menu */} - {hoveredTablo === tablo.id && ( -
+ {contextMenuTablo === tablo.id && ( +
e.stopPropagation()} + > {/* Regular menu items - always show */} {menuItems.map((item, index) => ( ))} - {/* Additional option for tablos in folders */} - {inFolder && folderId && ( - <> -
- - - )} + {/* Separator */} +
+ + {/* Tablo actions */} + + + + + {/* Status change options */} +
+
+ Changer le statut +
+ + +
)}
); }; - const renderFolder = (folder: Folder) => { - const isDragOver = - dragOverItem?.type === "folder" && dragOverItem?.id === folder.id; - const dragOverClass = isDragOver - ? dragOverPosition === "on" - ? "ring-2 ring-blue-400 ring-opacity-75" - : "" - : ""; - - return ( -
handleDragStart(e, "folder", folder.id)} - onDragOver={handleDragOver} - onDragEnter={(e) => handleDragEnter(e, "folder", folder.id)} - onDragLeave={handleDragLeave} - onDrop={(e) => handleDrop(e, "folder", folder.id)} - onDragEnd={handleDragEnd} - > - {/* Drop indicators */} - {isDragOver && dragOverPosition === "before" && ( -
- )} - {isDragOver && dragOverPosition === "after" && ( -
- )} - - {/* Folder view - always shows as closed since modal handles open state */} -
toggleFolder(folder.id)} - > - {/* Mini preview of tablos in folder */} -
- {folder.tablos.slice(0, 4).map((tablo, index) => ( -
- ))} -
- -
-
📁
- {editingFolderId === folder.id ? ( - setEditingFolderName(e.target.value)} - onBlur={saveEditingFolder} - onKeyDown={(e) => { - if (e.key === "Enter") { - saveEditingFolder(); - } else if (e.key === "Escape") { - cancelEditingFolder(); - } - }} - className="bg-white dark:bg-gray-600 text-gray-900 dark:text-white text-sm font-semibold px-2 py-1 rounded border-none outline-none focus:ring-2 focus:ring-blue-400 text-center" - autoFocus - onClick={(e) => e.stopPropagation()} - /> - ) : ( -

{ - e.stopPropagation(); - startEditingFolder(folder.id, folder.name); - }} - > - {folder.name} -

- )} -

- {folder.tablos.length} tablo - {folder.tablos.length !== 1 ? "s" : ""} -

-
-
-
- ); - }; - return ( -
+
setContextMenuTablo(null)}>

- Vos tablos + Tablos

+
-
- -
- - {/* Drag instruction */} - {draggedItem && ( -
-

- 💡 Glissez vers le centre pour créer un dossier, vers les bords - pour réorganiser, ou sur un dossier pour ajouter -

-
- )} - -
- {/* Render folders */} - {folders.map((folder) => renderFolder(folder))} - - {/* Render standalone tablos */} +
+ {/* Render tablos */} {tablos.map((tablo) => renderTablo(tablo))}
- {/* Modal */} - {isModalOpen && ( -
-
-

- Créer un nouveau tablo -

- -
- {/* Name Input */} -
- - setNewTabloName(e.target.value)} - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" - placeholder="Entrez le nom du tablo" - autoFocus - /> -
- - {/* Color Selection */} -
- -
- {availableColors.map((color) => ( -
-
-
- - {/* Modal Actions */} -
- - -
-
-
+ {/* Create Tablo Modal */} + {isCreateModalOpen && ( + )} - {/* Folder Modal */} - {openFolderId && ( -
-
- {(() => { - const currentFolder = folders.find((f) => f.id === openFolderId); - if (!currentFolder) return null; - - return ( - <> - {/* Header */} -
- {editingFolderId === openFolderId ? ( - setEditingFolderName(e.target.value)} - onBlur={saveEditingFolder} - onKeyDown={(e) => { - if (e.key === "Enter") { - saveEditingFolder(); - } else if (e.key === "Escape") { - cancelEditingFolder(); - } - }} - className="bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-2xl font-bold px-3 py-1 rounded border border-gray-300 dark:border-gray-600 outline-none focus:ring-2 focus:ring-blue-400 focus:border-blue-400 flex-1 mr-4" - autoFocus - /> - ) : ( -

- startEditingFolder(openFolderId, currentFolder.name) - } - > - 📁 {currentFolder.name} -

- )} -
- - {currentFolder.tablos.length} tablo - {currentFolder.tablos.length !== 1 ? "s" : ""} - - -
-
- - {/* Content */} -
- {currentFolder.tablos.length === 0 ? ( -
-
📂
-

- Ce dossier est vide -

-

- Glissez des tablos ici pour les organiser -

-
- ) : ( -
- {currentFolder.tablos.map((tablo) => - renderTablo(tablo, true, openFolderId) - )} -
- )} -
- - ); - })()} -
-
+ {/* Tablo Details Modal */} + {!!viewingTablo && ( + )}
);