3
0
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:
Qortal Dev 2024-10-08 17:27:49 -06:00
parent 0ede1b5590
commit 6b00c10598
18 changed files with 956 additions and 1153 deletions

View File

@ -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,

View File

@ -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";

View File

@ -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,

View File

@ -24,7 +24,7 @@ import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../VideoContent/VideoContent-functions.ts";
} from "../VideoContent/VideoContent-State.tsx";
import {
AuthorTextComment,
FileAttachmentContainer,

View 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");
}
};

View File

@ -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");
}
};

View File

@ -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 (
<>

View File

@ -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>
);
};

View 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,
};
};

View 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>
);
};

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import ResponsiveImage from "../../components/ResponsiveImage";
import ResponsiveImage from "../../../components/ResponsiveImage.tsx";
export const VideoCardImageContainer = ({
videoImage,

View File

@ -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",

View File

@ -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 (

View File

@ -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) {

View 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,
};
};

View File

@ -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;"
}
}));

View File

@ -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

View File

@ -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";