diff --git a/backend/src/routes/challenges.js b/backend/src/routes/challenges.js index 095270a..4fc3ca2 100644 --- a/backend/src/routes/challenges.js +++ b/backend/src/routes/challenges.js @@ -213,4 +213,32 @@ router.post('/:id/respond', authMiddleware, asyncHandler(async (req, res) => { res.json({ status }); })); +// Delete a challenge (only creator can delete) +router.delete('/:id', authMiddleware, asyncHandler(async (req, res) => { + const challengeId = req.params.id; + + // Get challenge and verify ownership + 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('Only the creator can delete this challenge', 403); + } + + // Delete related data (cascade should handle this, but being explicit) + await query('DELETE FROM predictions WHERE challenge_id = ?', [challengeId]); + await query('DELETE FROM challenge_participants WHERE challenge_id = ?', [challengeId]); + await query('DELETE FROM challenges WHERE id = ?', [challengeId]); + + res.json({ success: true, message: 'Challenge deleted successfully' }); +})); + export default router; diff --git a/backend/src/routes/friends.js b/backend/src/routes/friends.js index 85139a0..5d9cfa6 100644 --- a/backend/src/routes/friends.js +++ b/backend/src/routes/friends.js @@ -159,4 +159,26 @@ router.get('/requests', authMiddleware, asyncHandler(async (req, res) => { res.json({ requests }); })); +// Remove a friend +router.delete('/:friendId', authMiddleware, asyncHandler(async (req, res) => { + const friendId = parseInt(req.params.friendId); + + if (!friendId) { + throw new AppError('Friend ID required', 400); + } + + // Delete friendship in both directions + const result = await query( + `DELETE FROM friendships + WHERE (user_id = ? AND friend_id = ?) OR (user_id = ? AND friend_id = ?)`, + [req.user.userId, friendId, friendId, req.user.userId] + ); + + if (result.affectedRows === 0) { + throw new AppError('Friendship not found', 404); + } + + res.json({ success: true, message: 'Friend removed successfully' }); +})); + export default router; diff --git a/frontend/src/api.js b/frontend/src/api.js index 0a74360..33d3e8f 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -136,6 +136,18 @@ class API { return this.request('/friends/requests'); } + async removeFriend(friendId) { + return this.request(`/friends/${friendId}`, { + method: 'DELETE' + }); + } + + async deleteChallenge(challengeId) { + return this.request(`/challenges/${challengeId}`, { + method: 'DELETE' + }); + } + // TMDB async searchShows(query) { return this.request(`/tmdb/search?q=${encodeURIComponent(query)}`); diff --git a/frontend/src/pages/ChallengeDetail.jsx b/frontend/src/pages/ChallengeDetail.jsx index da21b94..7df80f3 100644 --- a/frontend/src/pages/ChallengeDetail.jsx +++ b/frontend/src/pages/ChallengeDetail.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef, useCallback } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useNavigate } from 'react-router-dom'; import toast from 'react-hot-toast'; import { useAuth } from '../AuthContext'; import { useSocket } from '../SocketContext'; @@ -8,6 +8,7 @@ import { useClickOutside } from '../hooks/useClickOutside'; export default function ChallengeDetail() { const { id } = useParams(); + const navigate = useNavigate(); const { user } = useAuth(); const { socket, joinChallenge, leaveChallenge } = useSocket(); const [challenge, setChallenge] = useState(null); @@ -21,6 +22,7 @@ export default function ChallengeDetail() { const [searchTimeout, setSearchTimeout] = useState(null); const [submitting, setSubmitting] = useState(false); const [validating, setValidating] = useState(null); + const [deleting, setDeleting] = useState(false); const searchRef = useRef(null); useClickOutside(searchRef, () => setSearchResults([])); @@ -199,6 +201,22 @@ export default function ChallengeDetail() { } }; + const handleDelete = async () => { + if (!confirm('Are you sure you want to delete this challenge? This action cannot be undone.')) { + return; + } + + setDeleting(true); + try { + await api.deleteChallenge(id); + toast.success('Challenge deleted successfully'); + navigate('/challenges'); + } catch (err) { + toast.error('Failed to delete challenge: ' + err.message); + setDeleting(false); + } + }; + if (loading) { return
Created by {challenge.challenge.creator_username}
- +