This commit is contained in:
2026-01-29 01:49:52 -05:00
parent 31c37d9bdd
commit 3e3f37a570
13 changed files with 365 additions and 57 deletions
+27 -10
View File
@@ -1,7 +1,9 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useParams, Link } from 'react-router-dom';
import toast from 'react-hot-toast';
import { useAuth } from '../AuthContext';
import api from '../api';
import { useClickOutside } from '../hooks/useClickOutside';
export default function ChallengeDetail() {
const { id } = useParams();
@@ -15,6 +17,11 @@ export default function ChallengeDetail() {
const [showInvite, setShowInvite] = useState(false);
const [loading, setLoading] = useState(true);
const [searchTimeout, setSearchTimeout] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [validating, setValidating] = useState(null);
const searchRef = useRef(null);
useClickOutside(searchRef, () => setSearchResults([]));
useEffect(() => {
loadChallenge();
@@ -55,25 +62,33 @@ export default function ChallengeDetail() {
e.preventDefault();
if (!newPrediction.trim()) return;
setSubmitting(true);
try {
await api.createPrediction({
challenge_id: id,
content: newPrediction
});
toast.success('Prediction submitted!');
setNewPrediction('');
await loadPredictions();
} catch (err) {
alert('Failed to create prediction: ' + err.message);
toast.error('Failed to create prediction: ' + err.message);
} finally {
setSubmitting(false);
}
};
const handleValidate = async (predictionId, status) => {
setValidating(predictionId);
try {
await api.validatePrediction(predictionId, status);
toast.success(status === 'validated' ? 'Prediction validated!' : 'Prediction invalidated');
await loadPredictions();
await loadLeaderboard();
} catch (err) {
alert('Failed to validate: ' + err.message);
toast.error('Failed to validate: ' + err.message);
} finally {
setValidating(null);
}
};
@@ -106,11 +121,11 @@ export default function ChallengeDetail() {
const handleInvite = async (userId) => {
try {
await api.inviteToChallenge(id, { user_ids: [userId] });
toast.success('Invitation sent!');
setInviteQuery('');
setSearchResults([]);
alert('Invitation sent!');
} catch (err) {
alert('Failed to send invite: ' + err.message);
toast.error('Failed to send invite: ' + err.message);
}
};
@@ -154,7 +169,7 @@ export default function ChallengeDetail() {
{/* Invite Section */}
{showInvite && (
<div className="card" style={{ marginBottom: '2rem', position: 'relative' }}>
<div className="card" style={{ marginBottom: '2rem', position: 'relative' }} ref={searchRef}>
<h3 style={{ marginBottom: '1rem' }}>Invite Someone</h3>
<input
type="text"
@@ -232,8 +247,8 @@ export default function ChallengeDetail() {
onChange={(e) => setNewPrediction(e.target.value)}
style={{ minHeight: '80px' }}
/>
<button type="submit" className="btn btn-primary" style={{ marginTop: '1rem' }}>
Submit Prediction
<button type="submit" className="btn btn-primary" style={{ marginTop: '1rem' }} disabled={submitting}>
{submitting ? 'Submitting...' : 'Submit Prediction'}
</button>
</form>
</div>
@@ -261,14 +276,16 @@ export default function ChallengeDetail() {
<button
className="btn btn-success btn-sm"
onClick={() => handleValidate(pred.id, 'validated')}
disabled={validating === pred.id}
>
Validate
{validating === pred.id ? '...' : '✓ Validate'}
</button>
<button
className="btn btn-danger btn-sm"
onClick={() => handleValidate(pred.id, 'invalidated')}
disabled={validating === pred.id}
>
Invalidate
{validating === pred.id ? '...' : '✗ Invalidate'}
</button>
</>
)}