Commit
·
6fb9a09
1
Parent(s):
55109cc
update
Browse files- frontend/app/Sidebar.js +49 -12
- frontend/app/layout.js +85 -34
- frontend/app/{[recipientusername] → u/[recipientusername]}/page.js +151 -90
- frontend/next.config.mjs +19 -1
frontend/app/Sidebar.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
-
import { useState } from 'react';
|
2 |
-
import
|
3 |
|
4 |
export default function Sidebar({
|
5 |
searchQuery,
|
@@ -9,6 +9,15 @@ export default function Sidebar({
|
|
9 |
setFilteredUsers,
|
10 |
}) {
|
11 |
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
const toggleSidebar = () => {
|
14 |
setIsSidebarOpen((prev) => !prev);
|
@@ -47,14 +56,15 @@ export default function Sidebar({
|
|
47 |
position: 'fixed',
|
48 |
top: '10px',
|
49 |
left: isSidebarOpen ? '210px' : '10px',
|
|
|
50 |
zIndex: 1000,
|
51 |
-
backgroundColor: '#
|
52 |
color: '#fff',
|
53 |
border: 'none',
|
54 |
-
borderRadius: '
|
55 |
padding: '10px',
|
56 |
cursor: 'pointer',
|
57 |
-
transition: 'left
|
58 |
}}
|
59 |
>
|
60 |
☰
|
@@ -65,7 +75,7 @@ export default function Sidebar({
|
|
65 |
style={{
|
66 |
position: 'fixed',
|
67 |
top: 0,
|
68 |
-
left: isSidebarOpen ? 0 : '-260px',
|
69 |
width: '250px',
|
70 |
height: '100%',
|
71 |
backgroundColor: '#333',
|
@@ -75,7 +85,7 @@ export default function Sidebar({
|
|
75 |
zIndex: 999,
|
76 |
}}
|
77 |
>
|
78 |
-
<h3 style={{ color: '#fff', marginBottom: '20px' }}>
|
79 |
|
80 |
<input
|
81 |
type="text"
|
@@ -87,12 +97,39 @@ export default function Sidebar({
|
|
87 |
|
88 |
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
89 |
{filteredUsers.map((recipient, index) => (
|
90 |
-
<li
|
91 |
-
|
92 |
-
|
93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
94 |
>
|
95 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
</span>
|
97 |
</li>
|
98 |
))}
|
|
|
1 |
+
import { useState, useEffect } from 'react';
|
2 |
+
import Image from 'next/image';
|
3 |
|
4 |
export default function Sidebar({
|
5 |
searchQuery,
|
|
|
9 |
setFilteredUsers,
|
10 |
}) {
|
11 |
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
12 |
+
const [me, setMe] = useState('');
|
13 |
+
|
14 |
+
useEffect(() => {
|
15 |
+
// Retrieve 'me' from localStorage
|
16 |
+
const storedMe = localStorage.getItem('me');
|
17 |
+
if (storedMe) {
|
18 |
+
setMe(storedMe);
|
19 |
+
}
|
20 |
+
}, []);
|
21 |
|
22 |
const toggleSidebar = () => {
|
23 |
setIsSidebarOpen((prev) => !prev);
|
|
|
56 |
position: 'fixed',
|
57 |
top: '10px',
|
58 |
left: isSidebarOpen ? '210px' : '10px',
|
59 |
+
transform: isSidebarOpen ? 'rotate(90deg)' : 'none',
|
60 |
zIndex: 1000,
|
61 |
+
backgroundColor: '#352e83',
|
62 |
color: '#fff',
|
63 |
border: 'none',
|
64 |
+
borderRadius: '10px',
|
65 |
padding: '10px',
|
66 |
cursor: 'pointer',
|
67 |
+
transition: 'left .4s ease',
|
68 |
}}
|
69 |
>
|
70 |
☰
|
|
|
75 |
style={{
|
76 |
position: 'fixed',
|
77 |
top: 0,
|
78 |
+
left: isSidebarOpen ? 0 : '-260px',
|
79 |
width: '250px',
|
80 |
height: '100%',
|
81 |
backgroundColor: '#333',
|
|
|
85 |
zIndex: 999,
|
86 |
}}
|
87 |
>
|
88 |
+
<h3 style={{ color: '#fff', marginBottom: '20px' }}>Find Chats</h3>
|
89 |
|
90 |
<input
|
91 |
type="text"
|
|
|
97 |
|
98 |
<ul style={{ listStyleType: 'none', padding: 0 }}>
|
99 |
{filteredUsers.map((recipient, index) => (
|
100 |
+
<li
|
101 |
+
key={index}
|
102 |
+
style={{
|
103 |
+
color: '#fff',
|
104 |
+
cursor: 'pointer',
|
105 |
+
marginBottom: '15px',
|
106 |
+
display: 'flex',
|
107 |
+
alignItems: 'center',
|
108 |
+
}}
|
109 |
+
onClick={() => handleUserSelect(recipient)}
|
110 |
+
>
|
111 |
+
{/* Profile Picture */}
|
112 |
+
<div
|
113 |
+
style={{
|
114 |
+
width: '40px',
|
115 |
+
height: '40px',
|
116 |
+
position: 'relative',
|
117 |
+
marginRight: '10px',
|
118 |
+
}}
|
119 |
>
|
120 |
+
<Image
|
121 |
+
src={`https://ui-avatars.com/api/?background=random&name=${encodeURIComponent(
|
122 |
+
recipient
|
123 |
+
)}`}
|
124 |
+
alt={`${recipient}'s avatar`}
|
125 |
+
layout="fill"
|
126 |
+
objectFit="cover"
|
127 |
+
style={{ borderRadius: '50%' }}
|
128 |
+
/>
|
129 |
+
</div>
|
130 |
+
{/* Username */}
|
131 |
+
<span style={{ textDecoration: 'none', color: '#fff', fontSize: '16px' }}>
|
132 |
+
{recipient} {recipient === me ? '(Me)' : ''}
|
133 |
</span>
|
134 |
</li>
|
135 |
))}
|
frontend/app/layout.js
CHANGED
@@ -1,18 +1,21 @@
|
|
1 |
'use client';
|
2 |
import { useState, useEffect } from 'react';
|
3 |
-
import { useRouter } from 'next/navigation';
|
4 |
import { WebSocketProvider, useWebSocket } from './WebSocketContext';
|
5 |
import './globals.css';
|
6 |
-
import Sidebar from './Sidebar';
|
7 |
|
8 |
export default function RootLayout({ children }) {
|
9 |
const [username, setUsername] = useState('');
|
10 |
const [password, setPassword] = useState('');
|
|
|
11 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
12 |
const [recipientList, setRecipientList] = useState([]);
|
13 |
const [searchQuery, setSearchQuery] = useState('');
|
14 |
const [filteredUsers, setFilteredUsers] = useState([]);
|
15 |
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
|
|
16 |
|
17 |
return (
|
18 |
<html lang="en">
|
@@ -23,8 +26,12 @@ export default function RootLayout({ children }) {
|
|
23 |
setUsername={setUsername}
|
24 |
password={password}
|
25 |
setPassword={setPassword}
|
|
|
|
|
26 |
isLoading={isLoading}
|
27 |
setIsLoading={setIsLoading}
|
|
|
|
|
28 |
recipientList={recipientList}
|
29 |
setRecipientList={setRecipientList}
|
30 |
searchQuery={searchQuery}
|
@@ -33,6 +40,8 @@ export default function RootLayout({ children }) {
|
|
33 |
setFilteredUsers={setFilteredUsers}
|
34 |
isLoggedIn={isLoggedIn}
|
35 |
setIsLoggedIn={setIsLoggedIn}
|
|
|
|
|
36 |
>
|
37 |
{children}
|
38 |
</WebSocketLayout>
|
@@ -47,8 +56,12 @@ function WebSocketLayout({
|
|
47 |
setUsername,
|
48 |
password,
|
49 |
setPassword,
|
|
|
|
|
50 |
isLoading,
|
51 |
setIsLoading,
|
|
|
|
|
52 |
recipientList,
|
53 |
setRecipientList,
|
54 |
searchQuery,
|
@@ -57,22 +70,48 @@ function WebSocketLayout({
|
|
57 |
setFilteredUsers,
|
58 |
isLoggedIn,
|
59 |
setIsLoggedIn,
|
|
|
|
|
60 |
children,
|
61 |
}) {
|
62 |
-
const { ws, status } = useWebSocket();
|
63 |
const router = useRouter();
|
64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
const connectAndAuthenticate = () => {
|
|
|
66 |
if (!username || !password) {
|
67 |
-
|
68 |
return;
|
69 |
}
|
70 |
|
71 |
setIsLoading(true);
|
72 |
|
73 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
74 |
if (ws && ws.readyState === WebSocket.OPEN) {
|
75 |
-
ws.send(
|
76 |
}
|
77 |
};
|
78 |
|
@@ -80,38 +119,23 @@ function WebSocketLayout({
|
|
80 |
if (ws) {
|
81 |
ws.onmessage = (event) => {
|
82 |
const data = JSON.parse(event.data);
|
83 |
-
console.log('Received message:', data);
|
84 |
|
85 |
if (data.status === 'success') {
|
86 |
setIsLoading(false);
|
87 |
setIsLoggedIn(true);
|
88 |
setRecipientList(data.recipients || []);
|
|
|
89 |
} else if (data.status === 'error') {
|
90 |
-
|
91 |
-
alert(data.message);
|
92 |
setIsLoading(false);
|
93 |
-
} else {
|
94 |
-
const { from: recipient, message } = data;
|
95 |
-
|
96 |
-
if (recipient && message) {
|
97 |
-
try {
|
98 |
-
const storedMessages = JSON.parse(localStorage.getItem(recipient)) || [];
|
99 |
-
storedMessages.push({ message, timestamp: data.timestamp });
|
100 |
-
localStorage.setItem(recipient, JSON.stringify(storedMessages));
|
101 |
-
console.log('LocalStorage after saving:', localStorage.getItem(recipient));
|
102 |
-
} catch (error) {
|
103 |
-
console.error('Error accessing localStorage:', error);
|
104 |
-
}
|
105 |
-
} else {
|
106 |
-
console.error('Received message is missing recipient or message.');
|
107 |
-
}
|
108 |
}
|
109 |
};
|
110 |
}
|
111 |
-
}, [ws]);
|
112 |
|
113 |
const handleUserSelect = (recipient) => {
|
114 |
-
router.push(
|
115 |
};
|
116 |
|
117 |
return (
|
@@ -125,14 +149,12 @@ function WebSocketLayout({
|
|
125 |
filteredUsers={filteredUsers}
|
126 |
handleUserSelect={handleUserSelect}
|
127 |
/>
|
128 |
-
<div style={{ flex: 1
|
129 |
-
{children}
|
130 |
-
</div>
|
131 |
</div>
|
132 |
) : (
|
133 |
<div style={loginContainerStyle}>
|
134 |
-
<h2 style={headingStyle}>Login</h2>
|
135 |
<div style={inputContainerStyle}>
|
|
|
136 |
<input
|
137 |
type="text"
|
138 |
placeholder="Username"
|
@@ -147,11 +169,31 @@ function WebSocketLayout({
|
|
147 |
onChange={(e) => setPassword(e.target.value)}
|
148 |
style={inputStyle}
|
149 |
/>
|
150 |
-
|
151 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
152 |
</button>
|
|
|
|
|
153 |
</div>
|
154 |
-
|
155 |
</div>
|
156 |
)}
|
157 |
</div>
|
@@ -163,7 +205,7 @@ const loginContainerStyle = {
|
|
163 |
justifyContent: 'center',
|
164 |
alignItems: 'center',
|
165 |
height: '100vh',
|
166 |
-
backgroundColor: '#
|
167 |
};
|
168 |
|
169 |
const headingStyle = {
|
@@ -199,3 +241,12 @@ const loginButtonStyle = {
|
|
199 |
cursor: 'pointer',
|
200 |
transition: 'background-color 0.3s',
|
201 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
'use client';
|
2 |
import { useState, useEffect } from 'react';
|
3 |
+
import { useRouter } from 'next/navigation';
|
4 |
import { WebSocketProvider, useWebSocket } from './WebSocketContext';
|
5 |
import './globals.css';
|
6 |
+
import Sidebar from './Sidebar';
|
7 |
|
8 |
export default function RootLayout({ children }) {
|
9 |
const [username, setUsername] = useState('');
|
10 |
const [password, setPassword] = useState('');
|
11 |
+
const [confirmPassword, setConfirmPassword] = useState('');
|
12 |
const [isLoading, setIsLoading] = useState(false);
|
13 |
+
const [error, setError] = useState('');
|
14 |
const [recipientList, setRecipientList] = useState([]);
|
15 |
const [searchQuery, setSearchQuery] = useState('');
|
16 |
const [filteredUsers, setFilteredUsers] = useState([]);
|
17 |
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
18 |
+
const [isSignup, setIsSignup] = useState(false);
|
19 |
|
20 |
return (
|
21 |
<html lang="en">
|
|
|
26 |
setUsername={setUsername}
|
27 |
password={password}
|
28 |
setPassword={setPassword}
|
29 |
+
confirmPassword={confirmPassword}
|
30 |
+
setConfirmPassword={setConfirmPassword}
|
31 |
isLoading={isLoading}
|
32 |
setIsLoading={setIsLoading}
|
33 |
+
error={error}
|
34 |
+
setError={setError}
|
35 |
recipientList={recipientList}
|
36 |
setRecipientList={setRecipientList}
|
37 |
searchQuery={searchQuery}
|
|
|
40 |
setFilteredUsers={setFilteredUsers}
|
41 |
isLoggedIn={isLoggedIn}
|
42 |
setIsLoggedIn={setIsLoggedIn}
|
43 |
+
isSignup={isSignup}
|
44 |
+
setIsSignup={setIsSignup}
|
45 |
>
|
46 |
{children}
|
47 |
</WebSocketLayout>
|
|
|
56 |
setUsername,
|
57 |
password,
|
58 |
setPassword,
|
59 |
+
confirmPassword,
|
60 |
+
setConfirmPassword,
|
61 |
isLoading,
|
62 |
setIsLoading,
|
63 |
+
error,
|
64 |
+
setError,
|
65 |
recipientList,
|
66 |
setRecipientList,
|
67 |
searchQuery,
|
|
|
70 |
setFilteredUsers,
|
71 |
isLoggedIn,
|
72 |
setIsLoggedIn,
|
73 |
+
isSignup,
|
74 |
+
setIsSignup,
|
75 |
children,
|
76 |
}) {
|
77 |
+
const { ws, status } = useWebSocket();
|
78 |
const router = useRouter();
|
79 |
|
80 |
+
const validateSignup = () => {
|
81 |
+
if (!username || !password || !confirmPassword) {
|
82 |
+
setError('Please fill in all fields');
|
83 |
+
return false;
|
84 |
+
}
|
85 |
+
if (password !== confirmPassword) {
|
86 |
+
setError('Passwords do not match');
|
87 |
+
return false;
|
88 |
+
}
|
89 |
+
return true;
|
90 |
+
};
|
91 |
+
|
92 |
const connectAndAuthenticate = () => {
|
93 |
+
setError('');
|
94 |
if (!username || !password) {
|
95 |
+
setError('Please enter both username and password');
|
96 |
return;
|
97 |
}
|
98 |
|
99 |
setIsLoading(true);
|
100 |
|
101 |
+
const requestData = JSON.stringify({ username, password });
|
102 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
103 |
+
ws.send(requestData);
|
104 |
+
}
|
105 |
+
};
|
106 |
+
|
107 |
+
const handleSignup = () => {
|
108 |
+
if (!validateSignup()) return;
|
109 |
+
|
110 |
+
setIsLoading(true);
|
111 |
+
|
112 |
+
const signupData = JSON.stringify({ username, password, action: 'signup' });
|
113 |
if (ws && ws.readyState === WebSocket.OPEN) {
|
114 |
+
ws.send(signupData);
|
115 |
}
|
116 |
};
|
117 |
|
|
|
119 |
if (ws) {
|
120 |
ws.onmessage = (event) => {
|
121 |
const data = JSON.parse(event.data);
|
122 |
+
console.log('Received message:', data);
|
123 |
|
124 |
if (data.status === 'success') {
|
125 |
setIsLoading(false);
|
126 |
setIsLoggedIn(true);
|
127 |
setRecipientList(data.recipients || []);
|
128 |
+
localStorage.setItem('me', username);
|
129 |
} else if (data.status === 'error') {
|
130 |
+
setError(data.message);
|
|
|
131 |
setIsLoading(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
}
|
133 |
};
|
134 |
}
|
135 |
+
}, [ws, username]);
|
136 |
|
137 |
const handleUserSelect = (recipient) => {
|
138 |
+
router.push(`/u/${recipient}`);
|
139 |
};
|
140 |
|
141 |
return (
|
|
|
149 |
filteredUsers={filteredUsers}
|
150 |
handleUserSelect={handleUserSelect}
|
151 |
/>
|
152 |
+
<div style={{ flex: 1 }}>{children}</div>
|
|
|
|
|
153 |
</div>
|
154 |
) : (
|
155 |
<div style={loginContainerStyle}>
|
|
|
156 |
<div style={inputContainerStyle}>
|
157 |
+
<h2 style={headingStyle}>{isSignup ? 'Signup' : 'Login'}</h2>
|
158 |
<input
|
159 |
type="text"
|
160 |
placeholder="Username"
|
|
|
169 |
onChange={(e) => setPassword(e.target.value)}
|
170 |
style={inputStyle}
|
171 |
/>
|
172 |
+
{isSignup && (
|
173 |
+
<input
|
174 |
+
type="password"
|
175 |
+
placeholder="Confirm Password"
|
176 |
+
value={confirmPassword}
|
177 |
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
178 |
+
style={inputStyle}
|
179 |
+
/>
|
180 |
+
)}
|
181 |
+
<button
|
182 |
+
onClick={isSignup ? handleSignup : connectAndAuthenticate}
|
183 |
+
style={loginButtonStyle}
|
184 |
+
>
|
185 |
+
{isLoading ? 'Processing...' : isSignup ? 'Signup' : 'Login'}
|
186 |
+
</button>
|
187 |
+
<button
|
188 |
+
onClick={() => setIsSignup(!isSignup)}
|
189 |
+
style={switchButtonStyle}
|
190 |
+
>
|
191 |
+
{isSignup ? 'Switch to Login' : 'Switch to Signup'}
|
192 |
</button>
|
193 |
+
{error && <div style={{ color: 'red', display: 'flex', alignItems: 'center'}}><div style={{ width: '8px', height: '8px', backgroundColor: 'red', borderRadius: '50px', marginRight: '5px' }}/> {error}</div>}
|
194 |
+
{status === 'Error' && <p style={{ color: 'red' }}>Connection failed. Please try again.</p>}
|
195 |
</div>
|
196 |
+
|
197 |
</div>
|
198 |
)}
|
199 |
</div>
|
|
|
205 |
justifyContent: 'center',
|
206 |
alignItems: 'center',
|
207 |
height: '100vh',
|
208 |
+
backgroundColor: '#1c1c37',
|
209 |
};
|
210 |
|
211 |
const headingStyle = {
|
|
|
241 |
cursor: 'pointer',
|
242 |
transition: 'background-color 0.3s',
|
243 |
};
|
244 |
+
|
245 |
+
const switchButtonStyle = {
|
246 |
+
marginTop: '10px',
|
247 |
+
color: '#fff',
|
248 |
+
background: 'none',
|
249 |
+
border: 'none',
|
250 |
+
cursor: 'pointer',
|
251 |
+
textDecoration: 'underline',
|
252 |
+
};
|
frontend/app/{[recipientusername] → u/[recipientusername]}/page.js
RENAMED
@@ -1,45 +1,52 @@
|
|
1 |
"use client";
|
2 |
import { useState, useEffect } from "react";
|
3 |
import { useParams } from "next/navigation";
|
4 |
-
import { useWebSocket } from "
|
|
|
5 |
|
6 |
export default function ChatPage() {
|
7 |
-
const { recipientusername } = useParams();
|
8 |
const [recipient, setRecipient] = useState(null);
|
9 |
const [message, setMessage] = useState("");
|
10 |
const [chatLog, setChatLog] = useState([]);
|
11 |
-
const { ws } = useWebSocket();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
// Update recipient state when recipientusername changes
|
14 |
useEffect(() => {
|
15 |
if (recipientusername) {
|
16 |
setRecipient(recipientusername);
|
17 |
}
|
18 |
}, [recipientusername]);
|
19 |
|
20 |
-
// Load chat history from localStorage when recipient changes
|
21 |
useEffect(() => {
|
22 |
if (recipient) {
|
23 |
const storedMessages = JSON.parse(localStorage.getItem(recipient)) || [];
|
24 |
-
setChatLog(storedMessages);
|
25 |
}
|
26 |
}, [recipient]);
|
27 |
|
28 |
-
// Re-fetch the chat log every 2 seconds
|
29 |
useEffect(() => {
|
30 |
const interval = setInterval(() => {
|
31 |
if (recipient) {
|
32 |
-
const updatedMessages =
|
33 |
-
|
|
|
34 |
}
|
35 |
-
}, 2000);
|
36 |
|
37 |
-
// Cleanup the interval when component unmounts
|
38 |
return () => clearInterval(interval);
|
39 |
}, [recipient]);
|
40 |
|
41 |
const handleSendMessage = () => {
|
42 |
-
// Check if the WebSocket is connected and the message is not empty
|
43 |
if (!ws) {
|
44 |
alert("WebSocket is not connected.");
|
45 |
return;
|
@@ -54,91 +61,168 @@ export default function ChatPage() {
|
|
54 |
const msgData = JSON.stringify({ recipient, message });
|
55 |
ws.send(msgData);
|
56 |
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
61 |
setMessage("");
|
62 |
|
63 |
-
|
64 |
-
const updatedChatLog = [
|
65 |
-
...chatLog,
|
66 |
-
{ from: "You", message, timestamp: new Date().toLocaleString() },
|
67 |
-
];
|
68 |
localStorage.setItem(recipient, JSON.stringify(updatedChatLog));
|
69 |
}
|
70 |
};
|
71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
return (
|
73 |
<div
|
74 |
style={{
|
75 |
backgroundColor: "#1a1a2e",
|
76 |
minHeight: "100vh",
|
|
|
|
|
77 |
fontFamily: "Segoe UI, Tahoma, Geneva, Verdana, sans-serif",
|
78 |
-
padding: "20px",
|
79 |
color: "#fff",
|
80 |
}}
|
81 |
>
|
82 |
{recipient ? (
|
83 |
<>
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
style={{
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
<div
|
113 |
-
key={index}
|
114 |
style={{
|
115 |
-
backgroundColor:
|
|
|
116 |
color: "#fff",
|
117 |
padding: "10px",
|
118 |
-
marginBottom: "10px",
|
119 |
borderRadius: "10px",
|
120 |
-
maxWidth: "
|
121 |
-
marginLeft: entry.from === "You" ? "auto" : "0",
|
122 |
wordWrap: "break-word",
|
123 |
}}
|
124 |
>
|
125 |
-
<p
|
126 |
-
style={{
|
127 |
-
margin: 0,
|
128 |
-
fontWeight: "bold",
|
129 |
-
fontSize: "14px",
|
130 |
-
color: "#e0b0ff",
|
131 |
-
}}
|
132 |
-
>
|
133 |
-
{entry.from}
|
134 |
-
</p>
|
135 |
<p style={{ margin: "5px 0" }}>{entry.message}</p>
|
136 |
<small style={{ fontSize: "12px", color: "#a1a1c7" }}>
|
137 |
{entry.timestamp}
|
138 |
</small>
|
139 |
</div>
|
140 |
-
|
141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
142 |
</div>
|
143 |
</>
|
144 |
) : (
|
@@ -147,26 +231,3 @@ export default function ChatPage() {
|
|
147 |
</div>
|
148 |
);
|
149 |
}
|
150 |
-
|
151 |
-
const inputStyle = {
|
152 |
-
width: "100%",
|
153 |
-
padding: "10px",
|
154 |
-
margin: "5px 0",
|
155 |
-
borderRadius: "5px",
|
156 |
-
border: "1px solid #4b2e83",
|
157 |
-
fontSize: "16px",
|
158 |
-
backgroundColor: "#2e2e4f",
|
159 |
-
color: "#fff",
|
160 |
-
};
|
161 |
-
|
162 |
-
const sendButtonStyle = {
|
163 |
-
padding: "12px 20px",
|
164 |
-
backgroundColor: "#6a4c9c",
|
165 |
-
color: "#fff",
|
166 |
-
border: "none",
|
167 |
-
borderRadius: "5px",
|
168 |
-
fontSize: "16px",
|
169 |
-
cursor: "pointer",
|
170 |
-
marginLeft: "10px",
|
171 |
-
transition: "background-color 0.3s",
|
172 |
-
};
|
|
|
1 |
"use client";
|
2 |
import { useState, useEffect } from "react";
|
3 |
import { useParams } from "next/navigation";
|
4 |
+
import { useWebSocket } from "../../WebSocketContext";
|
5 |
+
import Image from "next/image";
|
6 |
|
7 |
export default function ChatPage() {
|
8 |
+
const { recipientusername } = useParams();
|
9 |
const [recipient, setRecipient] = useState(null);
|
10 |
const [message, setMessage] = useState("");
|
11 |
const [chatLog, setChatLog] = useState([]);
|
12 |
+
const { ws } = useWebSocket();
|
13 |
+
|
14 |
+
const [me, setMe] = useState("");
|
15 |
+
|
16 |
+
useEffect(() => {
|
17 |
+
// Retrieve 'me' from localStorage
|
18 |
+
const storedMe = localStorage.getItem("me");
|
19 |
+
if (storedMe) {
|
20 |
+
setMe(storedMe);
|
21 |
+
}
|
22 |
+
}, []);
|
23 |
|
|
|
24 |
useEffect(() => {
|
25 |
if (recipientusername) {
|
26 |
setRecipient(recipientusername);
|
27 |
}
|
28 |
}, [recipientusername]);
|
29 |
|
|
|
30 |
useEffect(() => {
|
31 |
if (recipient) {
|
32 |
const storedMessages = JSON.parse(localStorage.getItem(recipient)) || [];
|
33 |
+
setChatLog(formatMessages(storedMessages));
|
34 |
}
|
35 |
}, [recipient]);
|
36 |
|
|
|
37 |
useEffect(() => {
|
38 |
const interval = setInterval(() => {
|
39 |
if (recipient) {
|
40 |
+
const updatedMessages =
|
41 |
+
JSON.parse(localStorage.getItem(recipient)) || [];
|
42 |
+
setChatLog(formatMessages(updatedMessages));
|
43 |
}
|
44 |
+
}, 2000);
|
45 |
|
|
|
46 |
return () => clearInterval(interval);
|
47 |
}, [recipient]);
|
48 |
|
49 |
const handleSendMessage = () => {
|
|
|
50 |
if (!ws) {
|
51 |
alert("WebSocket is not connected.");
|
52 |
return;
|
|
|
61 |
const msgData = JSON.stringify({ recipient, message });
|
62 |
ws.send(msgData);
|
63 |
|
64 |
+
const newMessage = {
|
65 |
+
from: "You",
|
66 |
+
message,
|
67 |
+
timestamp: new Date().toISOString(), // Use ISO 8601 for consistency
|
68 |
+
};
|
69 |
+
|
70 |
+
setChatLog((prev) => [...prev, formatMessage(newMessage)]);
|
71 |
setMessage("");
|
72 |
|
73 |
+
const updatedChatLog = [...chatLog, newMessage];
|
|
|
|
|
|
|
|
|
74 |
localStorage.setItem(recipient, JSON.stringify(updatedChatLog));
|
75 |
}
|
76 |
};
|
77 |
|
78 |
+
const formatMessages = (messages) => {
|
79 |
+
return messages.map(formatMessage);
|
80 |
+
};
|
81 |
+
|
82 |
+
const formatMessage = (message) => {
|
83 |
+
const formattedTimestamp = new Date(message.timestamp).toLocaleString();
|
84 |
+
return { ...message, timestamp: formattedTimestamp };
|
85 |
+
};
|
86 |
+
|
87 |
return (
|
88 |
<div
|
89 |
style={{
|
90 |
backgroundColor: "#1a1a2e",
|
91 |
minHeight: "100vh",
|
92 |
+
display: "flex",
|
93 |
+
flexDirection: "column",
|
94 |
fontFamily: "Segoe UI, Tahoma, Geneva, Verdana, sans-serif",
|
|
|
95 |
color: "#fff",
|
96 |
}}
|
97 |
>
|
98 |
{recipient ? (
|
99 |
<>
|
100 |
+
{/* Chat Header */}
|
101 |
+
<div
|
102 |
+
style={{
|
103 |
+
backgroundColor: "#170f3f",
|
104 |
+
padding: "10px",
|
105 |
+
display: "flex",
|
106 |
+
alignItems: "center",
|
107 |
+
gap: "10px",
|
108 |
+
}}
|
109 |
+
>
|
110 |
+
<Image
|
111 |
+
src={`https://ui-avatars.com/api/?name=${encodeURIComponent(
|
112 |
+
recipient
|
113 |
+
)}&background=random`}
|
114 |
+
alt={`${recipient}'s avatar`}
|
115 |
+
width={40}
|
116 |
+
height={40}
|
117 |
+
style={{ borderRadius: "50%", marginLeft: "40px" }}
|
118 |
+
/>
|
119 |
+
<h1 style={{ margin: 0, fontSize: "20px" }}>
|
120 |
+
{recipient === me ? `${recipient} (Me)` : recipient}
|
121 |
+
</h1>
|
122 |
+
</div>
|
123 |
+
|
124 |
+
{/* Chat Log */}
|
125 |
+
<div
|
126 |
+
style={{
|
127 |
+
flex: 1,
|
128 |
+
overflowY: "auto",
|
129 |
+
padding: "20px",
|
130 |
+
backgroundColor: "#1c1c2f",
|
131 |
+
display: "flex",
|
132 |
+
flexDirection: "column",
|
133 |
+
gap: "10px",
|
134 |
+
borderTop: "1px solid #3d3d6b",
|
135 |
+
}}
|
136 |
+
>
|
137 |
+
{chatLog.map((entry, index) => (
|
138 |
+
<div
|
139 |
+
key={index}
|
140 |
+
style={{
|
141 |
+
display: "flex",
|
142 |
+
alignItems: "flex-end",
|
143 |
+
flexDirection: entry.from === "You" ? "row-reverse" : "row",
|
144 |
+
gap: "10px",
|
145 |
+
}}
|
146 |
+
>
|
147 |
+
{/* Avatar */}
|
148 |
+
<Image
|
149 |
+
src={
|
150 |
+
entry.from === "You"
|
151 |
+
? `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
152 |
+
me
|
153 |
+
)}&background=random`
|
154 |
+
: `https://ui-avatars.com/api/?name=${encodeURIComponent(
|
155 |
+
recipient
|
156 |
+
)}&background=random`
|
157 |
+
}
|
158 |
+
alt={`${entry.from}'s avatar`}
|
159 |
+
width={30}
|
160 |
+
height={30}
|
161 |
+
style={{ borderRadius: "50%" }}
|
162 |
+
/>
|
163 |
+
|
164 |
+
{/* Message */}
|
165 |
<div
|
|
|
166 |
style={{
|
167 |
+
backgroundColor:
|
168 |
+
entry.from === "You" ? "#3b2e83" : "#3d446b",
|
169 |
color: "#fff",
|
170 |
padding: "10px",
|
|
|
171 |
borderRadius: "10px",
|
172 |
+
maxWidth: "70%",
|
|
|
173 |
wordWrap: "break-word",
|
174 |
}}
|
175 |
>
|
176 |
+
<p style={{ margin: 0, fontWeight: "bold" }}>{entry.from}</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
<p style={{ margin: "5px 0" }}>{entry.message}</p>
|
178 |
<small style={{ fontSize: "12px", color: "#a1a1c7" }}>
|
179 |
{entry.timestamp}
|
180 |
</small>
|
181 |
</div>
|
182 |
+
</div>
|
183 |
+
))}
|
184 |
+
</div>
|
185 |
+
|
186 |
+
{/* Message Input */}
|
187 |
+
<div
|
188 |
+
style={{
|
189 |
+
backgroundColor: "#171927",
|
190 |
+
borderTop: "1px solid rgb(95, 38, 207)",
|
191 |
+
padding: "10px",
|
192 |
+
display: "flex",
|
193 |
+
gap: "10px",
|
194 |
+
}}
|
195 |
+
>
|
196 |
+
<input
|
197 |
+
type="text"
|
198 |
+
placeholder="Type your message..."
|
199 |
+
value={message}
|
200 |
+
onChange={(e) => setMessage(e.target.value)}
|
201 |
+
style={{
|
202 |
+
flex: 1,
|
203 |
+
padding: "10px",
|
204 |
+
borderRadius: "5px",
|
205 |
+
border: "1px solid #4b2e83",
|
206 |
+
fontSize: "16px",
|
207 |
+
backgroundColor: "#2e2e4f",
|
208 |
+
color: "#fff",
|
209 |
+
}}
|
210 |
+
/>
|
211 |
+
<button
|
212 |
+
onClick={handleSendMessage}
|
213 |
+
style={{
|
214 |
+
padding: "10px 20px",
|
215 |
+
backgroundColor: "#3b2e83",
|
216 |
+
color: "#fff",
|
217 |
+
border: "none",
|
218 |
+
borderRadius: "5px",
|
219 |
+
fontSize: "16px",
|
220 |
+
cursor: "pointer",
|
221 |
+
transition: "background-color 0.3s",
|
222 |
+
}}
|
223 |
+
>
|
224 |
+
Send
|
225 |
+
</button>
|
226 |
</div>
|
227 |
</>
|
228 |
) : (
|
|
|
231 |
</div>
|
232 |
);
|
233 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/next.config.mjs
CHANGED
@@ -1,4 +1,22 @@
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
-
const nextConfig = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
|
4 |
export default nextConfig;
|
|
|
1 |
/** @type {import('next').NextConfig} */
|
2 |
+
const nextConfig = {
|
3 |
+
images: {
|
4 |
+
dangerouslyAllowSVG: true,
|
5 |
+
remotePatterns: [
|
6 |
+
{
|
7 |
+
protocol: 'https',
|
8 |
+
hostname: 'via.placeholder.com',
|
9 |
+
port: '',
|
10 |
+
pathname: '/**',
|
11 |
+
},
|
12 |
+
{
|
13 |
+
protocol: 'https',
|
14 |
+
hostname: 'ui-avatars.com',
|
15 |
+
port: '',
|
16 |
+
pathname: '/api/**',
|
17 |
+
},
|
18 |
+
],
|
19 |
+
},
|
20 |
+
};
|
21 |
|
22 |
export default nextConfig;
|