[dyad] wrote 6 file(s)

This commit is contained in:
Michael committed 2025-11-06 15:50:27 +01:00
1 parent 52267eb3ca
commit 1ce71f37c2
6 files changed
+670 -69

No files matched your search

+8 -19
View File
@@ -8,13 +8,14 @@ import { useEffect, useState } from "react";
import Index from "./pages/Index";
import Login from "./pages/Login";
import NotFound from "./pages/NotFound";
import AuthService from "./services/AuthService";
const queryClient = new QueryClient();
// Protected Route Component
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
const token = localStorage.getItem("token");
if (!token) {
const isAuthenticated = AuthService.isAuthenticated();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
@@ -25,24 +26,12 @@ const App = () => {
useEffect(() => {
// Vérifier le token au démarrage
const token = localStorage.getItem("token");
if (token) {
// 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(() => {
const checkAuth = async () => {
const isValid = await AuthService.checkTokenValidity();
setIsLoading(false);
});
} else {
setIsLoading(false);
}
};
checkAuth();
}, []);
if (isLoading) {
+304 -30
View File
@@ -6,30 +6,140 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
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 { 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 [user, setUser] = useState<any>(null);
const [showAdmin, setShowAdmin] = useState(false);
const [activeTab, setActiveTab] = useState("all");
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(() => {
const userStr = localStorage.getItem("user");
if (userStr) {
setUser(JSON.parse(userStr));
const user = AuthService.getUser();
if (user) {
setUser(user);
loadNotes();
}
}, []);
const handleLogout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
showSuccess("Déconnexion réussie");
window.location.href = "/login";
const loadNotes = async () => {
const notes = await NotesService.getNotes();
setNotes(notes);
if (notes.length > 0) {
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 (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Top Navigation */}
@@ -103,7 +213,11 @@ const Index = () => {
/>
</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" />
Nouvelle Note
</Button>
@@ -115,33 +229,53 @@ const Index = () => {
</TabsList>
</Tabs>
<div className="space-y-2">
{/* Notes list placeholder */}
<Card className="cursor-pointer hover:shadow-md transition-shadow">
<div className="space-y-2 max-h-[calc(100vh-240px)] overflow-y-auto">
{filteredNotes.length === 0 ? (
<p className="text-center text-muted-foreground py-4">Aucune note trouvée</p>
) : (
filteredNotes.map(note => (
<Card
key={note.id}
className={`cursor-pointer hover:shadow-md transition-shadow ${currentNote?.id === note.id ? 'border-primary' : ''}`}
onClick={() => setCurrentNote(note)}
>
<CardContent className="p-4">
<h3 className="font-medium">Note d'exemple</h3>
<h3 className="font-medium">{note.title || "Sans titre"}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
Ceci est un exemple de contenu de note. Cliquez pour éditer.
{note.content ? note.content.replace(/<[^>]*>/g, '') : ""}
</p>
</CardContent>
</Card>
))
)}
</div>
</div>
{/* Note Editor */}
{currentNote ? (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Input
type="text"
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">
<Button variant="outline" size="icon">
<Button
variant="outline"
size="icon"
onClick={() => handleUpdateNote({ archived: !currentNote.archived })}
>
<Archive className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon">
<Button
variant="outline"
size="icon"
onClick={handleDeleteNote}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
@@ -161,7 +295,8 @@ const Index = () => {
className="min-h-[500px] outline-none"
contentEditable
suppressContentEditableWarning
placeholder="Commencez à écrire..."
dangerouslySetInnerHTML={{ __html: currentNote.content }}
onBlur={(e) => handleUpdateNote({ content: e.currentTarget.innerHTML })}
/>
</CardContent>
</Card>
@@ -171,17 +306,57 @@ const Index = () => {
<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">
<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">
<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>
{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>
@@ -191,20 +366,84 @@ const Index = () => {
<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">
<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">
{/* Image placeholders */}
{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>
</div>
</div>
)}
</div>
</div>
@@ -225,28 +464,63 @@ const Index = () => {
</TabsList>
<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" />
Ajouter un utilisateur
</Button>
<div className="space-y-2">
<Card>
{users.length > 0 ? (
users.map(user => (
<Card key={user.id}>
<CardContent className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback>AD</AvatarFallback>
<AvatarFallback>{user.username.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
<div>
<p className="font-medium">admin</p>
<p className="text-sm text-muted-foreground">Administrateur</p>
<p className="font-medium">{user.username}</p>
<p className="text-sm text-muted-foreground">
{user.is_admin ? "Administrateur" : "Utilisateur"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm">Modifier</Button>
{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>
</TabsContent>
+6 -20
View File
@@ -2,40 +2,26 @@ import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
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 AuthService from "@/services/AuthService";
import { useNavigate } from "react-router-dom";
const Login = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
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");
const success = await AuthService.login(username, password);
if (success) {
navigate("/");
}
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);
}
};
return (
+110
View File
@@ -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();
+94
View File
@@ -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();
+148
View File
@@ -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();