Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion app/(protected)/dashboard/_apis/messages.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';
import myAxios from '@/app/_apis/myAxios.config';
import { IMessage } from '@/app/_lib/_interfaces/IMessage';
import axios, { AxiosError } from 'axios';
import { toast } from 'react-toastify';

export const getRoomMessages = async (roomId: string) => {
try {
const res = await myAxios.get<IMessage[]>(`/api/v1/chat/rooms/${roomId}/messages`);
console.log('Fetched room messages:', res.data);
return res.data;
} catch (error) {
console.error('Error fetching room messages:', error);
Expand All @@ -21,3 +22,53 @@ export const getChatMessages = async (chatId: string) => {
return [];
}
};

export const getTranslatedMessage = async (content: string, languageCode?: string) => {
try {
const res = await axios.post<string>(`/api/translate`, {
content,
languageCode: languageCode || 'en',
});
return res.data;
} catch (error) {
console.error('Error translating message:', error);
return null;
}
};

export const sendReport = async (message: IMessage) => {
try {
const res = await myAxios.post(`/api/v1/chat/rooms/${message.roomId}/report`, {
messageId: message.id,
reason: 'Misconduct or inappropriate content',
});

toast.success('Report sent successfully');

return res.data;
} catch (error) {
toast.error(
error instanceof AxiosError
? error.response?.data
: 'An error occurred while sending the report'
);
return null;
}
};

export const clearReports = async (message: IMessage) => {
try {
const res = await myAxios.post(`/api/v1/chat/rooms/${message.roomId}/clear-reports`, {
userId: message.userId,
});
toast.success('Reports cleared successfully');
return res.data;
} catch (error) {
toast.error(
error instanceof AxiosError
? error.response?.data
: 'An error occurred while clearing reports'
);
return null;
}
};
22 changes: 12 additions & 10 deletions app/(protected)/dashboard/_components/ChatsInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,9 @@ export default function ChatsInit({
sendMessage,
}: Props) {
const user = useAuth(state => state.user);
const [userSelected, setUserSelected] = useState<{ userId: string; displayName: string } | null>(
null
);
const [userSelected, setUserSelected] = useState<IMessage | null>(null);
const { containerRef, bottomRef } = useIsBottom({ items: [pendingMessages, messages] });

const handleUserSelect = (user: { userId: string; displayName: string }) => {
setUserSelected(user);
};

const handleCloseUserModal = () => {
setUserSelected(null);
};
Expand All @@ -48,14 +42,20 @@ export default function ChatsInit({
return false;
};

const selectUser = (message: IMessage) => {
if (room) {
setUserSelected(message);
}
};

return (
<>
<div ref={containerRef} className="p-4 h-full flex flex-col gap-5 lg:gap-7 overflow-auto">
{messages.map(msg => (
<Message key={msg.id} message={msg} selectUser={handleUserSelect} />
<Message key={msg.id} message={msg} selectUser={selectUser} />
))}
{pendingMessages.map(msg => (
<Message key={msg.id} message={msg} selectUser={handleUserSelect} />
<Message key={msg.id} message={msg} selectUser={selectUser} />
))}
<div ref={bottomRef} />
</div>
Expand All @@ -66,7 +66,9 @@ export default function ChatsInit({
room && <MessagesInputBlocked room={room} />
)}
</div>
{userSelected && <UserModal user={userSelected} handleClose={handleCloseUserModal} />}
{userSelected && (
<UserModal message={userSelected} handleClose={handleCloseUserModal} room={room} />
)}
</>
);
}
37 changes: 29 additions & 8 deletions app/(protected)/dashboard/_components/UserModal.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
'use client';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { createChat } from '../_apis/chats';
import { useRouter } from 'next/navigation';
import { IMessage } from '@/app/_lib/_interfaces/IMessage';
import { clearReports, sendReport } from '../_apis/messages';
import { IRoom } from '@/app/_lib/_interfaces/IRoom';
import { useAuth } from '@/app/_hooks/useAuth';

interface Props {
user: { userId: string; displayName: string } | null;
room?: IRoom | null;
message: IMessage | null;
handleClose: () => void;
}
export default function UserModal({ handleClose, user }: Props) {
export default function UserModal({ handleClose, message, room }: Props) {
const user = useAuth(state => state.user);
const queryClient = useQueryClient();
const router = useRouter();
const open = Boolean(user);
const open = Boolean(message);

const DoIHaveAdmin = () => room?.admins.some(admin => admin === user?.uid);

const handleClickOutside = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.target === e.currentTarget) {
handleClose();
}
};

const mutation = useMutation({
const mutation_send_message = useMutation({
mutationFn: (data: string) => createChat(data),
onSuccess: res => {
if (!res) return;
Expand All @@ -26,16 +35,28 @@ export default function UserModal({ handleClose, user }: Props) {
},
});

if (!user) return null;
if (!message) return null;

return (
<div
onClick={handleClickOutside}
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/50 ${open ? 'block' : 'hidden'}`}
>
<div className="w-[80%] lg:w-[60%] flex flex-col gap-4 bg-semidarkpurple p-4 rounded-lg shadow-lg">
<h3 className="font-bold">Envía un mensaje directo</h3>
<p>Puedes establecer una conversación privada con {user.displayName}</p>
<div className="flex justify-between items-center">
<h3 className="font-bold">Envía un mensaje directo</h3>
<div className="flex gap-2">
{room && DoIHaveAdmin() && (
<p className="cursor-pointer" onClick={() => clearReports(message)}>
🧹
</p>
)}
<p className="cursor-pointer" onClick={() => sendReport(message)}>
</p>
</div>
</div>
<p>Puedes establecer una conversación privada con {message.displayName}</p>
<div className="flex gap-2">
<button
className="cursor-pointer p-1 w-full rounded-md bg-lightblue"
Expand All @@ -45,7 +66,7 @@ export default function UserModal({ handleClose, user }: Props) {
</button>
<button
className="cursor-pointer p-1 w-full rounded-md bg-purple"
onClick={() => mutation.mutate(user.userId)}
onClick={() => mutation_send_message.mutate(message.userId)}
>
Enviar mensaje
</button>
Expand Down
14 changes: 14 additions & 0 deletions app/(protected)/dashboard/chat/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import AsideRoom from '@/app/_components/AsideRoom';

export default function ChatLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="flex lg:grid lg:grid-cols-[15rem_auto] h-screen w-full overflow-hidden bg-darkpurple">
<AsideRoom />
{children}
</div>
);
}
29 changes: 21 additions & 8 deletions app/(protected)/dashboard/room/_components/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useAuth } from '@/app/_hooks/useAuth';
import { useTranslateMessage } from '@/app/_hooks/useTranslateMessage';
import { IMessage } from '@/app/_lib/_interfaces/IMessage';

interface Props {
message: IMessage;
selectUser: (user: { userId: string; displayName: string }) => void;
selectUser: (message: IMessage) => void;
}

const colorList = [
Expand All @@ -30,20 +31,21 @@ function stringToRGBa(str: string) {

export default function Message({ message, selectUser }: Props) {
const { user } = useAuth();
const color = stringToRGBa(message.userId);
const { translated, translating, handleTranslate } = useTranslateMessage();

const handleSelectUser = () => {
if (message.userId !== user?.uid) {
selectUser({
userId: message.userId,
displayName: message.displayName,
});
selectUser(message);
}
};

if (!message.id) return null;

const color = stringToRGBa(message.userId);

return (
<div
className="relative rounded-2xl py-4 px-4 pr-20 flex flex-col gap-2 w-full h-fit max-w-fit"
className="relative rounded-2xl py-4 px-4 pr-20 flex flex-col gap-2 w-full h-fit max-w-fit group"
style={{ backgroundColor: color }}
>
{message.userId !== user?.uid ? (
Expand All @@ -53,7 +55,18 @@ export default function Message({ message, selectUser }: Props) {
) : (
<p className="text-xs lg:text-sm font-bold">Tú</p>
)}
<p className="text-xs lg:text-sm break-words whitespace-pre-wrap">{message.content}</p>
<p className="text-xs lg:text-sm break-words whitespace-pre-wrap">
{translated ? translated : message.content}
</p>
<div className="absolute opacity-0 group-hover:opacity-100 top-0 left-0 w-full h-full flex items-center justify-end pointer-events-none">
<button
className="pointer-events-auto px-2 cursor-pointer"
disabled={translating}
onClick={() => handleTranslate(message.content)}
>
{translating ? '🔄' : '🌐'}
</button>
</div>
<p className="text-xs text-gray-400 absolute bottom-2 right-2">
{message.status === 'pending'
? '⏱'
Expand Down
6 changes: 3 additions & 3 deletions app/_components/AsideRoom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export default function AsideRoom() {
{chats.map(chat => (
<li key={chat.id} className="text-white p-2 bg-purple-2 rounded-lg mb-2">
<Link href={`/dashboard/chat/${chat.id}`}>
<p className="font-bold">{chat.lastMessage.displayName}</p>
<p>{chat.lastMessage.content}</p>
<p className="font-bold">{chat.lastMessage?.displayName}</p>
<p className="truncate">{chat.lastMessage?.content}</p>
</Link>
</li>
))}
Expand All @@ -51,7 +51,7 @@ export default function AsideRoom() {
<li key={room.id} className="text-white p-2 bg-purple-2 rounded-lg mb-2">
<Link href={`/dashboard/room/${room.id}`}>
<p className="font-bold truncate">{room.name}</p>
<p className="truncate">{room.lastMessage.content}</p>
<p className="truncate">{room.lastMessage?.content}</p>
</Link>
</li>
))}
Expand Down
33 changes: 33 additions & 0 deletions app/_hooks/useTranslateMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useState } from 'react';
import { getTranslatedMessage } from '../(protected)/dashboard/_apis/messages';

export const useTranslateMessage = () => {
const [translated, setTranslated] = useState<string | null>(null);
const [translating, setTranslating] = useState<boolean>(false);

const handleTranslate = async (content: string) => {
setTranslating(true);

if (translated) {
setTranslated(null);
setTranslating(false);
return;
}

const translatedMessage = await getTranslatedMessage(content);

if (translatedMessage) {
setTranslated(translatedMessage);
} else {
setTranslated(null);
}

setTranslating(false);
};

return {
translated,
translating,
handleTranslate,
};
};
2 changes: 1 addition & 1 deletion app/_lib/_interfaces/IChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export interface IChat {
createdAt: Date | string;
updatedAt: Date | string;
isDeleted: boolean;
lastMessage: IMessage;
lastMessage?: IMessage;
userIds: string[];
}
2 changes: 1 addition & 1 deletion app/_lib/_interfaces/IRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface IRoom {
imageUrl: string;
isDeleted: boolean;
isPrivate: boolean;
lastMessage: IMessage;
lastMessage?: IMessage;
members: string[];
name: string;
ownerId: string;
Expand Down
23 changes: 23 additions & 0 deletions app/api/translate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* eslint-disable @typescript-eslint/no-require-imports */
import { NextRequest } from 'next/server';
const { Translate } = require('@google-cloud/translate').v2;

const getGoogleTranslateClient = () => {
const apiKey = process.env.GOOGLE_TRANSLATE_API_KEY;

return new Translate({
key: apiKey,
});
};

interface TranslationRequest {
content: string[];
languageCode: string;
}

export async function POST(req: NextRequest) {
const body: TranslationRequest = await req.json();
const client = getGoogleTranslateClient();
const [translatedText] = await client.translate(body.content, body.languageCode);
return Response.json(translatedText);
}
Loading