From f08dab3a549d16bfe5396ad9012a5b2b0e073525 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 2 Aug 2025 02:37:53 -0400 Subject: [PATCH] UI Changes --- app/main/page.tsx | 221 +++++++++++++++++++++++++++++++++++++--------- lib/reasons.ts | 21 +++++ 2 files changed, 198 insertions(+), 44 deletions(-) create mode 100644 lib/reasons.ts diff --git a/app/main/page.tsx b/app/main/page.tsx index 0a2b1f7..3fcb7a0 100644 --- a/app/main/page.tsx +++ b/app/main/page.tsx @@ -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(null); - const [videoUrl, setVideoUrl] = useState(null); // State for the blob URL - const [reason, setReason] = useState(''); + const [videoUrl, setVideoUrl] = useState(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([]); + const [otherReason, setOtherReason] = useState(''); + const [videoTimestamp, setVideoTimestamp] = useState(0); + const [recommendedGameInput, setRecommendedGameInput] = useState(''); // State for editable game name + const videoRef = useRef(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 ( -
- - - Vote on a Video +
+ + + Vote on a Video - + {!video && ( -
- +
+
)} - {error &&

{error}

} - {message &&

{message}

} + {error &&

{error}

} + {message &&

{message}

} - {/* The video player now uses the local blob URL */} {video && videoUrl && ( -
-
+
+
-
-

Person: {video.person}

-

Game: {video.game || 'N/A'}

-
-
- -