mirror of
https://github.com/Qortal/q-tube.git
synced 2025-02-11 17:55:51 +00:00
Refactor to split Home.tsx and VideoContent.tsx into the following components:
1. Home.tsx 2. Home-State.tsx 3. SearchSidebar.tsx 4. SearchSidebar-State.tsx 5. VideoContent.tsx 6. VideoContent-State.tsx By using custom hooks to hold all state data, these components are easier to read and edit.
This commit is contained in:
parent
0ede1b5590
commit
6b00c10598
@ -63,7 +63,7 @@ import {
|
||||
FiltersCheckbox,
|
||||
FiltersRow,
|
||||
FiltersSubContainer,
|
||||
} from "../../../pages/Home/VideoList-styles.tsx";
|
||||
} from "../../../pages/Home/Components/VideoList-styles.tsx";
|
||||
import { FrameExtractor } from "../../common/FrameExtractor/FrameExtractor.tsx";
|
||||
import {
|
||||
QTUBE_PLAYLIST_BASE,
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "../../../pages/ContentPages/VideoContent/VideoContent-functions.ts";
|
||||
} from "../../../pages/ContentPages/VideoContent/VideoContent-State.tsx";
|
||||
import { RootState } from "../../../state/store";
|
||||
import NotificationsIcon from "@mui/icons-material/Notifications";
|
||||
import { formatDate } from "../../../utils/time";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { VideoListComponentLevel } from "../../Home/VideoListComponentLevel.tsx";
|
||||
import { VideoListComponentLevel } from "../../Home/Components/VideoListComponentLevel.tsx";
|
||||
import { HeaderContainer, ProfileContainer } from "./Profile-styles.tsx";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "../VideoContent/VideoContent-functions.ts";
|
||||
} from "../VideoContent/VideoContent-State.tsx";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
FileAttachmentContainer,
|
||||
|
390
src/pages/ContentPages/VideoContent/VideoContent-State.tsx
Normal file
390
src/pages/ContentPages/VideoContent/VideoContent-State.tsx
Normal file
@ -0,0 +1,390 @@
|
||||
import { refType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
|
||||
import {
|
||||
QTUBE_VIDEO_BASE,
|
||||
SUPER_LIKE_BASE,
|
||||
} from "../../../constants/Identifiers.ts";
|
||||
import {
|
||||
minPriceSuperlike,
|
||||
titleFormatterOnSave,
|
||||
} from "../../../constants/Misc.ts";
|
||||
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
|
||||
import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts";
|
||||
import { addToHashMap } from "../../../state/features/videoSlice.ts";
|
||||
import { RootState } from "../../../state/store.ts";
|
||||
import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
export const useVideoContentState = () => {
|
||||
const { name: channelName, id } = useParams();
|
||||
|
||||
const userName = useSelector((state: RootState) => state.auth.user?.name);
|
||||
|
||||
const [isExpandedDescription, setIsExpandedDescription] =
|
||||
useState<boolean>(false);
|
||||
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
|
||||
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
|
||||
|
||||
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
|
||||
const containerRef = useRef<refType>(null);
|
||||
|
||||
const calculateAmountSuperlike = useMemo(() => {
|
||||
const totalQort = superlikeList?.reduce((acc, curr) => {
|
||||
if (curr?.amount && !isNaN(parseFloat(curr.amount)))
|
||||
return acc + parseFloat(curr.amount);
|
||||
else return acc;
|
||||
}, 0);
|
||||
return totalQort?.toFixed(2);
|
||||
}, [superlikeList]);
|
||||
const numberOfSuperlikes = useMemo(() => {
|
||||
return superlikeList?.length ?? 0;
|
||||
}, [superlikeList]);
|
||||
|
||||
const [nameAddress, setNameAddress] = useState<string>("");
|
||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||
null
|
||||
);
|
||||
const [videoData, setVideoData] = useState<any>(null);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState<boolean>(false);
|
||||
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
|
||||
const contentRef = useRef(null);
|
||||
|
||||
const getAddressName = async name => {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
});
|
||||
|
||||
if (response?.owner) {
|
||||
setNameAddress(response.owner);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (channelName) {
|
||||
getAddressName(channelName);
|
||||
}
|
||||
}, [channelName]);
|
||||
const avatarUrl = useMemo(() => {
|
||||
let url = "";
|
||||
if (channelName && userAvatarHash[channelName]) {
|
||||
url = userAvatarHash[channelName];
|
||||
}
|
||||
|
||||
return url;
|
||||
}, [userAvatarHash, channelName]);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const saveAsFilename = useMemo(() => {
|
||||
// nb. we prefer to construct the local filename to use for
|
||||
// saving, from the video "title" when possible
|
||||
if (videoData?.title) {
|
||||
// figure out filename extension
|
||||
let ext = ".mp4";
|
||||
if (videoData?.filename) {
|
||||
// nb. this regex copied from https://stackoverflow.com/a/680982
|
||||
const re = /(?:\.([^.]+))?$/;
|
||||
const match = re.exec(videoData.filename);
|
||||
if (match[1]) {
|
||||
ext = "." + match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return (videoData.title + ext).replace(titleFormatterOnSave, "");
|
||||
}
|
||||
|
||||
// otherwise use QDN filename if applicable
|
||||
if (videoData?.filename) {
|
||||
return videoData.filename.replace(titleFormatterOnSave, "");
|
||||
}
|
||||
|
||||
// TODO: this was the previous value, leaving here as the
|
||||
// fallback for now even though it probably is not needed..?
|
||||
return videoData?.filename || videoData?.title?.slice(0, 20) + ".mp4";
|
||||
}, [videoData]);
|
||||
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.video.hashMapVideos
|
||||
);
|
||||
const videoReference = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoReference } = videoData;
|
||||
if (
|
||||
videoReference?.identifier &&
|
||||
videoReference?.name &&
|
||||
videoReference?.service
|
||||
) {
|
||||
setIsVideoLoaded(true);
|
||||
return videoReference;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
const videoCover = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoImage } = videoData;
|
||||
return videoImage || null;
|
||||
}, [videoData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||
try {
|
||||
if (!name || !id) return;
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0&identifier=${id}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
|
||||
if (responseDataSearch?.length > 0) {
|
||||
let resourceData = responseDataSearch[0];
|
||||
resourceData = {
|
||||
title: resourceData?.metadata?.title,
|
||||
category: resourceData?.metadata?.category,
|
||||
categoryName: resourceData?.metadata?.categoryName,
|
||||
tags: resourceData?.metadata?.tags || [],
|
||||
description: resourceData?.metadata?.description,
|
||||
created: resourceData?.created,
|
||||
updated: resourceData?.updated,
|
||||
user: resourceData.name,
|
||||
videoImage: "",
|
||||
id: resourceData.identifier,
|
||||
};
|
||||
|
||||
const responseData = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "DOCUMENT",
|
||||
identifier: id,
|
||||
});
|
||||
|
||||
if (responseData && !responseData.error) {
|
||||
const combinedData = {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
|
||||
setVideoData(combinedData);
|
||||
dispatch(addToHashMap(combinedData));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (channelName && id) {
|
||||
const existingVideo = hashMapVideos[id + "-" + channelName];
|
||||
|
||||
if (existingVideo) {
|
||||
setVideoData(existingVideo);
|
||||
} else {
|
||||
getVideoData(channelName, id);
|
||||
}
|
||||
}
|
||||
}, [id, channelName]);
|
||||
|
||||
const descriptionThreshold = 200;
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
if (height > descriptionThreshold)
|
||||
setDescriptionHeight(descriptionThreshold);
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
const getComments = useCallback(async (id, nameAddressParam) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setLoadingSuperLikes(true);
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
|
||||
0,
|
||||
39
|
||||
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (
|
||||
comment.identifier &&
|
||||
comment.name &&
|
||||
comment?.metadata?.description
|
||||
) {
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description);
|
||||
if (!result) continue;
|
||||
const res = await getPaymentInfo(result);
|
||||
if (
|
||||
+res?.amount >= minPriceSuperlike &&
|
||||
res.recipient === nameAddressParam &&
|
||||
isTimestampWithinRange(res?.timestamp, comment.created)
|
||||
) {
|
||||
addSuperlikeRawDataGetToList({
|
||||
name: comment.name,
|
||||
identifier: comment.identifier,
|
||||
content: comment,
|
||||
});
|
||||
|
||||
comments = [
|
||||
...comments,
|
||||
{
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount,
|
||||
},
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSuperlikelist(comments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingSuperLikes(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nameAddress || !id) return;
|
||||
getComments(id, nameAddress);
|
||||
}, [getComments, id, nameAddress]);
|
||||
const subList = useSelector(
|
||||
(state: RootState) => state.persist.subscriptionList
|
||||
);
|
||||
|
||||
const focusVideo = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
console.log("in focusVideo");
|
||||
const target = e.target as Element;
|
||||
|
||||
const textTagNames = ["TEXTAREA", "P", "H[1-6]", "STRONG", "svg", "A"];
|
||||
const noText =
|
||||
textTagNames.findIndex(s => {
|
||||
return target?.tagName.match(s);
|
||||
}) < 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const clickOnEmptySpace = !target?.onclick && noText;
|
||||
|
||||
console.log("tagName is: ", target?.tagName);
|
||||
// clicking on link in superlikes bar shows deleted video when loading
|
||||
|
||||
if (target == e.currentTarget || clickOnEmptySpace) {
|
||||
console.log("in correctTarget");
|
||||
const focusRef = containerRef.current?.getContainerRef()?.current;
|
||||
focusRef.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
return {
|
||||
focusVideo,
|
||||
videoReference,
|
||||
channelName,
|
||||
id,
|
||||
videoCover,
|
||||
containerRef,
|
||||
isVideoLoaded,
|
||||
navigate,
|
||||
theme,
|
||||
userName,
|
||||
videoData,
|
||||
numberOfSuperlikes,
|
||||
calculateAmountSuperlike,
|
||||
setSuperlikelist,
|
||||
saveAsFilename,
|
||||
descriptionHeight,
|
||||
isExpandedDescription,
|
||||
setIsExpandedDescription,
|
||||
contentRef,
|
||||
descriptionThreshold,
|
||||
loadingSuperLikes,
|
||||
superlikeList,
|
||||
};
|
||||
};
|
||||
|
||||
export function isTimestampWithinRange(resTimestamp, resCreated) {
|
||||
// Calculate the absolute difference in milliseconds
|
||||
const difference = Math.abs(resTimestamp - resCreated);
|
||||
|
||||
// 2 minutes in milliseconds
|
||||
const twoMinutesInMilliseconds = 3 * 60 * 1000;
|
||||
|
||||
// Check if the difference is within 2 minutes
|
||||
return difference <= twoMinutesInMilliseconds;
|
||||
}
|
||||
|
||||
export function extractSigValue(metadescription) {
|
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) {
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) {
|
||||
const regex = /sig:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription);
|
||||
|
||||
if (relevantSubstring) {
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getPaymentInfo = async (signature: string) => {
|
||||
try {
|
||||
const url = `/transactions/signature/${signature}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
// Coin payment info must be added to responseData so we can display it to the user
|
||||
const responseData = await response.json();
|
||||
if (responseData && !responseData.error) {
|
||||
return responseData;
|
||||
} else {
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
};
|
@ -1,56 +0,0 @@
|
||||
export function isTimestampWithinRange(resTimestamp, resCreated) {
|
||||
// Calculate the absolute difference in milliseconds
|
||||
const difference = Math.abs(resTimestamp - resCreated);
|
||||
|
||||
// 2 minutes in milliseconds
|
||||
const twoMinutesInMilliseconds = 3 * 60 * 1000;
|
||||
|
||||
// Check if the difference is within 2 minutes
|
||||
return difference <= twoMinutesInMilliseconds;
|
||||
}
|
||||
|
||||
export function extractSigValue(metadescription) {
|
||||
// Function to extract the substring within double asterisks
|
||||
function extractSubstring(str) {
|
||||
const match = str.match(/\*\*(.*?)\*\*/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Function to extract the 'sig' value
|
||||
function extractSig(str) {
|
||||
const regex = /sig:(.*?)(;|$)/;
|
||||
const match = str.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
// Extracting the relevant substring
|
||||
const relevantSubstring = extractSubstring(metadescription);
|
||||
|
||||
if (relevantSubstring) {
|
||||
// Extracting the 'sig' value
|
||||
return extractSig(relevantSubstring);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const getPaymentInfo = async (signature: string) => {
|
||||
try {
|
||||
const url = `/transactions/signature/${signature}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
// Coin payment info must be added to responseData so we can display it to the user
|
||||
const responseData = await response.json();
|
||||
if (responseData && !responseData.error) {
|
||||
return responseData;
|
||||
} else {
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error("unable to get payment");
|
||||
}
|
||||
};
|
@ -1,14 +1,7 @@
|
||||
import DownloadIcon from "@mui/icons-material/Download";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import React from "react";
|
||||
|
||||
import DeletedVideo from "../../../assets/img/DeletedVideo.jpg";
|
||||
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
|
||||
import { FollowButton } from "../../../components/common/ContentButtons/FollowButton.tsx";
|
||||
@ -39,7 +32,8 @@ import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "./VideoContent-functions.ts";
|
||||
useVideoContentState,
|
||||
} from "./VideoContent-State.tsx";
|
||||
import {
|
||||
AuthorTextComment,
|
||||
FileAttachmentContainer,
|
||||
@ -54,288 +48,30 @@ import {
|
||||
} from "./VideoContent-styles.tsx";
|
||||
|
||||
export const VideoContent = () => {
|
||||
const { name: channelName, id } = useParams();
|
||||
const userName = useSelector((state: RootState) => state.auth.user?.name);
|
||||
|
||||
const [isExpandedDescription, setIsExpandedDescription] =
|
||||
useState<boolean>(false);
|
||||
const [superlikeList, setSuperlikelist] = useState<any[]>([]);
|
||||
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
|
||||
|
||||
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
|
||||
const containerRef = useRef<refType>(null);
|
||||
|
||||
const calculateAmountSuperlike = useMemo(() => {
|
||||
const totalQort = superlikeList?.reduce((acc, curr) => {
|
||||
if (curr?.amount && !isNaN(parseFloat(curr.amount)))
|
||||
return acc + parseFloat(curr.amount);
|
||||
else return acc;
|
||||
}, 0);
|
||||
return totalQort?.toFixed(2);
|
||||
}, [superlikeList]);
|
||||
const numberOfSuperlikes = useMemo(() => {
|
||||
return superlikeList?.length ?? 0;
|
||||
}, [superlikeList]);
|
||||
|
||||
const [nameAddress, setNameAddress] = useState<string>("");
|
||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||
null
|
||||
);
|
||||
const [videoData, setVideoData] = useState<any>(null);
|
||||
const [isVideoLoaded, setIsVideoLoaded] = useState<boolean>(false);
|
||||
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
|
||||
const contentRef = useRef(null);
|
||||
|
||||
const getAddressName = async name => {
|
||||
const response = await qortalRequest({
|
||||
action: "GET_NAME_DATA",
|
||||
name: name,
|
||||
});
|
||||
|
||||
if (response?.owner) {
|
||||
setNameAddress(response.owner);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (channelName) {
|
||||
getAddressName(channelName);
|
||||
}
|
||||
}, [channelName]);
|
||||
const avatarUrl = useMemo(() => {
|
||||
let url = "";
|
||||
if (channelName && userAvatarHash[channelName]) {
|
||||
url = userAvatarHash[channelName];
|
||||
}
|
||||
|
||||
return url;
|
||||
}, [userAvatarHash, channelName]);
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const saveAsFilename = useMemo(() => {
|
||||
// nb. we prefer to construct the local filename to use for
|
||||
// saving, from the video "title" when possible
|
||||
if (videoData?.title) {
|
||||
// figure out filename extension
|
||||
let ext = ".mp4";
|
||||
if (videoData?.filename) {
|
||||
// nb. this regex copied from https://stackoverflow.com/a/680982
|
||||
const re = /(?:\.([^.]+))?$/;
|
||||
const match = re.exec(videoData.filename);
|
||||
if (match[1]) {
|
||||
ext = "." + match[1];
|
||||
}
|
||||
}
|
||||
|
||||
return (videoData.title + ext).replace(titleFormatterOnSave, "");
|
||||
}
|
||||
|
||||
// otherwise use QDN filename if applicable
|
||||
if (videoData?.filename) {
|
||||
return videoData.filename.replace(titleFormatterOnSave, "");
|
||||
}
|
||||
|
||||
// TODO: this was the previous value, leaving here as the
|
||||
// fallback for now even though it probably is not needed..?
|
||||
return videoData?.filename || videoData?.title?.slice(0, 20) + ".mp4";
|
||||
}, [videoData]);
|
||||
|
||||
const hashMapVideos = useSelector(
|
||||
(state: RootState) => state.video.hashMapVideos
|
||||
);
|
||||
const videoReference = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoReference } = videoData;
|
||||
if (
|
||||
videoReference?.identifier &&
|
||||
videoReference?.name &&
|
||||
videoReference?.service
|
||||
) {
|
||||
setIsVideoLoaded(true);
|
||||
return videoReference;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
const videoCover = useMemo(() => {
|
||||
if (!videoData) return null;
|
||||
const { videoImage } = videoData;
|
||||
return videoImage || null;
|
||||
}, [videoData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||
try {
|
||||
if (!name || !id) return;
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0&identifier=${id}`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseDataSearch = await response.json();
|
||||
|
||||
if (responseDataSearch?.length > 0) {
|
||||
let resourceData = responseDataSearch[0];
|
||||
resourceData = {
|
||||
title: resourceData?.metadata?.title,
|
||||
category: resourceData?.metadata?.category,
|
||||
categoryName: resourceData?.metadata?.categoryName,
|
||||
tags: resourceData?.metadata?.tags || [],
|
||||
description: resourceData?.metadata?.description,
|
||||
created: resourceData?.created,
|
||||
updated: resourceData?.updated,
|
||||
user: resourceData.name,
|
||||
videoImage: "",
|
||||
id: resourceData.identifier,
|
||||
};
|
||||
|
||||
const responseData = await qortalRequest({
|
||||
action: "FETCH_QDN_RESOURCE",
|
||||
name: name,
|
||||
service: "DOCUMENT",
|
||||
identifier: id,
|
||||
});
|
||||
|
||||
if (responseData && !responseData.error) {
|
||||
const combinedData = {
|
||||
...resourceData,
|
||||
...responseData,
|
||||
};
|
||||
|
||||
setVideoData(combinedData);
|
||||
dispatch(addToHashMap(combinedData));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (channelName && id) {
|
||||
const existingVideo = hashMapVideos[id + "-" + channelName];
|
||||
|
||||
if (existingVideo) {
|
||||
setVideoData(existingVideo);
|
||||
} else {
|
||||
getVideoData(channelName, id);
|
||||
}
|
||||
}
|
||||
}, [id, channelName]);
|
||||
|
||||
const descriptionThreshold = 200;
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
if (height > descriptionThreshold)
|
||||
setDescriptionHeight(descriptionThreshold);
|
||||
}
|
||||
}, [videoData]);
|
||||
|
||||
const getComments = useCallback(async (id, nameAddressParam) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setLoadingSuperLikes(true);
|
||||
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${id.slice(
|
||||
0,
|
||||
39
|
||||
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const responseData = await response.json();
|
||||
let comments: any[] = [];
|
||||
for (const comment of responseData) {
|
||||
if (
|
||||
comment.identifier &&
|
||||
comment.name &&
|
||||
comment?.metadata?.description
|
||||
) {
|
||||
try {
|
||||
const result = extractSigValue(comment?.metadata?.description);
|
||||
if (!result) continue;
|
||||
const res = await getPaymentInfo(result);
|
||||
if (
|
||||
+res?.amount >= minPriceSuperlike &&
|
||||
res.recipient === nameAddressParam &&
|
||||
isTimestampWithinRange(res?.timestamp, comment.created)
|
||||
) {
|
||||
addSuperlikeRawDataGetToList({
|
||||
name: comment.name,
|
||||
identifier: comment.identifier,
|
||||
content: comment,
|
||||
});
|
||||
|
||||
comments = [
|
||||
...comments,
|
||||
{
|
||||
...comment,
|
||||
message: "",
|
||||
amount: res.amount,
|
||||
},
|
||||
];
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSuperlikelist(comments);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoadingSuperLikes(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nameAddress || !id) return;
|
||||
getComments(id, nameAddress);
|
||||
}, [getComments, id, nameAddress]);
|
||||
const subList = useSelector(
|
||||
(state: RootState) => state.persist.subscriptionList
|
||||
);
|
||||
|
||||
const focusVideo = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
console.log("in focusVideo");
|
||||
const target = e.target as Element;
|
||||
|
||||
const textTagNames = ["TEXTAREA", "P", "H[1-6]", "STRONG", "svg", "A"];
|
||||
const noText =
|
||||
textTagNames.findIndex(s => {
|
||||
return target?.tagName.match(s);
|
||||
}) < 0;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const clickOnEmptySpace = !target?.onclick && noText;
|
||||
|
||||
console.log("tagName is: ", target?.tagName);
|
||||
// clicking on link in superlikes bar shows deleted video when loading
|
||||
|
||||
if (target == e.currentTarget || clickOnEmptySpace) {
|
||||
console.log("in correctTarget");
|
||||
const focusRef = containerRef.current?.getContainerRef()?.current;
|
||||
focusRef.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
const {
|
||||
focusVideo,
|
||||
videoReference,
|
||||
channelName,
|
||||
id,
|
||||
videoCover,
|
||||
containerRef,
|
||||
isVideoLoaded,
|
||||
navigate,
|
||||
theme,
|
||||
userName,
|
||||
videoData,
|
||||
numberOfSuperlikes,
|
||||
calculateAmountSuperlike,
|
||||
setSuperlikelist,
|
||||
saveAsFilename,
|
||||
descriptionHeight,
|
||||
isExpandedDescription,
|
||||
setIsExpandedDescription,
|
||||
contentRef,
|
||||
descriptionThreshold,
|
||||
loadingSuperLikes,
|
||||
superlikeList,
|
||||
} = useVideoContentState();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,77 +0,0 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import { Box, useTheme } from "@mui/material";
|
||||
import {
|
||||
BottomParent,
|
||||
NameContainer,
|
||||
VideoCard,
|
||||
VideoCardName,
|
||||
VideoCardTitle,
|
||||
VideoContainer,
|
||||
VideoUploadDate,
|
||||
} from "./VideoList-styles";
|
||||
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||
import { formatDate, formatTimestampSeconds } from "../../utils/time";
|
||||
import { ChannelCard, ChannelTitle } from "./Home-styles";
|
||||
|
||||
interface VideoListProps {
|
||||
mode?: string;
|
||||
}
|
||||
export const Channels = ({ mode }: VideoListProps) => {
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const publishNames = useSelector(
|
||||
(state: RootState) => state.global.publishNames
|
||||
);
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
minHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
<VideoContainer>
|
||||
{publishNames &&
|
||||
publishNames?.slice(0, 10).map(name => {
|
||||
let avatarUrl = "";
|
||||
if (userAvatarHash[name]) {
|
||||
avatarUrl = userAvatarHash[name];
|
||||
}
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flex: 0,
|
||||
alignItems: "center",
|
||||
width: "auto",
|
||||
position: "relative",
|
||||
" @media (max-width: 450px)": {
|
||||
width: "100%",
|
||||
},
|
||||
}}
|
||||
key={name}
|
||||
>
|
||||
<ChannelCard
|
||||
onClick={() => {
|
||||
navigate(`/channel/${name}`);
|
||||
}}
|
||||
>
|
||||
<ChannelTitle>{name}</ChannelTitle>
|
||||
<ResponsiveImage src={avatarUrl} width={50} height={50} />
|
||||
</ChannelCard>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</VideoContainer>
|
||||
</Box>
|
||||
);
|
||||
};
|
104
src/pages/Home/Components/SearchSidebar-State.tsx
Normal file
104
src/pages/Home/Components/SearchSidebar-State.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { SelectChangeEvent } from "@mui/material";
|
||||
import { useEffect } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { categories } from "../../../constants/Categories.ts";
|
||||
import { changeFilterType } from "../../../state/features/persistSlice.ts";
|
||||
import {
|
||||
changefilterName,
|
||||
changefilterSearch,
|
||||
changeSelectedCategoryVideos,
|
||||
changeSelectedSubCategoryVideos,
|
||||
} from "../../../state/features/videoSlice.ts";
|
||||
import { RootState } from "../../../state/store.ts";
|
||||
|
||||
export const useSidebarState = (
|
||||
onSearch: (reset?: boolean, resetFilters?: boolean) => void
|
||||
) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
filterSearch,
|
||||
filterName,
|
||||
selectedCategoryVideos,
|
||||
selectedSubCategoryVideos,
|
||||
} = useSelector((state: RootState) => state.video);
|
||||
|
||||
const filterType = useSelector(
|
||||
(state: RootState) => state.persist.filterType
|
||||
);
|
||||
|
||||
const setFilterType = payload => {
|
||||
dispatch(changeFilterType(payload));
|
||||
};
|
||||
|
||||
const setFilterSearch = payload => {
|
||||
dispatch(changefilterSearch(payload));
|
||||
};
|
||||
|
||||
const setFilterName = payload => {
|
||||
dispatch(changefilterName(payload));
|
||||
};
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === "Enter") {
|
||||
onSearch(true);
|
||||
}
|
||||
};
|
||||
|
||||
const filtersToDefault = async () => {
|
||||
setFilterSearch("");
|
||||
setFilterName("");
|
||||
setSelectedCategoryVideos(null);
|
||||
setSelectedSubCategoryVideos(null);
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
onSearch(true, true);
|
||||
});
|
||||
};
|
||||
|
||||
const setSelectedCategoryVideos = payload => {
|
||||
dispatch(changeSelectedCategoryVideos(payload));
|
||||
};
|
||||
|
||||
const setSelectedSubCategoryVideos = payload => {
|
||||
dispatch(changeSelectedSubCategoryVideos(payload));
|
||||
};
|
||||
|
||||
const handleOptionCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>,
|
||||
subcategories: any[]
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Makes displayed videos reload when switching filter type. Removes need to click Search button after changing type
|
||||
onSearch(true);
|
||||
}, [filterType]);
|
||||
|
||||
return {
|
||||
filterSearch,
|
||||
setFilterSearch,
|
||||
handleInputKeyDown,
|
||||
filterName,
|
||||
setFilterName,
|
||||
selectedCategoryVideos,
|
||||
handleOptionCategoryChangeVideos,
|
||||
selectedSubCategoryVideos,
|
||||
handleOptionSubCategoryChangeVideos,
|
||||
filterType,
|
||||
setFilterType,
|
||||
filtersToDefault,
|
||||
};
|
||||
};
|
249
src/pages/Home/Components/SearchSidebar.tsx
Normal file
249
src/pages/Home/Components/SearchSidebar.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
Input,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
} from "@mui/material";
|
||||
import { StatsData } from "../../../components/StatsData.tsx";
|
||||
import { categories, subCategories } from "../../../constants/Categories.ts";
|
||||
import { useSidebarState } from "./SearchSidebar-State.tsx";
|
||||
import {
|
||||
FiltersCol,
|
||||
FiltersContainer,
|
||||
FiltersRadioButton,
|
||||
FiltersRow,
|
||||
FiltersSubContainer,
|
||||
} from "./VideoList-styles.tsx";
|
||||
|
||||
export interface SearchSidebarProps {
|
||||
onSearch: (reset?: boolean, resetFilters?: boolean) => void;
|
||||
}
|
||||
export const SearchSidebar = ({ onSearch }: SearchSidebarProps) => {
|
||||
const {
|
||||
filterSearch,
|
||||
filterName,
|
||||
filterType,
|
||||
setFilterSearch,
|
||||
handleInputKeyDown,
|
||||
setFilterName,
|
||||
selectedCategoryVideos,
|
||||
handleOptionCategoryChangeVideos,
|
||||
selectedSubCategoryVideos,
|
||||
handleOptionSubCategoryChangeVideos,
|
||||
setFilterType,
|
||||
filtersToDefault,
|
||||
} = useSidebarState(onSearch);
|
||||
|
||||
return (
|
||||
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
|
||||
<FiltersContainer>
|
||||
<StatsData />
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={e => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
placeholder="Search"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
sx={{
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value);
|
||||
}}
|
||||
value={filterName}
|
||||
placeholder="User's Name (Exact)"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<FiltersSubContainer>
|
||||
<FormControl sx={{ width: "100%", marginTop: "30px" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<FormControl fullWidth sx={{ marginBottom: 1 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
id="Category"
|
||||
>
|
||||
Category
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Category"
|
||||
input={<OutlinedInput label="Category" />}
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
sx={{
|
||||
// Target the input field
|
||||
".MuiSelect-select": {
|
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;",
|
||||
},
|
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": {
|
||||
fontSize: "20px", // Adjust if needed
|
||||
},
|
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": {
|
||||
".MuiMenuItem-root": {
|
||||
fontSize: "14px", // Change font size for the menu items
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedCategoryVideos &&
|
||||
subCategories[selectedCategoryVideos?.id] && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
id="Sub-Category"
|
||||
>
|
||||
Sub-Category
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Sub-Category" />}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
// Target the input field
|
||||
".MuiSelect-select": {
|
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;",
|
||||
},
|
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": {
|
||||
fontSize: "20px", // Adjust if needed
|
||||
},
|
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": {
|
||||
".MuiMenuItem-root": {
|
||||
fontSize: "14px", // Change font size for the menu items
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
</FormControl>
|
||||
</FiltersSubContainer>
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
Videos
|
||||
<FiltersRadioButton
|
||||
checked={filterType === "videos"}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterType("videos");
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
Playlists
|
||||
<FiltersRadioButton
|
||||
checked={filterType === "playlists"}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterType("playlists");
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
filtersToDefault();
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
reset
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onSearch(true);
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</FiltersContainer>
|
||||
</FiltersCol>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||
import ResponsiveImage from "../../../components/ResponsiveImage.tsx";
|
||||
|
||||
export const VideoCardImageContainer = ({
|
||||
videoImage,
|
@ -21,25 +21,6 @@ export const VideoContainer = styled(Grid)(({ theme }) => ({
|
||||
width: "100%",
|
||||
}));
|
||||
|
||||
// export const VideoCardContainer = styled(Grid)({
|
||||
// display: "flex",
|
||||
// flexWrap: "wrap",
|
||||
// padding: "5px 35px 15px 35px",
|
||||
// });
|
||||
|
||||
// export const VideoCardCol = styled(Grid)(({ theme }) => ({
|
||||
// display: "flex",
|
||||
// gap: 1,
|
||||
// alignItems: "center",
|
||||
// width: "auto",
|
||||
// position: "relative",
|
||||
// maxWidth: "100%",
|
||||
// flexGrow: 1,
|
||||
// [theme.breakpoints.down("sm")]: {
|
||||
// width: "100%",
|
||||
// },
|
||||
// }));
|
||||
|
||||
export const VideoCardContainer = styled("div")(({ theme }) => ({
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(250px, 1fr))",
|
||||
@ -55,19 +36,6 @@ export const VideoCardCol = styled("div")({
|
||||
// ... other styles
|
||||
});
|
||||
|
||||
export const StoresRow = styled(Grid)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
gap: "15px",
|
||||
width: "auto",
|
||||
position: "relative",
|
||||
"@media (max-width: 450px)": {
|
||||
width: "100%",
|
||||
},
|
||||
}));
|
||||
|
||||
export const VideoCard = styled(Grid)(({ theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
@ -96,25 +64,6 @@ export const VideoCard = styled(Grid)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const StoreCardInfo = styled(Grid)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
padding: "5px",
|
||||
marginTop: "15px",
|
||||
}));
|
||||
|
||||
export const VideoImageContainer = styled(Grid)(({ theme }) => ({}));
|
||||
|
||||
export const VideoCardImage = styled("img")(({ theme }) => ({
|
||||
maxWidth: "300px",
|
||||
minWidth: "150px",
|
||||
borderRadius: "5px",
|
||||
height: "150px",
|
||||
objectFit: "fill",
|
||||
width: "266px",
|
||||
}));
|
||||
|
||||
const DoubleLine = styled(Typography)`
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@ -152,43 +101,6 @@ export const BottomParent = styled(Box)(({ theme }) => ({
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
}));
|
||||
export const VideoCardDescription = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Karla",
|
||||
fontSize: "20px",
|
||||
letterSpacing: "0px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const StoreCardOwner = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Livvic",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: "17px",
|
||||
position: "absolute",
|
||||
bottom: "5px",
|
||||
right: "10px",
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const StoreCardYouOwn = styled(Box)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
top: "5px",
|
||||
right: "10px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
fontFamily: "Livvic",
|
||||
fontSize: "15px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const MyStoresRow = styled(Grid)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "flex-end",
|
||||
padding: "5px",
|
||||
width: "100%",
|
||||
}));
|
||||
|
||||
export const NameContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
@ -199,26 +111,6 @@ export const NameContainer = styled(Box)(({ theme }) => ({
|
||||
marginBottom: "2px",
|
||||
}));
|
||||
|
||||
export const MyStoresCard = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "auto",
|
||||
borderRadius: "4px",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: "5px 10px",
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "18px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
||||
|
||||
export const MyStoresCheckbox = styled(Checkbox)(({ theme }) => ({
|
||||
color: "#c0d4ff",
|
||||
"&.Mui-checked": {
|
||||
color: "#6596ff",
|
||||
},
|
||||
}));
|
||||
|
||||
export const ProductManagerRow = styled(Box)(({ theme }) => ({
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr auto",
|
||||
@ -253,17 +145,6 @@ export const FiltersRow = styled(Box)(({ theme }) => ({
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const FiltersTitle = styled(Typography)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
margin: "20px 0",
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "17px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({
|
||||
color: "#c0d4ff",
|
||||
"&.Mui-checked": {
|
||||
@ -278,31 +159,6 @@ export const FiltersRadioButton = styled(Radio)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
export const FilterSelect = styled(Autocomplete)(({ theme }) => ({
|
||||
"& #categories-select": {
|
||||
padding: "7px",
|
||||
},
|
||||
"& .MuiSelect-placeholder": {
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "17px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
},
|
||||
"& MuiFormLabel-root": {
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "17px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
},
|
||||
}));
|
||||
|
||||
export const FilterSelectMenuItems = styled(TextField)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "17px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const FiltersSubContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
@ -4,16 +4,16 @@ import { Avatar, Box, Tooltip, useTheme } from "@mui/material";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG.tsx";
|
||||
import ResponsiveImage from "../../components/ResponsiveImage.tsx";
|
||||
import { PlaylistSVG } from "../../../assets/svgs/PlaylistSVG.tsx";
|
||||
import ResponsiveImage from "../../../components/ResponsiveImage.tsx";
|
||||
import {
|
||||
blockUser,
|
||||
setEditPlaylist,
|
||||
setEditVideo,
|
||||
Video,
|
||||
} from "../../state/features/videoSlice.ts";
|
||||
import { RootState } from "../../state/store.ts";
|
||||
import { formatDate } from "../../utils/time.ts";
|
||||
} from "../../../state/features/videoSlice.ts";
|
||||
import { RootState } from "../../../state/store.ts";
|
||||
import { formatDate } from "../../../utils/time.ts";
|
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
|
||||
import {
|
||||
BlockIconContainer,
|
||||
@ -77,11 +77,6 @@ export const VideoList = ({ videos }: VideoListProps) => {
|
||||
hasHash = true;
|
||||
}
|
||||
|
||||
let avatarUrl = "";
|
||||
if (userAvatarHash[videoObj?.user]) {
|
||||
avatarUrl = userAvatarHash[videoObj?.user];
|
||||
}
|
||||
|
||||
// nb. this prevents showing metadata for a video which
|
||||
// belongs to a different user
|
||||
if (
|
@ -1,10 +1,10 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import { RootState } from "../../../state/store.ts";
|
||||
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
|
||||
import { useFetchVideos } from "../../hooks/useFetchVideos";
|
||||
import LazyLoad from "../../components/common/LazyLoad";
|
||||
import { useFetchVideos } from "../../../hooks/useFetchVideos.tsx";
|
||||
import LazyLoad from "../../../components/common/LazyLoad.tsx";
|
||||
import {
|
||||
BottomParent,
|
||||
NameContainer,
|
||||
@ -16,13 +16,13 @@ import {
|
||||
VideoCardTitle,
|
||||
VideoContainer,
|
||||
VideoUploadDate,
|
||||
} from "./VideoList-styles";
|
||||
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||
import { formatDate, formatTimestampSeconds } from "../../utils/time";
|
||||
import { Video } from "../../state/features/videoSlice";
|
||||
import { queue } from "../../wrappers/GlobalWrapper";
|
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer";
|
||||
import { QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
|
||||
} from "./VideoList-styles.tsx";
|
||||
import ResponsiveImage from "../../../components/ResponsiveImage.tsx";
|
||||
import { formatDate, formatTimestampSeconds } from "../../../utils/time.ts";
|
||||
import { Video } from "../../../state/features/videoSlice.ts";
|
||||
import { queue } from "../../../wrappers/GlobalWrapper.tsx";
|
||||
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
|
||||
import { QTUBE_VIDEO_BASE } from "../../../constants/Identifiers.ts";
|
||||
|
||||
interface VideoListProps {
|
||||
mode?: string;
|
||||
@ -133,7 +133,7 @@ export const VideoListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
>
|
||||
<VideoCardContainer>
|
||||
{videos.map((video: any, index: number) => {
|
||||
const existingVideo = hashMapVideos[video.id + '-' + video.user];
|
||||
const existingVideo = hashMapVideos[video.id + "-" + video.user];
|
||||
let hasHash = false;
|
||||
let videoObj = video;
|
||||
if (existingVideo) {
|
141
src/pages/Home/Home-State.tsx
Normal file
141
src/pages/Home/Home-State.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useFetchVideos } from "../../hooks/useFetchVideos.tsx";
|
||||
import {
|
||||
setHomePageSelectedTab,
|
||||
VideoListType,
|
||||
} from "../../state/features/persistSlice.ts";
|
||||
import { RootState } from "../../state/store";
|
||||
|
||||
export const useHomeState = (mode: string) => {
|
||||
const dispatch = useDispatch();
|
||||
const { filterType, selectedTab } = useSelector(
|
||||
(state: RootState) => state.persist
|
||||
);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [tabValue, setTabValue] = useState<VideoListType>(selectedTab);
|
||||
|
||||
const {
|
||||
filterName,
|
||||
filterSearch,
|
||||
filterValue,
|
||||
selectedCategoryVideos,
|
||||
selectedSubCategoryVideos,
|
||||
filteredVideos,
|
||||
isFiltering,
|
||||
filteredSubscriptionList,
|
||||
videos: globalVideos,
|
||||
} = useSelector((state: RootState) => state.video);
|
||||
|
||||
const isFilterMode = useRef(false);
|
||||
const firstFetch = useRef(false);
|
||||
const afterFetch = useRef(false);
|
||||
const isFetching = useRef(false);
|
||||
|
||||
const { getVideos, getVideosFiltered } = useFetchVideos();
|
||||
|
||||
const videos = isFiltering ? filteredVideos : globalVideos;
|
||||
|
||||
isFilterMode.current = !!isFiltering;
|
||||
|
||||
const changeTab = (e: React.SyntheticEvent, newValue: VideoListType) => {
|
||||
setTabValue(newValue);
|
||||
dispatch(setHomePageSelectedTab(newValue));
|
||||
};
|
||||
|
||||
const getVideosHandlerMount = React.useCallback(async () => {
|
||||
if (firstFetch.current) return;
|
||||
firstFetch.current = true;
|
||||
setIsLoading(true);
|
||||
await getVideos(
|
||||
{
|
||||
name: "",
|
||||
category: "",
|
||||
subcategory: "",
|
||||
keywords: "",
|
||||
contentType: filterType,
|
||||
},
|
||||
null,
|
||||
null,
|
||||
20,
|
||||
tabValue
|
||||
);
|
||||
afterFetch.current = true;
|
||||
isFetching.current = false;
|
||||
|
||||
setIsLoading(false);
|
||||
}, [getVideos]);
|
||||
|
||||
const getVideosHandler = React.useCallback(
|
||||
async (reset?: boolean, resetFilters?: boolean) => {
|
||||
if (!firstFetch.current || !afterFetch.current) return;
|
||||
if (isFetching.current) return;
|
||||
isFetching.current = true;
|
||||
|
||||
await getVideos(
|
||||
{
|
||||
name: filterName,
|
||||
category: selectedCategoryVideos?.id,
|
||||
subcategory: selectedSubCategoryVideos?.id,
|
||||
keywords: filterSearch,
|
||||
contentType: filterType,
|
||||
},
|
||||
reset,
|
||||
resetFilters,
|
||||
20,
|
||||
tabValue
|
||||
);
|
||||
isFetching.current = false;
|
||||
},
|
||||
[
|
||||
getVideos,
|
||||
filterValue,
|
||||
getVideosFiltered,
|
||||
isFiltering,
|
||||
filterName,
|
||||
selectedCategoryVideos,
|
||||
selectedSubCategoryVideos,
|
||||
filterSearch,
|
||||
filterType,
|
||||
tabValue,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
getVideosHandler(true);
|
||||
}, [tabValue]);
|
||||
const prevVal = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
if (isFiltering && filterValue !== prevVal?.current) {
|
||||
prevVal.current = filterValue;
|
||||
|
||||
getVideosHandler();
|
||||
}
|
||||
}, [filterValue, isFiltering, filteredVideos]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!firstFetch.current &&
|
||||
!isFilterMode.current &&
|
||||
globalVideos.length === 0
|
||||
) {
|
||||
isFetching.current = true;
|
||||
getVideosHandlerMount();
|
||||
} else {
|
||||
firstFetch.current = true;
|
||||
afterFetch.current = true;
|
||||
}
|
||||
}, [getVideosHandlerMount, globalVideos]);
|
||||
|
||||
return {
|
||||
tabValue,
|
||||
changeTab,
|
||||
videos,
|
||||
isLoading,
|
||||
filteredSubscriptionList,
|
||||
getVideosHandler,
|
||||
};
|
||||
};
|
@ -1,87 +1,11 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Grid, Typography, Checkbox } from "@mui/material";
|
||||
|
||||
|
||||
export const SubtitleContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: 'center',
|
||||
margin: '10px 0px',
|
||||
width: '100%'
|
||||
alignItems: "center",
|
||||
margin: "10px 0px",
|
||||
width: "100%",
|
||||
}));
|
||||
|
||||
export const Subtitle = styled(Typography)(({ theme }) => ({
|
||||
textAlign: 'center',
|
||||
fontSize: '20px'
|
||||
}));
|
||||
|
||||
const DoubleLine = styled(Typography)`
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const ChannelTitle = styled(DoubleLine)(({ theme }) => ({
|
||||
fontFamily: "Cairo",
|
||||
fontSize: "20px",
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
marginBottom: 'auto',
|
||||
textAlign: 'center'
|
||||
}));
|
||||
export const WelcomeTitle = styled(DoubleLine)(({ theme }) => ({
|
||||
fontFamily: "Cairo",
|
||||
fontSize: "24px",
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
textAlign: 'center'
|
||||
}));
|
||||
|
||||
export const WelcomeContainer = styled(Box)(({ theme }) => ({
|
||||
position: 'fixed',
|
||||
width: '90%',
|
||||
height: '90%',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
zIndex: 500,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}));
|
||||
|
||||
|
||||
export const ChannelCard = styled(Grid)(({ theme }) => ({
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: 'center',
|
||||
height: "auto",
|
||||
width: '300px',
|
||||
minHeight: '130px',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: "8px",
|
||||
padding: "10px 15px",
|
||||
gap: "20px",
|
||||
border:
|
||||
theme.palette.mode === "dark"
|
||||
? "none"
|
||||
: `1px solid ${theme.palette.primary.light}`,
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "0px 8px 10px 1px hsla(0,0%,0%,0.14), 0px 3px 14px 2px hsla(0,0%,0%,0.12), 0px 5px 5px -3px hsla(0,0%,0%,0.2)"
|
||||
: "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;"
|
||||
}
|
||||
}));
|
@ -1,495 +1,36 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
FormControl,
|
||||
Grid,
|
||||
Input,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
OutlinedInput,
|
||||
Select,
|
||||
SelectChangeEvent,
|
||||
Tab,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useFetchVideos } from "../../hooks/useFetchVideos";
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
|
||||
import { Box, Grid, Tab } from "@mui/material";
|
||||
import React from "react";
|
||||
import LazyLoad from "../../components/common/LazyLoad";
|
||||
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
|
||||
import { SearchSidebar } from "./Components/SearchSidebar.tsx";
|
||||
import {
|
||||
FiltersCol,
|
||||
FiltersContainer,
|
||||
FiltersRow,
|
||||
FiltersSubContainer,
|
||||
ProductManagerRow,
|
||||
FiltersRadioButton,
|
||||
} from "./VideoList-styles";
|
||||
} from "./Components/VideoList-styles.tsx";
|
||||
import VideoList from "./Components/VideoList.tsx";
|
||||
import { useHomeState } from "./Home-State.tsx";
|
||||
import { SubtitleContainer } from "./Home-styles";
|
||||
|
||||
import {
|
||||
changeSelectedCategoryVideos,
|
||||
changeSelectedSubCategoryVideos,
|
||||
changefilterName,
|
||||
changefilterSearch,
|
||||
} from "../../state/features/videoSlice";
|
||||
import {
|
||||
changeFilterType,
|
||||
resetSubscriptions,
|
||||
VideoListType,
|
||||
} from "../../state/features/persistSlice.ts";
|
||||
import { categories, subCategories } from "../../constants/Categories.ts";
|
||||
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
|
||||
import { TabContext, TabList, TabPanel } from "@mui/lab";
|
||||
import VideoList from "./VideoList.tsx";
|
||||
import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts";
|
||||
import { StatsData } from "../../components/StatsData.tsx";
|
||||
|
||||
interface HomeProps {
|
||||
mode?: string;
|
||||
}
|
||||
export const Home = ({ mode }: HomeProps) => {
|
||||
const prevVal = useRef("");
|
||||
const isFiltering = useSelector(
|
||||
(state: RootState) => state.video.isFiltering
|
||||
);
|
||||
const filterValue = useSelector(
|
||||
(state: RootState) => state.video.filterValue
|
||||
);
|
||||
const persistReducer = useSelector((state: RootState) => state.persist);
|
||||
const filterType = useSelector(
|
||||
(state: RootState) => state.persist.filterType
|
||||
);
|
||||
const filterSearch = useSelector(
|
||||
(state: RootState) => state.video.filterSearch
|
||||
);
|
||||
const filterName = useSelector((state: RootState) => state.video.filterName);
|
||||
const selectedCategoryVideos = useSelector(
|
||||
(state: RootState) => state.video.selectedCategoryVideos
|
||||
);
|
||||
|
||||
const { videos: globalVideos, filteredSubscriptionList } = useSelector(
|
||||
(state: RootState) => state.video
|
||||
);
|
||||
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [tabValue, setTabValue] = useState<VideoListType>(
|
||||
persistReducer.selectedTab
|
||||
);
|
||||
|
||||
const tabFontSize = "20px";
|
||||
|
||||
const setFilterType = payload => {
|
||||
dispatch(changeFilterType(payload));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Makes displayed videos reload when switching filter type. Removes need to click Search button after changing type
|
||||
getVideosHandler(true);
|
||||
}, [filterType]);
|
||||
const setFilterSearch = payload => {
|
||||
dispatch(changefilterSearch(payload));
|
||||
};
|
||||
|
||||
const setFilterName = payload => {
|
||||
dispatch(changefilterName(payload));
|
||||
};
|
||||
|
||||
const setSelectedCategoryVideos = payload => {
|
||||
dispatch(changeSelectedCategoryVideos(payload));
|
||||
};
|
||||
const selectedSubCategoryVideos = useSelector(
|
||||
(state: RootState) => state.video.selectedSubCategoryVideos
|
||||
);
|
||||
|
||||
const setSelectedSubCategoryVideos = payload => {
|
||||
dispatch(changeSelectedSubCategoryVideos(payload));
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const filteredVideos = useSelector(
|
||||
(state: RootState) => state.video.filteredVideos
|
||||
);
|
||||
|
||||
const isFilterMode = useRef(false);
|
||||
const firstFetch = useRef(false);
|
||||
const afterFetch = useRef(false);
|
||||
const isFetching = useRef(false);
|
||||
|
||||
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } =
|
||||
useFetchVideos();
|
||||
|
||||
const getVideosHandler = React.useCallback(
|
||||
async (reset?: boolean, resetFilters?: boolean) => {
|
||||
if (!firstFetch.current || !afterFetch.current) return;
|
||||
if (isFetching.current) return;
|
||||
isFetching.current = true;
|
||||
|
||||
await getVideos(
|
||||
{
|
||||
name: filterName,
|
||||
category: selectedCategoryVideos?.id,
|
||||
subcategory: selectedSubCategoryVideos?.id,
|
||||
keywords: filterSearch,
|
||||
contentType: filterType,
|
||||
},
|
||||
reset,
|
||||
resetFilters,
|
||||
20,
|
||||
tabValue
|
||||
);
|
||||
isFetching.current = false;
|
||||
},
|
||||
[
|
||||
getVideos,
|
||||
filterValue,
|
||||
getVideosFiltered,
|
||||
isFiltering,
|
||||
filterName,
|
||||
selectedCategoryVideos,
|
||||
selectedSubCategoryVideos,
|
||||
filterSearch,
|
||||
filterType,
|
||||
tabValue,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFiltering && filterValue !== prevVal?.current) {
|
||||
prevVal.current = filterValue;
|
||||
|
||||
getVideosHandler();
|
||||
}
|
||||
}, [filterValue, isFiltering, filteredVideos]);
|
||||
|
||||
const getVideosHandlerMount = React.useCallback(async () => {
|
||||
if (firstFetch.current) return;
|
||||
firstFetch.current = true;
|
||||
setIsLoading(true);
|
||||
await getVideos(
|
||||
{
|
||||
name: "",
|
||||
category: "",
|
||||
subcategory: "",
|
||||
keywords: "",
|
||||
contentType: filterType,
|
||||
},
|
||||
null,
|
||||
null,
|
||||
20,
|
||||
tabValue
|
||||
);
|
||||
afterFetch.current = true;
|
||||
isFetching.current = false;
|
||||
|
||||
setIsLoading(false);
|
||||
}, [getVideos]);
|
||||
|
||||
let videos = globalVideos;
|
||||
|
||||
if (isFiltering) {
|
||||
videos = filteredVideos;
|
||||
isFilterMode.current = true;
|
||||
} else {
|
||||
isFilterMode.current = false;
|
||||
}
|
||||
|
||||
// const interval = useRef<any>(null);
|
||||
|
||||
// const checkNewVideosFunc = useCallback(() => {
|
||||
// let isCalling = false;
|
||||
// interval.current = setInterval(async () => {
|
||||
// if (isCalling || !firstFetch.current) return;
|
||||
// isCalling = true;
|
||||
// await checkNewVideos();
|
||||
// isCalling = false;
|
||||
// }, 30000); // 1 second interval
|
||||
// }, [checkNewVideos]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isFiltering && interval.current) {
|
||||
// clearInterval(interval.current);
|
||||
// return;
|
||||
// }
|
||||
// checkNewVideosFunc();
|
||||
|
||||
// return () => {
|
||||
// if (interval?.current) {
|
||||
// clearInterval(interval.current);
|
||||
// }
|
||||
// };
|
||||
// }, [mode, checkNewVideosFunc, isFiltering]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!firstFetch.current &&
|
||||
!isFilterMode.current &&
|
||||
globalVideos.length === 0
|
||||
) {
|
||||
isFetching.current = true;
|
||||
getVideosHandlerMount();
|
||||
} else {
|
||||
firstFetch.current = true;
|
||||
afterFetch.current = true;
|
||||
}
|
||||
}, [getVideosHandlerMount, globalVideos]);
|
||||
|
||||
const filtersToDefault = async () => {
|
||||
setFilterSearch("");
|
||||
setFilterName("");
|
||||
setSelectedCategoryVideos(null);
|
||||
setSelectedSubCategoryVideos(null);
|
||||
|
||||
ReactDOM.flushSync(() => {
|
||||
getVideosHandler(true, true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleOptionCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = categories.find(option => option.id === +optionId);
|
||||
setSelectedCategoryVideos(selectedOption || null);
|
||||
};
|
||||
const handleOptionSubCategoryChangeVideos = (
|
||||
event: SelectChangeEvent<string>,
|
||||
subcategories: any[]
|
||||
) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = subcategories.find(
|
||||
option => option.id === +optionId
|
||||
);
|
||||
setSelectedSubCategoryVideos(selectedOption || null);
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === "Enter") {
|
||||
getVideosHandler(true);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getVideosHandler(true);
|
||||
}, [tabValue]);
|
||||
|
||||
const changeTab = (e: React.SyntheticEvent, newValue: VideoListType) => {
|
||||
setTabValue(newValue);
|
||||
dispatch(setHomePageSelectedTab(newValue));
|
||||
};
|
||||
const {
|
||||
tabValue,
|
||||
changeTab,
|
||||
videos,
|
||||
isLoading,
|
||||
filteredSubscriptionList,
|
||||
getVideosHandler,
|
||||
} = useHomeState(mode);
|
||||
|
||||
const tabFontSize = "100%";
|
||||
return (
|
||||
<>
|
||||
<Grid container sx={{ width: "100%" }}>
|
||||
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
|
||||
<FiltersContainer>
|
||||
<StatsData />
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={e => {
|
||||
setFilterSearch(e.target.value);
|
||||
}}
|
||||
value={filterSearch}
|
||||
placeholder="Search"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
sx={{
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
onChange={e => {
|
||||
setFilterName(e.target.value);
|
||||
}}
|
||||
value={filterName}
|
||||
placeholder="User's Name (Exact)"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
borderBottom: "1px solid white",
|
||||
"&&:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:after": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&:hover:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused:before": {
|
||||
borderBottom: "none",
|
||||
},
|
||||
"&&.Mui-focused": {
|
||||
outline: "none",
|
||||
},
|
||||
fontSize: "18px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<FiltersSubContainer>
|
||||
<FormControl sx={{ width: "100%", marginTop: "30px" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<FormControl fullWidth sx={{ marginBottom: 1 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
id="Category"
|
||||
>
|
||||
Category
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Category"
|
||||
input={<OutlinedInput label="Category" />}
|
||||
value={selectedCategoryVideos?.id || ""}
|
||||
onChange={handleOptionCategoryChangeVideos}
|
||||
sx={{
|
||||
// Target the input field
|
||||
".MuiSelect-select": {
|
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;",
|
||||
},
|
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": {
|
||||
fontSize: "20px", // Adjust if needed
|
||||
},
|
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": {
|
||||
".MuiMenuItem-root": {
|
||||
fontSize: "14px", // Change font size for the menu items
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{categories.map(option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{selectedCategoryVideos &&
|
||||
subCategories[selectedCategoryVideos?.id] && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel
|
||||
sx={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
id="Sub-Category"
|
||||
>
|
||||
Sub-Category
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="Sub-Category"
|
||||
input={<OutlinedInput label="Sub-Category" />}
|
||||
value={selectedSubCategoryVideos?.id || ""}
|
||||
onChange={e =>
|
||||
handleOptionSubCategoryChangeVideos(
|
||||
e,
|
||||
subCategories[selectedCategoryVideos?.id]
|
||||
)
|
||||
}
|
||||
sx={{
|
||||
// Target the input field
|
||||
".MuiSelect-select": {
|
||||
fontSize: "16px", // Change font size for the selected value
|
||||
padding: "10px 5px 15px 15px;",
|
||||
},
|
||||
// Target the dropdown icon
|
||||
".MuiSelect-icon": {
|
||||
fontSize: "20px", // Adjust if needed
|
||||
},
|
||||
// Target the dropdown menu
|
||||
"& .MuiMenu-paper": {
|
||||
".MuiMenuItem-root": {
|
||||
fontSize: "14px", // Change font size for the menu items
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subCategories[selectedCategoryVideos.id].map(
|
||||
option => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</Box>
|
||||
</FormControl>
|
||||
</FiltersSubContainer>
|
||||
<FiltersSubContainer>
|
||||
<FiltersRow>
|
||||
Videos
|
||||
<FiltersRadioButton
|
||||
checked={filterType === "videos"}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterType("videos");
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
<FiltersRow>
|
||||
Playlists
|
||||
<FiltersRadioButton
|
||||
checked={filterType === "playlists"}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilterType("playlists");
|
||||
}}
|
||||
inputProps={{ "aria-label": "controlled" }}
|
||||
/>
|
||||
</FiltersRow>
|
||||
</FiltersSubContainer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
filtersToDefault();
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
reset
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
getVideosHandler(true);
|
||||
}}
|
||||
sx={{
|
||||
marginTop: "20px",
|
||||
}}
|
||||
variant="contained"
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</FiltersContainer>
|
||||
</FiltersCol>
|
||||
<SearchSidebar onSearch={getVideosHandler} />
|
||||
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
|
||||
<ProductManagerRow>
|
||||
<Box
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
extractSigValue,
|
||||
getPaymentInfo,
|
||||
isTimestampWithinRange,
|
||||
} from "../pages/ContentPages/VideoContent/VideoContent-functions.ts";
|
||||
} from "../pages/ContentPages/VideoContent/VideoContent-State.tsx";
|
||||
|
||||
import { addUser } from "../state/features/authSlice";
|
||||
import NavBar from "../components/layout/Navbar/Navbar";
|
||||
|
Loading…
x
Reference in New Issue
Block a user