ways to remove and delete
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 33s
All checks were successful
Build Images and Deploy / Update-PROD-Stack (push) Successful in 33s
This commit is contained in:
@@ -213,4 +213,32 @@ router.post('/:id/respond', authMiddleware, asyncHandler(async (req, res) => {
|
|||||||
res.json({ status });
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -159,4 +159,26 @@ router.get('/requests', authMiddleware, asyncHandler(async (req, res) => {
|
|||||||
res.json({ requests });
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ class API {
|
|||||||
return this.request('/friends/requests');
|
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
|
// TMDB
|
||||||
async searchShows(query) {
|
async searchShows(query) {
|
||||||
return this.request(`/tmdb/search?q=${encodeURIComponent(query)}`);
|
return this.request(`/tmdb/search?q=${encodeURIComponent(query)}`);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
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 toast from 'react-hot-toast';
|
||||||
import { useAuth } from '../AuthContext';
|
import { useAuth } from '../AuthContext';
|
||||||
import { useSocket } from '../SocketContext';
|
import { useSocket } from '../SocketContext';
|
||||||
@@ -8,6 +8,7 @@ import { useClickOutside } from '../hooks/useClickOutside';
|
|||||||
|
|
||||||
export default function ChallengeDetail() {
|
export default function ChallengeDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { socket, joinChallenge, leaveChallenge } = useSocket();
|
const { socket, joinChallenge, leaveChallenge } = useSocket();
|
||||||
const [challenge, setChallenge] = useState(null);
|
const [challenge, setChallenge] = useState(null);
|
||||||
@@ -21,6 +22,7 @@ export default function ChallengeDetail() {
|
|||||||
const [searchTimeout, setSearchTimeout] = useState(null);
|
const [searchTimeout, setSearchTimeout] = useState(null);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [validating, setValidating] = useState(null);
|
const [validating, setValidating] = useState(null);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
const searchRef = useRef(null);
|
const searchRef = useRef(null);
|
||||||
|
|
||||||
useClickOutside(searchRef, () => setSearchResults([]));
|
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) {
|
if (loading) {
|
||||||
return <div className="loading">Loading challenge...</div>;
|
return <div className="loading">Loading challenge...</div>;
|
||||||
}
|
}
|
||||||
@@ -228,12 +246,23 @@ export default function ChallengeDetail() {
|
|||||||
<p style={{ color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
<p style={{ color: 'var(--text-muted)', marginBottom: '1rem' }}>
|
||||||
Created by {challenge.challenge.creator_username}
|
Created by {challenge.challenge.creator_username}
|
||||||
</p>
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-secondary btn-sm"
|
className="btn btn-secondary btn-sm"
|
||||||
onClick={() => setShowInvite(!showInvite)}
|
onClick={() => setShowInvite(!showInvite)}
|
||||||
>
|
>
|
||||||
{showInvite ? 'Cancel' : 'Invite Friends'}
|
{showInvite ? 'Cancel' : 'Invite Friends'}
|
||||||
</button>
|
</button>
|
||||||
|
{challenge.challenge.created_by === user.id && (
|
||||||
|
<button
|
||||||
|
className="btn btn-danger btn-sm"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
|
{deleting ? 'Deleting...' : 'Delete Challenge'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user