275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { useAuth } from '@/lib/auth';
|
|
import { apiRequest } from '@/lib/api';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Input } from '@/components/ui/input'; // Import Input component
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '@/components/ui/dialog';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Label } from '@/components/ui/label';
|
|
import {APPROVE_REASONS, REJECT_REASONS} from '@/lib/reasons';
|
|
|
|
const VotingPage = () => {
|
|
const [video, setVideo] = useState<any>(null);
|
|
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
|
const [message, setMessage] = useState('');
|
|
const [error, setError] = useState('');
|
|
const [showModal, setShowModal] = useState(false);
|
|
const [currentDecision, setCurrentDecision] = useState<'approve' | 'reject' | null>(null);
|
|
const [selectedReasons, setSelectedReasons] = useState<string[]>([]);
|
|
const [otherReason, setOtherReason] = useState('');
|
|
const [videoTimestamp, setVideoTimestamp] = useState<number>(0);
|
|
const [recommendedGameInput, setRecommendedGameInput] = useState<string>(''); // State for editable game name
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const auth = useAuth();
|
|
|
|
// Effect to fetch video metadata and blob URL
|
|
useEffect(() => {
|
|
if (!video) {
|
|
return;
|
|
}
|
|
|
|
let objectUrl: string;
|
|
|
|
const fetchVideoBlob = async () => {
|
|
try {
|
|
const response = await apiRequest(`/videos/${video.id}/stream`, {
|
|
token: auth.token,
|
|
wantsJson: false
|
|
});
|
|
|
|
const blob = await response.blob();
|
|
objectUrl = URL.createObjectURL(blob);
|
|
setVideoUrl(objectUrl);
|
|
|
|
} catch (err: any) {
|
|
setError(`Failed to load video: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
fetchVideoBlob();
|
|
|
|
// Initialize recommendedGameInput with the current video's game
|
|
setRecommendedGameInput(video.game || '');
|
|
|
|
return () => {
|
|
if (objectUrl) {
|
|
URL.revokeObjectURL(objectUrl);
|
|
}
|
|
};
|
|
}, [video, auth.token]);
|
|
|
|
// Effect to handle video time updates
|
|
useEffect(() => {
|
|
const videoElement = videoRef.current;
|
|
if (videoElement) {
|
|
const handleTimeUpdate = () => {
|
|
setVideoTimestamp(videoElement.currentTime);
|
|
};
|
|
|
|
videoElement.addEventListener('timeupdate', handleTimeUpdate);
|
|
|
|
return () => {
|
|
videoElement.removeEventListener('timeupdate', handleTimeUpdate);
|
|
};
|
|
}
|
|
}, [videoUrl]);
|
|
|
|
const fetchNextVideo = async () => {
|
|
setError('');
|
|
setMessage('');
|
|
setVideo(null);
|
|
setVideoUrl(null);
|
|
setSelectedReasons([]);
|
|
setOtherReason('');
|
|
setCurrentDecision(null);
|
|
setShowModal(false);
|
|
setVideoTimestamp(0);
|
|
setRecommendedGameInput(''); // Reset recommended game input
|
|
try {
|
|
const data = await apiRequest('/videos/vote-next', { token: auth.token });
|
|
setVideo(data);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
}
|
|
};
|
|
|
|
const handleVoteButtonClick = (decision: 'approve' | 'reject') => {
|
|
setCurrentDecision(decision);
|
|
setSelectedReasons([]);
|
|
setOtherReason('');
|
|
setShowModal(true);
|
|
};
|
|
|
|
const handleReasonChange = (reasonOption: string) => {
|
|
setSelectedReasons(prev =>
|
|
prev.includes(reasonOption)
|
|
? prev.filter(r => r !== reasonOption)
|
|
: [...prev, reasonOption]
|
|
);
|
|
};
|
|
|
|
const handleSubmitVote = async () => {
|
|
if (!video || !currentDecision) return;
|
|
setError('');
|
|
setMessage('');
|
|
|
|
const finalReasons = [...selectedReasons];
|
|
if (otherReason.trim()) {
|
|
finalReasons.push(otherReason.trim());
|
|
}
|
|
|
|
// Prepare the body for the API request
|
|
const body: { decision: string; reason: string[] | string; recommended_game?: string; timestamp: number } = {
|
|
decision: currentDecision,
|
|
reason: finalReasons, // Send reasons as an array
|
|
timestamp: videoTimestamp
|
|
};
|
|
|
|
// Add recommended_game only if it has been changed from the original
|
|
if (recommendedGameInput.trim() !== (video.game || '').trim()) {
|
|
body.recommended_game = recommendedGameInput.trim();
|
|
}
|
|
|
|
try {
|
|
await apiRequest(`/videos/${video.id}/vote`, {
|
|
method: 'POST',
|
|
body,
|
|
token: auth.token
|
|
});
|
|
setMessage(`Vote '${currentDecision}' submitted successfully!`);
|
|
setShowModal(false);
|
|
fetchNextVideo();
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
}
|
|
};
|
|
|
|
const reasonsToDisplay = currentDecision === 'approve' ? APPROVE_REASONS : REJECT_REASONS;
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto p-4">
|
|
<Card className="rounded-lg shadow-lg">
|
|
<CardHeader className="bg-gray-50 p-4 rounded-t-lg">
|
|
<CardTitle className="text-2xl font-bold text-gray-800">Vote on a Video</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-6 space-y-6">
|
|
{!video && (
|
|
<div className="text-center py-8">
|
|
<Button
|
|
onClick={fetchNextVideo}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg shadow-md transition duration-300 ease-in-out"
|
|
>
|
|
Get Next Video to Vote On
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{error && <p className="text-center text-red-500 font-medium">{error}</p>}
|
|
{message && <p className="text-center text-green-500 font-medium">{message}</p>}
|
|
|
|
{video && videoUrl && (
|
|
<div className="space-y-6">
|
|
<div className="bg-gray-900 rounded-lg overflow-hidden shadow-xl">
|
|
<video
|
|
ref={videoRef}
|
|
key={video.id}
|
|
className="w-full h-auto max-h-[500px] object-contain"
|
|
controls
|
|
src={videoUrl}
|
|
>
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</div>
|
|
<div className="text-sm text-gray-700 bg-gray-100 p-4 rounded-lg shadow-inner">
|
|
<p><strong className="font-semibold">Person:</strong> {video.person}</p>
|
|
<div className="flex items-center space-x-2">
|
|
<strong className="font-semibold">Game:</strong>
|
|
<Input
|
|
type="text"
|
|
value={recommendedGameInput}
|
|
onChange={(e) => setRecommendedGameInput(e.target.value)}
|
|
placeholder="Enter recommended game"
|
|
className="flex-1 border-none focus:ring-0 focus:outline-none bg-transparent p-0 h-auto text-sm"
|
|
/>
|
|
</div>
|
|
<p><strong className="font-semibold">Current Timestamp:</strong> {videoTimestamp.toFixed(2)} seconds</p>
|
|
</div>
|
|
<div className="flex space-x-4">
|
|
<Button
|
|
className="flex-1 bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg shadow-md transition duration-300 ease-in-out"
|
|
onClick={() => handleVoteButtonClick('approve')}
|
|
>
|
|
Approve 👍
|
|
</Button>
|
|
<Button
|
|
className="flex-1 bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg shadow-md transition duration-300 ease-in-out"
|
|
onClick={() => handleVoteButtonClick('reject')}
|
|
>
|
|
Reject 👎
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Voting Reasons Modal */}
|
|
<Dialog open={showModal} onOpenChange={setShowModal}>
|
|
<DialogContent className="sm:max-w-[425px] p-6 bg-white rounded-lg shadow-xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-xl font-bold text-gray-800">
|
|
Select Reasons for {currentDecision === 'approve' ? 'Approval' : 'Rejection'}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4 max-h-[400px] overflow-y-auto">
|
|
{reasonsToDisplay.map((option) => (
|
|
<div key={option} className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id={option}
|
|
checked={selectedReasons.includes(option)}
|
|
onCheckedChange={() => handleReasonChange(option)}
|
|
className="border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
/>
|
|
<Label htmlFor={option} className="text-gray-700 cursor-pointer">
|
|
{option}
|
|
</Label>
|
|
</div>
|
|
))}
|
|
<div className="space-y-2 mt-4">
|
|
<Label htmlFor="other-reason" className="text-gray-700">Other Reason (Optional)</Label>
|
|
<Textarea
|
|
id="other-reason"
|
|
value={otherReason}
|
|
onChange={(e) => setOtherReason(e.target.value)}
|
|
placeholder="Enter any other reasons here..."
|
|
className="w-full border-gray-300 rounded-md shadow-sm focus:border-blue-500 focus:ring-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter className="pt-4">
|
|
<Button
|
|
type="button"
|
|
onClick={handleSubmitVote}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg shadow-md transition duration-300 ease-in-out"
|
|
>
|
|
Submit Vote
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default VotingPage;
|