import React, { useState, useEffect, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
FlaskConical, // Outil de recherche/expérimentation
BookOpen, // Outil de gestion bibliographique
BarChart4, // Outil d'analyse statistique
LayoutDashboard, // Dashboard
Users, // Gestion des utilisateurs
Settings, // Paramètres
HelpCircle, // Aide/Documentation
Plus, // Ajouter un outil
Search, // Recherche
GripVertical, // Pour le drag and drop
ExternalLink,
Loader2,
CheckCircle,
AlertTriangle,
XCircle,
List,
Grid,
UserPlus, // Pour l'inscription
LogIn, // Pour la connexion
LogOut,
KeyRound
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
// ===============================
// Types & Interfaces
// ===============================
interface Tool {
id: string;
name: string;
description: string;
category: string;
url: string;
icon: React.ReactNode;
addedBy: string; // User ID
dateAdded: Date;
tags: string[];
ownedByCurrentUser: boolean;
}
interface User {
id: string;
name: string;
role: 'admin' | 'user';
}
type Layout = 'grid' | 'list';
// ===============================
// Mock Data & Constants
// ===============================
const CATEGORIES = [
{ name: 'Analyse de Données', value: 'data-analysis' },
{ name: 'Gestion de Références', value: 'reference-management' },
{ name: 'Visualisation', value: 'visualization' },
{ name: 'Collaboration', value: 'collaboration' },
{ name: 'Outils Généraux', value: 'general' },
{ name: 'Bio-informatique', value: 'bioinformatics' },
{ name: 'Chimie', value: 'chemistry' },
{ name: 'Mathématiques', value: 'mathematics' },
{ name: 'Physique', value: 'physics' },
{ name: 'Sciences Sociales', value: 'social-sciences' },
{ name: 'Humanités Numériques', value: 'digital-humanities' },
];
const ICONS = {
'data-analysis': BarChart4,
'reference-management': BookOpen,
'visualization': BarChart4, // Réutiliser, ou créer une icône spécifique
'collaboration': Users,
'general': FlaskConical,
'bioinformatics': FlaskConical, // À remplacer si une icône plus spécifique existe
'chemistry': FlaskConical, // À remplacer
'mathematics': FlaskConical, // À remplacer
'physics': FlaskConical, // À remplacer
'social-sciences': BookOpen, // À remplacer
'digital-humanities': BookOpen, // À remplacer
};
// ===============================
// Helper Functions
// ===============================
const getCategoryIcon = (categoryValue: string) => {
const category = CATEGORIES.find(c => c.value === categoryValue);
return category ? ICONS[category.value] || FlaskConical : FlaskConical;
};
// ===============================
// Composants
// ===============================
// Composant pour afficher une seule carte d'outil
const ToolCard = ({ tool, layout, onDelete }: { tool: Tool, layout: Layout, onDelete?: (id: string) => void }) => {
const Icon = getCategoryIcon(tool.category);
return (
{layout === 'grid' && }
{tool.name}
{CATEGORIES.find(c => c.value === tool.category)?.name || 'Non catégorisé'}
{tool.description}
{layout === 'list' && (
{tool.tags.map(tag => (
{tag}
))}
)}
{tool.ownedByCurrentUser && onDelete && (
)}
);
};
// Composant pour la barre de navigation latérale
const Sidebar = ({ onAddTool, onMyTools }: { onAddTool: () => void, onMyTools: () => void }) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(true); // État pour la visibilité sur mobile
return (
<>
{/* Bouton pour basculer la sidebar sur mobile */}
{/* Contenu de la sidebar */}
Recherche Tools
>
);
};
// Form schema for adding a tool
const addToolFormSchema = z.object({
name: z.string().min(2, {
message: "Le nom doit contenir au moins 2 caractères.",
}),
description: z.string().min(10, {
message: "La description doit contenir au moins 10 caractères.",
}),
url: z.string().url({ message: "URL invalide." }),
category: z.string().nonempty("Veuillez sélectionner une catégorie."),
tags: z.array(z.string()),
});
// Composant pour ajouter un outil
const AddToolSheet = ({ onAdd }: { onAdd: (tool: Tool) => void }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const form = useForm>({
resolver: zodResolver(addToolFormSchema),
defaultValues: {
name: "",
description: "",
url: "",
category: "",
tags: [],
},
})
const handleAddTag = (newTag: string, tags: string[], setTags: (tags: string[]) => void) => {
if (newTag.trim() && !tags.includes(newTag.trim())) {
setTags([...tags, newTag.trim()]);
}
};
const handleRemoveTag = (tagToRemove: string, tags: string[], setTags: (tags: string[]) => void) => {
setTags(tags.filter(tag => tag !== tagToRemove));
};
const onSubmit = async (values: z.infer) => {
setIsLoading(true);
setError(null);
// Simuler un délai d'ajout
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const newTool: Tool = {
id: crypto.randomUUID(),
name: values.name,
description: values.description,
category: values.category,
url: values.url,
icon: getCategoryIcon(values.category), // Utiliser la fonction pour obtenir l'icône
addedBy: 'user123', // Remplacer par l'ID de l'utilisateur connecté
dateAdded: new Date(),
tags: values.tags,
ownedByCurrentUser: true, // Marquer l'outil comme appartenant à l'utilisateur actuel
};
onAdd(newTool);
setIsLoading(false);
form.reset(); // Réinitialiser le formulaire
} catch (error: any) {
setError(`Une erreur s'est produite : ${error.message}`);
setIsLoading(false);
}
};
return (
Ajouter un nouvel outil
Partagez un outil que vous utilisez ou que vous trouvez utile pour la recherche.
);
};
// Form schema for user signup
const signUpFormSchema = z.object({
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
email: z.string().email({ message: "Invalid email address." }),
password: z.string().min(8, { message: "Password must be at least 8 characters." }),
});
// Form schema for user signin
const signInFormSchema = z.object({
email: z.string().email({ message: "Invalid email address." }),
password: z.string().min(8, { message: "Password must be at least 8 characters." }),
});
const ResearchToolsPlatform = () => {
const [tools, setTools] = useState([]);
const [filteredTools, setFilteredTools] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
const [layout, setLayout] = useState('grid'); // 'grid' or 'list'
const [showMyTools, setShowMyTools] = useState(false);
const [currentUser, setCurrentUser] = useState(null); // Simuler l'utilisateur connecté
const [isSignUpOpen, setIsSignUpOpen] = useState(false);
const [isSignInOpen, setIsSignInOpen] = useState(false);
const [loading, setLoading] = useState(true);
const signUpForm = useForm>({
resolver: zodResolver(signUpFormSchema),
defaultValues: {
name: "",
email: "",
password: "",
},
});
const signInForm = useForm>({
resolver: zodResolver(signInFormSchema),
defaultValues: {
email: "",
password: "",
},
});
// Simuler la récupération des données et l'authentification
useEffect(() => {
const mockTools: Tool[] = [
{
id: '1',
name: 'Sci-Hub',
description: 'Accédez à des articles scientifiques gratuitement.',
category: 'general',
url: 'https://sci-hub.se/',
icon: getCategoryIcon('general'),
addedBy: 'user123',
dateAdded: new Date(),
tags: ['articles', 'recherche', 'accès libre'],
ownedByCurrentUser: false
},
{
id: '2',
name: 'Zotero',
description: 'Logiciel de gestion de références bibliographiques.',
category: 'reference-management',
url: 'https://www.zotero.org/',
icon: getCategoryIcon('reference-management'),
addedBy: 'user456',
dateAdded: new Date(),
tags: ['bibliographie', 'citations', 'références'],
ownedByCurrentUser: false
},
{
id: '3',
name: 'Jupyter Notebook',
description: 'Environnement interactif pour le calcul scientifique.',
category: 'data-analysis',
url: 'https://jupyter.org/',
icon: getCategoryIcon('data-analysis'),
addedBy: 'user789',
dateAdded: new Date(),
tags: ['python', 'data science', 'notebooks'],
ownedByCurrentUser: false
},
{
id: '4',
name: 'Plotly',
description: 'Bibliothèque de visualisation interactive.',
category: 'visualization',
url: 'https://plotly.com/',
icon: getCategoryIcon('visualization'),
addedBy: 'user123',
dateAdded: new Date(),
tags: ['graphiques', 'visualisation', 'données'],
ownedByCurrentUser: false
},
{
id: '5',
name: 'Overleaf',
description: 'Editeur LaTeX collaboratif en ligne.',
category: 'collaboration',
url: 'https://www.overleaf.com/',
icon: getCategoryIcon('collaboration'),
addedBy: 'user456',
dateAdded: new Date(),
tags: ['latex', 'collaboration', 'édition'],
ownedByCurrentUser: false
},
{
id: '6',
name: 'BLAST',
description: 'Outil de recherche d\'alignements de séquences biologiques.',
category: 'bioinformatics',
url: 'https://blast.ncbi.nlm.nih.gov/Blast.cgi',
icon: getCategoryIcon('bioinformatics'),
addedBy: 'user789',
dateAdded: new Date(),
tags: ['bioinformatique', 'séquences', 'alignement'],
ownedByCurrentUser: false
},
{
id: '7',
name: 'PubChem',
description: 'Base de données de molécules chimiques et leurs activités biologiques.',
category: 'chemistry',
url: 'https://pubchem.ncbi.nlm.nih.gov/',
icon: getCategoryIcon('chemistry'),
addedBy: 'user123',
dateAdded: new Date(),
tags: ['chimie', 'molécules', 'données'],
ownedByCurrentUser: true // Outil ajouté par l'utilisateur connecté
},
{
id: '8',
name: 'Wolfram Alpha',
description: 'Moteur de connaissance computationnelle.',
category: 'mathematics',
url: 'https://www.wolframalpha.com/',
icon: getCategoryIcon('mathematics'),
addedBy: 'user456',
dateAdded: new Date(),
tags: ['mathématiques', 'calcul', 'connaissance'],
ownedByCurrentUser: true
},
{
id: '9',
name: 'arXiv',
description: 'Archive ouverte de prépublications scientifiques.',
category: 'physics',
url: 'https://arxiv.org/',
icon: getCategoryIcon('physics'),
addedBy: 'user789',
dateAdded: new Date(),
tags: ['physique', 'prépublications', 'recherche'],
ownedByCurrentUser: false
},
{
id: '10',
name: 'ICPSR',
description: 'Archive de données de recherche en sciences sociales.',
category: 'social-sciences',
url: 'https://www.icpsr.umich.edu/',
icon: getCategoryIcon('social-sciences'),
addedBy: 'user123',
dateAdded: new Date(),
tags: ['sciences sociales', 'données', 'recherche'],
ownedByCurrentUser: false
},
{
id: '11',
name: 'Voyant Tools',
description: 'Outil d\'analyse de texte pour les humanités numériques.',
category: 'digital-humanities',
url: 'https://voyant-tools.org/',
icon: getCategoryIcon('digital-humanities'),
addedBy: 'user456',
dateAdded: new Date(),
tags: ['texte', 'analyse', 'humanités numériques'],
ownedByCurrentUser: false
},
];
setTools(mockTools);
// Simuler la connexion d'un utilisateur
setTimeout(() => {
setCurrentUser({ id: 'user123', name: 'John Doe', role: 'user' });
setLoading(false);
}, 1500);
}, []);
// Filtrer les outils en fonction de la recherche et de l'affichage "Mes outils"
useEffect(() => {
let filtered = tools;
if (showMyTools && currentUser) {
filtered = tools.filter(tool => tool.addedBy === currentUser.id);
}
filtered = filtered.filter(tool =>
tool.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
tool.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
);
setFilteredTools(filtered);
}, [searchTerm, tools, showMyTools, currentUser]);
const handleAddTool = (newTool: Tool) => {
setTools(prevTools => {
const updatedTools = [newTool, ...prevTools];
// Mettre à jour ownedByCurrentUser pour le nouvel outil
return updatedTools.map(tool =>
tool.id === newTool.id ? { ...tool, ownedByCurrentUser: true } : tool
);
});
};
const handleShowMyTools = () => {
setShowMyTools(true);
};
const handleShowAllTools = () => {
setShowMyTools(false);
};
const handleDeleteTool = (id: string) => {
setTools(prevTools => prevTools.filter(tool => tool.id !== id));
};
const handleSignUp = async (values: z.infer) => {
// Simuler une requête d'inscription
console.log('Signup form values:', values);
setIsSignUpOpen(false); // Fermer la modal après l'inscription
// Ici, vous devriez appeler votre API pour créer un nouvel utilisateur
// et potentiellement le connecter directement.
// Pour cet exemple, on simule une connexion réussie :
setCurrentUser({ id: 'newUser123', name: values.name, role: 'user' });
};
const handleSignIn = async (values: z.infer) => {
console.log('Signin form values:', values);
setIsSignInOpen(false);
setCurrentUser({id: 'user123', name: "test user", role: "user"})
};
const handleSignOut = () => {
setCurrentUser(null);
// Clear local storage here
};
if (loading) {
return (
);
}
return (
{ }} onMyTools={handleShowMyTools} />
{showMyTools ? 'Mes Outils' : 'Outils de Recherche'}
setSearchTerm(e.target.value)}
className="w-64 bg-gray-800 border-gray-700 text-white"
/>
{!showMyTools && }
{showMyTools && (
)}
{currentUser ? (
) : (
<>
Connexion
Connectez-vous à votre compte pour accéder à toutes les fonctionnalités.
Inscription
Créez un compte pour commencer à utiliser la plateforme.
>
)}