Skip to content

Commit c5165fd

Browse files
Ani GasitashviliAni Gasitashvili
authored andcommitted
Admin features: dashboard, statistics, quiz/user management, UI improvements
1 parent 65a1110 commit c5165fd

File tree

13 files changed

+207
-42
lines changed

13 files changed

+207
-42
lines changed

.idea/dataSources.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

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

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"java.compile.nullAnalysis.mode": "automatic"
3+
}

quiz-frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"proxy": "http://localhost:8081"
3+
}

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,29 @@ const AdminDashboard = () => {
66
const [error, setError] = useState('');
77
const [newTitle, setNewTitle] = useState('');
88
const [newContent, setNewContent] = useState('');
9+
const [currentUsername, setCurrentUsername] = useState('');
910

1011
useEffect(() => {
1112
document.title = "Admin Dashboard";
1213
fetchUsers();
14+
fetchCurrentUser();
1315
}, []);
1416

17+
const fetchCurrentUser = async () => {
18+
try {
19+
const res = await fetch('http://localhost:8081/api/home', { credentials: 'include' });
20+
const data = await res.json();
21+
setCurrentUsername(data.user);
22+
} catch (err) {
23+
setCurrentUsername('');
24+
}
25+
};
26+
1527
const fetchUsers = async () => {
1628
try {
1729
const res = await axios.get('http://localhost:8081/api/admin/users', {
1830
withCredentials: true,
1931
});
20-
2132
setUsers(Array.isArray(res.data) ? res.data : []);
2233
} catch (err) {
2334
setError('Failed to fetch users. You must be an admin.');
@@ -72,7 +83,7 @@ const AdminDashboard = () => {
7283
<table>
7384
<thead>
7485
<tr>
75-
<th>ID</th>
86+
{/* <th>ID</th> */}
7687
<th>Username</th>
7788
<th>Role</th>
7889
<th>Actions</th>
@@ -81,14 +92,17 @@ const AdminDashboard = () => {
8192
<tbody>
8293
{users.map((user) => (
8394
<tr key={user.id}>
84-
<td>{user.id}</td>
95+
{/* <td>{user.id}</td> */}
8596
<td>{user.username}</td>
86-
<td>{user.role}</td>
97+
<td>{user.role === 'ROLE_ADMIN' ? 'Admin' : 'User'}</td>
8798
<td>
8899
{user.role !== 'ROLE_ADMIN' && (
89100
<button onClick={() => promoteUser(user.id)}>Promote</button>
90101
)}
91-
<button onClick={() => deleteUser(user.id)}>Delete</button>
102+
{/* Hide delete button for current user */}
103+
{user.username !== currentUsername && (
104+
<button onClick={() => deleteUser(user.id)}>Delete</button>
105+
)}
92106
</td>
93107
</tr>
94108
))}

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

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import { useNavigate } from 'react-router-dom';
77
const Home = () => {
88
const [message, setMessage] = useState('');
99
const [username, setUsername] = useState('');
10+
const [role, setRole] = useState(''); // NEW: store role
1011
const [error, setError] = useState('');
1112
const [announcements, setAnnouncements] = useState([]);
1213
const [isFriendsModalOpen, setFriendsModalOpen] = useState(false);
1314
const [isMessagesModalOpen, setMessagesModalOpen] = useState(false);
1415
const [isSignOutModalOpen, setSignOutModalOpen] = useState(false);
16+
const [showStats, setShowStats] = useState(false); // NEW: show/hide stats modal
17+
const [stats, setStats] = useState(null); // NEW: store stats
1518
const navigate = useNavigate();
1619

1720
useEffect(() => {
@@ -25,6 +28,7 @@ const Home = () => {
2528
.then((data) => {
2629
setMessage(data.message);
2730
setUsername(data.user);
31+
setRole(data.role); // NEW: set role
2832
})
2933
.catch((err) => setError(err.message));
3034

@@ -45,15 +49,54 @@ const Home = () => {
4549
navigate('/login');
4650
};
4751

52+
// Fetch statistics when modal is opened
53+
const handleShowStats = async () => {
54+
try {
55+
const res = await fetch('http://localhost:8081/api/admin/statistics', { credentials: 'include' });
56+
if (!res.ok) throw new Error('Failed to fetch statistics');
57+
const data = await res.json();
58+
setStats(data);
59+
} catch (e) {
60+
setStats({ error: 'Failed to fetch statistics' });
61+
}
62+
setShowStats(true);
63+
};
64+
4865
return (
4966
<div className="auth-container">
50-
{/* Top-left sign out button */}
51-
<div className="top-left-signout">
52-
<button className="signout-icon-button" onClick={() => setSignOutModalOpen(true)} title="Sign Out">
53-
⬅️
54-
</button>
67+
{/* Top-left action bar */}
68+
<div style={{ position: 'fixed', top: 24, left: 32, display: 'flex', gap: 12, zIndex: 2000, background: '#fff', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.10)', padding: '6px 12px' }}>
69+
{role === 'ROLE_ADMIN' && (
70+
<>
71+
<button onClick={() => navigate('/admin')} style={{ background: '#312e81', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 20px', fontWeight: 700, fontSize: 16, boxShadow: '0 2px 8px rgba(49,46,129,0.08)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseOver={e => e.currentTarget.style.background='#4338ca'} onMouseOut={e => e.currentTarget.style.background='#312e81'}>Admin</button>
72+
<button onClick={handleShowStats} style={{ background: '#0d9488', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 20px', fontWeight: 700, fontSize: 16, boxShadow: '0 2px 8px rgba(13,148,136,0.08)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseOver={e => e.currentTarget.style.background='#14b8a6'} onMouseOut={e => e.currentTarget.style.background='#0d9488'}>Statistics</button>
73+
</>
74+
)}
75+
<button onClick={() => setMessagesModalOpen(true)} style={{ background: '#2563eb', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 20px', fontWeight: 700, fontSize: 16, boxShadow: '0 2px 8px rgba(37,99,235,0.08)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseOver={e => e.currentTarget.style.background='#1d4ed8'} onMouseOut={e => e.currentTarget.style.background='#2563eb'}>Messages</button>
76+
<button onClick={() => setFriendsModalOpen(true)} style={{ background: '#059669', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 20px', fontWeight: 700, fontSize: 16, boxShadow: '0 2px 8px rgba(5,150,105,0.08)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseOver={e => e.currentTarget.style.background='#10b981'} onMouseOut={e => e.currentTarget.style.background='#059669'}>Friends</button>
77+
<button onClick={() => setSignOutModalOpen(true)} style={{ background: '#e11d48', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 20px', fontWeight: 700, fontSize: 16, boxShadow: '0 2px 8px rgba(225,29,72,0.08)', cursor: 'pointer', transition: 'background 0.2s' }} onMouseOver={e => e.currentTarget.style.background='#be123c'} onMouseOut={e => e.currentTarget.style.background='#e11d48'}>Sign Out</button>
5578
</div>
5679

80+
{/* Statistics Modal */}
81+
{showStats && (
82+
<div style={{ position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.25)', zIndex: 3000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
83+
<div style={{ background: '#fff', borderRadius: 12, padding: 32, minWidth: 320, boxShadow: '0 4px 24px rgba(0,0,0,0.12)', position: 'relative' }}>
84+
<button onClick={() => setShowStats(false)} style={{ position: 'absolute', top: 12, right: 12, background: 'none', border: 'none', fontSize: 22, cursor: 'pointer', color: '#888' }}>&times;</button>
85+
<h2 style={{ color: '#4f46e5', marginBottom: 18 }}>Site Statistics</h2>
86+
{stats ? (
87+
stats.error ? <div style={{ color: 'red' }}>{stats.error}</div> :
88+
<ul style={{ fontSize: 18, lineHeight: 2 }}>
89+
<li><b>Users:</b> {stats.users}</li>
90+
<li><b>Quizzes:</b> {stats.quizzes}</li>
91+
<li><b>Quizzes Taken:</b> {stats.quizzesTaken}</li>
92+
</ul>
93+
) : (
94+
<div>Loading...</div>
95+
)}
96+
</div>
97+
</div>
98+
)}
99+
57100
{/* Sign-out Modal */}
58101
{isSignOutModalOpen && (
59102
<div className="signout-modal-overlay">
@@ -89,7 +132,7 @@ const Home = () => {
89132
<li key={a.id}>
90133
<strong>{a.title}</strong><br />
91134
<span>{a.content}</span><br />
92-
<small>{new Date(a.createdAt).toLocaleString()}</small>
135+
<small>{new Date(a.createdAt).toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</small>
93136
<hr />
94137
</li>
95138
))}
@@ -101,16 +144,6 @@ const Home = () => {
101144
<button onClick={() => navigate('/quizzes')} style={{marginTop: '20px', marginRight: '10px'}}>Browse Quizzes</button>
102145
<button onClick={() => navigate('/create-quiz')} style={{marginTop: '20px'}}>Create a Quiz</button>
103146

104-
{/* Top-right floating icons */}
105-
<div className="top-right-icons">
106-
<button onClick={() => setMessagesModalOpen(true)} className="messages-icon-button" title="Messages">
107-
💬
108-
</button>
109-
<button onClick={() => setFriendsModalOpen(true)} className="friends-icon-button" title="Friends">
110-
👥
111-
</button>
112-
</div>
113-
114147
{/* Modals */}
115148
<FriendsModal
116149
isOpen={isFriendsModalOpen}

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

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ const QuizList = () => {
2424
const [quizzes, setQuizzes] = useState([]);
2525
const [error, setError] = useState('');
2626
const [useTestQuizzes, setUseTestQuizzes] = useState(false);
27+
const [role, setRole] = useState(''); // NEW: store role
2728
const navigate = useNavigate();
2829

2930
useEffect(() => {
31+
fetch('http://localhost:8081/api/home', { credentials: 'include' })
32+
.then((res) => res.json())
33+
.then((data) => setRole(data.role || ''));
34+
3035
fetch('http://localhost:8081/api/quizzes', {
3136
credentials: 'include'
3237
})
@@ -48,26 +53,42 @@ const QuizList = () => {
4853
});
4954
}, []);
5055

56+
const handleDelete = async (quizId) => {
57+
if (!window.confirm('Are you sure you want to delete this quiz?')) return;
58+
try {
59+
await fetch(`http://localhost:8081/api/quizzes/${quizId}`, {
60+
method: 'DELETE',
61+
credentials: 'include',
62+
});
63+
setQuizzes(quizzes.filter(q => q.id !== quizId));
64+
} catch (err) {
65+
alert('Failed to delete quiz.');
66+
}
67+
};
68+
5169
const displayQuizzes = useTestQuizzes ? TEST_QUIZZES : quizzes;
5270

5371
return (
54-
<div className="quiz-list-container">
55-
<h2 style={{textAlign:'center',marginBottom:'24px'}}>Available Quizzes</h2>
56-
{error && <div style={{ color: 'red', marginBottom: '1em' }}>{error}</div>}
57-
<ul className="quiz-list">
58-
{displayQuizzes.map(quiz => (
59-
<li key={quiz.id} className="quiz-card">
72+
<div className="quiz-list-container" style={{ maxWidth: 600, margin: '40px auto', background: '#fff', borderRadius: 16, boxShadow: '0 4px 24px rgba(0,0,0,0.10)', padding: 32 }}>
73+
<h2 style={{textAlign:'center',marginBottom:'32px', color: '#4f46e5', fontWeight: 700}}>Available Quizzes</h2>
74+
{error && <div className="auth-error">{error}</div>}
75+
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
76+
{displayQuizzes.map((quiz) => (
77+
<div key={quiz.id} style={{ background: '#f9fafb', borderRadius: 12, boxShadow: '0 2px 8px rgba(0,0,0,0.04)', padding: 20, display: 'flex', alignItems: 'center', justifyContent: 'space-between', transition: 'box-shadow 0.2s', border: '1px solid #e5e7eb' }}>
6078
<div>
61-
<div className="quiz-title">{quiz.title}</div>
62-
<div className="quiz-meta">by {quiz.createdBy || quiz.creator}</div>
79+
<div style={{ fontSize: 20, fontWeight: 600, color: '#22223b', marginBottom: 4 }}>{quiz.title}</div>
80+
<div style={{ color: '#6366f1', fontWeight: 500, fontSize: 15 }}>by {quiz.creator || quiz.createdBy || 'unknown'}</div>
81+
</div>
82+
<div style={{ display: 'flex', gap: 10 }}>
83+
<button onClick={() => navigate(`/quiz/${quiz.id}`)} style={{ padding: '8px 16px', background: '#6366f1', color: '#fff', border: 'none', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 15, transition: 'background 0.2s' }}>Check it out</button>
84+
{role === 'ROLE_ADMIN' && !useTestQuizzes && (
85+
<button onClick={() => handleDelete(quiz.id)} style={{ padding: '8px 16px', background: '#e11d48', color: '#fff', border: 'none', borderRadius: 6, fontWeight: 600, cursor: 'pointer', fontSize: 15, transition: 'background 0.2s' }}>Delete</button>
86+
)}
6387
</div>
64-
<button className="quiz-btn quiz-btn-primary" onClick={() => navigate(`/quiz-summary/${quiz.id}`)}>
65-
Check it out
66-
</button>
67-
</li>
88+
</div>
6889
))}
69-
</ul>
70-
{displayQuizzes.length === 0 && <div>No quizzes available.</div>}
90+
</div>
91+
{displayQuizzes.length === 0 && <div style={{textAlign:'center',marginTop:32}}>No quizzes available.</div>}
7192
</div>
7293
);
7394
};

quiz-website/src/main/java/com/quizapp/controller/AdminController.java

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
import com.quizapp.model.User;
44
import com.quizapp.repository.UserRepository;
5+
import com.quizapp.repository.FriendRequestRepository;
6+
import com.quizapp.repository.MessageRepository;
7+
import com.quizapp.repository.QuizAttemptRepository;
8+
import com.quizapp.repository.AnswerRepository;
9+
import com.quizapp.repository.QuizRepository;
510
import com.quizapp.util.SecurityUtils;
611
import lombok.RequiredArgsConstructor;
712
import org.springframework.http.ResponseEntity;
@@ -17,6 +22,11 @@
1722
public class AdminController {
1823

1924
private final UserRepository userRepository;
25+
private final FriendRequestRepository friendRequestRepository;
26+
private final MessageRepository messageRepository;
27+
private final QuizAttemptRepository quizAttemptRepository;
28+
private final AnswerRepository answerRepository;
29+
private final QuizRepository quizRepository;
2030

2131
// ✅ Get all users
2232
@GetMapping("/users")
@@ -53,11 +63,58 @@ public ResponseEntity<String> deleteUser(@PathVariable Long id, Authentication a
5363
return ResponseEntity.status(403).build();
5464
}
5565

56-
if (userRepository.existsById(id)) {
57-
userRepository.deleteById(id);
58-
return ResponseEntity.ok("User deleted successfully.");
59-
} else {
66+
Optional<User> userOpt = userRepository.findById(id);
67+
if (userOpt.isEmpty()) {
6068
return ResponseEntity.notFound().build();
6169
}
70+
User user = userOpt.get();
71+
String username = user.getUsername();
72+
73+
// 1. Delete all friend requests involving this user
74+
friendRequestRepository.deleteAll(friendRequestRepository.findByRequester(user));
75+
friendRequestRepository.deleteAll(friendRequestRepository.findByAddressee(user));
76+
77+
// 2. Remove this user from all friends' lists (bidirectional)
78+
for (User u : userRepository.findAll()) {
79+
if (u.getFriends().remove(user)) {
80+
userRepository.save(u);
81+
}
82+
}
83+
user.getFriends().clear();
84+
userRepository.save(user);
85+
86+
// 3. Delete all messages sent or received by this user
87+
messageRepository.deleteAll(messageRepository.findBySenderUsernameAndReceiverUsernameOrderByTimestampDesc(username, username));
88+
// Delete all messages where this user is sender or receiver
89+
messageRepository.deleteAll(messageRepository.findAll().stream().filter(m -> m.getSenderUsername().equals(username) || m.getReceiverUsername().equals(username)).toList());
90+
91+
// 4. Delete all quiz attempts and answers by this user
92+
var attempts = quizAttemptRepository.findByUserIdOrderByStartTimeDesc(user.getId());
93+
for (var attempt : attempts) {
94+
answerRepository.deleteAll(answerRepository.findByQuizAttemptIdOrderByQuestionNumber(attempt.getId()));
95+
quizAttemptRepository.delete(attempt);
96+
}
97+
98+
// 5. Finally, delete the user
99+
userRepository.deleteById(id);
100+
return ResponseEntity.ok("User deleted successfully.");
101+
}
102+
103+
@GetMapping("/statistics")
104+
public ResponseEntity<?> getStatistics(Authentication authentication) {
105+
if (!SecurityUtils.isAdmin(authentication)) {
106+
return ResponseEntity.status(403).build();
107+
}
108+
long userCount = userRepository.count();
109+
long quizCount = quizRepository.count();
110+
// Count only completed, non-practice quiz attempts
111+
long quizAttempts = quizAttemptRepository.findAll().stream().filter(a -> Boolean.TRUE.equals(a.getIsCompleted()) && Boolean.FALSE.equals(a.getIsPracticeMode())).count();
112+
return ResponseEntity.ok(
113+
java.util.Map.of(
114+
"users", userCount,
115+
"quizzes", quizCount,
116+
"quizzesTaken", quizAttempts
117+
)
118+
);
62119
}
63120
}

quiz-website/src/main/java/com/quizapp/controller/HomeController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ public Map<String, String> home(@AuthenticationPrincipal org.springframework.sec
2121
if (user != null) {
2222
response.put("message", "Welcome to the home page!");
2323
response.put("user", user.getUsername());
24+
// Add role to response
25+
response.put("role", user.getAuthorities().stream().findFirst().map(Object::toString).orElse(""));
2426
} else {
2527
response.put("message", "Not authenticated");
2628
response.put("user", null);
29+
response.put("role", "");
2730
}
2831
return response;
2932
}

0 commit comments

Comments
 (0)