Merge pull request #10 from NabilOuldHamou/dev

dev
This commit is contained in:
Nabil Ould Hamou 2024-12-18 09:41:04 +01:00 committed by GitHub
commit ad28aa3b28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 3524 additions and 2804 deletions

4
.gitignore vendored
View file

@ -1,11 +1,15 @@
node_modules
/logs
# Output
.output
.vercel
/.svelte-kit
/build
.idea
# OS
.DS_Store
Thumbs.db

6
.idea/vcs.xml generated Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
# Build stage
FROM node:18.18-alpine
WORKDIR /app
COPY . .
RUN npm i -g pnpm
EXPOSE 3000
RUN chmod +x /app/docker_entrypoint.sh
ENTRYPOINT ["/app/docker_entrypoint.sh"]

View file

@ -1,53 +1,77 @@
## Presentation
<h1 align="center" style="font-weight: bold;">Projet Web - M1 ISA 💻</h1>
Creation of a project that use two or more NoSQL technologie: Redis & MangoDB
<p align="center">
<a href="#tech">Technologies</a> - <a href="#started">Lancer le projet</a> - <a href="#auth">Auteurs</a>
</p>
Front: JavaScript
<p align="center">Projet réalisé dans le cadre du cours de <strong>Web languages for data storage and management</strong>.</p>
Back : Java (SpringBoot)
<h2 id="technologies">💻 Technologies</h2>
Api to Server Mongo (Nabil's Server)
- SvelteKit
- MongoDB
- Redis
- Docker
Update Redis Database
<h2 id="started">🚀 Lancer le projet</h2>
Afin de lancer le projet en local, vous aurez besoin de suivre les instructions suivantes
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
<h3>Clonez le dépôt</h3>
Commencez par cloner le dépôt Git sur votre machine
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
git clone https://github.com/NabilOuldHamou/M1_ProjetWeb
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
<h3>Production</h3>
Installez **Docker** sur votre machine puis utilisez la commande suivante pour lancer le projet
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
docker compose up
```
## Building
<h3>Développement</h3>
TODO
To create a production version of your app:
<h2 id="auth">🤝 Auteurs</h2>
```bash
npm run build
```
<table>
<tr>
<td align="center">
<a href="https://github.com/NabilOuldHamou">
<img src="https://github.com/NabilOuldHamou.png" width="100px;" alt="Nabil Profile Picture"/><br>
<sub>
<b>Nabil Ould Hamou</b>
</sub>
</a>
</td>
You can preview the production build with `npm run preview`.
<td align="center">
<a href="https://github.com/Luxray555">
<img src="https://github.com/Luxray555.png" width="100px;" alt="Bilal Profile Picture"/><br>
<sub>
<b>Bilal Dieumegard</b>
</sub>
</a>
</td>
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
<td align="center">
<a href="https://github.com/YacineHB">
<img src="https://github.com/YacineHB.png" width="100px;" alt="Yacine Profile Picture"/><br>
<sub>
<b>Yacine Hbada</b>
</sub>
</a>
</td>
<td align="center">
<a href="https://github.com/Yanax373">
<img src="https://github.com/Yanax373.png" width="100px;" alt="Yanis Profile Picture"/><br>
<sub>
<b>Yanis Bouarfa</b>
</sub>
</a>
</td>
</tr>
</table>

14
components.json Normal file
View file

@ -0,0 +1,14 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils"
},
"typescript": true
}

59
docker-compose.yml Normal file
View file

@ -0,0 +1,59 @@
services:
app:
build: .
hostname: app
depends_on:
- mongodb
- redis
environment:
- DATABASE_URL=mongodb://temp-root-username:temp-password@mongodb/chat_projetweb?authSource=admin
- JWT_SECRET=ba63466f102443f4bb6f3670891358bc4488d0c717f6ebcd3ee3c5144e55fe2d
ports:
- "3000:3000"
networks:
- app_network
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules
mongodb:
build: ./mongodb_rs
hostname: mongodb
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=temp-root-username
- MONGO_INITDB_ROOT_PASSWORD=temp-password
- MONGO_INITDB_DATABASE=chat_projetweb
- MONGO_REPLICA_HOST=mongodb
- MONGO_REPLICA_PORT=27017
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db/
- mongo-logs:/var/log/mongodb/
networks:
- app_network
redis:
image: redis:latest
hostname: redis-server
restart: always
ports:
- "6379:6379"
command: redis-server --save 20 1 --loglevel warning
environment:
- REDIS_PASSWORD=temp-redis-password
volumes:
- redis-data:/root/redis
networks:
- app_network
networks:
app_network:
driver: bridge
volumes:
mongo-data:
mongo-logs:
redis-data:

13
docker_entrypoint.sh Normal file
View file

@ -0,0 +1,13 @@
#!/bin/sh
set -xe
pnpm install
pnpm prisma generate
pnpm run build
pnpx prisma db push
ls
node build

11
mongodb_rs/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM mongo:5
# we take over the default & start mongo in replica set mode in a background task
ENTRYPOINT mongod --port $MONGO_REPLICA_PORT --replSet rs0 --bind_ip 0.0.0.0 & MONGOD_PID=$!; \
# we prepare the replica set with a single node and prepare the root user config
INIT_REPL_CMD="rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: '$MONGO_REPLICA_HOST:$MONGO_REPLICA_PORT' }] })"; \
INIT_USER_CMD="db.getUser('$MONGO_INITDB_ROOT_USERNAME') || db.createUser({ user: '$MONGO_INITDB_ROOT_USERNAME', pwd: '$MONGO_INITDB_ROOT_PASSWORD', roles: ['root'] })"; \
# we wait for the replica set to be ready and then submit the command just above
until (mongo admin --port $MONGO_REPLICA_PORT --eval "$INIT_REPL_CMD && $INIT_USER_CMD"); do sleep 1; done; \
# we are done but we keep the container by waiting on signals from the mongo task
echo "REPLICA SET ONLINE"; wait $MONGOD_PID;

View file

@ -16,6 +16,8 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"autoprefixer": "^10.4.20",
"bits-ui": "^0.21.16",
"clsx": "^2.1.1",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
@ -26,9 +28,27 @@
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.0",
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
"typescript-eslint": "^8.0.0"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"@sveltejs/adapter-node": "^5.2.10",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.1",
"argon2": "^0.41.1",
"jsonwebtoken": "^9.0.2",
"lucide-svelte": "^0.462.0",
"multer": "^1.4.5-lts.1",
"prisma": "^5.22.0",
"redis": "^4.7.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"svelte-radix": "^2.0.1",
"vite": "^5.0.3",
"winston": "^3.17.0"
}
}

2750
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

49
prisma/schema.prisma Normal file
View file

@ -0,0 +1,49 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
binaryTargets = ["debian-openssl-1.1.x", "linux-musl-openssl-3.0.x", "linux-musl-arm64-openssl-3.0.x"]
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
username String @unique
profilePicture String @default("default.png")
surname String
name String
email String @unique
password String
messages Message[]
@@map("users") // Table name in DB
}
model Channel {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String @unique
messages Message[]
createdAt DateTime @default(now())
@@map("channels") // Table name in DB
}
model Message {
id String @id @default(auto()) @map("_id") @db.ObjectId
user User @relation(fields: [userId], references: [id])
userId String @db.ObjectId
channel Channel @relation(fields: [channelId], references: [id])
channelId String @db.ObjectId
text String
createdAt DateTime @default(now())
@@map("messages") // Table name in DB
}

View file

@ -1,3 +1,78 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 210 40% 98%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

5
src/app.d.ts vendored
View file

@ -3,7 +3,10 @@
declare global {
namespace App {
// interface Error {}
// interface Locals {}
interface Locals {
token?: string;
userId?: string;
}
// interface PageData {}
// interface PageState {}
// interface Platform {}

8
src/hooks.server.ts Normal file
View file

@ -0,0 +1,8 @@
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
event.locals.token = await event.cookies.get('token');
event.locals.userId = await event.cookies.get('UID');
return await resolve(event);
};

View file

@ -0,0 +1,122 @@
<script lang="ts">
import * as Card from "$lib/components/ui/card";
import { formatDistanceToNow } from '$lib/utils/date.js';
import { onMount } from "svelte";
import ProfileInfo from "$lib/components/ui/ProfileInfo.svelte"; // Importer le composant ProfileInfo
export let userId: string; // Si c'est le message de l'utilisateur courant
export let message = null; // Contenu du message
export let setActiveProfile;
export let activeProfileId = null;
let user = null;
let myMessage;
async function fetchUser() {
const res = await fetch(`/api/users/${message.user.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const data = await res.json();
user = data;
}
function toggleProfileInfo() {
if (activeProfileId === message.id) {
// Si le profil cliqué est déjà actif, le fermer
setActiveProfile(null);
} else {
// Sinon, afficher ce profil et masquer les autres
setActiveProfile(message.id);
}
}
let timeElapsed: string;
// Fonction pour mettre à jour le temps écoulé
const updateElapsed = () => {
timeElapsed = formatDistanceToNow(message.createdAt);
};
// Initialisation de l'intervalle
onMount(() => {
fetchUser();
updateElapsed(); // Calcul initial
const interval = setInterval(updateElapsed, 1000); // Mise à jour toutes les secondes
myMessage = message.user.id === userId; // Vérifier si c'est le message de l'utilisateur courant
return () => {
clearInterval(interval); // Nettoyage lors du démontage
};
});
</script>
{#if user !== null}
<Card.Root class="relative">
<Card.Header
class="flex items-center justify-between {myMessage ? 'flex-row' : 'flex-row-reverse'}"
>
<!-- Conteneur pour la date -->
<span class="text-xs sm:text-sm md:text-base text-gray-500 items-top">
{timeElapsed}
</span>
<!-- Conteneur pour l'image et le nom d'utilisateur -->
<div class="flex items-center gap-3 {myMessage ? 'flex-row-reverse' : 'flex-row'}">
<div
class="relative"
on:click={toggleProfileInfo}
>
<!-- Image de profil -->
<img
src={`http://localhost:5173/${user.profilePicture}`}
alt="Profile Picture"
class="h-10 w-10 rounded-full border border-gray-300 cursor-pointer"
/>
<!-- Infos du profil (affichées au survol) -->
<ProfileInfo user={user} show={activeProfileId === message.id} position={myMessage} />
</div>
<div class="flex flex-col text-right {myMessage ? 'text-right' : 'text-left'}">
<Card.Title
class="text-gray-800 text-sm sm:text-base md:text-lg truncate {myMessage ? 'font-bold' : ''}"
>
{myMessage ? "(Moi)" : ""} {user.username}
</Card.Title>
</div>
</div>
</Card.Header>
<!-- Contenu du message -->
<Card.Content class="text-sm sm:text-base md:text-lg text-gray-700">
<p>{message.text}</p>
</Card.Content>
</Card.Root>
{/if}
<style>
img {
object-fit: cover; /* Assure un bon rendu des images */
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
</style>

View file

@ -0,0 +1,66 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { fade, fly } from 'svelte/transition'; // Importer fade et fly
export let message: string = ""; // Le message d'alerte
export let onClose: () => void = () => {}; // Fonction de fermeture de l'alerte
export let duration: number = 5000;
export let show = false;
// Fonction pour fermer l'alerte
const closeAlert = () => {
onClose();
};
// Gestion du timeout pour fermer l'alerte après un certain délai
let timeout: NodeJS.Timeout;
$: {
if (show) {
timeout = setTimeout(() => {
closeAlert(); // Fermer l'alerte après la durée
}, duration);
} else {
clearTimeout(timeout); // Si l'alerte est fermée, on annule le timeout
}
}
// Nettoyage du timeout si le composant est démonté avant la fin du délai
onDestroy(() => {
clearTimeout(timeout);
});
</script>
{#if show}
<div class="alert fixed top-4 right-4 bg-blue-500 text-white px-6 py-3 rounded-lg shadow-lg"
in:fly={{ y: -20, opacity: 0, duration: 300 }}
out:fly={{ y: -20, opacity: 0, duration: 300 }}>
<span>{message}</span>
</div>
{/if}
<style>
/* Styles de l'alerte */
.alert {
position: absolute;
top: 20px;
right: 20px;
background-color: #3182ce;
color: white;
padding: 10px 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
button {
background: transparent;
border: none;
color: white;
font-size: 1.2em;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,40 @@
<script lang="ts">
import { formatDistanceToNow } from '$lib/utils/date';
import { onMount } from 'svelte';
export let id: string; // ID du chat
export let title: string; // Nom ou titre du chat
export let lastMessage: string; // Dernier message affiché
export let createdAt: string; // Heure du dernier message
let timeElapsed: string;
// Fonction pour mettre à jour le temps écoulé
const updateElapsed = () => {
timeElapsed = formatDistanceToNow(createdAt);
};
// Initialisation de l'intervalle
onMount(() => {
updateElapsed(); // Calcul initial
const interval = setInterval(updateElapsed, 1000); // Mise à jour toutes les secondes
return () => {
clearInterval(interval); // Nettoyage lors du démontage
};
});
</script>
<a href={`/chats/${id}`} class="chat-item p-4 border rounded-md hover:bg-gray-100 cursor-pointer flex justify-between items-center">
<div>
<p class="font-semibold text-lg">{title}</p>
<p class="text-sm text-gray-500 truncate">{lastMessage}</p>
</div>
<p class="text-xs text-gray-400">{timeElapsed}</p>
</a>
<style>
.chat-item {
transition: background-color 0.2s ease-in-out;
}
</style>

View file

@ -0,0 +1,112 @@
<script lang="ts">
import { onMount } from 'svelte';
export let profilePicture: File | null = null;
export let defaultImage = '/profile-default.svg'; // Image par défaut si aucune image n'est sélectionnée
let clientPicture: string | null = profilePicture ? `/${profilePicture}` : defaultImage;
// Fonction exécutée lorsque l'utilisateur sélectionne une image
const handleFileChange = (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files?.length) {
profilePicture = input.files[0]; // Affectation du fichier sélectionné
clientPicture = URL.createObjectURL(profilePicture); // Prévisualisation de l'image
} else {
clientPicture = null;
profilePicture = null;
}
};
const handleDelete = () => {
profilePicture = null;
};
</script>
<!-- Conteneur principal -->
<div class="container">
<!-- Image de profil ou image par défaut -->
<img
src={clientPicture}
alt="Image de profil"
class="image-preview mb-10"
/>
<!-- Sélectionner une image -->
<label for="profilePicture" class="file-upload-btn">
Sélectionner une image
<input
type="file"
id="profilePicture"
class="file-input"
accept="image/*"
on:change={handleFileChange}
/>
</label>
<div class="action-buttons">
<!-- Bouton Supprimer l'image -->
<button
type="button"
class="bg-red-500 text-white px-4 py-2 rounded-lg hover:bg-red-600"
on:click={handleDelete}
disabled={!profilePicture}
>
Supprimer l'image
</button>
</div>
</div>
<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
text-align: center;
}
.file-upload-btn {
position: relative;
display: inline-block;
background-color: #3182ce;
color: white;
font-size: 16px;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 10px;
}
.file-upload-btn:hover {
background-color: #2563eb;
}
.file-upload-btn:active {
background-color: #1e40af;
}
.file-input {
display: none;
}
.image-preview {
margin-top: 20px;
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 50%; /* Arrondir l'image en cercle */
border: 4px solid #3182ce; /* Bordure autour de l'image */
}
.action-buttons {
margin-top: 20px;
}
.action-buttons button:disabled {
background-color: #cccccc;
cursor: not-allowed;
}
</style>

View file

@ -0,0 +1,97 @@
<script lang="ts">
import Alert from "$lib/components/ui/Alert.svelte"; // Importer le composant Alert
export let show = false;
export let listRef: HTMLElement | null = null;
export let onClose: () => void; // Callback pour fermer le composant
let showAlert = false;
let alertMessage = "";
export let socket;
let chatName = "";
const createChat = async () => {
if (chatName.trim()) {
try {
// Appel API pour créer le chat
const response = await fetch('/api/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: chatName }),
});
if (response.ok) {
const data = await response.json();
alertMessage = `Le chat "${data.name}" a été créé avec succès.`;
chatName = ""; // Réinitialiser
socket.emit("new-channel", data);
if (listRef) {
listRef.scrollTo({ top: 0, behavior: 'smooth' });
}
onClose?.(); // Fermer le composant après création
} else {
response.json().then(error => {
alertMessage = error.error;
});
}
} catch (err) {
alertMessage = err;
}
showAlert = true;
} else {
alertMessage = "Veuillez entrer un nom pour le chat.";
showAlert = true;
}
};
// Fonction pour détecter le clic en dehors
let createChatRef: HTMLElement | null = null;
</script>
{#if show}
<div class="fixed inset-0 flex justify-center items-center bg-black bg-opacity-50 z-50" on:click={onClose}>
<div
class="bg-white border border-gray-300 rounded-lg p-8 w-96"
bind:this={createChatRef}
on:click|stopPropagation
>
<h1 class="text-2xl font-bold mb-6 text-center">Créer un nouveau chat</h1>
<input
type="text"
bind:value={chatName}
placeholder="Nom du chat..."
class="w-full border border-gray-300 rounded-lg p-2 focus:outline-none focus:ring focus:border-blue-500 mb-4"
/>
<button
on:click={createChat}
class="bg-blue-500 text-white w-full px-4 py-2 rounded-lg hover:bg-blue-600 focus:outline-none"
>
Créer
</button>
</div>
</div>
{/if}
<Alert show={showAlert} message={alertMessage} onClose={() => (showAlert = false)} />
<style>
.fixed {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.bg-white {
background-color: white;
}
</style>

View file

@ -0,0 +1,139 @@
<script>
import Button from '$lib/components/ui/button/button.svelte';
export let user;
export let userSessionId;
export let show = false; // Contrôle si la carte est visible
export let onClose = () => {}; // Fonction pour fermer la carte
const disconnect = async () => {
try {
const response = await fetch('/disconnect', { method: 'POST' });
if (response.redirected) {
window.location.href = response.url;
}
} catch (error) {
console.error('Erreur lors de la déconnexion:', error);
}
};
const editProfile = () => {
window.location.href = '/user/edit';
};
</script>
{#if show}
<div class="overlay" role="dialog" aria-labelledby="profile-card-title" on:click={onClose}>
<div class="profile-card flex flex-col gap-5" on:click|stopPropagation>
<div class="profile-header">
<img src="http://localhost:5173/{user.profilePicture}" alt="Profile" class="profile-image" />
<h2 id="profile-card-title" class="profile-name">{user.username}</h2>
</div>
<div class="profile-info">
<div class="info-row">
<span class="info-label">Nom :</span>
<span class="info-value">{user.surname}</span>
</div>
<div class="info-row">
<span class="info-label">Prénom :</span>
<span class="info-value">{user.name}</span>
</div>
<div class="info-row">
<span class="info-label">Email :</span>
<span class="info-value">{user.email}</span>
</div>
</div>
{#if user.id === userSessionId}
<div class="actions">
<Button on:click={editProfile}>Éditer</Button>
<Button on:click={disconnect} variant="destructive">Déconnexion</Button>
</div>
{/if}
</div>
</div>
{/if}
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.profile-card {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
text-align: left;
width: 400px;
max-width: 90%;
}
.profile-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 20px;
}
.profile-image {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
margin-right: 15px;
}
.profile-name {
font-size: 1.8rem;
font-weight: bold;
color: #333;
}
.profile-info {
margin: 20px 0;
padding: 15px;
background-color: #f9f9f9;
border-radius: 10px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #eaeaea;
}
.info-row:last-child {
border-bottom: none;
}
.info-label {
font-weight: 600;
color: #555;
font-size: 0.9rem;
}
.info-value {
font-weight: 400;
color: #333;
font-size: 0.95rem;
}
.actions {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 15px;
}
</style>

View file

@ -0,0 +1,65 @@
<script>
export let user; // Infos utilisateur
export let position = 'right'; // Position de la photo de profil: 'left' ou 'right'
export let show = false; // Afficher ou masquer les infos utilisateur
</script>
{#if show}
<div class="user-info {position ? 'left' : 'right' }">
<h2 class="text-lg font-semibold">{user.username}</h2>
<p class="text-sm text-gray-500">{user.email}</p>
<p class="text-sm text-gray-500">{user.name} {user.surname}</p>
</div>
{/if}
<style>
.user-info {
position: absolute;
top: 0; /* Aligner en haut */
left: auto;
right: auto;
width: 200px; /* Largeur par défaut */
background: rgba(255, 255, 255, 0.95); /* Fond semi-transparent */
padding: 15px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 10; /* Gérer les priorités d'affichage */
transition: all 0.3s ease-in-out; /* Animation fluide pour les changements */
}
.user-info.left {
right: 110%; /* Position à gauche avec un espace */
}
.user-info.right {
left: 110%; /* Position à droite avec un espace */
}
/* Media Queries pour adapter le style */
@media (max-width: 768px) {
.user-info {
width: 150px; /* Réduire la largeur sur les écrans plus petits */
padding: 10px; /* Réduire le padding */
font-size: 0.9rem; /* Réduire la taille de la police */
}
}
@media (max-width: 480px) {
.user-info {
position: fixed; /* Position fixe pour éviter les débordements */
left: 10px; /* Centrer avec un padding interne */
right: 10px;
width: auto; /* Utiliser toute la largeur disponible */
max-width: 90%; /* Limiter la largeur pour éviter les débordements */
padding: 8px; /* Réduire le padding davantage */
font-size: 0.8rem; /* Police encore plus petite */
}
.user-info.left,
.user-info.right {
left: 10px; /* Ignorer les positionnements relatifs */
right: 10px;
}
}
</style>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Search } from "lucide-svelte"; // Icône de recherche depuis Lucide Svelte
export let placeholder: string = "Rechercher..."; // Texte par défaut pour l'input
export let onChange: (value: string) => void = () => {}; // Callback pour la recherche
</script>
<div class="w-full">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-5 w-5"/>
<input
type="text"
placeholder={placeholder}
class="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring focus:border-blue-300"
on:input={(event) => onChange(event.target.value)}
/>
</div>

View file

@ -0,0 +1,82 @@
<script lang="ts">
export let user;
export let openProfileCard = () => {};
</script>
<div
class="flex items-center gap-4 justify-between p-3 cursor-pointer hover:bg-gray-100 rounded-lg border border-gray-300 shadow-sm"
on:click={openProfileCard}
>
<div class="flex items-center gap-4">
<img
src={`http://localhost:5173/${user.profilePicture}`}
alt="Profile"
class="h-12 w-12 rounded-full border border-gray-300"
/>
<div class="flex flex-col">
<span class="font-medium text-gray-800">{user.username}</span>
<div class="flex items-center gap-1">
<span class="text-xs text-gray-500">{user.state}</span>
</div>
</div>
</div>
{#if user.state === "En ligne"}
<div class="online-indicator"></div>
{:else if user.state === "Ecrit"}
<div class="typing-animation">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
{/if}
</div>
<style>
/* Ajout d'une animation subtile lors du survol */
div:hover {
background-color: #f3f4f6;
transition: background-color 0.2s ease-in-out;
}
/* Styles pour le point vert (En ligne) */
.online-indicator {
width: 12px;
height: 12px;
background-color: #22c55e; /* Couleur verte */
border-radius: 50%;
box-shadow: 0 0 4px rgba(34, 197, 94, 0.6); /* Glow léger */
}
/* Styles pour l'animation des trois points */
.typing-animation {
display: flex;
align-items: center;
gap: 4px;
}
.dot {
width: 6px;
height: 6px;
background-color: #3b82f6; /* Couleur bleue */
border-radius: 50%;
animation: bounce 1.2s infinite ease-in-out;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
</style>

View file

@ -0,0 +1,25 @@
<script lang="ts">
import { Button as ButtonPrimitive } from "bits-ui";
import { type Events, type Props, buttonVariants } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = Props;
type $$Events = Events;
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export let size: $$Props["size"] = "default";
export let builders: $$Props["builders"] = [];
export { className as class };
</script>
<ButtonPrimitive.Root
{builders}
class={cn(buttonVariants({ variant, size, className }))}
type="button"
{...$$restProps}
on:click
on:keydown
>
<slot />
</ButtonPrimitive.Root>

View file

@ -0,0 +1,49 @@
import { type VariantProps, tv } from "tailwind-variants";
import type { Button as ButtonPrimitive } from "bits-ui";
import Root from "./button.svelte";
const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
type Variant = VariantProps<typeof buttonVariants>["variant"];
type Size = VariantProps<typeof buttonVariants>["size"];
type Props = ButtonPrimitive.Props & {
variant?: Variant;
size?: Size;
};
type Events = ButtonPrimitive.Events;
export {
Root,
type Props,
type Events,
//
Root as Button,
type Props as ButtonProps,
type Events as ButtonEvents,
buttonVariants,
};

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("p-6", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLParagraphElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<p class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
<slot />
</p>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex items-center p-6 pt-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-1.5 p-6 pb-0", className)} {...$$restProps}>
<slot />
</div>

View file

@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
tag?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let tag: $$Props["tag"] = "h3";
export { className as class };
</script>
<svelte:element
this={tag}
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>

View file

@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("bg-card text-card-foreground rounded-lg border shadow-sm", className)}
{...$$restProps}
>
<slot />
</div>

View file

@ -0,0 +1,24 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
};
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";

View file

@ -0,0 +1,29 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View file

@ -0,0 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<input
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View file

@ -0,0 +1,7 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Label as LabelPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = LabelPrimitive.Props;
type $$Events = LabelPrimitive.Events;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<LabelPrimitive.Root
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
on:mousedown
>
<slot />
</LabelPrimitive.Root>

View file

@ -0,0 +1,18 @@
import { Tabs as TabsPrimitive } from "bits-ui";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
const Root = TabsPrimitive.Root;
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View file

@ -0,0 +1,21 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<TabsPrimitive.Content
class={cn(
"ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
className
)}
{value}
{...$$restProps}
>
<slot />
</TabsPrimitive.Content>

View file

@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.ListProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<TabsPrimitive.List
class={cn(
"bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1",
className
)}
{...$$restProps}
>
<slot />
</TabsPrimitive.List>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
type $$Props = TabsPrimitive.TriggerProps;
type $$Events = TabsPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<TabsPrimitive.Trigger
class={cn(
"ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm",
className
)}
{value}
{...$$restProps}
on:click
>
<slot />
</TabsPrimitive.Trigger>

View file

@ -0,0 +1,28 @@
import Root from "./textarea.svelte";
type FormTextareaEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLTextAreaElement;
};
type TextareaEvents = {
blur: FormTextareaEvent<FocusEvent>;
change: FormTextareaEvent<Event>;
click: FormTextareaEvent<MouseEvent>;
focus: FormTextareaEvent<FocusEvent>;
keydown: FormTextareaEvent<KeyboardEvent>;
keypress: FormTextareaEvent<KeyboardEvent>;
keyup: FormTextareaEvent<KeyboardEvent>;
mouseover: FormTextareaEvent<MouseEvent>;
mouseenter: FormTextareaEvent<MouseEvent>;
mouseleave: FormTextareaEvent<MouseEvent>;
paste: FormTextareaEvent<ClipboardEvent>;
input: FormTextareaEvent<InputEvent>;
};
export {
Root,
//
Root as Textarea,
type TextareaEvents,
type FormTextareaEvent,
};

View file

@ -0,0 +1,38 @@
<script lang="ts">
import type { HTMLTextareaAttributes } from "svelte/elements";
import type { TextareaEvents } from "./index.js";
import { cn } from "$lib/utils.js";
type $$Props = HTMLTextareaAttributes;
type $$Events = TextareaEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
<textarea
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

21
src/lib/logger.ts Normal file
View file

@ -0,0 +1,21 @@
import winston from 'winston';
const { combine, timestamp, json, errors } = winston.format;
const logger = winston.createLogger({
levels: winston.config.syslog.levels,
format: combine(
errors({ stack: true }),
timestamp(),
json(),
),
transports: [
new winston.transports.Console({level: "debug"}),
new winston.transports.File({
filename: 'logs/app.log',
level: 'debug'
})
]
});
export default logger;

4
src/lib/prismaClient.ts Normal file
View file

@ -0,0 +1,4 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default prisma;

16
src/lib/redisClient.ts Normal file
View file

@ -0,0 +1,16 @@
import { createClient } from 'redis';
const client = await createClient({
url: process.env.REDIS_URL || 'redis://redis-server:6379'
});
client.on('error', (err) => console.error('Redis Error:', err));
try {
await client.connect();
} catch (error) {
console.error('Redis Error:', error);
}
export default client;

View file

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
export const messagesStore = writable<string[]>([]);

9
src/lib/stores/socket.ts Normal file
View file

@ -0,0 +1,9 @@
import { io } from "socket.io-client";
// Initialisation de la socket
export const initSocket = () => {
const socketInstance = io("http://localhost:5173");
let socketId = null;
return socketInstance
}

62
src/lib/utils.ts Normal file
View file

@ -0,0 +1,62 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { cubicOut } from "svelte/easing";
import type { TransitionConfig } from "svelte/transition";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === "none" ? "" : style.transform;
const scaleConversion = (
valueA: number,
scaleA: [number, number],
scaleB: [number, number]
) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (
style: Record<string, number | string | undefined>
): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, "");
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

51
src/lib/utils/date.ts Normal file
View file

@ -0,0 +1,51 @@
export function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false, // format 24 heures
});
}
//fonction qui renvoie le temps depuis la créetion du message à maintenant en fonction du temps on affichera le temps en secondes, minutes, heures, jours, semaines, mois, années
export function formatDistanceToNow(dateString) {
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
const seconds = Math.floor(diff / 1000);
if (seconds < 60) {
return `${seconds} s`;
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours} h`;
}
const days = Math.floor(hours / 24);
if (days < 7) {
return `${days} j`;
}
const weeks = Math.floor(days / 7);
if (weeks < 4) {
return `${weeks} sem`;
}
const months = Math.floor(days / 30);
if (months < 12) {
return `${months} mois`;
}
const years = Math.floor(months / 12);
return `${years} ans`;
}

12
src/lib/utils/sort.ts Normal file
View file

@ -0,0 +1,12 @@
export function sortChannels(channels) {
channels = channels.map((channel) => {
return {
...channel,
lastUpdate : channel.lastMessage != null ? channel.lastMessage.createdAt : channel.createdAt
};
});
return channels.sort((a, b) => {
return new Date(b.lastUpdate) - new Date(a.lastUpdate);
});
}

View file

@ -0,0 +1,24 @@
import { redirect } from '@sveltejs/kit';
export async function load({ locals, url, fetch }) {
const token = locals.token;
if (token == undefined && url.pathname !== "/") {
redirect(301, '/');
}
let user = null;
if (locals.userId !== undefined) {
const res = await fetch(`/api/users/${locals.userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
user = await res.json();
}
return { token, user }
}

View file

@ -3,4 +3,6 @@
let { children } = $props();
</script>
<main class="h-screen">
{@render children()}
</main>

View file

@ -0,0 +1,77 @@
import { type Actions } from '@sveltejs/kit';
import { redirect, error, fail } from '@sveltejs/kit';
import logger from '$lib/logger';
export async function load({locals}) {
if (locals.token != undefined) {
redirect(302, "/chats")
}
}
export const actions: Actions = {
login: async ({request, fetch, cookies}) => {
const formData = await request.formData();
const response = await fetch('/api/auth/login', {
method: "POST",
body: formData
});
const data = await response.json();
if (response.ok) {
cookies.set('token', data.token, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: (60 * 60) * 30,
});
cookies.set('UID', data.userId, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: (60 * 60) * 30,
});
logger.debug("Successfully created a cookie for the user and proceeded with the login.");
return redirect(302, "/chats");
} else {
return fail(400, { error: data.message });
}
},
register: async ({request, fetch, cookies}) => {
const formData = await request.formData();
const response = await fetch('/api/auth/register', {
method: "POST",
body: formData
});
const data = await response.json();
if (response.ok) {
cookies.set('token', data.token, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: (60 * 60) * 30,
});
cookies.set('UID', data.userId, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: (60 * 60) * 30,
});
logger.debug("Successfully created a cookie for the user and proceeded with the register.")
return redirect(302, "/chats");
} else {
return fail(400, { error: data.message });
}
}
}

View file

@ -1,2 +1,101 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import Alert from "$lib/components/ui/Alert.svelte";
import { Label } from "$lib/components/ui/label";
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import * as Card from "$lib/components/ui/card";
import { enhance } from '$app/forms';
import * as Tabs from "$lib/components/ui/tabs";
let { data, form } = $props();
import { writable } from 'svelte/store';
const showAlert = writable(false);
const alertMessage = writable("");
$effect(() => {
// Manipuler l'état via les stores, ce qui est plus réactif
if (form?.error) {
alertMessage.set(form.error);
showAlert.set(true);
} else {
showAlert.set(false);
}
});
let random = Math.trunc(Math.random() * 10);
</script>
<div class="w-full h-full flex justify-center items-center">
<Tabs.Root value="login" class="w-[450px]">
<Tabs.List class="grid w-full grid-cols-2">
<Tabs.Trigger value="login">Se connecter</Tabs.Trigger>
<Tabs.Trigger value="register">S'inscrire</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="login">
<Card.Root>
<Card.Header>
{#if random > 8}
<Card.Title>🌳 - Arabes</Card.Title>
{:else}
<Card.Title>🌳 - Arbres</Card.Title>
{/if}
<Card.Description>Connectez vous pour chatter!</Card.Description>
</Card.Header>
<form method="POST" action="?/login" use:enhance>
<Card.Content>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Adresse email</Label>
<Input type="text" name="email" id="email" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="password">Mot de passe</Label>
<Input type="password" name="password" id="password" />
</div>
</Card.Content>
<Card.Footer>
<Button type="submit">Se connecter</Button>
</Card.Footer>
</form>
</Card.Root>
</Tabs.Content>
<Tabs.Content value="register">
<Card.Root>
<Card.Header>
{#if random > 8}
<Card.Title>🌳 - Arabes</Card.Title>
{:else}
<Card.Title>🌳 - Arbres</Card.Title>
{/if}
<Card.Description>Inscrivez-vous pour chatter!</Card.Description>
</Card.Header>
<form method="POST" action="?/register" use:enhance>
<Card.Content>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Label for="username">Nom d'utilisateur</Label>
<Input type="text" name="username" id="username" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="email">Adresse email</Label>
<Input type="text" name="email" id="email" />
</div>
<div class="pt-4 grid w-full max-w-sm items-center gap-1.5">
<Label for="password">Mot de passe</Label>
<Input type="password" name="password" id="password" />
</div>
</Card.Content>
<Card.Footer>
<Button type="submit">S'inscrire</Button>
</Card.Footer>
</form>
</Card.Root>
</Tabs.Content>
</Tabs.Root>
</div>
<Alert message={$alertMessage} show={$showAlert} onClose={() => ($showAlert = false)} />

View file

@ -0,0 +1,46 @@
import prismaClient from '$lib/prismaClient';
import { error, json } from '@sveltejs/kit';
import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import logger from '$lib/logger';
export async function POST({request}) {
const formData = await request.formData();
// @ts-ignore
const email: string = formData.get('email').toString();
// @ts-ignore
const password: string = formData.get('password').toString();
const user = await prismaClient.user.findFirst({
where: {
email: email,
}
});
if (user == null) {
logger.debug(`Could not find user with email (${email}) in database`);
return error(400, {message: "Email ou mot de passe invalide."});
}
logger.debug(`Found user with email (${email}) in database`);
try {
if (await argon2.verify(user.password, password)) {
logger.debug(`Password for user ${user.email} is correct.`);
// @ts-ignore
const token = jwt.sign(user, process.env.JWT_SECRET, { expiresIn: "1h" });
logger.debug(`Generated a JWT token for user ${user.email}.`)
return json({token: token, userId: user.id});
} else {
return error(400, {message: "Email ou mot de passe invalide."});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
logger.error(e);
return error(500, {message: e.body.message});
}
}

View file

@ -0,0 +1,55 @@
import prismaClient from '$lib/prismaClient';
import { error, json } from '@sveltejs/kit';
import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import logger from '$lib/logger';
export async function POST({request}) {
const formData = await request.formData();
// @ts-ignore
const username: string = formData.get('username').toString().toLowerCase();
// @ts-ignore
const email: string = formData.get('email').toString().toLowerCase();
// @ts-ignore
const password: string = formData.get('password').toString();
const user = await prismaClient.user.findFirst({
where: {
OR: [
{ username: username },
{ email: email },
]
}
});
if (user != null) {
logger.debug(`A user with email (${email}) already exists in database`);
return error(400, {message: "Un compte avec cette adresse email ou nom d'utilisateur existe déjà."});
}
try {
const hash = await argon2.hash(password);
const newUser = await prismaClient.user.create({
data: {
username: username,
email: email,
password: hash,
surname: "",
name: ""
}
});
// @ts-ignore
const token = jwt.sign(newUser, process.env.JWT_SECRET, { expiresIn: "1h" });
logger.debug(`Generated a JWT token for user ${newUser.email}.`)
return json({token: token, userId: newUser.id});
} catch (e) {
logger.error(e);
return error(500, {message: "Erreur interne."});
}
}

View file

@ -0,0 +1,139 @@
import { json } from '@sveltejs/kit';
import prisma from '$lib/prismaClient';
import redisClient from '$lib/redisClient';
import logger from '$lib/logger';
import { sortChannels } from '$lib/utils/sort.ts';
// GET: Liste tous les canaux avec leur premier message
export async function GET({ url }) {
if(url.searchParams.get("name") != null && url.searchParams.get("name") != ""){
const name = url.searchParams.get("name");
try {
let canaux = await prisma.channel.findMany({
where: {
name: {
contains: name,
mode: 'insensitive',
},
},
include: {
messages: {
take: 1, // Récupère le dernier message
orderBy: { createdAt: 'desc' },// Trie par date décroissante
// as lastMessage not list last message
},
},
});
canaux = canaux.map((canaux) => {
return {
...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined
};
});
canaux = sortChannels(canaux);
return json(canaux);
} catch (err) {
logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 });
}
}else{
try {
let channels = [];
const cachedChannels = await redisClient.get('channels');
if (cachedChannels != null) {
logger.debug('Cache entry found, fetching channels from cache');
channels = JSON.parse(cachedChannels);
}else{
logger.debug('No cache entry was found, fetching channels from database');
}
if(channels.length < 10){
logger.debug('Fetching channels from database to fill cache');
let canaux = await prisma.channel.findMany({
include: {
messages: {
take: 1, // Récupère le dernier message
orderBy: { createdAt: 'desc' }, // Trie par date décroissante
},
},
});
canaux = canaux.map((canaux) => {
return {
...canaux,
lastMessage: canaux.messages.length > 0 ? canaux.messages[0] : null,
messages: undefined
};
});
channels = channels.concat(canaux);
channels = channels.filter((channel, index, self) =>
index === self.findIndex((t) => (
t.id === channel.id
))
);
channels = sortChannels(channels);
channels = channels.slice(0, 10);
await redisClient.set('channels', JSON.stringify(channels), { EX: 3600 });
}
return json(channels);
} catch (err) {
logger.error(err)
return json({ error: 'Erreur serveur' }, { status: 500 });
}
}
}
export async function POST({ request }) {
const { name } = await request.json();
try {
let canal = await prisma.channel.create({
data: {
name
},
});
logger.debug('Creating a new channel in database with id ' + canal.id);
const cachedChanels = await redisClient.get('channels');
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
canal = {
...canal,
lastMessage: null,
lastUpdate: canal.createdAt,
messages: undefined
}
channels.push(canal);
channels = sortChannels(channels);
logger.debug(`Added channel (${canal.id}) to channels cache.`);
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
return json(canal, { status: 201 });
} catch (err) {
console.log(err);
logger.error(err);
return json({ error: 'Erreur lors de la création du canal' }, { status: 500 });
}
}

View file

@ -0,0 +1,148 @@
import { json } from '@sveltejs/kit';
import prisma from '$lib/prismaClient';
import redisClient from '$lib/redisClient';
import logger from '$lib/logger';
// Récupérer les informations du canal et le dernier message (avec cache Redis)
export async function GET({ params }) {
const channelId = params.id;
const channelCacheKey = `channel:${channelId}:info`;
try {
const cachedChannel = await redisClient.get(channelCacheKey);
if (cachedChannel) {
logger.debug(`Cache entry found, fetching channel (${channelId}) from cache`);
return json(JSON.parse(cachedChannel));
}
logger.debug(`No cache entry was found, fetching channel (${channelId}) from database`);
const canal = await prisma.channel.findUnique({
where: { id: channelId },
});
if (!canal) {
logger.debug(`No channel for id ${channelId} was found in database`)
return json({ error: 'Canal non trouvé' }, { status: 404 });
}
const lastMessage = await prisma.message.findFirst({
where: { id: channelId },
orderBy: { createdAt: 'desc' },
});
// Créer un objet combiné pour le canal et le dernier message
const canalData = {
canal,
lastMessage, // Inclure uniquement le dernier message
};
const cachedChanels = await redisClient.get('channels');
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
channels.push(canal);
channels = channels.sort(
(
a: { messages: { createdAt: Date }[]; createdAt: Date },
b: { messages: { createdAt: Date }[]; createdAt: Date }
) => {
const lastMessageA = a.messages[0]?.createdAt || a.createdAt ? a.createdAt : new Date();
const lastMessageB = b.messages[0]?.createdAt || b.createdAt ? b.createdAt : new Date();
return new Date(lastMessageB).getTime() - new Date(lastMessageA).getTime();
}
);
logger.debug(`Added channel (${canal.id}) to channels cache.`);
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
logger.debug(`Creating a new cache entry with key channel:${channelId}:info`);
await redisClient.set(channelCacheKey, JSON.stringify(canalData), {EX: 600, NX: true}); // Cache pendant 5 minutes
return json(canalData);
} catch (err) {
logger.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 channelId = params.id;
try {
// Supprimer le canal de la base de données
await prisma.channel.delete({
where: { id: channelId },
});
logger.debug(`Deleting channel (${channelId}) from database`);
logger.debug(`Deleting channel (${channelId}) from cache`);
await redisClient.del(`channel:${channelId}: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 channelId = params.id;
const { nom } = await request.json();
// Clé cache pour les informations du canal et le dernier message
const canalCacheKey = `channel:${channelId}:info`;
try {
// Mettre à jour les informations du canal dans la base de données
const updatedCanal = await prisma.channel.update({
where: { id: channelId },
data: {
name: nom,
}
});
// Récupérer le dernier message associé au canal après mise à jour
const lastMessage = await prisma.message.findFirst({
where: { id: channelId },
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
};
const cachedChannels = await redisClient.get('channels');
let channelsArrays = cachedChannels != null ? JSON.parse(cachedChannels) : [];
channelsArrays = channelsArrays.filter((u: { id: string }) => u.id !== updatedCanal.id);
channelsArrays.push(canalData);
channelsArrays = channelsArrays.sort(
(
a: { messages: { createdAt: Date }[]; createdAt: Date },
b: { messages: { createdAt: Date }[]; createdAt: Date }
) => {
const lastMessageA = a.messages[0]?.createdAt || a.createdAt ? a.createdAt : new Date();
const lastMessageB = b.messages[0]?.createdAt || b.createdAt ? b.createdAt : new Date();
return new Date(lastMessageB).getTime() - new Date(lastMessageA).getTime();
}
);
logger.debug(`Updated channel (${channelId}) in channels cache`);
await redisClient.set('channels', JSON.stringify(channelsArrays), { EX: 600 })
logger.debug(`Updated cache entry with key channel:${channelId}:info`);
await redisClient.set(canalCacheKey, JSON.stringify(canalData), { EX: 600, NX: true });
return json(canalData);
} catch (err) {
console.error(err);
return json({ error: 'Erreur lors de la mise à jour du canal' }, { status: 500 });
}
}

View file

@ -0,0 +1,197 @@
import { json } from '@sveltejs/kit';
import prisma from '$lib/prismaClient';
import redisClient from '$lib/redisClient';
import logger from '$lib/logger';
import { sortChannels } from '$lib/utils/sort.ts';
export async function GET({ params, url }) {
const channelId = params.id;
logger.debug(`GET /api/channels/${channelId}/messages`);
const limit = parseInt(url.searchParams.get('limit') || '10');
const page = parseInt(url.searchParams.get('page') || '1');
const offset = (page - 1) * limit;
try {
logger.debug(`Tentative de récupération des messages du cache pour le channel : ${channelId}`);
let redisMessageKeys = await redisClient.zRangeWithScores(
`channel:${channelId}:messages`,
offset,
offset + limit - 1,
{ REV: true }
);
const redisPipelineRemove = redisClient.multi();
for (const messageKey of redisMessageKeys) {
// Vérifie si la clé existe dans Redis
const messageKeyValue = messageKey.value;
const exists = await redisClient.exists(messageKeyValue);
if (!exists) {
// Supprime la référence expirée dans le zSet
redisPipelineRemove.zRem(`channel:${channelId}:messages`, messageKeyValue);
redisMessageKeys = redisMessageKeys.filter((key) => key.value !== messageKeyValue);
}
}
await redisPipelineRemove.exec();
if (redisMessageKeys.length > 0) {
const messages = await Promise.all(
redisMessageKeys.map(async (key) => {
const message = await redisClient.get(key.value);
return JSON.parse(message);
})
);
const redisPipeline = redisClient.multi();
for (const key of redisMessageKeys) {
const message = await redisClient.get(key.value);
const msg = JSON.parse(message)
redisPipeline.set(key.value, JSON.stringify(msg), {EX: 1800});
redisPipeline.zAdd(`channel:${channelId}:messages`, {
score: key.score,
value: key.value,
});
}
await redisPipeline.exec();
return json({ limit, page, messages: messages.reverse() });
}
logger.debug(`Aucun message trouvé dans le cache, récupération depuis MongoDB pour le channel : ${channelId}`);
const messagesFromDB = await prisma.message.findMany({
where: { channelId },
select: {
id: true,
createdAt: true,
text: true,
user: {
select: {
id: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: offset,
take: limit,
});
if (messagesFromDB.length > 0) {
const redisPipeline = redisClient.multi();
for (const message of messagesFromDB) {
const messageKey = `message:${message.id}`;
redisPipeline.set(messageKey, JSON.stringify(message), {EX: 1800});
redisPipeline.zAdd(`channel:${channelId}:messages`, {
score: new Date(message.createdAt).getTime(),
value: messageKey,
});
}
await redisPipeline.exec();
}
return json({ limit, page, messages: messagesFromDB.reverse() });
} catch (err) {
logger.error(`Erreur lors de la récupération des messages : ${err.message}`);
return json({ error: 'Erreur lors de la récupération des messages' }, { status: 500 });
}
}
export async function POST({ params, request }) {
const channelId = params.id;
const { userId, text } = await request.json();
try {
// Créer un nouveau message dans MongoDB
let newMessage = await prisma.message.create({
data: {
userId,
channelId,
text,
},
select: {
id: true,
createdAt: true,
text: true,
user: {
select: {
id: true,
},
},
channel: {
select: {
id: true,
name: true,
},
}
},
});
// Ajouter le message dans Redis
await redisClient.set(`message:${newMessage.id}`, JSON.stringify(newMessage), {EX: 1800});
await redisClient.zAdd(`channel:${channelId}:messages`, {
score: new Date(newMessage.createdAt).getTime(),
value: `message:${newMessage.id}`,
});
//update the channels cache with the new message
const cachedChannels = await redisClient.get('channels');
let channels = cachedChannels ? JSON.parse(cachedChannels) : [];
let channel = channels.find((c) => c.id === channelId);
if(channel){
channel.lastMessage = {
id: newMessage.id,
text: newMessage.text,
user: newMessage.user,
createdAt: newMessage.createdAt,
};
channel.lastUpdate = newMessage.createdAt;
channel.messages = undefined;
}else{
channel = {...newMessage.channel, lastMessage: {
id: newMessage.id,
text: newMessage.text,
user: newMessage.user,
createdAt: newMessage.createdAt,
}, lastUpdate: newMessage.createdAt, messages: undefined};
channels = [channel, ...channels];
}
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
newMessage.channel = {
id: newMessage.channel.id,
name: newMessage.channel.name,
lastMessage: channel.lastMessage,
lastUpdate: channel.lastUpdate,
messages: undefined
};
logger.debug(`Nouveau message ajouté pour le channel : ${channelId}`);
return json(newMessage, { status: 201 });
} catch (err) {
logger.error(`Erreur lors de la création du message : ${err.message}`);
return json({ error: 'Erreur lors de la création du message' }, { status: 500 });
}
}
export async function DELETE({ params, request }) {
const channelId = params.id;
const { messageId } = await request.json();
try {
// Supprimer le message dans MongoDB
await prisma.message.delete({ where: { id: messageId } });
// Supprimer le message dans Redis
await redisClient.del(`message:${messageId}`);
await redisClient.zRem(`channel:${channelId}:messages`, `message:${messageId}`);
logger.debug(`Message supprimé pour le channel : ${channelId}`);
return json({ message: 'Message supprimé avec succès' });
} catch (err) {
logger.error(`Erreur lors de la suppression du message : ${err.message}`);
return json({ error: 'Erreur lors de la suppression du message' }, { status: 500 });
}
}

View file

@ -0,0 +1,62 @@
// src/routes/api/users/+server.js
import { json } from '@sveltejs/kit';
import redisClient from '$lib/redisClient';
import prisma from '$lib/prismaClient';
import logger from '$lib/logger';
export async function GET() {
try {
// Vérifier si les utilisateurs sont dans le cache Redis
const cachedUsers = await redisClient.get('users');
if (cachedUsers) {
logger.debug('Cache entry found, fetching users from cache');
return json(JSON.parse(cachedUsers));
}
logger.debug('No cache entry was found, fetching users from database');
// Sinon, récupérer les utilisateurs depuis MongoDB
const users = await prisma.user.findMany();
// Mettre les utilisateurs en cache
logger.debug('Caching users with EX of 600 secs');
await redisClient.set('users', JSON.stringify(users), { EX: 600 });
return json(users);
} catch (err) {
logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 });
}
}
export async function POST({ request }) {
const { username, surname, name, email, password } = await request.json();
try {
const user = await prisma.user.create({
data: {
username: username.toLowerCase(),
surname,
name,
email: email.toLowerCase(),
password,
},
});
logger.debug('Creating a new user in database with id ' + user.id);
// Mettre le nouvel utilisateur dans le cache
logger.debug(`Caching user (${user.id})`);
const cachedUsers = await redisClient.get('users');
const usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : [];
usersArray.push(user);
logger.debug(`Added user (${user.id}) to users cache.`);
await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 })
logger.debug(`Creating a new cache entry with key user:${user.id}, with EX of 3600 secs`);
await redisClient.set(`user:${user.id}`, JSON.stringify(user), { EX: 3600 });
return json(user, { status: 201 });
} catch (err) {
logger.error(err)
return json({ error: 'Erreur lors de la création de lutilisateur' }, { status: 500 });
}
}

View file

@ -0,0 +1,136 @@
import { json } from '@sveltejs/kit';
import redisClient from '$lib/redisClient';
import prisma from '$lib/prismaClient';
import logger from '$lib/logger';
import { writeFile } from 'node:fs/promises';
import { extname } from 'path';
import * as argon2 from 'argon2';
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) {
logger.debug(`Cache entry found, fetching user (${params.id}) from cache`);
return json(JSON.parse(cachedUser));
}
logger.debug(`No cache entry was found, fetching user (${params.id}) from database`);
// Si non, récupérer depuis MongoDB via Prisma
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
logger.debug(`No record of user (${params.id}) found in database`);
return json({ error: 'Utilisateur non trouvé' }, { status: 404 });
}
// Mettre l'utilisateur en cache
const cachedUsers = await redisClient.get('users');
const usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : [];
usersArray.push(user);
logger.debug(`Added user (${user.id}) to users cache.`);
await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 })
logger.debug(`Creating a new cache entry with key user:${user.id}, with EX of 3600 secs`);
await redisClient.set(`user:${userId}`, JSON.stringify(user), { EX: 3600 });
return json(user);
} catch (err) {
logger.error(err);
return json({ error: 'Erreur serveur' }, { status: 500 });
}
}
// Mettre à jour un utilisateur avec PUT
export async function PUT({ params, request }) {
const userId = params.id;
const formData = await request.formData();
const data: {username?: string, email?: string, surname?: string, name?: string, password?: string, profilePicture?: string} = {};
// @ts-ignore
const username = formData.get('username').toString();
// @ts-ignore
const surname = formData.get('surname').toString();
// @ts-ignore
const name = formData.get('name').toString();
// @ts-ignore
const email = formData.get('email').toString();
// @ts-ignore
const password = formData?.get('password');
// @ts-ignore
const profilePicture: File | null = formData?.get('profilePicture');
let filename: string | null = null;
if (profilePicture != null) {
filename = `${crypto.randomUUID()}${extname(profilePicture?.name)}`;
await writeFile(`static/${filename}`, Buffer.from(await profilePicture?.arrayBuffer()));
data.profilePicture = filename;
}
if (password != null) {
data.password = await argon2.hash(password.toString());
}
data.username = username;
data.surname = surname;
data.name = name;
data.email = email;
try {
const updatedUser = await prisma.user.update({
where: { id: userId },
data: data
});
logger.debug(`Updated user (${updatedUser.id}) in database`);
// Mettre à jour l'utilisateur dans le cache Redis
const cachedUsers = await redisClient.get('users');
let usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : [];
usersArray = usersArray.filter((u: { id: string }) => u.id !== updatedUser.id);
usersArray.push(updatedUser);
logger.debug(`Updated user (${updatedUser.id}) in users cache.`);
await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 })
logger.debug(`Updated cache entry with key user:${updatedUser.id}`);
await redisClient.set(`user:${userId}`, JSON.stringify(updatedUser), { EX: 3600 }); // Cache pendant 1 heure (3600 secondes)
return json(updatedUser);
} catch (err) {
logger.error(err);
return json({ error: 'Erreur lors de la mise à jour de lutilisateur' }, { status: 500 });
}
}
export async function DELETE({ params }) {
const userId = params.id;
try {
const deletedUser = await prisma.user.delete({
where: { id: userId },
});
logger.debug(`Deleted user (${deletedUser.id}) from database.`);
// Supprimer l'utilisateur du cache Redis
const cachedUsers = await redisClient.get('users');
let usersArray = cachedUsers != null ? JSON.parse(cachedUsers) : [];
usersArray = usersArray.filter((u: { id: string }) => u.id !== deletedUser.id);
logger.debug(`Deleted cache entry with key user:${deletedUser.id}`);
await redisClient.del(`user:${userId}`);
logger.debug(`Deleted user (${deletedUser.id}) from users cache.`);
await redisClient.set('users', JSON.stringify(usersArray), { EX: 600 })
return json({ message: 'Utilisateur supprimé avec succès' });
} catch (err) {
logger.error(err);
return json({ error: 'Erreur lors de la suppression de lutilisateur' }, { status: 500 });
}
}

View file

@ -0,0 +1,25 @@
export async function load({ fetch, locals }) {
try {
// Appel API ou récupération de données
const res = await fetch('/api/channels', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const channels = await res.json();
// Retourner les données à la page sous forme de props
return {
channels,
userId: locals.userId
};
} catch (error) {
console.error('Erreur lors du chargement des canaux:', error);
return {
channels: [],
userId: locals.userId
};
}
}

View file

@ -0,0 +1,118 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import Plus from "svelte-radix/Plus.svelte"; // Icône pour "Ajouter"
import Search from "$lib/components/ui/Search.svelte";
import ChatItem from "$lib/components/ui/ChatItem.svelte";
import ProfileCard from "$lib/components/ui/ProfileCard.svelte"; // Importer le composant ProfileCard
import CreateChat from "$lib/components/ui/CreateChat.svelte"; // Importer le composant CreateChat
import { initSocket } from "$lib/stores/socket";
let chatListRef: HTMLElement | null = null;
let showProfileCard = false; // État pour afficher ou masquer le ProfileCard
let showCreateChat = false; // État pour afficher ou masquer CreateChat
let socket = initSocket(); // Initialiser le socket
socket.on("new-channel", (channel) => {
channels = [channel, ...channels];
});
socket.on("new-message", (message) => {
const channel = message.channel
if(channels.find(c => c.id === channel.id)) {
channels = channels.map((c) => {
if (c.id === channel.id) {
c.lastMessage = message;
c.lastUpdate = message.createdAt;
}
return c;
});
channels = [channel, ...channels.filter((c) => c.id !== channel.id)];
}else{
channels = [channel, ...channels];
}
});
function openProfileCard() {
showProfileCard = true; // Inverser l'état pour afficher/masquer le ProfilCard
}
function closeProfileCard() {
showProfileCard = false; // Inverser l'état pour afficher/masquer le ProfilCard
}
function openCreateChat() {
showCreateChat = true; // Afficher le composant CreateChat
}
function closeCreateChat() {
showCreateChat = false; // Fermer le composant CreateChat
}
async function loadNewChannels(name : string) {
console.log('loadNewChannels');
const res = await fetch(`/api/channels?name=${name}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
channels = await res.json();
console.log(channels);
}
export let data;
export let channels = data.channels;// Assurez-vous que 'lastMessage' est facultatif si nécessaire
</script>
<div class="h-full flex flex-col gap-5 p-5">
<div class="flex justify-between items-center">
<!-- Bouton Profile avec l'image de l'utilisateur -->
<Button
size="default"
class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white"
on:click={openProfileCard}
>
<img src={data.user.profilePicture} alt="Profile" class="h-8 w-8 rounded-full" />
Profile
</Button>
<div class="flex items-center gap-2 w-full mr-6 ml-6">
<div class="relative w-full">
<Search class="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" onChange={loadNewChannels}/>
</div>
</div>
<!-- Bouton Nouveau Chat -->
<Button
size="default"
class="flex items-center gap-2 bg-blue-500 hover:bg-blue-600 text-white"
on:click={openCreateChat}
>
<Plus class="h-5 w-5" />
Nouveau Chat
</Button>
</div>
<div class="flex flex-col gap-4 overflow-y-auto" bind:this={chatListRef}>
{#each channels as channel}
<ChatItem
id={channel.id}
title={channel.name}
lastMessage={channel.lastMessage ? channel.lastMessage.text : "Ecrire le premier message"}
createdAt={channel.lastUpdate}
/>
{/each}
</div>
</div>
<CreateChat show={showCreateChat} socket={socket} onClose={closeCreateChat} listRef={chatListRef} />
<ProfileCard user={data.user} userSessionId={data.userId} show={showProfileCard} onClose={closeProfileCard} />
<style>
.h-full {
height: 100%;
background-color: #f9f9f9;
}
</style>

View file

@ -0,0 +1,33 @@
export async function load({ fetch, params, locals }) {
try {
const res = await fetch(`/api/channels/${params.id}/messages?page=1&limit=10`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const messages = await res.json();
const resUser = await fetch(`/api/users/${locals.userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const user = await resUser.json();
return {
messages,
channelId: params.id,
userId: locals.userId,
user: user
}
}catch (error) {
console.error('Erreur lors du chargement des messages:', error);
return {
messages: [],
channelId: params.id,
userId: locals.userId,
user: {}
};
}
}

View file

@ -0,0 +1,338 @@
<script lang="ts">
import Textarea from "$lib/components/ui/textarea/textarea.svelte";
import { Button } from "$lib/components/ui/button";
import PaperPlane from "svelte-radix/PaperPlane.svelte";
import Message from "$lib/components/Message.svelte";
import UserChat from '$lib/components/ui/UserChat.svelte';
import { tick, onDestroy, onMount } from 'svelte';
import { initSocket } from '$lib/stores/socket';
import { ArrowLeft } from 'lucide-svelte';
import { messagesStore } from '$lib/stores/messagesStore';
import ProfileCard from '$lib/components/ui/ProfileCard.svelte';
export let data;
messagesStore.set(data.messages.messages);
let user = data.user;
const socket = initSocket(); // Initialiser le socket
let users= [];
let scrollContainer: HTMLElement;
let messageText = '';
let activeProfileId = null;
let userChatSelected = {
id: '',
username: '',
name: '',
surname: '',
email: '',
profilePicture: '',
state: '',
};
let showProfileCard = false;
function openProfileCard(user) {
userChatSelected = user;
showProfileCard = true;
}
function closeProfileCard() {
showProfileCard = false;
}
function setActiveProfile(id) {
activeProfileId = id;
}
async function sendMessage() {
// Appel API pour envoyer le message
const response = await fetch(`/api/channels/${data.channelId}/messages`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId: data.userId, text: messageText }),
});
if (response.ok) {
let newMessage =await response.json();
// Envoyer le message avec les sockets (à implémenter)
socket.emit('new-message', newMessage);
console.log('Message envoyé avec succès');
messageText = '';
}else{
console.log('Erreur lors de l\'envoi du message');
}
}
let isLoading = false;
const limit = 10;
async function loadMoreMessages() {
if (isLoading) {
return;
}
isLoading = true;
const previousMessages = $messagesStore;
let newMessages = [];
try {
// Calculer la page à charger en fonction du nombre total de messages existants
const totalMessages = $messagesStore.length;
const pageToLoad = Math.floor(totalMessages / limit) + 1;
const response = await fetch(`/api/channels/${data.channelId}/messages?page=${pageToLoad}&limit=${limit}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
newMessages = await response.json();
if (newMessages.messages.length <= 0) {
console.log("Pas d'autres anciens messages");
return;
}
// Éviter les doublons en filtrant les messages déjà présents
const existingMessageIds = new Set($messagesStore.map((msg) => msg.id));
const filteredMessages = newMessages.messages.filter(
(msg) => !existingMessageIds.has(msg.id)
);
if (filteredMessages.length > 0) {
$messagesStore = [...filteredMessages, ...$messagesStore]; // Ajouter les nouveaux messages en haut
console.log(`${filteredMessages.length} nouveaux messages ajoutés`);
} else {
console.log("Aucun nouveau message à ajouter (tous déjà chargés)");
}
} else {
console.error("Erreur lors du chargement des anciens messages");
}
} catch (error) {
console.error("Erreur réseau lors du chargement des messages:", error);
} finally {
isLoading = false;
await tick();
const filteredNewMessages = newMessages.messages.filter((msg) => {
return !previousMessages.some((m) => m.id === msg.id);
});
scrollContainer.scrollTo({
top: filteredNewMessages.length*300,
});
}
}
function handleScroll() {
if (scrollContainer) {
// Détection quand on est en haut du scroll
if (scrollContainer.scrollTop <= 0 && !isLoading) {
loadMoreMessages();
}
}
}
async function handleEnter(event: KeyboardEvent) {
if (event.key === 'Enter') {
await sendMessage();
}
}
let stopWritingTimeout;
function handleWriting() {
clearTimeout(stopWritingTimeout);
socket.emit('writing', { userId: data.userId, channelId: data.channelId });
stopWritingTimeout = setTimeout(() => {
handleStopWriting();
}, 2000); // Attendre 2 secondes d'inactivité avant d'émettre stop-writing
}
function handleStopWriting() {
socket.emit('stop-writing', { userId: data.userId, channelId: data.channelId });
}
async function scrollToBottom(retries = 20) {
await tick();
const attemptScroll = () => {
if (scrollContainer) {
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: 'smooth',
});
}
};
// Protéger l'utilisation de requestAnimationFrame
if (typeof window !== 'undefined' && typeof requestAnimationFrame === 'function') {
attemptScroll();
if (retries > 0) {
requestAnimationFrame(() => scrollToBottom(retries - 1));
}
}
}
onDestroy(() => {
socket.emit('leave-channel', { userId: data.userId, channelId: data.channelId });
socket.disconnect(); // Déconnexion propre du socket
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll);
}
});
// Ecoute des événements socket
socket.on("new-message", async (message) => {
$messagesStore = [...$messagesStore, message]; // Add the new message to the store
await tick();
await scrollToBottom(); // Scroll to the bottom after the message is added
});
socket.on("load-users-channel", async (us) => {
users = us;
await tick();
});
socket.on("connect", () => {
socket.emit('new-user-join', { user:{ ...user, socketId:socket.id, state:"En ligne" }, channelId: data.channelId });
});
socket.on('user-writing', async (userId) => {
console.log('user-writing reçu pour userId:', userId);
// On met à jour l'état de l'utilisateur
users = users.map((u) => {
if (u.id === userId) {
// Mettre à jour le state
return { ...u, state: "Ecrit" }; // On recrée l'objet pour garantir la réactivité
}
return u;
});
// On recrée une nouvelle référence du tableau `users`
users = [...users]; // Cela force Svelte à détecter le changement dans la liste
console.log('Utilisateurs après mise à jour de l\'état:', users);
// Forcer une mise à jour avec tick
await tick();
});
socket.on('user-stop-writing', async (userId) => {
console.log('user-stop-writing reçu pour userId:', userId);
users = users.map((u) => {
if (u.id === userId) {
// Mettre à jour le state
return { ...u, state: "En ligne" }; // On recrée l'objet pour garantir la réactivité
}
return u;
});
users = [...users]; // Cela force Svelte à détecter le changement dans la liste
console.log('Utilisateurs après mise à jour de l\'état:', users);
await tick();
});
messagesStore.subscribe(async () => {
await tick();
});
let firstPageLoad = true;
onMount(async () => {
await tick();
if(firstPageLoad){
firstPageLoad = false;
await scrollToBottom();
}
});
</script>
<div class="h-full flex">
<!-- Liste des utilisateurs (colonne gauche) -->
<div class="w-1/4 bg-gray-100 border-r overflow-y-auto">
<div class="flex gap-4 px-4 mt-5">
<Button href="/chats" variant="outline" size="icon" ><ArrowLeft /></Button>
<h2 class="text-3xl font-bold">Utilisateurs</h2>
</div>
<div class="flex flex-col m-5 gap-2">
{#each users as u (u.id)}
<UserChat
user={u}
openProfileCard={() => openProfileCard(u)}
/>
{/each}
</div>
</div>
<!-- Chat principal (colonne droite) -->
<div class="flex-1 flex flex-col h-full">
<!-- Messages -->
<div
class="m-10 flex flex-col gap-5 overflow-auto flex-grow"
bind:this={scrollContainer}
on:scroll={handleScroll}
>
{#if isLoading}
<div class="loading-indicator">Chargement...</div>
{/if}
{#if $messagesStore !== undefined && $messagesStore.length > 0}
{#each $messagesStore as message}
<Message
userId={data.userId}
message={message}
activeProfileId={activeProfileId}
setActiveProfile={setActiveProfile}
/>
{/each}
{:else}
<div class="text-center text-gray-500 mt-10">Sélectionnez un message le chat est vide.</div>
{/if}
</div>
<!-- Input pour envoyer un message -->
<div class="px-10 py-5 w-full flex gap-2 border-t">
<Textarea
class="h-16 resize-none flex-grow"
placeholder="Écrivez un message..."
bind:value={messageText}
on:keypress={handleEnter}
on:input={handleWriting}
on:blur={handleStopWriting}
/>
<Button size="icon" class="h-16 w-16 bg-blue-500 hover:bg-blue-600 h-full" on:click={sendMessage}>
<PaperPlane class="h-6 w-6" />
</Button>
</div>
</div>
</div>
<ProfileCard user={userChatSelected} userSessionId={data.userId} show={showProfileCard} onClose={closeProfileCard}></ProfileCard>
<style>
.h-full {
height: 100%;
}
.loading-indicator {
text-align: center;
padding: 10px;
color: gray;
}
.overflow-y-auto {
scroll-behavior: smooth; /* Défilement fluide */
}
</style>

View file

@ -0,0 +1,10 @@
import { redirect } from '@sveltejs/kit';
export async function POST({ cookies }) {
// Supprimer les cookies "token" et "UID"
cookies.delete("token", { path: '/' });
cookies.delete("UID", { path: '/' });
// Rediriger vers la page d'accueil ou la page de connexion après déconnexion
throw redirect(303, '/');
}

View file

@ -0,0 +1,23 @@
export async function load({ fetch, locals }) {
try {
// Appel API ou récupération de données
const res = await fetch(`/api/users/${locals.userId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const user = await res.json();
// Retourner les données à la page sous forme de props
return {
user
};
} catch (error) {
console.error('Erreur lors du chargement des canaux:', error);
return {
user : null
};
}
}

View file

@ -0,0 +1,143 @@
<script lang="ts">
import ChoosePicture from "$lib/components/ui/ChoosePicture.svelte";
import { Button } from '$lib/components/ui/button';
export let data;
const user = data.user;
console.log(user);
let pseudo = user.username;
let firstName = user.name;
let lastName = user.surname;
let email = user.email;
let profilePicture = user.profilePicture; // Chemin initial ou valeur null
let message = '';
let showMessage = false;
// Fonction pour valider l'email
const validateEmail = (email: string) => {
const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return re.test(email);
};
// Fonction de soumission du formulaire
const handleSubmit = () => {
if (!pseudo || !firstName || !lastName || !email) {
message = 'Veuillez remplir tous les champs.';
showMessage = true;
} else if (!validateEmail(email)) {
message = 'L\'email est invalide.';
showMessage = true;
} else {
updateUser();
message = 'Informations mises à jour avec succès!';
showMessage = true;
}
};
async function updateUser() {
const formData = new FormData();
formData.append('username', pseudo);
formData.append('name', firstName);
formData.append('surname', lastName);
formData.append('email', email);
if (profilePicture) {
formData.append('profilePicture', profilePicture);
}
const res = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: formData,
});
const result = await res.json();
console.log(result);
}
</script>
<div class="flex items-center justify-center min-h-screen bg-gray-100 mg-10">
<div class="bg-white p-8 rounded-lg shadow-md w-full max-w-lg">
<h2 class="text-2xl font-semibold text-center mb-6">Modifier les informations du compte</h2>
<!-- Message d'alerte -->
{#if showMessage}
<div class="bg-blue-500 text-white p-3 rounded-lg text-center mb-4">
{message}
</div>
{/if}
<!-- Formulaire de modification du profil -->
<form on:submit|preventDefault={handleSubmit}>
<div class="mb-4">
<label for="pseudo" class="block text-sm font-semibold text-gray-700">Pseudo</label>
<input
type="text"
id="pseudo"
bind:value={pseudo}
class="w-full p-2 mt-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Votre pseudo"
/>
</div>
<div class="mb-4">
<label for="firstName" class="block text-sm font-semibold text-gray-700">Prénom</label>
<input
type="text"
id="firstName"
bind:value={firstName}
class="w-full p-2 mt-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Votre prénom"
/>
</div>
<div class="mb-4">
<label for="lastName" class="block text-sm font-semibold text-gray-700">Nom</label>
<input
type="text"
id="lastName"
bind:value={lastName}
class="w-full p-2 mt-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Votre nom"
/>
</div>
<div class="mb-4">
<label for="email" class="block text-sm font-semibold text-gray-700">Email</label>
<input
type="email"
id="email"
bind:value={email}
class="w-full p-2 mt-1 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Votre email"
/>
</div>
<!-- Intégration du composant de photo de profil -->
<div class="mb-4">
<ChoosePicture bind:profilePicture={profilePicture} />
</div>
<div class="mt-6 flex flex-col gap-6 items-center justify-center">
<button
type="submit"
class="bg-blue-500 text-white px-6 py-2 rounded-md hover:bg-blue-600 focus:outline-none"
>
Mettre à jour
</button>
<Button href="/chats" variant="secondary">Retour au menu principal</Button>
</div>
</form>
</div>
</div>
<style>
input, button {
font-family: inherit;
}
</style>

BIN
static/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<path fill="#494c4e" d="M9 0a9 9 0 0 0-9 9 8.654 8.654 0 0 0 .05.92 9 9 0 0 0 17.9 0A8.654 8.654 0 0 0 18 9a9 9 0 0 0-9-9zm5.42 13.42c-.01 0-.06.08-.07.08a6.975 6.975 0 0 1-10.7 0c-.01 0-.06-.08-.07-.08a.512.512 0 0 1-.09-.27.522.522 0 0 1 .34-.48c.74-.25 1.45-.49 1.65-.54a.16.16 0 0 1 .03-.13.49.49 0 0 1 .43-.36l1.27-.1a2.077 2.077 0 0 0-.19-.79v-.01a2.814 2.814 0 0 0-.45-.78 3.83 3.83 0 0 1-.79-2.38A3.38 3.38 0 0 1 8.88 4h.24a3.38 3.38 0 0 1 3.1 3.58 3.83 3.83 0 0 1-.79 2.38 2.814 2.814 0 0 0-.45.78v.01a2.077 2.077 0 0 0-.19.79l1.27.1a.49.49 0 0 1 .43.36.16.16 0 0 1 .03.13c.2.05.91.29 1.65.54a.49.49 0 0 1 .25.75z"/>
</svg>

After

Width:  |  Height:  |  Size: 843 B

View file

@ -1,5 +1,5 @@
import { mdsvex } from 'mdsvex';
import adapter from '@sveltejs/adapter-auto';
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
@ -8,11 +8,15 @@ const config = {
// for more information about preprocessors
preprocess: [vitePreprocess(), mdsvex()],
alias: {
"@/*": "./path/to/lib/*",
},
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
adapter: adapter(),
},
extensions: ['.svelte', '.svx']

View file

@ -1,11 +1,64 @@
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
import { fontFamily } from "tailwindcss/defaultTheme";
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
content: ["./src/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
extend: {}
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)"
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)"
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)"
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)"
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)"
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)"
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: [...fontFamily.sans]
}
}
},
};
plugins: []
} satisfies Config;
export default config;

View file

@ -1,6 +1,87 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { type ViteDevServer, defineConfig } from 'vite';
import { Server } from 'socket.io'
function isView(obj) {
return obj instanceof DataView || (obj && obj.buffer instanceof ArrayBuffer);
}
const webSocketServer = {
name: 'webSocketServer',
configureServer(server: ViteDevServer) {
if (!server.httpServer) return
const io = new Server(server.httpServer)
let channelsUsers = {};
io.on('connection', (socket) => {
socket.on('new-channel', (channel) => {
io.emit('new-channel', channel)
});
// Écouter les messages
socket.on('new-message', (message) => {
io.emit('new-message', message); // Diffusion du message
});
socket.on('new-user-join', (data) => {
const {user, channelId } = data;
if (!channelsUsers[channelId]) {
channelsUsers[channelId] = [];
}
if (!channelsUsers[channelId].find((u) => u.id === user.id)) {
// Ajouter l'utilisateur à la liste des utilisateurs du canal avec son socketId
channelsUsers[channelId].push(user);
}
socket.join(`channel:${channelId}`);
io.to(`channel:${channelId}`).emit('load-users-channel', channelsUsers[channelId]);
});
socket.on('leave-channel', (data) => {
const { userId, channelId } = data;
if (channelsUsers[channelId]) {
// Supprimez l'utilisateur du canal
channelsUsers[channelId] = channelsUsers[channelId].filter((u) => u.id !== userId);
io.to(`channel:${channelId}`).emit('load-users-channel', channelsUsers[channelId]);
console.log(`Utilisateur ${userId} a quitté le canal ${channelId}`);
}
});
socket.on('disconnect', () => {
console.log('Déconnexion du client');
for (const channelId in channelsUsers) {
channelsUsers[channelId] = channelsUsers[channelId].filter((u) => u.socketId !== socket.id);
io.to(`channel:${channelId}`).emit('load-users-channel', channelsUsers[channelId]);
}
console.log('Utilisateurs connectés:', channelsUsers);
});
socket.on('writing', (data) => {
const { userId, channelId } = data;
const us = channelsUsers[channelId]?.find((u) => u.id === userId);
if (us) {
us.state = "Ecrit";
io.to(`channel:${channelId}`).emit('user-writing', userId);
}
});
socket.on('stop-writing', (data) => {
const { userId, channelId } = data;
const us = channelsUsers[channelId]?.find((u) => u.id === userId);
if (us) {
us.state = "En ligne";
io.to(`channel:${channelId}`).emit('user-stop-writing', userId);
}
});
});
}
}
export default defineConfig({
plugins: [sveltekit()]
plugins: [sveltekit(), webSocketServer]
});