3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +00:00

added preview images on hover

This commit is contained in:
PhilReact 2023-12-24 01:50:37 +02:00
parent 5e211737b0
commit 469688b4e0
8 changed files with 387 additions and 158 deletions

View File

@ -1,4 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Compressor from 'compressorjs'
import { import {
AddCoverImageButton, AddCoverImageButton,
AddLogoIcon, AddLogoIcon,
@ -13,6 +15,8 @@ import {
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./Upload-styles"; } from "./Upload-styles";
import { CircularProgress } from "@mui/material";
import { import {
Box, Box,
FormControl, FormControl,
@ -46,6 +50,8 @@ import { QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { toBase64 } from "../UploadVideo/UploadVideo";
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -88,6 +94,8 @@ export const EditVideo = () => {
useState<any>(null); useState<any>(null);
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
useState<any>(null); useState<any>(null);
const [imageExtracts, setImageExtracts] = useState<any>([])
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
@ -205,6 +213,7 @@ export const EditVideo = () => {
setVideoPropertiesToSetToRedux(null); setVideoPropertiesToSetToRedux(null);
setFile(null); setFile(null);
setTitle(""); setTitle("");
setImageExtracts([])
setDescription(""); setDescription("");
setCoverImage(""); setCoverImage("");
}; };
@ -270,6 +279,7 @@ export const EditVideo = () => {
fullDescription, fullDescription,
videoImage: coverImage, videoImage: coverImage,
videoReference: editVideoProperties.videoReference, videoReference: editVideoProperties.videoReference,
extracts: file ? imageExtracts : editVideoProperties?.extracts,
commentsId: editVideoProperties.commentsId, commentsId: editVideoProperties.commentsId,
category, category,
subcategory, subcategory,
@ -346,21 +356,7 @@ export const EditVideo = () => {
} }
} }
const handleOnchange = (index: number, type: string, value: string) => {
// setFiles((prev) => {
// let formattedValue = value
// console.log({type})
// if(type === 'title'){
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
// }
// const copyFiles = [...prev];
// copyFiles[index] = {
// ...copyFiles[index],
// [type]: formattedValue,
// };
// return copyFiles;
// });
};
const handleOptionCategoryChangeVideos = ( const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
@ -380,6 +376,44 @@ export const EditVideo = () => {
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
}; };
const onFramesExtracted = async (imgs)=> {
try {
let imagesExtracts = []
for (const img of imgs){
try {
let compressedFile
const image = img
await new Promise<void>((resolve) => {
new Compressor(image, {
quality: .8,
maxWidth: 750,
mimeType: 'image/webp',
success(result) {
const file = new File([result], 'name', {
type: 'image/webp'
})
compressedFile = file
resolve()
},
error(err) {}
})
})
if (!compressedFile) continue
const base64Img = await toBase64(compressedFile)
imagesExtracts.push(base64Img)
} catch (error) {
console.error(error)
}
}
setImageExtracts(imagesExtracts)
} catch (error) {
}
}
return ( return (
<> <>
<Modal <Modal
@ -466,6 +500,9 @@ export const EditVideo = () => {
</FormControl> </FormControl>
)} )}
</Box> </Box>
{file && (
<FrameExtractor videoFile={file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs)}/>
)}
<React.Fragment> <React.Fragment>
{!coverImage ? ( {!coverImage ? (
<ImageUploader onPick={(img: string) => setCoverImage(img)}> <ImageUploader onPick={(img: string) => setCoverImage(img)}>
@ -548,7 +585,11 @@ export const EditVideo = () => {
onClick={() => { onClick={() => {
publishQDNResource(); publishQDNResource();
}} }}
disabled={file && imageExtracts.length === 0}
> >
{file && imageExtracts.length === 0 && (
<CircularProgress color="secondary" size={14} />
)}
Publish Publish
</CrowdfundActionButton> </CrowdfundActionButton>
</Box> </Box>

View File

@ -44,7 +44,7 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
<Box <Box
sx={{ sx={{
padding: '2px', padding: '2px',
maxHeight: '50%' height: '100%'
}} }}
> >

View File

@ -1,4 +1,5 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import Compressor from 'compressorjs'
import { import {
AddCoverImageButton, AddCoverImageButton,
AddLogoIcon, AddLogoIcon,
@ -13,6 +14,8 @@ import {
StyledButton, StyledButton,
TimesIcon, TimesIcon,
} from "./Upload-styles"; } from "./Upload-styles";
import { CircularProgress } from "@mui/material";
import { import {
Box, Box,
Button, Button,
@ -57,6 +60,17 @@ import { CardContentContainerComment } from "../common/Comments/Comments-styles"
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles"; import { FiltersCheckbox, FiltersRow, FiltersSubContainer } from "../../pages/Home/VideoList-styles";
import { FrameExtractor } from "../common/FrameExtractor/FrameExtractor";
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => resolve(reader.result)
reader.onerror = (error) => {
reject(error)
}
})
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -114,7 +128,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false) const [isCheckTitleByFile, setIsCheckTitleByFile] = useState(false)
const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false) const [isCheckSameCoverImage, setIsCheckSameCoverImage] = useState(false)
const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false) const [isCheckDescriptionIsTitle, setIsCheckDescriptionIsTitle] = useState(false)
const [imageExtracts, setImageExtracts] = useState<any>({})
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"video/*": [], "video/*": [],
@ -219,7 +233,8 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
let listOfPublishes = []; let listOfPublishes = [];
for (const publish of files) { for (let i = 0; i < files.length; i++) {
const publish = files[i]
const title = publish.title; const title = publish.title;
const description = isCheckDescriptionIsTitle ? publish.title : publish.description; const description = isCheckDescriptionIsTitle ? publish.title : publish.description;
const category = selectedCategoryVideos.id; const category = selectedCategoryVideos.id;
@ -271,6 +286,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
identifier: identifier, identifier: identifier,
service: "VIDEO", service: "VIDEO",
}, },
extracts: imageExtracts[i],
commentsId: `${QTUBE_VIDEO_BASE}_cm_${id}`, commentsId: `${QTUBE_VIDEO_BASE}_cm_${id}`,
category, category,
subcategory, subcategory,
@ -539,6 +555,49 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
} }
}; };
const onFramesExtracted = async (imgs, index)=> {
try {
let imagesExtracts = []
for (const img of imgs){
try {
let compressedFile
const image = img
await new Promise<void>((resolve) => {
new Compressor(image, {
quality: .8,
maxWidth: 750,
mimeType: 'image/webp',
success(result) {
const file = new File([result], 'name', {
type: 'image/webp'
})
compressedFile = file
resolve()
},
error(err) {}
})
})
if (!compressedFile) continue
const base64Img = await toBase64(compressedFile)
imagesExtracts.push(base64Img)
} catch (error) {
console.error(error)
}
}
setImageExtracts((prev)=> {
return {
...prev,
[index]: imagesExtracts
}
})
} catch (error) {
}
}
return ( return (
<> <>
{username && ( {username && (
@ -719,6 +778,7 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
{files.map((file, index) => { {files.map((file, index) => {
return ( return (
<React.Fragment key={index}> <React.Fragment key={index}>
<FrameExtractor videoFile={file.file} onFramesExtracted={(imgs)=> onFramesExtracted(imgs, index)}/>
<Typography>{file?.file?.name}</Typography> <Typography>{file?.file?.name}</Typography>
{!isCheckSameCoverImage && ( {!isCheckSameCoverImage && (
<> <>
@ -1126,24 +1186,30 @@ export const UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => {
) : ( ) : (
<CrowdfundActionButton <CrowdfundActionButton
variant="contained" variant="contained"
disabled={files?.length !== Object.keys(imageExtracts)?.length}
onClick={() => { onClick={() => {
next(); next();
}} }}
> >
Next {files?.length !== Object.keys(imageExtracts)?.length ? 'Generating image extracts' : ''}
{files?.length !== Object.keys(imageExtracts)?.length && (
<CircularProgress color="secondary" size={14} />
)}
Next
</CrowdfundActionButton> </CrowdfundActionButton>
)} )}
</Box> </Box>
</CrowdfundActionButtonRow> </CrowdfundActionButtonRow>
</ModalBody> </ModalBody>
</Modal> </Modal>
{isOpenMultiplePublish && ( {isOpenMultiplePublish && (
<MultiplePublish <MultiplePublish
isOpen={isOpenMultiplePublish} isOpen={isOpenMultiplePublish}
onSubmit={() => { onSubmit={() => {
setIsOpenMultiplePublish(false); setIsOpenMultiplePublish(false);
setIsOpen(false); setIsOpen(false);
setImageExtracts({})
setFiles([]); setFiles([]);
setStep("videos"); setStep("videos");
setPlaylistCoverImage(null); setPlaylistCoverImage(null);

View File

@ -0,0 +1,77 @@
import React, { useEffect, useRef, useState, useMemo } from 'react';
export const FrameExtractor = ({ videoFile, onFramesExtracted }) => {
const videoRef = useRef(null);
const [durations, setDurations] = useState([]);
const canvasRef = useRef(null);
useEffect(() => {
const video = videoRef.current;
video.addEventListener('loadedmetadata', () => {
const duration = video.duration;
if (isFinite(duration)) {
// Proceed with your logic
console.log('duration', duration)
const section = duration / 4;
let timestamps = [];
for (let i = 0; i < 4; i++) {
const randomTime = Math.random() * section + i * section;
timestamps.push(randomTime);
}
setDurations(timestamps);
} else {
onFramesExtracted([])
}
});
}, [videoFile]);
console.log({durations})
useEffect(() => {
if (durations.length === 4) {
extractFrames();
}
}, [durations]);
const fileUrl = useMemo(() => {
return URL.createObjectURL(videoFile);
}, [videoFile]);
const extractFrames = async () => {
const video = videoRef.current;
const canvas = canvasRef.current;
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
let frameData = [];
for (const time of durations) {
await new Promise((resolve) => {
video.currentTime = time;
const onSeeked = () => {
context.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => {
frameData.push(blob);
resolve();
}, 'image/png');
video.removeEventListener('seeked', onSeeked);
};
video.addEventListener('seeked', onSeeked, { once: true });
});
}
onFramesExtracted(frameData);
};
return (
<div>
<video ref={videoRef} style={{ display: 'none' }} src={fileUrl}></video>
<canvas ref={canvasRef} style={{ display: 'none' }}></canvas>
</div>
);
};

View File

@ -1,4 +1,4 @@
const useTestIdentifiers = false; const useTestIdentifiers = true;
export const QTUBE_VIDEO_BASE = useTestIdentifiers export const QTUBE_VIDEO_BASE = useTestIdentifiers
? "MYTEST_vid_" ? "MYTEST_vid_"

View File

@ -0,0 +1,42 @@
import React, { useState } from "react";
import ResponsiveImage from "../../components/ResponsiveImage";
export const VideoCardImageContainer = ({
videoImage,
frameImages,
height,
width,
}) => {
const [previewImage, setPreviewImage] = useState(null);
const intervalRef = React.useRef(null);
const startPreview = () => {
let frameIndex = 0;
intervalRef.current = setInterval(() => {
setPreviewImage(frameImages[frameIndex]);
frameIndex = (frameIndex + 1) % frameImages.length;
}, 500); // Change frame every 500 ms
};
const stopPreview = () => {
clearInterval(intervalRef.current);
setPreviewImage(null);
};
return (
<div
style={{
height,
maxWidth: "100%",
}}
onMouseEnter={startPreview}
onMouseLeave={stopPreview}
>
<ResponsiveImage
src={previewImage || videoImage}
width={266}
height={150}
/>
</div>
);
};

View File

@ -64,6 +64,7 @@ import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer"; import { LiskSuperLikeContainer } from "../../components/common/ListSuperLikes/LiskSuperLikeContainer";
import { VideoCardImageContainer } from "./VideoCardImageContainer";
interface VideoListProps { interface VideoListProps {
mode?: string; mode?: string;
@ -696,11 +697,13 @@ export const VideoList = ({ mode }: VideoListProps) => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`); navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}} }}
> >
<ResponsiveImage <VideoCardImageContainer width={266}
height={150} videoImage={videoObj.videoImage} frameImages={videoObj?.extracts || []} />
{/* <ResponsiveImage
src={videoObj.videoImage} src={videoObj.videoImage}
width={266} width={266}
height={150} height={150}
/> /> */}
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer

View File

@ -1,66 +1,68 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from 'react-router-dom' import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
import { useFetchVideos } from "../../hooks/useFetchVideos";
import LazyLoad from "../../components/common/LazyLoad";
import { import {
Avatar, BottomParent,
Box, NameContainer,
Button, ProductManagerRow,
Typography, VideoCard,
useTheme VideoCardCol,
} from '@mui/material' VideoCardContainer,
import { useFetchVideos } from '../../hooks/useFetchVideos' VideoCardName,
import LazyLoad from '../../components/common/LazyLoad' VideoCardTitle,
import { BottomParent, NameContainer, ProductManagerRow, VideoCard, VideoCardCol, VideoCardContainer, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles' VideoContainer,
import ResponsiveImage from '../../components/ResponsiveImage' VideoUploadDate,
import { formatDate, formatTimestampSeconds } from '../../utils/time' } from "./VideoList-styles";
import { Video } from '../../state/features/videoSlice' import ResponsiveImage from "../../components/ResponsiveImage";
import { queue } from '../../wrappers/GlobalWrapper' import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { QTUBE_VIDEO_BASE } from '../../constants' import { Video } from "../../state/features/videoSlice";
import { queue } from "../../wrappers/GlobalWrapper";
import { QTUBE_VIDEO_BASE } from "../../constants";
import { VideoCardImageContainer } from "./VideoCardImageContainer";
interface VideoListProps { interface VideoListProps {
mode?: string mode?: string;
} }
export const VideoListComponentLevel = ({ mode }: VideoListProps) => { export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
const { name: paramName } = useParams() const { name: paramName } = useParams();
const theme = useTheme() const theme = useTheme();
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true);
const firstFetch = useRef(false) const firstFetch = useRef(false);
const afterFetch = useRef(false) const afterFetch = useRef(false);
const hashMapVideos = useSelector( const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.video.hashMapVideos
) );
const countNewVideos = useSelector( const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos (state: RootState) => state.video.countNewVideos
) );
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const [videos, setVideos] = React.useState<Video[]>([]) const [videos, setVideos] = React.useState<Video[]>([]);
const navigate = useNavigate() const navigate = useNavigate();
const { const { getVideo, getNewVideos, checkNewVideos, checkAndUpdateVideo } =
getVideo, useFetchVideos();
getNewVideos,
checkNewVideos,
checkAndUpdateVideo
} = useFetchVideos()
const getVideos = React.useCallback(async () => { const getVideos = React.useCallback(async () => {
try { try {
const offset = videos.length const offset = videos.length;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}_&limit=20&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}` const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}_&limit=20&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
const structureData = responseData.map((video: any): Video => { const structureData = responseData.map((video: any): Video => {
return { return {
title: video?.metadata?.title, title: video?.metadata?.title,
@ -71,128 +73,126 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
created: video?.created, created: video?.created,
updated: video?.updated, updated: video?.updated,
user: video.name, user: video.name,
videoImage: '', videoImage: "",
id: video.identifier id: video.identifier,
} };
}) });
const copiedVideos: Video[] = [...videos] const copiedVideos: Video[] = [...videos];
structureData.forEach((video: Video) => { structureData.forEach((video: Video) => {
const index = videos.findIndex((p) => p.id === video.id) const index = videos.findIndex((p) => p.id === video.id);
if (index !== -1) { if (index !== -1) {
copiedVideos[index] = video copiedVideos[index] = video;
} else { } else {
copiedVideos.push(video) copiedVideos.push(video);
} }
}) });
setVideos(copiedVideos) setVideos(copiedVideos);
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateVideo(content) const res = checkAndUpdateVideo(content);
if (res) { if (res) {
queue.push(() => getVideo(content.user, content.id, content)); queue.push(() => getVideo(content.user, content.id, content));
} }
} }
} }
} catch (error) { } catch (error) {
} finally { } finally {
} }
}, [videos, hashMapVideos]) }, [videos, hashMapVideos]);
const getVideosHandler = React.useCallback(async () => { const getVideosHandler = React.useCallback(async () => {
if(!firstFetch.current || !afterFetch.current) return if (!firstFetch.current || !afterFetch.current) return;
await getVideos() await getVideos();
}, [getVideos]) }, [getVideos]);
const getVideosHandlerMount = React.useCallback(async () => { const getVideosHandlerMount = React.useCallback(async () => {
if(firstFetch.current) return if (firstFetch.current) return;
firstFetch.current = true firstFetch.current = true;
await getVideos() await getVideos();
afterFetch.current = true afterFetch.current = true;
setIsLoading(false) setIsLoading(false);
}, [getVideos]) }, [getVideos]);
useEffect(() => {
if (!firstFetch.current) {
getVideosHandlerMount();
useEffect(()=> {
if(!firstFetch.current){
getVideosHandlerMount()
} }
}, [getVideosHandlerMount]);
}, [getVideosHandlerMount ])
return ( return (
<ProductManagerRow> <ProductManagerRow>
<Box sx={{ <Box
width: '100%', sx={{
display: 'flex', width: "100%",
flexDirection: 'column', display: "flex",
alignItems: 'center' flexDirection: "column",
}}> alignItems: "center",
}}
<VideoCardContainer> >
{videos.map((video: any, index: number) => { <VideoCardContainer>
const existingVideo = hashMapVideos[video.id] {videos.map((video: any, index: number) => {
let hasHash = false const existingVideo = hashMapVideos[video.id];
let videoObj = video let hasHash = false;
if (existingVideo) { let videoObj = video;
videoObj = existingVideo if (existingVideo) {
hasHash = true videoObj = existingVideo;
} hasHash = true;
}
let avatarUrl = '' let avatarUrl = "";
if(userAvatarHash[videoObj?.user]){ if (userAvatarHash[videoObj?.user]) {
avatarUrl = userAvatarHash[videoObj?.user] avatarUrl = userAvatarHash[videoObj?.user];
} }
if(hasHash && (!videoObj?.videoImage || videoObj?.videoImage?.length < 50)){ if (
return null hasHash &&
} (!videoObj?.videoImage || videoObj?.videoImage?.length < 50)
) {
return null;
}
return (
return ( <VideoCardCol key={videoObj.id}>
<VideoCardCol
key={videoObj.id}
>
<VideoCard <VideoCard
onClick={() => { onClick={() => {
navigate(`/video/${videoObj.user}/${videoObj.id}`) navigate(`/video/${videoObj.user}/${videoObj.id}`);
}} }}
> >
<ResponsiveImage src={videoObj.videoImage} width={266} height={150}/> <VideoCardImageContainer
width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer> <NameContainer>
<Avatar sx={{height: 24, width: 24}} src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} alt={`${videoObj.user}'s avatar`} /> <Avatar
<VideoCardName>{videoObj.user}</VideoCardName> sx={{ height: 24, width: 24 }}
</NameContainer> src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj.user}'s avatar`}
{videoObj?.created && ( />
<VideoUploadDate>{formatDate(videoObj.created)}</VideoUploadDate> <VideoCardName>{videoObj.user}</VideoCardName>
)} </NameContainer>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</BottomParent> </BottomParent>
</VideoCard> </VideoCard>
</VideoCardCol>
);
</VideoCardCol> })}
)
})}
</VideoCardContainer> </VideoCardContainer>
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad> <LazyLoad
</Box> onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</ProductManagerRow> </ProductManagerRow>
) );
} };