diff --git a/backend/.gitignore b/backend/.gitignore index a2e9924..625eb5a 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -36,6 +36,8 @@ index2.html dockerbuild.sh *.sh +./myenv + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/backend/internal/chat/handler.go b/backend/internal/chat/handler.go index 676c8f8..7220a93 100644 --- a/backend/internal/chat/handler.go +++ b/backend/internal/chat/handler.go @@ -6,6 +6,8 @@ import ( "fmt" "log" "net/http" + "strconv" + "time" "github.com/gin-gonic/gin" ) @@ -70,3 +72,51 @@ func (h *Handler) JoinChatRoom(c *gin.Context) { c.JSON(http.StatusOK, res) } + +func (h *Handler) GetChatMessages(c *gin.Context) { + // Extract ChatRoomID from the context + chatRoomID, err := common.ChatRoomIDValidator(c) + if err != nil { + log.Printf("Error occurred with chat room ID: %v", err.Error()) + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid chat room ID"}) + return + } + + // Extract the cursor and pageSize from query parameters + cursorStr := c.DefaultQuery("cursor", "") // Cursor is provided by the client + pageSize := c.DefaultQuery("pageSize", "50") + + // Parse pageSize to an integer + pageSizeInt, err := strconv.Atoi(pageSize) + if err != nil || pageSizeInt <= 0 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid page size"}) + return + } + + // Parse the cursor string to a time.Time + var cursorTime *time.Time + if cursorStr != "" { + parsedCursor, err := time.Parse(time.RFC3339, cursorStr) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid cursor"}) + return + } + cursorTime = &parsedCursor + } + + // Call the service function to get paginated messages + messages, err := h.Service.GetPaginatedMessages(c.Request.Context(), chatRoomID, cursorTime, pageSizeInt) + if err != nil { + log.Printf("Error fetching messages: %v", err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Error fetching messages"}) + return + } + + // Determine the next cursor + var nextCursor string + if len(messages) > 0 { + nextCursor = messages[len(messages)-1].CreatedAt.Format(time.RFC3339) + } + + c.JSON(http.StatusOK, gin.H{"messages": messages, "nextCursor": nextCursor}) +} diff --git a/backend/internal/chat/model.go b/backend/internal/chat/model.go index 6920b7a..b7167f7 100644 --- a/backend/internal/chat/model.go +++ b/backend/internal/chat/model.go @@ -12,14 +12,14 @@ type ChatRoom struct { } type Message struct { - ID int `json:"id"` - ChatRoomID uuid.UUID `json:"chat_room_id"` - SenderID uuid.UUID `json:"sender_id"` - Content string `json:"content"` - MediaURL string `json:"media_url"` - CreatedAt time.Time `json:"created_at"` - ReadAt time.Time `json:"read_at"` - DeletedByUserID uuid.UUID `json:"deleted_by_user_id"` + ID int `json:"id"` + ChatRoomID uuid.UUID `json:"chat_room_id"` + SenderID uuid.UUID `json:"sender_id"` + Content string `json:"content"` + MediaURL string `json:"media_url"` + CreatedAt time.Time `json:"created_at"` + ReadAt *time.Time `json:"read_at,omitempty"` + DeletedByUserID *uuid.UUID `json:"deleted_by_user_id,omitempty"` } type UserInChatRoom struct { diff --git a/backend/internal/chat/repository.go b/backend/internal/chat/repository.go index 0ffcd32..2db0e28 100644 --- a/backend/internal/chat/repository.go +++ b/backend/internal/chat/repository.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "log" "time" @@ -17,8 +18,7 @@ type Repository interface { FetchRecentMessages(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error) JoinChatRoomByID(ctx context.Context, chatRoomID uuid.UUID, userID uuid.UUID) (*ChatRoom, error) SaveMessage(ctx context.Context, message *Message) error - //FetchMessagesFromRedis(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error) - FetchMessagesFromDatabase(ctx context.Context, chatRoomID uuid.UUID, page int, limit int) ([]Message, error) + GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursor *time.Time, pageSize int) ([]Message, error) } type DBTX interface { @@ -172,65 +172,71 @@ func (r *repository) SaveMessage(ctx context.Context, message *Message) error { return nil } -/* -func (r *repository) FetchMessagesFromRedis(ctx context.Context, chatRoomID uuid.UUID, limit int) ([]Message, error) { - redisKey := "chatroom:" + chatRoomID.String() + ":messages" - messagesJSON, err := r.redisClient.LRange(ctx, redisKey, -limit, -1).Result() - if err != nil { - if err == redis.Nil { - // Redis key does not exist - return nil, nil - } - // Some other error occurred - return nil, err - } - - var messages []Message - for _, mJSON := range messagesJSON { - var msg Message - err := json.Unmarshal([]byte(mJSON), &msg) - if err != nil { - log.Printf("Failed to unmarshal message: %v", err) - // Decide how you want to handle partial failure - continue +// GetPaginatedMessages retrieves messages from the database with pagination. +func (r *repository) GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursor *time.Time, pageSize int) ([]Message, error) { + /* + offset := (pageNum - 1) * pageSize // calculate the offset + if offset < 0 { + offset = 0 // ensure offset is not negative } - messages = append(messages, msg) - } - return messages, nil -} */ - -// FetchMessagesFromDatabase retrieves chat messages from PostgreSQL -func (r *repository) FetchMessagesFromDatabase(ctx context.Context, chatRoomID uuid.UUID, page int, limit int) ([]Message, error) { - offset := page * limit + query := ` + SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id + FROM messages + WHERE chat_room_id = $1 + ORDER BY created_at DESC + LIMIT $2 OFFSET $3 + ` */ query := ` - SELECT id, chat_room_id, sender_id, content, created_at + SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id FROM messages - WHERE chat_room_id = $1 + WHERE chat_room_id = $1 AND created_at < $2 ORDER BY created_at DESC - LIMIT $2 OFFSET $3 + LIMIT $3 ` - rows, err := r.db.QueryContext(ctx, query, chatRoomID, limit, offset) + rows, err := r.db.QueryContext(ctx, query, chatRoomID, cursor, pageSize) if err != nil { - log.Printf("Failed to fetch messages from database: %v", err) - return nil, err + return nil, fmt.Errorf("querying for paginated messages: %w", err) } defer rows.Close() var messages []Message for rows.Next() { var msg Message - if err := rows.Scan(&msg.ID, &msg.ChatRoomID, &msg.SenderID, &msg.Content, &msg.CreatedAt); err != nil { - log.Printf("Failed to scan message: %v", err) - // Decide how you want to handle partial failure - continue + var readAt sql.NullTime + var deletedByUserID sql.NullString // Use sql.NullString for UUID fields that can be NULL + + if err := rows.Scan( + &msg.ID, + &msg.ChatRoomID, + &msg.SenderID, + &msg.Content, + &msg.MediaURL, + &msg.CreatedAt, + &readAt, + &deletedByUserID, + ); err != nil { + return nil, fmt.Errorf("scanning message: %w", err) + } + + // Check if readAt is valid, if so, assign it to the struct + if readAt.Valid { + msg.ReadAt = &readAt.Time } + + // Check if deletedByUserID is valid, if so, convert to uuid.UUID and assign it to the struct + if deletedByUserID.Valid { + uid, err := uuid.Parse(deletedByUserID.String) + if err != nil { + return nil, fmt.Errorf("parsing UUID: %w", err) + } + msg.DeletedByUserID = &uid + } + messages = append(messages, msg) } - - // Check for any error that occurred during iteration if err = rows.Err(); err != nil { - return nil, err + return nil, fmt.Errorf("iterating rows: %w", err) } return messages, nil diff --git a/backend/internal/chat/repository_test.go b/backend/internal/chat/repository_test.go new file mode 100644 index 0000000..9a7aabf --- /dev/null +++ b/backend/internal/chat/repository_test.go @@ -0,0 +1,40 @@ +package chat + +import ( + "context" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestCreateChatRoom(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("An error '%s' was not expected when opening a stub database connection", err) + } + defer db.Close() + + repo := NewRepository(db) + + chatRoom := &ChatRoom{} + expectedID := uuid.New() + + // Set up the expectations + mock.ExpectQuery("INSERT INTO chat_rooms"). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(expectedID)) + + // Call the method under test + result, err := repo.CreateChatRoom(context.Background(), chatRoom) + + // Assert the expectations + assert.NoError(t, err) + assert.Equal(t, expectedID, result.ID) + + // Ensure all expectations were met + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } +} diff --git a/backend/internal/chat/service.go b/backend/internal/chat/service.go index 608eb2b..bcf3e96 100644 --- a/backend/internal/chat/service.go +++ b/backend/internal/chat/service.go @@ -17,6 +17,7 @@ type Service interface { GetChatRoomInfoByID(ctx context.Context, chatRoomID uuid.UUID) (*ChatRoomInfo, error) JoinChatRoomByID(ctx context.Context, chatRoomID uuid.UUID, userID uuid.UUID) (*ChatRoom, error) RegisterClient(ctx context.Context, hub *Hub, conn *websocket.Conn, chatroomID uuid.UUID, userID uuid.UUID, kafkaProducer *confluentKafka.Producer) (*Client, error) + GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursorTime *time.Time, pageSize int) ([]Message, error) } type service struct { @@ -88,18 +89,6 @@ func (s *service) GetChatRoomInfoByID(ctx context.Context, chatRoomID uuid.UUID) return chatRoomInfo, nil } -/* -func (s *service) GetChatMessages(ctx context.Context, chatRoomID uuid.UUID, page int) ([]Message, error) { - // Attempt to fetch from Redis - messages, err := s.Repository.FetchMessagesFromRedis(ctx, chatRoomID, page) - if err == nil && len(messages) > 0 { - return messages, nil - } - - // Fallback to another method if Redis is empty or there's an error - return s.Repository.FetchMessagesFromDatabase(ctx, chatRoomID, page, 0) -} */ - func (s *service) RegisterClient(ctx context.Context, hub *Hub, conn *websocket.Conn, chatroomID uuid.UUID, userID uuid.UUID, kafkaProducer *confluentKafka.Producer) (*Client, error) { senderID := userID @@ -110,3 +99,13 @@ func (s *service) RegisterClient(ctx context.Context, hub *Hub, conn *websocket. client.hub.register <- client return client, nil } + +func (s *service) GetPaginatedMessages(ctx context.Context, chatRoomID uuid.UUID, cursorTime *time.Time, pageSize int) ([]Message, error) { + + res, err := s.Repository.GetPaginatedMessages(ctx, chatRoomID, cursorTime, pageSize) + if err != nil { + log.Printf("Error occured with getting paginated messages for chat room ID %v: %v", chatRoomID, err) + return nil, err + } + return res, nil +} diff --git a/backend/router/router.go b/backend/router/router.go index 5846894..fa309e3 100644 --- a/backend/router/router.go +++ b/backend/router/router.go @@ -77,6 +77,7 @@ func InitRouter(cfg *RouterConfig) { chatRoutes.POST("/", cfg.ChatHandler.CreateChatRoom) chatRoutes.GET("/:id", cfg.ChatHandler.GetChatRoom) chatRoutes.POST("/:id", cfg.ChatHandler.JoinChatRoom) + chatRoutes.GET("/:id/messages", cfg.ChatHandler.GetChatMessages) // etc... } diff --git a/backend/scripts/query-analyze.txt b/backend/scripts/query-analyze.txt new file mode 100644 index 0000000..8d3d74c --- /dev/null +++ b/backend/scripts/query-analyze.txt @@ -0,0 +1,12 @@ +EXPLAIN ANALYZE SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id + FROM messages + WHERE chat_room_id = '21e546be-629d-4e84-9236-b05c69becb18' + ORDER BY created_at DESC + LIMIT 50 OFFSET 0; + +EXPLAIN ANALYZE SELECT id, chat_room_id, sender_id, content, media_url, created_at, read_at, deleted_by_user_id + FROM messages + WHERE chat_room_id = '21e546be-629d-4e84-9236-b05c69becb18' + AND id < 12000 + ORDER BY id DESC + LIMIT 50; diff --git a/frontend/src/components/Layout/Layout.module.scss b/frontend/src/components/Layout/Layout.module.scss new file mode 100644 index 0000000..76e4ffe --- /dev/null +++ b/frontend/src/components/Layout/Layout.module.scss @@ -0,0 +1,5 @@ +.layout { + display: flex; + flex-direction: column; + height: 100vh; /* This ensures the container takes the full height of the viewport */ +} diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..106e749 --- /dev/null +++ b/frontend/src/components/Layout/Layout.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import styles from './Layout.module.scss'; +import { Header } from '@components/Header'; + +type LayoutProps = { + children: React.ReactNode; +}; + +export const Layout = ({ children }: LayoutProps) => { + return ( +
+
+ {children} +
+ ); +}; diff --git a/frontend/src/features/chat/api/chatApi.ts b/frontend/src/features/chat/api/chatApi.ts new file mode 100644 index 0000000..fc36fd1 --- /dev/null +++ b/frontend/src/features/chat/api/chatApi.ts @@ -0,0 +1,50 @@ +import { axiosInstance } from '@lib/axios'; +import { AxiosError } from 'axios'; +import { ChatMessagesResponse } from '../types'; +import { authTokenKey } from '@config'; + +// Function to fetch chat messages with proper types +export async function fetchChatMessagesFn( + chatRoomId: string, + pageParam?: string // Make pageParam optional as it may not be provided for the initial request +): Promise { + // Parse the string into a Date object + const dateTimeString = '2023-11-09 21:06:13.567'; + const date = new Date(dateTimeString); + + // Format the Date object as an ISO 8601 string (commonly used in APIs) + const isoDateString = date.toISOString(); + const chatMessagesURL = `/api/chats/${chatRoomId}/messages?cursor=${isoDateString}`; + + console.log(isoDateString); // Outputs: 2023-11-09T21:06:13.567Z (note the 'Z' indicating UTC) + + const supabaseData = localStorage.getItem(authTokenKey); + + let accessToken = ''; + + if (supabaseData) { + const parsedData = JSON.parse(supabaseData); + accessToken = parsedData.access_token; + } else { + console.error('No Supabase data found in Local Storage'); + } + try { + console.log('Token = ', accessToken); + const { data } = await axiosInstance.get( + chatMessagesURL, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + } + ); + + return data; + } catch (error) { + const axiosError = error as AxiosError; + // Handle the error as you see fit for your application context + // This could involve logging the error, returning a default response, throwing a custom error, etc. + throw new Error(axiosError.message); + } +} diff --git a/frontend/src/features/chat/components/ChatArea.tsx b/frontend/src/features/chat/components/ChatArea.tsx index a5356fa..e39944c 100644 --- a/frontend/src/features/chat/components/ChatArea.tsx +++ b/frontend/src/features/chat/components/ChatArea.tsx @@ -1,28 +1,79 @@ -import React from 'react'; import styles from './ChatArea.module.scss'; import { Message } from './Message'; import { useChatMessageStore } from '@stores'; - -export type MessageType = { - sender: 'received' | 'sent'; - content: string; - timestamp: Date; - img?: string; // Optional image URL for the sender's profile (for 'received' messages) -}; +import { MessageType, ServerMessageType } from '../types'; +import { useChatMessages } from '@hooks'; +import { useEffect } from 'react'; +import { authTokenKey } from '@config'; type ChatAreaProps = { messages: MessageType[]; }; -export function ChatArea({ messages: propMessages }: ChatAreaProps) { +export function ChatArea({ messages: propMessages }: Readonly) { const { messages: storeMessages } = useChatMessageStore((state) => ({ messages: state.messages, })); + // Call the custom hook and pass the chatRoomId to it + const chatRoomId = '25e4eb83-5210-448d-be58-3a4c355113be'; + const { + data, + error, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + status, + } = useChatMessages(chatRoomId); + useEffect(() => { + console.log('Data loaded', data?.pages[0].messages); + }, [data]); + + if (status === 'loading') return

Loading...

; + if (status === 'error') return

Error: {error.message}

; + + const supabaseData = localStorage.getItem(authTokenKey); + + let currentUserId = ''; + + if (supabaseData) { + const parsedData = JSON.parse(supabaseData); + currentUserId = parsedData.user.id; + console.log('Current user id', currentUserId); + } else { + console.error('No Supabase data found in Local Storage'); + } + const restAPIMessages: ServerMessageType[] = + data?.pages.flatMap((page) => + page.messages.map((message: unknown) => { + // Assert the message to be of type ServerMessageType + const serverMessage = message as unknown as ServerMessageType; + + const adaptedMessage: ServerMessageType = { + id: serverMessage.id, + chat_room_id: serverMessage.chat_room_id, + sender: + serverMessage.sender_id === currentUserId ? 'sent' : 'received', // Set the sender based on the current user's ID + media_url: serverMessage.media_url, + created_at: serverMessage.created_at, + content: serverMessage.content, + sender_id: serverMessage.sender_id, + }; + + return adaptedMessage; + }) + ) ?? []; + + const combinedMessages = [...storeMessages, ...propMessages].sort( + (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); return (
- {[...storeMessages, ...propMessages].map((message, timestamp) => ( - + {restAPIMessages.map((message) => ( + + ))} + {combinedMessages.map((message) => ( + ))}
); diff --git a/frontend/src/features/chat/components/InputArea.tsx b/frontend/src/features/chat/components/InputArea.tsx index 1a8ccf5..f1e0413 100644 --- a/frontend/src/features/chat/components/InputArea.tsx +++ b/frontend/src/features/chat/components/InputArea.tsx @@ -6,7 +6,7 @@ type InputAreaProps = { onMessageSend: (text: string) => void; }; -export function InputArea({ onMessageSend }: InputAreaProps) { +export function InputArea({ onMessageSend }: Readonly) { const [message, setMessage] = useState(''); // State to hold the input value const { sendMessage } = useWebSocketConnection(); const handleInputChange = (e: React.ChangeEvent) => { diff --git a/frontend/src/features/chat/components/Message.tsx b/frontend/src/features/chat/components/Message.tsx index 84d595a..3043dab 100644 --- a/frontend/src/features/chat/components/Message.tsx +++ b/frontend/src/features/chat/components/Message.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styles from './Message.module.scss'; type MessageProps = { - sender: 'received' | 'sent'; + sender: string; text: string; img?: string; }; @@ -14,9 +14,6 @@ export const Message = ({ sender, text, img }: MessageProps) => { sender === 'received' ? styles.messageReceived : styles.messageSent } > - {sender === 'received' && ( - Sender's Profile - )}

{text}

); diff --git a/frontend/src/features/chat/index.ts b/frontend/src/features/chat/index.ts index f102e3c..0f823dd 100644 --- a/frontend/src/features/chat/index.ts +++ b/frontend/src/features/chat/index.ts @@ -1,3 +1,5 @@ export * from './components/ChatArea'; export * from './components/InputArea'; export * from './components/Message'; +export * from './api/chatApi'; +export * from './types'; diff --git a/frontend/src/features/chat/types/index.ts b/frontend/src/features/chat/types/index.ts index 0cbb493..0aab125 100644 --- a/frontend/src/features/chat/types/index.ts +++ b/frontend/src/features/chat/types/index.ts @@ -1,5 +1,35 @@ export type MessageType = { sender: 'received' | 'sent'; - text: string; + senderID: string; + content: string; + createdAt: Date; img?: string; // Optional image URL for the sender's profile (for 'received' messages) }; + +// A type for the incoming message that reflects the server's response +export type ServerMessageType = { + id: number; + chat_room_id: string; + sender_id: string; + content: string; + media_url: string; + created_at: Date; + sender: 'received' | 'sent'; +}; + +export type CombinedMessageType = ChatMessage & ServerMessageType; + +// TypeScript interfaces to represent the API response structure +export interface ChatMessage { + id: number; + chatRoomId: string; + senderId: string; + content: string; + mediaUrl: string; + createdAt: string; // Use string if you plan to manually handle date parsing, otherwise consider using Date type +} + +export interface ChatMessagesResponse { + messages: ChatMessage[]; + nextCursor?: string; // Assuming your API provides a cursor for the next page +} diff --git a/frontend/src/hooks/index.ts b/frontend/src/hooks/index.ts new file mode 100644 index 0000000..2c00986 --- /dev/null +++ b/frontend/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useWebSocketConnection'; +export * from './useChatMessages'; diff --git a/frontend/src/hooks/useChatMessages.tsx b/frontend/src/hooks/useChatMessages.tsx new file mode 100644 index 0000000..2ede0bd --- /dev/null +++ b/frontend/src/hooks/useChatMessages.tsx @@ -0,0 +1,12 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { ChatMessagesResponse, fetchChatMessagesFn } from '@features/chat'; + +export const useChatMessages = (chatRoomId: string) => { + return useInfiniteQuery( + ['chatMessages', chatRoomId], // Query key includes chatRoomId to ensure uniqueness per chat room + ({ pageParam }) => fetchChatMessagesFn(chatRoomId, pageParam), // Fetch function + { + getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined, // Determine the cursor for the next page + } + ); +}; diff --git a/frontend/src/hooks/useWebSocketConnection.ts b/frontend/src/hooks/useWebSocketConnection.ts index f5ebed1..b97f431 100644 --- a/frontend/src/hooks/useWebSocketConnection.ts +++ b/frontend/src/hooks/useWebSocketConnection.ts @@ -1,5 +1,5 @@ import { WEBSOCKET_URL, authTokenKey } from '@config'; -import { MessageType } from '@features/chat'; +import { MessageType, ServerMessageType } from '@features/chat'; import { useChatMessageStore } from '@stores'; import { useQueryClient } from '@tanstack/react-query'; import { useEffect, useRef, useState } from 'react'; @@ -16,7 +16,8 @@ export const useWebSocketConnection = () => { const sendMessage = (message: string) => { if (webSocket.current) { console.log('Sending message:', message); - webSocket.current.send(message); + + webSocket.current.send(message); // serialize the object to a JSON string } }; @@ -35,7 +36,7 @@ export const useWebSocketConnection = () => { const webSocketURL = WEBSOCKET_URL + '/chats/' + - '21e546be-629d-4e84-9236-b05c69becb18?' + + '25e4eb83-5210-448d-be58-3a4c355113be?' + 'token=' + accessToken; @@ -47,7 +48,20 @@ export const useWebSocketConnection = () => { }; const handleWebSocketOnMessage = (event: MessageEvent) => { - const receivedMessage: MessageType = JSON.parse(event.data); + const serverMessage: ServerMessageType = JSON.parse(event.data); + + // Convert the snake_case properties from the server message to camelCase for the MessageType + const receivedMessage: MessageType = { + sender: 'received', + senderID: serverMessage.sender_id, + content: serverMessage.content, + createdAt: new Date(serverMessage.created_at), + img: serverMessage.media_url || undefined, + }; + + console.log('Receiving messages..', receivedMessage); + + // receivedMessage.sender = 'received'; addMessage(receivedMessage); }; diff --git a/frontend/src/App.css b/frontend/src/pages/LobbyPage/Lobby.module.scss similarity index 100% rename from frontend/src/App.css rename to frontend/src/pages/LobbyPage/Lobby.module.scss diff --git a/frontend/src/pages/LobbyPage/Lobby.tsx b/frontend/src/pages/LobbyPage/Lobby.tsx new file mode 100644 index 0000000..225d450 --- /dev/null +++ b/frontend/src/pages/LobbyPage/Lobby.tsx @@ -0,0 +1,23 @@ +import { ChatArea, InputArea, MessageType } from '@features/chat'; +import { useState } from 'react'; + +export function Lobby() { + const [messages, setMessages] = useState([]); + + const handleNewMessage = (text: string) => { + const newMessage: MessageType = { + sender: 'sent', + senderID: '1', // TODO: replace with actual user ID + content: text, + createdAt: new Date(), + }; + setMessages((prevMessages) => [...prevMessages, newMessage]); + }; + + return ( + <> + + + + ); +} diff --git a/frontend/src/pages/LobbyPage/index.ts b/frontend/src/pages/LobbyPage/index.ts new file mode 100644 index 0000000..08355c7 --- /dev/null +++ b/frontend/src/pages/LobbyPage/index.ts @@ -0,0 +1 @@ +export * from './Lobby'; diff --git a/frontend/src/pages/RootPage/Root.module.scss b/frontend/src/pages/RootPage/Root.module.scss index 9b221d4..55c56dd 100644 --- a/frontend/src/pages/RootPage/Root.module.scss +++ b/frontend/src/pages/RootPage/Root.module.scss @@ -1,5 +1,16 @@ -.container { - display: flex; - flex-direction: column; - height: 100vh; /* This ensures the container takes the full height of the viewport */ +.authContainer { + max-width: 25rem; /* max-width in rem */ + margin: 1rem auto; /* vertical and horizontal margin in rem */ + padding: 1rem; /* padding in rem */ + box-shadow: 0 0.25rem 0.375rem rgba(0, 0, 0, 0.1); /* shadow offset and blur in rem */ + border-radius: 0.5rem; /* border-radius in rem */ + background: white; /* background color */ +} + +/* Responsive styles */ +@media (max-width: 48rem) { + /* media query in rem */ + .authContainer { + margin: 1rem; /* margin in rem */ + } } diff --git a/frontend/src/pages/RootPage/Root.tsx b/frontend/src/pages/RootPage/Root.tsx index 089a7f5..b129073 100644 --- a/frontend/src/pages/RootPage/Root.tsx +++ b/frontend/src/pages/RootPage/Root.tsx @@ -3,26 +3,15 @@ import { Auth } from '@supabase/auth-ui-react'; import { Session, createClient } from '@supabase/supabase-js'; import { ThemeSupa } from '@supabase/auth-ui-shared'; import { useState, useEffect } from 'react'; +import { Layout } from '@components/Layout/Layout'; +import { Outlet } from 'react-router-dom'; import styles from './Root.module.scss'; -import { Header } from '@components/Header'; -import { ChatArea, InputArea, MessageType } from '@features/chat'; const supabase = createClient(SUPABASE_URL, SUPABASE_API_KEY); export function Root() { const [session, setSession] = useState(null); - const [messages, setMessages] = useState([]); - - const handleNewMessage = (text: string) => { - const newMessage: MessageType = { - sender: 'sent', - content: text, - timestamp: new Date(), - }; - setMessages((prevMessages) => [...prevMessages, newMessage]); - }; - useEffect(() => { supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); @@ -38,13 +27,19 @@ export function Root() { }, []); if (!session) { - return ; + return ( +
+ +
+ ); } return ( -
-
- - -
+ + + ); } diff --git a/frontend/src/providers/Routes.tsx b/frontend/src/providers/Routes.tsx index 1e5bfd8..92a6727 100644 --- a/frontend/src/providers/Routes.tsx +++ b/frontend/src/providers/Routes.tsx @@ -1,11 +1,18 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import { Root } from '@pages'; +import { Lobby } from '@pages/LobbyPage'; export function Routes() { const router = createBrowserRouter([ { path: '/', element: , + children: [ + { + path: '/', + element: , + }, + ], }, ]);