Skip to content

Commit d69f8fd

Browse files
committed
feat: added chat feature
1 parent 59574e8 commit d69f8fd

File tree

14 files changed

+403
-21
lines changed

14 files changed

+403
-21
lines changed

client/package-lock.json

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@radix-ui/react-select": "^2.1.1",
2525
"@radix-ui/react-separator": "^1.1.0",
2626
"@radix-ui/react-slot": "^1.1.0",
27+
"@radix-ui/react-tabs": "^1.1.1",
2728
"@radix-ui/react-toast": "^1.2.1",
2829
"@radix-ui/react-tooltip": "^1.1.3",
2930
"@reduxjs/toolkit": "^2.2.7",
@@ -46,6 +47,7 @@
4647
"react-icons": "^5.3.0",
4748
"react-markdown": "^9.0.1",
4849
"react-redux": "^9.1.2",
50+
"react-resizable-panels": "^2.1.4",
4951
"react-router-dom": "^6.25.1",
5052
"react-wrap-balancer": "^1.1.1",
5153
"redux": "^5.0.1",

client/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Home from './pages/Home';
77
import Profile from './pages/Profile';
88
import { Toaster } from "@/components/ui/sonner";
99
import EditProfileForm from './pages/EditProfileForm';
10+
import { MessagePage } from './components/Messages/MessagePage';
1011

1112
const App = () => {
1213

@@ -19,6 +20,7 @@ const App = () => {
1920
<Route path="/login" element={<Login />} />
2021
<Route path="/home" element={<Home />} />
2122
<Route path="/settings" element={<EditProfileForm />} />
23+
<Route path="/message" element={<MessagePage/>} />
2224
<Route path="/u/:username" element={<Profile />} />
2325
<Route path="*" element={<div>404</div>} />
2426
</Routes>
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { useEffect,useState } from "react"
2+
import {
3+
Search,
4+
} from "lucide-react"
5+
import { Input } from "@/components/ui/input"
6+
import {
7+
ResizableHandle,
8+
ResizablePanel,
9+
ResizablePanelGroup,
10+
} from "@/components/ui/resizable"
11+
import { TooltipProvider } from "@/components/ui/tooltip"
12+
import axios from "axios"
13+
14+
const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5000';
15+
16+
interface User {
17+
username: string;
18+
}
19+
20+
interface ChatMessage {
21+
sender_username: string;
22+
message: string;
23+
}
24+
25+
export function Message() {
26+
const [currentUserId, setCurrentUserId] = useState<string>("");
27+
const [searchTerm, setSearchTerm] = useState<string>("");
28+
const [users, setUsers] = useState<User[]>([]);
29+
const [selectedUser, setSelectedUser] = useState<User | null>(null);
30+
const [message, setMessage] = useState<string>("");
31+
const [chattedUsers, setChattedUsers] = useState<User[]>([]);
32+
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
33+
34+
useEffect(() => {
35+
const username = localStorage.getItem('devhub_username') || "";
36+
setCurrentUserId(username);
37+
}, []);
38+
39+
const handleSearch = async () => {
40+
if (searchTerm) {
41+
try {
42+
const response = await axios.get(`${backendUrl}/search_users?username=${searchTerm}`);
43+
setUsers(response.data);
44+
} catch (error) {
45+
console.error("Error searching users:", error);
46+
}
47+
} else {
48+
setUsers([]); // Clear users if search term is empty
49+
}
50+
}
51+
52+
const handleSendMessage = async () => {
53+
if (selectedUser && message.trim() !== "") {
54+
try {
55+
await axios.post(`${backendUrl}/send_message`, {
56+
sender_username: currentUserId,
57+
receiver_username: selectedUser.username,
58+
message: message
59+
});
60+
setMessage("");
61+
fetchChatMessages();
62+
63+
if (!chattedUsers.some(user => user.username === selectedUser.username)) {
64+
setChattedUsers([...chattedUsers, selectedUser]);
65+
}
66+
} catch (error) {
67+
console.error("Error sending message:", error);
68+
}
69+
}
70+
}
71+
72+
const fetchChatMessages = async () => {
73+
if (selectedUser) {
74+
try {
75+
const response = await axios.get(`${backendUrl}/get_messages/${selectedUser.username}`);
76+
setChatMessages(response.data);
77+
} catch (error) {
78+
console.error("Error fetching messages:", error);
79+
}
80+
}
81+
}
82+
83+
useEffect(() => {
84+
handleSearch();
85+
}, [searchTerm]);
86+
87+
const handleUserSelect = (user: User) => {
88+
setSelectedUser(user);
89+
setUsers([]);
90+
setChatMessages([]);
91+
fetchChatMessages();
92+
}
93+
94+
return (
95+
<TooltipProvider delayDuration={0}>
96+
<ResizablePanelGroup
97+
direction="horizontal"
98+
onLayout={(sizes: number[]) => {
99+
document.cookie = `react-resizable-panels:layout:mail=${JSON.stringify(sizes)}`;
100+
}}
101+
className="h-full items-stretch"
102+
>
103+
<ResizablePanel minSize={30} className="flex flex-col">
104+
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
105+
<form onSubmit={(e) => { e.preventDefault(); handleSearch(); }}>
106+
<div className="relative">
107+
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
108+
<Input
109+
placeholder="Search users"
110+
className="pl-8"
111+
value={searchTerm}
112+
onChange={(e) => setSearchTerm(e.target.value)}
113+
/>
114+
</div>
115+
</form>
116+
<ul className="mt-2">
117+
{users.map(user => (
118+
<li key={user.username} onClick={() => handleUserSelect(user)}>
119+
{user.username}
120+
</li>
121+
))}
122+
</ul>
123+
</div>
124+
<div className="flex-grow overflow-auto">
125+
<h3>Chatted Users</h3>
126+
<ul>
127+
{chattedUsers.map(user => (
128+
<li key={user.username} onClick={() => handleUserSelect(user)}>
129+
{user.username}
130+
</li>
131+
))}
132+
</ul>
133+
</div>
134+
</ResizablePanel>
135+
<ResizableHandle withHandle />
136+
<ResizablePanel minSize={30} className="flex flex-col">
137+
<div className="flex-grow overflow-auto">
138+
{selectedUser && (
139+
<div>
140+
<h3>Chat with {selectedUser.username}</h3>
141+
<div>
142+
{chatMessages.map((msg, index) => (
143+
<div key={index}>
144+
<strong>{msg.sender_username === currentUserId ? "You" : selectedUser.username}:</strong> {msg.message}
145+
</div>
146+
))}
147+
</div>
148+
<input
149+
type="text"
150+
value={message}
151+
onChange={(e) => setMessage(e.target.value)}
152+
placeholder="Type a message"
153+
/>
154+
<button onClick={handleSendMessage}>Send</button>
155+
</div>
156+
)}
157+
</div>
158+
</ResizablePanel>
159+
</ResizablePanelGroup>
160+
</TooltipProvider>
161+
)
162+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import {
2+
SidebarInset,
3+
SidebarProvider,
4+
SidebarTrigger,
5+
} from "@/components/ui/sidebar"
6+
import { SidebarLeft } from '@/components/Sidebar/Sidebar'
7+
import { Message } from "./Message";
8+
9+
export const MessagePage: React.FC = () => {
10+
11+
return (
12+
<SidebarProvider>
13+
<SidebarLeft />
14+
<SidebarInset>
15+
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 bg-background">
16+
<div className="flex flex-1 items-center gap-2 px-3">
17+
<SidebarTrigger />
18+
</div>
19+
</header>
20+
<main className="flex flex-col flex-grow p-4 overflow-hidden">
21+
<div className=" h-full">
22+
<Message />
23+
</div>
24+
</main>
25+
</SidebarInset>
26+
</SidebarProvider>
27+
)
28+
}

client/src/components/Sidebar/Sidebar.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,10 @@ export default function DevhubSidebar() {
5252
export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>) {
5353
const navigate = useNavigate();
5454

55-
// Handle logout logic
5655
const handleLogout = (e: React.MouseEvent<HTMLAnchorElement>) => {
57-
e.preventDefault(); // Prevent the default anchor behavior
56+
e.preventDefault();
5857
localStorage.removeItem('devhub_username');
59-
navigate('/login'); // Redirect to the login page
58+
navigate('/login');
6059
};
6160

6261
const sidebarLeftData = {
@@ -67,10 +66,9 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
6766
icon: Sparkles,
6867
},
6968
{
70-
title: "Inbox",
71-
url: "#",
69+
title: "Message",
70+
url: "/message",
7271
icon: Inbox,
73-
badge: "10",
7472
},
7573
],
7674
navSecondary: [
@@ -96,9 +94,9 @@ export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>)
9694
},
9795
{
9896
title: "Logout",
99-
url: "#", // Keep the url as # for now
97+
url: "#",
10098
icon: LogOut,
101-
onClick: handleLogout, // Call the logout function on click
99+
onClick: handleLogout,
102100
}
103101
]
104102
};
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { GripVertical } from "lucide-react"
2+
import * as ResizablePrimitive from "react-resizable-panels"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const ResizablePanelGroup = ({
7+
className,
8+
...props
9+
}: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) => (
10+
<ResizablePrimitive.PanelGroup
11+
className={cn(
12+
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
13+
className
14+
)}
15+
{...props}
16+
/>
17+
)
18+
19+
const ResizablePanel = ResizablePrimitive.Panel
20+
21+
const ResizableHandle = ({
22+
withHandle,
23+
className,
24+
...props
25+
}: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
26+
withHandle?: boolean
27+
}) => (
28+
<ResizablePrimitive.PanelResizeHandle
29+
className={cn(
30+
"relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
31+
className
32+
)}
33+
{...props}
34+
>
35+
{withHandle && (
36+
<div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
37+
<GripVertical className="h-2.5 w-2.5" />
38+
</div>
39+
)}
40+
</ResizablePrimitive.PanelResizeHandle>
41+
)
42+
43+
export { ResizablePanelGroup, ResizablePanel, ResizableHandle }

0 commit comments

Comments
 (0)