Compare commits
3 Commits
1070f9b3d2
...
854b7b76a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 854b7b76a3 | |||
| f47ea8efaa | |||
| 75d6eb8bbc |
@@ -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;
|
||||||
|
|||||||
@@ -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)}`);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user