xtablo-source/ui/src/components/TabloModal.tsx
2025-10-16 21:11:30 +02:00

911 lines
39 KiB
TypeScript

import { Button } from "@ui/components/ui/button";
import { useInviteUser } from "@ui/hooks/invite";
import {
useCreateTabloFile,
useDeleteTabloFile,
useDownloadTabloFile,
useTabloFileNames,
} from "@ui/hooks/tablo_data";
import { useTabloMembers } from "@ui/hooks/tablos";
import { toast } from "@ui/lib/toast";
import { useUser } from "@ui/providers/UserStoreProvider";
import { TabloUpdate, UserTablo } from "@ui/types/tablos.types";
import { FileTrigger } from "@ui/ui-library/file-trigger";
import { DownloadIcon, Trash2Icon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { ClickOutside } from "./ClickOutside";
import { ImageColorPicker } from "./ImageColorPicker";
import { StatusPicker } from "./StatusPicker";
type StatusType = "todo" | "in_progress" | "done";
interface TabloModalProps {
tablo: UserTablo | null;
onEdit: (updatedTablo: TabloUpdate & { id: string }) => void;
onClose: () => void;
readOnly?: boolean;
}
export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
const currentUser = useUser();
const isAdmin = tablo?.is_admin ?? false;
const [editData, setEditData] = useState<UserTablo | null>(tablo);
const [isEditingName, setIsEditingName] = useState(false);
const [creationMode, setCreationMode] = useState<"image" | "color">("color");
const [selectedColor, setSelectedColor] = useState(tablo?.color || "bg-blue-500");
const [error, setError] = useState("");
const { data: members } = useTabloMembers(tablo?.id ?? "");
const [showMembers, setShowMembers] = useState(false);
const [inviteEmail, setInviteEmail] = useState("");
const { mutate: inviteUser, isPending: isInvitingUser } = useInviteUser();
const {
data: fileData,
isLoading: filesLoading,
error: filesError,
} = useTabloFileNames(tablo?.id ?? "");
const [showFiles, setShowFiles] = useState(false);
// File upload state
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [deletingFile, setDeletingFile] = useState<string | null>(null);
const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
const createFile = useCreateTabloFile();
const deleteFile = useDeleteTabloFile();
const downloadFile = useDownloadTabloFile();
const handleSaveEdit = () => {
if (editData && onEdit) {
// Clear the unused field based on selection
const updatedTablo: TabloUpdate & { id: string } = {
id: editData.id,
name: editData.name,
//TODO: image: creationMode === "image" ? editData.image : null,
color: creationMode === "color" ? selectedColor : null,
status: editData.status,
};
onEdit(updatedTablo);
}
};
const handleSendInvite = () => {
if (inviteEmail.trim()) {
inviteUser({ email: inviteEmail, tablo_id: tablo?.id ?? "" });
}
};
const isEmailValid = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleFileSelect = (files: FileList | null) => {
const file = files?.[0];
if (!file) return;
// Validate file size (20MB limit)
const maxSize = 20 * 1024 * 1024; // 20MB in bytes
if (file.size > maxSize) {
setError("Le fichier ne peut pas dépasser 20MB");
return;
}
setError("");
setSelectedFile(file);
};
const handleFileUpload = async () => {
if (!selectedFile || !tablo?.id) return;
setIsUploading(true);
try {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const content = e.target?.result as string;
await createFile.mutateAsync({
tabloId: tablo.id,
fileName: selectedFile.name,
data: {
content,
contentType: selectedFile.type || "application/octet-stream",
},
});
// Reset upload state
setSelectedFile(null);
setIsUploading(false);
} catch (uploadError) {
setIsUploading(false);
console.error("Upload error:", uploadError);
}
};
reader.onerror = () => {
setIsUploading(false);
toast.add(
{
title: "Erreur de lecture",
description: "Impossible de lire le fichier sélectionné",
type: "error",
},
{
timeout: 5000,
}
);
};
// Read file as base64 data URL for binary files, or as text for text files
if (selectedFile.type.startsWith("text/") || selectedFile.type === "application/json") {
reader.readAsText(selectedFile);
} else {
reader.readAsDataURL(selectedFile);
}
} catch (error) {
setIsUploading(false);
console.error("Upload error:", error);
}
};
const cancelFileUpload = () => {
setSelectedFile(null);
};
const handleDeleteFile = async (fileName: string) => {
if (!tablo?.id) return;
// Simple confirmation
if (!window.confirm(`Êtes-vous sûr de vouloir supprimer le fichier "${fileName}" ?`)) {
return;
}
setDeletingFile(fileName);
try {
await deleteFile.mutateAsync({
tabloId: tablo.id,
fileName,
});
} catch (error) {
console.error("Delete error:", error);
} finally {
setDeletingFile(null);
}
};
const handleDownloadFile = async (fileName: string) => {
if (!tablo?.id) return;
setDownloadingFile(fileName);
try {
await downloadFile.mutateAsync({
tabloId: tablo.id,
fileName,
});
} catch (error) {
console.error("Download error:", error);
} finally {
setDownloadingFile(null);
}
};
if (!tablo) return null;
const currentData = editData || tablo;
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && isAdmin) {
handleSaveEdit();
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onClose, handleSaveEdit, isAdmin]);
// Auto-focus name input when editing
const nameInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isEditingName && nameInputRef.current) {
nameInputRef.current.focus();
nameInputRef.current.select();
}
}, [isEditingName]);
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
<ClickOutside onClickOutside={onClose}>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full min-w-[32rem] max-w-2xl max-h-[95vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<div className="flex items-center space-x-3 flex-1">
{/* Tablo Color/Image Preview */}
<div className="flex-shrink-0">
{tablo.image ? (
<img
src={tablo.image}
alt={tablo.name}
className="w-10 h-10 rounded-lg object-cover"
/>
) : (
<div
className={`w-10 h-10 rounded-lg ${
tablo.color || "bg-blue-500"
} flex items-center justify-center`}
>
<span className="text-white font-bold text-sm">
{tablo.name.charAt(0).toUpperCase()}
</span>
</div>
)}
</div>
{/* Title */}
<div className="flex-1 min-w-0">
{isAdmin && isEditingName ? (
<ClickOutside onClickOutside={() => setIsEditingName(false)}>
<input
ref={nameInputRef}
type="text"
value={editData?.name}
onChange={(e) =>
setEditData((prev) => (prev ? { ...prev, name: e.target.value } : null))
}
onKeyDown={(e) => {
if (e.key === "Enter") {
setIsEditingName(false);
}
}}
className="text-xl font-bold text-gray-900 dark:text-white bg-transparent border-b-2 border-blue-500 focus:outline-none focus:border-blue-600 w-full"
placeholder="Nom du tablo"
/>
</ClickOutside>
) : (
<div>
<h2
className={`text-xl font-bold text-gray-900 dark:text-white truncate ${
isAdmin
? "cursor-text hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
: ""
}`}
onClick={isAdmin ? () => setIsEditingName(true) : undefined}
title={tablo.name}
>
{tablo.name}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{isAdmin ? "Administrateur" : "Invité"} {" "}
{currentData.status === "todo" && "À faire"}
{currentData.status === "in_progress" && "En cours"}
{currentData.status === "done" && "Terminé"}
</p>
</div>
)}
</div>
</div>
{/* Close Button */}
<button
onClick={onClose}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
title="Fermer (Échap)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* Error Banner */}
{error && (
<div className="mx-6 mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center space-x-2">
<svg
className="w-5 h-5 text-red-500 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="text-red-700 dark:text-red-300 text-sm">{error}</span>
<button
onClick={() => setError("")}
className="ml-auto text-red-500 hover:text-red-700 dark:hover:text-red-300"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
)}
{/* Content - Expandable */}
<div className="flex-grow px-6 py-4 overflow-y-auto space-y-6">
{!isAdmin ? (
/* Read-only content */
<div className="space-y-4 mb-4">
{/* Tablo Preview */}
<div className="relative h-48 rounded-lg overflow-hidden">
{tablo.image ? (
<img
src={tablo.image}
alt={tablo.name}
className="w-full h-full object-cover"
/>
) : (
<div
className={`w-full h-full ${
tablo.color || "bg-gray-400"
} flex items-center justify-center`}
>
<h3 className="text-white font-bold text-2xl text-center px-4">
{tablo.name}
</h3>
</div>
)}
</div>
{/* Status Display */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Statut
</label>
<div className="text-gray-900 dark:text-white">
{currentData.status === "todo" && "À faire"}
{currentData.status === "in_progress" && "En cours"}
{currentData.status === "done" && "Terminé"}
</div>
</div>
{/* Access Level */}
<div className="space-y-2">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
Votre rôle
</label>
<div className="text-gray-900 dark:text-white">
{tablo.is_admin ? "Administrateur" : "Invité"}
</div>
</div>
</div>
) : (
/* Editable content */
<>
<ImageColorPicker
creationMode={creationMode}
setCreationMode={setCreationMode}
selectedColor={selectedColor}
setSelectedColor={setSelectedColor}
/>
{/* Details */}
<div className="space-y-4 mb-4">
<div>
<StatusPicker
selectedStatus={currentData.status as StatusType}
setSelectedStatus={(status) =>
setEditData((prev) => (prev ? { ...prev, status } : null))
}
/>
</div>
{/* Invite User Section */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-3">
Inviter un utilisateur
</h3>
<div className="flex space-x-2">
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="Email de l'utilisateur à inviter"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
{isInvitingUser ? (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
</div>
) : (
<button
type="button"
onClick={handleSendInvite}
disabled={!isEmailValid(inviteEmail)}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-md transition-colors"
>
Inviter
</button>
)}
</div>
</div>
</div>
</>
)}
</div>
{/* Files Section */}
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Fichiers</h3>
{fileData?.fileNames && (
<span className="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-xs font-medium px-2 py-1 rounded-full">
{fileData.fileNames.length}
</span>
)}
</div>
<button
type="button"
onClick={() => setShowFiles(!showFiles)}
className="flex items-center space-x-2 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-white dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<span>{showFiles ? "Masquer" : "Afficher"}</span>
<svg
className={`w-4 h-4 transition-transform ${showFiles ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
{showFiles && (
<div className="space-y-4">
{/* File Upload Section - Only for Admins */}
{isAdmin && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 mb-4">
<div className="flex items-center space-x-2 mb-3">
<svg
className="w-4 h-4 text-green-600 dark:text-green-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
<h4 className="text-sm font-semibold text-gray-900 dark:text-white">
Ajouter un fichier
</h4>
</div>
{!selectedFile ? (
<div className="space-y-3">
<FileTrigger allowsMultiple={false} onSelect={handleFileSelect}>
<Button
variant="outline"
className="w-full justify-center py-8 border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-blue-400 dark:hover:border-blue-500 bg-gray-50 dark:bg-gray-800/50 hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-colors"
>
<div className="flex flex-col items-center space-y-2">
<svg
className="w-8 h-8 text-gray-400 dark:text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<div className="text-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
Cliquez pour sélectionner un fichier
</span>
</div>
</div>
</Button>
</FileTrigger>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center space-x-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900 dark:text-white">
{selectedFile.name}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
{(selectedFile.size / 1024 / 1024).toFixed(2)} MB
</p>
</div>
</div>
<div className="flex space-x-3">
<button
type="button"
onClick={handleFileUpload}
disabled={isUploading}
className="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-green-600 hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-lg transition-colors flex items-center justify-center space-x-2 shadow-sm"
>
{isUploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
<span>Ajout en cours...</span>
</>
) : (
<>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<span>Ajouter le fichier</span>
</>
)}
</button>
<button
type="button"
onClick={cancelFileUpload}
disabled={isUploading}
className="px-4 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors"
>
Annuler
</button>
</div>
</div>
)}
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
Taille maximale: 2MB
</p>
</div>
)}
{/* File List */}
<div>
{filesLoading ? (
<div className="flex items-center justify-center p-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
<span className="ml-3 text-sm text-gray-500 dark:text-gray-400">
Chargement des fichiers...
</span>
</div>
) : filesError ? (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center space-x-2">
<svg
className="w-5 h-5 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span className="text-sm text-red-700 dark:text-red-300">
Erreur lors du chargement des fichiers
</span>
</div>
</div>
) : fileData && fileData.fileNames && fileData.fileNames.length > 0 ? (
<div className="space-y-2">
{fileData.fileNames.map((fileName, index) => {
const fileExtension = fileName.split(".").pop()?.toLowerCase() || "";
const isImage = ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(
fileExtension
);
const isPdf = fileExtension === "pdf";
const isText = ["txt", "md", "json", "csv"].includes(fileExtension);
return (
<div
key={index}
className="flex items-center space-x-3 p-3 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-sm transition-shadow group"
>
<button
onClick={() => handleDownloadFile(fileName)}
disabled={downloadingFile === fileName}
className={`w-10 h-10 rounded-lg flex items-center justify-center text-white text-sm font-medium transition-all hover:scale-105 ${
isImage
? "bg-purple-500 hover:bg-purple-600"
: isPdf
? "bg-red-500 hover:bg-red-600"
: isText
? "bg-blue-500 hover:bg-blue-600"
: "bg-gray-500 hover:bg-gray-600"
} ${
downloadingFile === fileName
? "opacity-50 cursor-not-allowed"
: "cursor-pointer"
}`}
title={`Télécharger ${fileName}`}
>
{downloadingFile === fileName ? (
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
) : isImage ? (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
) : isPdf ? (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"
/>
</svg>
) : (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
)}
</button>
<div className="flex-1 min-w-0">
<p
className="text-sm font-medium text-gray-900 dark:text-white truncate"
title={fileName}
>
{fileName}
</p>
<p className="text-xs text-gray-500 dark:text-gray-400 uppercase">
{fileExtension || "Fichier"}
</p>
</div>
<div className="flex items-center space-x-1">
<Button
size="sm"
variant="ghost"
onClick={() => handleDownloadFile(fileName)}
disabled={downloadingFile === fileName}
className="p-2 text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:text-blue-400 dark:hover:bg-blue-900/20 transition-colors"
aria-label={`Télécharger ${fileName}`}
>
{downloadingFile === fileName ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"></div>
) : (
<DownloadIcon className="w-4 h-4" />
)}
</Button>
{isAdmin && (
<Button
size="sm"
variant="ghost"
onClick={() => handleDeleteFile(fileName)}
disabled={deletingFile === fileName}
className="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900/20 transition-colors"
aria-label={`Supprimer ${fileName}`}
>
{deletingFile === fileName ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-red-500"></div>
) : (
<Trash2Icon className="w-4 h-4" />
)}
</Button>
)}
</div>
</div>
);
})}
</div>
) : (
<div className="text-center py-8">
<svg
className="w-12 h-12 text-gray-300 dark:text-gray-600 mx-auto mb-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 5a2 2 0 012-2h2a2 2 0 012 2v0H8v0z"
/>
</svg>
<p className="text-sm text-gray-500 dark:text-gray-400">
Aucun fichier dans ce tablo
</p>
{isAdmin && (
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
Ajoutez votre premier fichier ci-dessus
</p>
)}
</div>
)}
</div>
</div>
)}
</div>
{/* Members Section */}
<div className="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Membres</h3>
{members && (
<span className="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-xs font-medium px-2 py-1 rounded-full">
{members.length}
</span>
)}
</div>
<button
type="button"
onClick={() => setShowMembers(!showMembers)}
className="flex items-center space-x-2 px-3 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-white dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<span>{showMembers ? "Masquer" : "Afficher"}</span>
<svg
className={`w-4 h-4 transition-transform ${showMembers ? "rotate-180" : ""}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
</div>
{showMembers && (
<div className="space-y-2">
{members && members.length > 0 ? (
members.map((member, index) => (
<div
key={index}
className="flex items-center space-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-md"
>
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-medium">
{member.name.charAt(0).toUpperCase()}
</div>
<span className="text-sm text-gray-900 dark:text-white">{member.name}</span>
{member.is_admin ? (
<span className="text-xs text-gray-500 dark:text-gray-400">
{member.id === currentUser?.id ? "(Vous, Admin)" : "(Admin)"}
</span>
) : (
<span className="text-xs text-gray-500 dark:text-gray-400">
{member.id === currentUser?.id ? "(Vous, Invité)" : "(Invité)"}
</span>
)}
</div>
))
) : (
<p className="text-sm text-gray-500 dark:text-gray-400">Aucun membre trouvé</p>
)}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0 bg-gray-50 dark:bg-gray-900/50">
<div className="flex space-x-3 ml-auto">
{!isAdmin ? (
<button
type="button"
className="px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 rounded-lg transition-colors"
onClick={onClose}
>
Fermer
</button>
) : (
<>
<button
type="button"
className="px-6 py-2.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 rounded-lg transition-colors"
onClick={onClose}
>
Annuler
</button>
<button
type="button"
className="px-6 py-2.5 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg shadow-sm transition-colors flex items-center space-x-2"
onClick={handleSaveEdit}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span>Sauvegarder</span>
</button>
</>
)}
</div>
</div>
</div>
</ClickOutside>
</div>
);
};