mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-22 22:58:15 +02:00
[dyad] wrote 6 file(s)
This commit is contained in:
1 parent
52267eb3ca
commit
1ce71f37c2
6 files changed
+751
-150
No files matched your search
+8
-19
@@ -8,13 +8,14 @@ import { useEffect, useState } from "react";
|
|||||||
import Index from "./pages/Index";
|
import Index from "./pages/Index";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
import NotFound from "./pages/NotFound";
|
import NotFound from "./pages/NotFound";
|
||||||
|
import AuthService from "./services/AuthService";
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
// Protected Route Component
|
// Protected Route Component
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
const token = localStorage.getItem("token");
|
const isAuthenticated = AuthService.isAuthenticated();
|
||||||
if (!token) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
@@ -25,24 +26,12 @@ const App = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Vérifier le token au démarrage
|
// Vérifier le token au démarrage
|
||||||
const token = localStorage.getItem("token");
|
const checkAuth = async () => {
|
||||||
if (token) {
|
const isValid = await AuthService.checkTokenValidity();
|
||||||
// Optionnel: Vérifier la validité du token avec le serveur
|
|
||||||
fetch("/api/auth/me", {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
localStorage.removeItem("user");
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
|||||||
+383
-109
@@ -6,30 +6,140 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { showSuccess } from "@/utils/toast";
|
|
||||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||||
import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, Image, CheckSquare } from "lucide-react";
|
import { PlusCircle, Search, User, LogOut, Settings, ChevronDown, Plus, Archive, Trash2, Image, CheckSquare } from "lucide-react";
|
||||||
|
import AuthService from "@/services/AuthService";
|
||||||
|
import AdminService from "@/services/AdminService";
|
||||||
|
import NotesService, { Note } from "@/services/NotesService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
const [user, setUser] = useState<any>(null);
|
const [user, setUser] = useState<any>(null);
|
||||||
const [showAdmin, setShowAdmin] = useState(false);
|
const [showAdmin, setShowAdmin] = useState(false);
|
||||||
const [activeTab, setActiveTab] = useState("all");
|
const [activeTab, setActiveTab] = useState("all");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [notes, setNotes] = useState<Note[]>([]);
|
||||||
|
const [currentNote, setCurrentNote] = useState<Note | null>(null);
|
||||||
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userStr = localStorage.getItem("user");
|
const user = AuthService.getUser();
|
||||||
if (userStr) {
|
if (user) {
|
||||||
setUser(JSON.parse(userStr));
|
setUser(user);
|
||||||
|
loadNotes();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const loadNotes = async () => {
|
||||||
localStorage.removeItem("token");
|
const notes = await NotesService.getNotes();
|
||||||
localStorage.removeItem("user");
|
setNotes(notes);
|
||||||
showSuccess("Déconnexion réussie");
|
if (notes.length > 0) {
|
||||||
window.location.href = "/login";
|
setCurrentNote(notes[0]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
if (user?.is_admin) {
|
||||||
|
const users = await AdminService.getUsers();
|
||||||
|
setUsers(users);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
AuthService.logout();
|
||||||
|
navigate("/login");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateNote = async () => {
|
||||||
|
const newNote = await NotesService.createNote("Nouvelle note");
|
||||||
|
if (newNote) {
|
||||||
|
setNotes([newNote, ...notes]);
|
||||||
|
setCurrentNote(newNote);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateNote = async (updatedFields: Partial<Note>) => {
|
||||||
|
if (!currentNote) return;
|
||||||
|
|
||||||
|
const updatedNote = { ...currentNote, ...updatedFields };
|
||||||
|
const success = await NotesService.updateNote(updatedNote);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setCurrentNote(updatedNote);
|
||||||
|
setNotes(notes.map(note => note.id === updatedNote.id ? updatedNote : note));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteNote = async () => {
|
||||||
|
if (!currentNote?.id) return;
|
||||||
|
|
||||||
|
if (window.confirm("Êtes-vous sûr de vouloir supprimer cette note ?")) {
|
||||||
|
const success = await NotesService.deleteNote(currentNote.id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const updatedNotes = notes.filter(note => note.id !== currentNote.id);
|
||||||
|
setNotes(updatedNotes);
|
||||||
|
setCurrentNote(updatedNotes.length > 0 ? updatedNotes[0] : null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddUser = async () => {
|
||||||
|
const username = prompt("Nom d'utilisateur:");
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
const password = prompt("Mot de passe:");
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
const isAdmin = window.confirm("Donner les droits administrateur ?");
|
||||||
|
|
||||||
|
const newUser = await AdminService.createUser(username, password, isAdmin);
|
||||||
|
if (newUser) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePassword = async (userId: number) => {
|
||||||
|
const password = prompt("Nouveau mot de passe:");
|
||||||
|
if (!password) return;
|
||||||
|
|
||||||
|
const success = await AdminService.updateUserPassword(userId, password);
|
||||||
|
if (success) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleAdmin = async (userId: number, currentStatus: boolean) => {
|
||||||
|
const success = await AdminService.toggleAdminStatus(userId, currentStatus);
|
||||||
|
if (success) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteUser = async (userId: number) => {
|
||||||
|
if (window.confirm("Êtes-vous sûr de vouloir supprimer cet utilisateur ?")) {
|
||||||
|
const success = await AdminService.deleteUser(userId);
|
||||||
|
if (success) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Charger les utilisateurs quand le panel admin s'ouvre
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAdmin) {
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
}, [showAdmin]);
|
||||||
|
|
||||||
|
const filteredNotes = notes.filter(note =>
|
||||||
|
(activeTab === "all" || (activeTab === "archived" && note.archived)) &&
|
||||||
|
(searchQuery === "" ||
|
||||||
|
note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
note.content.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
{/* Top Navigation */}
|
{/* Top Navigation */}
|
||||||
@@ -103,7 +213,11 @@ const Index = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className="w-full flex items-center gap-2" size="lg">
|
<Button
|
||||||
|
className="w-full flex items-center gap-2"
|
||||||
|
size="lg"
|
||||||
|
onClick={handleCreateNote}
|
||||||
|
>
|
||||||
<PlusCircle className="h-4 w-4" />
|
<PlusCircle className="h-4 w-4" />
|
||||||
Nouvelle Note
|
Nouvelle Note
|
||||||
</Button>
|
</Button>
|
||||||
@@ -115,96 +229,221 @@ const Index = () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2 max-h-[calc(100vh-240px)] overflow-y-auto">
|
||||||
{/* Notes list placeholder */}
|
{filteredNotes.length === 0 ? (
|
||||||
<Card className="cursor-pointer hover:shadow-md transition-shadow">
|
<p className="text-center text-muted-foreground py-4">Aucune note trouvée</p>
|
||||||
<CardContent className="p-4">
|
) : (
|
||||||
<h3 className="font-medium">Note d'exemple</h3>
|
filteredNotes.map(note => (
|
||||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
<Card
|
||||||
Ceci est un exemple de contenu de note. Cliquez pour éditer.
|
key={note.id}
|
||||||
</p>
|
className={`cursor-pointer hover:shadow-md transition-shadow ${currentNote?.id === note.id ? 'border-primary' : ''}`}
|
||||||
</CardContent>
|
onClick={() => setCurrentNote(note)}
|
||||||
</Card>
|
>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h3 className="font-medium">{note.title || "Sans titre"}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||||
|
{note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Note Editor */}
|
{/* Note Editor */}
|
||||||
<div className="space-y-4">
|
{currentNote ? (
|
||||||
<div className="flex items-center justify-between">
|
<div className="space-y-4">
|
||||||
<Input
|
<div className="flex items-center justify-between">
|
||||||
type="text"
|
<Input
|
||||||
placeholder="Titre de la note"
|
type="text"
|
||||||
className="text-xl font-semibold border-none shadow-none text-2xl h-auto px-0 focus-visible:ring-0"
|
placeholder="Titre de la note"
|
||||||
/>
|
className="text-xl font-semibold border-none shadow-none text-2xl h-auto px-0 focus-visible:ring-0"
|
||||||
|
value={currentNote.title}
|
||||||
|
onChange={(e) => handleUpdateNote({ title: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button variant="outline" size="icon">
|
<Button
|
||||||
<Archive className="h-4 w-4" />
|
variant="outline"
|
||||||
</Button>
|
size="icon"
|
||||||
<Button variant="outline" size="icon">
|
onClick={() => handleUpdateNote({ archived: !currentNote.archived })}
|
||||||
<Trash2 className="h-4 w-4" />
|
>
|
||||||
|
<Archive className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleDeleteNote}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs defaultValue="content">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="content">Contenu</TabsTrigger>
|
||||||
|
<TabsTrigger value="todos">Tâches</TabsTrigger>
|
||||||
|
<TabsTrigger value="images">Images</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="content" className="mt-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div
|
||||||
|
className="min-h-[500px] outline-none"
|
||||||
|
contentEditable
|
||||||
|
suppressContentEditableWarning
|
||||||
|
dangerouslySetInnerHTML={{ __html: currentNote.content }}
|
||||||
|
onBlur={(e) => handleUpdateNote({ content: e.currentTarget.innerHTML })}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="todos" className="mt-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const text = prompt("Nouvelle tâche:");
|
||||||
|
if (text) {
|
||||||
|
const updatedTodos = [...(currentNote.todos || []), { text, completed: false }];
|
||||||
|
handleUpdateNote({ todos: updatedTodos });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Ajouter une tâche
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{currentNote.todos && currentNote.todos.length > 0 ? (
|
||||||
|
currentNote.todos.map((todo, index) => (
|
||||||
|
<div key={index} className="flex items-center gap-2 p-2 border rounded-md">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={todo.completed}
|
||||||
|
onChange={() => {
|
||||||
|
const updatedTodos = [...currentNote.todos];
|
||||||
|
updatedTodos[index].completed = !updatedTodos[index].completed;
|
||||||
|
handleUpdateNote({ todos: updatedTodos });
|
||||||
|
}}
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
<span className={todo.completed ? "line-through text-muted-foreground" : ""}>
|
||||||
|
{todo.text}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="ml-auto h-6 w-6"
|
||||||
|
onClick={() => {
|
||||||
|
const updatedTodos = [...currentNote.todos];
|
||||||
|
updatedTodos.splice(index, 1);
|
||||||
|
handleUpdateNote({ todos: updatedTodos });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-4">Aucune tâche</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="images" className="mt-4">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label htmlFor="image-upload" className="cursor-pointer">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Image className="h-4 w-4" />
|
||||||
|
Ajouter une image
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
id="image-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={async (e) => {
|
||||||
|
if (e.target.files && e.target.files[0] && currentNote.id) {
|
||||||
|
const image = await NotesService.uploadImage(currentNote.id, e.target.files[0]);
|
||||||
|
if (image) {
|
||||||
|
const updatedImages = [...(currentNote.images || []), image];
|
||||||
|
handleUpdateNote({ images: updatedImages });
|
||||||
|
}
|
||||||
|
e.target.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
|
{currentNote.images && currentNote.images.length > 0 ? (
|
||||||
|
currentNote.images.map((image, index) => (
|
||||||
|
<div key={index} className="relative aspect-square rounded-md overflow-hidden border">
|
||||||
|
<img
|
||||||
|
src={`/uploads/${image.filename}`}
|
||||||
|
alt={`Note image ${index}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="icon"
|
||||||
|
className="absolute top-1 right-1 h-6 w-6 rounded-full"
|
||||||
|
onClick={async () => {
|
||||||
|
if (image.id && currentNote.id) {
|
||||||
|
const success = await NotesService.deleteImage(currentNote.id, image.id);
|
||||||
|
if (success) {
|
||||||
|
const updatedImages = currentNote.images.filter((_, i) => i !== index);
|
||||||
|
handleUpdateNote({ images: updatedImages });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="col-span-full text-center text-muted-foreground py-4">Aucune image</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-muted-foreground">Aucune note sélectionnée</p>
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
onClick={handleCreateNote}
|
||||||
|
>
|
||||||
|
Créer une note
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<Tabs defaultValue="content">
|
|
||||||
<TabsList>
|
|
||||||
<TabsTrigger value="content">Contenu</TabsTrigger>
|
|
||||||
<TabsTrigger value="todos">Tâches</TabsTrigger>
|
|
||||||
<TabsTrigger value="images">Images</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<TabsContent value="content" className="mt-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4">
|
|
||||||
<div
|
|
||||||
className="min-h-[500px] outline-none"
|
|
||||||
contentEditable
|
|
||||||
suppressContentEditableWarning
|
|
||||||
placeholder="Commencez à écrire..."
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="todos" className="mt-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4 space-y-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
|
||||||
<Plus className="h-4 w-4" />
|
|
||||||
Ajouter une tâche
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 p-2 border rounded-md">
|
|
||||||
<CheckSquare className="h-4 w-4 text-muted-foreground" />
|
|
||||||
<span>Exemple de tâche</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="images" className="mt-4">
|
|
||||||
<Card>
|
|
||||||
<CardContent className="p-4 space-y-4">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Button variant="outline" size="sm" className="flex items-center gap-2">
|
|
||||||
<Image className="h-4 w-4" />
|
|
||||||
Ajouter une image
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
|
||||||
{/* Image placeholders */}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -225,28 +464,63 @@ const Index = () => {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="users" className="mt-4 space-y-4">
|
<TabsContent value="users" className="mt-4 space-y-4">
|
||||||
<Button className="flex items-center gap-2">
|
<Button
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={handleAddUser}
|
||||||
|
>
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
Ajouter un utilisateur
|
Ajouter un utilisateur
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Card>
|
{users.length > 0 ? (
|
||||||
<CardContent className="p-4 flex items-center justify-between">
|
users.map(user => (
|
||||||
<div className="flex items-center gap-3">
|
<Card key={user.id}>
|
||||||
<Avatar>
|
<CardContent className="p-4 flex items-center justify-between">
|
||||||
<AvatarFallback>AD</AvatarFallback>
|
<div className="flex items-center gap-3">
|
||||||
</Avatar>
|
<Avatar>
|
||||||
<div>
|
<AvatarFallback>{user.username.charAt(0).toUpperCase()}</AvatarFallback>
|
||||||
<p className="font-medium">admin</p>
|
</Avatar>
|
||||||
<p className="text-sm text-muted-foreground">Administrateur</p>
|
<div>
|
||||||
</div>
|
<p className="font-medium">{user.username}</p>
|
||||||
</div>
|
<p className="text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-2">
|
{user.is_admin ? "Administrateur" : "Utilisateur"}
|
||||||
<Button variant="outline" size="sm">Modifier</Button>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
<div className="flex items-center gap-2">
|
||||||
|
{user.id !== 1 && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleChangePassword(user.id)}
|
||||||
|
>
|
||||||
|
Mot de passe
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleToggleAdmin(user.id, user.is_admin)}
|
||||||
|
>
|
||||||
|
{user.is_admin ? "Retirer admin" : "Rendre admin"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDeleteUser(user.id)}
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-muted-foreground py-4">Chargement des utilisateurs...</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
+8
-22
@@ -2,40 +2,26 @@ import { useState } from "react";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||||
import { showError, showSuccess } from "@/utils/toast";
|
|
||||||
import { MadeWithDyad } from "@/components/made-with-dyad";
|
import { MadeWithDyad } from "@/components/made-with-dyad";
|
||||||
|
import AuthService from "@/services/AuthService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
const handleLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
const success = await AuthService.login(username, password);
|
||||||
const response = await fetch("/api/auth/login", {
|
if (success) {
|
||||||
method: "POST",
|
navigate("/");
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ username, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.message || "Erreur de connexion");
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem("token", data.token);
|
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
|
||||||
showSuccess("Connexion réussie");
|
|
||||||
window.location.href = "/";
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : "Erreur de connexion");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import AuthService from "./AuthService";
|
||||||
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
is_admin: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdminService {
|
||||||
|
async getUsers(): Promise<User[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/users", {
|
||||||
|
headers: AuthService.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la récupération des utilisateurs");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUser(username: string, password: string, isAdmin: boolean): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/users", {
|
||||||
|
method: "POST",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify({ username, password, is_admin: isAdmin })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || "Erreur lors de la création de l'utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Utilisateur créé avec succès");
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateUserPassword(userId: number, password: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${userId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la modification du mot de passe");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Mot de passe modifié avec succès");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleAdminStatus(userId: number, currentStatus: boolean): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${userId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify({ is_admin: !currentStatus })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la modification des droits");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Droits modifiés avec succès");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUser(userId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/users/${userId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: AuthService.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la suppression de l'utilisateur");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Utilisateur supprimé avec succès");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AdminService();
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
is_admin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthService {
|
||||||
|
private token: string | null;
|
||||||
|
private user: User | null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.token = localStorage.getItem("token");
|
||||||
|
const userStr = localStorage.getItem("user");
|
||||||
|
this.user = userStr ? JSON.parse(userStr) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
return !!this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUser(): User | null {
|
||||||
|
return this.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken(): string | null {
|
||||||
|
return this.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
getHeaders(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
"Authorization": `Bearer ${this.token}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(username: string, password: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ username, password })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.message || "Erreur de connexion");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = data.token;
|
||||||
|
this.user = data.user;
|
||||||
|
localStorage.setItem("token", this.token);
|
||||||
|
localStorage.setItem("user", JSON.stringify(this.user));
|
||||||
|
|
||||||
|
showSuccess("Connexion réussie");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur de connexion");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(): void {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
this.token = null;
|
||||||
|
this.user = null;
|
||||||
|
showSuccess("Déconnexion réussie");
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkTokenValidity(): Promise<boolean> {
|
||||||
|
if (!this.token) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/auth/me", {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
this.logout();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
this.logout();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AuthService();
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import AuthService from "./AuthService";
|
||||||
|
import { showError, showSuccess } from "@/utils/toast";
|
||||||
|
|
||||||
|
interface Todo {
|
||||||
|
id?: number;
|
||||||
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
|
position?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
id?: number;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Note {
|
||||||
|
id?: number;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
archived?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
todos: Todo[];
|
||||||
|
images: Image[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotesService {
|
||||||
|
async getNotes(): Promise<Note[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/notes", {
|
||||||
|
headers: AuthService.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la récupération des notes");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNote(title: string, content: string = ""): Promise<Note | null> {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/notes", {
|
||||||
|
method: "POST",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify({ title, content, todos: [] })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la création de la note");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Note créée avec succès");
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNote(note: Note): Promise<boolean> {
|
||||||
|
if (!note.id) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${note.id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: AuthService.getHeaders(),
|
||||||
|
body: JSON.stringify(note)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la mise à jour de la note");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(noteId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: AuthService.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la suppression de la note");
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess("Note supprimée avec succès");
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImage(noteId: number, file: File): Promise<Image | null> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("image", file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/images`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": AuthService.getHeaders().Authorization
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de l'upload de l'image");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteImage(noteId: number, imageId: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/notes/${noteId}/images/${imageId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: AuthService.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Erreur lors de la suppression de l'image");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : "Erreur serveur");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new NotesService();
|
||||||
Reference in new issue
Block a user