Skip to content

Commit

Permalink
Feat/experiment/chat (#57)
Browse files Browse the repository at this point in the history
[WIP] add ChatModule (socket.io) to backend
[WIP] add ChatRoom page to frontend
  • Loading branch information
lim396 authored Nov 17, 2023
1 parent 9544500 commit 3ff0073
Show file tree
Hide file tree
Showing 10 changed files with 391 additions and 25 deletions.
10 changes: 9 additions & 1 deletion backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { RoomModule } from './room/room.module';
import { EventsModule } from './events/events.module';
import { ChatModule } from './chat/chat.module';

@Module({
imports: [UserModule, PrismaModule, AuthModule, RoomModule, EventsModule],
imports: [
UserModule,
PrismaModule,
AuthModule,
RoomModule,
EventsModule,
ChatModule,
],
controllers: [AppController],
providers: [AppService],
})
Expand Down
39 changes: 39 additions & 0 deletions backend/src/chat/chat.gateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
ConnectedSocket,
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { Logger } from '@nestjs/common';

@WebSocketGateway({
cors: {
origin: '*',
},
})
export class ChatGateway {
@WebSocketServer()
server: Server;

private logger: Logger = new Logger('ChatGateway');

@SubscribeMessage('newMessage')
chatMessage(
@MessageBody() data: string,
@ConnectedSocket() client: Socket,
): void {
this.logger.log('message recieved');
this.logger.log(data);
this.server.emit('sendToClient', data, client.id);
}

handleConnection(@ConnectedSocket() client: Socket) {
this.logger.log(`Client connected: ${client.id}`);
}

handleDisconnect(@ConnectedSocket() client: Socket) {
this.logger.log(`Client disconnected: ${client.id}`);
}
}
7 changes: 7 additions & 0 deletions backend/src/chat/chat.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
providers: [ChatGateway],
})
export class ChatModule {}
125 changes: 104 additions & 21 deletions frontend/app/room/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,109 @@
async function getRoom(id: number) {
const res = await fetch(`${process.env.API_URL}/room/${id}`, {
cache: "no-cache",
});
const room = await res.json();
return room;
}
"use client";

import {
Card,
CardHeader,
CardContent,
CardFooter,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import ChatHome from "@/app/ui/room/chat-room";
import { useState, useEffect } from "react";
import { socket } from "@/socket";

type Chat = {
text: string;
};

type MessageLog = Array<Chat>;

export default function ChatRoomPage() {
const [message, setMessage] = useState("");
const [messageLog, setMessageLog] = useState<MessageLog>([
{
text: "example message logs",
},
{
text: "Hello world",
},
{
text: "hoge hoge",
},
]);

useEffect(() => {
const newMessageReceived = (e: any) => {
console.log(`received message: `, e);
setMessageLog((oldMessageLog) => [...oldMessageLog, { text: e }]);
console.log(messageLog);
};
socket.on("sendToClient", newMessageReceived);
return () => {
console.log(`return from useEffect`);
socket.off("sendToClient", newMessageReceived);
};
}, [messageLog]);

useEffect(() => {
socket.connect(); // no-op if the socket is already connected
return () => {
console.log("disconnect");
socket.disconnect();
};
}, []);

const sendMessage = async (e: React.SyntheticEvent) => {
e.preventDefault();
const newMessage = message;
console.log(`sendMessage`, newMessage);
socket.emit("newMessage", newMessage);
setMessage("");
};

export default async function getRoomInfo({
params: { id },
}: {
params: { id: number };
}) {
const room = await getRoom(id);
return (
<div>
<h1>
<b>Room info</b>
</h1>
<p>
room ID: {room.id} <br />
room name: {room.name}
</p>
<div className="flex items-center justify-center">
<Card className="w-[800px] grid grid-rows-[min-content_1fr_min-content]">
<CardHeader>
<CardTitle>Chat room</CardTitle>
<CardDescription>Experimental chat room</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px] w-full space-y-1 pr-3">
<div className="flex gap-3 text-slate-600 text-sm">
<ul>
{messageLog.map((message, i) => {
return (
<div key={i} className="flex gap-3 text-slate-600 text-sm">
{" "}
<li>{message.text}</li>
</div>
);
})}
</ul>
</div>
</ScrollArea>
</CardContent>
<CardFooter>
<form
className="space-x-2 w-full flex gap-2"
id="chat-content"
onSubmit={sendMessage}
>
<Input
placeholder="Message..."
name="message"
value={message}
onChange={(e) => setMessage(e.target.value)}
type="text"
/>
<Button type="submit">Send</Button>
</form>
</CardFooter>
</Card>
</div>
);
}
35 changes: 35 additions & 0 deletions frontend/app/ui/room/chat-room.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
Card,
CardHeader,
CardContent,
CardFooter,
CardDescription,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";

export default function ChatHome() {
return (
<div className="flex items-center justify-center">
<Card className="w-[800px] h-[650px] grid grid-rows-[min-content_1fr_min-content]">
<CardHeader>
<CardTitle>Chat room</CardTitle>
<CardDescription>Experimental chat room</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex gap-3 text-slate-600 text-sm">
<p className="mt-2">example chat: Hello!</p>
</div>
<div className="flex gap-3 text-slate-600 text-sm">
<p className="ml-2">example chat: Hello!</p>
</div>
</CardContent>
<CardFooter className="space-x-2">
<Input placeholder="Message..." />
<Button type="submit">Send</Button>
</CardFooter>
</Card>
</div>
);
}
48 changes: 48 additions & 0 deletions frontend/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import * as React from "react";
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";

import { cn } from "@/lib/utils";

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
));
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className,
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
));
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;

export { ScrollArea, ScrollBar };
24 changes: 24 additions & 0 deletions frontend/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react";

import { cn } from "@/lib/utils";

export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
{...props}
/>
);
},
);
Textarea.displayName = "Textarea";

export { Textarea };
Loading

0 comments on commit 3ff0073

Please sign in to comment.