UI Changes

This commit is contained in:
Logan Cusano
2025-08-02 02:37:53 -04:00
parent eb3cee1b66
commit f08dab3a54
2 changed files with 198 additions and 44 deletions

View File

@@ -1,35 +1,50 @@
'use client';
import { useState, useEffect } from 'react';
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); // State for the blob URL
const [reason, setReason] = useState('');
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();
// This effect runs when the `video` metadata is fetched
// Effect to fetch video metadata and blob URL
useEffect(() => {
// If there's no video metadata, do nothing.
if (!video) {
return;
}
let objectUrl: string; // To keep track of the URL for cleanup
let objectUrl: string;
const fetchVideoBlob = async () => {
try {
// Fetch the video stream as a raw response, not JSON
const response = await apiRequest(`/videos/${video.id}/stream`, {
token: auth.token,
wantsJson: false // We expect a blob, not JSON
wantsJson: false
});
const blob = await response.blob();
@@ -43,7 +58,9 @@ const VotingPage = () => {
fetchVideoBlob();
// Cleanup function: revoke the object URL to free up memory
// Initialize recommendedGameInput with the current video's game
setRecommendedGameInput(video.game || '');
return () => {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
@@ -51,16 +68,34 @@ const VotingPage = () => {
};
}, [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 () => {
// Reset all states for the new video
setError('');
setMessage('');
setVideo(null);
setVideoUrl(null);
setReason('');
setSelectedReasons([]);
setOtherReason('');
setCurrentDecision(null);
setShowModal(false);
setVideoTimestamp(0);
setRecommendedGameInput(''); // Reset recommended game input
try {
// This just gets the video metadata (id, person, game)
const data = await apiRequest('/videos/vote-next', { token: auth.token });
setVideo(data);
} catch (err: any) {
@@ -68,74 +103,172 @@ const VotingPage = () => {
}
};
const submitVote = async (decision: string) => {
if (!video) return;
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 {
const body = { decision, reason, recommended_game: '' };
await apiRequest(`/videos/${video.id}/vote`, {
method: 'POST',
body,
token: auth.token
});
setMessage(`Vote '${decision}' submitted successfully!`);
// Reset state to prepare for the next video
setVideo(null);
setVideoUrl(null);
setReason('');
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">
<Card>
<CardHeader>
<CardTitle>Vote on a Video</CardTitle>
<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="space-y-6">
<CardContent className="p-6 space-y-6">
{!video && (
<div className="text-center">
<Button onClick={fetchNextVideo}>Get Next Video to Vote On</Button>
<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">{error}</p>}
{message && <p className="text-center text-green-500">{message}</p>}
{error && <p className="text-center text-red-500 font-medium">{error}</p>}
{message && <p className="text-center text-green-500 font-medium">{message}</p>}
{/* The video player now uses the local blob URL */}
{video && videoUrl && (
<div className="space-y-4">
<div>
<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 rounded-lg bg-black"
className="w-full h-auto max-h-[500px] object-contain"
controls
src={videoUrl} // Use the state variable for the src
src={videoUrl}
>
Your browser does not support the video tag.
</video>
</div>
<div className="text-sm text-gray-600">
<p><strong>Person:</strong> {video.person}</p>
<p><strong>Game:</strong> {video.game || 'N/A'}</p>
</div>
<div className="space-y-2">
<label htmlFor="reason">Reason (Optional)</label>
<Textarea id="reason" value={reason} onChange={(e) => setReason(e.target.value)} />
<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="w-full bg-green-600 hover:bg-green-700" onClick={() => submitVote('approve')}>Approve</Button>
<Button className="w-full bg-red-600 hover:bg-red-700" onClick={() => submitVote('reject')}>Reject</Button>
<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;
export default VotingPage;

21
lib/reasons.ts Normal file
View File

@@ -0,0 +1,21 @@
export const APPROVE_REASONS = [
'High quality content',
'Engaging gameplay',
'Good commentary',
'Good production value',
'Clear audio/video',
'Funny',
'Action-packed',
'Great game choice',
'Good for rewatchability',
'Used in existing video'
];
export const REJECT_REASONS = [
'Low quality content',
'Unengaging gameplay',
'Uninspiring',
'Lacks depth',
'Too long/short',
'Racist/whatever-ist',
];