diff --git a/.dockerignore b/.dockerignore index 5681fc3..c2a3835 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,5 @@ # Ignorar dependencias locales -# node_modules se utilizará para mandarse desde github actions +node_modules # Ignorar config de Git .git diff --git a/.github/workflows/develop-CD.yml b/.github/workflows/develop-CD.yml index 70ce8cc..86b9279 100644 --- a/.github/workflows/develop-CD.yml +++ b/.github/workflows/develop-CD.yml @@ -17,9 +17,19 @@ jobs: with: ref: develop - # 👉 Build la imagen Docker + # 👉 Build Docker image with Firebase args - name: Build Docker image - run: docker build -f Dockerfile.prod -t ${{ secrets.DOCKER_USERNAME }}/frontend-dev:stage-${{ github.sha }} . + 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 }} \ + --build-arg NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ secrets.FIREBASE_STORAGE_BUCKET }} \ + --build-arg NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.FIREBASE_MESSAGING_SENDER_ID }} \ + --build-arg NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.FIREBASE_APP_ID }} \ + --build-arg NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=${{ secrets.FIREBASE_MEASUREMENT_ID }} \ + -f Dockerfile.prod -t ${{ secrets.DOCKER_USERNAME }}/frontend-dev:${{ github.sha }} . # 👉 Log in to DockerHub - name: DockerHub Login @@ -27,7 +37,7 @@ jobs: # 👉 Push la imagen - name: Push Docker image to DockerHub - run: docker push ${{ secrets.DOCKER_USERNAME }}/frontend-dev:stage-${{ github.sha }} + run: docker push ${{ secrets.DOCKER_USERNAME }}/frontend-dev:${{ github.sha }} # 👉 Add VM to known_hosts - name: Add VM to known_hosts @@ -35,6 +45,17 @@ jobs: mkdir -p ~/.ssh ssh-keyscan ${{ secrets.GCP_VM_IP }} >> ~/.ssh/known_hosts + - name: Actualizar o agregar imagen en .env + uses: appleboy/ssh-action@v0.1.4 + with: + host: ${{ secrets.GCP_VM_IP }} + username: ${{ secrets.GCP_VM_USER }} + key: ${{ secrets.GCP_SSH_KEY }} + script: | + cd /home/proyectosdanils/ + sed -i '/^DOCKER_IMAGE_FRONTEND_DEV=/d' .env + echo "DOCKER_IMAGE_FRONTEND_DEV=${{ secrets.DOCKER_USERNAME }}/frontend-dev:${{ github.sha }}" >> .env + # 👉 Desplegar en la VM - name: Deploy on GCP VM uses: appleboy/ssh-action@v0.1.4 @@ -44,7 +65,4 @@ jobs: key: ${{ secrets.GCP_SSH_KEY }} script: | cd /home/proyectosdanils/ - docker compose down frontend-dev - docker pull ${{ secrets.DOCKER_USERNAME }}/frontend-dev:stage-${{ github.sha }} - export DOCKER_IMAGE_FRONTEND_DEV=${{ secrets.DOCKER_USERNAME }}/frontend-dev:stage-${{ github.sha }} docker compose up -d frontend-dev diff --git a/.github/workflows/master-CD.yml b/.github/workflows/master-CD.yml index bd001d3..fc64994 100644 --- a/.github/workflows/master-CD.yml +++ b/.github/workflows/master-CD.yml @@ -18,9 +18,19 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # 👉 Build la imagen Docker + # 👉 Build Docker image with Firebase args - name: Build Docker image - run: docker build -f Dockerfile.prod -t ${{ secrets.DOCKER_USERNAME }}/frontend-prod:${{ github.sha }} . + 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 }} \ + --build-arg NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=${{ secrets.FIREBASE_STORAGE_BUCKET }} \ + --build-arg NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=${{ secrets.FIREBASE_MESSAGING_SENDER_ID }} \ + --build-arg NEXT_PUBLIC_FIREBASE_APP_ID=${{ secrets.FIREBASE_APP_ID }} \ + --build-arg NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=${{ secrets.FIREBASE_MEASUREMENT_ID }} \ + -f Dockerfile.prod -t ${{ secrets.DOCKER_USERNAME }}/frontend-prod:${{ github.sha }} . # 👉 Log in to DockerHub - name: DockerHub Login @@ -36,6 +46,17 @@ jobs: mkdir -p ~/.ssh ssh-keyscan ${{ secrets.GCP_VM_IP }} >> ~/.ssh/known_hosts + - name: Actualizar o agregar imagen en .env + uses: appleboy/ssh-action@v0.1.4 + with: + host: ${{ secrets.GCP_VM_IP }} + username: ${{ secrets.GCP_VM_USER }} + key: ${{ secrets.GCP_SSH_KEY }} + script: | + cd /home/proyectosdanils/ + sed -i '/^DOCKER_IMAGE_FRONTEND_PROD=/d' .env + echo "DOCKER_IMAGE_FRONTEND_PROD=${{ secrets.DOCKER_USERNAME }}/frontend-prod:${{ github.sha }}" >> .env + # 👉 Desplegar en la VM - name: Deploy on GCP VM uses: appleboy/ssh-action@v0.1.4 @@ -45,7 +66,4 @@ jobs: key: ${{ secrets.GCP_SSH_KEY }} script: | cd /home/proyectosdanils/ - docker compose down frontend-prod - docker pull ${{ secrets.DOCKER_USERNAME }}/frontend-prod:${{ github.sha }} - export DOCKER_IMAGE_FRONTEND_PROD=${{ secrets.DOCKER_USERNAME }}/frontend-prod:${{ github.sha }} docker compose up -d frontend-prod \ No newline at end of file diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..07ba684 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,17 @@ +FROM node:22-alpine + +RUN mkdir -p /app + +WORKDIR /app + +COPY package.json ./ + +RUN npm install + +COPY . . + +COPY next.config.ts ./next.config.ts + +EXPOSE 3000 + +CMD ["npm", "run", "dev"] diff --git a/Dockerfile.prod b/Dockerfile.prod index b44a2df..e055abd 100644 --- a/Dockerfile.prod +++ b/Dockerfile.prod @@ -1,14 +1,33 @@ FROM node:22-alpine -RUN mkdir -p /home/frontend-prod +RUN mkdir -p /app -WORKDIR /home/frontend-prod +WORKDIR /app -COPY package.json package-lock.json ./ +COPY package.json ./ 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 +ARG NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET +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 +ENV NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=$NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET +ENV NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=$NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID +ENV NEXT_PUBLIC_FIREBASE_APP_ID=$NEXT_PUBLIC_FIREBASE_APP_ID +ENV NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=$NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID + + RUN npm run build EXPOSE 3000 diff --git a/app/(auth)/login-gest/page.tsx b/app/(auth)/login-gest/page.tsx deleted file mode 100644 index e5b3a5a..0000000 --- a/app/(auth)/login-gest/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Link from 'next/link'; - -export default function LoginGest() { - return ( -
-
-
-

- Entra como Invitado y Únete a la Conversación -

-

- Conecta con otros asistentes desde cualquier parte del mundo. -

-
-
-
-
- - -
- -
-
-

¿Quieres crear una cuenta?

- - Click aquí - -
-
-
-
- ); -} diff --git a/app/(auth)/login/_components/DesktopForm.tsx b/app/(auth)/login/_components/DesktopForm.tsx index ba34169..175b03e 100644 --- a/app/(auth)/login/_components/DesktopForm.tsx +++ b/app/(auth)/login/_components/DesktopForm.tsx @@ -1,4 +1,13 @@ -export default function DesktopForm({ setMode }: { setMode: (mode: string) => void }) { +'use client'; +import GoogleLoginButton from '@/app/_components/GoogleLoginButton'; +import { useFormStatus } from 'react-dom'; +interface Props { + setMode: (mode: string) => void; + registerAction: (payload: FormData) => void; + loginAction: (payload: FormData) => void; +} + +export default function DesktopForm({ setMode, registerAction, loginAction }: Props) { return (
@@ -8,7 +17,7 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo

Inicia sesión para acceder a tus salas

-
+
@@ -31,9 +41,12 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo placeholder="Contraseña" className="w-full border-1 border-purple-300 p-2 rounded" autoComplete="current-password" + name="password" />
- + + +

¿Aún no tienes una cuenta?

@@ -43,21 +56,23 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo
+ {/* REGISTRO */}

Regístrate y Conecta

Podrás guardar tus conversaciones y salas

-
+
@@ -70,6 +85,7 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo placeholder="Correo electrónico" autoComplete="email" className="w-full border-1 border-purple-300 p-2 rounded" + name="email" />
@@ -82,9 +98,10 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo placeholder="Contraseña" className="w-full border-1 border-purple-300 p-2 rounded" autoComplete="current-password" + name="password" />
- +

¿Ya tienes una cuenta?

@@ -96,3 +113,12 @@ export default function DesktopForm({ setMode }: { setMode: (mode: string) => vo
); } + +function SubmitButton({ text }: { text: string }) { + const { pending } = useFormStatus(); + return ( + + ); +} diff --git a/app/(auth)/login/_components/MobileForm.tsx b/app/(auth)/login/_components/MobileForm.tsx index 47dab7b..a62dddd 100644 --- a/app/(auth)/login/_components/MobileForm.tsx +++ b/app/(auth)/login/_components/MobileForm.tsx @@ -1,8 +1,15 @@ +'use client'; +import GoogleLoginButton from '@/app/_components/GoogleLoginButton'; +import { useState } from 'react'; + interface Props { - mobileModeLogin: boolean; - setMobileModeLogin: (value: boolean) => void; + registerAction: (payload: FormData) => void; + loginAction: (payload: FormData) => void; } -export default function MobileForm({ mobileModeLogin, setMobileModeLogin }: Props) { + +export default function MobileForm({ registerAction, loginAction }: Props) { + const [mobileModeLogin, setMobileModeLogin] = useState(true); + return (

Inicia sesión para acceder a tus salas

-
+
@@ -37,9 +45,11 @@ export default function MobileForm({ mobileModeLogin, setMobileModeLogin }: Prop placeholder="Contraseña" autoComplete="current-password" className="w-full border-1 border-purple-300 p-2 rounded" + name="password" />
+

¿Aún no tienes una cuenta?

@@ -56,7 +66,7 @@ export default function MobileForm({ mobileModeLogin, setMobileModeLogin }: Prop

Regístrate y Conecta

Podrás guardar tus conversaciones y salas

-
+
@@ -78,6 +89,7 @@ export default function MobileForm({ mobileModeLogin, setMobileModeLogin }: Prop placeholder="Correo electrónico" autoComplete="email" className="w-full border-1 border-purple-300 p-2 rounded" + name="email" />
@@ -90,6 +102,7 @@ export default function MobileForm({ mobileModeLogin, setMobileModeLogin }: Prop placeholder="Contraseña" autoComplete="current-password" className="w-full border-1 border-purple-300 p-2 rounded" + name="password" />
diff --git a/app/(auth)/login/_lib/_actions/session.ts b/app/(auth)/login/_lib/_actions/session.ts new file mode 100644 index 0000000..ba84e6d --- /dev/null +++ b/app/(auth)/login/_lib/_actions/session.ts @@ -0,0 +1,30 @@ +// app/actions/session.ts +'use server'; + +import { cookies } from 'next/headers'; + +export async function createSession(token: string) { + (await cookies()).set('session', token, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7, // 1 semana + }); +} + +export async function createPendingPath(path: string) { + (await cookies()).set('pendingPath', path, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 1, // 1 dia + }); +} + +export async function deletePendingPath() { + (await cookies()).delete('pendingPath'); +} + +export async function deleteSession() { + (await cookies()).delete('session'); +} diff --git a/app/(auth)/login/_lib/_schemas/auth.ts b/app/(auth)/login/_lib/_schemas/auth.ts new file mode 100644 index 0000000..d6e75d3 --- /dev/null +++ b/app/(auth)/login/_lib/_schemas/auth.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const registerSchema = z.object({ + email: z.string().email({ message: 'Invalid email address' }).trim(), + password: z.string().min(6, { message: 'Password must be at least 6 characters long' }).trim(), + displayName: z + .string() + .min(3, { message: 'Display name must be at least 3 characters long' }) + .trim(), +}); + +export const loginSchema = z.object({ + email: z.string().email({ message: 'Invalid email address' }).trim(), + password: z.string().min(6, { message: 'Password must be at least 6 characters long' }).trim(), +}); diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index 072b80e..9e4fd3d 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -3,11 +3,22 @@ import Image from 'next/image'; import { useState } from 'react'; import DesktopForm from './_components/DesktopForm'; import MobileForm from './_components/MobileForm'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/app/_hooks/useAuth'; export default function Login() { const [mode, setMode] = useState('login'); - const [mobileModeLogin, setMobileModeLogin] = useState(true); + const { login, register } = useAuth(); + const router = useRouter(); + const handleLogin = async (payload: FormData) => { + await login(payload); + router.push('/dashboard'); + }; + const handleRegister = async (payload: FormData) => { + await register(payload); + router.push('/dashboard'); + }; return (
multicolor
{/* Desktop */} - + {/* Mobile */} - +
multicolor
); diff --git a/app/(auth)/page.tsx b/app/(auth)/page.tsx index 0c74bf1..bd9c650 100644 --- a/app/(auth)/page.tsx +++ b/app/(auth)/page.tsx @@ -1,33 +1,52 @@ import Image from 'next/image'; import homeDecoration from '@/public/home-decoration.svg'; +import Link from 'next/link'; + export default function Home() { return ( -
+
-

+

Para todas tus necesidades

Una web creada por personas para conectar con personas

-

+

Transmite tu mensaje en segundos con tus compañeros de trabajo, estudio, viaje, fiesta y completos desconocidos.

- + +
+ Ingresar a una Sala +
+ +
+
+ Background Decoration + Chat Simulation
- Chat Simulation
@@ -40,10 +59,19 @@ export default function Home() { retroalimentación cuando lo necesites e interactúa con nuevas personas.

-
- Event +
+
+ Event +
-
+

Ideal para Eventos

Parchat es la solución perfecta para mantener a tus asistentes conectados y bien @@ -51,14 +79,16 @@ export default function Home() { un código de acceso y permite que todos participen fácilmente desde cualquier lugar.

-
+ +

Comunicación al Instante

Al igual que los servicios de mensajería más populares, aquí los mensajes llegan al momento de ser enviados, no te perderás ningún detalle.

-
+ +

Siempre Tienes el Control

Gestiona fácilmente los asistentes en tu evento, y elimina rápidamente a quienes @@ -88,10 +118,11 @@ export default function Home() { alt="Event" width={0} height={0} - className="w-[50%] min-w-[200px] max-w-[300px]" + draggable={false} + className="w-[50%] min-w-[350px] max-w-[400px] transition-all duration-500 ease-in-out hover:scale-104 hover:animate-pulse" />

-

Eventos y Convenciones

+

Eventos y Convenciones

Mantén a todos los asistentes conectados y al tanto de anuncios importantes. Perfecto para convenciones, conciertos y festivales. @@ -104,10 +135,11 @@ export default function Home() { alt="Event" width={0} height={0} - className="w-[50%] min-w-[200px] max-w-[300px]" + draggable={false} + className="w-[50%] min-w-[350px] max-w-[400px] transition-all duration-500 ease-in-out hover:scale-104 hover:animate-pulse" />

-

Universidades

+

Universidades

Resuelve dudas de clase, organiza trabajos en grupo o coordina transporte con tus compañeros. ¡Comunicación sin complicaciones! @@ -120,10 +152,11 @@ export default function Home() { alt="Event" width={0} height={0} - className="w-[50%] min-w-[200px] max-w-[300px]" + draggable={false} + className="w-[50%] min-w-[350px] max-w-[400px] transition-all duration-500 ease-in-out hover:scale-104 hover:animate-pulse" />

-

Ambientes Empresariales

+

Ambientes Empresariales

Crea chats de oficina para coordinar tareas sin necesidad de compartir números personales. Comunicación rápida y efectiva en el trabajo. 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 new file mode 100644 index 0000000..9ac23ea --- /dev/null +++ b/app/(protected)/dashboard/_apis/messages.ts @@ -0,0 +1,74 @@ +'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(`/api/v1/chat/rooms/${roomId}/messages`); + return res.data; + } catch (error) { + console.error('Error fetching room messages:', error); + return []; + } +}; + +export const getChatMessages = async (chatId: string) => { + try { + const res = await myAxios.get(`/api/v1/chat/direct/${chatId}/messages`); + return res.data; + } catch { + return []; + } +}; + +export const getTranslatedMessage = async (content: string, languageCode?: string) => { + try { + const res = await axios.post(`/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; + } +}; diff --git a/app/(protected)/dashboard/_apis/rooms.ts b/app/(protected)/dashboard/_apis/rooms.ts new file mode 100644 index 0000000..5950071 --- /dev/null +++ b/app/(protected)/dashboard/_apis/rooms.ts @@ -0,0 +1,64 @@ +/* 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; + 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/ChatList.tsx b/app/(protected)/dashboard/_components/ChatList.tsx new file mode 100644 index 0000000..4e6fed8 --- /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 => ( +

+ +

+ {sala.displayNames[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..04d2ec9 --- /dev/null +++ b/app/(protected)/dashboard/_components/ChatsInit.tsx @@ -0,0 +1,74 @@ +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(null); + const { containerRef, bottomRef } = useIsBottom({ items: [pendingMessages, messages] }); + + const handleCloseUserModal = () => { + setUserSelected(null); + }; + + const canSendMessage = () => { + if (room) { + return room.members.some(member => member === user?.uid); + } + if (chat) { + return true; + } + return false; + }; + + const selectUser = (message: IMessage) => { + if (room) { + setUserSelected(message); + } + }; + + 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 43e114d..566428a 100644 --- a/app/(protected)/dashboard/_components/RoomList.tsx +++ b/app/(protected)/dashboard/_components/RoomList.tsx @@ -1,133 +1,53 @@ +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..7ad1b70 --- /dev/null +++ b/app/(protected)/dashboard/_components/RoomSearch.tsx @@ -0,0 +1,36 @@ +'use client'; + +import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { RoomIcon } from '@/app/_ui/icons'; +import RoomList from './RoomList'; +import { useGetPublicRooms } from '../_hooks/useGetPublicRooms'; + +export default function RoomSearch() { + const { isLoading, fileteredRooms, handleSubmit } = useGetPublicRooms(); + + if (isLoading) return ; + + return ( + <> + + + + + {fileteredRooms ? ( + + ) : ( +
+

No encontramos salas

+
+ )} + + ); +} diff --git a/app/(protected)/dashboard/_components/UserModal.tsx b/app/(protected)/dashboard/_components/UserModal.tsx new file mode 100644 index 0000000..029d1ff --- /dev/null +++ b/app/(protected)/dashboard/_components/UserModal.tsx @@ -0,0 +1,77 @@ +'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 { + room?: IRoom | null; + message: IMessage | null; + handleClose: () => void; +} +export default function UserModal({ handleClose, message, room }: Props) { + const user = useAuth(state => state.user); + const queryClient = useQueryClient(); + const router = useRouter(); + const open = Boolean(message); + + const DoIHaveAdmin = () => room?.admins.some(admin => admin === user?.uid); + + const handleClickOutside = (e: React.MouseEvent) => { + if (e.target === e.currentTarget) { + handleClose(); + } + }; + + const mutation_send_message = useMutation({ + mutationFn: (data: string) => createChat(data), + onSuccess: res => { + if (!res) return; + queryClient.invalidateQueries({ queryKey: [`all-chats-dashboard`] }); + router.push(`/dashboard/chat/${res.id}`); + }, + }); + + if (!message) return null; + + return ( +
+
+
+

Envía un mensaje directo

+
+ {room && DoIHaveAdmin() && ( +

clearReports(message)}> + 🧹 +

+ )} +

sendReport(message)}> + ❗ +

+
+
+

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

+
+ + +
+
+
+ ); +} diff --git a/app/(protected)/dashboard/_hooks/useGetPublicRooms.ts b/app/(protected)/dashboard/_hooks/useGetPublicRooms.ts new file mode 100644 index 0000000..d1c31a8 --- /dev/null +++ b/app/(protected)/dashboard/_hooks/useGetPublicRooms.ts @@ -0,0 +1,42 @@ +import { IRoom } from '@/app/_lib/_interfaces/IRoom'; +import { useQuery } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { getAllRooms } from '../_apis/rooms'; + +export const useGetPublicRooms = () => { + const { data: rooms, isLoading } = useQuery({ + queryKey: ['all-rooms-dashboard'], + queryFn: getAllRooms, + }); + + const [fileteredRooms, setFilteredRooms] = useState(rooms); + + useEffect(() => { + if (rooms) { + setFilteredRooms(rooms); + } + }, [rooms]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const searchValue = e.currentTarget.value; + + // Aquí puedes manejar la búsqueda con el valor de searchValue + if (searchValue.trim() === '' || !rooms) { + setFilteredRooms(rooms); + return; + } + + const filtered = rooms.filter(room => + room.name.toLowerCase().includes(searchValue.toLowerCase()) + ); + + setFilteredRooms(filtered.length > 0 ? filtered : []); + }; + + return { + isLoading, + fileteredRooms, + handleSubmit, + }; +}; 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/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/chat/layout.tsx b/app/(protected)/dashboard/chat/layout.tsx new file mode 100644 index 0000000..d8601d1 --- /dev/null +++ b/app/(protected)/dashboard/chat/layout.tsx @@ -0,0 +1,14 @@ +import AsideRoom from '@/app/_components/AsideRoom'; + +export default function ChatLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ + {children} +
+ ); +} 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..0667a7d --- /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..4371877 --- /dev/null +++ b/app/(protected)/dashboard/my-profile/page.tsx @@ -0,0 +1,117 @@ +'use client'; +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 ( +
+
+
+ +

+ 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..727399a 100644 --- a/app/(protected)/dashboard/page.tsx +++ b/app/(protected)/dashboard/page.tsx @@ -1,14 +1,49 @@ +'use client'; +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({ + queryKey: ['my-rooms'], + queryFn: getRoomsImIn, + }); + + const { data: chats, isLoading: chatsLoading } = useQuery({ + queryKey: ['all-chats-dashboard'], + queryFn: getAllChats, + }); + + if (chatsLoading || isLoading) return ; + + console.log('Chats:', chats); + return ( -
+

Mis Chats

-
- +
+

Conversaciones

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

Aun no tienes conversaciones

+
+ )} +

Salas grupales

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

Aun no tienes chats

+
+ )}
-
+
); } diff --git a/app/(protected)/dashboard/room/[id]/page.tsx b/app/(protected)/dashboard/room/[id]/page.tsx index 40623e6..49409e7 100644 --- a/app/(protected)/dashboard/room/[id]/page.tsx +++ b/app/(protected)/dashboard/room/[id]/page.tsx @@ -1,111 +1,53 @@ +'use client'; +import { useQuery } from '@tanstack/react-query'; import MessagesList from '../_components/MessagesList'; -import { IMessage } from '@/app/_interfaces/IMessage'; +import LoadingEmoji from '@/app/_components/LoadingEmoji'; +import { use } from 'react'; +import { getRoomById } from '../../_apis/rooms'; +import { useRoomMessages } from '../hooks/useRoomMessages'; +import { toast } from 'react-toastify'; 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 { messages, loading } = useRoomMessages({ id }); + + const { data: room, isLoading: roomLoading } = useQuery({ + queryKey: [`room-${id}`], + queryFn: () => getRoomById(id), + }); + + if (loading || roomLoading) return ; + + const handleCopyUrl = () => { + const url = `${window.location.origin}/dashboard/room/${id}`; + navigator.clipboard + .writeText(url) + .then(() => { + toast.success('URL copiada al portapapeles'); + }) + .catch(() => { + toast.error('Error al copiar la URL'); + }); + }; + return (
-
-

Nombre de la sala {id}

-

Pública

+
+
+

{room?.name}

+

{room?.isPrivate ? 'Privada' : '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..6edf644 --- /dev/null +++ b/app/(protected)/dashboard/room/_components/CreateChat.tsx @@ -0,0 +1,111 @@ +'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'; +import { useRouter } from 'next/navigation'; + +export default function CreateChat() { + const [createMode, setCreateMode] = useState(false); + const router = useRouter(); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: (data: ICreateRoom) => createRoom(data), + onSuccess: res => { + if (!res) return; + queryClient.invalidateQueries({ queryKey: [`all-rooms-dashboard`] }); + setCreateMode(false); + router.push(`/dashboard/room/${res.id}`); + }, + }); + + 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 ? ( +
+
+ + +
+
+ +