diff --git a/app/styles/Tagging.module.css b/app/styles/Tagging.module.css index a0deb69..e78a017 100644 --- a/app/styles/Tagging.module.css +++ b/app/styles/Tagging.module.css @@ -1,7 +1,265 @@ +/* Tagging.module.css */ + .container { - padding: 10px; + padding: 15px; /* Reduced padding */ + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; +} + +/* Minimalistic Video ID Input Line */ +.videoIdLine { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +.videoIdLine input { + flex: 1.5; + padding: 6px 10px; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-size: 14px; +} + +.videoIdLine button { + padding: 6px 12px; + border: none; + border-radius: 4px; + background-color: #0070f3; + color: #ffffff; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.videoIdLine button:hover { + background-color: #005bb5; +} + +.topSection { + display: flex; + flex-direction: row; + gap: 20px; + align-items: flex-start; +} + +.videoSection { + flex: 4; /* 60% of the container's width (assuming total flex is 5) */ + display: flex; + flex-direction: column; + gap: 10px; /* Space between elements */ +} + +.videoPlayer { + width: 100%; + aspect-ratio: 16 / 9; /* Maintains aspect ratio */ + border-radius: 8px; + overflow: hidden; +} + +.timestampTableContainer { + flex: 1; /* Matches the height of the video player */ + overflow-y: auto; /* Adds scrollbar if content overflows */ +} + +.timestampTable { + width: 100%; + border-collapse: collapse; +} + +.timestampTable th, +.timestampTable td { + padding: 8px; + border: 1px solid #dddddd; + text-align: center; + font-size: 14px; +} + +.timestampTable th { + background-color: #e0e0e0; + font-weight: 600; +} + +.inputGroup { + margin-top: 15px; + display: flex; + flex-direction: column; +} + +.inputGroup label { + margin-bottom: 5px; + font-weight: 600; + color: #333333; +} + +.inputGroup input { + padding: 8px 12px; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-size: 14px; +} + +.buttonsGroup { + margin-top: 15px; + display: flex; + gap: 10px; +} + +.buttonsGroup button { + padding: 10px 16px; + border: none; + border-radius: 4px; + background-color: #0070f3; + color: #ffffff; + font-size: 14px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.buttonsGroup button:hover { + background-color: #005bb5; } -.table { +.courtSection { + flex: 2; /* 40% of the container's width (assuming total flex is 5) */ display: flex; + align-items: center; + justify-content: center; + padding: 10px; /* Smaller padding */ +} + +.courtImage { + width: 100%; + height: 100%; + max-height: 100%; + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s ease; +} + +.courtImage:hover { + transform: scale(1.05); +} + +.controlsSection { + flex: 2; /* Adjust as needed */ + display: flex; + flex-direction: column; + gap: 3px; +} + +.timerTable { + width: 100%; /* Full width of controlsSection */ + margin-top: 20px; + border-collapse: collapse; +} + +.timerTable td { + padding: 6px; /* Smaller padding */ + border: 1px solid #dddddd; + font-size: 14px; +} + +.inputField { + width: 100%; + padding: 6px 10px; + border: 1px solid #e0e0e0; + border-radius: 4px; + font-size: 14px; +} + +.keybindingsTable { + width: 100%; + border-collapse: collapse; +} + +.keybindingsTable th, +.keybindingsTable td { + padding: 8px; + border: 1px solid #dddddd; + text-align: left; + font-size: 14px; +} + +.keybindingsTable th { + background-color: #e0e0e0; + font-weight: 600; +} + +.clickedInfo { + padding: 12px; + border: 1px solid #dddddd; + border-radius: 6px; + background-color: #fafafa; +} + +.clickedInfo h3 { + margin-bottom: 10px; + font-size: 16px; + color: #333333; +} + +.clickedInfo p { + margin: 5px 0; + font-size: 14px; + color: #555555; +} + +.customHr { + border: none; + height: 2px; + background-color: #e0e0e0; + margin: 30px 0; +} + +.deleteButton { + padding: 6px 10px; + background-color: #e74c3c; + border: none; + border-radius: 4px; + color: #ffffff; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.deleteButton:hover { + background-color: #c0392b; +} + +@media (max-width: 1200px) { + .topSection { + flex-direction: column; + align-items: center; + } + + .courtSection { + flex: 1; + width: 100%; + } + + .videoSection, + .controlsSection { + width: 100%; + } + + .timestampTableContainer { + height: 200px; /* Adjust as needed for smaller screens */ + } + + .videoIdLine input { + width: 80%; + } +} + +@media (max-width: 768px) { + .buttonsGroup { + flex-direction: column; + } + + .buttonsGroup button { + width: 100%; + } } diff --git a/app/timestamp-tagger/page.js b/app/timestamp-tagger/page.js index f0d3706..0785961 100644 --- a/app/timestamp-tagger/page.js +++ b/app/timestamp-tagger/page.js @@ -2,9 +2,11 @@ import React, { useState, useEffect, useRef, Suspense } from 'react' import { useSearchParams } from 'next/navigation' -import VideoPlayer from '@/app/components/VideoPlayer' +import VideoPlayer from '../components/VideoPlayer.js' +import TennisCourtSVG from '@/public/TennisCourtSVG.js' import styles from '@/app/styles/Tagging.module.css' +// TagTable Component remains unchanged const TagTable = ({ pair, index, @@ -21,6 +23,8 @@ const TagTable = ({ type="text" value={pair[0]} onChange={(event) => handleStartTimeChange(index, event.target.value)} + className={styles.inputField} + placeholder="Start Time (ms)" /> @@ -28,6 +32,8 @@ const TagTable = ({ type="text" value={pair[1]} onChange={(event) => handleEndTimeChange(index, event.target.value)} + className={styles.inputField} + placeholder="End Time (ms)" /> @@ -35,14 +41,17 @@ const TagTable = ({ type="text" value={pair[2]} onChange={(event) => handlePlayerWonChange(index, event.target.value)} + className={styles.inputField} + placeholder="Point Winner" /> @@ -51,39 +60,35 @@ const TagTable = ({ const KeybindingsTable = () => { return ( - +
- - + + - + - + - + - + - + @@ -104,16 +109,35 @@ export default function TagMatch() { const FRAMERATE = 30 const inputRef = useRef(null) + // State variables for clicked point + const [clickedQuadrant, setClickedQuadrant] = useState(null) + const [clickedCoordinates, setClickedCoordinates] = useState(null) + const searchParams = useSearchParams() useEffect(() => { - setVideoId(searchParams.get('videoId')) - }, []) + const initialVideoId = searchParams.get('videoId') || '' + setVideoId(initialVideoId) + }, [searchParams]) const handleVideoIdChange = (event) => { setVideoId(event.target.value) } + const handleLoadVideo = () => { + if (videoId.trim() === '') { + alert('Please enter a valid YouTube Video ID.') + return + } + // Optionally, you can update the URL's search params here + // or handle any additional logic when loading a new video + // For example, you might want to reset timeList and timerValue + setTimeList([]) + setTimerValue(0) + setCurTimeStart(0) + // If using URL search params, you might need to update them + } + const handleKeyDown = (event) => { if (!videoObject) return @@ -124,6 +148,8 @@ export default function TagMatch() { return } + const key = event.key.toLowerCase() + const keyActions = { ' ': () => { const playing = videoObject.getPlayerState() === 1 @@ -132,18 +158,16 @@ export default function TagMatch() { d: () => { const newTimestamp = Math.round(videoObject.getCurrentTime() * 1000) if (!timeList.some((pair) => pair[1] === 0)) { - setTimeList((timeList) => - [...timeList, [newTimestamp, 0, '']].sort( - (pair1, pair2) => pair1[0] - pair2[0] - ) + setTimeList((prevList) => + [...prevList, [newTimestamp, 0, '']].sort((a, b) => a[0] - b[0]) ) setCurTimeStart(newTimestamp) } }, f: () => { const newTimestamp = Math.round(videoObject.getCurrentTime() * 1000) - setTimeList((timeList) => - timeList.map((pair) => + setTimeList((prevList) => + prevList.map((pair) => pair[1] === 0 && newTimestamp >= pair[0] ? [pair[0], newTimestamp, 'Player 1'] : pair @@ -152,8 +176,8 @@ export default function TagMatch() { }, g: () => { const newTimestamp = Math.round(videoObject.getCurrentTime() * 1000) - setTimeList((timeList) => - timeList.map((pair) => + setTimeList((prevList) => + prevList.map((pair) => pair[1] === 0 && newTimestamp >= pair[0] ? [pair[0], newTimestamp, 'Player 2'] : pair @@ -168,41 +192,29 @@ export default function TagMatch() { q: () => videoObject.seekTo(videoObject.getCurrentTime() - 5, true), s: () => videoObject.seekTo(videoObject.getCurrentTime() + 10, true), a: () => videoObject.seekTo(videoObject.getCurrentTime() - 10, true), - 2: () => videoObject.setPlaybackRate(2), - 1: () => videoObject.setPlaybackRate(1) + 1: () => videoObject.setPlaybackRate(1), + 2: () => videoObject.setPlaybackRate(2) } - const action = keyActions[event.key] + const action = keyActions[key] if (action) action() } const handleStartTimeChange = (index, value) => { const updatedTimeList = [...timeList] - updatedTimeList[index] = [ - parseInt(value), - updatedTimeList[index][1], - updatedTimeList[index][2] - ] + updatedTimeList[index][0] = parseInt(value) || 0 setTimeList(updatedTimeList) } const handleEndTimeChange = (index, value) => { const updatedTimeList = [...timeList] - updatedTimeList[index] = [ - updatedTimeList[index][0], - parseInt(value), - updatedTimeList[index][2] - ] + updatedTimeList[index][1] = parseInt(value) || 0 setTimeList(updatedTimeList) } const handlePlayerWonChange = (index, value) => { const updatedTimeList = [...timeList] - updatedTimeList[index] = [ - updatedTimeList[index][0], - updatedTimeList[index][1], - value - ] + updatedTimeList[index][2] = value setTimeList(updatedTimeList) } @@ -220,11 +232,13 @@ export default function TagMatch() { const handleMillisecondsChange = (value) => { const milliseconds = parseInt(value) - videoObject.seekTo(milliseconds / 1000, true) + if (!isNaN(milliseconds)) { + videoObject.seekTo(milliseconds / 1000, true) + } } const handleRemoveTime = (index) => { - const updatedTimeList = [...timeList].filter((item, i) => i !== index) + const updatedTimeList = [...timeList].filter((_, i) => i !== index) setTimeList(updatedTimeList) } @@ -246,8 +260,10 @@ export default function TagMatch() { const handleCopyColumns = () => { const columns = [ - 'Index,Start Time,End Time', - ...timeList.map((pair, index) => `${index + 1},${pair[0]},${pair[1]}`) + 'Index,Start Time,End Time,Point Winner', + ...timeList.map( + (pair, index) => `${index + 1},${pair[0]},${pair[1]},${pair[2]}` + ) ].join('\n') navigator.clipboard.writeText(columns) } @@ -276,39 +292,151 @@ export default function TagMatch() { } }, [videoObject]) + // Updated handleCourtClick function remains unchanged + const handleCourtClick = (event) => { + if (!videoObject) return + + const rect = event.currentTarget.getBoundingClientRect() + const widthOfCourt = rect.width + const heightOfCourt = rect.height + + // Assuming the court width in inches is 432 (36 feet) + const courtWidthInInches = 432 + const courtHeightInInches = 780 // 65 feet (common tennis court length) + + // Calculate the scale ratios + const xRatio = courtWidthInInches / widthOfCourt + const yRatio = courtHeightInInches / heightOfCourt + + // Calculate click position relative to the SVG container + const x = event.clientX - rect.left + const y = event.clientY - rect.top + + // Convert to inches + const xInches = Math.round(x * xRatio) + const yInches = Math.round(y * yRatio) + + // Determine the quadrant + const midX = courtWidthInInches / 2 + const midY = courtHeightInInches / 2 + let quadrant = '' + + if (xInches < midX && yInches < midY) quadrant = 'Top-Left' + else if (xInches >= midX && yInches < midY) quadrant = 'Top-Right' + else if (xInches < midX && yInches >= midY) quadrant = 'Bottom-Left' + else if (xInches >= midX && yInches >= midY) quadrant = 'Bottom-Right' + + // Update the state to display in the separate box + setClickedQuadrant(quadrant) + setClickedCoordinates({ x: xInches, y: yInches }) + + // Removed adding coordinates to timeList + } + return ( Loading...}>
-
-
-
+
+ {/* Video Player and Timestamp Table */} +
+
- - + + +
+ + {/* Timestamp Table */} +
+
- Key - - Action - KeyAction
[space][Space] Pause/Play
[d] [f] [g][D] [F] [G] Start Timestamp | End Timestamp, Player 1 Won | End Timestamp, Player 2 Won
[r] [e][R] [E] Forward | Backward 1s
[w] [q][W] [Q] Forward | Backward 5s
[s] [a][S] [A] Forward | Backward 10s
+ + + + + + + + + + + + + + {timeList.length !== 0 && + timeList.map((pair, index) => { + if (curTimeStart === pair[0]) { + return ( + + ) + } else return null + })} + + + + + + {timeList.map((pair, index) => ( + + ))} + +
IndexStart Time (ms)End Time (ms)Point WinnerRemove
+ Current Timestamp +
+ All Timestamps +
+ + + + {/* Tennis Court SVG */} +
+ - -
-
- + + {/* Controls Section */} +
+
- + + - + + @@ -331,11 +459,11 @@ export default function TagMatch() { placeholder="Minutes" value={Math.floor(timerValue / 60000)} onChange={(event) => { - const minutes = parseFloat(event.target.value) + const minutes = parseFloat(event.target.value) || 0 const seconds = (timerValue % 60000) / 1000 handleMinutesSecondsChange(minutes, seconds) }} - style={{ marginRight: '10px' }} + className={styles.inputField} /> @@ -345,69 +473,47 @@ export default function TagMatch() { placeholder="Seconds" value={Math.round((timerValue % 60000) / 1000)} onChange={(event) => { - const seconds = parseFloat(event.target.value) + const seconds = parseFloat(event.target.value) || 0 const minutes = Math.floor(timerValue / 60000) handleMinutesSecondsChange(minutes, seconds) }} + className={styles.inputField} />
Current Time: {timerValue}ms + Current Time: + {timerValue} ms
Jump to: + Jump to: +
@@ -319,7 +447,7 @@ export default function TagMatch() { onChange={(event) => handleMillisecondsChange(event.target.value) } - style={{ marginRight: '10px' }} + className={styles.inputField} /> ms minutes seconds
+ + + {/* Display Clicked Quadrant and Coordinates Under KeybindingsTable */} +
+ {clickedQuadrant && clickedCoordinates ? ( +
+

Clicked Point Details

+

+ Quadrant: {clickedQuadrant} +

+

+ Coordinates: (X: {clickedCoordinates.x}{' '} + inches, Y: {clickedCoordinates.y} inches) +

+
+ ) : ( +
+

Clicked Point Details

+

Click on the court to see details here.

+
+ )} +
-
- - - - - - - - - - - - - - - {timeList.length !== 0 && - timeList.map((pair, index) => { - if (curTimeStart === pair[0]) { - return ( - - ) - } else return null - })} - - - - - - {timeList.map((pair, index) => ( - - ))} - -
IndexStart TimeEnd TimePoint WinnerRemove
Current Timestamp
All Timestamps
+ {/* Horizontal Line */} +
+ + {/* Note: Timestamp Table has been moved inside videoSection */} )