Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,026 changes: 1,003 additions & 23 deletions client/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"axios": "^1.11.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fabric": "^7.0.0-beta1",
"lucide-react": "^0.542.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
Expand Down
129 changes: 11 additions & 118 deletions client/src/components/Main/MainContent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import React, { useState, useEffect } from "react";
import {
Info,
MessageSquare,
Settings,
BookOpen,
LayoutGrid,
} from "lucide-react";
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";
import { PublicRoomPreview } from "../Room/PublicRoomPreview";
import { EditRoomPanel } from "../Room/EditRoomModal";
import ChatPanel from "../chat/ChatPanel";
import { MainContentHeader } from "./MainContentHeader";
import Whiteboard from "../whiteboard/Whiteboard";
import { useSocketMessages } from "@/hooks/useSocketMessages";

interface MainContentProps {
selectedRoom: StudyRoom | null;
Expand All @@ -32,6 +29,8 @@ export function MainContent({
"chat" | "info" | "whiteboard" | "resourceHub" | "settings"
>("chat");

useSocketMessages();

useEffect(() => {
setMainContentTab("chat");
}, [selectedRoom?._id]);
Expand Down Expand Up @@ -69,19 +68,15 @@ export function MainContent({
canEdit={canEdit}
/>

<div className="flex-1 overflow-y-auto">
<div className="flex-1 overflow-hidden">
{mainContentTab === "chat" && <ChatPanel />}
{mainContentTab === "info" && userData && (
<RoomInfoPanel room={selectedRoom} userId={userData._id} />
)}
{mainContentTab === "whiteboard" && (
<Placeholder
icon={
<LayoutGrid className="mx-auto size-10 text-emerald-500 mb-3" />
}
title="Whiteboard Space"
description="Collaborative workspace area will appear here soon."
/>
<div className="w-full h-full">
<Whiteboard roomId={selectedRoom._id} />
</div>
)}
{mainContentTab === "resourceHub" && (
<Placeholder
Expand All @@ -102,108 +97,6 @@ export function MainContent({
);
}

function MainContentHeader({
roomName,
roomImage,
mainContentTab,
setMainContentTab,
canEdit,
}: {
roomName: string;
roomImage?: string;
mainContentTab: "chat" | "info" | "whiteboard" | "resourceHub" | "settings";
setMainContentTab: (
tab: "chat" | "info" | "whiteboard" | "resourceHub" | "settings"
) => void;
canEdit: boolean;
}) {
const tabs = [
{ key: "chat", label: "Chat", icon: <MessageSquare className="size-4" /> },
{
key: "whiteboard",
label: "Whiteboard",
icon: <LayoutGrid className="size-4" />,
},
{
key: "resourceHub",
label: "Resources",
icon: <BookOpen className="size-4" />,
},
{ key: "info", label: "Info", icon: <Info className="size-4" /> },
];

if (canEdit) {
tabs.push({
key: "settings",
label: "Settings",
icon: <Settings className="size-4" />,
});
}

return (
<div className="px-6 py-4 bg-neutral-900 border-b border-emerald-900/40 flex justify-between items-center shadow-sm sticky top-0 z-20">
<div
className="flex items-center gap-4 flex-1 min-w-0"
onClick={() => {
if (mainContentTab === "info") {
setMainContentTab("chat");
} else {
setMainContentTab("info");
}
}}
>
<div className="avatar">
<div className="w-10 h-10 rounded-xl overflow-hidden bg-emerald-950 ring-1 ring-emerald-700/40">
{roomImage ? (
<img
src={roomImage}
alt={roomName}
className="object-cover w-full h-full"
/>
) : (
<div className="flex items-center justify-center h-full w-full text-emerald-400 font-semibold">
{roomName.substring(0, 2).toUpperCase()}
</div>
)}
</div>
</div>
<h2 className="font-bold text-lg truncate text-emerald-100">
{roomName}
</h2>
</div>

<div className="flex items-center gap-2">
<div className="inline-flex items-center gap-1 p-1 bg-neutral-800 rounded-xl border border-emerald-800/40 shadow-inner">
{tabs.map((tab) => (
<button
key={tab.key}
type="button"
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
mainContentTab === tab.key
? "bg-emerald-600 text-white shadow-md"
: "text-emerald-300 hover:text-white hover:bg-emerald-800/40"
}`}
onClick={() =>
setMainContentTab(
tab.key as
| "chat"
| "info"
| "whiteboard"
| "resourceHub"
| "settings"
)
}
>
{tab.icon}
<span>{tab.label}</span>
</button>
))}
</div>
</div>
</div>
);
}

function Placeholder({
icon,
title,
Expand Down
103 changes: 103 additions & 0 deletions client/src/components/Main/MainContentHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { BookOpen, Info, LayoutGrid, MessageSquare, Settings } from "lucide-react";

export function MainContentHeader({
roomName,
roomImage,
mainContentTab,
setMainContentTab,
canEdit,
}: {
roomName: string;
roomImage?: string;
mainContentTab: "chat" | "info" | "whiteboard" | "resourceHub" | "settings";
setMainContentTab: (
tab: "chat" | "info" | "whiteboard" | "resourceHub" | "settings"
) => void;
canEdit: boolean;
}) {
const tabs = [
{ key: "chat", label: "Chat", icon: <MessageSquare className="size-4" /> },
{
key: "whiteboard",
label: "Whiteboard",
icon: <LayoutGrid className="size-4" />,
},
{
key: "resourceHub",
label: "Resources",
icon: <BookOpen className="size-4" />,
},
{ key: "info", label: "Info", icon: <Info className="size-4" /> },
];

if (canEdit) {
tabs.push({
key: "settings",
label: "Settings",
icon: <Settings className="size-4" />,
});
}

return (
<div className="px-6 py-4 bg-neutral-900 border-b border-emerald-900/40 flex justify-between items-center shadow-sm sticky top-0 z-20">
<div
className="flex items-center gap-4 flex-1 min-w-0"
onClick={() => {
if (mainContentTab === "info") {
setMainContentTab("chat");
} else {
setMainContentTab("info");
}
}}
>
<div className="avatar">
<div className="w-10 h-10 rounded-xl overflow-hidden bg-emerald-950 ring-1 ring-emerald-700/40">
{roomImage ? (
<img
src={roomImage}
alt={roomName}
className="object-cover w-full h-full"
/>
) : (
<div className="flex items-center justify-center h-full w-full text-emerald-400 font-semibold">
{roomName.substring(0, 2).toUpperCase()}
</div>
)}
</div>
</div>
<h2 className="font-bold text-lg truncate text-emerald-100">
{roomName}
</h2>
</div>

<div className="flex items-center gap-2">
<div className="inline-flex items-center gap-1 p-1 bg-neutral-800 rounded-xl border border-emerald-800/40 shadow-inner">
{tabs.map((tab) => (
<button
key={tab.key}
type="button"
className={`inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
mainContentTab === tab.key
? "bg-emerald-600 text-white shadow-md"
: "text-emerald-300 hover:text-white hover:bg-emerald-800/40"
}`}
onClick={() =>
setMainContentTab(
tab.key as
| "chat"
| "info"
| "whiteboard"
| "resourceHub"
| "settings"
)
}
>
{tab.icon}
<span>{tab.label}</span>
</button>
))}
</div>
</div>
</div>
);
}
10 changes: 5 additions & 5 deletions client/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@ import React, { useEffect, useRef, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import MessageBubble from "./MessageBubble";
import type { RootState } from "@/redux/store";
import { useSocketMessages } from "@/hooks/useSocketMessages";
import { getMessagesOfRoom } from "@/api/message";
import type { Message } from "@/config/schema/Message";
import { setInitialMessages } from "@/redux/slices/roomSlice";
import ChatInput from "./ChatInput";

const EMPTY_MESSAGES: Message[] = [];

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

useSocketMessages();

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

Expand All @@ -38,7 +39,6 @@ const ChatPanel: React.FC = () => {
})
);
} catch (err) {
console.error("Failed to fetch initial messages:", err);
setError("Failed to load messages.");
} finally {
setIsLoading(false);
Expand Down
Loading