Compare commits

...

3 Commits

Author SHA1 Message Date
854b7b76a3 Merge pull request 'test leaving challange invited to' (#2) from stage into main
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 32s
Reviewed-on: #2
2026-01-31 00:49:10 +00:00
f47ea8efaa Merge branch 'main' into stage 2026-01-31 00:49:07 +00:00
75d6eb8bbc test leaving challange invited to 2026-01-30 19:45:22 -05:00
3 changed files with 69 additions and 1 deletions

View File

@@ -213,6 +213,43 @@ router.post('/:id/respond', authMiddleware, asyncHandler(async (req, res) => {
res.json({ status }); res.json({ status });
})); }));
// Leave a challenge (participants only, not creator)
router.post('/:id/leave', authMiddleware, asyncHandler(async (req, res) => {
const challengeId = req.params.id;
// Get challenge to verify it exists and user is not creator
const challenges = await query(
'SELECT * FROM challenges WHERE id = ?',
[challengeId]
);
if (challenges.length === 0) {
throw new AppError('Challenge not found', 404);
}
const challenge = challenges[0];
if (challenge.created_by === req.user.userId) {
throw new AppError('Challenge creator cannot leave. Delete the challenge instead.', 403);
}
// Check if user is a participant
const participation = await query(
'SELECT * FROM challenge_participants WHERE challenge_id = ? AND user_id = ?',
[challengeId, req.user.userId]
);
if (participation.length === 0) {
throw new AppError('You are not a participant of this challenge', 404);
}
// Delete user's participation and predictions
await query('DELETE FROM predictions WHERE challenge_id = ? AND user_id = ?', [challengeId, req.user.userId]);
await query('DELETE FROM challenge_participants WHERE challenge_id = ? AND user_id = ?', [challengeId, req.user.userId]);
res.json({ success: true, message: 'Left challenge successfully' });
}));
// Delete a challenge (only creator can delete) // Delete a challenge (only creator can delete)
router.delete('/:id', authMiddleware, asyncHandler(async (req, res) => { router.delete('/:id', authMiddleware, asyncHandler(async (req, res) => {
const challengeId = req.params.id; const challengeId = req.params.id;

View File

@@ -148,6 +148,12 @@ class API {
}); });
} }
async leaveChallenge(challengeId) {
return this.request(`/challenges/${challengeId}/leave`, {
method: 'POST'
});
}
// TMDB // TMDB
async searchShows(query) { async searchShows(query) {
return this.request(`/tmdb/search?q=${encodeURIComponent(query)}`); return this.request(`/tmdb/search?q=${encodeURIComponent(query)}`);

View File

@@ -25,6 +25,7 @@ export default function ChallengeDetail() {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const [friends, setFriends] = useState([]); const [friends, setFriends] = useState([]);
const [inviting, setInviting] = useState(null); const [inviting, setInviting] = useState(null);
const [leaving, setLeaving] = useState(false);
const searchRef = useRef(null); const searchRef = useRef(null);
useClickOutside(searchRef, () => setSearchResults([])); useClickOutside(searchRef, () => setSearchResults([]));
@@ -235,6 +236,22 @@ export default function ChallengeDetail() {
} }
}; };
const handleLeave = async () => {
if (!confirm('Are you sure you want to leave this challenge? Your predictions will be removed.')) {
return;
}
setLeaving(true);
try {
await api.leaveChallenge(id);
toast.success('Left challenge successfully');
navigate('/challenges');
} catch (err) {
toast.error('Failed to leave challenge: ' + err.message);
setLeaving(false);
}
};
if (loading) { if (loading) {
return <div className="loading">Loading challenge...</div>; return <div className="loading">Loading challenge...</div>;
} }
@@ -271,7 +288,7 @@ export default function ChallengeDetail() {
> >
{showInvite ? 'Cancel' : 'Invite Friends'} {showInvite ? 'Cancel' : 'Invite Friends'}
</button> </button>
{challenge.challenge.created_by === user.id && ( {challenge.challenge.created_by === user.id ? (
<button <button
className="btn btn-danger btn-sm" className="btn btn-danger btn-sm"
onClick={handleDelete} onClick={handleDelete}
@@ -279,6 +296,14 @@ export default function ChallengeDetail() {
> >
{deleting ? 'Deleting...' : 'Delete Challenge'} {deleting ? 'Deleting...' : 'Delete Challenge'}
</button> </button>
) : (
<button
className="btn btn-danger btn-sm"
onClick={handleLeave}
disabled={leaving}
>
{leaving ? 'Leaving...' : 'Leave Challenge'}
</button>
)} )}
</div> </div>
</div> </div>