Skip to content

Commit 137b8a7

Browse files
Ani GasitashviliAni Gasitashvili
authored andcommitted
feat: Implement friends module with request/accept functionality
1 parent 5600ed6 commit 137b8a7

File tree

19 files changed

+715
-41
lines changed

19 files changed

+715
-41
lines changed

.idea/dataSources.xml

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

quiz-frontend/quiz-frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "quiz-frontend",
33
"version": "0.1.0",
44
"private": true,
5+
"proxy": "http://localhost:8081",
56
"dependencies": {
67
"@testing-library/dom": "^10.4.0",
78
"@testing-library/jest-dom": "^6.6.3",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { useState } from 'react';
2+
import axios from 'axios';
3+
4+
const AddFriend = () => {
5+
const [friendUsername, setFriendUsername] = useState('');
6+
const [message, setMessage] = useState('');
7+
const [error, setError] = useState('');
8+
9+
const handleSubmit = async (e) => {
10+
e.preventDefault();
11+
setMessage('');
12+
setError('');
13+
14+
try {
15+
const response = await axios.post('/api/friends/add', { username: friendUsername }, { withCredentials: true });
16+
setMessage(response.data.message);
17+
setFriendUsername(''); // Clear input after success
18+
} catch (err) {
19+
setError(err.response?.data?.error || 'Failed to add friend.');
20+
}
21+
};
22+
23+
return (
24+
<div className="add-friend-container">
25+
<h3>Add a Friend</h3>
26+
<form onSubmit={handleSubmit}>
27+
<input
28+
type="text"
29+
placeholder="Enter friend's username"
30+
value={friendUsername}
31+
onChange={(e) => setFriendUsername(e.target.value)}
32+
required
33+
/>
34+
<button type="submit">Add Friend</button>
35+
</form>
36+
{message && <p className="success-message">{message}</p>}
37+
{error && <p className="error-message">{error}</p>}
38+
</div>
39+
);
40+
};
41+
42+
export default AddFriend;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useState, useEffect } from 'react';
2+
import axios from 'axios';
3+
4+
const FriendList = () => {
5+
const [friends, setFriends] = useState([]);
6+
const [error, setError] = useState('');
7+
8+
const fetchFriends = async () => {
9+
try {
10+
const response = await axios.get('/api/friends/list', { withCredentials: true });
11+
setFriends(response.data);
12+
} catch (err) {
13+
setError('Could not fetch friends.');
14+
}
15+
};
16+
17+
useEffect(() => {
18+
fetchFriends();
19+
}, []);
20+
21+
return (
22+
<div className="friend-list-container">
23+
<h3>Your Friends</h3>
24+
{error && <p className="error-message">{error}</p>}
25+
{friends.length > 0 ? (
26+
<ul className="friends-list">
27+
{friends.map((friend, index) => (
28+
<li key={index} className="friend-item">
29+
<span>{friend}</span>
30+
</li>
31+
))}
32+
</ul>
33+
) : (
34+
<p>You haven't added any friends yet.</p>
35+
)}
36+
</div>
37+
);
38+
};
39+
40+
export default FriendList;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React, { useState, useEffect } from 'react';
2+
import axios from 'axios';
3+
4+
const FriendRequests = ({ onRequestsCountChange }) => {
5+
const [requests, setRequests] = useState([]);
6+
const [message, setMessage] = useState('');
7+
8+
const fetchRequests = async () => {
9+
try {
10+
const response = await axios.get('/api/friends/requests', { withCredentials: true });
11+
setRequests(response.data);
12+
onRequestsCountChange(response.data.length);
13+
} catch (err) {
14+
setMessage('Could not fetch friend requests.');
15+
}
16+
};
17+
18+
useEffect(() => {
19+
fetchRequests();
20+
}, []);
21+
22+
const handleAccept = async (requestId) => {
23+
try {
24+
await axios.post(`/api/friends/accept/${requestId}`, {}, { withCredentials: true });
25+
fetchRequests(); // Re-fetch to update the list
26+
} catch (err) {
27+
setMessage('Failed to accept request.');
28+
}
29+
};
30+
31+
return (
32+
<div className="friend-requests-container">
33+
{message && <p>{message}</p>}
34+
{requests.length > 0 ? (
35+
<ul className="friend-requests-list">
36+
{requests.map((request) => (
37+
<li key={request.id} className="request-item">
38+
<span>{request.requesterUsername}</span>
39+
<button onClick={() => handleAccept(request.id)} className="accept-request-button">
40+
Accept
41+
</button>
42+
</li>
43+
))}
44+
</ul>
45+
) : (
46+
<p>No new friend requests.</p>
47+
)}
48+
</div>
49+
);
50+
};
51+
52+
export default FriendRequests;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* Friends.css */
2+
3+
.friends-icon-button {
4+
position: fixed;
5+
bottom: 90px; /* Moved up */
6+
right: 20px;
7+
background-color: #6a1b9a; /* A deep purple */
8+
color: white;
9+
border: none;
10+
border-radius: 50%;
11+
width: 60px;
12+
height: 60px;
13+
font-size: 28px;
14+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
15+
cursor: pointer;
16+
display: flex;
17+
justify-content: center;
18+
align-items: center;
19+
transition: transform 0.2s ease-in-out;
20+
z-index: 1000;
21+
}
22+
23+
.friends-icon-button:hover {
24+
transform: scale(1.1);
25+
}
26+
27+
.friends-modal-overlay {
28+
position: fixed;
29+
top: 0;
30+
left: 0;
31+
right: 0;
32+
bottom: 0;
33+
background-color: rgba(0, 0, 0, 0.6);
34+
display: flex;
35+
justify-content: center;
36+
align-items: center;
37+
z-index: 1050;
38+
}
39+
40+
.friends-modal-content {
41+
background: white;
42+
padding: 25px;
43+
border-radius: 12px;
44+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
45+
width: 90%;
46+
max-width: 500px;
47+
max-height: 80vh;
48+
overflow-y: auto;
49+
position: relative;
50+
}
51+
52+
.friends-modal-close-button {
53+
position: absolute;
54+
top: 15px;
55+
right: 15px;
56+
background: none;
57+
border: none;
58+
font-size: 24px;
59+
cursor: pointer;
60+
color: #888;
61+
}
62+
63+
.friends-modal-tabs {
64+
display: flex;
65+
border-bottom: 2px solid #eee;
66+
margin-bottom: 20px;
67+
}
68+
69+
.friends-modal-tab {
70+
padding: 10px 20px;
71+
cursor: pointer;
72+
border: none;
73+
background: none;
74+
font-size: 16px;
75+
font-weight: 500;
76+
color: #999;
77+
position: relative;
78+
}
79+
80+
.friends-modal-tab.active {
81+
color: #6a1b9a;
82+
border-bottom: 2px solid #6a1b9a;
83+
}
84+
85+
.friends-list, .friend-requests-list, .user-search-results {
86+
list-style: none;
87+
padding: 0;
88+
}
89+
90+
.friend-item, .request-item, .search-result-item {
91+
display: flex;
92+
justify-content: space-between;
93+
align-items: center;
94+
padding: 12px 8px;
95+
border-bottom: 1px solid #f0f0f0;
96+
}
97+
98+
.accept-request-button, .add-friend-button {
99+
background-color: #4caf50; /* Green */
100+
color: white;
101+
border: none;
102+
padding: 8px 12px;
103+
border-radius: 6px;
104+
cursor: pointer;
105+
transition: background-color 0.2s;
106+
}
107+
108+
.accept-request-button:hover, .add-friend-button:hover {
109+
background-color: #45a049;
110+
}
111+
112+
.notification-badge {
113+
background-color: #f44336; /* Red */
114+
color: white;
115+
border-radius: 50%;
116+
padding: 2px 6px;
117+
font-size: 12px;
118+
position: absolute;
119+
top: 5px;
120+
right: 5px;
121+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useState } from 'react';
2+
import './Friends.css'; // Import our new styles
3+
import FriendList from './FriendList';
4+
import FriendRequests from './FriendRequests';
5+
import UserSearch from './UserSearch';
6+
7+
const FriendsModal = ({ isOpen, onClose }) => {
8+
const [activeTab, setActiveTab] = useState('list'); // 'list', 'requests', 'add'
9+
const [requestsCount, setRequestsCount] = useState(0);
10+
11+
if (!isOpen) {
12+
return null;
13+
}
14+
15+
const handleRequestsCountChange = (count) => {
16+
setRequestsCount(count);
17+
};
18+
19+
return (
20+
<div className="friends-modal-overlay">
21+
<div className="friends-modal-content">
22+
<button onClick={onClose} className="friends-modal-close-button">&times;</button>
23+
<h2>Friends</h2>
24+
<div className="friends-modal-tabs">
25+
<button
26+
className={`friends-modal-tab ${activeTab === 'list' ? 'active' : ''}`}
27+
onClick={() => setActiveTab('list')}
28+
>
29+
My Friends
30+
</button>
31+
<button
32+
className={`friends-modal-tab ${activeTab === 'requests' ? 'active' : ''}`}
33+
onClick={() => setActiveTab('requests')}
34+
>
35+
Requests
36+
{requestsCount > 0 && <span className="notification-badge">{requestsCount}</span>}
37+
</button>
38+
<button
39+
className={`friends-modal-tab ${activeTab === 'add' ? 'active' : ''}`}
40+
onClick={() => setActiveTab('add')}
41+
>
42+
Add Friend
43+
</button>
44+
</div>
45+
<div className="friends-modal-body">
46+
{activeTab === 'list' && <FriendList />}
47+
{activeTab === 'requests' && <FriendRequests onRequestsCountChange={handleRequestsCountChange} />}
48+
{activeTab === 'add' && <UserSearch />}
49+
</div>
50+
</div>
51+
</div>
52+
);
53+
};
54+
55+
export default FriendsModal;

quiz-frontend/quiz-frontend/src/components/Home.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import React, { useEffect, useState } from 'react';
2+
import FriendsModal from './FriendsModal';
3+
import './Friends.css';
24

35
const Home = () => {
46
const [message, setMessage] = useState('');
57
const [username, setUsername] = useState('');
68
const [error, setError] = useState('');
79
const [announcements, setAnnouncements] = useState([]);
10+
const [isFriendsModalOpen, setFriendsModalOpen] = useState(false);
811

912
useEffect(() => {
1013
document.title = "Home";
@@ -55,6 +58,17 @@ const Home = () => {
5558
</ul>
5659
)}
5760
</div>
61+
62+
{/* Floating Friends Icon Button */}
63+
<button onClick={() => setFriendsModalOpen(true)} className="friends-icon-button">
64+
👥
65+
</button>
66+
67+
{/* Friends Modal */}
68+
<FriendsModal
69+
isOpen={isFriendsModalOpen}
70+
onClose={() => setFriendsModalOpen(false)}
71+
/>
5872
</div>
5973
);
6074
};

0 commit comments

Comments
 (0)