From c215d4e9635429abbb961b9c1eed250f178aa3a3 Mon Sep 17 00:00:00 2001 From: DanilsGit Date: Tue, 10 Jun 2025 22:26:52 -0500 Subject: [PATCH] Add login with google and direct chats --- app/(auth)/login/_components/DesktopForm.tsx | 4 + app/(auth)/login/_components/MobileForm.tsx | 2 + app/(auth)/login/page.tsx | 6 +- app/(protected)/dashboard/_apis/chats.ts | 34 ++++++ app/(protected)/dashboard/_apis/messages.ts | 10 ++ app/(protected)/dashboard/_apis/rooms.ts | 3 +- .../dashboard/_components/ChatList.tsx | 32 +++++ .../dashboard/_components/ChatsInit.tsx | 72 ++++++++++++ .../dashboard/_components/RoomList.tsx | 4 +- .../dashboard/_components/UserModal.tsx | 56 +++++++++ .../dashboard/_hooks/useUserProfile.ts | 2 + app/(protected)/dashboard/chat/[id]/page.tsx | 44 +++++++ .../chat/_components/MessageList.tsx | 23 ++++ .../dashboard/chat/hooks/useChatMessages.ts | 26 ++++ .../dashboard/information/page.tsx | 2 +- app/(protected)/dashboard/my-profile/page.tsx | 27 +++-- app/(protected)/dashboard/page.tsx | 18 ++- app/(protected)/dashboard/room/[id]/page.tsx | 11 +- .../dashboard/room/_components/CreateChat.tsx | 4 +- .../dashboard/room/_components/Message.tsx | 22 +++- .../room/_components/MessagesList.tsx | 35 ++---- .../dashboard/room/hooks/useRoomMessages.ts | 26 ++++ app/_components/AsideDashboard.tsx | 23 +--- app/_components/GoogleLoginButton.tsx | 29 +++++ app/_components/Header.tsx | 3 +- app/_hooks/useAuth.tsx | 25 +++- app/_hooks/useChatSocket.tsx | 111 ++++++++---------- app/_hooks/useRoomSocket.tsx | 103 ++++++++++++++++ app/_lib/_firebase/firebase.config.ts | 3 +- app/_lib/_interfaces/IChat.ts | 10 ++ app/_lib/_interfaces/IMessage.ts | 1 + app/_ui/globals.css | 20 ++++ middleware.ts | 7 +- package-lock.json | 10 ++ package.json | 1 + public/google.svg | 9 ++ 36 files changed, 675 insertions(+), 143 deletions(-) create mode 100644 app/(protected)/dashboard/_apis/chats.ts create mode 100644 app/(protected)/dashboard/_components/ChatList.tsx create mode 100644 app/(protected)/dashboard/_components/ChatsInit.tsx create mode 100644 app/(protected)/dashboard/_components/UserModal.tsx create mode 100644 app/(protected)/dashboard/chat/[id]/page.tsx create mode 100644 app/(protected)/dashboard/chat/_components/MessageList.tsx create mode 100644 app/(protected)/dashboard/chat/hooks/useChatMessages.ts create mode 100644 app/(protected)/dashboard/room/hooks/useRoomMessages.ts create mode 100644 app/_components/GoogleLoginButton.tsx create mode 100644 app/_hooks/useRoomSocket.tsx create mode 100644 app/_lib/_interfaces/IChat.ts create mode 100644 public/google.svg diff --git a/app/(auth)/login/_components/DesktopForm.tsx b/app/(auth)/login/_components/DesktopForm.tsx index b4c8d71..175b03e 100644 --- a/app/(auth)/login/_components/DesktopForm.tsx +++ b/app/(auth)/login/_components/DesktopForm.tsx @@ -1,4 +1,5 @@ 'use client'; +import GoogleLoginButton from '@/app/_components/GoogleLoginButton'; import { useFormStatus } from 'react-dom'; interface Props { setMode: (mode: string) => void; @@ -44,6 +45,8 @@ export default function DesktopForm({ setMode, registerAction, loginAction }: Pr /> + +

¿Aún no tienes una cuenta?

@@ -53,6 +56,7 @@ export default function DesktopForm({ setMode, registerAction, loginAction }: Pr
+ {/* REGISTRO */}

Regístrate y Conecta

diff --git a/app/(auth)/login/_components/MobileForm.tsx b/app/(auth)/login/_components/MobileForm.tsx index 43c1a57..a62dddd 100644 --- a/app/(auth)/login/_components/MobileForm.tsx +++ b/app/(auth)/login/_components/MobileForm.tsx @@ -1,4 +1,5 @@ 'use client'; +import GoogleLoginButton from '@/app/_components/GoogleLoginButton'; import { useState } from 'react'; interface Props { @@ -48,6 +49,7 @@ export default function MobileForm({ registerAction, loginAction }: Props) { /> +

¿Aún no tienes una cuenta?

diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 95726b0..9e4fd3d 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -15,12 +15,10 @@ export default function Login() { await login(payload); router.push('/dashboard'); }; - const handleRegister = async (payload: FormData) => { await register(payload); router.push('/dashboard'); }; - return (
multicolor
{/* Desktop */} @@ -42,7 +40,7 @@ export default function Login() { alt="multicolor" width={0} height={0} - className={`hidden lg:block absolute w-[50vw] transition-all duration-1000 ease-in-out ${mode === 'login' ? 'right-0' : 'right-[-45%]'} rounded-bl-2xl rounded-tl-2xl`} + className={`hidden lg:block absolute w-[50vw] transition-all duration-1000 ease-in-out ${mode === 'login' ? 'right-0' : 'right-[-45%]'} rounded-bl-2xl rounded-tl-2xl bg-black`} />
); diff --git a/app/(protected)/dashboard/_apis/chats.ts b/app/(protected)/dashboard/_apis/chats.ts new file mode 100644 index 0000000..4dea0d2 --- /dev/null +++ b/app/(protected)/dashboard/_apis/chats.ts @@ -0,0 +1,34 @@ +import myAxios from '@/app/_apis/myAxios.config'; +import { IChat } from '@/app/_lib/_interfaces/IChat'; +import { toast } from 'react-toastify'; + +export const createChat = async (otherUserId: string) => { + try { + const res = await myAxios.post(`/api/v1/chat/direct/${otherUserId}`); + return res.data; + } catch (error) { + console.error('Error creating chat message:', error); + toast.error('Error al crear el chat'); + return null; + } +}; + +export const getChatById = async (chatId: string) => { + try { + const res = await myAxios.get(`/api/v1/chat/direct/${chatId}`); + return res.data; + } catch (error) { + console.error('Error fetching chat by ID:', error); + return null; + } +}; + +export const getAllChats = async () => { + try { + const res = await myAxios.get('/api/v1/chat/direct/me'); + return res.data; + } catch (error) { + console.error('Error fetching chats:', error); + return []; + } +}; diff --git a/app/(protected)/dashboard/_apis/messages.ts b/app/(protected)/dashboard/_apis/messages.ts index 3568b88..d09aac7 100644 --- a/app/(protected)/dashboard/_apis/messages.ts +++ b/app/(protected)/dashboard/_apis/messages.ts @@ -1,3 +1,4 @@ +'use client'; import myAxios from '@/app/_apis/myAxios.config'; import { IMessage } from '@/app/_lib/_interfaces/IMessage'; @@ -10,3 +11,12 @@ export const getRoomMessages = async (roomId: string) => { return []; } }; + +export const getChatMessages = async (chatId: string) => { + try { + const res = await myAxios.get(`/api/v1/chat/direct/${chatId}/messages`); + return res.data; + } catch { + return []; + } +}; diff --git a/app/(protected)/dashboard/_apis/rooms.ts b/app/(protected)/dashboard/_apis/rooms.ts index 2567d1c..5950071 100644 --- a/app/(protected)/dashboard/_apis/rooms.ts +++ b/app/(protected)/dashboard/_apis/rooms.ts @@ -7,14 +7,13 @@ import { toast } from 'react-toastify'; export const createRoom = async (data: ICreateRoom) => { const { name, description, userIds } = data; - console.log('data', data); if (!name || !description || !userIds) { toast.error('Por favor completa todos los campos'); return null; } try { - const res = await myAxios.post('/api/v1/chat/rooms', data); + const res = await myAxios.post('/api/v1/chat/rooms', data); toast.success('Sala creada con éxito'); return res.data; } catch (error: any) { diff --git a/app/(protected)/dashboard/_components/ChatList.tsx b/app/(protected)/dashboard/_components/ChatList.tsx new file mode 100644 index 0000000..d324340 --- /dev/null +++ b/app/(protected)/dashboard/_components/ChatList.tsx @@ -0,0 +1,32 @@ +import { IChat } from '@/app/_lib/_interfaces/IChat'; +import Link from 'next/link'; + +interface Props { + chats: IChat[]; +} + +export default function ChatList({ chats }: Props) { + return ( + <> + {chats.map(sala => ( +
+ +

+ Chat con {sala.userIds[0]} +

+
+

+ + {new Date(sala.createdAt).toLocaleDateString('es-ES')} + +

+
+ +
+ ))} + + ); +} diff --git a/app/(protected)/dashboard/_components/ChatsInit.tsx b/app/(protected)/dashboard/_components/ChatsInit.tsx new file mode 100644 index 0000000..ddea3e4 --- /dev/null +++ b/app/(protected)/dashboard/_components/ChatsInit.tsx @@ -0,0 +1,72 @@ +import { IChat } from '@/app/_lib/_interfaces/IChat'; +import { IMessage } from '@/app/_lib/_interfaces/IMessage'; +import { IRoom } from '@/app/_lib/_interfaces/IRoom'; +import Message from '../room/_components/Message'; +import { useState } from 'react'; +import MessageInput from '../room/_components/MessageInput'; +import MessagesInputBlocked from '../room/_components/MessagesInputBlocked'; +import UserModal from './UserModal'; +import { useAuth } from '@/app/_hooks/useAuth'; +import useIsBottom from '@/app/_hooks/useScroll'; + +interface Props { + room?: IRoom; + chat?: IChat; + messages: IMessage[]; + pendingMessages: IMessage[]; + sendMessage: (msg: string) => void; +} + +export default function ChatsInit({ + room, + chat, + messages, + pendingMessages = [], + sendMessage, +}: Props) { + const user = useAuth(state => state.user); + const [userSelected, setUserSelected] = useState<{ userId: string; displayName: string } | null>( + null + ); + const { containerRef, bottomRef } = useIsBottom({ items: [pendingMessages, messages] }); + + const handleUserSelect = (user: { userId: string; displayName: string }) => { + setUserSelected(user); + }; + + const handleCloseUserModal = () => { + setUserSelected(null); + }; + + const canSendMessage = () => { + if (room) { + return room.members.some(member => member === user?.uid); + } + if (chat) { + return true; + } + return false; + }; + + return ( + <> +
+ {messages.map(msg => ( + + ))} + {pendingMessages.map(msg => ( + + ))} +
+
+
+ {canSendMessage() ? ( + + ) : ( + room && + )} +
+ {userSelected && } + + ); +} diff --git a/app/(protected)/dashboard/_components/RoomList.tsx b/app/(protected)/dashboard/_components/RoomList.tsx index 10cda39..566428a 100644 --- a/app/(protected)/dashboard/_components/RoomList.tsx +++ b/app/(protected)/dashboard/_components/RoomList.tsx @@ -32,7 +32,7 @@ export default function RoomList({ rooms }: Props) {

-
+ {/*
{CanExitRoom(sala) && ( -
+
*/} ))} diff --git a/app/(protected)/dashboard/_components/UserModal.tsx b/app/(protected)/dashboard/_components/UserModal.tsx new file mode 100644 index 0000000..5fa6303 --- /dev/null +++ b/app/(protected)/dashboard/_components/UserModal.tsx @@ -0,0 +1,56 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { createChat } from '../_apis/chats'; +import { useRouter } from 'next/navigation'; + +interface Props { + user: { userId: string; displayName: string } | null; + handleClose: () => void; +} +export default function UserModal({ handleClose, user }: Props) { + const queryClient = useQueryClient(); + const router = useRouter(); + const open = Boolean(user); + + const handleClickOutside = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + handleClose(); + } + }; + + const mutation = useMutation({ + mutationFn: (data: string) => createChat(data), + onSuccess: res => { + if (!res) return; + queryClient.invalidateQueries({ queryKey: [`all-chats-dashboard`] }); + router.push(`/dashboard/chat/${res.id}`); + }, + }); + + if (!user) return null; + + return ( +
+
+

Envía un mensaje directo

+

Puedes establecer una conversación privada con {user.displayName}

+
+ + +
+
+
+ ); +} diff --git a/app/(protected)/dashboard/_hooks/useUserProfile.ts b/app/(protected)/dashboard/_hooks/useUserProfile.ts index b0ee7ed..27aad53 100644 --- a/app/(protected)/dashboard/_hooks/useUserProfile.ts +++ b/app/(protected)/dashboard/_hooks/useUserProfile.ts @@ -8,6 +8,8 @@ import { toast } from 'react-toastify'; export const useUserProfile = () => { const { user } = useAuth(); + console.log(user); + const [loading, setLoading] = useState(true); const [userForm, setUserForm] = useState({ displayName: user?.displayName || '', diff --git a/app/(protected)/dashboard/chat/[id]/page.tsx b/app/(protected)/dashboard/chat/[id]/page.tsx new file mode 100644 index 0000000..535fda2 --- /dev/null +++ b/app/(protected)/dashboard/chat/[id]/page.tsx @@ -0,0 +1,44 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { use } from 'react'; +import MessagesList from '../_components/MessageList'; +import { getChatById } from '../../_apis/chats'; +import { useChatMessages } from '../hooks/useChatMessages'; + +interface RouterProps { + params: Promise<{ + id: string; + }>; +} +export default function ChatPage({ params }: RouterProps) { + const id = use(params).id; + + const { messages, loading } = useChatMessages({ id }); + + const { data: chat, isLoading: chatLoading } = useQuery({ + queryKey: [`chat-${id}`], + queryFn: () => getChatById(id), + }); + + if (loading || chatLoading) return ; + + if (!chat) { + return ( +
+

Chat no encontrado

+
+ ); + } + + return ( +
+
+
+

Chat Directo

+
+
+ {chat && } +
+ ); +} diff --git a/app/(protected)/dashboard/chat/_components/MessageList.tsx b/app/(protected)/dashboard/chat/_components/MessageList.tsx new file mode 100644 index 0000000..2e0e93c --- /dev/null +++ b/app/(protected)/dashboard/chat/_components/MessageList.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { IMessage } from '@/app/_lib/_interfaces/IMessage'; +import { IChat } from '@/app/_lib/_interfaces/IChat'; +import ChatsInit from '../../_components/ChatsInit'; +import { useChatSocket } from '@/app/_hooks/useChatSocket'; + +interface Props { + chat: IChat; + initial_messages: IMessage[]; +} + +export default function MessagesList({ chat, initial_messages }: Props) { + const { messages, pendingMessages, sendMessage } = useChatSocket({ initial_messages, chat }); + return ( + + ); +} diff --git a/app/(protected)/dashboard/chat/hooks/useChatMessages.ts b/app/(protected)/dashboard/chat/hooks/useChatMessages.ts new file mode 100644 index 0000000..95ea2c9 --- /dev/null +++ b/app/(protected)/dashboard/chat/hooks/useChatMessages.ts @@ -0,0 +1,26 @@ +import { IMessage } from '@/app/_lib/_interfaces/IMessage'; +import { useEffect, useState } from 'react'; +import { getChatMessages } from '../../_apis/messages'; + +interface Props { + id: string; +} + +export const useChatMessages = ({ id }: Props) => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const getMessages = async () => { + const newMessages = await getChatMessages(id); + setMessages(newMessages); + setLoading(false); + }; + getMessages(); + }, [id]); + + return { + messages, + loading, + }; +}; diff --git a/app/(protected)/dashboard/information/page.tsx b/app/(protected)/dashboard/information/page.tsx index 865eb4c..0667a7d 100644 --- a/app/(protected)/dashboard/information/page.tsx +++ b/app/(protected)/dashboard/information/page.tsx @@ -4,7 +4,7 @@ import CreateChat from '../room/_components/CreateChat'; export default function InformationPage() { return (
-
+

Información

diff --git a/app/(protected)/dashboard/my-profile/page.tsx b/app/(protected)/dashboard/my-profile/page.tsx index 02ca6e7..4371877 100644 --- a/app/(protected)/dashboard/my-profile/page.tsx +++ b/app/(protected)/dashboard/my-profile/page.tsx @@ -3,18 +3,23 @@ import { WrenchIcon } from '@/app/_ui/icons'; import Image from 'next/image'; import { useUserProfile } from '../_hooks/useUserProfile'; import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { useAuth } from '@/app/_hooks/useAuth'; +import { useRouter } from 'next/navigation'; export default function MyProfile() { const { userForm, handleChange, handleSubmit, loading } = useUserProfile(); + const { logout } = useAuth(); + const router = useRouter(); + const handleLogout = async () => { + await logout(); + router.push('/login'); + }; if (loading) return ; return ( -
- -
+
+

@@ -22,7 +27,7 @@ export default function MyProfile() { ofensivas!

-
+ {/*
Eliminar foto -
+
*/}
-
+
@@ -100,6 +105,12 @@ export default function MyProfile() { Guardar cambios +
); diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 51cfcb7..bb35bcf 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -3,6 +3,8 @@ import { useQuery } from '@tanstack/react-query'; import { getRoomsImIn } from './_apis/rooms'; import RoomList from './_components/RoomList'; import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { getAllChats } from './_apis/chats'; +import ChatList from './_components/ChatList'; export default function Dashboard() { const { data: rooms, isLoading } = useQuery({ @@ -10,7 +12,12 @@ export default function Dashboard() { queryFn: getRoomsImIn, }); - if (isLoading) return ; + const { data: chats, isLoading: chatsLoading } = useQuery({ + queryKey: ['all-chats-dashboard'], + queryFn: getAllChats, + }); + + if (chatsLoading || isLoading) return ; return (
@@ -18,6 +25,15 @@ export default function Dashboard() {

Mis Chats

+

Conversaciones

+ {chats ? ( + + ) : ( +
+

Aun no tienes conversaciones

+
+ )} +

Salas grupales

{rooms ? ( ) : ( diff --git a/app/(protected)/dashboard/room/[id]/page.tsx b/app/(protected)/dashboard/room/[id]/page.tsx index b7e5c5e..9293c2f 100644 --- a/app/(protected)/dashboard/room/[id]/page.tsx +++ b/app/(protected)/dashboard/room/[id]/page.tsx @@ -3,8 +3,8 @@ import { useQuery } from '@tanstack/react-query'; import MessagesList from '../_components/MessagesList'; import LoadingEmoji from '@/app/_components/LoadingEmoji'; import { use } from 'react'; -import { getRoomMessages } from '../../_apis/messages'; import { getRoomById } from '../../_apis/rooms'; +import { useRoomMessages } from '../hooks/useRoomMessages'; interface RouterProps { params: Promise<{ @@ -14,23 +14,20 @@ interface RouterProps { export default function RoomPage({ params }: RouterProps) { const id = use(params).id; - const { data: messages, isLoading: messagesLoading } = useQuery({ - queryKey: [`room-messages-${id}`], - queryFn: () => getRoomMessages(id), - }); + const { messages, loading } = useRoomMessages({ id }); const { data: room, isLoading: roomLoading } = useQuery({ queryKey: [`room-${id}`], queryFn: () => getRoomById(id), }); - if (messagesLoading || roomLoading) return ; + if (loading || roomLoading) return ; return (

{room?.name}

-

Pública

+

{room?.isPrivate ? 'Privada' : 'Pública'}

{room && }
diff --git a/app/(protected)/dashboard/room/_components/CreateChat.tsx b/app/(protected)/dashboard/room/_components/CreateChat.tsx index bde8d24..6edf644 100644 --- a/app/(protected)/dashboard/room/_components/CreateChat.tsx +++ b/app/(protected)/dashboard/room/_components/CreateChat.tsx @@ -6,10 +6,11 @@ import { useState } from 'react'; import { createRoom } from '../../_apis/rooms'; import { ICreateRoom } from '@/app/_lib/_interfaces/IRoom'; import { auth } from '@/app/_lib/_firebase/firebase.config'; +import { useRouter } from 'next/navigation'; export default function CreateChat() { const [createMode, setCreateMode] = useState(false); - + const router = useRouter(); const queryClient = useQueryClient(); const mutation = useMutation({ @@ -18,6 +19,7 @@ export default function CreateChat() { if (!res) return; queryClient.invalidateQueries({ queryKey: [`all-rooms-dashboard`] }); setCreateMode(false); + router.push(`/dashboard/room/${res.id}`); }, }); diff --git a/app/(protected)/dashboard/room/_components/Message.tsx b/app/(protected)/dashboard/room/_components/Message.tsx index 4707c8f..6fcb8da 100644 --- a/app/(protected)/dashboard/room/_components/Message.tsx +++ b/app/(protected)/dashboard/room/_components/Message.tsx @@ -1,7 +1,9 @@ +import { useAuth } from '@/app/_hooks/useAuth'; import { IMessage } from '@/app/_lib/_interfaces/IMessage'; interface Props { message: IMessage; + selectUser: (user: { userId: string; displayName: string }) => void; } const colorList = [ @@ -26,15 +28,31 @@ function stringToRGBa(str: string) { return colorList[index]; } -export default function Message({ message }: Props) { +export default function Message({ message, selectUser }: Props) { + const { user } = useAuth(); const color = stringToRGBa(message.userId); + const handleSelectUser = () => { + if (message.userId !== user?.uid) { + selectUser({ + userId: message.userId, + displayName: message.displayName, + }); + } + }; + return (
-

{message.userId}

+ {message.userId !== user?.uid ? ( +

+ {message.displayName} +

+ ) : ( +

+ )}

{message.content}

{message.status === 'pending' diff --git a/app/(protected)/dashboard/room/_components/MessagesList.tsx b/app/(protected)/dashboard/room/_components/MessagesList.tsx index fb4012d..416c36c 100644 --- a/app/(protected)/dashboard/room/_components/MessagesList.tsx +++ b/app/(protected)/dashboard/room/_components/MessagesList.tsx @@ -1,13 +1,9 @@ 'use client'; -import { useChatSocket } from '@/app/_hooks/useChatSocket'; import { IMessage } from '@/app/_lib/_interfaces/IMessage'; -import Message from './Message'; -import useIsBottom from '@/app/_hooks/useScroll'; -import MessageInput from './MessageInput'; import { IRoom } from '@/app/_lib/_interfaces/IRoom'; -import { useAuth } from '@/app/_hooks/useAuth'; -import MessagesInputBlocked from './MessagesInputBlocked'; +import ChatsInit from '../../_components/ChatsInit'; +import { useRoomSocket } from '@/app/_hooks/useRoomSocket'; interface Props { room: IRoom; @@ -15,27 +11,14 @@ interface Props { } export default function MessagesList({ room, initial_messages }: Props) { - const { messages, sendMessage } = useChatSocket({ room, initial_messages }); - const { containerRef, bottomRef } = useIsBottom({ items: messages }); - const user = useAuth(state => state.user); - - const canSendMessage = () => room?.members.some(member => member === user?.uid); + const { messages, sendMessage, pendingMessages } = useRoomSocket({ room, initial_messages }); return ( - <> -

- {messages.map(msg => ( - - ))} -
-
-
- {canSendMessage() ? ( - - ) : ( - - )} -
- + ); } diff --git a/app/(protected)/dashboard/room/hooks/useRoomMessages.ts b/app/(protected)/dashboard/room/hooks/useRoomMessages.ts new file mode 100644 index 0000000..f09ab42 --- /dev/null +++ b/app/(protected)/dashboard/room/hooks/useRoomMessages.ts @@ -0,0 +1,26 @@ +import { IMessage } from '@/app/_lib/_interfaces/IMessage'; +import { useEffect, useState } from 'react'; +import { getRoomMessages } from '../../_apis/messages'; + +interface Props { + id: string; +} + +export const useRoomMessages = ({ id }: Props) => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const getMessages = async () => { + const newMessages = await getRoomMessages(id); + setMessages(newMessages); + setLoading(false); + }; + getMessages(); + }, [id]); + + return { + messages, + loading, + }; +}; diff --git a/app/_components/AsideDashboard.tsx b/app/_components/AsideDashboard.tsx index ad8a017..d359afc 100644 --- a/app/_components/AsideDashboard.tsx +++ b/app/_components/AsideDashboard.tsx @@ -1,7 +1,7 @@ 'use client'; import Image from 'next/image'; import Link from 'next/link'; -import { AnonymousMaskIcon, InfoIcon, LinkIcon, RoomIcon } from '../_ui/icons'; +import { AnonymousMaskIcon, InfoIcon, RoomIcon } from '../_ui/icons'; import { useIsMobileLarge } from '../_hooks/useIsMobileLarge'; import { useAuth } from '../_hooks/useAuth'; import { useRouter } from 'next/navigation'; @@ -44,12 +44,6 @@ export default function AsideDashboard() { Descubre -
  • - - - Autores - -
  • @@ -76,27 +70,22 @@ export default function AsideDashboard() { )} {/* Mobile aside */} {isMobileLg && ( -
    • @@ -80,7 +80,6 @@ export default function Header() { > Parchemos - Entrar como Invitado
    diff --git a/app/_hooks/useAuth.tsx b/app/_hooks/useAuth.tsx index c1daf65..72b8fbc 100644 --- a/app/_hooks/useAuth.tsx +++ b/app/_hooks/useAuth.tsx @@ -1,3 +1,4 @@ +'use client'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { toast } from 'react-toastify'; import { create } from 'zustand'; @@ -6,17 +7,20 @@ import { onAuthStateChanged, setPersistence, signInWithEmailAndPassword, + signInWithPopup, User, } from 'firebase/auth'; -import { auth } from '../_lib/_firebase/firebase.config'; +import { auth, googleProvider } from '../_lib/_firebase/firebase.config'; import { loginSchema, registerSchema } from '../(auth)/login/_lib/_schemas/auth'; import { registerUser } from '../_apis/auth'; import { createSession, deleteSession } from '../(auth)/login/_lib/_actions/session'; +import { GoogleAuthProvider } from 'firebase/auth/web-extension'; type Auth = { user: User | null | undefined; initializeAuth: () => void; login: (formData: FormData) => Promise; + loginWithGoogle: () => Promise; register: (formData: FormData) => Promise; logout: () => Promise; }; @@ -65,6 +69,25 @@ export const useAuth = create()(set => ({ toast.error(mensaje); } }, + loginWithGoogle: async () => { + try { + await setPersistence(auth, browserLocalPersistence); + + // Iniciar sesión con Google usando el proveedor de autenticación + const result = await signInWithPopup(auth, googleProvider); + const credential = GoogleAuthProvider.credentialFromResult(result); + const token = credential?.accessToken; + + if (!token) { + throw new Error('No se pudo obtener el token de acceso de Google'); + } + + // Crear una sesión con el token obtenido + await createSession(token); + } catch (error) { + console.error('Error during Google login:', error); + } + }, register: async (formData: FormData) => { const result = registerSchema.safeParse(Object.fromEntries(formData.entries())); diff --git a/app/_hooks/useChatSocket.tsx b/app/_hooks/useChatSocket.tsx index 10c2896..f8eebec 100644 --- a/app/_hooks/useChatSocket.tsx +++ b/app/_hooks/useChatSocket.tsx @@ -1,19 +1,58 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; import { IMessage } from '../_lib/_interfaces/IMessage'; -import { IRoom } from '../_lib/_interfaces/IRoom'; import { auth } from '../_lib/_firebase/firebase.config'; import { toast } from 'react-toastify'; +import { IChat } from '../_lib/_interfaces/IChat'; interface Props { - room: IRoom; + chat: IChat; initial_messages: IMessage[]; } -export function useChatSocket({ initial_messages, room }: Props) { +export function useChatSocket({ initial_messages, chat }: Props) { const [messages, setMessages] = useState(initial_messages); + const [pendingMessages, setPendingMessages] = useState([]); const socketRef = useRef(null); + const receiveMessage = (data: IMessage) => { + setPendingMessages(prev => prev.filter(msg => msg.content !== data.content)); + + setMessages(prev => { + return [...prev, data]; + }); + }; + + const sendMessage = (msg: string) => { + if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { + // Genera un ID temporal único (puedes usar Date.now() o UUID) + // const tempId = `pending-${Date.now()}`; + // Añade el mensaje al estado local como "pendiente" + // setPendingMessages(prev => [ + // ...prev, + // { + // id: tempId, + // content: msg, + // createdAt: new Date().toISOString(), + // status: 'pending', + // roomId: room.id, + // userId: auth.currentUser?.uid, + // } as IMessage, + // ]); + // Envía el mensaje al servidor + const messageToSend = { + type: 'DIRECT_CHAT', + payload: { + content: msg, + roomID: chat.id, + type: 'text', + }, + timestamp: new Date().toISOString(), + }; + socketRef.current.send(JSON.stringify(messageToSend)); + } + }; + useEffect(() => { const initSocket = async () => { const token = await auth.currentUser?.getIdToken(); @@ -23,11 +62,10 @@ export function useChatSocket({ initial_messages, room }: Props) { socketRef.current = socket; socket.onopen = () => { - console.log('WebSocket abierto'); socket.send( JSON.stringify({ - type: 'JOIN_ROOM', - payload: room.id, + type: 'JOIN_DIRECT_CHAT', + payload: chat.id, timestamp: new Date().toISOString(), }) ); @@ -57,64 +95,7 @@ export function useChatSocket({ initial_messages, room }: Props) { return () => { socketRef.current?.close(); }; - }, [room]); - - const receiveMessage = (data: IMessage) => { - setMessages(prev => { - // Busca y reemplaza el mensaje pendiente (si existe) - const updatedMessages = prev.map(msg => { - // Compara por contenido y timestamp aproximado (o usa otro criterio único) - if ( - msg.status === 'pending' && - msg.content === data.content && - // Si el backend devuelve el mismo timestamp (o casi) - Math.abs(new Date(msg.createdAt).getTime() - new Date(data.createdAt).getTime()) < 1000 - ) { - return { ...data, status: 'delivered' } as IMessage; // Mensaje confirmado - } - return msg; - }); - - // Si no era un mensaje pendiente, añádelo normalmente - if (!updatedMessages.some(msg => msg.id === data.id)) { - updatedMessages.push({ ...data, status: 'delivered' }); - } - - return updatedMessages; - }); - }; - - const sendMessage = (msg: string) => { - if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { - // Genera un ID temporal único (puedes usar Date.now() o UUID) - const tempId = `pending-${Date.now()}`; - - // Añade el mensaje al estado local como "pendiente" - setMessages(prev => [ - ...prev, - { - id: tempId, - content: msg, - createdAt: new Date().toISOString(), // Timestamp local - status: 'pending', - roomId: room.id, - userId: auth.currentUser?.uid, - } as IMessage, - ]); - - // Envía el mensaje al servidor - const messageToSend = { - type: 'CHAT_ROOM', - payload: { - content: msg, - roomID: room.id, - type: 'text', - }, - timestamp: new Date().toISOString(), - }; - socketRef.current.send(JSON.stringify(messageToSend)); - } - }; + }, [chat]); - return { messages, sendMessage }; + return { messages, sendMessage, pendingMessages }; } diff --git a/app/_hooks/useRoomSocket.tsx b/app/_hooks/useRoomSocket.tsx new file mode 100644 index 0000000..de6859d --- /dev/null +++ b/app/_hooks/useRoomSocket.tsx @@ -0,0 +1,103 @@ +'use client'; +import { useEffect, useRef, useState } from 'react'; +import { IMessage } from '../_lib/_interfaces/IMessage'; +import { IRoom } from '../_lib/_interfaces/IRoom'; +import { auth } from '../_lib/_firebase/firebase.config'; +import { toast } from 'react-toastify'; + +interface Props { + room: IRoom; + initial_messages: IMessage[]; +} + +export function useRoomSocket({ initial_messages, room }: Props) { + const [messages, setMessages] = useState(initial_messages); + const [pendingMessages, setPendingMessages] = useState([]); + const socketRef = useRef(null); + + const receiveMessage = (data: IMessage) => { + setPendingMessages(prev => prev.filter(msg => msg.content !== data.content)); + + setMessages(prev => { + return [...prev, data]; + }); + }; + + const sendMessage = (msg: string) => { + if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { + // Genera un ID temporal único (puedes usar Date.now() o UUID) + const tempId = `pending-${Date.now()}`; + + // Añade el mensaje al estado local como "pendiente" + setPendingMessages(prev => [ + ...prev, + { + id: tempId, + content: msg, + createdAt: new Date().toISOString(), + status: 'pending', + roomId: room.id, + userId: auth.currentUser?.uid, + } as IMessage, + ]); + + // Envía el mensaje al servidor + const messageToSend = { + type: 'CHAT_ROOM', + payload: { + content: msg, + roomID: room.id, + type: 'text', + }, + timestamp: new Date().toISOString(), + }; + socketRef.current.send(JSON.stringify(messageToSend)); + } + }; + + useEffect(() => { + const initSocket = async () => { + const token = await auth.currentUser?.getIdToken(); + const socket = new WebSocket( + `${process.env.NEXT_PUBLIC_API_URL}/api/v1/chat/ws?token=${token}` + ); + socketRef.current = socket; + + socket.onopen = () => { + socket.send( + JSON.stringify({ + type: 'JOIN_ROOM', + payload: room.id, + timestamp: new Date().toISOString(), + }) + ); + }; + + socket.onmessage = event => { + const data = JSON.parse(event.data); + + if (data.type === 'ERROR') { + toast.error(data.payload); + return; + } + + receiveMessage(data.payload); + }; + + socket.onerror = err => { + console.error('WebSocket error:', err); + }; + + socket.onclose = () => { + console.log('WebSocket cerrado'); + }; + }; + + initSocket(); + return () => { + socketRef.current?.close(); + }; + }, [room]); + + return { messages, sendMessage, pendingMessages }; +} diff --git a/app/_lib/_firebase/firebase.config.ts b/app/_lib/_firebase/firebase.config.ts index ca167de..f9aa792 100644 --- a/app/_lib/_firebase/firebase.config.ts +++ b/app/_lib/_firebase/firebase.config.ts @@ -1,5 +1,5 @@ import { getApp, getApps, initializeApp } from 'firebase/app'; -import { getAuth } from 'firebase/auth'; +import { getAuth, GoogleAuthProvider } from 'firebase/auth'; const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, @@ -13,3 +13,4 @@ const firebaseConfig = { export const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig); export const auth = getAuth(app); +export const googleProvider = new GoogleAuthProvider(); diff --git a/app/_lib/_interfaces/IChat.ts b/app/_lib/_interfaces/IChat.ts new file mode 100644 index 0000000..b3014bc --- /dev/null +++ b/app/_lib/_interfaces/IChat.ts @@ -0,0 +1,10 @@ +import { IMessage } from './IMessage'; + +export interface IChat { + id: string; + createdAt: Date | string; + updatedAt: Date | string; + isDeleted: boolean; + lastMessage: IMessage; + userIds: string[]; +} diff --git a/app/_lib/_interfaces/IMessage.ts b/app/_lib/_interfaces/IMessage.ts index ef61e20..7854110 100644 --- a/app/_lib/_interfaces/IMessage.ts +++ b/app/_lib/_interfaces/IMessage.ts @@ -6,5 +6,6 @@ export interface IMessage { isDeleted: boolean; roomId: string; userId: string; + displayName: string; status?: 'pending' | 'delivered'; // Nuevo campo } diff --git a/app/_ui/globals.css b/app/_ui/globals.css index 0914fb2..32b21cd 100644 --- a/app/_ui/globals.css +++ b/app/_ui/globals.css @@ -90,4 +90,24 @@ body { .animate-rotate-medium { animation: rotateImage 5s linear infinite; +} + + +.Modal{ + position: absolute; + background-color: #fff; + min-width: 40vw; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: 1em; + font-size: 1em; + border: none; + border-radius: 1em; + font-family: 'Montserrat', sans-serif; + display: flex; + flex-direction: column; + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.418); + max-height: 90vh; + overflow-y: auto; } \ No newline at end of file diff --git a/middleware.ts b/middleware.ts index b3c7e6a..e32f58b 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,13 +1,14 @@ import { cookies } from 'next/headers'; import { NextRequest, NextResponse } from 'next/server'; - const protectedRoutes = ['/dashboard']; const publicRoutes = ['/login', '/login-gest']; export default async function middleware(req: NextRequest) { const path = req.nextUrl.pathname; - const isProtectedRoute = protectedRoutes.includes(path); - const isPublicRoute = publicRoutes.includes(path); + const isProtectedRoute = protectedRoutes.some( + route => path === route || path.startsWith(route + '/') + ); + const isPublicRoute = publicRoutes.some(route => path === route || path.startsWith(route + '/')); const cookie = (await cookies()).get('session')?.value; const session = cookie ? cookie : null; diff --git a/package-lock.json b/package-lock.json index df814f8..aa8f73f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-modal": "^3.16.3", "autoprefixer": "^10.4.21", "eslint": "^9", "eslint-config-next": "15.2.3", @@ -2438,6 +2439,15 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/react-modal": { + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", diff --git a/package.json b/package.json index 5313dc1..479ef86 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@types/react-modal": "^3.16.3", "autoprefixer": "^10.4.21", "eslint": "^9", "eslint-config-next": "15.2.3", diff --git a/public/google.svg b/public/google.svg new file mode 100644 index 0000000..3bb05d6 --- /dev/null +++ b/public/google.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file