From f419921544e3015d19617c8800092ea9caf5a2a4 Mon Sep 17 00:00:00 2001 From: DanilsGit Date: Fri, 16 May 2025 19:23:14 -0500 Subject: [PATCH] add websockets and create rooms --- .github/workflows/develop-CD.yml | 1 + .github/workflows/master-CD.yml | 1 + Dockerfile.local | 2 +- Dockerfile.prod | 2 + app/(protected)/dashboard/_apis/messages.ts | 12 + app/(protected)/dashboard/_apis/rooms.ts | 65 ++ .../dashboard/_components/RoomList.tsx | 140 +--- .../dashboard/_components/RoomSearch.tsx | 48 ++ .../dashboard/_hooks/useUserProfile.ts | 97 +++ .../dashboard/information/loading.tsx | 5 + .../dashboard/information/page.tsx | 24 + app/(protected)/dashboard/my-profile/page.tsx | 106 +++ app/(protected)/dashboard/page.tsx | 25 +- app/(protected)/dashboard/room/[id]/page.tsx | 119 +-- .../dashboard/room/_components/CreateChat.tsx | 109 +++ .../dashboard/room/_components/Message.tsx | 16 +- .../room/_components/MessagesInputBlocked.tsx | 34 + .../room/_components/MessagesList.tsx | 19 +- app/(protected)/layout.tsx | 13 + app/(protected)/loading.tsx | 5 + app/_apis/myAxios.config.ts | 11 +- app/_apis/mySSAxios.config.ts | 28 - app/_components/AsideDashboard.tsx | 26 +- app/_hooks/useAuth.tsx | 36 +- app/_hooks/useChatSocket.tsx | 153 ++-- app/_lib/_interfaces/IMessage.ts | 8 +- app/_lib/_interfaces/IRoom.ts | 23 + app/_lib/_interfaces/IUser.ts | 11 - app/_ui/globals.css | 5 + app/_ui/icons.tsx | 26 + app/layout.tsx | 29 +- app/providers.tsx | 11 + package-lock.json | 790 +++++++++--------- package.json | 1 + 34 files changed, 1257 insertions(+), 744 deletions(-) create mode 100644 app/(protected)/dashboard/_apis/messages.ts create mode 100644 app/(protected)/dashboard/_apis/rooms.ts create mode 100644 app/(protected)/dashboard/_components/RoomSearch.tsx create mode 100644 app/(protected)/dashboard/_hooks/useUserProfile.ts create mode 100644 app/(protected)/dashboard/information/loading.tsx create mode 100644 app/(protected)/dashboard/information/page.tsx create mode 100644 app/(protected)/dashboard/my-profile/page.tsx create mode 100644 app/(protected)/dashboard/room/_components/CreateChat.tsx create mode 100644 app/(protected)/dashboard/room/_components/MessagesInputBlocked.tsx create mode 100644 app/(protected)/loading.tsx delete mode 100644 app/_apis/mySSAxios.config.ts create mode 100644 app/_lib/_interfaces/IRoom.ts delete mode 100644 app/_lib/_interfaces/IUser.ts create mode 100644 app/providers.tsx diff --git a/.github/workflows/develop-CD.yml b/.github/workflows/develop-CD.yml index f0cd50d..86b9279 100644 --- a/.github/workflows/develop-CD.yml +++ b/.github/workflows/develop-CD.yml @@ -21,6 +21,7 @@ jobs: - name: Build Docker image run: | docker build \ + --build-arg NEXT_PUBLIC_API_URL=${{ secrets.API_URL_DEV }} \ --build-arg NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.FIREBASE_API_KEY }} \ --build-arg NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ secrets.FIREBASE_AUTH_DOMAIN }} \ --build-arg NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }} \ diff --git a/.github/workflows/master-CD.yml b/.github/workflows/master-CD.yml index 7487e85..fc64994 100644 --- a/.github/workflows/master-CD.yml +++ b/.github/workflows/master-CD.yml @@ -22,6 +22,7 @@ jobs: - name: Build Docker image run: | docker build \ + --build-arg NEXT_PUBLIC_API_URL=${{ secrets.API_URL_PROD }} \ --build-arg NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.FIREBASE_API_KEY }} \ --build-arg NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=${{ secrets.FIREBASE_AUTH_DOMAIN }} \ --build-arg NEXT_PUBLIC_FIREBASE_PROJECT_ID=${{ secrets.FIREBASE_PROJECT_ID }} \ diff --git a/Dockerfile.local b/Dockerfile.local index 7f1ab7b..07ba684 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -1,4 +1,4 @@ -FROM node:22-slim +FROM node:22-alpine RUN mkdir -p /app diff --git a/Dockerfile.prod b/Dockerfile.prod index 14b440d..e055abd 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -9,6 +9,7 @@ RUN npm install COPY . . +ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_FIREBASE_API_KEY ARG NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ARG NEXT_PUBLIC_FIREBASE_PROJECT_ID @@ -17,6 +18,7 @@ ARG NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID ARG NEXT_PUBLIC_FIREBASE_APP_ID ARG NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_FIREBASE_API_KEY=$NEXT_PUBLIC_FIREBASE_API_KEY ENV NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=$NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN ENV NEXT_PUBLIC_FIREBASE_PROJECT_ID=$NEXT_PUBLIC_FIREBASE_PROJECT_ID diff --git a/app/(protected)/dashboard/_apis/messages.ts b/app/(protected)/dashboard/_apis/messages.ts new file mode 100644 index 0000000..3568b88 --- /dev/null +++ b/app/(protected)/dashboard/_apis/messages.ts @@ -0,0 +1,12 @@ +import myAxios from '@/app/_apis/myAxios.config'; +import { IMessage } from '@/app/_lib/_interfaces/IMessage'; + +export const getRoomMessages = async (roomId: string) => { + try { + const res = await myAxios.get(`/api/v1/chat/rooms/${roomId}/messages`); + return res.data; + } catch (error) { + console.error('Error fetching room messages:', error); + return []; + } +}; diff --git a/app/(protected)/dashboard/_apis/rooms.ts b/app/(protected)/dashboard/_apis/rooms.ts new file mode 100644 index 0000000..2567d1c --- /dev/null +++ b/app/(protected)/dashboard/_apis/rooms.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; + +import myAxios from '@/app/_apis/myAxios.config'; +import { ICreateRoom, IRoom } from '@/app/_lib/_interfaces/IRoom'; +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); + toast.success('Sala creada con éxito'); + return res.data; + } catch (error: any) { + toast.error(error.response?.data || 'Error al crear la sala'); + return null; + } +}; + +export const getRoomsImIn = async () => { + try { + const res = await myAxios.get('/api/v1/chat/rooms/me'); + return res.data; + } catch (error) { + console.error('Error fetching rooms:', error); + return []; + } +}; + +export const getAllRooms = async () => { + try { + const res = await myAxios.get('/api/v1/chat/rooms'); + return res.data.filter(room => !room.isPrivate); + } catch (error) { + console.error('Error fetching all rooms:', error); + return []; + } +}; + +export const getRoomById = async (roomId: string) => { + try { + const res = await myAxios.get(`/api/v1/chat/rooms/${roomId}`); + return res.data; + } catch (error) { + console.error('Error fetching room by ID:', error); + return null; + } +}; + +export const joinRoom = async (roomId: string) => { + try { + const res = await myAxios.post(`/api/v1/chat/rooms/${roomId}/join`); + toast.success('Te has unido al chat'); + return res.data; + } catch (error: any) { + toast.error(error.response?.data || 'Error al unirte al chat'); + return null; + } +}; diff --git a/app/(protected)/dashboard/_components/RoomList.tsx b/app/(protected)/dashboard/_components/RoomList.tsx index 43e114d..10cda39 100644 --- a/app/(protected)/dashboard/_components/RoomList.tsx +++ b/app/(protected)/dashboard/_components/RoomList.tsx @@ -1,125 +1,45 @@ +import { auth } from '@/app/_lib/_firebase/firebase.config'; +import { IRoom } from '@/app/_lib/_interfaces/IRoom'; import { CancelIcon, DoorBellIcon } from '@/app/_ui/icons'; import { Tooltip } from '@mui/material'; import Link from 'next/link'; -const salas = [ - { - id: 1, - name: 'Sala 1', - created_by: 'Usuario 1', - created_at: '2023-10-01', - }, - { - id: 2, - name: 'Sala 2', - created_by: 'Usuario 2', - created_at: '2023-10-02', - }, - { - id: 3, - name: 'Sala 3', - created_by: 'Usuario 3', - created_at: '2023-10-03', - }, - { - id: 4, - name: 'Sala 4', - created_by: 'Usuario 4', - created_at: '2023-10-04', - }, - { - id: 5, - name: 'Sala 5', - created_by: 'Usuario 5', - created_at: '2023-10-05', - }, - { - id: 6, - name: 'Sala 6', - created_by: 'Usuario 6', - created_at: '2023-10-06', - }, - { - id: 7, - name: 'Sala 7', - created_by: 'Usuario 7', - created_at: '2023-10-07', - }, - { - id: 8, - name: 'Sala 8', - created_by: 'Usuario 8', - created_at: '2023-10-08', - }, - { - id: 9, - name: 'Sala 9', - created_by: 'Usuario 9', - created_at: '2023-10-09', - }, - { - id: 10, - name: 'Sala 10', - created_by: 'Usuario 10', - created_at: '2023-10-10', - }, - { - id: 11, - name: 'Sala 11', - created_by: 'Usuario 11', - created_at: '2023-10-11', - }, - { - id: 12, - name: 'Sala 12', - created_by: 'Usuario 12', - created_at: '2023-10-12', - }, - { - id: 13, - name: 'Sala 13', - created_by: 'Usuario 13', - created_at: '2023-10-13', - }, - { - id: 14, - name: 'Sala 14', - created_by: 'Usuario 14', - created_at: '2023-10-14', - }, - { - id: 15, - name: 'Sala 15', - created_by: 'Usuario 15', - created_at: '2023-10-14', - }, -]; +interface Props { + rooms: IRoom[]; +} + +export default function RoomList({ rooms }: Props) { + const userId = auth.currentUser?.uid; + + const CanExitRoom = (room: IRoom) => { + return room.members.some(member => member === userId); + }; -export default function RoomList() { return ( -
- {salas.map(sala => ( + <> + {rooms.map(sala => (
-

{sala.name}

-
-

- Creado por: {sala.created_by} -

+

{sala.name}

+

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

-
- - - +
+ {CanExitRoom(sala) && ( + + + + )}
))} -
+ ); } diff --git a/app/(protected)/dashboard/_components/RoomSearch.tsx b/app/(protected)/dashboard/_components/RoomSearch.tsx new file mode 100644 index 0000000..1129f7f --- /dev/null +++ b/app/(protected)/dashboard/_components/RoomSearch.tsx @@ -0,0 +1,48 @@ +'use client'; + +import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { RoomIcon } from '@/app/_ui/icons'; +import { useQuery } from '@tanstack/react-query'; +import { getAllRooms } from '../_apis/rooms'; +import RoomList from './RoomList'; + +export default function RoomSearch() { + const { data: rooms, isLoading } = useQuery({ + queryKey: ['all-rooms-dashboard'], + queryFn: getAllRooms, + }); + + if (isLoading) return ; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const searchValue = form.search.value; + + // Aquí puedes manejar la búsqueda con el valor de searchValue + console.log('Buscando sala:', searchValue); + }; + + return ( + <> +
+ + +
+ {rooms ? ( + + ) : ( +
+

No encontramos salas

+
+ )} + + ); +} diff --git a/app/(protected)/dashboard/_hooks/useUserProfile.ts b/app/(protected)/dashboard/_hooks/useUserProfile.ts new file mode 100644 index 0000000..b0ee7ed --- /dev/null +++ b/app/(protected)/dashboard/_hooks/useUserProfile.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +'use client'; +import { useAuth } from '@/app/_hooks/useAuth'; +import { updateEmail, updatePassword, updateProfile } from 'firebase/auth'; +import { useEffect, useState } from 'react'; +import { toast } from 'react-toastify'; + +export const useUserProfile = () => { + const { user } = useAuth(); + + const [loading, setLoading] = useState(true); + const [userForm, setUserForm] = useState({ + displayName: user?.displayName || '', + photoURL: user?.photoURL || null, + email: user?.email || '', + password: '', + confirmPassword: '', + }); + + useEffect(() => { + if (user) { + setUserForm({ + displayName: user.displayName || '', + photoURL: user.photoURL || null, + email: user.email || '', + password: '', + confirmPassword: '', + }); + } + setLoading(false); + }, [user]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setUserForm(prev => ({ + ...prev, + [name]: value, + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!user) { + toast.error('No hay usuario autenticado'); + return; + } + + // Validar que la contraseña y la confirmación coincidan + if (userForm.password || userForm.confirmPassword) { + if (userForm.password !== userForm.confirmPassword) { + toast.error('Las contraseñas no coinciden'); + return; + } + } + + setLoading(true); + + try { + // Actualizar email si cambió + if (userForm.email !== user.email) { + await updateEmail(user, userForm.email); + } + + // Actualizar perfil (displayName y photoURL) si cambiaron + if (userForm.displayName !== user.displayName) { + await updateProfile(user, { + displayName: userForm.displayName, + }); + } + if (userForm.photoURL !== user.photoURL) { + await updateProfile(user, { + photoURL: userForm.photoURL, + }); + } + + // Actualizar contraseña si se proporcionó + if (userForm.password) { + await updatePassword(user, userForm.password); + } + + toast.success('Perfil actualizado correctamente'); + } catch (error: any) { + toast.error(`Error al actualizar perfil: ${error.message}`); + } + + setLoading(false); + }; + + return { + userForm, + handleChange, + handleSubmit, + user, + loading, + }; +}; diff --git a/app/(protected)/dashboard/information/loading.tsx b/app/(protected)/dashboard/information/loading.tsx new file mode 100644 index 0000000..571334d --- /dev/null +++ b/app/(protected)/dashboard/information/loading.tsx @@ -0,0 +1,5 @@ +import LoadingEmoji from '@/app/_components/LoadingEmoji'; + +export default function InformationLoading() { + return ; +} diff --git a/app/(protected)/dashboard/information/page.tsx b/app/(protected)/dashboard/information/page.tsx new file mode 100644 index 0000000..865eb4c --- /dev/null +++ b/app/(protected)/dashboard/information/page.tsx @@ -0,0 +1,24 @@ +import RoomSearch from '../_components/RoomSearch'; +import CreateChat from '../room/_components/CreateChat'; + +export default function InformationPage() { + return ( +
+
+

Información

+
+
+

+ Parchat está diseñado para facilitar la comunicación entre diferentes personas y + comunidades manteniendo la privacidad de los usuario. Puedes crear salas de chat, unirte a + ellas y participar en conversaciones en tiempo real. +

+ +
+
+
+ +
+
+ ); +} diff --git a/app/(protected)/dashboard/my-profile/page.tsx b/app/(protected)/dashboard/my-profile/page.tsx new file mode 100644 index 0000000..02ca6e7 --- /dev/null +++ b/app/(protected)/dashboard/my-profile/page.tsx @@ -0,0 +1,106 @@ +'use client'; +import { WrenchIcon } from '@/app/_ui/icons'; +import Image from 'next/image'; +import { useUserProfile } from '../_hooks/useUserProfile'; +import LoadingEmoji from '@/app/_components/LoadingEmoji'; + +export default function MyProfile() { + const { userForm, handleChange, handleSubmit, loading } = useUserProfile(); + + if (loading) return ; + + return ( +
+ +
+
+ +

+ En esta sección puedes personalizar tu perfil a tu gusto, ¡recuerda no usar palabras{' '} + ofensivas! +

+
+
+ image of user + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ ); +} diff --git a/app/(protected)/dashboard/page.tsx b/app/(protected)/dashboard/page.tsx index 63049b6..51cfcb7 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -1,14 +1,31 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; +import { getRoomsImIn } from './_apis/rooms'; import RoomList from './_components/RoomList'; +import LoadingEmoji from '@/app/_components/LoadingEmoji'; export default function Dashboard() { + const { data: rooms, isLoading } = useQuery({ + queryKey: ['my-rooms'], + queryFn: getRoomsImIn, + }); + + if (isLoading) return ; + return ( -
+

Mis Chats

-
- +
+ {rooms ? ( + + ) : ( +
+

Aun no tienes chats

+
+ )}
-
+ ); } diff --git a/app/(protected)/dashboard/room/[id]/page.tsx b/app/(protected)/dashboard/room/[id]/page.tsx index f34469b..b7e5c5e 100644 --- a/app/(protected)/dashboard/room/[id]/page.tsx +++ b/app/(protected)/dashboard/room/[id]/page.tsx @@ -1,111 +1,38 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; import MessagesList from '../_components/MessagesList'; -import { IMessage } from '@/app/_lib/_interfaces/IMessage'; +import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { use } from 'react'; +import { getRoomMessages } from '../../_apis/messages'; +import { getRoomById } from '../../_apis/rooms'; interface RouterProps { params: Promise<{ id: string; }>; } -export default async function RoomPage({ params }: RouterProps) { - const { id } = await params; - const messages = await fetchMessages(); +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 { data: room, isLoading: roomLoading } = useQuery({ + queryKey: [`room-${id}`], + queryFn: () => getRoomById(id), + }); + + if (messagesLoading || roomLoading) return ; + return (
-

Nombre de la sala {id}

+

{room?.name}

Pública

- + {room && }
); } - -async function fetchMessages(): Promise { - // Simula una llamada a una API externa - return new Promise(resolve => { - setTimeout(() => { - resolve([ - { - id: '1', - send_by: 'Danils', - send_at: new Date().toISOString(), - content: 'Hola, bienvenido a la sala!', - }, - { - id: '2', - send_by: 'Valtimore', - send_at: new Date().toISOString(), - content: 'Hola! ¿Cómo están?', - }, - { - id: '3', - send_by: 'Zers', - send_at: new Date().toISOString(), - content: '¡Todo bien! ¿Y tú?', - }, - { - id: '4', - send_by: 'Liferip', - send_at: new Date().toISOString(), - content: '¡Genial! ¿Qué tal el clima?', - }, - { - id: '5', - send_by: 'LIFERIP', - send_at: new Date().toISOString(), - content: 'Sigue lloviendo, pero no importa.', - }, - { - id: '6', - send_by: 'user1', - send_at: new Date().toISOString(), - content: '¡Qué suerte! Aquí hace mucho calor.', - }, - { - id: '7', - send_by: 'user2', - send_at: new Date().toISOString(), - content: '¿Alguien sabe qué hora es?', - }, - { - id: '8', - send_by: 'user1', - send_at: new Date().toISOString(), - content: 'Son las 3 PM.', - }, - { - id: '9', - send_by: 'user2', - send_at: new Date().toISOString(), - content: 'Gracias!', - }, - { - id: '10', - send_by: 'user1', - send_at: new Date().toISOString(), - content: 'De nada!', - }, - { - id: '11', - send_by: 'user2', - send_at: new Date().toISOString(), - content: '¿Alguien quiere jugar un juego?', - }, - { - id: '12', - send_by: 'user1', - send_at: new Date().toISOString(), - content: '¡Sí! ¿Qué juego?', - }, - { - id: '13', - send_by: 'user2', - send_at: new Date().toISOString(), - content: - 'Podemos jugar a adivinar el tamaño de mi aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa aa a a a aaaaaaaaaaaaaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' + - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - }, - ]); - }, 2000); - }); -} diff --git a/app/(protected)/dashboard/room/_components/CreateChat.tsx b/app/(protected)/dashboard/room/_components/CreateChat.tsx new file mode 100644 index 0000000..bde8d24 --- /dev/null +++ b/app/(protected)/dashboard/room/_components/CreateChat.tsx @@ -0,0 +1,109 @@ +'use client'; + +import { CancelIcon } from '@/app/_ui/icons'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; +import { createRoom } from '../../_apis/rooms'; +import { ICreateRoom } from '@/app/_lib/_interfaces/IRoom'; +import { auth } from '@/app/_lib/_firebase/firebase.config'; + +export default function CreateChat() { + const [createMode, setCreateMode] = useState(false); + + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: (data: ICreateRoom) => createRoom(data), + onSuccess: res => { + if (!res) return; + queryClient.invalidateQueries({ queryKey: [`all-rooms-dashboard`] }); + setCreateMode(false); + }, + }); + + const handleCreateRoom = (e: React.FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + const name = (form.elements.namedItem('name') as HTMLInputElement).value; + const description = (form.elements.namedItem('description') as HTMLTextAreaElement).value; + const isPrivate = (form.elements.namedItem('isPrivate') as HTMLSelectElement).value === 'true'; + const data: ICreateRoom = { + name, + description, + isPrivate, + userIds: [auth.currentUser?.uid as string], + }; + mutation.mutate(data); + }; + + return ( +
+ {createMode ? ( +
+
+ + +
+
+ +