Upload files to "src/pages/Home"

This commit is contained in:
2025-07-24 16:49:30 +00:00
commit 041727aee0
2 changed files with 596 additions and 0 deletions

352
src/pages/Home/Home.tsx Normal file
View File

@@ -0,0 +1,352 @@
import { Box, Grid, Input, useTheme } from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import {
AutocompleteQappNames,
getPublishedQappNames,
QappNamesRef,
} from "../../components/common/AutocompleteQappNames.tsx";
import {
CategoryList,
CategoryListRef,
getCategoriesFetchString,
} from "../../components/common/CategoryList/CategoryList.tsx";
import {
CategorySelect,
CategorySelectRef,
} from "../../components/common/CategoryList/CategorySelect.tsx";
import LazyLoad from "../../components/common/LazyLoad";
import { StatsData } from "../../components/StatsData.tsx";
import {
allCategories,
allCategoryData,
} from "../../constants/Categories/Categories.ts";
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
import {
changefilterName,
changefilterSearch,
changeFilterType,
setQappNames,
} from "../../state/features/fileSlice.ts";
import { RootState } from "../../state/store";
import { SubtitleContainer, ThemeButton } from "./Home-styles";
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
import { IssueList } from "./IssueList.tsx";
interface HomeProps {
mode?: string;
}
export const Home = ({ mode }: HomeProps) => {
const theme = useTheme();
const isFiltering = useSelector((state: RootState) => state.file.isFiltering);
const filterValue = useSelector((state: RootState) => state.file.filterValue);
const [isLoading, setIsLoading] = useState<boolean>(false);
const filterType = useSelector((state: RootState) => state.file.filterType);
const setFilterType = payload => {
dispatch(changeFilterType(payload));
};
const filterSearch = useSelector(
(state: RootState) => state.file.filterSearch
);
const QappNames = useSelector(
(state: RootState) => state.file.publishedQappNames
);
const autocompleteRef = useRef<QappNamesRef>(null);
const setFilterSearch = payload => {
dispatch(changefilterSearch(payload));
};
const filterName = useSelector((state: RootState) => state.file.filterName);
const setFilterName = payload => {
dispatch(changefilterName(payload));
};
const isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetching = useRef(false);
const prevVal = useRef("");
const categoryListRef = useRef<CategoryListRef>(null);
const categorySelectRef = useRef<CategorySelectRef>(null);
const [showCategoryList, setShowCategoryList] = useState<boolean>(true);
const [showCategorySelect, setShowCategorySelect] = useState<boolean>(true);
const { files: globalVideos } = useSelector((state: RootState) => state.file);
const dispatch = useDispatch();
const filteredFiles = useSelector(
(state: RootState) => state.file.filteredFiles
);
const [QappNamesParam, setQappNamesParam] = useState<string[]>([]);
useEffect(() => {
getPublishedQappNames().then(QappNamesResult => {
dispatch(setQappNames(QappNamesResult));
setQappNamesParam(QappNamesResult);
});
}, []);
const {
getIssues,
checkAndUpdateIssue,
getIssue,
hashMapFiles,
getNewIssues,
checkNewIssues,
getIssuesFiltered,
getIssuesCount,
} = useFetchIssues();
const getIssuesHandler = React.useCallback(
async (reset?: boolean, resetFilters?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
const selectedCategories =
categoryListRef.current?.getSelectedCategories() || [];
const issueType = categorySelectRef?.current?.getSelectedCategory();
let categoriesString = getCategoriesFetchString(selectedCategories);
if (issueType) categoriesString = ":" + issueType + ";";
await getIssues(
{
name: filterName,
categories: categoriesString,
QappName: autocompleteRef?.current?.getQappNameFetchString(),
keywords: filterSearch,
type: filterType,
},
reset,
resetFilters
);
isFetching.current = false;
},
[
getIssues,
filterValue,
getIssuesFiltered,
isFiltering,
filterName,
filterSearch,
filterType,
]
);
const searchOnEnter = e => {
if (e.keyCode == 13) {
getIssuesHandler(true);
}
};
useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue;
getIssuesHandler();
}
}, [filterValue, isFiltering, filteredFiles, getIssuesCount]);
const getFilesHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getIssues();
afterFetch.current = true;
isFetching.current = false;
setIsLoading(false);
}, [getIssues]);
let issues = globalVideos;
if (isFiltering) {
issues = filteredFiles;
isFilterMode.current = true;
} else {
isFilterMode.current = false;
}
useEffect(() => {
if (
!firstFetch.current &&
!isFilterMode.current &&
globalVideos.length === 0
) {
isFetching.current = true;
getFilesHandlerMount();
} else {
firstFetch.current = true;
afterFetch.current = true;
}
}, [getFilesHandlerMount, globalVideos]);
const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch("");
setFilterName("");
categoryListRef.current?.clearCategories();
categorySelectRef.current?.clearCategory();
autocompleteRef.current?.setSelectedValue(null);
ReactDOM.flushSync(() => {
getIssuesHandler(true, true);
});
};
return (
<Grid container sx={{ width: "100%" }}>
<FiltersCol item xs={12} md={2} sm={3}>
<FiltersContainer>
<StatsData />
<Input
id="standard-adornment-name"
onChange={e => {
setFilterSearch(e.target.value);
}}
onKeyDown={searchOnEnter}
value={filterSearch}
placeholder="Search"
sx={{
color: theme.palette.text.primary,
borderBottom: `1px solid ${theme.palette.text.primary}`,
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "20px",
}}
/>
<Input
id="standard-adornment-name"
onChange={e => {
setFilterName(e.target.value);
}}
onKeyDown={searchOnEnter}
value={filterName}
placeholder="User's Name (Exact)"
sx={{
marginTop: "20px",
borderBottom: `1px solid ${theme.palette.text.primary}`,
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "20px",
}}
/>
{showCategoryList && (
<CategoryList
categoryData={allCategoryData}
ref={categoryListRef}
afterChange={value => {
setShowCategorySelect(!value[0]);
}}
/>
)}
{showCategorySelect && (
<CategorySelect
categoryData={allCategories}
ref={categorySelectRef}
sx={{ marginTop: "20px" }}
afterChange={value => {
setShowCategoryList(!value);
}}
/>
)}
{QappNamesParam.length > 0 && (
<AutocompleteQappNames
ref={autocompleteRef}
namesList={QappNamesParam}
sx={{ marginTop: "20px" }}
required={false}
afterChange={() => {
const currentSelectedCategories =
categoryListRef?.current?.getSelectedCategories();
categoryListRef?.current?.setSelectedCategories([
"3",
currentSelectedCategories[1],
currentSelectedCategories[2],
]);
}}
/>
)}
<ThemeButton
onClick={() => {
filtersToDefault();
}}
sx={{
marginTop: "20px",
fontWeight: 1000,
}}
variant="contained"
>
reset
</ThemeButton>
<ThemeButton
onClick={() => {
getIssuesHandler(true);
}}
sx={{
marginTop: "20px",
fontWeight: 1000,
}}
variant="contained"
>
Search
</ThemeButton>
</FiltersContainer>
</FiltersCol>
<Grid item xs={12} md={10} sm={9}>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}}
>
<SubtitleContainer
sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%",
maxWidth: "1400px",
}}
></SubtitleContainer>
<IssueList issues={issues} />
<LazyLoad
onLoadMore={getIssuesHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</Grid>
</Grid>
);
};

View File

@@ -0,0 +1,244 @@
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
import React, { useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import {
getIconsFromObject,
getnamesFromObject,
} from "../../constants/Categories/CategoryFunctions.ts";
import { fontSizeExLarge } from "../../constants/Misc.ts";
import {
blockUser,
Issue,
setEditFile,
} from "../../state/features/fileSlice.ts";
import { RootState } from "../../state/store.ts";
import { BountyData } from "../../utils/qortalRequests.ts";
import { formatDate } from "../../utils/time.ts";
import {
BlockIconContainer,
IconsBox,
IssueCard,
IssueContainer,
NameAndDateContainer,
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from "./IssueList-styles.tsx";
interface FileListProps {
issues: Issue[];
}
export const IssueList = ({ issues }: FileListProps) => {
const hashMapIssues = useSelector(
(state: RootState) => state.file.hashMapFiles
);
const theme = useTheme();
const [showIcons, setShowIcons] = useState(null);
const username = useSelector((state: RootState) => state.auth?.user?.name);
const dispatch = useDispatch();
const navigate = useNavigate();
const blockUserFunc = async (user: string) => {
if (user === "Q-Support") return;
try {
const response = await qortalRequest({
action: "ADD_LIST_ITEMS",
list_name: "blockedNames",
items: [user],
});
if (response === true) {
dispatch(blockUser(user));
}
} catch (error) {
console.log(error);
}
};
const filteredIssues = useMemo(() => {
return issues.filter((issue: any) => hashMapIssues[issue.id]?.isValid);
}, [issues, hashMapIssues]);
return (
<IssueContainer>
{filteredIssues.map((issue: any, index: number) => {
const existingFile = hashMapIssues[issue?.id];
let hasHash = false;
let issueObj = issue;
if (existingFile) {
issueObj = existingFile;
hasHash = true;
}
const bountyData: BountyData = {
...issueObj.bountyData,
...issue.bountyData,
};
return (
<Box
sx={{
display: "flex",
alignItems: "center",
width: "100%",
height: "75px",
position: "relative",
}}
key={issueObj.id}
onMouseEnter={() => setShowIcons(issueObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
{hasHash ? (
<>
<IconsBox
sx={{
opacity: showIcons === issueObj.id ? 1 : 0,
zIndex: 2,
}}
>
{issueObj?.user === username && (
<BlockIconContainer
onClick={() => {
dispatch(setEditFile(issueObj));
}}
>
<EditIcon />
Edit Issue
</BlockIconContainer>
)}
{issueObj?.user !== username && (
<BlockIconContainer
onClick={() => {
blockUserFunc(issueObj?.user);
}}
>
<BlockIcon />
Block User
</BlockIconContainer>
)}
</IconsBox>
<IssueCard
onClick={() => {
navigate(`/issue/${issueObj?.user}/${issueObj?.id}`);
}}
sx={{
height: "100%",
width: "100%",
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
width: "280px",
}}
>
<IssueIcons
issueData={issueObj}
style={{ marginRight: "20px" }}
showBackupIcon={true}
/>
</div>
<Box
sx={{
display: "flex",
justifyContent: "left",
alignItems: "center",
width: "250px",
fontSize: fontSizeExLarge,
fontFamily: "Cairo",
letterSpacing: "0.4px",
color: theme.palette.text.primary,
userSelect: "none",
}}
>
<BountyDisplay
bountyData={bountyData}
divStyle={{ marginLeft: "20px" }}
/>
</Box>
<VideoCardTitle sx={{ fontWeight: "bold", width: "400px" }}>
{issueObj.title}
</VideoCardTitle>
</Box>
{issue?.feeData?.isPaid && (
<IssueIcon
iconSrc={QORTicon}
style={{ marginRight: "20px" }}
/>
)}
<NameAndDateContainer
sx={{ width: "200px", height: "100%" }}
onClick={e => {
e.stopPropagation();
navigate(`/channel/${issueObj?.user}`);
}}
>
<div
style={{
display: "flex",
width: "200px",
}}
>
<Avatar
sx={{ height: 24, width: 24, marginRight: "10px" }}
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
alt={`${issueObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{issueObj?.user}
</VideoCardName>
</div>
{issueObj?.created && (
<VideoUploadDate>
{formatDate(issueObj.created)}
</VideoUploadDate>
)}
</NameAndDateContainer>
</IssueCard>
</>
) : (
<Skeleton
variant="rectangular"
style={{
width: "100%",
height: "100%",
paddingBottom: "10px",
objectFit: "contain",
visibility: "visible",
borderRadius: "8px",
}}
/>
)}
</Box>
);
})}
</IssueContainer>
);
};