Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions app/client/src/components/admin/CompactVideoCard.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Button, ButtonGroup, Grid, IconButton, InputBase, Typography, Box } from '@mui/material'
import { Button, ButtonGroup, Grid, IconButton, InputBase, Typography, Box, Checkbox } from '@mui/material'
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'
import VisibilityIcon from '@mui/icons-material/Visibility'
import EditIcon from '@mui/icons-material/Edit'
Expand All @@ -15,7 +15,17 @@ const URL = getUrl()
const PURL = getPublicWatchUrl()
const SERVED_BY = getServedBy()

const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, authenticated, deleted }) => {
const CompactVideoCard = ({
video,
openVideoHandler,
alertHandler,
cardWidth,
authenticated,
deleted,
editMode = false,
isSelected = false,
onSelect = () => {},
}) => {
const [intVideo, setIntVideo] = React.useState(video)
const [videoId, setVideoId] = React.useState(video.video_id)
const [title, setTitle] = React.useState(video.info?.title)
Expand Down Expand Up @@ -144,6 +154,10 @@ const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, au
width: '100%',
bgcolor: 'rgba(0, 0, 0, 0)',
lineHeight: 0,
border: isSelected ? '3px solid' : '3px solid transparent',
borderColor: isSelected ? 'primary.main' : 'transparent',
borderRadius: '6px',
transition: 'border 0.3s ease',
}}
>
<ButtonGroup
Expand Down Expand Up @@ -233,11 +247,33 @@ const CompactVideoCard = ({ video, openVideoHandler, alertHandler, cardWidth, au
>
<div
style={{ position: 'relative', cursor: 'pointer' }}
onClick={() => openVideoHandler(video.video_id)}
onClick={() => (editMode ? onSelect(video.video_id) : openVideoHandler(video.video_id))}
onMouseEnter={debouncedMouseEnter}
onMouseLeave={handleMouseLeave}
onMouseDown={handleMouseDown}
>
{/* Checkbox for edit mode */}
{editMode && (
<Checkbox
checked={isSelected}
onChange={(e) => {
e.stopPropagation()
onSelect(video.video_id)
}}
sx={{
position: 'absolute',
top: 8,
left: 8,
zIndex: 2,
color: 'white',
bgcolor: 'rgba(0, 0, 0, 0.5)',
borderRadius: '4px',
'&.Mui-checked': {
color: 'primary.main',
},
}}
/>
)}
{!video.available && (
<Box
sx={{ position: 'absolute', top: 0, left: 0, background: '#FF000060', width: '100%', height: '100%' }}
Expand Down
6 changes: 6 additions & 0 deletions app/client/src/components/admin/VideoCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const VideoCards = ({
fetchVideos,
authenticated,
size,
editMode = false,
selectedVideos = new Set(),
onVideoSelect = () => {},
}) => {
const [vids, setVideos] = React.useState(videos)
const [alert, setAlert] = React.useState({ open: false })
Expand Down Expand Up @@ -165,6 +168,9 @@ const VideoCards = ({
cardWidth={size}
authenticated={authenticated}
deleted={handleDelete}
editMode={editMode}
isSelected={selectedVideos.has(v.video_id)}
onSelect={onVideoSelect}
/>
))}
</Grid>
Expand Down
16 changes: 15 additions & 1 deletion app/client/src/components/admin/VisibilityCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@ import { useIsVisible } from 'react-is-visible'
import { Grid } from '@mui/material'
import CompactVideoCard from './CompactVideoCard'

const VisibilityCard = ({ video, openVideo, handleAlert, cardWidth, authenticated, openDetailsModal, deleted }) => {
const VisibilityCard = ({
video,
openVideo,
handleAlert,
cardWidth,
authenticated,
openDetailsModal,
deleted,
editMode = false,
isSelected = false,
onSelect = () => {},
}) => {
const nodeRef = useRef()
const isVisible = useIsVisible(nodeRef)

Expand All @@ -22,6 +33,9 @@ const VisibilityCard = ({ video, openVideo, handleAlert, cardWidth, authenticate
authenticated={authenticated}
openDetailsModal={openDetailsModal}
deleted={deleted}
editMode={editMode}
isSelected={isSelected}
onSelect={onSelect}
/>
) : (
<div
Expand Down
128 changes: 128 additions & 0 deletions app/client/src/components/game/GameSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react'
import { Autocomplete, TextField, InputAdornment, Box, CircularProgress } from '@mui/material'
import SportsEsportsIcon from '@mui/icons-material/SportsEsports'
import { GameService } from '../../services'

/**
* Reusable game search autocomplete component
* Searches SteamGridDB, creates game if needed, and calls onGameLinked callback
*/
const GameSearch = ({ onGameLinked, onError, disabled = false, placeholder = 'Search for a game...', sx = {} }) => {
const [selectedGame, setSelectedGame] = React.useState(null)
const [gameOptions, setGameOptions] = React.useState([])
const [gameSearchLoading, setGameSearchLoading] = React.useState(false)
const [gameLinkLoading, setGameLinkLoading] = React.useState(false)

const searchGames = async (query) => {
if (!query || query.length < 2) {
setGameOptions([])
return
}
setGameSearchLoading(true)
try {
const results = (await GameService.searchSteamGrid(query)).data || []
setGameOptions(results)
} catch (err) {
setGameOptions([])
}
setGameSearchLoading(false)
}

const handleGameChange = async (_, newValue) => {
if (!newValue) {
setSelectedGame(null)
return
}

setSelectedGame(newValue)
setGameLinkLoading(true)

try {
// Check if game already exists
const allGames = (await GameService.getGames()).data
let game = allGames.find((g) => g.steamgriddb_id === newValue.id)

if (!game) {
// Create the game
const assets = (await GameService.getGameAssets(newValue.id)).data
const gameData = {
steamgriddb_id: newValue.id,
name: newValue.name,
release_date: newValue.release_date
? new Date(newValue.release_date * 1000).toISOString().split('T')[0]
: null,
hero_url: assets.hero_url,
logo_url: assets.logo_url,
icon_url: assets.icon_url,
}
game = (await GameService.createGame(gameData)).data
}

// Call the callback with the game
if (onGameLinked) {
onGameLinked(game)
}

// Reset the autocomplete
setSelectedGame(null)
setGameOptions([])
} catch (err) {
console.error('Error creating/linking game:', err)
if (onError) {
onError(err)
}
setSelectedGame(null)
} finally {
setGameLinkLoading(false)
}
}

return (
<Autocomplete
value={selectedGame}
onChange={handleGameChange}
onInputChange={(_, val) => searchGames(val)}
options={gameOptions}
getOptionLabel={(option) => option.name || ''}
loading={gameSearchLoading}
disabled={disabled || gameLinkLoading}
renderInput={(params) => (
<TextField
{...params}
placeholder={placeholder}
size="small"
sx={{
'& .MuiOutlinedInput-notchedOutline': { border: 'none' },
...sx,
}}
InputProps={{
...params.InputProps,
startAdornment: (
<InputAdornment position="start">
<SportsEsportsIcon sx={{ color: 'rgba(255, 255, 255, 0.7)' }} />
</InputAdornment>
),
endAdornment: (
<>
{gameLinkLoading && (
<InputAdornment position="end">
<CircularProgress size={20} sx={{ mr: 1 }} />
</InputAdornment>
)}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
renderOption={(props, option) => (
<Box component="li" {...props} key={option.id}>
{option.name}
{option.release_date && ` (${new Date(option.release_date * 1000).getFullYear()})`}
</Box>
)}
/>
)
}

export default GameSearch
Loading