Merge pull request #5 from NabilOuldHamou/features/reactivity
features/reactivity
This commit is contained in:
commit
ae4400b02e
9 changed files with 266 additions and 66 deletions
|
@ -1,15 +1,47 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Card from "$lib/components/ui/card";
|
import * as Card from "$lib/components/ui/card";
|
||||||
|
import { formatDistanceToNow } from "$lib/utils/date.js";
|
||||||
|
|
||||||
export let username;
|
export let username: string;
|
||||||
export let messageContent;
|
export let messageContent: string;
|
||||||
|
export let profilePicture: string | null; // Peut être null
|
||||||
|
export let createdAt: string; // Date de création du message
|
||||||
|
|
||||||
|
// Image par défaut si profilePicture est null
|
||||||
|
let defaultProfilePicture = "/images/default-profile.png";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root class="relative">
|
||||||
<Card.Header>
|
<Card.Header class="flex items-center flex-row justify-between">
|
||||||
<Card.Title>{username}</Card.Title>
|
<!-- Image de profil collée à gauche -->
|
||||||
|
<div class="flex flex-row gap-3 items-center">
|
||||||
|
<img
|
||||||
|
src={profilePicture || defaultProfilePicture}
|
||||||
|
alt="Profile Picture"
|
||||||
|
class="h-10 w-10 rounded-full border border-gray-300"
|
||||||
|
/>
|
||||||
|
<!-- Section contenant le pseudo -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Card.Title class="text-gray-800 text-sm sm:text-base md:text-lg truncate">
|
||||||
|
{username}
|
||||||
|
</Card.Title>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs sm:text-sm md:text-base text-gray-500 items-top">
|
||||||
|
{formatDistanceToNow(createdAt)}
|
||||||
|
</span>
|
||||||
</Card.Header>
|
</Card.Header>
|
||||||
|
|
||||||
|
<!-- Contenu du message -->
|
||||||
<Card.Content>
|
<Card.Content>
|
||||||
|
<p class="text-sm sm:text-base md:text-lg text-gray-700">
|
||||||
{messageContent}
|
{messageContent}
|
||||||
|
</p>
|
||||||
</Card.Content>
|
</Card.Content>
|
||||||
</Card.Root>
|
</Card.Root>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
object-fit: cover; /* Assure un bon rendu des images */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
if (chatName.trim()) {
|
if (chatName.trim()) {
|
||||||
try {
|
try {
|
||||||
// Appel API pour créer le chat
|
// Appel API pour créer le chat
|
||||||
const response = await fetch('/api/canals', {
|
const response = await fetch('/api/channels', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import { Search } from "lucide-svelte"; // Icône de recherche depuis Lucide Svelte
|
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 placeholder: string = "Rechercher..."; // Texte par défaut pour l'input
|
||||||
|
|
||||||
|
export let onChange: (value: string) => void = () => {}; // Callback pour la recherche
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
|
@ -11,6 +13,7 @@
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
class="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring focus:border-blue-300"
|
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>
|
</div>
|
||||||
|
|
||||||
|
|
58
src/lib/components/ui/UserChat.svelte
Normal file
58
src/lib/components/ui/UserChat.svelte
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let profilePicture: string; // URL de l'image de profil
|
||||||
|
export let username: string; // Pseudo de l'utilisateur
|
||||||
|
export let status: string = "En ligne"; // Statut par défaut
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 p-3 cursor-pointer hover:bg-gray-100 rounded-lg border border-gray-300 shadow-sm">
|
||||||
|
<img
|
||||||
|
src={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">{username}</span>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-xs text-gray-500">{status}</span>
|
||||||
|
{#if status === "En ligne"}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-3 w-3 text-green-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-3 w-3 text-gray-500"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Ajout d'une animation subtile lors du survol */
|
||||||
|
div:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,34 @@ import redisClient from '$lib/redisClient';
|
||||||
import logger from '$lib/logger';
|
import logger from '$lib/logger';
|
||||||
|
|
||||||
// GET: Liste tous les canaux avec leur premier message
|
// GET: Liste tous les canaux avec leur premier message
|
||||||
export async function GET() {
|
export async function GET({ params, 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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
canaux = sortChannels(canaux);
|
||||||
|
|
||||||
|
return json(canaux);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
return json({ error: 'Erreur serveur' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}else{
|
||||||
try {
|
try {
|
||||||
const cachedChannels = await redisClient.get('channels');
|
const cachedChannels = await redisClient.get('channels');
|
||||||
|
|
||||||
|
@ -23,11 +50,7 @@ export async function GET() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
canaux = canaux.sort((a, b) => {
|
canaux = sortChannels(canaux);
|
||||||
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('Caching channels with EX of 3600 secs');
|
logger.debug('Caching channels with EX of 3600 secs');
|
||||||
await redisClient.set('channels', JSON.stringify(canaux), { EX: 3600 });
|
await redisClient.set('channels', JSON.stringify(canaux), { EX: 3600 });
|
||||||
|
@ -40,34 +63,48 @@ export async function GET() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
const { name } = await request.json();
|
const { name } = await request.json();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const canal = await prisma.channel.create({
|
const canal = await prisma.channel.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
logger.debug('Creating a new channel in database with id ' + canal.id);
|
logger.debug('Creating a new channel in database with id ' + canal.id);
|
||||||
|
|
||||||
const cachedChanels = await redisClient.get('channels');
|
const cachedChanels = await redisClient.get('channels');
|
||||||
const channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
|
|
||||||
|
let channels = cachedChanels != null ? JSON.parse(cachedChanels) : [];
|
||||||
|
console.log(channels);
|
||||||
|
console.log(canal);
|
||||||
|
|
||||||
channels.push(canal);
|
channels.push(canal);
|
||||||
|
|
||||||
|
channels = sortChannels(channels);
|
||||||
|
console.log(channels);
|
||||||
|
|
||||||
logger.debug(`Added channel (${canal.id}) to channels cache.`);
|
logger.debug(`Added channel (${canal.id}) to channels cache.`);
|
||||||
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
|
await redisClient.set('channels', JSON.stringify(channels), { EX: 600 });
|
||||||
|
|
||||||
return json(canal, { status: 201 });
|
return json(canal, { status: 201 });
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
return json({ error: 'Erreur lors de la création du canal' }, { status: 500 });
|
return json({ error: 'Erreur lors de la création du canal' }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortChannels(channels) {
|
||||||
|
return channels.sort((a, b) => {
|
||||||
|
// Vérifie si 'a.messages' existe et est un tableau, sinon utilise la date de création du canal
|
||||||
|
const lastMessageA = Array.isArray(a.messages) && a.messages.length > 0 ? a.messages[0]?.createdAt : a.createdAt;
|
||||||
|
const lastMessageB = Array.isArray(b.messages) && b.messages.length > 0 ? b.messages[0]?.createdAt : b.createdAt;
|
||||||
|
|
||||||
|
return new Date(lastMessageB).getTime() - new Date(lastMessageA).getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ export async function GET({ params, url }) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const page = url.searchParams.get('page') != null ? parseInt(url.searchParams.get('page')) : 1;
|
const page = url.searchParams.get('page') != null ? parseInt(url.searchParams.get('page')) : 1;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const limit = url.searchParams.get('limit') != null ? parseInt(url.searchParams.get('limit')) : 10;
|
const limit = 10;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Générer une clé cache Redis unique en fonction du canal et des paramètres de pagination
|
// Générer une clé cache Redis unique en fonction du canal et des paramètres de pagination
|
||||||
const cacheKey = `channel:${channelId}:messages:page:${page}:limit:${limit}`;
|
const cacheKey = `channel:${channelId}:messages:page:${page}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cachedMessages = await redisClient.get(cacheKey);
|
const cachedMessages = await redisClient.get(cacheKey);
|
||||||
|
@ -104,21 +104,21 @@ export async function DELETE({ params }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour mettre à jour tous les caches des messages
|
// Fonction pour mettre à jour tous les caches des messages
|
||||||
function updateCaches(channelId: string) {
|
async function updateCaches(channelId: string) {
|
||||||
let page : number = 1;
|
let page : number = 1;
|
||||||
let limit : number = 10;
|
let limit : number = 10;
|
||||||
let offset : number = (page - 1) * limit;
|
let offset : number = (page - 1) * limit;
|
||||||
while (true) {
|
while (true) {
|
||||||
const cacheKey = `channel:${channelId}:messages:page:${page}:limit:${limit}`;
|
const cacheKey = `channel:${channelId}:messages:page:${page}`;
|
||||||
const cachedMessages = await redisClient.get(cacheKey);
|
const cachedMessages = await redisClient.get(cacheKey);
|
||||||
if (!cachedMessages) {
|
if (!cachedMessages) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const totalMessages = await prisma.message.count({
|
const totalMessages = await prisma.message.count({
|
||||||
where: { canalId },
|
where: { channelId },
|
||||||
});
|
});
|
||||||
const messages = await prisma.message.findMany({
|
const messages = await prisma.message.findMany({
|
||||||
where: { canalId },
|
where: { channelId },
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
select: { id: true, pseudo: true },
|
select: { id: true, pseudo: true },
|
||||||
|
|
|
@ -38,10 +38,20 @@
|
||||||
showCreateChat = false; // Fermer le composant CreateChat
|
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 data;
|
||||||
export let channels = data.channels;// Assurez-vous que 'lastMessage' est facultatif si nécessaire
|
export let channels = data.channels;// Assurez-vous que 'lastMessage' est facultatif si nécessaire
|
||||||
console.log(channels);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col gap-5 p-5">
|
<div class="h-full flex flex-col gap-5 p-5">
|
||||||
|
@ -58,7 +68,7 @@
|
||||||
|
|
||||||
<div class="flex items-center gap-2 w-full mr-6 ml-6">
|
<div class="flex items-center gap-2 w-full mr-6 ml-6">
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<Search class="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" />
|
<Search class="absolute left-2 top-1/2 transform -translate-y-1/2 text-gray-400" onChange={loadNewChannels}/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
19
src/routes/chats/[id]/+page.server.ts
Normal file
19
src/routes/chats/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export async function load({ fetch, params }) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/channels/${params.id}/messages?page=1`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const messages = await res.json();
|
||||||
|
return {
|
||||||
|
messages
|
||||||
|
}
|
||||||
|
}catch (error) {
|
||||||
|
console.error('Erreur lors du chargement des messages:', error);
|
||||||
|
return {
|
||||||
|
messages: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,17 +3,58 @@
|
||||||
import { Button } from "$lib/components/ui/button";
|
import { Button } from "$lib/components/ui/button";
|
||||||
import PaperPlane from "svelte-radix/PaperPlane.svelte";
|
import PaperPlane from "svelte-radix/PaperPlane.svelte";
|
||||||
import Message from "$lib/components/Message.svelte";
|
import Message from "$lib/components/Message.svelte";
|
||||||
|
import UserChat from '$lib/components/ui/UserChat.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
export let messages = data.messages;
|
||||||
|
export let users = data.users; // Liste des utilisateurs
|
||||||
|
let selectedUser = null; // Utilisateur actuellement sélectionné
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full">
|
<div class="h-full flex">
|
||||||
<div class="px-10 flex flex-col gap-5 h-[90%]">
|
<!-- Liste des utilisateurs (colonne gauche) -->
|
||||||
<Message username="Yanax" messageContent="Salut les amis" />
|
<div class="w-1/4 bg-gray-100 border-r overflow-y-auto">
|
||||||
<Message username="Luxray" messageContent="Salut Yanax" />
|
<h2 class="text-3xl font-bold px-4 mt-5">Utilisateurs</h2>
|
||||||
|
<div class="flex flex-col m-5 gap-2">
|
||||||
|
{#each users as user}
|
||||||
|
<UserChat
|
||||||
|
profilePicture={user.profilePicture}
|
||||||
|
username={user.username}
|
||||||
|
status={user.status}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed bottom-5 px-10 w-full flex gap-2">
|
</div>
|
||||||
<Textarea class="h-20 resize-none" placeholder="Messsage..." />
|
|
||||||
<Button size="icon" class="h-20 w-20">
|
<!-- Chat principal (colonne droite) -->
|
||||||
<PaperPlane class="h-18 w-18" />
|
<div class="flex-1 flex flex-col h-full">
|
||||||
|
<!-- Messages -->
|
||||||
|
<div class="m-10 flex flex-col gap-5 overflow-y-auto flex-grow ">
|
||||||
|
<!-- Afficher les messages (mock d'un utilisateur sélectionné ou aucun message par défaut) -->
|
||||||
|
{#if messages.length > 0}
|
||||||
|
{#each messages as message}
|
||||||
|
<Message username={message.username} messageContent={message.messageContent} />
|
||||||
|
{/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..." />
|
||||||
|
<Button size="icon" class="h-16 w-16 bg-blue-500 hover:bg-blue-600 h-full">
|
||||||
|
<PaperPlane class="h-6 w-6" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.h-full {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.selected {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
Loading…
Add table
Reference in a new issue