Marketplace inversée (Next.js 16 App Router + Supabase Cloud) : auth, schéma + RLS, publication de besoins avec upload d'images, feed avec filtres, offres, acceptation via RPC, messagerie temps réel, dashboards, et Dockerfile pour déploiement Coolify.
108 lines
2.9 KiB
TypeScript
108 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
import { sendMessage } from "@/lib/actions/messages";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type Message = {
|
|
id: string;
|
|
conversation_id: string;
|
|
sender_id: string;
|
|
body: string;
|
|
created_at: string;
|
|
};
|
|
|
|
export function ChatWindow({
|
|
conversationId,
|
|
currentUserId,
|
|
initialMessages,
|
|
}: {
|
|
conversationId: string;
|
|
currentUserId: string;
|
|
initialMessages: Message[];
|
|
}) {
|
|
const [messages, setMessages] = useState<Message[]>(initialMessages);
|
|
const bottomRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const supabase = createClient();
|
|
const channel = supabase
|
|
.channel(`conversation:${conversationId}`)
|
|
.on(
|
|
"postgres_changes",
|
|
{
|
|
event: "INSERT",
|
|
schema: "public",
|
|
table: "messages",
|
|
filter: `conversation_id=eq.${conversationId}`,
|
|
},
|
|
(payload) => {
|
|
const newMessage = payload.new as Message;
|
|
setMessages((current) => {
|
|
if (current.some((m) => m.id === newMessage.id)) return current;
|
|
return [...current, newMessage];
|
|
});
|
|
}
|
|
)
|
|
.subscribe();
|
|
|
|
return () => {
|
|
supabase.removeChannel(channel);
|
|
};
|
|
}, [conversationId]);
|
|
|
|
useEffect(() => {
|
|
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
}, [messages]);
|
|
|
|
const sendMessageWithConversation = sendMessage.bind(null, conversationId);
|
|
|
|
return (
|
|
<div className="flex h-[60vh] flex-col rounded-lg border sm:h-[70vh]">
|
|
<div className="flex-1 space-y-3 overflow-y-auto p-4">
|
|
{messages.map((message) => {
|
|
const isMine = message.sender_id === currentUserId;
|
|
return (
|
|
<div
|
|
key={message.id}
|
|
className={cn("flex", isMine ? "justify-end" : "justify-start")}
|
|
>
|
|
<div
|
|
className={cn(
|
|
"max-w-[75%] rounded-lg px-3 py-2 text-sm",
|
|
isMine
|
|
? "bg-primary text-primary-foreground"
|
|
: "bg-muted"
|
|
)}
|
|
>
|
|
{message.body}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
<div ref={bottomRef} />
|
|
</div>
|
|
<form
|
|
action={async (formData) => {
|
|
await sendMessageWithConversation(formData);
|
|
(document.getElementById("chat-input") as HTMLInputElement).value =
|
|
"";
|
|
}}
|
|
className="flex gap-2 border-t p-3"
|
|
>
|
|
<Input
|
|
id="chat-input"
|
|
name="body"
|
|
placeholder="Écris un message..."
|
|
autoComplete="off"
|
|
className="flex-1"
|
|
/>
|
|
<Button type="submit">Envoyer</Button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|