Skip to content

Commit 4e5b8db

Browse files
committed
report player button in SharePlay, Set Host in room settings, player_reports table, transfer_host socket event
1 parent affe719 commit 4e5b8db

3 files changed

Lines changed: 98 additions & 17 deletions

File tree

backend/server-ready.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,6 +1392,15 @@ async function initDatabase() {
13921392
voters JSONB DEFAULT '[]',
13931393
kicked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
13941394
)`,
1395+
`CREATE TABLE IF NOT EXISTS player_reports (
1396+
id SERIAL PRIMARY KEY,
1397+
reporter_user_id INT REFERENCES users(id),
1398+
reported_user_id INT REFERENCES users(id),
1399+
room_code VARCHAR(10),
1400+
reason VARCHAR(50) NOT NULL,
1401+
note TEXT,
1402+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
1403+
)`,
13951404
];
13961405
for (const m of migrations) {
13971406
try { await client.query(m); } catch(e) { console.log('Migration skipped:', e.message); }
@@ -2466,6 +2475,24 @@ app.post('/game/report', async (req, res) => {
24662475
} catch (err) { res.status(500).json({ error: err.message }); }
24672476
});
24682477

2478+
app.post('/game/report-player', async (req, res) => {
2479+
const u = getTokenUser(req);
2480+
const { reportedUserId, roomCode, reason, note } = req.body;
2481+
if (!reportedUserId || !reason) return res.status(400).json({ error: 'reportedUserId and reason required' });
2482+
if (u && u.id === reportedUserId) return res.status(400).json({ error: 'Cannot report yourself' });
2483+
try {
2484+
if (u) {
2485+
const blocked = await isUserBlocked(u.id);
2486+
if (blocked) return res.status(403).json({ error: 'Account is blocked' });
2487+
}
2488+
await runQuery('INSERT INTO player_reports (reporter_user_id, reported_user_id, room_code, reason, note) VALUES ($1,$2,$3,$4,$5)', [
2489+
u?.id || null, reportedUserId, roomCode || null, reason, note || null
2490+
]);
2491+
console.log(`🚩 Player ${reportedUserId} reported by ${u ? `user ${u.id}` : 'guest'} - ${reason}`);
2492+
res.json({ success: true });
2493+
} catch (err) { res.status(500).json({ error: err.message }); }
2494+
});
2495+
24692496
// ── LEADERBOARD - excludes anonymous users and admins ──────────────────────────
24702497
app.get('/leaderboard', async (req, res) => {
24712498
const { categoryId, timeframe, includeAnonymous } = req.query;

backend/shareplay.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,25 @@ module.exports = function initSharePlay(server, pool, jwtSecret) {
817817
cancelKickVote(room, currentCode, io);
818818
});
819819

820+
socket.on('transfer_host', (data) => {
821+
if (!currentCode) return;
822+
const room = rooms.get(currentCode);
823+
if (!room) return;
824+
const profile = getProfile();
825+
const amHost = room.hostSocketId === socket.id || (profile.userId && profile.userId === room.hostUserId);
826+
if (!amHost) return;
827+
const targetSocketId = data?.targetSocketId;
828+
if (!targetSocketId || !room.players.has(targetSocketId)) return;
829+
if (targetSocketId === socket.id) return;
830+
room.hostSocketId = targetSocketId;
831+
const target = room.players.get(targetSocketId);
832+
if (target?.userId) room.hostUserId = target.userId;
833+
for (const [, p] of room.players) p.isHost = false;
834+
target.isHost = true;
835+
io.to(currentCode).emit('players_update', { players: playerList(room) });
836+
io.to(currentCode).emit('room_message', { message: `${profile.displayName} transferred host to ${target.displayName}` });
837+
});
838+
820839
socket.on('leave_room', () => dropFromRoom());
821840
socket.on('disconnect', () => dropFromRoom());
822841
});

frontend/src/SharePlay.js

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ const COMPLEXITY_COLOR = { easy: '#28a745', medium: '#fd7e14', hard: '#dc3545' }
9090

9191
// ── Settings panel (shared by admin/host) ────────────────────────────────────
9292

93-
function SettingsPanel({ settings, categories, onChange, onApply, title }) {
93+
function SettingsPanel({ settings, categories, onChange, onApply, title, players, myUserId, onSetHost }) {
9494
const [local, setLocal] = useState(settings);
9595
const [showCategoryModal, setShowCategoryModal] = useState(false);
9696
const [categorySearch, setCategorySearch] = useState('');
@@ -194,6 +194,23 @@ function SettingsPanel({ settings, categories, onChange, onApply, title }) {
194194

195195
{onChange && <div style={{ borderTop: '1px solid var(--border-color,#ddd)', paddingTop: '10px' }}>{toggle('Public (show in room list)', local.isPublic, v => set('isPublic', v))}</div>}
196196

197+
{onSetHost && players && players.length > 1 && (
198+
<div style={{ borderTop: '1px solid var(--border-color,#ddd)', paddingTop: '10px' }}>
199+
<span style={label}>Transfer Host</span>
200+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
201+
<select id="set-host-select" style={{ flex: 1, padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border-color,#ddd)', background: 'var(--card-bg,#fff)', color: 'var(--text-color,#333)', fontSize: '0.85rem' }}>
202+
{players.filter(p => p.userId && p.userId !== myUserId).map(p => (
203+
<option key={p.userId} value={p.socketId}>{p.displayName}{p.isHost ? ' (current host)' : ''}</option>
204+
))}
205+
</select>
206+
<button style={btn('#6c757d', { padding: '6px 12px', fontSize: '0.8rem' })} onClick={() => {
207+
const sel = document.getElementById('set-host-select');
208+
if (sel?.value) onSetHost(sel.value);
209+
}}>Transfer</button>
210+
</div>
211+
</div>
212+
)}
213+
197214
<button style={btn()} onClick={() => onApply(local)}>Apply Settings</button>
198215
</div>
199216
</div>
@@ -231,11 +248,11 @@ export default function SharePlay() {
231248
// Report / Suggest (persist across question changes intentionally)
232249
const [showRequestModal, setShowRequestModal] = useState(false);
233250
const [showReportMenu, setShowReportMenu] = useState(false);
251+
const [reportPlayerId, setReportPlayerId] = useState('');
234252
const [reportType, setReportType] = useState('general');
235253
const [reportNote, setReportNote] = useState('');
236254
const [reportMessage, setReportMessage] = useState('');
237255
const reportMenuRef = useRef(null);
238-
const reportingQIdRef = useRef(null); // capture question ID at open time
239256

240257
// Question / voting
241258
const [currentQuestion, setCurrentQuestion] = useState(null);
@@ -488,28 +505,35 @@ export default function SharePlay() {
488505
const handleSuggest = useCallback(() => setShowRequestModal(true), []);
489506

490507
const handleReport = useCallback(async (reasonOverride) => {
491-
const qId = reportingQIdRef.current;
492-
if (!qId) return;
508+
if (!reportPlayerId) return;
493509
try {
494510
const token = getToken();
495511
const headers = token ? { Authorization: `Bearer ${token}` } : undefined;
496-
await axios.post(`${REST_BASE}/api/game/report`, { questionId: qId, reason: reasonOverride || 'General Report' }, { headers });
497-
setReportMessage('✅ Reported successfully.');
512+
const reportedPlayer = players.find(p => p.userId === Number(reportPlayerId));
513+
await axios.post(`${REST_BASE}/api/game/report-player`, {
514+
reportedUserId: Number(reportPlayerId),
515+
roomCode,
516+
reason: reasonOverride || 'General Report',
517+
note: reportNote.trim() || undefined,
518+
}, { headers });
519+
setReportMessage(`✅ Reported ${reportedPlayer?.displayName || 'player'} successfully.`);
498520
} catch (err) {
499-
setReportMessage(`⚠️ ${err.response?.data?.error || 'Question flagged for review.'}`);
521+
setReportMessage(`⚠️ ${err.response?.data?.error || 'Failed to report player.'}`);
500522
}
501-
}, []);
523+
}, [reportPlayerId, players, roomCode, reportNote]);
502524

503525
const submitReport = useCallback(async () => {
526+
if (!reportPlayerId) { setReportMessage('⚠️ Select a player to report.'); return; }
504527
const trimmed = reportNote.trim();
505-
const needsNote = reportType === 'inappropriate' || reportType === 'incorrect';
528+
const needsNote = reportType === 'inappropriate' || reportType === 'cheating';
506529
if (needsNote && !trimmed) { setReportMessage('⚠️ Please add a short description.'); return; }
507-
const label = reportType === 'inappropriate' ? 'Inappropriate' : reportType === 'incorrect' ? 'Incorrect' : 'General Report';
530+
const label = reportType === 'inappropriate' ? 'Inappropriate' : reportType === 'cheating' ? 'Cheating' : reportType === 'harassment' ? 'Harassment' : 'General Report';
508531
await handleReport(needsNote ? `${label}: ${trimmed}` : label);
509532
setShowReportMenu(false);
510533
setReportNote('');
511534
setReportType('general');
512-
}, [reportNote, reportType, handleReport]);
535+
setReportPlayerId('');
536+
}, [reportNote, reportType, reportPlayerId, handleReport]);
513537

514538
const copyCode = useCallback(() => {
515539
navigator.clipboard?.writeText(roomCode).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }).catch(() => {});
@@ -1118,6 +1142,9 @@ export default function SharePlay() {
11181142
onChange={true}
11191143
onApply={applyHostSettings}
11201144
title="⚙ Room Settings"
1145+
players={players}
1146+
myUserId={user?.id}
1147+
onSetHost={(targetSocketId) => socketRef.current?.emit('transfer_host', { targetSocketId })}
11211148
/>
11221149
)}
11231150
</div>
@@ -1136,24 +1163,32 @@ export default function SharePlay() {
11361163
<button
11371164
style={{ ...btn('#6c757d'), fontSize: '0.85rem', padding: '7px 14px' }}
11381165
onClick={() => {
1139-
reportingQIdRef.current = currentQuestion.id;
11401166
setShowReportMenu(v => !v);
11411167
setReportMessage('');
1168+
setReportPlayerId('');
11421169
}}
11431170
>
11441171
⚠ Report
11451172
</button>
11461173

11471174
{showReportMenu && (
1148-
<div style={{ position: 'absolute', bottom: '110%', left: 0, background: 'var(--card-bg,#fff)', border: '1px solid var(--border-color,#ddd)', borderRadius: '10px', padding: '14px', width: '260px', zIndex: 20, boxShadow: '0 6px 20px rgba(0,0,0,0.15)' }}>
1149-
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '6px' }}>Report Type</div>
1175+
<div style={{ position: 'absolute', bottom: '110%', left: 0, background: 'var(--card-bg,#fff)', border: '1px solid var(--border-color,#ddd)', borderRadius: '10px', padding: '14px', width: '280px', zIndex: 20, boxShadow: '0 6px 20px rgba(0,0,0,0.15)' }}>
1176+
<div style={{ fontSize: '0.8rem', color: '#888', marginBottom: '6px' }}>Report Player</div>
1177+
<select value={reportPlayerId} onChange={e => setReportPlayerId(e.target.value)}
1178+
style={{ width: '100%', padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border-color,#ddd)', background: 'var(--card-bg,#fff)', color: 'var(--text-color,#333)', marginBottom: '8px' }}>
1179+
<option value="">Select a player...</option>
1180+
{players.filter(p => p.userId && p.userId !== user?.id).map(p => (
1181+
<option key={p.userId} value={p.userId}>{p.displayName}</option>
1182+
))}
1183+
</select>
11501184
<select value={reportType} onChange={e => setReportType(e.target.value)}
11511185
style={{ width: '100%', padding: '6px 8px', borderRadius: '6px', border: '1px solid var(--border-color,#ddd)', background: 'var(--card-bg,#fff)', color: 'var(--text-color,#333)', marginBottom: '8px' }}>
11521186
<option value="general">General Report</option>
1153-
<option value="inappropriate">Inappropriate</option>
1154-
<option value="incorrect">Incorrect Answer</option>
1187+
<option value="inappropriate">Inappropriate Behavior</option>
1188+
<option value="cheating">Cheating</option>
1189+
<option value="harassment">Harassment</option>
11551190
</select>
1156-
{(reportType === 'inappropriate' || reportType === 'incorrect') && (
1191+
{(reportType === 'inappropriate' || reportType === 'cheating' || reportType === 'harassment') && (
11571192
<textarea value={reportNote} onChange={e => setReportNote(e.target.value)}
11581193
placeholder="Add a short description..."
11591194
rows={3}

0 commit comments

Comments
 (0)