From 1c967dd1dba2e36b1cc861fa9df4052434229fc1 Mon Sep 17 00:00:00 2001 From: Bilal Dieumegard Date: Sun, 1 Dec 2024 15:04:54 +0100 Subject: [PATCH] Ajouts du front de la page contenant les chats, de la creation de chat, des composants d'alerte, de profil et la page de modification de profil. --- package.json | 2 + src/lib/components/ui/Alert.svelte | 49 ++++ src/lib/components/ui/ChatItem.svelte | 19 ++ src/lib/components/ui/ChoosePicture.svelte | 105 ++++++++ src/lib/components/ui/ProfileCard.svelte | 28 ++ src/lib/components/ui/ProfileModal.svelte | 36 +++ src/lib/components/ui/Search.svelte | 16 ++ src/routes/api/canal/[id]/+page.server.ts | 123 +++++++++ .../api/canal/[id]/messages/+page.server.ts | 149 +++++++++++ src/routes/api/canals/+page.server.ts | 47 ++++ src/routes/api/user/[id]/+page.server.ts | 241 ++++++++++++++++++ src/routes/api/users/+page.server.ts | 27 ++ src/routes/chat/+page.svelte | 54 +++- src/routes/chats/+page.svelte | 32 +++ src/routes/user/edit/+page.svelte | 140 ++++++++++ static/profile-default.svg | 4 + 16 files changed, 1070 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/ui/Alert.svelte create mode 100644 src/lib/components/ui/ChatItem.svelte create mode 100644 src/lib/components/ui/ChoosePicture.svelte create mode 100644 src/lib/components/ui/ProfileCard.svelte create mode 100644 src/lib/components/ui/ProfileModal.svelte create mode 100644 src/lib/components/ui/Search.svelte create mode 100644 src/routes/api/canal/[id]/+page.server.ts create mode 100644 src/routes/api/canal/[id]/messages/+page.server.ts create mode 100644 src/routes/api/canals/+page.server.ts create mode 100644 src/routes/api/user/[id]/+page.server.ts create mode 100644 src/routes/api/users/+page.server.ts create mode 100644 src/routes/chats/+page.svelte create mode 100644 src/routes/user/edit/+page.svelte create mode 100644 static/profile-default.svg diff --git a/package.json b/package.json index fb08de5..1f27afd 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "vite": "^5.0.3" }, "dependencies": { + "lucide-svelte": "^0.462.0", + "multer": "^1.4.5-lts.1", "svelte-radix": "^2.0.1" } } diff --git a/src/lib/components/ui/Alert.svelte b/src/lib/components/ui/Alert.svelte new file mode 100644 index 0000000..0558081 --- /dev/null +++ b/src/lib/components/ui/Alert.svelte @@ -0,0 +1,49 @@ + + + +
+ + {message} + + + +
+ + diff --git a/src/lib/components/ui/ChatItem.svelte b/src/lib/components/ui/ChatItem.svelte new file mode 100644 index 0000000..5afb5ee --- /dev/null +++ b/src/lib/components/ui/ChatItem.svelte @@ -0,0 +1,19 @@ + + +
+
+

{title}

+

{lastMessage}

+
+

{time}

+
+ + \ No newline at end of file diff --git a/src/lib/components/ui/ChoosePicture.svelte b/src/lib/components/ui/ChoosePicture.svelte new file mode 100644 index 0000000..ede5fcd --- /dev/null +++ b/src/lib/components/ui/ChoosePicture.svelte @@ -0,0 +1,105 @@ + + + + + +
+ + Image de profil + + + + +
+ + +
+
diff --git a/src/lib/components/ui/ProfileCard.svelte b/src/lib/components/ui/ProfileCard.svelte new file mode 100644 index 0000000..65faf27 --- /dev/null +++ b/src/lib/components/ui/ProfileCard.svelte @@ -0,0 +1,28 @@ + + +
+ + Photo de profil + + +
+

{name}

+

{bio}

+
+
+ + diff --git a/src/lib/components/ui/ProfileModal.svelte b/src/lib/components/ui/ProfileModal.svelte new file mode 100644 index 0000000..36d195a --- /dev/null +++ b/src/lib/components/ui/ProfileModal.svelte @@ -0,0 +1,36 @@ + + +
+
+
+

{name}

+ +
+ +
+ Photo de profil +

{bio}

+
+ +
+ +
+
+
+ + diff --git a/src/lib/components/ui/Search.svelte b/src/lib/components/ui/Search.svelte new file mode 100644 index 0000000..749d684 --- /dev/null +++ b/src/lib/components/ui/Search.svelte @@ -0,0 +1,16 @@ + + +
+ + + +
+ diff --git a/src/routes/api/canal/[id]/+page.server.ts b/src/routes/api/canal/[id]/+page.server.ts new file mode 100644 index 0000000..cfdb6f3 --- /dev/null +++ b/src/routes/api/canal/[id]/+page.server.ts @@ -0,0 +1,123 @@ +import { json } from '@sveltejs/kit'; +import prisma from '$lib/prismaClient'; +import redisClient from '$lib/redisClient'; // Assurez-vous d'importer le client Redis + +// Récupérer les informations du canal et le dernier message (avec cache Redis) +export async function GET({ params }) { + const canalId = parseInt(params.id); + + // Clé cache pour les informations du canal et le dernier message + const canalCacheKey = `canal:${canalId}:info`; + + try { + // Vérifier si les informations du canal et le dernier message sont dans le cache Redis + const cachedCanalData = await redisClient.get(canalCacheKey); + if (cachedCanalData) { + console.log('✅ Cache hit pour les informations du canal et le dernier message'); + return json(JSON.parse(cachedCanalData)); + } + + console.log('❌ Cache miss'); + // Si non, récupérer les informations du canal et le dernier message depuis Prisma + const canal = await prisma.canal.findUnique({ + where: { id: canalId }, + include: { + users: true, // Inclut les utilisateurs associés au canal + }, + }); + + if (!canal) { + return json({ error: 'Canal non trouvé' }, { status: 404 }); + } + + // Récupérer le dernier message + const lastMessage = await prisma.message.findFirst({ + where: { canalId }, + include: { + user: { select: { id: true, pseudo: true } }, + }, + orderBy: { createdAt: 'desc' }, // Trie par date décroissante, donc le dernier message est récupéré en premier + }); + + // Créer un objet combiné pour le canal et le dernier message + const canalData = { + canal, + lastMessage, // Inclure uniquement le dernier message + }; + + // Mettre en cache les informations du canal et le dernier message pendant 5 minutes + await redisClient.set(canalCacheKey, JSON.stringify(canalData), 'EX', 300); // Cache pendant 5 minutes + + console.log('❌ Cache miss - Mise en cache des résultats'); + return json(canalData); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la récupération du canal ou du dernier message' }, { status: 500 }); + } +} + +// Supprimer un canal et invalider le cache associé +export async function DELETE({ params }) { + const canalId = parseInt(params.id); + + try { + // Supprimer le canal de la base de données + await prisma.canal.delete({ + where: { id: canalId }, + }); + + // Invalider le cache + await redisClient.del(`canal:${canalId}:info`); + + return json({ message: 'Canal supprimé avec succès' }); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la suppression du canal' }, { status: 500 }); + } +} + +// Modifier un canal +export async function PUT({ params, request }) { + const canalId = parseInt(params.id); + const { nom, domaine } = await request.json(); // On suppose que ce sont les champs à mettre à jour + + // Clé cache pour les informations du canal et le dernier message + const canalCacheKey = `canal:${canalId}:info`; + + try { + // Mettre à jour les informations du canal dans la base de données + const updatedCanal = await prisma.canal.update({ + where: { id: canalId }, + data: { + nom, // Nom du canal + domaine, // Domaine du canal + }, + include: { + users: true, // Inclut les utilisateurs associés au canal + }, + }); + + // Récupérer le dernier message associé au canal après mise à jour + const lastMessage = await prisma.message.findFirst({ + where: { canalId }, + include: { + user: { select: { id: true, pseudo: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + + // Créer un objet combiné pour les nouvelles informations du canal et le dernier message + const canalData = { + canal: updatedCanal, + lastMessage, // Inclure uniquement le dernier message + }; + + // Mettre en cache les nouvelles informations pendant 5 minutes + await redisClient.set(canalCacheKey, JSON.stringify(canalData), 'EX', 60 * 5); // Cache pendant 5 minutes + + return json(canalData); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la mise à jour du canal' }, { status: 500 }); + } +} diff --git a/src/routes/api/canal/[id]/messages/+page.server.ts b/src/routes/api/canal/[id]/messages/+page.server.ts new file mode 100644 index 0000000..9fd337b --- /dev/null +++ b/src/routes/api/canal/[id]/messages/+page.server.ts @@ -0,0 +1,149 @@ +import { json } from '@sveltejs/kit'; +import prisma from '$lib/prismaClient'; +import redisClient from '$lib/redisClient'; // Assure-toi d'importer ton client Redis + +export async function GET({ params, url }) { + const canalId = parseInt(params.id); + + // Gestion de la pagination avec des paramètres optionnels `page` et `limit` + const page = parseInt(url.searchParams.get('page')) || 1; + const limit = parseInt(url.searchParams.get('limit')) || 10; + const offset = (page - 1) * limit; + + // Générer une clé cache Redis unique en fonction du canal et des paramètres de pagination + const cacheKey = `canal:${canalId}:messages:page:${page}:limit:${limit}`; + + try { + // 1. Vérifier si les messages sont déjà dans le cache Redis + const cachedMessages = await redisClient.get(cacheKey); + if (cachedMessages) { + console.log('✅ Cache hit'); + return json(JSON.parse(cachedMessages)); // Si les données sont en cache, les retourner + } + + // 2. Si les messages ne sont pas en cache, récupérer depuis la base de données + const messages = await prisma.message.findMany({ + where: { canalId }, + include: { + user: { + select: { id: true, pseudo: true }, // Inclut uniquement l’ID et le pseudo de l’utilisateur + }, + }, + orderBy: { + createdAt: 'asc', // Trie par date croissante + }, + skip: offset, + take: limit, + }); + + // 3. Compter le nombre total de messages pour la pagination + const totalMessages = await prisma.message.count({ + where: { canalId }, + }); + + const response = { + messages, + pagination: { + page, + limit, + totalMessages, + totalPages: Math.ceil(totalMessages / limit), + }, + }; + + // 4. Mettre en cache les messages avec une expiration (par exemple 5 minutes) + await redisClient.set(cacheKey, JSON.stringify(response), 'EX', 60 * 5); // Cache pendant 5 minutes + + console.log('❌ Cache miss - Mise en cache des résultats'); + return json(response); // Retourner les données récupérées + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la récupération des messages' }, { status: 500 }); + } +} + +export async function POST({ params, request }) { + const canalId = parseInt(params.id); + const { userId, text } = await request.json(); + + try { + // Créer un nouveau message dans la base de données + const newMessage = await prisma.message.create({ + data: { + userId, + canalId, + text, + }, + include: { user: { select: { id: true, pseudo: true } } }, + }); + + updateCaches(); // Mettre à jour les caches après la création d’un nouveau message + + return json(newMessage, { status: 201 }); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la création du message' }, { status: 500 }); + } +} + +export async function DELETE({ params }) { + const messageId = parseInt(params.id); + + try { + // Supprimer le message de la base de données + await prisma.message.delete({ + where: { id: messageId }, + }); + + updateCaches(); // Mettre à jour les caches après la suppression d’un message + + return json({ message: 'Message supprimé avec succès' }); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la suppression du message' }, { status: 500 }); + } +} + +// Fonction pour mettre à jour tous les caches des messages +function updateCaches(canalId) { + // Mettre à jour tous les caches + // Mettre à jour toutes les pages dans le cache + let page : number = 1; + let limit : number = 10; + let offset : number = (page - 1) * limit; + while (true) { + const cacheKey = `canal:${canalId}:messages:page:${page}:limit:${limit}`; + const cachedMessages = await redisClient.get(cacheKey); + if (!cachedMessages) { + break; + } + const totalMessages = await prisma.message.count({ + where: { canalId }, + }); + const messages = await prisma.message.findMany({ + where: { canalId }, + include: { + user: { + select: { id: true, pseudo: true }, + }, + }, + orderBy: { + createdAt: 'asc', + }, + skip: offset, + take: limit, + }); + const response = { + messages, + pagination: { + page, + limit, + totalMessages, + totalPages: Math.ceil(totalMessages / limit), + }, + }; + await redisClient.set(cacheKey, JSON.stringify(response), 'EX', 60 * 5); + page++; + offset = (page - 1) * limit; + } +} diff --git a/src/routes/api/canals/+page.server.ts b/src/routes/api/canals/+page.server.ts new file mode 100644 index 0000000..b154366 --- /dev/null +++ b/src/routes/api/canals/+page.server.ts @@ -0,0 +1,47 @@ +import { json } from '@sveltejs/kit'; +import prisma from '$lib/prismaClient'; +import redisClient from '$lib/redisClient'; + +// GET: Liste tous les canaux +export async function GET() { + try { + const cachedCanaux = await redisClient.get('canaux'); + if (cachedCanaux) { + console.log('✅ Cache hit'); + return json(JSON.parse(cachedCanaux)); + } + + console.log('❌ Cache miss'); + const canaux = await prisma.canal.findMany({ + include: { users: true, messages: true }, // Inclut les relations + }); + + await redisClient.set('canaux', JSON.stringify(canaux), { EX: 600 }); // Met en cache + return json(canaux); + } catch (err) { + console.error(err); + return json({ error: 'Erreur serveur' }, { status: 500 }); + } +} + +export async function POST({ request }) { + const { nom, domaine, userIds } = await request.json(); + + try { + const canal = await prisma.canal.create({ + data: { + nom, + domaine, + users: { + connect: userIds.map((id) => ({ id })), // Associe des utilisateurs au canal + }, + }, + }); + + return json(canal, { status: 201 }); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la création du canal' }, { status: 500 }); + } +} + diff --git a/src/routes/api/user/[id]/+page.server.ts b/src/routes/api/user/[id]/+page.server.ts new file mode 100644 index 0000000..5052a08 --- /dev/null +++ b/src/routes/api/user/[id]/+page.server.ts @@ -0,0 +1,241 @@ +import { json } from '@sveltejs/kit'; +import redisClient from '$lib/redisClient'; +import prisma from '$lib/prismaClient'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; + +const destinationDir = '/uploads'; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, `.${destinationDir}'); // Dossier où les images sont stockées` + }, + filename: (req, file, cb) => { + cb(null, `${Date.now()}-${file.originalname}`); + }, + fileFilter(req, file, cb) { + const fileExtension = path.extname(file.originalname).toLowerCase(); + if (fileExtension !== '.jpg' && fileExtension !== '.jpeg' && fileExtension !== '.png') { + return cb(new Error('Seules les images JPG, JPEG et PNG sont autorisées.')); + } + cb(null, true); + } +}); + +const upload = multer({ storage }); + +export async function GET({ params }) { + const userId = params.id; + + try { + // Vérifier si l'utilisateur est dans le cache Redis + const cachedUser = await redisClient.get(`user:${userId}`); + if (cachedUser) { + console.log('✅ Cache hit'); + return json(JSON.parse(cachedUser)); + } + + console.log('❌ Cache miss'); + // Si non, récupérer depuis MongoDB via Prisma + const user = await prisma.user.findUnique({ + where: { id: parseInt(userId) }, + }); + + if (!user) { + return json({ error: 'Utilisateur non trouvé' }, { status: 404 }); + } + + // Mettre l'utilisateur en cache + await redisClient.set(`user:${userId}`, JSON.stringify(user), { EX: 3600 }); + + return json(user); + } catch (err) { + console.error(err); + return json({ error: 'Erreur serveur' }, { status: 500 }); + } +} + +export async function POST({ request }) { + return new Promise((resolve, reject) => { + // Utilisation de multer pour récupérer le fichier + upload.single('profilePicture')(request.raw, request.raw, async (err) => { + if (err) { + console.error('Erreur de téléchargement:', err); + return reject(json({ error: 'Erreur lors du téléchargement du fichier' }, { status: 500 })); + } + + // Récupérer les données du formulaire (sans le fichier) + const { pseudo, nom, prenom, email, password } = await request.json(); + + // L'URL de l'image sera le chemin relatif à partir du dossier uploads + const imageUrl = request.file ? `${destinationDir}/${request.file.filename}` : null; + + try { + // Créer un nouvel utilisateur avec l'URL de l'image + const user = await prisma.user.create({ + data: { + pseudo, + nom, + prenom, + email, + password, + profilePictureUrl: imageUrl, // Stocker l'URL de l'image + }, + }); + + // Mettre l'utilisateur dans le cache Redis + await redisClient.set(`user:${user.id}`, JSON.stringify(user), { EX: 3600 }); + + // Réponse avec les données de l'utilisateur + return resolve(json(user, { status: 201 })); + } catch (error) { + console.error('Erreur lors de la création de l\'utilisateur:', error); + return reject(json({ error: 'Erreur lors de la création de l\'utilisateur' }, { status: 500 })); + } + }); + }); +} + +// Mettre à jour un utilisateur avec PUT +export async function PUT({ params, request }) { + const userId = parseInt(params.id); + + const cachedUser = await redisClient.get(`user:${userId}`); + // Récupérer l'utilisateur à partir de la base de données + let existingUser; + + if (cachedUser) { + console.log('✅ Cache hit'); + // Si l'utilisateur est dans le cache, on le parse + existingUser = JSON.parse(cachedUser); + } else { + // Si l'utilisateur n'est pas dans le cache, on le récupère de la base de données + console.log('❌ Cache miss'); + existingUser = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!existingUser) { + return json({ error: 'Utilisateur non trouvé' }, { status: 404 }); + } + + // Utilisation de multer pour récupérer l'image (si présente) + return new Promise((resolve, reject) => { + upload.single('profilePicture')(request.raw, request.raw, async (err) => { + if (err) { + console.error('Erreur de téléchargement:', err); + return reject(json({ error: 'Erreur lors du téléchargement du fichier' }, { status: 500 })); + } + + // Extraire les autres données (pseudo, nom, etc.) du body de la requête + const { pseudo, nom, prenom, email, password } = await request.json(); + + let updatedUserData = { + pseudo, + nom, + prenom, + email, + password, // Assurez-vous de bien sécuriser les mots de passe + }; + + // Si une nouvelle image est envoyée + if (request.file) { + // Vérifiez si l'utilisateur a déjà une image + if (existingUser.profilePictureUrl) { + // Supprimer l'ancienne image + const oldImagePath = `.${destinationDir}/${path.basename(existingUser.profilePictureUrl)}`; + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); // Suppression du fichier + } + } + + // Ajouter la nouvelle image à la base de données + updatedUserData.profilePictureUrl = `${destinationDir}/${request.file.filename}`; + } else if (!request.file && existingUser.profilePictureUrl) { + // Si aucune image n'est envoyée, supprimer l'image actuelle + const oldImagePath = `.${destinationDir}/${path.basename(existingUser.profilePictureUrl)}`; + if (fs.existsSync(oldImagePath)) { + fs.unlinkSync(oldImagePath); // Supprimer l'ancienne image + } + + // Mettre à jour l'URL de l'image en null + updatedUserData.profilePictureUrl = null; + } + + try { + // Mettre à jour l'utilisateur dans la base de données + const updatedUser = await prisma.user.update({ + where: { id: userId }, + data: updatedUserData, + }); + + // Mettre à jour l'utilisateur dans le cache Redis + await redisClient.set(`user:${userId}`, JSON.stringify(updatedUser), 'EX', 3600); // Cache pendant 1 heure (3600 secondes) + + // Réponse avec l'utilisateur mis à jour + return resolve(json(updatedUser)); + } catch (error) { + console.error('Erreur lors de la mise à jour de l\'utilisateur:', error); + return reject(json({ error: 'Erreur lors de la mise à jour de l\'utilisateur' }, { status: 500 })); + } + }); + }); +} + + +export async function DELETE({ params }) { + const userId = parseInt(params.id); + + try { + // Vérifier si l'utilisateur est dans le cache Redis + const cachedUser = await redisClient.get(`user:${userId}`); + let userToDelete; + + if (cachedUser) { + console.log('✅ Cache hit'); + // Si l'utilisateur est dans le cache, on le parse + userToDelete = JSON.parse(cachedUser); + } else { + // Si l'utilisateur n'est pas dans le cache, on le récupère de la base de données + console.log('❌ Cache miss'); + userToDelete = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!userToDelete) { + return json({ error: 'Utilisateur non trouvé' }, { status: 404 }); + } + + // Mettre l'utilisateur dans le cache Redis + await redisClient.set(`user:${userId}`, JSON.stringify(userToDelete), { EX: 3600 }); // Cache pendant 1 heure + } + + // Si l'utilisateur a une image de profil, la supprimer + if (userToDelete.profilePictureUrl) { + // Calculer le chemin du fichier à supprimer + const imagePath = `.${destinationDir}/${path.basename(userToDelete.profilePictureUrl)}`; + if (fs.existsSync(imagePath)) { + fs.unlinkSync(imagePath); // Supprimer le fichier image + } + } + + // Supprimer l'utilisateur de la base de données + await prisma.user.delete({ + where: { id: userId }, + }); + + // Supprimer l'utilisateur du cache Redis + await redisClient.del(`user:${userId}`); + + // Réponse après suppression réussie + return json({ message: 'Utilisateur et image supprimés avec succès' }); + } catch (err) { + console.error(err); + return json({ error: 'Erreur lors de la suppression de l’utilisateur' }, { status: 500 }); + } +} + + + + diff --git a/src/routes/api/users/+page.server.ts b/src/routes/api/users/+page.server.ts new file mode 100644 index 0000000..0d7b259 --- /dev/null +++ b/src/routes/api/users/+page.server.ts @@ -0,0 +1,27 @@ +// src/routes/api/users/+server.js +import { json } from '@sveltejs/kit'; +import redisClient from '$lib/redisClient'; +import prisma from '$lib/prismaClient'; + +export async function GET() { + try { + // Vérifier si les utilisateurs sont dans le cache Redis + const cachedUsers = await redisClient.get('users'); + if (cachedUsers) { + console.log('✅ Cache hit'); + return json(JSON.parse(cachedUsers)); + } + + console.log('❌ Cache miss'); + // Sinon, récupérer les utilisateurs depuis MongoDB + const users = await prisma.user.findMany(); + + // Mettre les utilisateurs en cache + await redisClient.set('users', JSON.stringify(users), { EX: 600 }); + + return json(users); + } catch (err) { + console.error(err); + return json({ error: 'Erreur serveur' }, { status: 500 }); + } +} diff --git a/src/routes/chat/+page.svelte b/src/routes/chat/+page.svelte index 6e7ab5b..eacda00 100644 --- a/src/routes/chat/+page.svelte +++ b/src/routes/chat/+page.svelte @@ -1,3 +1,53 @@ - + + + +
+
+

Créer un nouveau chat

+ + + + + + +
+
+ + +{#if showAlert} + showAlert = false} /> +{/if} + + diff --git a/src/routes/chats/+page.svelte b/src/routes/chats/+page.svelte new file mode 100644 index 0000000..fabf379 --- /dev/null +++ b/src/routes/chats/+page.svelte @@ -0,0 +1,32 @@ + + +
+
+
+
+ +
+
+ +
+ +
+ + +
+
+ + \ No newline at end of file diff --git a/src/routes/user/edit/+page.svelte b/src/routes/user/edit/+page.svelte new file mode 100644 index 0000000..ce54044 --- /dev/null +++ b/src/routes/user/edit/+page.svelte @@ -0,0 +1,140 @@ + + +
+
+

Modifier les informations du compte

+ + + {#if showMessage} +
+ {message} +
+ {/if} + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + {#if profilePicture} +
Image sélectionnée : {profilePicture.name}
+ {/if} +
+ +
+ +
+
+
+
+ + diff --git a/static/profile-default.svg b/static/profile-default.svg new file mode 100644 index 0000000..726ac6f --- /dev/null +++ b/static/profile-default.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file