Skip to content
3 changes: 0 additions & 3 deletions client/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const signUp = async (credentials: SignUpCredentials): Promise<void> => {
try {
await api.post("/auth/register", credentials);
} catch (error) {
console.error(error);
throw error;
}
};
Expand All @@ -17,7 +16,6 @@ export const signIn = async (credentials: LoginCredentials): Promise<void> => {
try {
await api.post("/auth/login", credentials);
} catch (error) {
console.error(error);
throw error;
}
};
Expand All @@ -26,7 +24,6 @@ export const logOut = async (): Promise<void> => {
try {
await api.post("/auth/logout");
} catch (error) {
console.error(error);
throw error;
}
};
13 changes: 4 additions & 9 deletions client/src/api/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ import api from "@/config/axiosConfig";
import type { Message } from "@/config/schema/Message";

export const getMessagesOfRoom = async (roomId: string): Promise<Message[]> => {
try {
const response = await api.get(`/messages/${roomId}`, {
withCredentials: true,
});
return response.data.messages;
} catch (error) {
console.error(error);
throw error;
}
const response = await api.get(`/messages/${roomId}`, {
withCredentials: true,
});
return response.data.messages;
};
1 change: 0 additions & 1 deletion client/src/api/otp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,3 @@ export const verifyOTP = async (
throw error;
}
};

52 changes: 52 additions & 0 deletions client/src/api/room_resource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import api from "@/config/axiosConfig";
import type { Resource } from "@/config/schema/Resource";

export const createResource = async (
roomId: string,
sectionId: string,
data: { title: string; link: string }
): Promise<Resource> => {
try {
const response = await api.post(
`/rooms/${roomId}/sections/${sectionId}/create`,
data,
{ withCredentials: true }
);
return response.data;
} catch (error) {
throw error;
}
};

export const updateResource = async (
roomId: string,
sectionId: string,
resourceId: string,
data: { title: string; link: string }
): Promise<Resource> => {
try {
const response = await api.put(
`/rooms/${roomId}/sections/${sectionId}/${resourceId}`,
data,
{ withCredentials: true }
);
return response.data;
} catch (error) {
throw error;
}
};

export const deleteResource = async (
roomId: string,
sectionId: string,
resourceId: string
): Promise<void> => {
try {
await api.delete(
`/rooms/${roomId}/sections/${sectionId}/${resourceId}`,
{ withCredentials: true }
);
} catch (error) {
throw error;
}
};
48 changes: 48 additions & 0 deletions client/src/api/room_section.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import api from "@/config/axiosConfig";
import type { Section } from "@/config/schema/Section";

export const createSection = async (
roomId: string,
title: string
): Promise<Section> => {
try {
const response = await api.post(
`/rooms/${roomId}/sections/create`,
{ title },
{ withCredentials: true }
);
return response.data;
} catch (error) {
throw error;
}
};

export const updateSection = async (
roomId: string,
sectionId: string,
title: string
): Promise<Section> => {
try {
const response = await api.put(
`/rooms/${roomId}/sections/${sectionId}`,
{ title },
{ withCredentials: true }
);
return response.data;
} catch (error) {
throw error;
}
};

export const deleteSection = async (
roomId: string,
sectionId: string
): Promise<void> => {
try {
await api.delete(`/rooms/${roomId}/sections/${sectionId}`, {
withCredentials: true,
});
} catch (error) {
throw error;
}
};
2 changes: 1 addition & 1 deletion client/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const fetchCurrentUser = async (): Promise<User> => {
}
};

export const updateUser = async (formData: FormData): Promise<any> => {
export const updateUser = async (formData: FormData): Promise<User> => {
try {
const response = await api.post("/user/update", formData, {
withCredentials: true,
Expand Down
32 changes: 2 additions & 30 deletions client/src/components/Main/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { useState, useEffect } from "react";
import { BookOpen } from "lucide-react";
import type { StudyRoom } from "@/config/schema/StudyRoom";
import { RoomInfoPanel } from "@/components/Room/RoomInfoPanel";
import { WelcomePlaceholder } from "@/components/common/WelcomePlaceHolder";
Expand All @@ -9,6 +8,7 @@ import ChatPanel from "../chat/ChatPanel";
import { MainContentHeader } from "./MainContentHeader";
import Whiteboard from "../whiteboard/Whiteboard";
import { useSocketMessages } from "@/hooks/useSocketMessages";
import ResourceHubPanel from "../resourcehub/ResourceHubPanel";

interface MainContentProps {
selectedRoom: StudyRoom | null;
Expand Down Expand Up @@ -78,15 +78,7 @@ export function MainContent({
<Whiteboard roomId={selectedRoom._id} />
</div>
)}
{mainContentTab === "resourceHub" && (
<Placeholder
icon={
<BookOpen className="mx-auto size-10 text-emerald-500 mb-3" />
}
title="Resource Hub"
description="All uploaded study materials and shared links will show up here."
/>
)}
{mainContentTab === "resourceHub" && <ResourceHubPanel />}
{mainContentTab === "settings" && canEdit && (
<div className="p-6 bg-neutral-900 shadow-md rounded-xl border border-emerald-800/40">
<EditRoomPanel room={selectedRoom} />
Expand All @@ -96,23 +88,3 @@ export function MainContent({
</div>
);
}

function Placeholder({
icon,
title,
description,
}: {
icon: React.ReactNode;
title: string;
description: string;
}) {
return (
<div className="p-6 text-emerald-100 flex items-center justify-center h-full bg-neutral-950">
<div className="text-center max-w-md mx-auto">
{icon}
<h3 className="font-semibold text-lg mb-1 text-emerald-400">{title}</h3>
<p className="text-sm text-emerald-200/70">{description}</p>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion client/src/components/Main/MainContentHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function MainContentHeader({
setMainContentTab: (
tab: "chat" | "info" | "whiteboard" | "resourceHub" | "settings"
) => void;
canEdit: boolean;
canEdit: Boolean;
}) {
const tabs = [
{ key: "chat", label: "Chat", icon: <MessageSquare className="size-4" /> },
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Room/RoomInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const RoomInfoPanel = ({

return (
<li
key={member._id}
key={member.user._id}
className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${
isCurrentUser
? "bg-emerald-800/10 border-emerald-600/30"
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ChatInput: React.FC<any> = () => {
}, [message]);

const handleSubmit = (e: React.FormEvent) => {
if (!userData) return;
e.preventDefault();
const trimmedMessage = message.trim();
if (!trimmedMessage || !selectedRoom) return;
Expand All @@ -38,10 +39,9 @@ const ChatInput: React.FC<any> = () => {
content: trimmedMessage,
room: selectedRoom._id,
sender: {
_id: userData?._id,
username: userData?.username,
_id: userData._id,
name: userData?.name,
profileImage: userData?.profileImage,
profileImage: userData.profileImage,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
Expand Down
49 changes: 27 additions & 22 deletions client/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,65 @@ import { useSelector, useDispatch } from "react-redux";
import MessageBubble from "./MessageBubble";
import type { RootState } from "@/redux/store";
import { getMessagesOfRoom } from "@/api/message";
import type { Message } from "@/config/schema/Message";
import { setInitialMessages } from "@/redux/slices/roomSlice";
import { setMessages } from "@/redux/slices/roomSlice";
import ChatInput from "./ChatInput";
import type { Message } from "@/config/schema/Message";

const EMPTY_MESSAGES: Message[] = [];

const ChatPanel: React.FC = () => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [isFetchingInitial, setIsFetchingInitial] = useState(false);
const [error, setError] = useState<string | null>(null);

const selectedRoom = useSelector(
(state: RootState) => state.room.selectedRoom
);
const messages: Message[] = useSelector((state: RootState) =>
selectedRoom
? state.room.messages[selectedRoom._id] || EMPTY_MESSAGES
: EMPTY_MESSAGES

const messages: Message[] = useSelector(
() => selectedRoom?.messages || EMPTY_MESSAGES
);

const { userData } = useSelector((state: RootState) => state.user);

useEffect(() => {
if (selectedRoom) {
const fetchInitialMessages = async () => {
setIsLoading(true);
const hasMessages =
selectedRoom.messages && selectedRoom.messages.length > 0;

if (!hasMessages) {
setIsFetchingInitial(true);
}

const fetchMessages = async () => {
setError(null);
try {
const fetchedMessages = await getMessagesOfRoom(selectedRoom._id);
dispatch(
setInitialMessages({
setMessages({
roomId: selectedRoom._id,
messages: fetchedMessages,
})
);
} catch (err) {
} catch {
setError("Failed to load messages.");
} finally {
setIsLoading(false);
setIsFetchingInitial(false);
}
};
fetchInitialMessages();

fetchMessages();
}
}, [selectedRoom, dispatch]);

useEffect(() => {
if (!isLoading && messages.length > 0) {
if (messages.length > 0) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, isLoading]);
}, [messages.length]);

if (!selectedRoom && !isLoading) {
if (!selectedRoom && !isFetchingInitial) {
return (
<div className="flex flex-col h-full text-emerald-400 bg-gray-900">
<div className="flex-1 flex items-center justify-center">
Expand All @@ -73,24 +80,22 @@ const ChatPanel: React.FC = () => {
return (
<div className="flex flex-col h-full text-emerald-400 bg-gray-900">
<div className="flex-1 overflow-y-auto p-4 space-y-2">
{isLoading && (
{isFetchingInitial && messages.length === 0 && (
<div className="flex items-center justify-center h-full">
<span className="loading loading-spinner text-emerald-500"></span>
</div>
)}
{!isLoading && error && (
{!isFetchingInitial && error && messages.length === 0 && (
<div className="flex items-center justify-center h-full text-emerald-600">
<p>{error}</p>
</div>
)}
{!isLoading && !error && messages.length === 0 && (
{!isFetchingInitial && !error && messages.length === 0 && (
<div className="flex items-center justify-center h-full text-emerald-400/50">
<p>No messages yet. Start the conversation!</p>
</div>
)}
{!isLoading &&
!error &&
messages.length > 0 &&
{messages.length > 0 &&
messages.map((msg) => {
const isOwnMessage = msg.sender._id === userData?._id;
const messageKey =
Expand Down
6 changes: 2 additions & 4 deletions client/src/components/chat/MessageBubble.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Message } from "@/config/schema/Message";
import React from "react";
import { User } from "lucide-react";
import type { Message } from "@/config/schema/Message";

interface MessageBubbleProps {
message: Message;
Expand All @@ -17,9 +17,7 @@ const MessageBubble: React.FC<MessageBubbleProps> = ({
? "bg-emerald-500 text-white px-4 py-2 max-w-xs break-words rounded-tl-xl rounded-tr-xl rounded-bl-xl rounded-br-md"
: "bg-emerald-200 text-emerald-900 px-4 py-2 max-w-xs break-words rounded-tl-xl rounded-tr-xl rounded-br-xl rounded-bl-md";

const senderName = isOwnMessage
? "You"
: message.sender?.name || message.sender?.username || "Unknown";
const senderName = isOwnMessage ? "You" : message.sender?.name || "Unknown";

const time = new Date(message.createdAt || Date.now()).toLocaleTimeString(
[],
Expand Down
Loading